@samitouri / QOS-React-2 / commits / deeeaf1d22

Entangle overlapping transitions per queue (#20670)

When multiple transitions update the same queue, only the most recent one should be allowed to finish. We shouldn't show intermediate states. See #17418 for background on why this is important. The way this currently works is that we always assign the same lane to all transitions. It's impossible for one transition to finish without also finishing all the others. The downside of the current approach is that it's too aggressive. Not all transitions are related to each other, so one should not block the other. The new approach is to only entangle transitions if they update one or more of the same state hooks (or class components), because this indicates that they are related. If they are unrelated, then they can finish in any order, as long as they have different lanes. However, this commit does not change anything about how the lanes are assigned. All it does is add the mechanism to entangle per queue. So it doesn't actually change any behavior, yet. But it's a requirement for my next step, which is to assign different lanes to consecutive transitions until we run out and cycle back to the beginning.

Andrew Clark committed Jan 27, 2021 at 13:55 UTC deeeaf1d222ba29bcce6eda0a1c4423ce10e727a
10 files changed +216 -22
packages/react-reconciler/src/ReactFiberClassComponent.new.js
+13 -3
@@ -40,6 +40,7 @@ import {
40
41 import {
42 enqueueUpdate,
43 + entangleTransitions,
44 processUpdateQueue,
45 checkHasForceUpdateAfterProcessing,
46 resetHasForceUpdateBeforeProcessing,
@@ -214,7 +215,10 @@ const classComponentUpdater = {
215 }
216
217 enqueueUpdate(fiber, update, lane);
217 - scheduleUpdateOnFiber(fiber, lane, eventTime);
218 + const root = scheduleUpdateOnFiber(fiber, lane, eventTime);
219 + if (root !== null) {
220 + entangleTransitions(root, fiber, lane);
221 + }
222
223 if (__DEV__) {
224 if (enableDebugTracing) {
@@ -246,7 +250,10 @@ const classComponentUpdater = {
250 }
251
252 enqueueUpdate(fiber, update, lane);
249 - scheduleUpdateOnFiber(fiber, lane, eventTime);
253 + const root = scheduleUpdateOnFiber(fiber, lane, eventTime);
254 + if (root !== null) {
255 + entangleTransitions(root, fiber, lane);
256 + }
257
258 if (__DEV__) {
259 if (enableDebugTracing) {
@@ -277,7 +284,10 @@ const classComponentUpdater = {
284 }
285
286 enqueueUpdate(fiber, update, lane);
280 - scheduleUpdateOnFiber(fiber, lane, eventTime);
287 + const root = scheduleUpdateOnFiber(fiber, lane, eventTime);
288 + if (root !== null) {
289 + entangleTransitions(root, fiber, lane);
290 + }
291
292 if (__DEV__) {
293 if (enableDebugTracing) {
packages/react-reconciler/src/ReactFiberClassComponent.old.js
+13 -3
@@ -40,6 +40,7 @@ import {
40
41 import {
42 enqueueUpdate,
43 + entangleTransitions,
44 processUpdateQueue,
45 checkHasForceUpdateAfterProcessing,
46 resetHasForceUpdateBeforeProcessing,
@@ -214,7 +215,10 @@ const classComponentUpdater = {
215 }
216
217 enqueueUpdate(fiber, update);
217 - scheduleUpdateOnFiber(fiber, lane, eventTime);
218 + const root = scheduleUpdateOnFiber(fiber, lane, eventTime);
219 + if (root !== null) {
220 + entangleTransitions(root, fiber, lane);
221 + }
222
223 if (__DEV__) {
224 if (enableDebugTracing) {
@@ -246,7 +250,10 @@ const classComponentUpdater = {
250 }
251
252 enqueueUpdate(fiber, update);
249 - scheduleUpdateOnFiber(fiber, lane, eventTime);
253 + const root = scheduleUpdateOnFiber(fiber, lane, eventTime);
254 + if (root !== null) {
255 + entangleTransitions(root, fiber, lane);
256 + }
257
258 if (__DEV__) {
259 if (enableDebugTracing) {
@@ -277,7 +284,10 @@ const classComponentUpdater = {
284 }
285
286 enqueueUpdate(fiber, update);
280 - scheduleUpdateOnFiber(fiber, lane, eventTime);
287 + const root = scheduleUpdateOnFiber(fiber, lane, eventTime);
288 + if (root !== null) {
289 + entangleTransitions(root, fiber, lane);
290 + }
291
292 if (__DEV__) {
293 if (enableDebugTracing) {
packages/react-reconciler/src/ReactFiberHooks.new.js
+37 -2
@@ -45,6 +45,8 @@ import {
45 isSubsetOfLanes,
46 mergeLanes,
47 removeLanes,
48 + intersectLanes,
49 + isTransitionLane,
50 markRootEntangled,
51 markRootMutableRead,
52 getCurrentUpdateLanePriority,
@@ -104,7 +106,11 @@ import {getIsRendering} from './ReactCurrentFiber';
106 import {logStateUpdateScheduled} from './DebugTracing';
107 import {markStateUpdateScheduled} from './SchedulingProfiler';
108 import {CacheContext} from './ReactFiberCacheComponent.new';
107 -import {createUpdate, enqueueUpdate} from './ReactUpdateQueue.new';
109 +import {
110 + createUpdate,
111 + enqueueUpdate,
112 + entangleTransitions,
113 +} from './ReactUpdateQueue.new';
114 import {pushInterleavedQueue} from './ReactFiberInterleavedUpdates.new';
115
116 const {ReactCurrentDispatcher, ReactCurrentBatchConfig} = ReactSharedInternals;
@@ -121,6 +127,7 @@ type Update<S, A> = {|
127 export type UpdateQueue<S, A> = {|
128 pending: Update<S, A> | null,
129 interleaved: Update<S, A> | null,
130 + lanes: Lanes,
131 dispatch: (A => mixed) | null,
132 lastRenderedReducer: ((S, A) => S) | null,
133 lastRenderedState: S | null,
@@ -654,6 +661,7 @@ function mountReducer<S, I, A>(
661 const queue = (hook.queue = {
662 pending: null,
663 interleaved: null,
664 + lanes: NoLanes,
665 dispatch: null,
666 lastRenderedReducer: reducer,
667 lastRenderedState: (initialState: any),
@@ -811,6 +819,10 @@ function updateReducer<S, I, A>(
819 markSkippedUpdateLanes(interleavedLane);
820 interleaved = ((interleaved: any).next: Update<S, A>);
821 } while (interleaved !== lastInterleaved);
822 + } else if (baseQueue === null) {
823 + // `queue.lanes` is used for entangling transitions. We can set it back to
824 + // zero once the queue is empty.
825 + queue.lanes = NoLanes;
826 }
827
828 const dispatch: Dispatch<A> = (queue.dispatch: any);
@@ -1102,6 +1114,7 @@ function useMutableSource<Source, Snapshot>(
1114 const newQueue = {
1115 pending: null,
1116 interleaved: null,
1117 + lanes: NoLanes,
1118 dispatch: null,
1119 lastRenderedReducer: basicStateReducer,
1120 lastRenderedState: snapshot,
@@ -1158,6 +1171,7 @@ function mountState<S>(
1171 const queue = (hook.queue = {
1172 pending: null,
1173 interleaved: null,
1174 + lanes: NoLanes,
1175 dispatch: null,
1176 lastRenderedReducer: basicStateReducer,
1177 lastRenderedState: (initialState: any),
@@ -1821,6 +1835,9 @@ function refreshCache<T>(fiber: Fiber, seedKey: ?() => T, seedValue: T) {
1835 const lane = requestUpdateLane(provider);
1836 const eventTime = requestEventTime();
1837 const root = scheduleUpdateOnFiber(provider, lane, eventTime);
1838 + if (root !== null) {
1839 + entangleTransitions(root, fiber, lane);
1840 + }
1841
1842 const seededCache = new Map();
1843 if (seedKey !== null && seedKey !== undefined && root !== null) {
@@ -1960,7 +1977,25 @@ function dispatchAction<S, A>(
1977 warnIfNotCurrentlyActingUpdatesInDev(fiber);
1978 }
1979 }
1963 - scheduleUpdateOnFiber(fiber, lane, eventTime);
1980 + const root = scheduleUpdateOnFiber(fiber, lane, eventTime);
1981 +
1982 + if (isTransitionLane(lane) && root !== null) {
1983 + let queueLanes = queue.lanes;
1984 +
1985 + // If any entangled lanes are no longer pending on the root, then they
1986 + // must have finished. We can remove them from the shared queue, which
1987 + // represents a superset of the actually pending lanes. In some cases we
1988 + // may entangle more than we need to, but that's OK. In fact it's worse if
1989 + // we *don't* entangle when we should.
1990 + queueLanes = intersectLanes(queueLanes, root.pendingLanes);
1991 +
1992 + // Entangle the new transition lane with the other transition lanes.
1993 + const newQueueLanes = mergeLanes(queueLanes, lane);
1994 + if (newQueueLanes !== queueLanes) {
1995 + queue.lanes = newQueueLanes;
1996 + markRootEntangled(root, newQueueLanes);
1997 + }
1998 + }
1999 }
2000
2001 if (__DEV__) {
packages/react-reconciler/src/ReactFiberHooks.old.js
+39 -2
@@ -45,6 +45,8 @@ import {
45 isSubsetOfLanes,
46 mergeLanes,
47 removeLanes,
48 + intersectLanes,
49 + isTransitionLane,
50 markRootEntangled,
51 markRootMutableRead,
52 getCurrentUpdateLanePriority,
@@ -103,7 +105,11 @@ import {getIsRendering} from './ReactCurrentFiber';
105 import {logStateUpdateScheduled} from './DebugTracing';
106 import {markStateUpdateScheduled} from './SchedulingProfiler';
107 import {CacheContext} from './ReactFiberCacheComponent.old';
106 -import {createUpdate, enqueueUpdate} from './ReactUpdateQueue.old';
108 +import {
109 + createUpdate,
110 + enqueueUpdate,
111 + entangleTransitions,
112 +} from './ReactUpdateQueue.old';
113
114 const {ReactCurrentDispatcher, ReactCurrentBatchConfig} = ReactSharedInternals;
115
@@ -118,6 +124,7 @@ type Update<S, A> = {|
124
125 type UpdateQueue<S, A> = {|
126 pending: Update<S, A> | null,
127 + lanes: Lanes,
128 dispatch: (A => mixed) | null,
129 lastRenderedReducer: ((S, A) => S) | null,
130 lastRenderedState: S | null,
@@ -650,6 +657,7 @@ function mountReducer<S, I, A>(
657 hook.memoizedState = hook.baseState = initialState;
658 const queue = (hook.queue = {
659 pending: null,
660 + lanes: NoLanes,
661 dispatch: null,
662 lastRenderedReducer: reducer,
663 lastRenderedState: (initialState: any),
@@ -792,6 +800,12 @@ function updateReducer<S, I, A>(
800 queue.lastRenderedState = newState;
801 }
802
803 + if (baseQueue === null) {
804 + // `queue.lanes` is used for entangling transitions. We can set it back to
805 + // zero once the queue is empty.
806 + queue.lanes = NoLanes;
807 + }
808 +
809 const dispatch: Dispatch<A> = (queue.dispatch: any);
810 return [hook.memoizedState, dispatch];
811 }
@@ -1080,6 +1094,7 @@ function useMutableSource<Source, Snapshot>(
1094 // including any interleaving updates that occur.
1095 const newQueue = {
1096 pending: null,
1097 + lanes: NoLanes,
1098 dispatch: null,
1099 lastRenderedReducer: basicStateReducer,
1100 lastRenderedState: snapshot,
@@ -1135,6 +1150,7 @@ function mountState<S>(
1150 hook.memoizedState = hook.baseState = initialState;
1151 const queue = (hook.queue = {
1152 pending: null,
1153 + lanes: NoLanes,
1154 dispatch: null,
1155 lastRenderedReducer: basicStateReducer,
1156 lastRenderedState: (initialState: any),
@@ -1798,6 +1814,9 @@ function refreshCache<T>(fiber: Fiber, seedKey: ?() => T, seedValue: T) {
1814 const lane = requestUpdateLane(provider);
1815 const eventTime = requestEventTime();
1816 const root = scheduleUpdateOnFiber(provider, lane, eventTime);
1817 + if (root !== null) {
1818 + entangleTransitions(root, fiber, lane);
1819 + }
1820
1821 const seededCache = new Map();
1822 if (seedKey !== null && seedKey !== undefined && root !== null) {
@@ -1914,7 +1933,25 @@ function dispatchAction<S, A>(
1933 warnIfNotCurrentlyActingUpdatesInDev(fiber);
1934 }
1935 }
1917 - scheduleUpdateOnFiber(fiber, lane, eventTime);
1936 + const root = scheduleUpdateOnFiber(fiber, lane, eventTime);
1937 +
1938 + if (isTransitionLane(lane) && root !== null) {
1939 + let queueLanes = queue.lanes;
1940 +
1941 + // If any entangled lanes are no longer pending on the root, then they
1942 + // must have finished. We can remove them from the shared queue, which
1943 + // represents a superset of the actually pending lanes. In some cases we
1944 + // may entangle more than we need to, but that's OK. In fact it's worse if
1945 + // we *don't* entangle when we should.
1946 + queueLanes = intersectLanes(queueLanes, root.pendingLanes);
1947 +
1948 + // Entangle the new transition lane with the other transition lanes.
1949 + const newQueueLanes = mergeLanes(queueLanes, lane);
1950 + if (newQueueLanes !== queueLanes) {
1951 + queue.lanes = newQueueLanes;
1952 + markRootEntangled(root, newQueueLanes);
1953 + }
1954 + }
1955 }
1956
1957 if (__DEV__) {
packages/react-reconciler/src/ReactFiberLane.new.js
+10 -3
@@ -310,9 +310,8 @@ export function getNextLanes(root: FiberRoot, wipLanes: Lanes): Lanes {
310 }
311
312 if (enableTransitionEntanglement) {
313 - // We don't need to include higher priority lanes, because in this
314 - // experiment we always unsuspend all transitions whenever we receive
315 - // an update.
313 + // We don't need to do anything extra here, because we apply per-lane
314 + // transition entanglement in the entanglement loop below.
315 } else {
316 // If there are higher priority lanes, we'll include them even if they
317 // are suspended.
@@ -492,6 +491,10 @@ export function includesOnlyTransitions(lanes: Lanes) {
491 return (lanes & TransitionLanes) === lanes;
492 }
493
494 +export function isTransitionLane(lane: Lane) {
495 + return (lane & TransitionLanes) !== 0;
496 +}
497 +
498 // To ensure consistency across multiple updates in the same event, this should
499 // be a pure function, so that it always returns the same lane for given inputs.
500 export function findUpdateLane(
@@ -634,6 +637,10 @@ export function removeLanes(set: Lanes, subset: Lanes | Lane): Lanes {
637 return set & ~subset;
638 }
639
640 +export function intersectLanes(a: Lanes | Lane, b: Lanes | Lane): Lanes {
641 + return a & b;
642 +}
643 +
644 // Seems redundant, but it changes the type from a single lane (used for
645 // updates) to a group of lanes (used for flushing work).
646 export function laneToLanes(lane: Lane): Lanes {
packages/react-reconciler/src/ReactFiberLane.old.js
+10 -3
@@ -310,9 +310,8 @@ export function getNextLanes(root: FiberRoot, wipLanes: Lanes): Lanes {
310 }
311
312 if (enableTransitionEntanglement) {
313 - // We don't need to include higher priority lanes, because in this
314 - // experiment we always unsuspend all transitions whenever we receive
315 - // an update.
313 + // We don't need to do anything extra here, because we apply per-lane
314 + // transition entanglement in the entanglement loop below.
315 } else {
316 // If there are higher priority lanes, we'll include them even if they
317 // are suspended.
@@ -492,6 +491,10 @@ export function includesOnlyTransitions(lanes: Lanes) {
491 return (lanes & TransitionLanes) === lanes;
492 }
493
494 +export function isTransitionLane(lane: Lane) {
495 + return (lane & TransitionLanes) !== 0;
496 +}
497 +
498 // To ensure consistency across multiple updates in the same event, this should
499 // be a pure function, so that it always returns the same lane for given inputs.
500 export function findUpdateLane(
@@ -634,6 +637,10 @@ export function removeLanes(set: Lanes, subset: Lanes | Lane): Lanes {
637 return set & ~subset;
638 }
639
640 +export function intersectLanes(a: Lanes | Lane, b: Lanes | Lane): Lanes {
641 + return a & b;
642 +}
643 +
644 // Seems redundant, but it changes the type from a single lane (used for
645 // updates) to a group of lanes (used for flushing work).
646 export function laneToLanes(lane: Lane): Lanes {
packages/react-reconciler/src/ReactFiberReconciler.new.js
+9 -2
@@ -65,7 +65,11 @@ import {
65 IsThisRendererActing,
66 act,
67 } from './ReactFiberWorkLoop.new';
68 -import {createUpdate, enqueueUpdate} from './ReactUpdateQueue.new';
68 +import {
69 + createUpdate,
70 + enqueueUpdate,
71 + entangleTransitions,
72 +} from './ReactUpdateQueue.new';
73 import {
74 isRendering as ReactCurrentFiberIsRendering,
75 current as ReactCurrentFiberCurrent,
@@ -315,7 +319,10 @@ export function updateContainer(
319 }
320
321 enqueueUpdate(current, update, lane);
318 - scheduleUpdateOnFiber(current, lane, eventTime);
322 + const root = scheduleUpdateOnFiber(current, lane, eventTime);
323 + if (root !== null) {
324 + entangleTransitions(root, current, lane);
325 + }
326
327 return lane;
328 }
packages/react-reconciler/src/ReactFiberReconciler.old.js
+9 -2
@@ -65,7 +65,11 @@ import {
65 IsThisRendererActing,
66 act,
67 } from './ReactFiberWorkLoop.old';
68 -import {createUpdate, enqueueUpdate} from './ReactUpdateQueue.old';
68 +import {
69 + createUpdate,
70 + enqueueUpdate,
71 + entangleTransitions,
72 +} from './ReactUpdateQueue.old';
73 import {
74 isRendering as ReactCurrentFiberIsRendering,
75 current as ReactCurrentFiberCurrent,
@@ -315,7 +319,10 @@ export function updateContainer(
319 }
320
321 enqueueUpdate(current, update);
318 - scheduleUpdateOnFiber(current, lane, eventTime);
322 + const root = scheduleUpdateOnFiber(current, lane, eventTime);
323 + if (root !== null) {
324 + entangleTransitions(root, current, lane);
325 + }
326
327 return lane;
328 }
packages/react-reconciler/src/ReactUpdateQueue.new.js
+37 -1
@@ -84,7 +84,7 @@
84 // regardless of priority. Intermediate state may vary according to system
85 // resources, but the final state is always the same.
86
87 -import type {Fiber} from './ReactInternalTypes';
87 +import type {Fiber, FiberRoot} from './ReactInternalTypes';
88 import type {Lanes, Lane} from './ReactFiberLane.new';
89
90 import {
@@ -92,6 +92,9 @@ import {
92 NoLanes,
93 isSubsetOfLanes,
94 mergeLanes,
95 + isTransitionLane,
96 + intersectLanes,
97 + markRootEntangled,
98 } from './ReactFiberLane.new';
99 import {
100 enterDisallowedContextReadInDEV,
@@ -128,6 +131,7 @@ export type Update<State> = {|
131 export type SharedQueue<State> = {|
132 pending: Update<State> | null,
133 interleaved: Update<State> | null,
134 + lanes: Lanes,
135 |};
136
137 export type UpdateQueue<State> = {|
@@ -167,6 +171,7 @@ export function initializeUpdateQueue<State>(fiber: Fiber): void {
171 shared: {
172 pending: null,
173 interleaved: null,
174 + lanes: NoLanes,
175 },
176 effects: null,
177 };
@@ -260,6 +265,33 @@ export function enqueueUpdate<State>(
265 }
266 }
267
268 +export function entangleTransitions(root: FiberRoot, fiber: Fiber, lane: Lane) {
269 + const updateQueue = fiber.updateQueue;
270 + if (updateQueue === null) {
271 + // Only occurs if the fiber has been unmounted.
272 + return;
273 + }
274 +
275 + const sharedQueue: SharedQueue<mixed> = (updateQueue: any).shared;
276 + if (isTransitionLane(lane)) {
277 + let queueLanes = sharedQueue.lanes;
278 +
279 + // If any entangled lanes are no longer pending on the root, then they must
280 + // have finished. We can remove them from the shared queue, which represents
281 + // a superset of the actually pending lanes. In some cases we may entangle
282 + // more than we need to, but that's OK. In fact it's worse if we *don't*
283 + // entangle when we should.
284 + queueLanes = intersectLanes(queueLanes, root.pendingLanes);
285 +
286 + // Entangle the new transition lane with the other transition lanes.
287 + const newQueueLanes = mergeLanes(queueLanes, lane);
288 + if (newQueueLanes !== queueLanes) {
289 + sharedQueue.lanes = newQueueLanes;
290 + markRootEntangled(root, newQueueLanes);
291 + }
292 + }
293 +}
294 +
295 export function enqueueCapturedUpdate<State>(
296 workInProgress: Fiber,
297 capturedUpdate: Update<State>,
@@ -595,6 +627,10 @@ export function processUpdateQueue<State>(
627 newLanes = mergeLanes(newLanes, interleaved.lane);
628 interleaved = ((interleaved: any).next: Update<State>);
629 } while (interleaved !== lastInterleaved);
630 + } else if (firstBaseUpdate === null) {
631 + // `queue.lanes` is used for entangling transitions. We can set it back to
632 + // zero once the queue is empty.
633 + queue.shared.lanes = NoLanes;
634 }
635
636 // Set the remaining expiration time to be whatever is remaining in the queue.
packages/react-reconciler/src/ReactUpdateQueue.old.js
+39 -1
@@ -84,7 +84,7 @@
84 // regardless of priority. Intermediate state may vary according to system
85 // resources, but the final state is always the same.
86
87 -import type {Fiber} from './ReactInternalTypes';
87 +import type {Fiber, FiberRoot} from './ReactInternalTypes';
88 import type {Lanes, Lane} from './ReactFiberLane.old';
89
90 import {
@@ -92,6 +92,9 @@ import {
92 NoLanes,
93 isSubsetOfLanes,
94 mergeLanes,
95 + isTransitionLane,
96 + intersectLanes,
97 + markRootEntangled,
98 } from './ReactFiberLane.old';
99 import {
100 enterDisallowedContextReadInDEV,
@@ -123,6 +126,7 @@ export type Update<State> = {|
126
127 type SharedQueue<State> = {|
128 pending: Update<State> | null,
129 + lanes: Lanes,
130 |};
131
132 export type UpdateQueue<State> = {|
@@ -161,6 +165,7 @@ export function initializeUpdateQueue<State>(fiber: Fiber): void {
165 lastBaseUpdate: null,
166 shared: {
167 pending: null,
168 + lanes: NoLanes,
169 },
170 effects: null,
171 };
@@ -234,6 +239,33 @@ export function enqueueUpdate<State>(fiber: Fiber, update: Update<State>) {
239 }
240 }
241
242 +export function entangleTransitions(root: FiberRoot, fiber: Fiber, lane: Lane) {
243 + const updateQueue = fiber.updateQueue;
244 + if (updateQueue === null) {
245 + // Only occurs if the fiber has been unmounted.
246 + return;
247 + }
248 +
249 + const sharedQueue: SharedQueue<mixed> = (updateQueue: any).shared;
250 + if (isTransitionLane(lane)) {
251 + let queueLanes = sharedQueue.lanes;
252 +
253 + // If any entangled lanes are no longer pending on the root, then they must
254 + // have finished. We can remove them from the shared queue, which represents
255 + // a superset of the actually pending lanes. In some cases we may entangle
256 + // more than we need to, but that's OK. In fact it's worse if we *don't*
257 + // entangle when we should.
258 + queueLanes = intersectLanes(queueLanes, root.pendingLanes);
259 +
260 + // Entangle the new transition lane with the other transition lanes.
261 + const newQueueLanes = mergeLanes(queueLanes, lane);
262 + if (newQueueLanes !== queueLanes) {
263 + sharedQueue.lanes = newQueueLanes;
264 + markRootEntangled(root, newQueueLanes);
265 + }
266 + }
267 +}
268 +
269 export function enqueueCapturedUpdate<State>(
270 workInProgress: Fiber,
271 capturedUpdate: Update<State>,
@@ -559,6 +591,12 @@ export function processUpdateQueue<State>(
591 queue.firstBaseUpdate = newFirstBaseUpdate;
592 queue.lastBaseUpdate = newLastBaseUpdate;
593
594 + if (firstBaseUpdate === null) {
595 + // `queue.lanes` is used for entangling transitions. We can set it back to
596 + // zero once the queue is empty.
597 + queue.shared.lanes = NoLanes;
598 + }
599 +
600 // Set the remaining expiration time to be whatever is remaining in the queue.
601 // This should be fine because the only two other things that contribute to
602 // expiration time are props and context. We're already in the middle of the