@samitouri / QOS-React-2 / commits / 3019210df2

Expiration times (#10426)

* [Work-in-progress] Assign expiration times to updates An expiration time represents a time in the future by which an update should flush. The priority of the update is related to the difference between the current clock time and the expiration time. This has the effect of increasing the priority of updates as time progresses, to prevent starvation. This lays the initial groundwork for expiration times without changing any behavior. Future commits will replace work priority with expiration times. * Replace pendingWorkPriority with expiration times Instead of a priority, a fiber has an expiration time that represents a point in the future by which it should render. Pending updates still have priorities so that they can be coalesced. We use a host config method to read the current time. This commit implements everything except that method, which currently returns a constant value. So this just proves that expiration times work the same as priorities when time is frozen. Subsequent commits will show the effect of advancing time. * Triangle Demo should use a class shouldComponentUpdate was removed from functional components. Running the demo shows, now that expiration is enabled, the demo does not starve. (Still won't run smoothly until we add back the ability to resume interrupted work.) * Use a magic value for task expiration time There are a few cases related to sync mode where we need to distinguish between work that is scheduled as task and work that is treated like task because it expires. For example, batchedUpdates. We don't want to perform any work until the end of the batch, regardless of how much time has elapsed. * Use current time to calculate expiration time * Add unit tests for expiration and coalescing * Delete unnecessary abstraction * Move performance.now polyfill to ReactDOMFrameScheduling * Add expiration to fuzz tester * Expiration nits - Rename Done -> NoWork - Use max int32 instead of max safe int - Use bitwise operations instead of Math functions

Andrew Clark committed Oct 9, 2017 at 18:39 UTC 3019210df2b486416ed94d7b9becffaf254e81c4
20 files changed +965 -393
fixtures/fiber-triangle/index.html
+42 -39
@@ -76,51 +76,54 @@
76 }
77 }
78
79 - function SierpinskiTriangle({ x, y, s, children }) {
80 - if (s <= targetSize) {
81 - return (
82 - <Dot
83 - x={x - (targetSize / 2)}
84 - y={y - (targetSize / 2)}
85 - size={targetSize}
86 - text={children}
87 - />
79 + class SierpinskiTriangle extends React.Component {
80 + shouldComponentUpdate(nextProps) {
81 + var o = this.props;
82 + var n = nextProps;
83 + return !(
84 + o.x === n.x &&
85 + o.y === n.y &&
86 + o.s === n.s &&
87 + o.children === n.children
88 );
89 - return r;
89 }
91 - var newSize = s / 2;
92 - var slowDown = true;
93 - if (slowDown) {
94 - var e = performance.now() + 0.8;
95 - while (performance.now() < e) {
96 - // Artificially long execution time.
90 + render() {
91 + let {x, y, s, children} = this.props;
92 + if (s <= targetSize) {
93 + return (
94 + <Dot
95 + x={x - (targetSize / 2)}
96 + y={y - (targetSize / 2)}
97 + size={targetSize}
98 + text={children}
99 + />
100 + );
101 + return r;
102 + }
103 + var newSize = s / 2;
104 + var slowDown = true;
105 + if (slowDown) {
106 + var e = performance.now() + 0.8;
107 + while (performance.now() < e) {
108 + // Artificially long execution time.
109 + }
110 }
98 - }
111
100 - s /= 2;
112 + s /= 2;
113
102 - return [
103 - <SierpinskiTriangle x={x} y={y - (s / 2)} s={s}>
104 - {children}
105 - </SierpinskiTriangle>,
106 - <SierpinskiTriangle x={x - s} y={y + (s / 2)} s={s}>
107 - {children}
108 - </SierpinskiTriangle>,
109 - <SierpinskiTriangle x={x + s} y={y + (s / 2)} s={s}>
110 - {children}
111 - </SierpinskiTriangle>,
112 - ];
114 + return [
115 + <SierpinskiTriangle x={x} y={y - (s / 2)} s={s}>
116 + {children}
117 + </SierpinskiTriangle>,
118 + <SierpinskiTriangle x={x - s} y={y + (s / 2)} s={s}>
119 + {children}
120 + </SierpinskiTriangle>,
121 + <SierpinskiTriangle x={x + s} y={y + (s / 2)} s={s}>
122 + {children}
123 + </SierpinskiTriangle>,
124 + ];
125 + }
126 }
114 - SierpinskiTriangle.shouldComponentUpdate = function(oldProps, newProps) {
115 - var o = oldProps;
116 - var n = newProps;
117 - return !(
118 - o.x === n.x &&
119 - o.y === n.y &&
120 - o.s === n.s &&
121 - o.children === n.children
122 - );
123 - };
127
128 class ExampleApplication extends React.Component {
129 constructor() {
src/renderers/art/ReactARTFiberEntry.js
+2
@@ -532,6 +532,8 @@ const ARTRenderer = ReactFiberReconciler({
532 );
533 },
534
535 + now: ReactDOMFrameScheduling.now,
536 +
537 useSyncScheduling: true,
538 });
539
src/renderers/dom/fiber/ReactDOMFiberEntry.js
+2
@@ -437,6 +437,8 @@ var DOMRenderer = ReactFiberReconciler({
437 }
438 },
439
440 + now: ReactDOMFrameScheduling.now,
441 +
442 canHydrateInstance(
443 instance: Instance | TextInstance,
444 type: string,
src/renderers/native-rt/ReactNativeRTFiberRenderer.js
+5
@@ -227,6 +227,11 @@ const NativeRTRenderer = ReactFiberReconciler({
227 },
228
229 useSyncScheduling: true,
230 +
231 + now(): number {
232 + // TODO: Enable expiration by implementing this method.
233 + return 0;
234 + },
235 });
236
237 module.exports = NativeRTRenderer;
src/renderers/native/ReactNativeFiberRenderer.js
+5
@@ -377,6 +377,11 @@ const NativeRenderer = ReactFiberReconciler({
377 },
378
379 useSyncScheduling: true,
380 +
381 + now(): number {
382 + // TODO: Enable expiration by implementing this method.
383 + return 0;
384 + },
385 });
386
387 module.exports = NativeRenderer;
src/renderers/noop/ReactNoopEntry.js
+17 -3
@@ -83,6 +83,8 @@ function removeChild(
83 parentInstance.children.splice(index, 1);
84 }
85
86 +let elapsedTimeInMs = 0;
87 +
88 var NoopRenderer = ReactFiberReconciler({
89 getRootHostContext() {
90 if (failInBeginPhase) {
@@ -201,6 +203,10 @@ var NoopRenderer = ReactFiberReconciler({
203 prepareForCommit(): void {},
204
205 resetAfterCommit(): void {},
206 +
207 + now(): number {
208 + return elapsedTimeInMs;
209 + },
210 });
211
212 var rootContainers = new Map();
@@ -336,6 +342,14 @@ var ReactNoop = {
342 expect(actual).toEqual(expected);
343 },
344
345 + expire(ms: number): void {
346 + elapsedTimeInMs += ms;
347 + },
348 +
349 + flushExpired(): Array<mixed> {
350 + return ReactNoop.flushUnitsOfWork(0);
351 + },
352 +
353 yield(value: mixed) {
354 if (yieldedValues === null) {
355 yieldedValues = [value];
@@ -400,7 +414,7 @@ var ReactNoop = {
414 ' '.repeat(depth + 1) + '~',
415 firstUpdate && firstUpdate.partialState,
416 firstUpdate.callback ? 'with callback' : '',
403 - '[' + firstUpdate.priorityLevel + ']',
417 + '[' + firstUpdate.expirationTime + ']',
418 );
419 var next;
420 while ((next = firstUpdate.next)) {
@@ -408,7 +422,7 @@ var ReactNoop = {
422 ' '.repeat(depth + 1) + '~',
423 next.partialState,
424 next.callback ? 'with callback' : '',
411 - '[' + firstUpdate.priorityLevel + ']',
425 + '[' + firstUpdate.expirationTime + ']',
426 );
427 }
428 }
@@ -418,7 +432,7 @@ var ReactNoop = {
432 ' '.repeat(depth) +
433 '- ' +
434 (fiber.type ? fiber.type.name || fiber.type : '[root]'),
421 - '[' + fiber.pendingWorkPriority + (fiber.pendingProps ? '*' : '') + ']',
435 + '[' + fiber.expirationTime + (fiber.pendingProps ? '*' : '') + ']',
436 );
437 if (fiber.updateQueue) {
438 logUpdateQueue(fiber.updateQueue, depth);
src/renderers/shared/ReactDOMFrameScheduling.js
+32 -13
@@ -37,6 +37,20 @@ if (__DEV__) {
37 }
38 }
39
40 +const hasNativePerformanceNow =
41 + typeof performance === 'object' && typeof performance.now === 'function';
42 +
43 +let now;
44 +if (hasNativePerformanceNow) {
45 + now = function() {
46 + return performance.now();
47 + };
48 +} else {
49 + now = function() {
50 + return Date.now();
51 + };
52 +}
53 +
54 // TODO: There's no way to cancel, because Fiber doesn't atm.
55 let rIC: (callback: (deadline: Deadline) => void) => number;
56
@@ -67,19 +81,23 @@ if (!ExecutionEnvironment.canUseDOM) {
81 var previousFrameTime = 33;
82 var activeFrameTime = 33;
83
70 - var frameDeadlineObject = {
71 - timeRemaining: typeof performance === 'object' &&
72 - typeof performance.now === 'function'
73 - ? function() {
74 - // We assume that if we have a performance timer that the rAF callback
75 - // gets a performance timer value. Not sure if this is always true.
76 - return frameDeadline - performance.now();
77 - }
78 - : function() {
79 - // As a fallback we use Date.now.
80 - return frameDeadline - Date.now();
81 - },
82 - };
84 + var frameDeadlineObject;
85 + if (hasNativePerformanceNow) {
86 + frameDeadlineObject = {
87 + timeRemaining() {
88 + // We assume that if we have a performance timer that the rAF callback
89 + // gets a performance timer value. Not sure if this is always true.
90 + return frameDeadline - performance.now();
91 + },
92 + };
93 + } else {
94 + frameDeadlineObject = {
95 + timeRemaining() {
96 + // Fallback to Date.now()
97 + return frameDeadline - Date.now();
98 + },
99 + };
100 + }
101
102 // We use the postMessage trick to defer idle work until after the repaint.
103 var messageKey = '__reactIdleCallback$' + Math.random().toString(36).slice(2);
@@ -153,4 +171,5 @@ if (!ExecutionEnvironment.canUseDOM) {
171 rIC = requestIdleCallback;
172 }
173
174 +exports.now = now;
175 exports.rIC = rIC;
src/renderers/shared/fiber/ReactChildFiber.js
+133 -81
@@ -13,7 +13,7 @@
13 import type {ReactElement} from 'ReactElementType';
14 import type {ReactCoroutine, ReactPortal, ReactYield} from 'ReactTypes';
15 import type {Fiber} from 'ReactFiber';
16 -import type {PriorityLevel} from 'ReactPriorityLevel';
16 +import type {ExpirationTime} from 'ReactFiberExpirationTime';
17
18 var {REACT_COROUTINE_TYPE, REACT_YIELD_TYPE} = require('ReactCoroutine');
19 var {REACT_PORTAL_TYPE} = require('ReactPortal');
@@ -288,19 +288,19 @@ function ChildReconciler(shouldClone, shouldTrackSideEffects) {
288 return existingChildren;
289 }
290
291 - function useFiber(fiber: Fiber, priority: PriorityLevel): Fiber {
291 + function useFiber(fiber: Fiber, expirationTime: ExpirationTime): Fiber {
292 // We currently set sibling to null and index to 0 here because it is easy
293 // to forget to do before returning it. E.g. for the single child case.
294 if (shouldClone) {
295 - const clone = createWorkInProgress(fiber, priority);
295 + const clone = createWorkInProgress(fiber, expirationTime);
296 clone.index = 0;
297 clone.sibling = null;
298 return clone;
299 } else {
300 - // We override the pending priority even if it is higher, because if
301 - // we're reconciling at a lower priority that means that this was
300 + // We override the expiration time even if it is earlier, because if
301 + // we're reconciling at a later time that means that this was
302 // down-prioritized.
303 - fiber.pendingWorkPriority = priority;
303 + fiber.expirationTime = expirationTime;
304 fiber.effectTag = NoEffect;
305 fiber.index = 0;
306 fiber.sibling = null;
@@ -349,20 +349,20 @@ function ChildReconciler(shouldClone, shouldTrackSideEffects) {
349 returnFiber: Fiber,
350 current: Fiber | null,
351 textContent: string,
352 - priority: PriorityLevel,
352 + expirationTime: ExpirationTime,
353 ) {
354 if (current === null || current.tag !== HostText) {
355 // Insert
356 const created = createFiberFromText(
357 textContent,
358 returnFiber.internalContextTag,
359 - priority,
359 + expirationTime,
360 );
361 created.return = returnFiber;
362 return created;
363 } else {
364 // Update
365 - const existing = useFiber(current, priority);
365 + const existing = useFiber(current, expirationTime);
366 existing.pendingProps = textContent;
367 existing.return = returnFiber;
368 return existing;
@@ -373,21 +373,21 @@ function ChildReconciler(shouldClone, shouldTrackSideEffects) {
373 returnFiber: Fiber,
374 current: Fiber | null,
375 element: ReactElement,
376 - priority: PriorityLevel,
376 + expirationTime: ExpirationTime,
377 ): Fiber {
378 if (current === null || current.type !== element.type) {
379 // Insert
380 const created = createFiberFromElement(
381 element,
382 returnFiber.internalContextTag,
383 - priority,
383 + expirationTime,
384 );
385 created.ref = coerceRef(current, element);
386 created.return = returnFiber;
387 return created;
388 } else {
389 // Move based on index
390 - const existing = useFiber(current, priority);
390 + const existing = useFiber(current, expirationTime);
391 existing.ref = coerceRef(current, element);
392 existing.pendingProps = element.props;
393 existing.return = returnFiber;
@@ -403,7 +403,7 @@ function ChildReconciler(shouldClone, shouldTrackSideEffects) {
403 returnFiber: Fiber,
404 current: Fiber | null,
405 coroutine: ReactCoroutine,
406 - priority: PriorityLevel,
406 + expirationTime: ExpirationTime,
407 ): Fiber {
408 // TODO: Should this also compare handler to determine whether to reuse?
409 if (current === null || current.tag !== CoroutineComponent) {
@@ -411,13 +411,13 @@ function ChildReconciler(shouldClone, shouldTrackSideEffects) {
411 const created = createFiberFromCoroutine(
412 coroutine,
413 returnFiber.internalContextTag,
414 - priority,
414 + expirationTime,
415 );
416 created.return = returnFiber;
417 return created;
418 } else {
419 // Move based on index
420 - const existing = useFiber(current, priority);
420 + const existing = useFiber(current, expirationTime);
421 existing.pendingProps = coroutine;
422 existing.return = returnFiber;
423 return existing;
@@ -428,21 +428,21 @@ function ChildReconciler(shouldClone, shouldTrackSideEffects) {
428 returnFiber: Fiber,
429 current: Fiber | null,
430 yieldNode: ReactYield,
431 - priority: PriorityLevel,
431 + expirationTime: ExpirationTime,
432 ): Fiber {
433 if (current === null || current.tag !== YieldComponent) {
434 // Insert
435 const created = createFiberFromYield(
436 yieldNode,
437 returnFiber.internalContextTag,
438 - priority,
438 + expirationTime,
439 );
440 created.type = yieldNode.value;
441 created.return = returnFiber;
442 return created;
443 } else {
444 // Move based on index
445 - const existing = useFiber(current, priority);
445 + const existing = useFiber(current, expirationTime);
446 existing.type = yieldNode.value;
447 existing.return = returnFiber;
448 return existing;
@@ -453,7 +453,7 @@ function ChildReconciler(shouldClone, shouldTrackSideEffects) {
453 returnFiber: Fiber,
454 current: Fiber | null,
455 portal: ReactPortal,
456 - priority: PriorityLevel,
456 + expirationTime: ExpirationTime,
457 ): Fiber {
458 if (
459 current === null ||
@@ -465,13 +465,13 @@ function ChildReconciler(shouldClone, shouldTrackSideEffects) {
465 const created = createFiberFromPortal(
466 portal,
467 returnFiber.internalContextTag,
468 - priority,
468 + expirationTime,
469 );
470 created.return = returnFiber;
471 return created;
472 } else {
473 // Update
474 - const existing = useFiber(current, priority);
474 + const existing = useFiber(current, expirationTime);
475 existing.pendingProps = portal.children || [];
476 existing.return = returnFiber;
477 return existing;
@@ -482,20 +482,20 @@ function ChildReconciler(shouldClone, shouldTrackSideEffects) {
482 returnFiber: Fiber,
483 current: Fiber | null,
484 fragment: Iterable<*>,
485 - priority: PriorityLevel,
485 + expirationTime: ExpirationTime,
486 ): Fiber {
487 if (current === null || current.tag !== Fragment) {
488 // Insert
489 const created = createFiberFromFragment(
490 fragment,
491 returnFiber.internalContextTag,
492 - priority,
492 + expirationTime,
493 );
494 created.return = returnFiber;
495 return created;
496 } else {
497 // Update
498 - const existing = useFiber(current, priority);
498 + const existing = useFiber(current, expirationTime);
499 existing.pendingProps = fragment;
500 existing.return = returnFiber;
501 return existing;
@@ -505,7 +505,7 @@ function ChildReconciler(shouldClone, shouldTrackSideEffects) {
505 function createChild(
506 returnFiber: Fiber,
507 newChild: any,
508 - priority: PriorityLevel,
508 + expirationTime: ExpirationTime,
509 ): Fiber | null {
510 if (typeof newChild === 'string' || typeof newChild === 'number') {
511 // Text nodes doesn't have keys. If the previous node is implicitly keyed
@@ -514,7 +514,7 @@ function ChildReconciler(shouldClone, shouldTrackSideEffects) {
514 const created = createFiberFromText(
515 '' + newChild,
516 returnFiber.internalContextTag,
517 - priority,
517 + expirationTime,
518 );
519 created.return = returnFiber;
520 return created;
@@ -526,7 +526,7 @@ function ChildReconciler(shouldClone, shouldTrackSideEffects) {
526 const created = createFiberFromElement(
527 newChild,
528 returnFiber.internalContextTag,
529 - priority,
529 + expirationTime,
530 );
531 created.ref = coerceRef(null, newChild);
532 created.return = returnFiber;
@@ -537,7 +537,7 @@ function ChildReconciler(shouldClone, shouldTrackSideEffects) {
537 const created = createFiberFromCoroutine(
538 newChild,
539 returnFiber.internalContextTag,
540 - priority,
540 + expirationTime,
541 );
542 created.return = returnFiber;
543 return created;
@@ -547,7 +547,7 @@ function ChildReconciler(shouldClone, shouldTrackSideEffects) {
547 const created = createFiberFromYield(
548 newChild,
549 returnFiber.internalContextTag,
550 - priority,
550 + expirationTime,
551 );
552 created.type = newChild.value;
553 created.return = returnFiber;
@@ -558,7 +558,7 @@ function ChildReconciler(shouldClone, shouldTrackSideEffects) {
558 const created = createFiberFromPortal(
559 newChild,
560 returnFiber.internalContextTag,
561 - priority,
561 + expirationTime,
562 );
563 created.return = returnFiber;
564 return created;
@@ -569,7 +569,7 @@ function ChildReconciler(shouldClone, shouldTrackSideEffects) {
569 const created = createFiberFromFragment(
570 newChild,
571 returnFiber.internalContextTag,
572 - priority,
572 + expirationTime,
573 );
574 created.return = returnFiber;
575 return created;
@@ -591,7 +591,7 @@ function ChildReconciler(shouldClone, shouldTrackSideEffects) {
591 returnFiber: Fiber,
592 oldFiber: Fiber | null,
593 newChild: any,
594 - priority: PriorityLevel,
594 + expirationTime: ExpirationTime,
595 ): Fiber | null {
596 // Update the fiber if the keys match, otherwise return null.
597
@@ -604,14 +604,24 @@ function ChildReconciler(shouldClone, shouldTrackSideEffects) {
604 if (key !== null) {
605 return null;
606 }
607 - return updateTextNode(returnFiber, oldFiber, '' + newChild, priority);
607 + return updateTextNode(
608 + returnFiber,
609 + oldFiber,
610 + '' + newChild,
611 + expirationTime,
612 + );
613 }
614
615 if (typeof newChild === 'object' && newChild !== null) {
616 switch (newChild.$$typeof) {
617 case REACT_ELEMENT_TYPE: {
618 if (newChild.key === key) {
614 - return updateElement(returnFiber, oldFiber, newChild, priority);
619 + return updateElement(
620 + returnFiber,
621 + oldFiber,
622 + newChild,
623 + expirationTime,
624 + );
625 } else {
626 return null;
627 }
@@ -619,7 +629,12 @@ function ChildReconciler(shouldClone, shouldTrackSideEffects) {
629
630 case REACT_COROUTINE_TYPE: {
631 if (newChild.key === key) {
622 - return updateCoroutine(returnFiber, oldFiber, newChild, priority);
632 + return updateCoroutine(
633 + returnFiber,
634 + oldFiber,
635 + newChild,
636 + expirationTime,
637 + );
638 } else {
639 return null;
640 }
@@ -630,7 +645,7 @@ function ChildReconciler(shouldClone, shouldTrackSideEffects) {
645 // we can continue to replace it without aborting even if it is not a
646 // yield.
647 if (key === null) {
633 - return updateYield(returnFiber, oldFiber, newChild, priority);
648 + return updateYield(returnFiber, oldFiber, newChild, expirationTime);
649 } else {
650 return null;
651 }
@@ -638,7 +653,12 @@ function ChildReconciler(shouldClone, shouldTrackSideEffects) {
653
654 case REACT_PORTAL_TYPE: {
655 if (newChild.key === key) {
641 - return updatePortal(returnFiber, oldFiber, newChild, priority);
656 + return updatePortal(
657 + returnFiber,
658 + oldFiber,
659 + newChild,
660 + expirationTime,
661 + );
662 } else {
663 return null;
664 }
@@ -651,7 +671,7 @@ function ChildReconciler(shouldClone, shouldTrackSideEffects) {
671 if (key !== null) {
672 return null;
673 }
654 - return updateFragment(returnFiber, oldFiber, newChild, priority);
674 + return updateFragment(returnFiber, oldFiber, newChild, expirationTime);
675 }
676
677 throwOnInvalidObjectType(returnFiber, newChild);
@@ -671,13 +691,18 @@ function ChildReconciler(shouldClone, shouldTrackSideEffects) {
691 returnFiber: Fiber,
692 newIdx: number,
693 newChild: any,
674 - priority: PriorityLevel,
694 + expirationTime: ExpirationTime,
695 ): Fiber | null {
696 if (typeof newChild === 'string' || typeof newChild === 'number') {
697 // Text nodes doesn't have keys, so we neither have to check the old nor
698 // new node for the key. If both are text nodes, they match.
699 const matchedFiber = existingChildren.get(newIdx) || null;
680 - return updateTextNode(returnFiber, matchedFiber, '' + newChild, priority);
700 + return updateTextNode(
701 + returnFiber,
702 + matchedFiber,
703 + '' + newChild,
704 + expirationTime,
705 + );
706 }
707
708 if (typeof newChild === 'object' && newChild !== null) {
@@ -687,7 +712,12 @@ function ChildReconciler(shouldClone, shouldTrackSideEffects) {
712 existingChildren.get(
713 newChild.key === null ? newIdx : newChild.key,
714 ) || null;
690 - return updateElement(returnFiber, matchedFiber, newChild, priority);
715 + return updateElement(
716 + returnFiber,
717 + matchedFiber,
718 + newChild,
719 + expirationTime,
720 + );
721 }
722
723 case REACT_COROUTINE_TYPE: {
@@ -695,14 +725,24 @@ function ChildReconciler(shouldClone, shouldTrackSideEffects) {
725 existingChildren.get(
726 newChild.key === null ? newIdx : newChild.key,
727 ) || null;
698 - return updateCoroutine(returnFiber, matchedFiber, newChild, priority);
728 + return updateCoroutine(
729 + returnFiber,
730 + matchedFiber,
731 + newChild,
732 + expirationTime,
733 + );
734 }
735
736 case REACT_YIELD_TYPE: {
737 // Yields doesn't have keys, so we neither have to check the old nor
738 // new node for the key. If both are yields, they match.
739 const matchedFiber = existingChildren.get(newIdx) || null;
705 - return updateYield(returnFiber, matchedFiber, newChild, priority);
740 + return updateYield(
741 + returnFiber,
742 + matchedFiber,
743 + newChild,
744 + expirationTime,
745 + );
746 }
747
748 case REACT_PORTAL_TYPE: {
@@ -710,13 +750,23 @@ function ChildReconciler(shouldClone, shouldTrackSideEffects) {
750 existingChildren.get(
751 newChild.key === null ? newIdx : newChild.key,
752 ) || null;
713 - return updatePortal(returnFiber, matchedFiber, newChild, priority);
753 + return updatePortal(
754 + returnFiber,
755 + matchedFiber,
756 + newChild,
757 + expirationTime,
758 + );
759 }
760 }
761
762 if (isArray(newChild) || getIteratorFn(newChild)) {
763 const matchedFiber = existingChildren.get(newIdx) || null;
719 - return updateFragment(returnFiber, matchedFiber, newChild, priority);
764 + return updateFragment(
765 + returnFiber,
766 + matchedFiber,
767 + newChild,
768 + expirationTime,
769 + );
770 }
771
772 throwOnInvalidObjectType(returnFiber, newChild);
@@ -782,7 +832,7 @@ function ChildReconciler(shouldClone, shouldTrackSideEffects) {
832 returnFiber: Fiber,
833 currentFirstChild: Fiber | null,
834 newChildren: Array<*>,
785 - priority: PriorityLevel,
835 + expirationTime: ExpirationTime,
836 ): Fiber | null {
837 // This algorithm can't optimize by searching from boths ends since we
838 // don't have backpointers on fibers. I'm trying to see how far we can get
@@ -830,7 +880,7 @@ function ChildReconciler(shouldClone, shouldTrackSideEffects) {
880 returnFiber,
881 oldFiber,
882 newChildren[newIdx],
833 - priority,
883 + expirationTime,
884 );
885 if (newFiber === null) {
886 // TODO: This breaks on empty slots like null children. That's
@@ -877,7 +927,7 @@ function ChildReconciler(shouldClone, shouldTrackSideEffects) {
927 const newFiber = createChild(
928 returnFiber,
929 newChildren[newIdx],
880 - priority,
930 + expirationTime,
931 );
932 if (!newFiber) {
933 continue;
@@ -904,7 +954,7 @@ function ChildReconciler(shouldClone, shouldTrackSideEffects) {
954 returnFiber,
955 newIdx,
956 newChildren[newIdx],
907 - priority,
957 + expirationTime,
958 );
959 if (newFiber) {
960 if (shouldTrackSideEffects) {
@@ -941,7 +991,7 @@ function ChildReconciler(shouldClone, shouldTrackSideEffects) {
991 returnFiber: Fiber,
992 currentFirstChild: Fiber | null,
993 newChildrenIterable: Iterable<*>,
944 - priority: PriorityLevel,
994 + expirationTime: ExpirationTime,
995 ): Fiber | null {
996 // This is the same implementation as reconcileChildrenArray(),
997 // but using the iterator instead.
@@ -1005,7 +1055,12 @@ function ChildReconciler(shouldClone, shouldTrackSideEffects) {
1055 } else {
1056 nextOldFiber = oldFiber.sibling;
1057 }
1008 - const newFiber = updateSlot(returnFiber, oldFiber, step.value, priority);
1058 + const newFiber = updateSlot(
1059 + returnFiber,
1060 + oldFiber,
1061 + step.value,
1062 + expirationTime,
1063 + );
1064 if (newFiber === null) {
1065 // TODO: This breaks on empty slots like null children. That's
1066 // unfortunate because it triggers the slow path all the time. We need
@@ -1048,7 +1103,7 @@ function ChildReconciler(shouldClone, shouldTrackSideEffects) {
1103 // If we don't have any more existing children we can choose a fast path
1104 // since the rest will all be insertions.
1105 for (; !step.done; newIdx++, (step = newChildren.next())) {
1051 - const newFiber = createChild(returnFiber, step.value, priority);
1106 + const newFiber = createChild(returnFiber, step.value, expirationTime);
1107 if (newFiber === null) {
1108 continue;
1109 }
@@ -1074,7 +1129,7 @@ function ChildReconciler(shouldClone, shouldTrackSideEffects) {
1129 returnFiber,
1130 newIdx,
1131 step.value,
1077 - priority,
1132 + expirationTime,
1133 );
1134 if (newFiber !== null) {
1135 if (shouldTrackSideEffects) {
@@ -1111,7 +1166,7 @@ function ChildReconciler(shouldClone, shouldTrackSideEffects) {
1166 returnFiber: Fiber,
1167 currentFirstChild: Fiber | null,
1168 textContent: string,
1114 - priority: PriorityLevel,
1169 + expirationTime: ExpirationTime,
1170 ): Fiber {
1171 // There's no need to check for keys on text nodes since we don't have a
1172 // way to define them.
@@ -1119,7 +1174,7 @@ function ChildReconciler(shouldClone, shouldTrackSideEffects) {
1174 // We already have an existing node so let's just update it and delete
1175 // the rest.
1176 deleteRemainingChildren(returnFiber, currentFirstChild.sibling);
1122 - const existing = useFiber(currentFirstChild, priority);
1177 + const existing = useFiber(currentFirstChild, expirationTime);
1178 existing.pendingProps = textContent;
1179 existing.return = returnFiber;
1180 return existing;
@@ -1130,7 +1185,7 @@ function ChildReconciler(shouldClone, shouldTrackSideEffects) {
1185 const created = createFiberFromText(
1186 textContent,
1187 returnFiber.internalContextTag,
1133 - priority,
1188 + expirationTime,
1189 );
1190 created.return = returnFiber;
1191 return created;
@@ -1140,7 +1195,7 @@ function ChildReconciler(shouldClone, shouldTrackSideEffects) {
1195 returnFiber: Fiber,
1196 currentFirstChild: Fiber | null,
1197 element: ReactElement,
1143 - priority: PriorityLevel,
1198 + expirationTime: ExpirationTime,
1199 ): Fiber {
1200 const key = element.key;
1201 let child = currentFirstChild;
@@ -1150,7 +1205,7 @@ function ChildReconciler(shouldClone, shouldTrackSideEffects) {
1205 if (child.key === key) {
1206 if (child.type === element.type) {
1207 deleteRemainingChildren(returnFiber, child.sibling);
1153 - const existing = useFiber(child, priority);
1208 + const existing = useFiber(child, expirationTime);
1209 existing.ref = coerceRef(child, element);
1210 existing.pendingProps = element.props;
1211 existing.return = returnFiber;
@@ -1172,7 +1227,7 @@ function ChildReconciler(shouldClone, shouldTrackSideEffects) {
1227 const created = createFiberFromElement(
1228 element,
1229 returnFiber.internalContextTag,
1175 - priority,
1230 + expirationTime,
1231 );
1232 created.ref = coerceRef(currentFirstChild, element);
1233 created.return = returnFiber;
@@ -1183,7 +1238,7 @@ function ChildReconciler(shouldClone, shouldTrackSideEffects) {
1238 returnFiber: Fiber,
1239 currentFirstChild: Fiber | null,
1240 coroutine: ReactCoroutine,
1186 - priority: PriorityLevel,
1241 + expirationTime: ExpirationTime,
1242 ): Fiber {
1243 const key = coroutine.key;
1244 let child = currentFirstChild;
@@ -1193,7 +1248,7 @@ function ChildReconciler(shouldClone, shouldTrackSideEffects) {
1248 if (child.key === key) {
1249 if (child.tag === CoroutineComponent) {
1250 deleteRemainingChildren(returnFiber, child.sibling);
1196 - const existing = useFiber(child, priority);
1251 + const existing = useFiber(child, expirationTime);
1252 existing.pendingProps = coroutine;
1253 existing.return = returnFiber;
1254 return existing;
@@ -1210,7 +1265,7 @@ function ChildReconciler(shouldClone, shouldTrackSideEffects) {
1265 const created = createFiberFromCoroutine(
1266 coroutine,
1267 returnFiber.internalContextTag,
1213 - priority,
1268 + expirationTime,
1269 );
1270 created.return = returnFiber;
1271 return created;
@@ -1220,14 +1275,14 @@ function ChildReconciler(shouldClone, shouldTrackSideEffects) {
1275 returnFiber: Fiber,
1276 currentFirstChild: Fiber | null,
1277 yieldNode: ReactYield,
1223 - priority: PriorityLevel,
1278 + expirationTime: ExpirationTime,
1279 ): Fiber {
1280 // There's no need to check for keys on yields since they're stateless.
1281 let child = currentFirstChild;
1282 if (child !== null) {
1283 if (child.tag === YieldComponent) {
1284 deleteRemainingChildren(returnFiber, child.sibling);
1230 - const existing = useFiber(child, priority);
1285 + const existing = useFiber(child, expirationTime);
1286 existing.type = yieldNode.value;
1287 existing.return = returnFiber;
1288 return existing;
@@ -1239,7 +1294,7 @@ function ChildReconciler(shouldClone, shouldTrackSideEffects) {
1294 const created = createFiberFromYield(
1295 yieldNode,
1296 returnFiber.internalContextTag,
1242 - priority,
1297 + expirationTime,
1298 );
1299 created.type = yieldNode.value;
1300 created.return = returnFiber;
@@ -1250,7 +1305,7 @@ function ChildReconciler(shouldClone, shouldTrackSideEffects) {
1305 returnFiber: Fiber,
1306 currentFirstChild: Fiber | null,
1307 portal: ReactPortal,
1253 - priority: PriorityLevel,
1308 + expirationTime: ExpirationTime,
1309 ): Fiber {
1310 const key = portal.key;
1311 let child = currentFirstChild;
@@ -1264,7 +1319,7 @@ function ChildReconciler(shouldClone, shouldTrackSideEffects) {
1319 child.stateNode.implementation === portal.implementation
1320 ) {
1321 deleteRemainingChildren(returnFiber, child.sibling);
1267 - const existing = useFiber(child, priority);
1322 + const existing = useFiber(child, expirationTime);
1323 existing.pendingProps = portal.children || [];
1324 existing.return = returnFiber;
1325 return existing;
@@ -1281,7 +1336,7 @@ function ChildReconciler(shouldClone, shouldTrackSideEffects) {
1336 const created = createFiberFromPortal(
1337 portal,
1338 returnFiber.internalContextTag,
1284 - priority,
1339 + expirationTime,
1340 );
1341 created.return = returnFiber;
1342 return created;
@@ -1294,7 +1349,7 @@ function ChildReconciler(shouldClone, shouldTrackSideEffects) {
1349 returnFiber: Fiber,
1350 currentFirstChild: Fiber | null,
1351 newChild: any,
1297 - priority: PriorityLevel,
1352 + expirationTime: ExpirationTime,
1353 ): Fiber | null {
1354 // This function is not recursive.
1355 // If the top level item is an array, we treat it as a set of children,
@@ -1304,8 +1359,6 @@ function ChildReconciler(shouldClone, shouldTrackSideEffects) {
1359 // Handle object types
1360 const isObject = typeof newChild === 'object' && newChild !== null;
1361 if (isObject) {
1307 - // Support only the subset of return types that Stack supports. Treat
1308 - // everything else as empty, but log a warning.
1362 switch (newChild.$$typeof) {
1363 case REACT_ELEMENT_TYPE:
1364 return placeSingleChild(
@@ -1313,7 +1366,7 @@ function ChildReconciler(shouldClone, shouldTrackSideEffects) {
1366 returnFiber,
1367 currentFirstChild,
1368 newChild,
1316 - priority,
1369 + expirationTime,
1370 ),
1371 );
1372
@@ -1323,17 +1376,16 @@ function ChildReconciler(shouldClone, shouldTrackSideEffects) {
1376 returnFiber,
1377 currentFirstChild,
1378 newChild,
1326 - priority,
1379 + expirationTime,
1380 ),
1381 );
1329 -
1382 case REACT_YIELD_TYPE:
1383 return placeSingleChild(
1384 reconcileSingleYield(
1385 returnFiber,
1386 currentFirstChild,
1387 newChild,
1336 - priority,
1388 + expirationTime,
1389 ),
1390 );
1391
@@ -1343,7 +1395,7 @@ function ChildReconciler(shouldClone, shouldTrackSideEffects) {
1395 returnFiber,
1396 currentFirstChild,
1397 newChild,
1346 - priority,
1398 + expirationTime,
1399 ),
1400 );
1401 }
@@ -1355,7 +1407,7 @@ function ChildReconciler(shouldClone, shouldTrackSideEffects) {
1407 returnFiber,
1408 currentFirstChild,
1409 '' + newChild,
1358 - priority,
1410 + expirationTime,
1411 ),
1412 );
1413 }
@@ -1365,7 +1417,7 @@ function ChildReconciler(shouldClone, shouldTrackSideEffects) {
1417 returnFiber,
1418 currentFirstChild,
1419 newChild,
1368 - priority,
1420 + expirationTime,
1421 );
1422 }
1423
@@ -1374,7 +1426,7 @@ function ChildReconciler(shouldClone, shouldTrackSideEffects) {
1426 returnFiber,
1427 currentFirstChild,
1428 newChild,
1377 - priority,
1429 + expirationTime,
1430 );
1431 }
1432
@@ -1446,7 +1498,7 @@ exports.cloneChildFibers = function(
1498 let currentChild = workInProgress.child;
1499 let newChild = createWorkInProgress(
1500 currentChild,
1449 - currentChild.pendingWorkPriority,
1501 + currentChild.expirationTime,
1502 );
1503 // TODO: Pass this as an argument, since it's easy to forget.
1504 newChild.pendingProps = currentChild.pendingProps;
@@ -1457,7 +1509,7 @@ exports.cloneChildFibers = function(
1509 currentChild = currentChild.sibling;
1510 newChild = newChild.sibling = createWorkInProgress(
1511 currentChild,
1460 - currentChild.pendingWorkPriority,
1512 + currentChild.expirationTime,
1513 );
1514 newChild.pendingProps = currentChild.pendingProps;
1515 newChild.return = workInProgress;
src/renderers/shared/fiber/ReactFiber.js
+23 -18
@@ -21,6 +21,7 @@ import type {TypeOfWork} from 'ReactTypeOfWork';
21 import type {TypeOfInternalContext} from 'ReactTypeOfInternalContext';
22 import type {TypeOfSideEffect} from 'ReactTypeOfSideEffect';
23 import type {PriorityLevel} from 'ReactPriorityLevel';
24 +import type {ExpirationTime} from 'ReactFiberExpirationTime';
25 import type {UpdateQueue} from 'ReactFiberUpdateQueue';
26
27 var {
@@ -35,7 +36,9 @@ var {
36 Fragment,
37 } = require('ReactTypeOfWork');
38
38 -var {NoWork} = require('ReactPriorityLevel');
39 +var {NoWork: NoWorkPriority} = require('ReactPriorityLevel');
40 +
41 +var {NoWork} = require('ReactFiberExpirationTime');
42
43 var {NoContext} = require('ReactTypeOfInternalContext');
44
@@ -134,8 +137,9 @@ export type Fiber = {|
137 firstEffect: Fiber | null,
138 lastEffect: Fiber | null,
139
137 - // This will be used to quickly determine if a subtree has no pending changes.
138 - pendingWorkPriority: PriorityLevel,
140 + // Represents a time in the future by which this work should be completed.
141 + // This is also used to quickly determine if a subtree has no pending changes.
142 + expirationTime: ExpirationTime,
143
144 // This is a pooled version of a Fiber. Every fiber that gets updated will
145 // eventually have a pair. There are cases when we can clean up pairs to save
@@ -189,7 +193,7 @@ function FiberNode(
193 this.firstEffect = null;
194 this.lastEffect = null;
195
192 - this.pendingWorkPriority = NoWork;
196 + this.expirationTime = NoWork;
197
198 this.alternate = null;
199
@@ -233,7 +237,7 @@ function shouldConstruct(Component) {
237 // This is used to create an alternate fiber to do work on.
238 exports.createWorkInProgress = function(
239 current: Fiber,
236 - renderPriority: PriorityLevel,
240 + expirationTime: ExpirationTime,
241 ): Fiber {
242 let workInProgress = current.alternate;
243 if (workInProgress === null) {
@@ -270,7 +274,7 @@ exports.createWorkInProgress = function(
274 workInProgress.lastEffect = null;
275 }
276
273 - workInProgress.pendingWorkPriority = renderPriority;
277 + workInProgress.expirationTime = expirationTime;
278
279 workInProgress.child = current.child;
280 workInProgress.memoizedProps = current.memoizedProps;
@@ -296,7 +300,7 @@ exports.createHostRootFiber = function(): Fiber {
300 exports.createFiberFromElement = function(
301 element: ReactElement,
302 internalContextTag: TypeOfInternalContext,
299 - priorityLevel: PriorityLevel,
303 + expirationTime: ExpirationTime,
304 ): Fiber {
305 let owner = null;
306 if (__DEV__) {
@@ -310,7 +314,7 @@ exports.createFiberFromElement = function(
314 owner,
315 );
316 fiber.pendingProps = element.props;
313 - fiber.pendingWorkPriority = priorityLevel;
317 + fiber.expirationTime = expirationTime;
318
319 if (__DEV__) {
320 fiber._debugSource = element._source;
@@ -323,24 +327,24 @@ exports.createFiberFromElement = function(
327 exports.createFiberFromFragment = function(
328 elements: ReactFragment,
329 internalContextTag: TypeOfInternalContext,
326 - priorityLevel: PriorityLevel,
330 + expirationTime: ExpirationTime,
331 ): Fiber {
332 // TODO: Consider supporting keyed fragments. Technically, we accidentally
333 // support that in the existing React.
334 const fiber = createFiber(Fragment, null, internalContextTag);
335 fiber.pendingProps = elements;
332 - fiber.pendingWorkPriority = priorityLevel;
336 + fiber.expirationTime = expirationTime;
337 return fiber;
338 };
339
340 exports.createFiberFromText = function(
341 content: string,
342 internalContextTag: TypeOfInternalContext,
339 - priorityLevel: PriorityLevel,
343 + expirationTime: ExpirationTime,
344 ): Fiber {
345 const fiber = createFiber(HostText, null, internalContextTag);
346 fiber.pendingProps = content;
343 - fiber.pendingWorkPriority = priorityLevel;
347 + fiber.expirationTime = expirationTime;
348 return fiber;
349 };
350
@@ -411,7 +415,7 @@ exports.createFiberFromHostInstanceForDeletion = function(): Fiber {
415 exports.createFiberFromCoroutine = function(
416 coroutine: ReactCoroutine,
417 internalContextTag: TypeOfInternalContext,
414 - priorityLevel: PriorityLevel,
418 + expirationTime: ExpirationTime,
419 ): Fiber {
420 const fiber = createFiber(
421 CoroutineComponent,
@@ -420,27 +424,28 @@ exports.createFiberFromCoroutine = function(
424 );
425 fiber.type = coroutine.handler;
426 fiber.pendingProps = coroutine;
423 - fiber.pendingWorkPriority = priorityLevel;
427 + fiber.expirationTime = expirationTime;
428 return fiber;
429 };
430
431 exports.createFiberFromYield = function(
432 yieldNode: ReactYield,
433 internalContextTag: TypeOfInternalContext,
430 - priorityLevel: PriorityLevel,
434 + expirationTime: ExpirationTime,
435 ): Fiber {
436 const fiber = createFiber(YieldComponent, null, internalContextTag);
437 + fiber.expirationTime = expirationTime;
438 return fiber;
439 };
440
441 exports.createFiberFromPortal = function(
442 portal: ReactPortal,
443 internalContextTag: TypeOfInternalContext,
439 - priorityLevel: PriorityLevel,
444 + expirationTime: ExpirationTime,
445 ): Fiber {
446 const fiber = createFiber(HostPortal, portal.key, internalContextTag);
447 fiber.pendingProps = portal.children || [];
443 - fiber.pendingWorkPriority = priorityLevel;
448 + fiber.expirationTime = expirationTime;
449 fiber.stateNode = {
450 containerInfo: portal.containerInfo,
451 implementation: portal.implementation,
@@ -452,5 +457,5 @@ exports.largerPriority = function(
457 p1: PriorityLevel,
458 p2: PriorityLevel,
459 ): PriorityLevel {
455 - return p1 !== NoWork && (p2 === NoWork || p2 > p1) ? p1 : p2;
460 + return p1 !== NoWorkPriority && (p2 === NoWorkPriority || p2 > p1) ? p1 : p2;
461 };
src/renderers/shared/fiber/ReactFiberBeginWork.js
+85 -48
@@ -17,6 +17,7 @@ import type {HydrationContext} from 'ReactFiberHydrationContext';
17 import type {FiberRoot} from 'ReactFiberRoot';
18 import type {HostConfig} from 'ReactFiberReconciler';
19 import type {PriorityLevel} from 'ReactPriorityLevel';
20 +import type {ExpirationTime} from 'ReactFiberExpirationTime';
21
22 var {
23 mountChildFibersInPlace,
@@ -47,7 +48,7 @@ var {
48 YieldComponent,
49 Fragment,
50 } = ReactTypeOfWork;
50 -var {NoWork, OffscreenPriority} = require('ReactPriorityLevel');
51 +var {NoWork, Never} = require('ReactFiberExpirationTime');
52 var {
53 PerformedWork,
54 Placement,
@@ -71,8 +72,16 @@ module.exports = function<T, P, I, TI, PI, C, CX, PL>(
72 config: HostConfig<T, P, I, TI, PI, C, CX, PL>,
73 hostContext: HostContext<C, CX>,
74 hydrationContext: HydrationContext<C, CX>,
74 - scheduleUpdate: (fiber: Fiber, priorityLevel: PriorityLevel) => void,
75 - getPriorityContext: (fiber: Fiber, forceAsync: boolean) => PriorityLevel,
75 + scheduleUpdate: (fiber: Fiber, expirationTime: ExpirationTime) => void,
76 + getPriorityContext: (
77 + fiber: Fiber,
78 + forceAsync: boolean,
79 + ) => PriorityLevel | null,
80 + recalculateCurrentTime: () => ExpirationTime,
81 + getExpirationTimeForPriority: (
82 + currentTime: ExpirationTime,
83 + priorityLevel: PriorityLevel | null,
84 + ) => ExpirationTime,
85 ) {
86 const {
87 shouldSetTextContent,
@@ -99,23 +108,25 @@ module.exports = function<T, P, I, TI, PI, C, CX, PL>(
108 getPriorityContext,
109 memoizeProps,
110 memoizeState,
111 + recalculateCurrentTime,
112 + getExpirationTimeForPriority,
113 );
114
115 + // TODO: Remove this and use reconcileChildrenAtExpirationTime directly.
116 function reconcileChildren(current, workInProgress, nextChildren) {
105 - const priorityLevel = workInProgress.pendingWorkPriority;
106 - reconcileChildrenAtPriority(
117 + reconcileChildrenAtExpirationTime(
118 current,
119 workInProgress,
120 nextChildren,
110 - priorityLevel,
121 + workInProgress.expirationTime,
122 );
123 }
124
114 - function reconcileChildrenAtPriority(
125 + function reconcileChildrenAtExpirationTime(
126 current,
127 workInProgress,
128 nextChildren,
118 - priorityLevel,
129 + renderExpirationTime,
130 ) {
131 if (current === null) {
132 // If this is a fresh new component that hasn't been rendered yet, we
@@ -126,7 +137,7 @@ module.exports = function<T, P, I, TI, PI, C, CX, PL>(
137 workInProgress,
138 workInProgress.child,
139 nextChildren,
129 - priorityLevel,
140 + renderExpirationTime,
141 );
142 } else if (current.child === workInProgress.child) {
143 // If the current child is the same as the work in progress, it means that
@@ -139,7 +150,7 @@ module.exports = function<T, P, I, TI, PI, C, CX, PL>(
150 workInProgress,
151 workInProgress.child,
152 nextChildren,
142 - priorityLevel,
153 + renderExpirationTime,
154 );
155 } else {
156 // If, on the other hand, it is already using a clone, that means we've
@@ -149,7 +160,7 @@ module.exports = function<T, P, I, TI, PI, C, CX, PL>(
160 workInProgress,
161 workInProgress.child,
162 nextChildren,
152 - priorityLevel,
163 + renderExpirationTime,
164 );
165 }
166 }
@@ -223,7 +234,7 @@ module.exports = function<T, P, I, TI, PI, C, CX, PL>(
234 function updateClassComponent(
235 current: Fiber | null,
236 workInProgress: Fiber,
226 - priorityLevel: PriorityLevel,
237 + renderExpirationTime: ExpirationTime,
238 ) {
239 // Push context providers early to prevent context stack mismatches.
240 // During mounting we don't know the child context yet as the instance doesn't exist.
@@ -235,18 +246,18 @@ module.exports = function<T, P, I, TI, PI, C, CX, PL>(
246 if (!workInProgress.stateNode) {
247 // In the initial pass we might need to construct the instance.
248 constructClassInstance(workInProgress, workInProgress.pendingProps);
238 - mountClassInstance(workInProgress, priorityLevel);
249 + mountClassInstance(workInProgress, renderExpirationTime);
250 shouldUpdate = true;
251 } else {
252 invariant(false, 'Resuming work not yet implemented.');
253 // In a resume, we'll already have an instance we can reuse.
243 - // shouldUpdate = resumeMountClassInstance(workInProgress, priorityLevel);
254 + // shouldUpdate = resumeMountClassInstance(workInProgress, renderExpirationTime);
255 }
256 } else {
257 shouldUpdate = updateClassInstance(
258 current,
259 workInProgress,
249 - priorityLevel,
260 + renderExpirationTime,
261 );
262 }
263 return finishClassComponent(
@@ -318,7 +329,7 @@ module.exports = function<T, P, I, TI, PI, C, CX, PL>(
329 pushHostContainer(workInProgress, root.containerInfo);
330 }
331
321 - function updateHostRoot(current, workInProgress, priorityLevel) {
332 + function updateHostRoot(current, workInProgress, renderExpirationTime) {
333 pushHostRootContext(workInProgress);
334 const updateQueue = workInProgress.updateQueue;
335 if (updateQueue !== null) {
@@ -330,11 +341,11 @@ module.exports = function<T, P, I, TI, PI, C, CX, PL>(
341 null,
342 prevState,
343 null,
333 - priorityLevel,
344 + renderExpirationTime,
345 );
346 if (prevState === state) {
347 // If the state is the same as before, that's a bailout because we had
337 - // no work matching this priority.
348 + // no work that expires at this time.
349 resetHydrationState();
350 return bailoutOnAlreadyFinishedWork(current, workInProgress);
351 }
@@ -361,7 +372,7 @@ module.exports = function<T, P, I, TI, PI, C, CX, PL>(
372 workInProgress,
373 workInProgress.child,
374 element,
364 - priorityLevel,
375 + renderExpirationTime,
376 );
377 } else {
378 // Otherwise reset hydration state in case we aborted and resumed another
@@ -377,7 +388,7 @@ module.exports = function<T, P, I, TI, PI, C, CX, PL>(
388 return bailoutOnAlreadyFinishedWork(current, workInProgress);
389 }
390
380 - function updateHostComponent(current, workInProgress, renderPriority) {
391 + function updateHostComponent(current, workInProgress, renderExpirationTime) {
392 pushHostContext(workInProgress);
393
394 if (current === null) {
@@ -423,13 +434,13 @@ module.exports = function<T, P, I, TI, PI, C, CX, PL>(
434
435 // Check the host config to see if the children are offscreen/hidden.
436 if (
426 - renderPriority !== OffscreenPriority &&
437 + renderExpirationTime !== Never &&
438 !useSyncScheduling &&
439 shouldDeprioritizeSubtree(type, nextProps)
440 ) {
441 // Down-prioritize the children.
431 - workInProgress.pendingWorkPriority = OffscreenPriority;
432 - // Bailout and come back to this fiber later at OffscreenPriority.
442 + workInProgress.expirationTime = Never;
443 + // Bailout and come back to this fiber later.
444 return null;
445 }
446
@@ -452,7 +463,11 @@ module.exports = function<T, P, I, TI, PI, C, CX, PL>(
463 return null;
464 }
465
455 - function mountIndeterminateComponent(current, workInProgress, priorityLevel) {
466 + function mountIndeterminateComponent(
467 + current,
468 + workInProgress,
469 + renderExpirationTime,
470 + ) {
471 invariant(
472 current === null,
473 'An indeterminate component should never have mounted. This error is ' +
@@ -487,7 +502,7 @@ module.exports = function<T, P, I, TI, PI, C, CX, PL>(
502 // We will invalidate the child context in finishClassComponent() right after rendering.
503 const hasContext = pushContextProvider(workInProgress);
504 adoptClassInstance(workInProgress, value);
490 - mountClassInstance(workInProgress, priorityLevel);
505 + mountClassInstance(workInProgress, renderExpirationTime);
506 return finishClassComponent(current, workInProgress, true, hasContext);
507 } else {
508 // Proceed under the assumption that this is a functional component
@@ -532,7 +547,11 @@ module.exports = function<T, P, I, TI, PI, C, CX, PL>(
547 }
548 }
549
535 - function updateCoroutineComponent(current, workInProgress) {
550 + function updateCoroutineComponent(
551 + current,
552 + workInProgress,
553 + renderExpirationTime,
554 + ) {
555 var nextCoroutine = (workInProgress.pendingProps: null | ReactCoroutine);
556 if (hasContextChanged()) {
557 // Normally we can bail out on props equality but if context has changed
@@ -556,30 +575,29 @@ module.exports = function<T, P, I, TI, PI, C, CX, PL>(
575 }
576
577 const nextChildren = nextCoroutine.children;
559 - const priorityLevel = workInProgress.pendingWorkPriority;
578
561 - // The following is a fork of reconcileChildrenAtPriority but using
579 + // The following is a fork of reconcileChildrenAtExpirationTime but using
580 // stateNode to store the child.
581 if (current === null) {
582 workInProgress.stateNode = mountChildFibersInPlace(
583 workInProgress,
584 workInProgress.stateNode,
585 nextChildren,
568 - priorityLevel,
586 + renderExpirationTime,
587 );
588 } else if (current.child === workInProgress.child) {
589 workInProgress.stateNode = reconcileChildFibers(
590 workInProgress,
591 workInProgress.stateNode,
592 nextChildren,
575 - priorityLevel,
593 + renderExpirationTime,
594 );
595 } else {
596 workInProgress.stateNode = reconcileChildFibersInPlace(
597 workInProgress,
598 workInProgress.stateNode,
599 nextChildren,
582 - priorityLevel,
600 + renderExpirationTime,
601 );
602 }
603
@@ -589,9 +607,12 @@ module.exports = function<T, P, I, TI, PI, C, CX, PL>(
607 return workInProgress.stateNode;
608 }
609
592 - function updatePortalComponent(current, workInProgress) {
610 + function updatePortalComponent(
611 + current,
612 + workInProgress,
613 + renderExpirationTime,
614 + ) {
615 pushHostContainer(workInProgress, workInProgress.stateNode.containerInfo);
594 - const priorityLevel = workInProgress.pendingWorkPriority;
616 let nextChildren = workInProgress.pendingProps;
617 if (hasContextChanged()) {
618 // Normally we can bail out on props equality but if context has changed
@@ -621,7 +642,7 @@ module.exports = function<T, P, I, TI, PI, C, CX, PL>(
642 workInProgress,
643 workInProgress.child,
644 nextChildren,
624 - priorityLevel,
645 + renderExpirationTime,
646 );
647 memoizeProps(workInProgress, nextChildren);
648 } else {
@@ -716,11 +737,11 @@ module.exports = function<T, P, I, TI, PI, C, CX, PL>(
737 function beginWork(
738 current: Fiber | null,
739 workInProgress: Fiber,
719 - priorityLevel: PriorityLevel,
740 + renderExpirationTime: ExpirationTime,
741 ): Fiber | null {
742 if (
722 - workInProgress.pendingWorkPriority === NoWork ||
723 - workInProgress.pendingWorkPriority > priorityLevel
743 + workInProgress.expirationTime === NoWork ||
744 + workInProgress.expirationTime > renderExpirationTime
745 ) {
746 return bailoutOnLowPriority(current, workInProgress);
747 }
@@ -730,16 +751,24 @@ module.exports = function<T, P, I, TI, PI, C, CX, PL>(
751 return mountIndeterminateComponent(
752 current,
753 workInProgress,
733 - priorityLevel,
754 + renderExpirationTime,
755 );
756 case FunctionalComponent:
757 return updateFunctionalComponent(current, workInProgress);
758 case ClassComponent:
738 - return updateClassComponent(current, workInProgress, priorityLevel);
759 + return updateClassComponent(
760 + current,
761 + workInProgress,
762 + renderExpirationTime,
763 + );
764 case HostRoot:
740 - return updateHostRoot(current, workInProgress, priorityLevel);
765 + return updateHostRoot(current, workInProgress, renderExpirationTime);
766 case HostComponent:
742 - return updateHostComponent(current, workInProgress, priorityLevel);
767 + return updateHostComponent(
768 + current,
769 + workInProgress,
770 + renderExpirationTime,
771 + );
772 case HostText:
773 return updateHostText(current, workInProgress);
774 case CoroutineHandlerPhase:
@@ -747,13 +776,21 @@ module.exports = function<T, P, I, TI, PI, C, CX, PL>(
776 workInProgress.tag = CoroutineComponent;
777 // Intentionally fall through since this is now the same.
778 case CoroutineComponent:
750 - return updateCoroutineComponent(current, workInProgress);
779 + return updateCoroutineComponent(
780 + current,
781 + workInProgress,
782 + renderExpirationTime,
783 + );
784 case YieldComponent:
785 // A yield component is just a placeholder, we can just run through the
786 // next one immediately.
787 return null;
788 case HostPortal:
756 - return updatePortalComponent(current, workInProgress);
789 + return updatePortalComponent(
790 + current,
791 + workInProgress,
792 + renderExpirationTime,
793 + );
794 case Fragment:
795 return updateFragment(current, workInProgress);
796 default:
@@ -768,7 +805,7 @@ module.exports = function<T, P, I, TI, PI, C, CX, PL>(
805 function beginFailedWork(
806 current: Fiber | null,
807 workInProgress: Fiber,
771 - priorityLevel: PriorityLevel,
808 + renderExpirationTime: ExpirationTime,
809 ) {
810 // Push context providers here to avoid a push/pop context mismatch.
811 switch (workInProgress.tag) {
@@ -801,8 +838,8 @@ module.exports = function<T, P, I, TI, PI, C, CX, PL>(
838 }
839
840 if (
804 - workInProgress.pendingWorkPriority === NoWork ||
805 - workInProgress.pendingWorkPriority > priorityLevel
841 + workInProgress.expirationTime === NoWork ||
842 + workInProgress.expirationTime > renderExpirationTime
843 ) {
844 return bailoutOnLowPriority(current, workInProgress);
845 }
@@ -814,11 +851,11 @@ module.exports = function<T, P, I, TI, PI, C, CX, PL>(
851
852 // Unmount the current children as if the component rendered null
853 const nextChildren = null;
817 - reconcileChildrenAtPriority(
854 + reconcileChildrenAtExpirationTime(
855 current,
856 workInProgress,
857 nextChildren,
821 - priorityLevel,
858 + renderExpirationTime,
859 );
860
861 if (workInProgress.tag === ClassComponent) {
src/renderers/shared/fiber/ReactFiberClassComponent.js
+56 -12
@@ -12,6 +12,7 @@
12
13 import type {Fiber} from 'ReactFiber';
14 import type {PriorityLevel} from 'ReactPriorityLevel';
15 +import type {ExpirationTime} from 'ReactFiberExpirationTime';
16
17 var {Update} = require('ReactTypeOfSideEffect');
18
@@ -77,10 +78,18 @@ if (__DEV__) {
78 }
79
80 module.exports = function(
80 - scheduleUpdate: (fiber: Fiber, priorityLevel: PriorityLevel) => void,
81 - getPriorityContext: (fiber: Fiber, forceAsync: boolean) => PriorityLevel,
81 + scheduleUpdate: (fiber: Fiber, expirationTime: ExpirationTime) => void,
82 + getPriorityContext: (
83 + fiber: Fiber,
84 + forceAsync: boolean,
85 + ) => PriorityLevel | null,
86 memoizeProps: (workInProgress: Fiber, props: any) => void,
87 memoizeState: (workInProgress: Fiber, state: any) => void,
88 + recalculateCurrentTime: () => ExpirationTime,
89 + getExpirationTimeForPriority: (
90 + currentTime: ExpirationTime,
91 + priorityLevel: PriorityLevel | null,
92 + ) => ExpirationTime,
93 ) {
94 // Class component state updater
95 const updater = {
@@ -88,32 +97,67 @@ module.exports = function(
97 enqueueSetState(instance, partialState, callback) {
98 const fiber = ReactInstanceMap.get(instance);
99 const priorityLevel = getPriorityContext(fiber, false);
100 + const currentTime = recalculateCurrentTime();
101 + const expirationTime = getExpirationTimeForPriority(
102 + currentTime,
103 + priorityLevel,
104 + );
105 callback = callback === undefined ? null : callback;
106 if (__DEV__) {
107 warnOnInvalidCallback(callback, 'setState');
108 }
95 - addUpdate(fiber, partialState, callback, priorityLevel);
96 - scheduleUpdate(fiber, priorityLevel);
109 + addUpdate(
110 + fiber,
111 + partialState,
112 + callback,
113 + priorityLevel,
114 + expirationTime,
115 + currentTime,
116 + );
117 + scheduleUpdate(fiber, expirationTime);
118 },
119 enqueueReplaceState(instance, state, callback) {
120 const fiber = ReactInstanceMap.get(instance);
121 const priorityLevel = getPriorityContext(fiber, false);
122 + const currentTime = recalculateCurrentTime();
123 + const expirationTime = getExpirationTimeForPriority(
124 + currentTime,
125 + priorityLevel,
126 + );
127 callback = callback === undefined ? null : callback;
128 if (__DEV__) {
129 warnOnInvalidCallback(callback, 'replaceState');
130 }
105 - addReplaceUpdate(fiber, state, callback, priorityLevel);
106 - scheduleUpdate(fiber, priorityLevel);
131 + addReplaceUpdate(
132 + fiber,
133 + state,
134 + callback,
135 + priorityLevel,
136 + expirationTime,
137 + currentTime,
138 + );
139 + scheduleUpdate(fiber, expirationTime);
140 },
141 enqueueForceUpdate(instance, callback) {
142 const fiber = ReactInstanceMap.get(instance);
143 const priorityLevel = getPriorityContext(fiber, false);
144 + const currentTime = recalculateCurrentTime();
145 + const expirationTime = getExpirationTimeForPriority(
146 + currentTime,
147 + priorityLevel,
148 + );
149 callback = callback === undefined ? null : callback;
150 if (__DEV__) {
151 warnOnInvalidCallback(callback, 'forceUpdate');
152 }
115 - addForceUpdate(fiber, callback, priorityLevel);
116 - scheduleUpdate(fiber, priorityLevel);
153 + addForceUpdate(
154 + fiber,
155 + callback,
156 + priorityLevel,
157 + expirationTime,
158 + currentTime,
159 + );
160 + scheduleUpdate(fiber, expirationTime);
161 },
162 };
163
@@ -383,7 +427,7 @@ module.exports = function(
427 // Invokes the mount life-cycles on a previously never rendered instance.
428 function mountClassInstance(
429 workInProgress: Fiber,
386 - priorityLevel: PriorityLevel,
430 + renderExpirationTime: ExpirationTime,
431 ): void {
432 const current = workInProgress.alternate;
433
@@ -430,7 +474,7 @@ module.exports = function(
474 instance,
475 state,
476 props,
433 - priorityLevel,
477 + renderExpirationTime,
478 );
479 }
480 }
@@ -548,7 +592,7 @@ module.exports = function(
592 function updateClassInstance(
593 current: Fiber,
594 workInProgress: Fiber,
551 - priorityLevel: PriorityLevel,
595 + renderExpirationTime: ExpirationTime,
596 ): boolean {
597 const instance = workInProgress.stateNode;
598 resetInputPointers(workInProgress, instance);
@@ -597,7 +641,7 @@ module.exports = function(
641 instance,
642 oldState,
643 newProps,
600 - priorityLevel,
644 + renderExpirationTime,
645 );
646 } else {
647 newState = oldState;
src/renderers/shared/fiber/ReactFiberCompleteWork.js
+13 -10
@@ -12,7 +12,7 @@
12
13 import type {ReactCoroutine} from 'ReactTypes';
14 import type {Fiber} from 'ReactFiber';
15 -import type {PriorityLevel} from 'ReactPriorityLevel';
15 +import type {ExpirationTime} from 'ReactFiberExpirationTime';
16 import type {HostContext} from 'ReactFiberHostContext';
17 import type {HydrationContext} from 'ReactFiberHydrationContext';
18 import type {FiberRoot} from 'ReactFiberRoot';
@@ -25,7 +25,7 @@ var {
25 } = require('ReactFiberContext');
26 var ReactTypeOfWork = require('ReactTypeOfWork');
27 var ReactTypeOfSideEffect = require('ReactTypeOfSideEffect');
28 -var ReactPriorityLevel = require('ReactPriorityLevel');
28 +var ReactFiberExpirationTime = require('ReactFiberExpirationTime');
29 var {
30 IndeterminateComponent,
31 FunctionalComponent,
@@ -40,7 +40,7 @@ var {
40 Fragment,
41 } = ReactTypeOfWork;
42 var {Placement, Ref, Update} = ReactTypeOfSideEffect;
43 -var {OffscreenPriority} = ReactPriorityLevel;
43 +var {Never} = ReactFiberExpirationTime;
44
45 var invariant = require('fbjs/lib/invariant');
46
@@ -113,6 +113,7 @@ module.exports = function<T, P, I, TI, PI, C, CX, PL>(
113 function moveCoroutineToHandlerPhase(
114 current: Fiber | null,
115 workInProgress: Fiber,
116 + renderExpirationTime: ExpirationTime,
117 ) {
118 var coroutine = (workInProgress.memoizedProps: ?ReactCoroutine);
119 invariant(
@@ -139,13 +140,11 @@ module.exports = function<T, P, I, TI, PI, C, CX, PL>(
140 var nextChildren = fn(props, yields);
141
142 var currentFirstChild = current !== null ? current.child : null;
142 - // Inherit the priority of the returnFiber.
143 - const priority = workInProgress.pendingWorkPriority;
143 workInProgress.child = reconcileChildFibers(
144 workInProgress,
145 currentFirstChild,
146 nextChildren,
148 - priority,
147 + renderExpirationTime,
148 );
149 return workInProgress.child;
150 }
@@ -181,15 +180,15 @@ module.exports = function<T, P, I, TI, PI, C, CX, PL>(
180 function completeWork(
181 current: Fiber | null,
182 workInProgress: Fiber,
184 - renderPriority: PriorityLevel,
183 + renderExpirationTime: ExpirationTime,
184 ): Fiber | null {
185 // Get the latest props.
186 let newProps = workInProgress.pendingProps;
187 if (newProps === null) {
188 newProps = workInProgress.memoizedProps;
189 } else if (
191 - workInProgress.pendingWorkPriority !== OffscreenPriority ||
192 - renderPriority === OffscreenPriority
190 + workInProgress.expirationTime !== Never ||
191 + renderExpirationTime === Never
192 ) {
193 // Reset the pending props, unless this was a down-prioritization.
194 workInProgress.pendingProps = null;
@@ -358,7 +357,11 @@ module.exports = function<T, P, I, TI, PI, C, CX, PL>(
357 return null;
358 }
359 case CoroutineComponent:
361 - return moveCoroutineToHandlerPhase(current, workInProgress);
360 + return moveCoroutineToHandlerPhase(
361 + current,
362 + workInProgress,
363 + renderExpirationTime,
364 + );
365 case CoroutineHandlerPhase:
366 // Reset the tag to now be a first phase coroutine.
367 workInProgress.tag = CoroutineComponent;
src/renderers/shared/fiber/ReactFiberExpirationTime.js new
+123
@@ -0,0 +1,123 @@
1 +/**
2 + * Copyright (c) 2013-present, Facebook, Inc.
3 + *
4 + * This source code is licensed under the MIT license found in the
5 + * LICENSE file in the root directory of this source tree.
6 + *
7 + * @providesModule ReactFiberExpirationTime
8 + * @flow
9 + */
10 +
11 +'use strict';
12 +
13 +import type {PriorityLevel} from 'ReactPriorityLevel';
14 +const {
15 + NoWork: NoWorkPriority,
16 + SynchronousPriority,
17 + TaskPriority,
18 + HighPriority,
19 + LowPriority,
20 + OffscreenPriority,
21 +} = require('ReactPriorityLevel');
22 +
23 +const invariant = require('fbjs/lib/invariant');
24 +
25 +// TODO: Use an opaque type once ESLint et al support the syntax
26 +export type ExpirationTime = number;
27 +
28 +const NoWork = 0;
29 +const Sync = 1;
30 +const Task = 2;
31 +const Never = 2147483647; // Max int32: Math.pow(2, 31) - 1
32 +
33 +const UNIT_SIZE = 10;
34 +const MAGIC_NUMBER_OFFSET = 3;
35 +
36 +exports.NoWork = NoWork;
37 +exports.Never = Never;
38 +
39 +// 1 unit of expiration time represents 10ms.
40 +function msToExpirationTime(ms: number): ExpirationTime {
41 + // Always add an offset so that we don't clash with the magic number for NoWork.
42 + return ((ms / UNIT_SIZE) | 0) + MAGIC_NUMBER_OFFSET;
43 +}
44 +exports.msToExpirationTime = msToExpirationTime;
45 +
46 +function ceiling(num: number, precision: number): number {
47 + return (((((num * precision) | 0) + 1) / precision) | 0) + 1;
48 +}
49 +
50 +function bucket(
51 + currentTime: ExpirationTime,
52 + expirationInMs: number,
53 + precisionInMs: number,
54 +): ExpirationTime {
55 + return ceiling(
56 + currentTime + expirationInMs / UNIT_SIZE,
57 + precisionInMs / UNIT_SIZE,
58 + );
59 +}
60 +
61 +// Given the current clock time and a priority level, returns an expiration time
62 +// that represents a point in the future by which some work should complete.
63 +// The lower the priority, the further out the expiration time. We use rounding
64 +// to batch like updates together. The further out the expiration time, the
65 +// more we want to batch, so we use a larger precision when rounding.
66 +function priorityToExpirationTime(
67 + currentTime: ExpirationTime,
68 + priorityLevel: PriorityLevel,
69 +): ExpirationTime {
70 + switch (priorityLevel) {
71 + case NoWork:
72 + return NoWorkPriority;
73 + case SynchronousPriority:
74 + return Sync;
75 + case TaskPriority:
76 + return Task;
77 + case HighPriority: {
78 + // Should complete within ~100ms. 120ms max.
79 + return bucket(currentTime, 100, 20);
80 + }
81 + case LowPriority: {
82 + // Should complete within ~1000ms. 1200ms max.
83 + return bucket(currentTime, 1000, 200);
84 + }
85 + case OffscreenPriority:
86 + return Never;
87 + default:
88 + invariant(
89 + false,
90 + 'Switch statement should be exhuastive. ' +
91 + 'This error is likely caused by a bug in React. Please file an issue.',
92 + );
93 + }
94 +}
95 +exports.priorityToExpirationTime = priorityToExpirationTime;
96 +
97 +// Given the current clock time and an expiration time, returns the
98 +// corresponding priority level. The more time has advanced, the higher the
99 +// priority level.
100 +function expirationTimeToPriorityLevel(
101 + currentTime: ExpirationTime,
102 + expirationTime: ExpirationTime,
103 +): PriorityLevel {
104 + // First check for magic values
105 + switch (expirationTime) {
106 + case NoWorkPriority:
107 + return NoWork;
108 + case Sync:
109 + return SynchronousPriority;
110 + case Task:
111 + return TaskPriority;
112 + case Never:
113 + return OffscreenPriority;
114 + default:
115 + break;
116 + }
117 + if (expirationTime <= currentTime) {
118 + return TaskPriority;
119 + }
120 + // TODO: We don't currently distinguish between high and low priority.
121 + return LowPriority;
122 +}
123 +exports.expirationTimeToPriorityLevel = expirationTimeToPriorityLevel;
src/renderers/shared/fiber/ReactFiberReconciler.js
+18 -2
@@ -122,6 +122,8 @@ export type HostConfig<T, P, I, TI, PI, C, CX, PL> = {
122 prepareForCommit(): void,
123 resetAfterCommit(): void,
124
125 + now(): number,
126 +
127 // Optional hydration
128 canHydrateInstance?: (instance: I | TI, type: T, props: P) => boolean,
129 canHydrateTextInstance?: (instance: I | TI, text: string) => boolean,
@@ -235,6 +237,8 @@ module.exports = function<T, P, I, TI, PI, C, CX, PL>(
237 var {
238 scheduleUpdate,
239 getPriorityContext,
240 + getExpirationTimeForPriority,
241 + recalculateCurrentTime,
242 batchedUpdates,
243 unbatchedUpdates,
244 flushSync,
@@ -274,6 +278,11 @@ module.exports = function<T, P, I, TI, PI, C, CX, PL>(
278 element.type.prototype != null &&
279 (element.type.prototype: any).unstable_isAsyncReactComponent === true;
280 const priorityLevel = getPriorityContext(current, forceAsync);
281 + const currentTime = recalculateCurrentTime();
282 + const expirationTime = getExpirationTimeForPriority(
283 + currentTime,
284 + priorityLevel,
285 + );
286 const nextState = {element};
287 callback = callback === undefined ? null : callback;
288 if (__DEV__) {
@@ -284,8 +293,15 @@ module.exports = function<T, P, I, TI, PI, C, CX, PL>(
293 callback,
294 );
295 }
287 - addTopLevelUpdate(current, nextState, callback, priorityLevel);
288 - scheduleUpdate(current, priorityLevel);
296 + addTopLevelUpdate(
297 + current,
298 + nextState,
299 + callback,
300 + priorityLevel,
301 + expirationTime,
302 + currentTime,
303 + );
304 + scheduleUpdate(current, expirationTime);
305 }
306
307 return {
src/renderers/shared/fiber/ReactFiberScheduler.js
+182 -114
@@ -15,6 +15,7 @@ import type {FiberRoot} from 'ReactFiberRoot';
15 import type {HostConfig, Deadline} from 'ReactFiberReconciler';
16 import type {PriorityLevel} from 'ReactPriorityLevel';
17 import type {HydrationContext} from 'ReactFiberHydrationContext';
18 +import type {ExpirationTime} from 'ReactFiberExpirationTime';
19
20 export type CapturedError = {
21 componentName: ?string,
@@ -50,7 +51,7 @@ var ReactFiberHydrationContext = require('ReactFiberHydrationContext');
51 var {ReactCurrentOwner} = require('ReactGlobalSharedState');
52 var getComponentName = require('getComponentName');
53
53 -var {createWorkInProgress, largerPriority} = require('ReactFiber');
54 +var {createWorkInProgress} = require('ReactFiber');
55 var {onCommitRoot} = require('ReactFiberDevToolsHook');
56
57 var {
@@ -62,6 +63,13 @@ var {
63 OffscreenPriority,
64 } = require('ReactPriorityLevel');
65
66 +var {
67 + Never,
68 + msToExpirationTime,
69 + priorityToExpirationTime,
70 + expirationTimeToPriorityLevel,
71 +} = require('ReactFiberExpirationTime');
72 +
73 var {AsyncUpdates} = require('ReactTypeOfInternalContext');
74
75 var {
@@ -83,7 +91,7 @@ var {
91 ClassComponent,
92 } = require('ReactTypeOfWork');
93
86 -var {getUpdatePriority} = require('ReactFiberUpdateQueue');
94 +var {getUpdateExpirationTime} = require('ReactFiberUpdateQueue');
95
96 var {resetContext} = require('ReactFiberContext');
97
@@ -166,6 +174,8 @@ module.exports = function<T, P, I, TI, PI, C, CX, PL>(
174 hydrationContext,
175 scheduleUpdate,
176 getPriorityContext,
177 + recalculateCurrentTime,
178 + getExpirationTimeForPriority,
179 );
180 const {completeWork} = ReactFiberCompleteWork(
181 config,
@@ -181,16 +191,19 @@ module.exports = function<T, P, I, TI, PI, C, CX, PL>(
191 commitDetachRef,
192 } = ReactFiberCommitWork(config, captureError);
193 const {
194 + now,
195 scheduleDeferredCallback,
196 useSyncScheduling,
197 prepareForCommit,
198 resetAfterCommit,
199 } = config;
200
201 + // Represents the current time in ms.
202 + const startTime = now();
203 + let mostRecentCurrentTime: ExpirationTime = msToExpirationTime(0);
204 +
205 // The priority level to use when scheduling an update. We use NoWork to
206 // represent the default priority.
192 - // TODO: Should we change this to an array instead of using the call stack?
193 - // Might be less confusing.
207 let priorityContext: PriorityLevel = NoWork;
208
209 // Keeps track of whether we're currently in a work loop.
@@ -208,7 +221,8 @@ module.exports = function<T, P, I, TI, PI, C, CX, PL>(
221
222 // The next work in progress fiber that we're currently working on.
223 let nextUnitOfWork: Fiber | null = null;
211 - let nextPriorityLevel: PriorityLevel = NoWork;
224 + // The time at which we're currently rendering work.
225 + let nextRenderExpirationTime: ExpirationTime = NoWork;
226
227 // The next fiber with an effect that we're currently committing.
228 let nextEffect: Fiber | null = null;
@@ -251,14 +265,11 @@ module.exports = function<T, P, I, TI, PI, C, CX, PL>(
265 resetHostContainer();
266 }
267
254 - // resetNextUnitOfWork mutates the current priority context. It is reset after
255 - // after the workLoop exits, so never call resetNextUnitOfWork from outside
256 - // the work loop.
268 function resetNextUnitOfWork() {
269 // Clear out roots with no more work on them, or if they have uncaught errors
270 while (
271 nextScheduledRoot !== null &&
261 - nextScheduledRoot.current.pendingWorkPriority === NoWork
272 + nextScheduledRoot.current.expirationTime === NoWork
273 ) {
274 // Unschedule this root.
275 nextScheduledRoot.isScheduled = false;
@@ -270,7 +281,7 @@ module.exports = function<T, P, I, TI, PI, C, CX, PL>(
281 if (nextScheduledRoot === lastScheduledRoot) {
282 nextScheduledRoot = null;
283 lastScheduledRoot = null;
273 - nextPriorityLevel = NoWork;
284 + nextRenderExpirationTime = NoWork;
285 return null;
286 }
287 // Continue with the next root.
@@ -279,22 +290,22 @@ module.exports = function<T, P, I, TI, PI, C, CX, PL>(
290 }
291
292 let root = nextScheduledRoot;
282 - let highestPriorityRoot = null;
283 - let highestPriorityLevel = NoWork;
293 + let earliestExpirationRoot = null;
294 + let earliestExpirationTime = NoWork;
295 while (root !== null) {
296 if (
286 - root.current.pendingWorkPriority !== NoWork &&
287 - (highestPriorityLevel === NoWork ||
288 - highestPriorityLevel > root.current.pendingWorkPriority)
297 + root.current.expirationTime !== NoWork &&
298 + (earliestExpirationTime === NoWork ||
299 + earliestExpirationTime > root.current.expirationTime)
300 ) {
290 - highestPriorityLevel = root.current.pendingWorkPriority;
291 - highestPriorityRoot = root;
301 + earliestExpirationTime = root.current.expirationTime;
302 + earliestExpirationRoot = root;
303 }
304 // We didn't find anything to do in this root, so let's try the next one.
305 root = root.nextScheduledRoot;
306 }
296 - if (highestPriorityRoot !== null) {
297 - nextPriorityLevel = highestPriorityLevel;
307 + if (earliestExpirationRoot !== null) {
308 + nextRenderExpirationTime = earliestExpirationTime;
309 // Before we start any new work, let's make sure that we have a fresh
310 // stack to work from.
311 // TODO: This call is buried a bit too deep. It would be nice to have
@@ -303,18 +314,18 @@ module.exports = function<T, P, I, TI, PI, C, CX, PL>(
314 resetContextStack();
315
316 nextUnitOfWork = createWorkInProgress(
306 - highestPriorityRoot.current,
307 - highestPriorityLevel,
317 + earliestExpirationRoot.current,
318 + earliestExpirationTime,
319 );
309 - if (highestPriorityRoot !== nextRenderedTree) {
320 + if (earliestExpirationRoot !== nextRenderedTree) {
321 // We've switched trees. Reset the nested update counter.
322 nestedUpdateCount = 0;
312 - nextRenderedTree = highestPriorityRoot;
323 + nextRenderedTree = earliestExpirationRoot;
324 }
325 return;
326 }
327
317 - nextPriorityLevel = NoWork;
328 + nextRenderExpirationTime = NoWork;
329 nextUnitOfWork = null;
330 nextRenderedTree = null;
331 return;
@@ -392,7 +403,6 @@ module.exports = function<T, P, I, TI, PI, C, CX, PL>(
403 while (nextEffect !== null) {
404 const effectTag = nextEffect.effectTag;
405
395 - // Use Task priority for lifecycle updates
406 if (effectTag & (Update | Callback)) {
407 if (__DEV__) {
408 recordEffect();
@@ -446,10 +456,7 @@ module.exports = function<T, P, I, TI, PI, C, CX, PL>(
456 'in React. Please file an issue.',
457 );
458
449 - if (
450 - nextPriorityLevel === SynchronousPriority ||
451 - nextPriorityLevel === TaskPriority
452 - ) {
459 + if (nextRenderExpirationTime <= mostRecentCurrentTime) {
460 // Keep track of the number of iterations to prevent an infinite
461 // update loop.
462 nestedUpdateCount++;
@@ -583,35 +590,39 @@ module.exports = function<T, P, I, TI, PI, C, CX, PL>(
590 commitPhaseBoundaries = null;
591 }
592
586 - // This tree is done. Reset the unit of work pointer to the next highest
587 - // priority root. If there's no more work left, the pointer is set to null.
593 + // This tree is done. Reset the unit of work pointer to the root that
594 + // expires soonest. If there's no work left, the pointer is set to null.
595 resetNextUnitOfWork();
596 }
597
591 - function resetWorkPriority(
598 + function resetExpirationTime(
599 workInProgress: Fiber,
593 - renderPriority: PriorityLevel,
600 + renderTime: ExpirationTime,
601 ) {
595 - if (
596 - workInProgress.pendingWorkPriority !== NoWork &&
597 - workInProgress.pendingWorkPriority > renderPriority
598 - ) {
599 - // This was a down-prioritization. Don't bubble priority from children.
602 + if (renderTime !== Never && workInProgress.expirationTime === Never) {
603 + // The children of this component are hidden. Don't bubble their
604 + // expiration times.
605 return;
606 }
607
603 - // Check for pending update priority.
604 - let newPriority = getUpdatePriority(workInProgress);
608 + // Check for pending updates.
609 + let newExpirationTime = getUpdateExpirationTime(workInProgress);
610
611 // TODO: Coroutines need to visit stateNode
612
613 + // Bubble up the earliest expiration time.
614 let child = workInProgress.child;
615 while (child !== null) {
610 - // Ensure that remaining work priority bubbles up.
611 - newPriority = largerPriority(newPriority, child.pendingWorkPriority);
616 + if (
617 + child.expirationTime !== NoWork &&
618 + (newExpirationTime === NoWork ||
619 + newExpirationTime > child.expirationTime)
620 + ) {
621 + newExpirationTime = child.expirationTime;
622 + }
623 child = child.sibling;
624 }
614 - workInProgress.pendingWorkPriority = newPriority;
625 + workInProgress.expirationTime = newExpirationTime;
626 }
627
628 function completeUnitOfWork(workInProgress: Fiber): Fiber | null {
@@ -624,7 +635,11 @@ module.exports = function<T, P, I, TI, PI, C, CX, PL>(
635 if (__DEV__) {
636 ReactDebugCurrentFiber.setCurrentFiber(workInProgress);
637 }
627 - const next = completeWork(current, workInProgress, nextPriorityLevel);
638 + const next = completeWork(
639 + current,
640 + workInProgress,
641 + nextRenderExpirationTime,
642 + );
643 if (__DEV__) {
644 ReactDebugCurrentFiber.resetCurrentFiber();
645 }
@@ -632,7 +647,7 @@ module.exports = function<T, P, I, TI, PI, C, CX, PL>(
647 const returnFiber = workInProgress.return;
648 const siblingFiber = workInProgress.sibling;
649
635 - resetWorkPriority(workInProgress, nextPriorityLevel);
650 + resetExpirationTime(workInProgress, nextRenderExpirationTime);
651
652 if (next !== null) {
653 if (__DEV__) {
@@ -720,7 +735,7 @@ module.exports = function<T, P, I, TI, PI, C, CX, PL>(
735 startWorkTimer(workInProgress);
736 ReactDebugCurrentFiber.setCurrentFiber(workInProgress);
737 }
723 - let next = beginWork(current, workInProgress, nextPriorityLevel);
738 + let next = beginWork(current, workInProgress, nextRenderExpirationTime);
739 if (__DEV__) {
740 ReactDebugCurrentFiber.resetCurrentFiber();
741 }
@@ -750,7 +765,11 @@ module.exports = function<T, P, I, TI, PI, C, CX, PL>(
765 startWorkTimer(workInProgress);
766 ReactDebugCurrentFiber.setCurrentFiber(workInProgress);
767 }
753 - let next = beginFailedWork(current, workInProgress, nextPriorityLevel);
768 + let next = beginFailedWork(
769 + current,
770 + workInProgress,
771 + nextRenderExpirationTime,
772 + );
773 if (__DEV__) {
774 ReactDebugCurrentFiber.resetCurrentFiber();
775 }
@@ -785,7 +804,8 @@ module.exports = function<T, P, I, TI, PI, C, CX, PL>(
804 if (
805 capturedErrors !== null &&
806 capturedErrors.size > 0 &&
788 - nextPriorityLevel === TaskPriority
807 + nextRenderExpirationTime !== NoWork &&
808 + nextRenderExpirationTime <= mostRecentCurrentTime
809 ) {
810 while (nextUnitOfWork !== null) {
811 if (hasCapturedError(nextUnitOfWork)) {
@@ -801,14 +821,12 @@ module.exports = function<T, P, I, TI, PI, C, CX, PL>(
821 'a bug in React. Please file an issue.',
822 );
823 // We just completed a root. Commit it now.
804 - priorityContext = TaskPriority;
824 commitAllWork(pendingCommit);
806 - priorityContext = nextPriorityLevel;
807 -
825 if (
826 capturedErrors === null ||
827 capturedErrors.size === 0 ||
811 - nextPriorityLevel !== TaskPriority
828 + nextRenderExpirationTime === NoWork ||
829 + nextRenderExpirationTime > mostRecentCurrentTime
830 ) {
831 // There are no more unhandled errors. We can exit this special
832 // work loop. If there's still additional work, we'll perform it
@@ -822,28 +840,26 @@ module.exports = function<T, P, I, TI, PI, C, CX, PL>(
840 }
841
842 function workLoop(
825 - minPriorityLevel: PriorityLevel,
843 + minExpirationTime: ExpirationTime,
844 deadline: Deadline | null,
845 ) {
846 if (pendingCommit !== null) {
829 - priorityContext = TaskPriority;
847 commitAllWork(pendingCommit);
848 handleCommitPhaseErrors();
849 } else if (nextUnitOfWork === null) {
850 resetNextUnitOfWork();
851 }
852
836 - if (nextPriorityLevel === NoWork || nextPriorityLevel > minPriorityLevel) {
853 + if (
854 + nextRenderExpirationTime === NoWork ||
855 + nextRenderExpirationTime > minExpirationTime
856 + ) {
857 return;
858 }
859
840 - // During the render phase, updates should have the same priority at which
841 - // we're rendering.
842 - priorityContext = nextPriorityLevel;
843 -
860 loop: do {
845 - if (nextPriorityLevel <= TaskPriority) {
846 - // Flush all synchronous and task work.
861 + if (nextRenderExpirationTime <= mostRecentCurrentTime) {
862 + // Flush all expired work.
863 while (nextUnitOfWork !== null) {
864 nextUnitOfWork = performUnitOfWork(nextUnitOfWork);
865 if (nextUnitOfWork === null) {
@@ -853,24 +869,22 @@ module.exports = function<T, P, I, TI, PI, C, CX, PL>(
869 'a bug in React. Please file an issue.',
870 );
871 // We just completed a root. Commit it now.
856 - priorityContext = TaskPriority;
872 commitAllWork(pendingCommit);
858 - priorityContext = nextPriorityLevel;
873 // Clear any errors that were scheduled during the commit phase.
874 handleCommitPhaseErrors();
861 - // The priority level may have changed. Check again.
875 + // The render time may have changed. Check again.
876 if (
863 - nextPriorityLevel === NoWork ||
864 - nextPriorityLevel > minPriorityLevel ||
865 - nextPriorityLevel > TaskPriority
877 + nextRenderExpirationTime === NoWork ||
878 + nextRenderExpirationTime > minExpirationTime ||
879 + nextRenderExpirationTime > mostRecentCurrentTime
880 ) {
867 - // The priority level does not match.
881 + // We've completed all the expired work.
882 break;
883 }
884 }
885 }
886 } else if (deadline !== null) {
873 - // Flush asynchronous work until the deadline expires.
887 + // Flush asynchronous work until the deadline runs out of time.
888 while (nextUnitOfWork !== null && !deadlineHasExpired) {
889 if (deadline.timeRemaining() > timeHeuristicForUnitOfWork) {
890 nextUnitOfWork = performUnitOfWork(nextUnitOfWork);
@@ -887,18 +901,16 @@ module.exports = function<T, P, I, TI, PI, C, CX, PL>(
901 // We just completed a root. If we have time, commit it now.
902 // Otherwise, we'll commit it in the next frame.
903 if (deadline.timeRemaining() > timeHeuristicForUnitOfWork) {
890 - priorityContext = TaskPriority;
904 commitAllWork(pendingCommit);
892 - priorityContext = nextPriorityLevel;
905 // Clear any errors that were scheduled during the commit phase.
906 handleCommitPhaseErrors();
895 - // The priority level may have changed. Check again.
907 + // The render time may have changed. Check again.
908 if (
897 - nextPriorityLevel === NoWork ||
898 - nextPriorityLevel > minPriorityLevel ||
899 - nextPriorityLevel < HighPriority
909 + nextRenderExpirationTime === NoWork ||
910 + nextRenderExpirationTime > minExpirationTime ||
911 + nextRenderExpirationTime <= mostRecentCurrentTime
912 ) {
901 - // The priority level does not match.
913 + // We've completed all the async work.
914 break;
915 }
916 } else {
@@ -913,12 +925,16 @@ module.exports = function<T, P, I, TI, PI, C, CX, PL>(
925
926 // There might be work left. Depending on the priority, we should
927 // either perform it now or schedule a callback to perform it later.
916 - switch (nextPriorityLevel) {
928 + const currentTime = recalculateCurrentTime();
929 + switch (expirationTimeToPriorityLevel(
930 + currentTime,
931 + nextRenderExpirationTime,
932 + )) {
933 case SynchronousPriority:
934 case TaskPriority:
935 // We have remaining synchronous or task work. Keep performing it,
936 // regardless of whether we're inside a callback.
921 - if (nextPriorityLevel <= minPriorityLevel) {
937 + if (nextRenderExpirationTime <= minExpirationTime) {
938 continue loop;
939 }
940 break loop;
@@ -932,7 +948,10 @@ module.exports = function<T, P, I, TI, PI, C, CX, PL>(
948 break loop;
949 }
950 // We are inside a callback.
935 - if (!deadlineHasExpired && nextPriorityLevel <= minPriorityLevel) {
951 + if (
952 + !deadlineHasExpired &&
953 + nextRenderExpirationTime <= minExpirationTime
954 + ) {
955 // We still have time. Keep working.
956 continue loop;
957 }
@@ -954,7 +973,7 @@ module.exports = function<T, P, I, TI, PI, C, CX, PL>(
973 function performWorkCatchBlock(
974 failedWork: Fiber,
975 boundary: Fiber,
957 - minPriorityLevel: PriorityLevel,
976 + minExpirationTime: ExpirationTime,
977 deadline: Deadline | null,
978 ) {
979 // We're going to restart the error boundary that captured the error.
@@ -970,7 +989,7 @@ module.exports = function<T, P, I, TI, PI, C, CX, PL>(
989 nextUnitOfWork = performFailedUnitOfWork(boundary);
990
991 // Continue working.
973 - workLoop(minPriorityLevel, deadline);
992 + workLoop(minExpirationTime, deadline);
993 }
994
995 function performWork(
@@ -988,21 +1007,32 @@ module.exports = function<T, P, I, TI, PI, C, CX, PL>(
1007 );
1008 isPerformingWork = true;
1009
991 - // The priority context changes during the render phase. We'll need to
992 - // reset it at the end.
1010 + // Updates that occur during the commit phase should have task priority
1011 + // by default. (Render phase updates are special; getPriorityContext
1012 + // accounts for their behavior.)
1013 const previousPriorityContext = priorityContext;
1014 + priorityContext = TaskPriority;
1015 +
1016 + // Read the current time from the host environment.
1017 + const currentTime = recalculateCurrentTime();
1018 + const minExpirationTime = getExpirationTimeForPriority(
1019 + currentTime,
1020 + minPriorityLevel,
1021 + );
1022 +
1023 + nestedUpdateCount = 0;
1024
1025 let didError = false;
1026 let error = null;
1027 if (__DEV__) {
998 - invokeGuardedCallback(null, workLoop, null, minPriorityLevel, deadline);
1028 + invokeGuardedCallback(null, workLoop, null, minExpirationTime, deadline);
1029 if (hasCaughtError()) {
1030 didError = true;
1031 error = clearCaughtError();
1032 }
1033 } else {
1034 try {
1005 - workLoop(minPriorityLevel, deadline);
1035 + workLoop(minExpirationTime, deadline);
1036 } catch (e) {
1037 didError = true;
1038 error = e;
@@ -1049,7 +1079,7 @@ module.exports = function<T, P, I, TI, PI, C, CX, PL>(
1079 null,
1080 failedWork,
1081 boundary,
1052 - minPriorityLevel,
1082 + minExpirationTime,
1083 deadline,
1084 );
1085 if (hasCaughtError()) {
@@ -1062,7 +1092,7 @@ module.exports = function<T, P, I, TI, PI, C, CX, PL>(
1092 performWorkCatchBlock(
1093 failedWork,
1094 boundary,
1065 - minPriorityLevel,
1095 + minExpirationTime,
1096 deadline,
1097 );
1098 error = null;
@@ -1076,19 +1106,21 @@ module.exports = function<T, P, I, TI, PI, C, CX, PL>(
1106 break;
1107 }
1108
1079 - // Reset the priority context to its previous value.
1080 - priorityContext = previousPriorityContext;
1081 -
1109 // If we're inside a callback, set this to false, since we just flushed it.
1110 if (deadline !== null) {
1111 isCallbackScheduled = false;
1112 }
1113 // If there's remaining async work, make sure we schedule another callback.
1087 - if (nextPriorityLevel > TaskPriority && !isCallbackScheduled) {
1114 + if (
1115 + nextRenderExpirationTime > mostRecentCurrentTime &&
1116 + !isCallbackScheduled
1117 + ) {
1118 scheduleDeferredCallback(performDeferredWork);
1119 isCallbackScheduled = true;
1120 }
1121
1122 + priorityContext = previousPriorityContext;
1123 +
1124 const errorToThrow = firstUncaughtError;
1125
1126 // We're done performing work. Time to clean up.
@@ -1353,8 +1385,8 @@ module.exports = function<T, P, I, TI, PI, C, CX, PL>(
1385 }
1386 }
1387
1356 - function scheduleRoot(root: FiberRoot, priorityLevel: PriorityLevel) {
1357 - if (priorityLevel === NoWork) {
1388 + function scheduleRoot(root: FiberRoot, expirationTime: ExpirationTime) {
1389 + if (expirationTime === NoWork) {
1390 return;
1391 }
1392
@@ -1372,13 +1404,13 @@ module.exports = function<T, P, I, TI, PI, C, CX, PL>(
1404 }
1405 }
1406
1375 - function scheduleUpdate(fiber: Fiber, priorityLevel: PriorityLevel) {
1376 - return scheduleUpdateImpl(fiber, priorityLevel, false);
1407 + function scheduleUpdate(fiber: Fiber, expirationTime: ExpirationTime) {
1408 + return scheduleUpdateImpl(fiber, expirationTime, false);
1409 }
1410
1411 function scheduleUpdateImpl(
1412 fiber: Fiber,
1381 - priorityLevel: PriorityLevel,
1413 + expirationTime: ExpirationTime,
1414 isErrorRecovery: boolean,
1415 ) {
1416 if (__DEV__) {
@@ -1396,7 +1428,7 @@ module.exports = function<T, P, I, TI, PI, C, CX, PL>(
1428 );
1429 }
1430
1399 - if (!isPerformingWork && priorityLevel <= nextPriorityLevel) {
1431 + if (!isPerformingWork && expirationTime <= nextRenderExpirationTime) {
1432 // We must reset the current unit of work pointer so that we restart the
1433 // search from the root during the next tick, in case there is now higher
1434 // priority work somewhere earlier than before.
@@ -1413,36 +1445,40 @@ module.exports = function<T, P, I, TI, PI, C, CX, PL>(
1445 let node = fiber;
1446 let shouldContinue = true;
1447 while (node !== null && shouldContinue) {
1416 - // Walk the parent path to the root and update each node's priority. Once
1417 - // we reach a node whose priority matches (and whose alternate's priority
1418 - // matches) we can exit safely knowing that the rest of the path is correct.
1448 + // Walk the parent path to the root and update each node's expiration
1449 + // time. Once we reach a node whose expiration matches (and whose
1450 + // alternate's expiration matches) we can exit safely knowing that the
1451 + // rest of the path is correct.
1452 shouldContinue = false;
1453 if (
1421 - node.pendingWorkPriority === NoWork ||
1422 - node.pendingWorkPriority > priorityLevel
1454 + node.expirationTime === NoWork ||
1455 + node.expirationTime > expirationTime
1456 ) {
1424 - // Priority did not match. Update and keep going.
1457 + // Expiration time did not match. Update and keep going.
1458 shouldContinue = true;
1426 - node.pendingWorkPriority = priorityLevel;
1459 + node.expirationTime = expirationTime;
1460 }
1461 if (node.alternate !== null) {
1462 if (
1430 - node.alternate.pendingWorkPriority === NoWork ||
1431 - node.alternate.pendingWorkPriority > priorityLevel
1463 + node.alternate.expirationTime === NoWork ||
1464 + node.alternate.expirationTime > expirationTime
1465 ) {
1433 - // Priority did not match. Update and keep going.
1466 + // Expiration time did not match. Update and keep going.
1467 shouldContinue = true;
1435 - node.alternate.pendingWorkPriority = priorityLevel;
1468 + node.alternate.expirationTime = expirationTime;
1469 }
1470 }
1471 if (node.return === null) {
1472 if (node.tag === HostRoot) {
1473 const root: FiberRoot = (node.stateNode: any);
1441 - scheduleRoot(root, priorityLevel);
1474 + scheduleRoot(root, expirationTime);
1475 if (!isPerformingWork) {
1476 + const priorityLevel = expirationTimeToPriorityLevel(
1477 + mostRecentCurrentTime,
1478 + expirationTime,
1479 + );
1480 switch (priorityLevel) {
1481 case SynchronousPriority:
1445 - // Perform this update now.
1482 if (isUnbatchingUpdates) {
1483 // We're inside unbatchedUpdates, which is inside either
1484 // batchedUpdates or a lifecycle. We should only flush
@@ -1457,11 +1493,12 @@ module.exports = function<T, P, I, TI, PI, C, CX, PL>(
1493 invariant(
1494 isBatchingUpdates,
1495 'Task updates can only be scheduled as a nested update or ' +
1460 - 'inside batchedUpdates.',
1496 + 'inside batchedUpdates. This error is likely caused by a ' +
1497 + 'bug in React. Please file an issue.',
1498 );
1499 break;
1500 default:
1464 - // Schedule a callback to perform the work later.
1501 + // This update is async. Schedule a callback.
1502 if (!isCallbackScheduled) {
1503 scheduleDeferredCallback(performDeferredWork);
1504 isCallbackScheduled = true;
@@ -1484,7 +1521,13 @@ module.exports = function<T, P, I, TI, PI, C, CX, PL>(
1521 function getPriorityContext(
1522 fiber: Fiber,
1523 forceAsync: boolean,
1487 - ): PriorityLevel {
1524 + ): PriorityLevel | null {
1525 + if (isPerformingWork && !isCommitting) {
1526 + // Updates during the render phase should expire at the same time as
1527 + // the work that is being rendered. Return null to indicate.
1528 + return null;
1529 + }
1530 +
1531 let priorityLevel = priorityContext;
1532 if (priorityLevel === NoWork) {
1533 if (
@@ -1509,8 +1552,31 @@ module.exports = function<T, P, I, TI, PI, C, CX, PL>(
1552 return priorityLevel;
1553 }
1554
1555 + function getExpirationTimeForPriority(
1556 + currentTime: ExpirationTime,
1557 + priorityLevel: PriorityLevel | null,
1558 + ): ExpirationTime {
1559 + if (priorityLevel === null) {
1560 + // A priorityLevel of null indicates that this update should expire at
1561 + // the same time as whatever is currently being rendered.
1562 + return nextRenderExpirationTime;
1563 + }
1564 + return priorityToExpirationTime(currentTime, priorityLevel);
1565 + }
1566 +
1567 function scheduleErrorRecovery(fiber: Fiber) {
1513 - scheduleUpdateImpl(fiber, TaskPriority, true);
1568 + const taskTime = getExpirationTimeForPriority(
1569 + mostRecentCurrentTime,
1570 + TaskPriority,
1571 + );
1572 + scheduleUpdateImpl(fiber, taskTime, true);
1573 + }
1574 +
1575 + function recalculateCurrentTime(): ExpirationTime {
1576 + // Subtract initial time so it fits inside 32bits
1577 + const ms = now() - startTime;
1578 + mostRecentCurrentTime = msToExpirationTime(ms);
1579 + return mostRecentCurrentTime;
1580 }
1581
1582 function batchedUpdates<A, R>(fn: (a: A) => R, a: A): R {
@@ -1575,6 +1641,8 @@ module.exports = function<T, P, I, TI, PI, C, CX, PL>(
1641 return {
1642 scheduleUpdate: scheduleUpdate,
1643 getPriorityContext: getPriorityContext,
1644 + recalculateCurrentTime: recalculateCurrentTime,
1645 + getExpirationTimeForPriority: getExpirationTimeForPriority,
1646 batchedUpdates: batchedUpdates,
1647 unbatchedUpdates: unbatchedUpdates,
1648 flushSync: flushSync,
src/renderers/shared/fiber/ReactFiberUpdateQueue.js
+77 -52
@@ -12,14 +12,11 @@
12
13 import type {Fiber} from 'ReactFiber';
14 import type {PriorityLevel} from 'ReactPriorityLevel';
15 +import type {ExpirationTime} from 'ReactFiberExpirationTime';
16
17 const {Callback: CallbackEffect} = require('ReactTypeOfSideEffect');
18
18 -const {
19 - NoWork,
20 - SynchronousPriority,
21 - TaskPriority,
22 -} = require('ReactPriorityLevel');
19 +const {NoWork} = require('ReactFiberExpirationTime');
20
21 const {ClassComponent, HostRoot} = require('ReactTypeOfWork');
22
@@ -35,8 +32,9 @@ type PartialState<State, Props> =
32 // Callbacks are not validated until invocation
33 type Callback = mixed;
34
38 -type Update = {
39 - priorityLevel: PriorityLevel,
35 +export type Update = {
36 + priorityLevel: PriorityLevel | null,
37 + expirationTime: ExpirationTime,
38 partialState: PartialState<any, any>,
39 callback: Callback | null,
40 isReplace: boolean,
@@ -69,25 +67,6 @@ export type UpdateQueue = {
67 let _queue1;
68 let _queue2;
69
72 -function comparePriority(a: PriorityLevel, b: PriorityLevel): number {
73 - // When comparing update priorities, treat sync and Task work as equal.
74 - // TODO: Could we avoid the need for this by always coercing sync priority
75 - // to Task when scheduling an update?
76 - if (
77 - (a === TaskPriority || a === SynchronousPriority) &&
78 - (b === TaskPriority || b === SynchronousPriority)
79 - ) {
80 - return 0;
81 - }
82 - if (a === NoWork && b !== NoWork) {
83 - return -255;
84 - }
85 - if (a !== NoWork && b === NoWork) {
86 - return 255;
87 - }
88 - return a - b;
89 -}
90 -
70 function createUpdateQueue(): UpdateQueue {
71 const queue: UpdateQueue = {
72 first: null,
@@ -104,6 +83,7 @@ function createUpdateQueue(): UpdateQueue {
83 function cloneUpdate(update: Update): Update {
84 return {
85 priorityLevel: update.priorityLevel,
86 + expirationTime: update.expirationTime,
87 partialState: update.partialState,
88 callback: update.callback,
89 isReplace: update.isReplace,
@@ -113,14 +93,31 @@ function cloneUpdate(update: Update): Update {
93 };
94 }
95
96 +const COALESCENCE_THRESHOLD: ExpirationTime = 10;
97 +
98 function insertUpdateIntoQueue(
99 queue: UpdateQueue,
100 update: Update,
101 insertAfter: Update | null,
102 insertBefore: Update | null,
103 + currentTime: ExpirationTime,
104 ) {
105 if (insertAfter !== null) {
106 insertAfter.next = update;
107 + // If we receive multiple updates to the same fiber at the same priority
108 + // level, we coalesce them by assigning the same expiration time, so that
109 + // they all flush at the same time. Because this causes an interruption, it
110 + // could lead to starvation, so we stop coalescing once the time until the
111 + // expiration time reaches a certain threshold.
112 + if (
113 + insertAfter !== null &&
114 + insertAfter.priorityLevel === update.priorityLevel
115 + ) {
116 + const coalescedTime = insertAfter.expirationTime;
117 + if (coalescedTime - currentTime > COALESCENCE_THRESHOLD) {
118 + update.expirationTime = coalescedTime;
119 + }
120 + }
121 } else {
122 // This is the first item in the queue.
123 update.next = queue.first;
@@ -138,13 +135,10 @@ function insertUpdateIntoQueue(
135 // Returns the update after which the incoming update should be inserted into
136 // the queue, or null if it should be inserted at beginning.
137 function findInsertionPosition(queue, update): Update | null {
141 - const priorityLevel = update.priorityLevel;
138 + const expirationTime = update.expirationTime;
139 let insertAfter = null;
140 let insertBefore = null;
144 - if (
145 - queue.last !== null &&
146 - comparePriority(queue.last.priorityLevel, priorityLevel) <= 0
147 - ) {
141 + if (queue.last !== null && queue.last.expirationTime <= expirationTime) {
142 // Fast path for the common case where the update should be inserted at
143 // the end of the queue.
144 insertAfter = queue.last;
@@ -152,7 +146,7 @@ function findInsertionPosition(queue, update): Update | null {
146 insertBefore = queue.first;
147 while (
148 insertBefore !== null &&
155 - comparePriority(insertBefore.priorityLevel, priorityLevel) <= 0
149 + insertBefore.expirationTime <= expirationTime
150 ) {
151 insertAfter = insertBefore;
152 insertBefore = insertBefore.next;
@@ -213,7 +207,11 @@ function ensureUpdateQueues(fiber: Fiber) {
207 // we shouldn't make a copy.
208 //
209 // If the update is cloned, it returns the cloned update.
216 -function insertUpdate(fiber: Fiber, update: Update): Update | null {
210 +function insertUpdate(
211 + fiber: Fiber,
212 + update: Update,
213 + currentTime: ExpirationTime,
214 +): Update | null {
215 // We'll have at least one and at most two distinct update queues.
216 ensureUpdateQueues(fiber);
217 const queue1 = _queue1;
@@ -240,7 +238,13 @@ function insertUpdate(fiber: Fiber, update: Update): Update | null {
238
239 if (queue2 === null) {
240 // If there's no alternate queue, there's nothing else to do but insert.
243 - insertUpdateIntoQueue(queue1, update, insertAfter1, insertBefore1);
241 + insertUpdateIntoQueue(
242 + queue1,
243 + update,
244 + insertAfter1,
245 + insertBefore1,
246 + currentTime,
247 + );
248 return null;
249 }
250
@@ -252,7 +256,13 @@ function insertUpdate(fiber: Fiber, update: Update): Update | null {
256
257 // Now we can insert into the first queue. This must come after finding both
258 // insertion positions because it mutates the list.
255 - insertUpdateIntoQueue(queue1, update, insertAfter1, insertBefore1);
259 + insertUpdateIntoQueue(
260 + queue1,
261 + update,
262 + insertAfter1,
263 + insertBefore1,
264 + currentTime,
265 + );
266
267 // See if the insertion positions are equal. Be careful to only compare
268 // non-null values.
@@ -275,7 +285,13 @@ function insertUpdate(fiber: Fiber, update: Update): Update | null {
285 // The insertion positions are different, so we need to clone the update and
286 // insert the clone into the alternate queue.
287 const update2 = cloneUpdate(update);
278 - insertUpdateIntoQueue(queue2, update2, insertAfter2, insertBefore2);
288 + insertUpdateIntoQueue(
289 + queue2,
290 + update2,
291 + insertAfter2,
292 + insertBefore2,
293 + currentTime,
294 + );
295 return update2;
296 }
297 }
@@ -284,10 +300,13 @@ function addUpdate(
300 fiber: Fiber,
301 partialState: PartialState<any, any> | null,
302 callback: mixed,
287 - priorityLevel: PriorityLevel,
303 + priorityLevel: PriorityLevel | null,
304 + expirationTime: ExpirationTime,
305 + currentTime: ExpirationTime,
306 ): void {
307 const update = {
308 priorityLevel,
309 + expirationTime,
310 partialState,
311 callback,
312 isReplace: false,
@@ -295,7 +314,7 @@ function addUpdate(
314 isTopLevelUnmount: false,
315 next: null,
316 };
298 - insertUpdate(fiber, update);
317 + insertUpdate(fiber, update, currentTime);
318 }
319 exports.addUpdate = addUpdate;
320
@@ -303,10 +322,13 @@ function addReplaceUpdate(
322 fiber: Fiber,
323 state: any | null,
324 callback: Callback | null,
306 - priorityLevel: PriorityLevel,
325 + priorityLevel: PriorityLevel | null,
326 + expirationTime: ExpirationTime,
327 + currentTime: ExpirationTime,
328 ): void {
329 const update = {
330 priorityLevel,
331 + expirationTime,
332 partialState: state,
333 callback,
334 isReplace: true,
@@ -314,17 +336,20 @@ function addReplaceUpdate(
336 isTopLevelUnmount: false,
337 next: null,
338 };
317 - insertUpdate(fiber, update);
339 + insertUpdate(fiber, update, currentTime);
340 }
341 exports.addReplaceUpdate = addReplaceUpdate;
342
343 function addForceUpdate(
344 fiber: Fiber,
345 callback: Callback | null,
324 - priorityLevel: PriorityLevel,
346 + priorityLevel: PriorityLevel | null,
347 + expirationTime: ExpirationTime,
348 + currentTime: ExpirationTime,
349 ): void {
350 const update = {
351 priorityLevel,
352 + expirationTime,
353 partialState: null,
354 callback,
355 isReplace: false,
@@ -332,11 +357,11 @@ function addForceUpdate(
357 isTopLevelUnmount: false,
358 next: null,
359 };
335 - insertUpdate(fiber, update);
360 + insertUpdate(fiber, update, currentTime);
361 }
362 exports.addForceUpdate = addForceUpdate;
363
339 -function getUpdatePriority(fiber: Fiber): PriorityLevel {
364 +function getUpdateExpirationTime(fiber: Fiber): ExpirationTime {
365 const updateQueue = fiber.updateQueue;
366 if (updateQueue === null) {
367 return NoWork;
@@ -344,20 +369,23 @@ function getUpdatePriority(fiber: Fiber): PriorityLevel {
369 if (fiber.tag !== ClassComponent && fiber.tag !== HostRoot) {
370 return NoWork;
371 }
347 - return updateQueue.first !== null ? updateQueue.first.priorityLevel : NoWork;
372 + return updateQueue.first !== null ? updateQueue.first.expirationTime : NoWork;
373 }
349 -exports.getUpdatePriority = getUpdatePriority;
374 +exports.getUpdateExpirationTime = getUpdateExpirationTime;
375
376 function addTopLevelUpdate(
377 fiber: Fiber,
378 partialState: PartialState<any, any>,
379 callback: Callback | null,
355 - priorityLevel: PriorityLevel,
380 + priorityLevel: PriorityLevel | null,
381 + expirationTime: ExpirationTime,
382 + currentTime: ExpirationTime,
383 ): void {
384 const isTopLevelUnmount = partialState.element === null;
385
386 const update = {
387 priorityLevel,
388 + expirationTime,
389 partialState,
390 callback,
391 isReplace: false,
@@ -365,7 +393,7 @@ function addTopLevelUpdate(
393 isTopLevelUnmount,
394 next: null,
395 };
368 - const update2 = insertUpdate(fiber, update);
396 + const update2 = insertUpdate(fiber, update, currentTime);
397
398 if (isTopLevelUnmount) {
399 // TODO: Redesign the top-level mount/update/unmount API to avoid this
@@ -404,7 +432,7 @@ function beginUpdateQueue(
432 instance: any,
433 prevState: any,
434 props: any,
407 - priorityLevel: PriorityLevel,
435 + renderExpirationTime: ExpirationTime,
436 ): any {
437 if (current !== null && current.updateQueue === queue) {
438 // We need to create a work-in-progress queue, by cloning the current queue.
@@ -434,10 +462,7 @@ function beginUpdateQueue(
462 let state = prevState;
463 let dontMutatePrevState = true;
464 let update = queue.first;
437 - while (
438 - update !== null &&
439 - comparePriority(update.priorityLevel, priorityLevel) <= 0
440 - ) {
465 + while (update !== null && update.expirationTime <= renderExpirationTime) {
466 // Remove each update from the queue right before it is processed. That way
467 // if setState is called from inside an updater function, the new update
468 // will be inserted in the correct position.
src/renderers/shared/fiber/__tests__/ReactExpiration-test.js new
+125
@@ -0,0 +1,125 @@
1 +/**
2 + * Copyright (c) 2013-present, Facebook, Inc.
3 + *
4 + * This source code is licensed under the MIT license found in the
5 + * LICENSE file in the root directory of this source tree.
6 + */
7 +
8 +'use strict';
9 +
10 +var React;
11 +var ReactNoop;
12 +
13 +describe('ReactExpiration', () => {
14 + beforeEach(() => {
15 + jest.resetModules();
16 + React = require('react');
17 + ReactNoop = require('react-noop-renderer');
18 + });
19 +
20 + function span(prop) {
21 + return {type: 'span', children: [], prop};
22 + }
23 +
24 + it('increases priority of updates as time progresses', () => {
25 + ReactNoop.render(<span prop="done" />);
26 +
27 + expect(ReactNoop.getChildren()).toEqual([]);
28 +
29 + // Nothing has expired yet because time hasn't advanced.
30 + ReactNoop.flushExpired();
31 + expect(ReactNoop.getChildren()).toEqual([]);
32 +
33 + // Advance by 300ms, not enough to expire the low pri update.
34 + ReactNoop.expire(300);
35 + ReactNoop.flushExpired();
36 + expect(ReactNoop.getChildren()).toEqual([]);
37 +
38 + // Advance by another second. Now the update should expire and flush.
39 + ReactNoop.expire(1000);
40 + ReactNoop.flushExpired();
41 + expect(ReactNoop.getChildren()).toEqual([span('done')]);
42 + });
43 +
44 + it('coalesces updates to the same component', () => {
45 + const foos = [];
46 + class Foo extends React.Component {
47 + constructor() {
48 + super();
49 + this.state = {step: 0};
50 + foos.push(this);
51 + }
52 + render() {
53 + return <span prop={this.state.step} />;
54 + }
55 + }
56 +
57 + ReactNoop.render([<Foo key="A" />, <Foo key="B" />]);
58 + ReactNoop.flush();
59 + const [a, b] = foos;
60 +
61 + a.setState({step: 1});
62 +
63 + // Advance time by 500ms.
64 + ReactNoop.expire(500);
65 +
66 + // Update A again. This update should coalesce with the previous update.
67 + a.setState({step: 2});
68 + // Update B. This is the first update, so it has nothing to coalesce with.
69 + b.setState({step: 1});
70 +
71 + // Advance time. This should be enough to flush both updates to A, but not
72 + // the update to B. If only the first update to A flushes, but not the
73 + // second, then it wasn't coalesced properly.
74 + ReactNoop.expire(600);
75 + ReactNoop.flushExpired();
76 + expect(ReactNoop.getChildren()).toEqual([span(2), span(0)]);
77 +
78 + // Now expire the update to B.
79 + ReactNoop.expire(500);
80 + ReactNoop.flushExpired();
81 + expect(ReactNoop.getChildren()).toEqual([span(2), span(1)]);
82 + });
83 +
84 + it('stops coalescing after a certain threshold', () => {
85 + let instance;
86 + class Foo extends React.Component {
87 + state = {step: 0};
88 + render() {
89 + instance = this;
90 + return <span prop={this.state.step} />;
91 + }
92 + }
93 +
94 + ReactNoop.render(<Foo />);
95 + ReactNoop.flush();
96 +
97 + instance.setState({step: 1});
98 +
99 + // Advance time by 500 ms.
100 + ReactNoop.expire(500);
101 +
102 + // Update again. This update should coalesce with the previous update.
103 + instance.setState({step: 2});
104 +
105 + // Advance time by 480ms. Not enough to expire the updates.
106 + ReactNoop.expire(480);
107 + ReactNoop.flushExpired();
108 + expect(ReactNoop.getChildren()).toEqual([span(0)]);
109 +
110 + // Update again. This update should NOT be coalesced, because the
111 + // previous updates have almost expired.
112 + instance.setState({step: 3});
113 +
114 + // Advance time. This should expire the first two updates,
115 + // but not the third.
116 + ReactNoop.expire(500);
117 + ReactNoop.flushExpired();
118 + expect(ReactNoop.getChildren()).toEqual([span(2)]);
119 +
120 + // Now expire the remaining update.
121 + ReactNoop.expire(1000);
122 + ReactNoop.flushExpired();
123 + expect(ReactNoop.getChildren()).toEqual([span(3)]);
124 + });
125 +});
src/renderers/shared/fiber/__tests__/ReactFiberHostContext-test.js
+3
@@ -46,6 +46,9 @@ describe('ReactFiberHostContext', () => {
46 appendChildToContainer: function() {
47 return null;
48 },
49 + now: function() {
50 + return 0;
51 + },
52 useSyncScheduling: true,
53 });
54
src/renderers/shared/fiber/__tests__/ReactIncrementalTriangle-test.js
+17 -1
@@ -54,6 +54,14 @@ describe('ReactIncrementalTriangle', () => {
54 };
55 }
56
57 + const EXPIRE = 'EXPIRE';
58 + function expire(ms) {
59 + return {
60 + type: EXPIRE,
61 + ms,
62 + };
63 + }
64 +
65 function TriangleSimulator() {
66 let triangles = [];
67 let leafTriangles = [];
@@ -212,6 +220,9 @@ describe('ReactIncrementalTriangle', () => {
220 targetTriangle.activate();
221 }
222 break;
223 + case EXPIRE:
224 + ReactNoop.expire(action.ms);
225 + break;
226 default:
227 break;
228 }
@@ -251,7 +262,7 @@ describe('ReactIncrementalTriangle', () => {
262 }
263
264 function randomAction() {
254 - switch (randomInteger(0, 4)) {
265 + switch (randomInteger(0, 5)) {
266 case 0:
267 return flush(randomInteger(0, totalTriangles * 1.5));
268 case 1:
@@ -260,6 +271,8 @@ describe('ReactIncrementalTriangle', () => {
271 return interrupt();
272 case 3:
273 return toggle(randomInteger(0, totalChildren));
274 + case 4:
275 + return expire(randomInteger(0, 1500));
276 default:
277 throw new Error('Switch statement should be exhaustive');
278 }
@@ -290,6 +303,9 @@ describe('ReactIncrementalTriangle', () => {
303 case TOGGLE:
304 result += `toggle(${action.childIndex})`;
305 break;
306 + case EXPIRE:
307 + result += `expire(${action.ms})`;
308 + break;
309 default:
310 throw new Error('Switch statement should be exhaustive');
311 }
src/renderers/testing/ReactTestRendererFiberEntry.js
+5
@@ -247,6 +247,11 @@ var TestRenderer = ReactFiberReconciler({
247 useSyncScheduling: true,
248
249 getPublicInstance,
250 +
251 + now(): number {
252 + // Test renderer does not use expiration
253 + return 0;
254 + },
255 });
256
257 var defaultTestOptions = {