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

Make ExpirationTime an opaque type (#18732)

* Add LanePriority type React's internal scheduler has more priority levels than the external Scheduler package. Let's use React as the source of truth for tracking the priority of updates so we have more control. We'll still fall back to Scheduler in the default case. In the future, we should consider removing `runWithPriority` from Scheduler and replacing the valid use cases with React-specific APIs. This commit adds a new type, called a LanePriority to disambiguate from the Scheduler one. ("Lane" refers to another type that I'm planning. It roughly translates to "thread." Each lane will have a priority associated with it.) I'm not actually using the lane anywhere, yet. Only setting stuff up. * Remove expiration times train model In the old reconciler, expiration times are computed by applying an offset to the current system time. This has the effect of increasing the priority of updates as time progresses. Because we also use expiration times as a kind of "thread" identifier, it turns out this is quite limiting because we can only flush work sequentially along the timeline. The new model will use a bitmask to represent parallel threads that can be worked on in any combination and in any order. In this commit, expiration times and the linear timeline are still in place, but they are no longer based on a timestamp. Effectively, they are constants based on their priority level. * Stop using ExpirationTime to represent timestamps Follow up to the previous commit. This converts the remaining places where we were using the ExpirationTime type to represent a timestamp, like Suspense timeouts. * Fork Dependencies and PendingInteractionMap types These contain expiration times * Make ExpirationTime an opaque type ExpirationTime is currently just an alias for the `number` type, for a few reasons. One is that it predates Flow's opaque type feature. Another is that making it opaque means we have to move all our comparisons and number math to the ExpirationTime module, and use utility functions everywhere else. However, this is actually what we want in the new system, because the Lanes type that will replace ExpirationTime is a bitmask with a particular layout, and performing operations on it will involve more than just number comparisions and artihmetic. I don't want this logic to spread ad hoc around the whole codebase. The utility functions get inlined by Closure so it doesn't matter performance-wise. I automated most of the changes with JSCodeshift, with only a few manual tweaks to stuff like imports. My goal was to port the logic exactly to prevent subtle mistakes, without trying to simplify anything in the process. I'll likely need to audit many of these sites again when I replace them with the new type, though, especially the ones in ReactFiberRoot. I added the codemods I used to the `scripts` directory. I won't merge these to master. I'll remove them in a subsequent commit. I'm only committing them here so they show up in the PR for future reference. I had a lot of trouble getting Flow to pass. Somehow it was not inferring the correct type of the constants exported from the ExpirationTime module, despite being annotated correctly. I tried converting them them to constructor functions — `NoWork` becomes `NoWork()` — and that made it work. I used that to unblock me, and fixed all the other type errors. Once there were no more type errors, I tried converting the constructors back to constants. Started getting errors again. Then I added a type constraint everywhere a constant was referenced. That fixed it. I also figured out that you only have to add a constraint when the constant is passed to another function, even if the function is annotated. So this indicates to me that it's probably a Flow bug. I'll file an issue with Flow. * Delete temporary codemods used in previous commit I only added these to the previous commit so that I can easily run it again when rebasing. When the stack is squashed, it will be as if they never existed.

Andrew Clark committed Apr 24, 2020 at 23:26 UTC db6513914f99c260090f26f0a547ee1432c934e6
38 files changed +1438 -911
packages/react-devtools-shared/src/backend/renderer.js
+5 -1
@@ -2142,7 +2142,6 @@ export function attach(
2142 const {
2143 _debugOwner,
2144 _debugSource,
2145 - dependencies,
2145 stateNode,
2146 memoizedProps,
2147 memoizedState,
@@ -2150,6 +2149,11 @@ export function attach(
2149 type,
2150 } = fiber;
2151
2152 + const dependencies =
2153 + (fiber: any).dependencies ||
2154 + (fiber: any).dependencies_old ||
2155 + (fiber: any).dependencies_new;
2156 +
2157 const elementType = getElementTypeForFiber(fiber);
2158
2159 const usesHooks =
packages/react-dom/src/__tests__/ReactDOMServerIntegrationHooks-test.js
+38 -19
@@ -1278,25 +1278,44 @@ describe('ReactDOMServerHooks', () => {
1278 // State update should trigger the ID to update, which changes the props
1279 // of ChildWithID. This should cause ChildWithID to hydrate before Children
1280
1281 - expect(Scheduler).toFlushAndYieldThrough(
1282 - __DEV__
1283 - ? [
1284 - 'Child with ID',
1285 - // Fallbacks are immdiately committed in TestUtils version
1286 - // of act
1287 - // 'Child with ID',
1288 - // 'Child with ID',
1289 - 'Child One',
1290 - 'Child Two',
1291 - ]
1292 - : [
1293 - 'Child with ID',
1294 - 'Child with ID',
1295 - 'Child with ID',
1296 - 'Child One',
1297 - 'Child Two',
1298 - ],
1299 - );
1281 + gate(flags => {
1282 + if (__DEV__) {
1283 + expect(Scheduler).toFlushAndYieldThrough([
1284 + 'Child with ID',
1285 + // Fallbacks are immdiately committed in TestUtils version
1286 + // of act
1287 + // 'Child with ID',
1288 + // 'Child with ID',
1289 + 'Child One',
1290 + 'Child Two',
1291 + ]);
1292 + } else if (flags.new) {
1293 + // Upgrading a dehyrdating boundary works a little differently in
1294 + // the new reconciler. After the update on the boundary is
1295 + // scheduled, it waits until the end of the current time slice
1296 + // before restarting at the higher priority.
1297 + expect(Scheduler).toFlushAndYieldThrough([
1298 + 'Child with ID',
1299 + 'Child with ID',
1300 + 'Child with ID',
1301 + 'Child with ID',
1302 + 'Child One',
1303 + 'Child Two',
1304 + ]);
1305 + } else {
1306 + // Whereas the old reconciler relies on a Scheduler hack to
1307 + // interrupt the current task. It's not clear if this is any
1308 + // better or worse, though. Regardless it's not a big deal since
1309 + // the time slices aren't that big.
1310 + expect(Scheduler).toFlushAndYieldThrough([
1311 + 'Child with ID',
1312 + 'Child with ID',
1313 + 'Child with ID',
1314 + 'Child One',
1315 + 'Child Two',
1316 + ]);
1317 + }
1318 + });
1319
1320 expect(child1Ref.current).toBe(null);
1321 expect(childWithIDRef.current).toEqual(
packages/react-dom/src/__tests__/ReactDOMServerSelectiveHydration-test.internal.js
+15 -1
@@ -885,7 +885,21 @@ describe('ReactDOMServerSelectiveHydration', () => {
885
886 // Start rendering. This will force the first boundary to hydrate
887 // by scheduling it at one higher pri than Idle.
888 - expect(Scheduler).toFlushAndYieldThrough(['App', 'A']);
888 + expect(Scheduler).toFlushAndYieldThrough(
889 + gate(flags =>
890 + flags.new
891 + ? // An update was scheduled to force hydrate the boundary, but the
892 + // new reconciler will continue rendering at Idle until the next
893 + // time React yields. This is fine though because it will switch
894 + // to the hydration level when it re-enters the work loop.
895 + ['App', 'AA']
896 + : // The old reconciler gives Scheduler a `timeout` argument, which
897 + // affects the ordering of tasks in the queue. That triggers an
898 + // immediate interruption, as opposed to at the end of the current
899 + // time slice.
900 + ['App', 'A'],
901 + ),
902 + );
903
904 // Hover over A which (could) schedule at one higher pri than Idle.
905 dispatchMouseHoverEvent(spanA, null);
packages/react-dom/src/events/DeprecatedDOMEventResponderSystem.js
+11 -3
@@ -33,7 +33,10 @@ import {
33 executeUserEventHandler,
34 } from './ReactDOMUpdateBatching';
35 import type {Fiber} from 'react-reconciler/src/ReactInternalTypes';
36 -import {enableDeprecatedFlareAPI} from 'shared/ReactFeatureFlags';
36 +import {
37 + enableDeprecatedFlareAPI,
38 + enableNewReconciler,
39 +} from 'shared/ReactFeatureFlags';
40 import invariant from 'shared/invariant';
41
42 import {getClosestInstanceFromNode} from '../client/ReactDOMComponentTree';
@@ -282,7 +285,9 @@ function doesFiberHaveResponder(
285 ): boolean {
286 const tag = fiber.tag;
287 if (tag === HostComponent || tag === ScopeComponent) {
285 - const dependencies = fiber.dependencies;
288 + const dependencies = enableNewReconciler
289 + ? fiber.dependencies_new
290 + : fiber.dependencies_old;
291 if (dependencies !== null) {
292 const respondersMap = dependencies.responders;
293 if (respondersMap !== null && respondersMap.has(responder)) {
@@ -381,7 +386,10 @@ function traverseAndHandleEventResponderInstances(
386 let node = targetFiber;
387 let insidePortal = false;
388 while (node !== null) {
384 - const {dependencies, tag} = node;
389 + const {tag} = node;
390 + const dependencies = enableNewReconciler
391 + ? node.dependencies_new
392 + : node.dependencies_old;
393 if (tag === HostPortal) {
394 insidePortal = true;
395 } else if (
packages/react-reconciler/src/ReactChildFiber.new.js
+15 -15
@@ -12,7 +12,7 @@ import type {ReactPortal} from 'shared/ReactTypes';
12 import type {BlockComponent} from 'react/src/ReactBlock';
13 import type {LazyComponent} from 'react/src/ReactLazy';
14 import type {Fiber} from './ReactInternalTypes';
15 -import type {ExpirationTime} from './ReactFiberExpirationTime.new';
15 +import type {ExpirationTimeOpaque} from './ReactFiberExpirationTime.new';
16
17 import getComponentName from 'shared/getComponentName';
18 import {Placement, Deletion} from './ReactSideEffectTags';
@@ -377,7 +377,7 @@ function ChildReconciler(shouldTrackSideEffects) {
377 returnFiber: Fiber,
378 current: Fiber | null,
379 textContent: string,
380 - expirationTime: ExpirationTime,
380 + expirationTime: ExpirationTimeOpaque,
381 ) {
382 if (current === null || current.tag !== HostText) {
383 // Insert
@@ -400,7 +400,7 @@ function ChildReconciler(shouldTrackSideEffects) {
400 returnFiber: Fiber,
401 current: Fiber | null,
402 element: ReactElement,
403 - expirationTime: ExpirationTime,
403 + expirationTime: ExpirationTimeOpaque,
404 ): Fiber {
405 if (current !== null) {
406 if (
@@ -456,7 +456,7 @@ function ChildReconciler(shouldTrackSideEffects) {
456 returnFiber: Fiber,
457 current: Fiber | null,
458 portal: ReactPortal,
459 - expirationTime: ExpirationTime,
459 + expirationTime: ExpirationTimeOpaque,
460 ): Fiber {
461 if (
462 current === null ||
@@ -484,7 +484,7 @@ function ChildReconciler(shouldTrackSideEffects) {
484 returnFiber: Fiber,
485 current: Fiber | null,
486 fragment: Iterable<*>,
487 - expirationTime: ExpirationTime,
487 + expirationTime: ExpirationTimeOpaque,
488 key: null | string,
489 ): Fiber {
490 if (current === null || current.tag !== Fragment) {
@@ -508,7 +508,7 @@ function ChildReconciler(shouldTrackSideEffects) {
508 function createChild(
509 returnFiber: Fiber,
510 newChild: any,
511 - expirationTime: ExpirationTime,
511 + expirationTime: ExpirationTimeOpaque,
512 ): Fiber | null {
513 if (typeof newChild === 'string' || typeof newChild === 'number') {
514 // Text nodes don't have keys. If the previous node is implicitly keyed
@@ -573,7 +573,7 @@ function ChildReconciler(shouldTrackSideEffects) {
573 returnFiber: Fiber,
574 oldFiber: Fiber | null,
575 newChild: any,
576 - expirationTime: ExpirationTime,
576 + expirationTime: ExpirationTimeOpaque,
577 ): Fiber | null {
578 // Update the fiber if the keys match, otherwise return null.
579
@@ -662,7 +662,7 @@ function ChildReconciler(shouldTrackSideEffects) {
662 returnFiber: Fiber,
663 newIdx: number,
664 newChild: any,
665 - expirationTime: ExpirationTime,
665 + expirationTime: ExpirationTimeOpaque,
666 ): Fiber | null {
667 if (typeof newChild === 'string' || typeof newChild === 'number') {
668 // Text nodes don't have keys, so we neither have to check the old nor
@@ -785,7 +785,7 @@ function ChildReconciler(shouldTrackSideEffects) {
785 returnFiber: Fiber,
786 currentFirstChild: Fiber | null,
787 newChildren: Array<*>,
788 - expirationTime: ExpirationTime,
788 + expirationTime: ExpirationTimeOpaque,
789 ): Fiber | null {
790 // This algorithm can't optimize by searching from both ends since we
791 // don't have backpointers on fibers. I'm trying to see how far we can get
@@ -944,7 +944,7 @@ function ChildReconciler(shouldTrackSideEffects) {
944 returnFiber: Fiber,
945 currentFirstChild: Fiber | null,
946 newChildrenIterable: Iterable<*>,
947 - expirationTime: ExpirationTime,
947 + expirationTime: ExpirationTimeOpaque,
948 ): Fiber | null {
949 // This is the same implementation as reconcileChildrenArray(),
950 // but using the iterator instead.
@@ -1134,7 +1134,7 @@ function ChildReconciler(shouldTrackSideEffects) {
1134 returnFiber: Fiber,
1135 currentFirstChild: Fiber | null,
1136 textContent: string,
1137 - expirationTime: ExpirationTime,
1137 + expirationTime: ExpirationTimeOpaque,
1138 ): Fiber {
1139 // There's no need to check for keys on text nodes since we don't have a
1140 // way to define them.
@@ -1162,7 +1162,7 @@ function ChildReconciler(shouldTrackSideEffects) {
1162 returnFiber: Fiber,
1163 currentFirstChild: Fiber | null,
1164 element: ReactElement,
1165 - expirationTime: ExpirationTime,
1165 + expirationTime: ExpirationTimeOpaque,
1166 ): Fiber {
1167 const key = element.key;
1168 let child = currentFirstChild;
@@ -1266,7 +1266,7 @@ function ChildReconciler(shouldTrackSideEffects) {
1266 returnFiber: Fiber,
1267 currentFirstChild: Fiber | null,
1268 portal: ReactPortal,
1269 - expirationTime: ExpirationTime,
1269 + expirationTime: ExpirationTimeOpaque,
1270 ): Fiber {
1271 const key = portal.key;
1272 let child = currentFirstChild;
@@ -1309,7 +1309,7 @@ function ChildReconciler(shouldTrackSideEffects) {
1309 returnFiber: Fiber,
1310 currentFirstChild: Fiber | null,
1311 newChild: any,
1312 - expirationTime: ExpirationTime,
1312 + expirationTime: ExpirationTimeOpaque,
1313 ): Fiber | null {
1314 // This function is not recursive.
1315 // If the top level item is an array, we treat it as a set of children,
@@ -1464,7 +1464,7 @@ export function cloneChildFibers(
1464 // Reset a workInProgress child set to prepare it for a second pass.
1465 export function resetChildFibers(
1466 workInProgress: Fiber,
1467 - renderExpirationTime: ExpirationTime,
1467 + renderExpirationTime: ExpirationTimeOpaque,
1468 ): void {
1469 let child = workInProgress.child;
1470 while (child !== null) {
packages/react-reconciler/src/ReactFiber.new.js
+40 -38
@@ -18,7 +18,7 @@ import type {Fiber} from './ReactInternalTypes';
18 import type {RootTag} from './ReactRootTags';
19 import type {WorkTag} from './ReactWorkTags';
20 import type {TypeOfMode} from './ReactTypeOfMode';
21 -import type {ExpirationTime} from './ReactFiberExpirationTime.new';
21 +import type {ExpirationTimeOpaque} from './ReactFiberExpirationTime.new';
22 import type {SuspenseInstance} from './ReactFiberHostConfig';
23
24 import invariant from 'shared/invariant';
@@ -134,7 +134,7 @@ function FiberNode(
134 this.memoizedProps = null;
135 this.updateQueue = null;
136 this.memoizedState = null;
137 - this.dependencies = null;
137 + this.dependencies_new = null;
138
139 this.mode = mode;
140
@@ -145,8 +145,8 @@ function FiberNode(
145 this.firstEffect = null;
146 this.lastEffect = null;
147
148 - this.expirationTime = NoWork;
149 - this.childExpirationTime = NoWork;
148 + this.expirationTime_opaque = NoWork;
149 + this.childExpirationTime_opaque = NoWork;
150
151 this.alternate = null;
152
@@ -309,8 +309,9 @@ export function createWorkInProgress(current: Fiber, pendingProps: any): Fiber {
309 }
310 }
311
312 - workInProgress.childExpirationTime = current.childExpirationTime;
313 - workInProgress.expirationTime = current.expirationTime;
312 + workInProgress.childExpirationTime_opaque =
313 + current.childExpirationTime_opaque;
314 + workInProgress.expirationTime_opaque = current.expirationTime_opaque;
315
316 workInProgress.child = current.child;
317 workInProgress.memoizedProps = current.memoizedProps;
@@ -319,8 +320,8 @@ export function createWorkInProgress(current: Fiber, pendingProps: any): Fiber {
320
321 // Clone the dependencies object. This is mutated during the render phase, so
322 // it cannot be shared with the current fiber.
322 - const currentDependencies = current.dependencies;
323 - workInProgress.dependencies =
323 + const currentDependencies = current.dependencies_new;
324 + workInProgress.dependencies_new =
325 currentDependencies === null
326 ? null
327 : {
@@ -364,7 +365,7 @@ export function createWorkInProgress(current: Fiber, pendingProps: any): Fiber {
365 // Used to reuse a Fiber for a second pass.
366 export function resetWorkInProgress(
367 workInProgress: Fiber,
367 - renderExpirationTime: ExpirationTime,
368 + renderExpirationTime: ExpirationTimeOpaque,
369 ) {
370 // This resets the Fiber to what createFiber or createWorkInProgress would
371 // have set the values to before during the first pass. Ideally this wouldn't
@@ -386,15 +387,15 @@ export function resetWorkInProgress(
387 const current = workInProgress.alternate;
388 if (current === null) {
389 // Reset to createFiber's initial values.
389 - workInProgress.childExpirationTime = NoWork;
390 - workInProgress.expirationTime = renderExpirationTime;
390 + workInProgress.childExpirationTime_opaque = NoWork;
391 + workInProgress.expirationTime_opaque = renderExpirationTime;
392
393 workInProgress.child = null;
394 workInProgress.memoizedProps = null;
395 workInProgress.memoizedState = null;
396 workInProgress.updateQueue = null;
397
397 - workInProgress.dependencies = null;
398 + workInProgress.dependencies_new = null;
399
400 workInProgress.stateNode = null;
401
@@ -406,8 +407,9 @@ export function resetWorkInProgress(
407 }
408 } else {
409 // Reset to the cloned values that createWorkInProgress would've.
409 - workInProgress.childExpirationTime = current.childExpirationTime;
410 - workInProgress.expirationTime = current.expirationTime;
410 + workInProgress.childExpirationTime_opaque =
411 + current.childExpirationTime_opaque;
412 + workInProgress.expirationTime_opaque = current.expirationTime_opaque;
413
414 workInProgress.child = current.child;
415 workInProgress.memoizedProps = current.memoizedProps;
@@ -416,8 +418,8 @@ export function resetWorkInProgress(
418
419 // Clone the dependencies object. This is mutated during the render phase, so
420 // it cannot be shared with the current fiber.
419 - const currentDependencies = current.dependencies;
420 - workInProgress.dependencies =
421 + const currentDependencies = current.dependencies_new;
422 + workInProgress.dependencies_new =
423 currentDependencies === null
424 ? null
425 : {
@@ -463,7 +465,7 @@ export function createFiberFromTypeAndProps(
465 pendingProps: any,
466 owner: null | Fiber,
467 mode: TypeOfMode,
466 - expirationTime: ExpirationTime,
468 + expirationTime: ExpirationTimeOpaque,
469 ): Fiber {
470 let fiberTag = IndeterminateComponent;
471 // The resolved type is set if we know what the final type will be. I.e. it's not lazy.
@@ -591,7 +593,7 @@ export function createFiberFromTypeAndProps(
593 const fiber = createFiber(fiberTag, pendingProps, key, mode);
594 fiber.elementType = type;
595 fiber.type = resolvedType;
594 - fiber.expirationTime = expirationTime;
596 + fiber.expirationTime_opaque = expirationTime;
597
598 return fiber;
599 }
@@ -599,7 +601,7 @@ export function createFiberFromTypeAndProps(
601 export function createFiberFromElement(
602 element: ReactElement,
603 mode: TypeOfMode,
602 - expirationTime: ExpirationTime,
604 + expirationTime: ExpirationTimeOpaque,
605 ): Fiber {
606 let owner = null;
607 if (__DEV__) {
@@ -626,11 +628,11 @@ export function createFiberFromElement(
628 export function createFiberFromFragment(
629 elements: ReactFragment,
630 mode: TypeOfMode,
629 - expirationTime: ExpirationTime,
631 + expirationTime: ExpirationTimeOpaque,
632 key: null | string,
633 ): Fiber {
634 const fiber = createFiber(Fragment, elements, key, mode);
633 - fiber.expirationTime = expirationTime;
635 + fiber.expirationTime_opaque = expirationTime;
636 return fiber;
637 }
638
@@ -638,13 +640,13 @@ export function createFiberFromFundamental(
640 fundamentalComponent: ReactFundamentalComponent<any, any>,
641 pendingProps: any,
642 mode: TypeOfMode,
641 - expirationTime: ExpirationTime,
643 + expirationTime: ExpirationTimeOpaque,
644 key: null | string,
645 ): Fiber {
646 const fiber = createFiber(FundamentalComponent, pendingProps, key, mode);
647 fiber.elementType = fundamentalComponent;
648 fiber.type = fundamentalComponent;
647 - fiber.expirationTime = expirationTime;
649 + fiber.expirationTime_opaque = expirationTime;
650 return fiber;
651 }
652
@@ -652,20 +654,20 @@ function createFiberFromScope(
654 scope: ReactScope,
655 pendingProps: any,
656 mode: TypeOfMode,
655 - expirationTime: ExpirationTime,
657 + expirationTime: ExpirationTimeOpaque,
658 key: null | string,
659 ) {
660 const fiber = createFiber(ScopeComponent, pendingProps, key, mode);
661 fiber.type = scope;
662 fiber.elementType = scope;
661 - fiber.expirationTime = expirationTime;
663 + fiber.expirationTime_opaque = expirationTime;
664 return fiber;
665 }
666
667 function createFiberFromProfiler(
668 pendingProps: any,
669 mode: TypeOfMode,
668 - expirationTime: ExpirationTime,
670 + expirationTime: ExpirationTimeOpaque,
671 key: null | string,
672 ): Fiber {
673 if (__DEV__) {
@@ -678,7 +680,7 @@ function createFiberFromProfiler(
680 // TODO: The Profiler fiber shouldn't have a type. It has a tag.
681 fiber.elementType = REACT_PROFILER_TYPE;
682 fiber.type = REACT_PROFILER_TYPE;
681 - fiber.expirationTime = expirationTime;
683 + fiber.expirationTime_opaque = expirationTime;
684
685 if (enableProfilerTimer) {
686 fiber.stateNode = {
@@ -693,7 +695,7 @@ function createFiberFromProfiler(
695 export function createFiberFromSuspense(
696 pendingProps: any,
697 mode: TypeOfMode,
696 - expirationTime: ExpirationTime,
698 + expirationTime: ExpirationTimeOpaque,
699 key: null | string,
700 ) {
701 const fiber = createFiber(SuspenseComponent, pendingProps, key, mode);
@@ -704,14 +706,14 @@ export function createFiberFromSuspense(
706 fiber.type = REACT_SUSPENSE_TYPE;
707 fiber.elementType = REACT_SUSPENSE_TYPE;
708
707 - fiber.expirationTime = expirationTime;
709 + fiber.expirationTime_opaque = expirationTime;
710 return fiber;
711 }
712
713 export function createFiberFromSuspenseList(
714 pendingProps: any,
715 mode: TypeOfMode,
714 - expirationTime: ExpirationTime,
716 + expirationTime: ExpirationTimeOpaque,
717 key: null | string,
718 ) {
719 const fiber = createFiber(SuspenseListComponent, pendingProps, key, mode);
@@ -722,17 +724,17 @@ export function createFiberFromSuspenseList(
724 fiber.type = REACT_SUSPENSE_LIST_TYPE;
725 }
726 fiber.elementType = REACT_SUSPENSE_LIST_TYPE;
725 - fiber.expirationTime = expirationTime;
727 + fiber.expirationTime_opaque = expirationTime;
728 return fiber;
729 }
730
731 export function createFiberFromText(
732 content: string,
733 mode: TypeOfMode,
732 - expirationTime: ExpirationTime,
734 + expirationTime: ExpirationTimeOpaque,
735 ): Fiber {
736 const fiber = createFiber(HostText, content, null, mode);
735 - fiber.expirationTime = expirationTime;
737 + fiber.expirationTime_opaque = expirationTime;
738 return fiber;
739 }
740
@@ -755,11 +757,11 @@ export function createFiberFromDehydratedFragment(
757 export function createFiberFromPortal(
758 portal: ReactPortal,
759 mode: TypeOfMode,
758 - expirationTime: ExpirationTime,
760 + expirationTime: ExpirationTimeOpaque,
761 ): Fiber {
762 const pendingProps = portal.children !== null ? portal.children : [];
763 const fiber = createFiber(HostPortal, pendingProps, portal.key, mode);
762 - fiber.expirationTime = expirationTime;
764 + fiber.expirationTime_opaque = expirationTime;
765 fiber.stateNode = {
766 containerInfo: portal.containerInfo,
767 pendingChildren: null, // Used by persistent updates
@@ -799,14 +801,14 @@ export function assignFiberPropertiesInDEV(
801 target.memoizedProps = source.memoizedProps;
802 target.updateQueue = source.updateQueue;
803 target.memoizedState = source.memoizedState;
802 - target.dependencies = source.dependencies;
804 + target.dependencies_new = source.dependencies_new;
805 target.mode = source.mode;
806 target.effectTag = source.effectTag;
807 target.nextEffect = source.nextEffect;
808 target.firstEffect = source.firstEffect;
809 target.lastEffect = source.lastEffect;
808 - target.expirationTime = source.expirationTime;
809 - target.childExpirationTime = source.childExpirationTime;
810 + target.expirationTime_opaque = source.expirationTime_opaque;
811 + target.childExpirationTime_opaque = source.childExpirationTime_opaque;
812 target.alternate = source.alternate;
813 if (enableProfilerTimer) {
814 target.actualDuration = source.actualDuration;
packages/react-reconciler/src/ReactFiber.old.js
+7 -7
@@ -134,7 +134,7 @@ function FiberNode(
134 this.memoizedProps = null;
135 this.updateQueue = null;
136 this.memoizedState = null;
137 - this.dependencies = null;
137 + this.dependencies_old = null;
138
139 this.mode = mode;
140
@@ -319,8 +319,8 @@ export function createWorkInProgress(current: Fiber, pendingProps: any): Fiber {
319
320 // Clone the dependencies object. This is mutated during the render phase, so
321 // it cannot be shared with the current fiber.
322 - const currentDependencies = current.dependencies;
323 - workInProgress.dependencies =
322 + const currentDependencies = current.dependencies_old;
323 + workInProgress.dependencies_old =
324 currentDependencies === null
325 ? null
326 : {
@@ -394,7 +394,7 @@ export function resetWorkInProgress(
394 workInProgress.memoizedState = null;
395 workInProgress.updateQueue = null;
396
397 - workInProgress.dependencies = null;
397 + workInProgress.dependencies_old = null;
398
399 workInProgress.stateNode = null;
400
@@ -416,8 +416,8 @@ export function resetWorkInProgress(
416
417 // Clone the dependencies object. This is mutated during the render phase, so
418 // it cannot be shared with the current fiber.
419 - const currentDependencies = current.dependencies;
420 - workInProgress.dependencies =
419 + const currentDependencies = current.dependencies_old;
420 + workInProgress.dependencies_old =
421 currentDependencies === null
422 ? null
423 : {
@@ -799,7 +799,7 @@ export function assignFiberPropertiesInDEV(
799 target.memoizedProps = source.memoizedProps;
800 target.updateQueue = source.updateQueue;
801 target.memoizedState = source.memoizedState;
802 - target.dependencies = source.dependencies;
802 + target.dependencies_old = source.dependencies_old;
803 target.mode = source.mode;
804 target.effectTag = source.effectTag;
805 target.nextEffect = source.nextEffect;
packages/react-reconciler/src/ReactFiberBeginWork.new.js
+144 -82
@@ -12,7 +12,7 @@ import type {BlockComponent} from 'react/src/ReactBlock';
12 import type {LazyComponent as LazyComponentType} from 'react/src/ReactLazy';
13 import type {Fiber} from './ReactInternalTypes';
14 import type {FiberRoot} from './ReactInternalTypes';
15 -import type {ExpirationTime} from './ReactFiberExpirationTime.new';
15 +import type {ExpirationTimeOpaque} from './ReactFiberExpirationTime.new';
16 import type {
17 SuspenseState,
18 SuspenseListRenderState,
@@ -99,7 +99,10 @@ import {
99 NoWork,
100 Never,
101 Sync,
102 - computeAsyncExpiration,
102 + DefaultUpdateTime,
103 + isSameOrHigherPriority,
104 + isSameExpirationTime,
105 + bumpPriorityHigher,
106 } from './ReactFiberExpirationTime.new';
107 import {
108 ConcurrentMode,
@@ -173,7 +176,6 @@ import {
176 } from './ReactFiber.new';
177 import {
178 markSpawnedWork,
176 - requestCurrentTimeForUpdate,
179 retryDehydratedSuspenseBoundary,
180 scheduleUpdateOnFiber,
181 renderDidSuspendDelayIfPossible,
@@ -213,7 +215,7 @@ export function reconcileChildren(
215 current: Fiber | null,
216 workInProgress: Fiber,
217 nextChildren: any,
216 - renderExpirationTime: ExpirationTime,
218 + renderExpirationTime: ExpirationTimeOpaque,
219 ) {
220 if (current === null) {
221 // If this is a fresh new component that hasn't been rendered yet, we
@@ -246,7 +248,7 @@ function forceUnmountCurrentAndReconcile(
248 current: Fiber,
249 workInProgress: Fiber,
250 nextChildren: any,
249 - renderExpirationTime: ExpirationTime,
251 + renderExpirationTime: ExpirationTimeOpaque,
252 ) {
253 // This function is fork of reconcileChildren. It's used in cases where we
254 // want to reconcile without matching against the existing set. This has the
@@ -279,7 +281,7 @@ function updateForwardRef(
281 workInProgress: Fiber,
282 Component: any,
283 nextProps: any,
282 - renderExpirationTime: ExpirationTime,
284 + renderExpirationTime: ExpirationTimeOpaque,
285 ) {
286 // TODO: current can be non-null here even if the component
287 // hasn't yet mounted. This happens after the first render suspends.
@@ -374,7 +376,7 @@ function updateMemoComponent(
376 Component: any,
377 nextProps: any,
378 updateExpirationTime,
377 - renderExpirationTime: ExpirationTime,
379 + renderExpirationTime: ExpirationTimeOpaque,
380 ): null | Fiber {
381 if (current === null) {
382 const type = Component.type;
@@ -446,7 +448,7 @@ function updateMemoComponent(
448 }
449 }
450 const currentChild = ((current.child: any): Fiber); // This is always exactly one child
449 - if (updateExpirationTime < renderExpirationTime) {
451 + if (!isSameOrHigherPriority(updateExpirationTime, renderExpirationTime)) {
452 // This will be the props with resolved defaultProps,
453 // unlike current.memoizedProps which will be the unresolved ones.
454 const prevProps = currentChild.memoizedProps;
@@ -476,7 +478,7 @@ function updateSimpleMemoComponent(
478 Component: any,
479 nextProps: any,
480 updateExpirationTime,
479 - renderExpirationTime: ExpirationTime,
481 + renderExpirationTime: ExpirationTimeOpaque,
482 ): null | Fiber {
483 // TODO: current can be non-null here even if the component
484 // hasn't yet mounted. This happens when the inner render suspends.
@@ -521,7 +523,7 @@ function updateSimpleMemoComponent(
523 (__DEV__ ? workInProgress.type === current.type : true)
524 ) {
525 didReceiveUpdate = false;
524 - if (updateExpirationTime < renderExpirationTime) {
526 + if (!isSameOrHigherPriority(updateExpirationTime, renderExpirationTime)) {
527 // The pending update priority was cleared at the beginning of
528 // beginWork. We're about to bail out, but there might be additional
529 // updates at a lower priority. Usually, the priority level of the
@@ -535,7 +537,7 @@ function updateSimpleMemoComponent(
537 // contains hooks.
538 // TODO: Move the reset at in beginWork out of the common path so that
539 // this is no longer necessary.
538 - workInProgress.expirationTime = current.expirationTime;
540 + workInProgress.expirationTime_opaque = current.expirationTime_opaque;
541 return bailoutOnAlreadyFinishedWork(
542 current,
543 workInProgress,
@@ -556,7 +558,7 @@ function updateSimpleMemoComponent(
558 function updateFragment(
559 current: Fiber | null,
560 workInProgress: Fiber,
559 - renderExpirationTime: ExpirationTime,
561 + renderExpirationTime: ExpirationTimeOpaque,
562 ) {
563 const nextChildren = workInProgress.pendingProps;
564 reconcileChildren(
@@ -571,7 +573,7 @@ function updateFragment(
573 function updateMode(
574 current: Fiber | null,
575 workInProgress: Fiber,
574 - renderExpirationTime: ExpirationTime,
576 + renderExpirationTime: ExpirationTimeOpaque,
577 ) {
578 const nextChildren = workInProgress.pendingProps.children;
579 reconcileChildren(
@@ -586,7 +588,7 @@ function updateMode(
588 function updateProfiler(
589 current: Fiber | null,
590 workInProgress: Fiber,
589 - renderExpirationTime: ExpirationTime,
591 + renderExpirationTime: ExpirationTimeOpaque,
592 ) {
593 if (enableProfilerTimer) {
594 workInProgress.effectTag |= Update;
@@ -716,7 +718,7 @@ function updateBlock<Props, Data>(
718 workInProgress: Fiber,
719 block: BlockComponent<Props, Data>,
720 nextProps: any,
719 - renderExpirationTime: ExpirationTime,
721 + renderExpirationTime: ExpirationTimeOpaque,
722 ) {
723 // TODO: current can be non-null here even if the component
724 // hasn't yet mounted. This happens after the first render suspends.
@@ -794,7 +796,7 @@ function updateClassComponent(
796 workInProgress: Fiber,
797 Component: any,
798 nextProps,
797 - renderExpirationTime: ExpirationTime,
799 + renderExpirationTime: ExpirationTimeOpaque,
800 ) {
801 if (__DEV__) {
802 if (workInProgress.type !== workInProgress.elementType) {
@@ -893,7 +895,7 @@ function finishClassComponent(
895 Component: any,
896 shouldUpdate: boolean,
897 hasContext: boolean,
896 - renderExpirationTime: ExpirationTime,
898 + renderExpirationTime: ExpirationTimeOpaque,
899 ) {
900 // Refs should update even if shouldComponentUpdate returns false
901 markRef(current, workInProgress);
@@ -1070,7 +1072,11 @@ function updateHostRoot(current, workInProgress, renderExpirationTime) {
1072 return workInProgress.child;
1073 }
1074
1073 -function updateHostComponent(current, workInProgress, renderExpirationTime) {
1075 +function updateHostComponent(
1076 + current: Fiber | null,
1077 + workInProgress: Fiber,
1078 + renderExpirationTime: ExpirationTimeOpaque,
1079 +) {
1080 pushHostContext(workInProgress);
1081
1082 if (current === null) {
@@ -1101,14 +1107,17 @@ function updateHostComponent(current, workInProgress, renderExpirationTime) {
1107 // Check the host config to see if the children are offscreen/hidden.
1108 if (
1109 workInProgress.mode & ConcurrentMode &&
1104 - renderExpirationTime !== Never &&
1110 + !isSameExpirationTime(
1111 + renderExpirationTime,
1112 + (Never: ExpirationTimeOpaque),
1113 + ) &&
1114 shouldDeprioritizeSubtree(type, nextProps)
1115 ) {
1116 if (enableSchedulerTracing) {
1108 - markSpawnedWork(Never);
1117 + markSpawnedWork((Never: ExpirationTimeOpaque));
1118 }
1119 // Schedule this fiber to re-render at offscreen priority. Then bailout.
1111 - workInProgress.expirationTime = workInProgress.childExpirationTime = Never;
1120 + workInProgress.expirationTime_opaque = workInProgress.childExpirationTime_opaque = Never;
1121 return null;
1122 }
1123
@@ -1609,7 +1618,7 @@ function validateFunctionComponentInDev(workInProgress: Fiber, Component: any) {
1618 }
1619
1620 function mountSuspenseState(
1612 - renderExpirationTime: ExpirationTime,
1621 + renderExpirationTime: ExpirationTimeOpaque,
1622 ): SuspenseState {
1623 return {
1624 dehydrated: null,
@@ -1620,7 +1629,7 @@ function mountSuspenseState(
1629
1630 function updateSuspenseState(
1631 prevSuspenseState: SuspenseState,
1623 - renderExpirationTime: ExpirationTime,
1632 + renderExpirationTime: ExpirationTimeOpaque,
1633 ): SuspenseState {
1634 const prevSuspendedTime = prevSuspenseState.baseTime;
1635 return {
@@ -1628,7 +1637,10 @@ function updateSuspenseState(
1637 baseTime:
1638 // Choose whichever time is inclusive of the other one. This represents
1639 // the union of all the levels that suspended.
1631 - prevSuspendedTime !== NoWork && prevSuspendedTime < renderExpirationTime
1640 + !isSameExpirationTime(
1641 + prevSuspendedTime,
1642 + (NoWork: ExpirationTimeOpaque),
1643 + ) && !isSameOrHigherPriority(prevSuspendedTime, renderExpirationTime)
1644 ? prevSuspendedTime
1645 : renderExpirationTime,
1646 retryTime: NoWork,
@@ -1639,7 +1651,7 @@ function shouldRemainOnFallback(
1651 suspenseContext: SuspenseContext,
1652 current: null | Fiber,
1653 workInProgress: Fiber,
1642 - renderExpirationTime: ExpirationTime,
1654 + renderExpirationTime: ExpirationTimeOpaque,
1655 ) {
1656 // If we're already showing a fallback, there are cases where we need to
1657 // remain on that fallback regardless of whether the content has resolved.
@@ -1651,7 +1663,10 @@ function shouldRemainOnFallback(
1663 // the level that triggered the fallback, we must continue showing it,
1664 // regardless of what the Suspense context says.
1665 const baseTime = suspenseState.baseTime;
1654 - if (baseTime !== NoWork && baseTime < renderExpirationTime) {
1666 + if (
1667 + !isSameExpirationTime(baseTime, (NoWork: ExpirationTimeOpaque)) &&
1668 + !isSameOrHigherPriority(baseTime, renderExpirationTime)
1669 + ) {
1670 return true;
1671 }
1672 // Otherwise, fall through to check the Suspense context.
@@ -1675,19 +1690,24 @@ function getRemainingWorkInPrimaryTree(
1690 workInProgress: Fiber,
1691 renderExpirationTime,
1692 ) {
1678 - const currentChildExpirationTime = current.childExpirationTime;
1693 + const currentChildExpirationTime = current.childExpirationTime_opaque;
1694 const currentSuspenseState: SuspenseState = current.memoizedState;
1695 if (currentSuspenseState !== null) {
1696 // This boundary already timed out. Check if this render includes the level
1697 // that previously suspended.
1698 const baseTime = currentSuspenseState.baseTime;
1684 - if (baseTime !== NoWork && baseTime < renderExpirationTime) {
1699 + if (
1700 + !isSameExpirationTime(baseTime, (NoWork: ExpirationTimeOpaque)) &&
1701 + !isSameOrHigherPriority(baseTime, renderExpirationTime)
1702 + ) {
1703 // There's pending work at a lower level that might now be unblocked.
1704 return baseTime;
1705 }
1706 }
1707
1690 - if (currentChildExpirationTime < renderExpirationTime) {
1708 + if (
1709 + !isSameOrHigherPriority(currentChildExpirationTime, renderExpirationTime)
1710 + ) {
1711 // The highest priority remaining work is not part of this render. So the
1712 // remaining work has not changed.
1713 return currentChildExpirationTime;
@@ -1702,8 +1722,8 @@ function getRemainingWorkInPrimaryTree(
1722 // separate task thread, this would be: currentChildBits & ~renderBits
1723 const root = getWorkInProgressRoot();
1724 if (root !== null) {
1705 - const lastPendingTime = root.lastPendingTime;
1706 - if (lastPendingTime < renderExpirationTime) {
1725 + const lastPendingTime = root.lastPendingTime_opaque;
1726 + if (!isSameOrHigherPriority(lastPendingTime, renderExpirationTime)) {
1727 return lastPendingTime;
1728 }
1729 }
@@ -1906,7 +1926,7 @@ function updateSuspenseComponent(
1926 // going to render this fragment.
1927 null,
1928 mode,
1909 - NoWork,
1929 + (NoWork: ExpirationTimeOpaque),
1930 null,
1931 );
1932 primaryChildFragment.return = workInProgress;
@@ -1958,7 +1978,7 @@ function updateSuspenseComponent(
1978 fallbackChildFragment.return = workInProgress;
1979 primaryChildFragment.sibling = fallbackChildFragment;
1980 fallbackChildFragment.effectTag |= Placement;
1961 - primaryChildFragment.childExpirationTime = getRemainingWorkInPrimaryTree(
1981 + primaryChildFragment.childExpirationTime_opaque = getRemainingWorkInPrimaryTree(
1982 current,
1983 workInProgress,
1984 renderExpirationTime,
@@ -2028,7 +2048,7 @@ function updateSuspenseComponent(
2048 );
2049 fallbackChildFragment.return = workInProgress;
2050 primaryChildFragment.sibling = fallbackChildFragment;
2031 - primaryChildFragment.childExpirationTime = getRemainingWorkInPrimaryTree(
2051 + primaryChildFragment.childExpirationTime_opaque = getRemainingWorkInPrimaryTree(
2052 current,
2053 workInProgress,
2054 renderExpirationTime,
@@ -2129,7 +2149,7 @@ function updateSuspenseComponent(
2149 fallbackChildFragment.return = workInProgress;
2150 primaryChildFragment.sibling = fallbackChildFragment;
2151 fallbackChildFragment.effectTag |= Placement;
2132 - primaryChildFragment.childExpirationTime = getRemainingWorkInPrimaryTree(
2152 + primaryChildFragment.childExpirationTime_opaque = getRemainingWorkInPrimaryTree(
2153 current,
2154 workInProgress,
2155 renderExpirationTime,
@@ -2158,7 +2178,7 @@ function updateSuspenseComponent(
2178 function retrySuspenseComponentWithoutHydrating(
2179 current: Fiber,
2180 workInProgress: Fiber,
2161 - renderExpirationTime: ExpirationTime,
2181 + renderExpirationTime: ExpirationTimeOpaque,
2182 ) {
2183 // We're now not suspended nor dehydrated.
2184 workInProgress.memoizedState = null;
@@ -2180,7 +2200,7 @@ function retrySuspenseComponentWithoutHydrating(
2200 function mountDehydratedSuspenseComponent(
2201 workInProgress: Fiber,
2202 suspenseInstance: SuspenseInstance,
2183 - renderExpirationTime: ExpirationTime,
2203 + renderExpirationTime: ExpirationTimeOpaque,
2204 ): null | Fiber {
2205 // During the first pass, we'll bail out and not drill into the children.
2206 // Instead, we'll leave the content in place and try to hydrate it later.
@@ -2194,7 +2214,7 @@ function mountDehydratedSuspenseComponent(
2214 'the server rendered components.',
2215 );
2216 }
2197 - workInProgress.expirationTime = Sync;
2217 + workInProgress.expirationTime_opaque = Sync;
2218 } else if (isSuspenseInstanceFallback(suspenseInstance)) {
2219 // This is a client-only boundary. Since we won't get any content from the server
2220 // for this, we need to schedule that at a higher priority based on when it would
@@ -2207,19 +2227,18 @@ function mountDehydratedSuspenseComponent(
2227 // a protocol to transfer that time, we'll just estimate it by using the current
2228 // time. This will mean that Suspense timeouts are slightly shifted to later than
2229 // they should be.
2210 - const serverDisplayTime = requestCurrentTimeForUpdate();
2230 // Schedule a normal pri update to render this content.
2212 - const newExpirationTime = computeAsyncExpiration(serverDisplayTime);
2231 + const newExpirationTime = DefaultUpdateTime;
2232 if (enableSchedulerTracing) {
2233 markSpawnedWork(newExpirationTime);
2234 }
2216 - workInProgress.expirationTime = newExpirationTime;
2235 + workInProgress.expirationTime_opaque = newExpirationTime;
2236 } else {
2237 // We'll continue hydrating the rest at offscreen priority since we'll already
2238 // be showing the right content coming from the server, it is no rush.
2220 - workInProgress.expirationTime = Never;
2239 + workInProgress.expirationTime_opaque = Never;
2240 if (enableSchedulerTracing) {
2222 - markSpawnedWork(Never);
2241 + markSpawnedWork((Never: ExpirationTimeOpaque));
2242 }
2243 }
2244 return null;
@@ -2230,7 +2249,7 @@ function updateDehydratedSuspenseComponent(
2249 workInProgress: Fiber,
2250 suspenseInstance: SuspenseInstance,
2251 suspenseState: SuspenseState,
2233 - renderExpirationTime: ExpirationTime,
2252 + renderExpirationTime: ExpirationTimeOpaque,
2253 ): null | Fiber {
2254 // We should never be hydrating at this point because it is the first pass,
2255 // but after we've already committed once.
@@ -2256,16 +2275,28 @@ function updateDehydratedSuspenseComponent(
2275 }
2276 // We use childExpirationTime to indicate that a child might depend on context, so if
2277 // any context has changed, we need to treat is as if the input might have changed.
2259 - const hasContextChanged = current.childExpirationTime >= renderExpirationTime;
2278 + const hasContextChanged = isSameOrHigherPriority(
2279 + current.childExpirationTime_opaque,
2280 + renderExpirationTime,
2281 + );
2282 if (didReceiveUpdate || hasContextChanged) {
2283 // This boundary has changed since the first render. This means that we are now unable to
2284 // hydrate it. We might still be able to hydrate it using an earlier expiration time, if
2285 // we are rendering at lower expiration than sync.
2264 - if (renderExpirationTime < Sync) {
2265 - if (suspenseState.retryTime <= renderExpirationTime) {
2286 + if (
2287 + !isSameOrHigherPriority(
2288 + renderExpirationTime,
2289 + (Sync: ExpirationTimeOpaque),
2290 + )
2291 + ) {
2292 + if (
2293 + isSameOrHigherPriority(renderExpirationTime, suspenseState.retryTime)
2294 + ) {
2295 // This render is even higher pri than we've seen before, let's try again
2296 // at even higher pri.
2268 - const attemptHydrationAtExpirationTime = renderExpirationTime + 1;
2297 + const attemptHydrationAtExpirationTime = bumpPriorityHigher(
2298 + renderExpirationTime,
2299 + );
2300 suspenseState.retryTime = attemptHydrationAtExpirationTime;
2301 scheduleUpdateOnFiber(current, attemptHydrationAtExpirationTime);
2302 // TODO: Early abort this render.
@@ -2340,14 +2371,22 @@ function updateDehydratedSuspenseComponent(
2371
2372 function scheduleWorkOnFiber(
2373 fiber: Fiber,
2343 - renderExpirationTime: ExpirationTime,
2374 + renderExpirationTime: ExpirationTimeOpaque,
2375 ) {
2345 - if (fiber.expirationTime < renderExpirationTime) {
2346 - fiber.expirationTime = renderExpirationTime;
2376 + if (
2377 + !isSameOrHigherPriority(fiber.expirationTime_opaque, renderExpirationTime)
2378 + ) {
2379 + fiber.expirationTime_opaque = renderExpirationTime;
2380 }
2381 const alternate = fiber.alternate;
2349 - if (alternate !== null && alternate.expirationTime < renderExpirationTime) {
2350 - alternate.expirationTime = renderExpirationTime;
2382 + if (
2383 + alternate !== null &&
2384 + !isSameOrHigherPriority(
2385 + alternate.expirationTime_opaque,
2386 + renderExpirationTime,
2387 + )
2388 + ) {
2389 + alternate.expirationTime_opaque = renderExpirationTime;
2390 }
2391 scheduleWorkOnParentPath(fiber.return, renderExpirationTime);
2392 }
@@ -2355,7 +2394,7 @@ function scheduleWorkOnFiber(
2394 function propagateSuspenseContextChange(
2395 workInProgress: Fiber,
2396 firstChild: null | Fiber,
2358 - renderExpirationTime: ExpirationTime,
2397 + renderExpirationTime: ExpirationTimeOpaque,
2398 ): void {
2399 // Mark any Suspense boundaries with fallbacks as having work to do.
2400 // If they were previously forced into fallbacks, they may now be able
@@ -2605,7 +2644,7 @@ function initSuspenseListRenderState(
2644 function updateSuspenseListComponent(
2645 current: Fiber | null,
2646 workInProgress: Fiber,
2608 - renderExpirationTime: ExpirationTime,
2647 + renderExpirationTime: ExpirationTimeOpaque,
2648 ) {
2649 const nextProps = workInProgress.pendingProps;
2650 const revealOrder: SuspenseListRevealOrder = nextProps.revealOrder;
@@ -2733,7 +2772,7 @@ function updateSuspenseListComponent(
2772 function updatePortalComponent(
2773 current: Fiber | null,
2774 workInProgress: Fiber,
2736 - renderExpirationTime: ExpirationTime,
2775 + renderExpirationTime: ExpirationTimeOpaque,
2776 ) {
2777 pushHostContainer(workInProgress, workInProgress.stateNode.containerInfo);
2778 const nextChildren = workInProgress.pendingProps;
@@ -2763,7 +2802,7 @@ function updatePortalComponent(
2802 function updateContextProvider(
2803 current: Fiber | null,
2804 workInProgress: Fiber,
2766 - renderExpirationTime: ExpirationTime,
2805 + renderExpirationTime: ExpirationTimeOpaque,
2806 ) {
2807 const providerType: ReactProviderType<any> = workInProgress.type;
2808 const context: ReactContext<any> = providerType._context;
@@ -2820,7 +2859,7 @@ let hasWarnedAboutUsingContextAsConsumer = false;
2859 function updateContextConsumer(
2860 current: Fiber | null,
2861 workInProgress: Fiber,
2823 - renderExpirationTime: ExpirationTime,
2862 + renderExpirationTime: ExpirationTimeOpaque,
2863 ) {
2864 let context: ReactContext<any> = workInProgress.type;
2865 // The logic below for Context differs depending on PROD or DEV mode. In
@@ -2921,11 +2960,11 @@ export function markWorkInProgressReceivedUpdate() {
2960 function bailoutOnAlreadyFinishedWork(
2961 current: Fiber | null,
2962 workInProgress: Fiber,
2924 - renderExpirationTime: ExpirationTime,
2963 + renderExpirationTime: ExpirationTimeOpaque,
2964 ): Fiber | null {
2965 if (current !== null) {
2966 // Reuse previous dependencies
2928 - workInProgress.dependencies = current.dependencies;
2967 + workInProgress.dependencies_new = current.dependencies_new;
2968 }
2969
2970 if (enableProfilerTimer) {
@@ -2933,14 +2972,16 @@ function bailoutOnAlreadyFinishedWork(
2972 stopProfilerTimerIfRunning(workInProgress);
2973 }
2974
2936 - const updateExpirationTime = workInProgress.expirationTime;
2937 - if (updateExpirationTime !== NoWork) {
2975 + const updateExpirationTime = workInProgress.expirationTime_opaque;
2976 + if (
2977 + !isSameExpirationTime(updateExpirationTime, (NoWork: ExpirationTimeOpaque))
2978 + ) {
2979 markUnprocessedUpdateTime(updateExpirationTime);
2980 }
2981
2982 // Check if the children have any pending work.
2942 - const childExpirationTime = workInProgress.childExpirationTime;
2943 - if (childExpirationTime < renderExpirationTime) {
2983 + const childExpirationTime = workInProgress.childExpirationTime_opaque;
2984 + if (!isSameOrHigherPriority(childExpirationTime, renderExpirationTime)) {
2985 // The children don't have any work either. We can skip them.
2986 // TODO: Once we add back resuming, we should check if the children are
2987 // a work-in-progress set. If so, we need to transfer their effects.
@@ -3019,9 +3060,9 @@ function remountFiber(
3060 function beginWork(
3061 current: Fiber | null,
3062 workInProgress: Fiber,
3022 - renderExpirationTime: ExpirationTime,
3063 + renderExpirationTime: ExpirationTimeOpaque,
3064 ): Fiber | null {
3024 - const updateExpirationTime = workInProgress.expirationTime;
3065 + const updateExpirationTime = workInProgress.expirationTime_opaque;
3066
3067 if (__DEV__) {
3068 if (workInProgress._debugNeedsRemount && current !== null) {
@@ -3035,7 +3076,7 @@ function beginWork(
3076 workInProgress.pendingProps,
3077 workInProgress._debugOwner || null,
3078 workInProgress.mode,
3038 - workInProgress.expirationTime,
3079 + workInProgress.expirationTime_opaque,
3080 ),
3081 );
3082 }
@@ -3054,7 +3095,9 @@ function beginWork(
3095 // If props or context changed, mark the fiber as having performed work.
3096 // This may be unset if the props are determined to be equal later (memo).
3097 didReceiveUpdate = true;
3057 - } else if (updateExpirationTime < renderExpirationTime) {
3098 + } else if (
3099 + !isSameOrHigherPriority(updateExpirationTime, renderExpirationTime)
3100 + ) {
3101 didReceiveUpdate = false;
3102 // This fiber does not have any pending work. Bailout without entering
3103 // the begin phase. There's still some bookkeeping we that needs to be done
@@ -3068,14 +3111,17 @@ function beginWork(
3111 pushHostContext(workInProgress);
3112 if (
3113 workInProgress.mode & ConcurrentMode &&
3071 - renderExpirationTime !== Never &&
3114 + !isSameExpirationTime(
3115 + renderExpirationTime,
3116 + (Never: ExpirationTimeOpaque),
3117 + ) &&
3118 shouldDeprioritizeSubtree(workInProgress.type, newProps)
3119 ) {
3120 if (enableSchedulerTracing) {
3075 - markSpawnedWork(Never);
3121 + markSpawnedWork((Never: ExpirationTimeOpaque));
3122 }
3123 // Schedule this fiber to re-render at offscreen priority. Then bailout.
3078 - workInProgress.expirationTime = workInProgress.childExpirationTime = Never;
3124 + workInProgress.expirationTime_opaque = workInProgress.childExpirationTime_opaque = Never;
3125 return null;
3126 }
3127 break;
@@ -3100,8 +3146,10 @@ function beginWork(
3146 case Profiler:
3147 if (enableProfilerTimer) {
3148 // Profiler should only call onRender when one of its descendants actually rendered.
3103 - const hasChildWork =
3104 - workInProgress.childExpirationTime >= renderExpirationTime;
3149 + const hasChildWork = isSameOrHigherPriority(
3150 + workInProgress.childExpirationTime_opaque,
3151 + renderExpirationTime,
3152 + );
3153 if (hasChildWork) {
3154 workInProgress.effectTag |= Update;
3155 }
@@ -3136,8 +3184,13 @@ function beginWork(
3184 // child fragment.
3185 const primaryChildFragment: Fiber = (workInProgress.child: any);
3186 const primaryChildExpirationTime =
3139 - primaryChildFragment.childExpirationTime;
3140 - if (primaryChildExpirationTime >= renderExpirationTime) {
3187 + primaryChildFragment.childExpirationTime_opaque;
3188 + if (
3189 + isSameOrHigherPriority(
3190 + primaryChildExpirationTime,
3191 + renderExpirationTime,
3192 + )
3193 + ) {
3194 // The primary children have pending work. Use the normal path
3195 // to attempt to render the primary children again.
3196 return updateSuspenseComponent(
@@ -3162,12 +3215,19 @@ function beginWork(
3215 // based on similar logic in `resetChildExpirationTime`.
3216 let primaryChild = primaryChildFragment.child;
3217 while (primaryChild !== null) {
3165 - const childUpdateExpirationTime = primaryChild.expirationTime;
3218 + const childUpdateExpirationTime =
3219 + primaryChild.expirationTime_opaque;
3220 const childChildExpirationTime =
3167 - primaryChild.childExpirationTime;
3221 + primaryChild.childExpirationTime_opaque;
3222 if (
3169 - childUpdateExpirationTime >= renderExpirationTime ||
3170 - childChildExpirationTime >= renderExpirationTime
3223 + isSameOrHigherPriority(
3224 + childUpdateExpirationTime,
3225 + renderExpirationTime,
3226 + ) ||
3227 + isSameOrHigherPriority(
3228 + childChildExpirationTime,
3229 + renderExpirationTime,
3230 + )
3231 ) {
3232 // Found a child with an update with sufficient priority.
3233 // Use the normal path to render the primary children again.
@@ -3211,8 +3271,10 @@ function beginWork(
3271 const didSuspendBefore =
3272 (current.effectTag & DidCapture) !== NoEffect;
3273
3214 - const hasChildWork =
3215 - workInProgress.childExpirationTime >= renderExpirationTime;
3274 + const hasChildWork = isSameOrHigherPriority(
3275 + workInProgress.childExpirationTime_opaque,
3276 + renderExpirationTime,
3277 + );
3278
3279 if (didSuspendBefore) {
3280 if (hasChildWork) {
@@ -3277,7 +3339,7 @@ function beginWork(
3339 // the update queue. However, there's an exception: SimpleMemoComponent
3340 // sometimes bails out later in the begin phase. This indicates that we should
3341 // move this assignment out of the common path and into each branch.
3280 - workInProgress.expirationTime = NoWork;
3342 + workInProgress.expirationTime_opaque = NoWork;
3343
3344 switch (workInProgress.tag) {
3345 case IndeterminateComponent: {
packages/react-reconciler/src/ReactFiberBeginWork.old.js
+1 -1
@@ -2925,7 +2925,7 @@ function bailoutOnAlreadyFinishedWork(
2925 ): Fiber | null {
2926 if (current !== null) {
2927 // Reuse previous dependencies
2928 - workInProgress.dependencies = current.dependencies;
2928 + workInProgress.dependencies_old = current.dependencies_old;
2929 }
2930
2931 if (enableProfilerTimer) {
packages/react-reconciler/src/ReactFiberClassComponent.new.js
+23 -30
@@ -8,7 +8,7 @@
8 */
9
10 import type {Fiber} from './ReactInternalTypes';
11 -import type {ExpirationTime} from './ReactFiberExpirationTime.new';
11 +import type {ExpirationTimeOpaque} from './ReactFiberExpirationTime.new';
12 import type {UpdateQueue} from './ReactUpdateQueue.new';
13
14 import * as React from 'react';
@@ -40,7 +40,7 @@ import {
40 initializeUpdateQueue,
41 cloneUpdateQueue,
42 } from './ReactUpdateQueue.new';
43 -import {NoWork} from './ReactFiberExpirationTime.new';
43 +import {NoWork, isSameExpirationTime} from './ReactFiberExpirationTime.new';
44 import {
45 cacheContext,
46 getMaskedContext,
@@ -50,8 +50,8 @@ import {
50 } from './ReactFiberContext.new';
51 import {readContext} from './ReactFiberNewContext.new';
52 import {
53 - requestCurrentTimeForUpdate,
54 - computeExpirationForFiber,
53 + requestEventTime,
54 + requestUpdateExpirationTime,
55 scheduleUpdateOnFiber,
56 } from './ReactFiberWorkLoop.new';
57 import {requestCurrentSuspenseConfig} from './ReactFiberSuspenseConfig';
@@ -92,7 +92,7 @@ if (__DEV__) {
92 if (callback === null || typeof callback === 'function') {
93 return;
94 }
95 - const key = `${callerName}_${(callback: any)}`;
95 + const key = callerName + '_' + (callback: any);
96 if (!didWarnOnInvalidCallback.has(key)) {
97 didWarnOnInvalidCallback.add(key);
98 console.error(
@@ -177,7 +177,12 @@ export function applyDerivedStateFromProps(
177
178 // Once the update queue is empty, persist the derived state onto the
179 // base state.
180 - if (workInProgress.expirationTime === NoWork) {
180 + if (
181 + isSameExpirationTime(
182 + workInProgress.expirationTime_opaque,
183 + (NoWork: ExpirationTimeOpaque),
184 + )
185 + ) {
186 // Queue is always non-null for classes
187 const updateQueue: UpdateQueue<any> = (workInProgress.updateQueue: any);
188 updateQueue.baseState = memoizedState;
@@ -188,15 +193,11 @@ const classComponentUpdater = {
193 isMounted,
194 enqueueSetState(inst, payload, callback) {
195 const fiber = getInstance(inst);
191 - const currentTime = requestCurrentTimeForUpdate();
196 + const eventTime = requestEventTime();
197 const suspenseConfig = requestCurrentSuspenseConfig();
193 - const expirationTime = computeExpirationForFiber(
194 - currentTime,
195 - fiber,
196 - suspenseConfig,
197 - );
198 + const expirationTime = requestUpdateExpirationTime(fiber, suspenseConfig);
199
199 - const update = createUpdate(currentTime, expirationTime, suspenseConfig);
200 + const update = createUpdate(eventTime, expirationTime, suspenseConfig);
201 update.payload = payload;
202 if (callback !== undefined && callback !== null) {
203 if (__DEV__) {
@@ -210,15 +211,11 @@ const classComponentUpdater = {
211 },
212 enqueueReplaceState(inst, payload, callback) {
213 const fiber = getInstance(inst);
213 - const currentTime = requestCurrentTimeForUpdate();
214 + const eventTime = requestEventTime();
215 const suspenseConfig = requestCurrentSuspenseConfig();
215 - const expirationTime = computeExpirationForFiber(
216 - currentTime,
217 - fiber,
218 - suspenseConfig,
219 - );
216 + const expirationTime = requestUpdateExpirationTime(fiber, suspenseConfig);
217
221 - const update = createUpdate(currentTime, expirationTime, suspenseConfig);
218 + const update = createUpdate(eventTime, expirationTime, suspenseConfig);
219 update.tag = ReplaceState;
220 update.payload = payload;
221
@@ -234,15 +231,11 @@ const classComponentUpdater = {
231 },
232 enqueueForceUpdate(inst, callback) {
233 const fiber = getInstance(inst);
237 - const currentTime = requestCurrentTimeForUpdate();
234 + const eventTime = requestEventTime();
235 const suspenseConfig = requestCurrentSuspenseConfig();
239 - const expirationTime = computeExpirationForFiber(
240 - currentTime,
241 - fiber,
242 - suspenseConfig,
243 - );
236 + const expirationTime = requestUpdateExpirationTime(fiber, suspenseConfig);
237
245 - const update = createUpdate(currentTime, expirationTime, suspenseConfig);
238 + const update = createUpdate(eventTime, expirationTime, suspenseConfig);
239 update.tag = ForceUpdate;
240
241 if (callback !== undefined && callback !== null) {
@@ -778,7 +771,7 @@ function mountClassInstance(
771 workInProgress: Fiber,
772 ctor: any,
773 newProps: any,
781 - renderExpirationTime: ExpirationTime,
774 + renderExpirationTime: ExpirationTimeOpaque,
775 ): void {
776 if (__DEV__) {
777 checkClassInstance(workInProgress, ctor, newProps);
@@ -873,7 +866,7 @@ function resumeMountClassInstance(
866 workInProgress: Fiber,
867 ctor: any,
868 newProps: any,
876 - renderExpirationTime: ExpirationTime,
869 + renderExpirationTime: ExpirationTimeOpaque,
870 ): boolean {
871 const instance = workInProgress.stateNode;
872
@@ -1008,7 +1001,7 @@ function updateClassInstance(
1001 workInProgress: Fiber,
1002 ctor: any,
1003 newProps: any,
1011 - renderExpirationTime: ExpirationTime,
1004 + renderExpirationTime: ExpirationTimeOpaque,
1005 ): boolean {
1006 const instance = workInProgress.stateNode;
1007
packages/react-reconciler/src/ReactFiberCommitWork.new.js
+3 -3
@@ -17,7 +17,7 @@ import type {
17 } from './ReactFiberHostConfig';
18 import type {Fiber} from './ReactInternalTypes';
19 import type {FiberRoot} from './ReactInternalTypes';
20 -import type {ExpirationTime} from './ReactFiberExpirationTime.new';
20 +import type {ExpirationTimeOpaque} from './ReactFiberExpirationTime.new';
21 import type {SuspenseState} from './ReactFiberSuspenseComponent.new';
22 import type {UpdateQueue} from './ReactUpdateQueue.new';
23 import type {FunctionComponentUpdateQueue} from './ReactFiberHooks.new';
@@ -502,7 +502,7 @@ function commitLifeCycles(
502 finishedRoot: FiberRoot,
503 current: Fiber | null,
504 finishedWork: Fiber,
505 - committedExpirationTime: ExpirationTime,
505 + committedExpirationTime: ExpirationTimeOpaque,
506 ): void {
507 switch (finishedWork.tag) {
508 case FunctionComponent:
@@ -1115,7 +1115,7 @@ function detachFiber(fiber: Fiber) {
1115 // traversal in a later effect. See PR #16820.
1116 fiber.alternate = null;
1117 fiber.child = null;
1118 - fiber.dependencies = null;
1118 + fiber.dependencies_new = null;
1119 fiber.firstEffect = null;
1120 fiber.lastEffect = null;
1121 fiber.memoizedProps = null;
packages/react-reconciler/src/ReactFiberCommitWork.old.js
+1 -1
@@ -1115,7 +1115,7 @@ function detachFiber(fiber: Fiber) {
1115 // traversal in a later effect. See PR #16820.
1116 fiber.alternate = null;
1117 fiber.child = null;
1118 - fiber.dependencies = null;
1118 + fiber.dependencies_old = null;
1119 fiber.firstEffect = null;
1120 fiber.lastEffect = null;
1121 fiber.memoizedProps = null;
packages/react-reconciler/src/ReactFiberCompleteWork.new.js
+15 -8
@@ -8,7 +8,7 @@
8 */
9
10 import type {Fiber} from './ReactInternalTypes';
11 -import type {ExpirationTime} from './ReactFiberExpirationTime.new';
11 +import type {ExpirationTimeOpaque} from './ReactFiberExpirationTime.new';
12 import type {
13 ReactFundamentalComponentInstance,
14 ReactScopeInstance,
@@ -130,7 +130,11 @@ import {
130 renderHasNotSuspendedYet,
131 } from './ReactFiberWorkLoop.new';
132 import {createFundamentalStateInstance} from './ReactFiberFundamental.new';
133 -import {Never} from './ReactFiberExpirationTime.new';
133 +import {
134 + Never,
135 + isSameOrHigherPriority,
136 + bumpPriorityLower,
137 +} from './ReactFiberExpirationTime.new';
138 import {resetChildFibers} from './ReactChildFiber.new';
139 import {updateDeprecatedEventListeners} from './ReactFiberDeprecatedEvents.new';
140 import {createScopeMethods} from './ReactFiberScope.new';
@@ -638,7 +642,7 @@ function cutOffTailIfNeeded(
642 function completeWork(
643 current: Fiber | null,
644 workInProgress: Fiber,
641 - renderExpirationTime: ExpirationTime,
645 + renderExpirationTime: ExpirationTimeOpaque,
646 ): Fiber | null {
647 const newProps = workInProgress.pendingProps;
648
@@ -845,7 +849,7 @@ function completeWork(
849 );
850 prepareToHydrateHostSuspenseInstance(workInProgress);
851 if (enableSchedulerTracing) {
848 - markSpawnedWork(Never);
852 + markSpawnedWork((Never: ExpirationTimeOpaque));
853 }
854 return null;
855 } else {
@@ -870,7 +874,7 @@ function completeWork(
874
875 if ((workInProgress.effectTag & DidCapture) !== NoEffect) {
876 // Something suspended. Re-render with the fallback children.
873 - workInProgress.expirationTime = renderExpirationTime;
877 + workInProgress.expirationTime_opaque = renderExpirationTime;
878 // Do not reset the effect list.
879 return workInProgress;
880 }
@@ -1119,7 +1123,10 @@ function completeWork(
1123 // the expiration.
1124 now() * 2 - renderState.renderingStartTime >
1125 renderState.tailExpiration &&
1122 - renderExpirationTime > Never
1126 + !isSameOrHigherPriority(
1127 + (Never: ExpirationTimeOpaque),
1128 + renderExpirationTime,
1129 + )
1130 ) {
1131 // We have now passed our CPU deadline and we'll just give up further
1132 // attempts to render the main content and only render fallbacks.
@@ -1135,8 +1142,8 @@ function completeWork(
1142 // So we'll pick it back up the very next render pass once we've had
1143 // an opportunity to yield for paint.
1144
1138 - const nextPriority = renderExpirationTime - 1;
1139 - workInProgress.expirationTime = workInProgress.childExpirationTime = nextPriority;
1145 + const nextPriority = bumpPriorityLower(renderExpirationTime);
1146 + workInProgress.expirationTime_opaque = workInProgress.childExpirationTime_opaque = nextPriority;
1147 if (enableSchedulerTracing) {
1148 markSpawnedWork(nextPriority);
1149 }
packages/react-reconciler/src/ReactFiberDeprecatedEvents.new.js
+3 -3
@@ -150,10 +150,10 @@ export function updateDeprecatedEventListeners(
150 rootContainerInstance: null | Container,
151 ): void {
152 const visistedResponders = new Set();
153 - let dependencies = fiber.dependencies;
153 + let dependencies = fiber.dependencies_new;
154 if (listeners != null) {
155 if (dependencies === null) {
156 - dependencies = fiber.dependencies = {
156 + dependencies = fiber.dependencies_new = {
157 expirationTime: NoWork,
158 firstContext: null,
159 responders: new Map(),
@@ -218,7 +218,7 @@ export function createDeprecatedResponderListener(
218 }
219
220 export function unmountDeprecatedResponderListeners(fiber: Fiber) {
221 - const dependencies = fiber.dependencies;
221 + const dependencies = fiber.dependencies_new;
222
223 if (dependencies !== null) {
224 const respondersMap = dependencies.responders;
packages/react-reconciler/src/ReactFiberDeprecatedEvents.old.js
+3 -3
@@ -150,10 +150,10 @@ export function updateDeprecatedEventListeners(
150 rootContainerInstance: null | Container,
151 ): void {
152 const visistedResponders = new Set();
153 - let dependencies = fiber.dependencies;
153 + let dependencies = fiber.dependencies_old;
154 if (listeners != null) {
155 if (dependencies === null) {
156 - dependencies = fiber.dependencies = {
156 + dependencies = fiber.dependencies_old = {
157 expirationTime: NoWork,
158 firstContext: null,
159 responders: new Map(),
@@ -218,7 +218,7 @@ export function createDeprecatedResponderListener(
218 }
219
220 export function unmountDeprecatedResponderListeners(fiber: Fiber) {
221 - const dependencies = fiber.dependencies;
221 + const dependencies = fiber.dependencies_old;
222
223 if (dependencies !== null) {
224 const respondersMap = dependencies.responders;
packages/react-reconciler/src/ReactFiberDevToolsHook.new.js
+5 -11
@@ -8,12 +8,8 @@
8 */
9
10 import {enableProfilerTimer} from 'shared/ReactFeatureFlags';
11 -import {getCurrentTime} from './ReactFiberWorkLoop.new';
12 -import {inferPriorityFromExpirationTime} from './ReactFiberExpirationTime.new';
11
14 -import type {Fiber} from './ReactInternalTypes';
15 -import type {FiberRoot} from './ReactInternalTypes';
16 -import type {ExpirationTime} from './ReactFiberExpirationTime.new';
12 +import type {Fiber, FiberRoot, ReactPriorityLevel} from './ReactInternalTypes';
13 import type {ReactNodeList} from 'shared/ReactTypes';
14
15 import {DidCapture} from './ReactSideEffectTags';
@@ -82,16 +78,14 @@ export function onScheduleRoot(root: FiberRoot, children: ReactNodeList) {
78 }
79 }
80
85 -export function onCommitRoot(root: FiberRoot, expirationTime: ExpirationTime) {
81 +export function onCommitRoot(
82 + root: FiberRoot,
83 + priorityLevel: ReactPriorityLevel,
84 +) {
85 if (injectedHook && typeof injectedHook.onCommitFiberRoot === 'function') {
86 try {
87 const didError = (root.current.effectTag & DidCapture) === DidCapture;
88 if (enableProfilerTimer) {
90 - const currentTime = getCurrentTime();
91 - const priorityLevel = inferPriorityFromExpirationTime(
92 - currentTime,
93 - expirationTime,
94 - );
89 injectedHook.onCommitFiberRoot(
90 rendererID,
91 root,
packages/react-reconciler/src/ReactFiberExpirationTime.new.js
+45 -99
@@ -18,9 +18,9 @@ import {
18 IdlePriority,
19 } from './SchedulerWithReactIntegration.new';
20
21 -export type ExpirationTime = number;
21 +export opaque type ExpirationTimeOpaque = number;
22
23 -export const NoWork = 0;
23 +export const NoWork: ExpirationTimeOpaque = 0;
24 // TODO: Think of a better name for Never. The key difference with Idle is that
25 // Never work can be committed in an inconsistent state without tearing the UI.
26 // The main example is offscreen content, like a hidden subtree. So one possible
@@ -28,119 +28,65 @@ export const NoWork = 0;
28 // which are inconsistent in the sense that they haven't finished yet, but
29 // aren't visibly inconsistent because the server rendered HTML matches what the
30 // hydrated tree would look like.
31 -export const Never = 1;
31 +export const Never: ExpirationTimeOpaque = 1;
32 // Idle is slightly higher priority than Never. It must completely finish in
33 // order to be consistent.
34 -export const Idle = 2;
34 +export const Idle: ExpirationTimeOpaque = 2;
35 // Continuous Hydration is slightly higher than Idle and is used to increase
36 // priority of hover targets.
37 -export const ContinuousHydration = 3;
38 -export const LongTransition = 49999;
39 -export const ShortTransition = 99999;
40 -export const Sync = MAX_SIGNED_31_BIT_INT;
41 -export const Batched = Sync - 1;
37 +export const ContinuousHydration: ExpirationTimeOpaque = 3;
38 +export const LongTransition: ExpirationTimeOpaque = 49999;
39 +export const ShortTransition: ExpirationTimeOpaque = 99999;
40 +export const DefaultUpdateTime: ExpirationTimeOpaque = 1073741296;
41 +export const UserBlockingUpdateTime: ExpirationTimeOpaque = 1073741761;
42 +export const Sync: ExpirationTimeOpaque = MAX_SIGNED_31_BIT_INT;
43 +export const Batched: ExpirationTimeOpaque = Sync - 1;
44
43 -const UNIT_SIZE = 10;
44 -const MAGIC_NUMBER_OFFSET = Batched - 1;
45 -
46 -// 1 unit of expiration time represents 10ms.
47 -export function msToExpirationTime(ms: number): ExpirationTime {
48 - // Always subtract from the offset so that we don't clash with the magic number for NoWork.
49 - return MAGIC_NUMBER_OFFSET - ((ms / UNIT_SIZE) | 0);
50 -}
51 -
52 -export function expirationTimeToMs(expirationTime: ExpirationTime): number {
53 - return (MAGIC_NUMBER_OFFSET - expirationTime) * UNIT_SIZE;
54 -}
55 -
56 -function ceiling(num: number, precision: number): number {
57 - return (((num / precision) | 0) + 1) * precision;
58 -}
59 -
60 -function computeExpirationBucket(
61 - currentTime,
62 - expirationInMs,
63 - bucketSizeMs,
64 -): ExpirationTime {
65 - return (
66 - MAGIC_NUMBER_OFFSET -
67 - ceiling(
68 - MAGIC_NUMBER_OFFSET - currentTime + expirationInMs / UNIT_SIZE,
69 - bucketSizeMs / UNIT_SIZE,
70 - )
71 - );
72 -}
73 -
74 -// TODO: This corresponds to Scheduler's NormalPriority, not LowPriority. Update
75 -// the names to reflect.
76 -export const LOW_PRIORITY_EXPIRATION = 5000;
77 -export const LOW_PRIORITY_BATCH_SIZE = 250;
78 -
79 -export function computeAsyncExpiration(
80 - currentTime: ExpirationTime,
81 -): ExpirationTime {
82 - return computeExpirationBucket(
83 - currentTime,
84 - LOW_PRIORITY_EXPIRATION,
85 - LOW_PRIORITY_BATCH_SIZE,
86 - );
87 -}
88 -
89 -export function computeSuspenseTimeout(
90 - currentTime: ExpirationTime,
91 - timeoutMs: number,
92 -): ExpirationTime {
93 - const currentTimeMs = expirationTimeToMs(currentTime);
94 - const deadlineMs = currentTimeMs + timeoutMs;
95 - return msToExpirationTime(deadlineMs);
96 -}
97 -
98 -// We intentionally set a higher expiration time for interactive updates in
99 -// dev than in production.
100 -//
101 -// If the main thread is being blocked so long that you hit the expiration,
102 -// it's a problem that could be solved with better scheduling.
103 -//
104 -// People will be more likely to notice this and fix it with the long
105 -// expiration time in development.
106 -//
107 -// In production we opt for better UX at the risk of masking scheduling
108 -// problems, by expiring fast.
109 -export const HIGH_PRIORITY_EXPIRATION = __DEV__ ? 500 : 150;
110 -export const HIGH_PRIORITY_BATCH_SIZE = 100;
111 -
112 -export function computeInteractiveExpiration(currentTime: ExpirationTime) {
113 - return computeExpirationBucket(
114 - currentTime,
115 - HIGH_PRIORITY_EXPIRATION,
116 - HIGH_PRIORITY_BATCH_SIZE,
117 - );
118 -}
45 +// Accounts for -1 trick to bump updates into a different batch
46 +const ADJUSTMENT_OFFSET = 5;
47
48 export function inferPriorityFromExpirationTime(
121 - currentTime: ExpirationTime,
122 - expirationTime: ExpirationTime,
49 + expirationTime: ExpirationTimeOpaque,
50 ): ReactPriorityLevel {
124 - if (expirationTime === Sync) {
51 + if (expirationTime >= Batched - ADJUSTMENT_OFFSET) {
52 return ImmediatePriority;
53 }
127 - if (expirationTime === Never || expirationTime === Idle) {
128 - return IdlePriority;
129 - }
130 - const msUntil =
131 - expirationTimeToMs(expirationTime) - expirationTimeToMs(currentTime);
132 - if (msUntil <= 0) {
133 - return ImmediatePriority;
134 - }
135 - if (msUntil <= HIGH_PRIORITY_EXPIRATION + HIGH_PRIORITY_BATCH_SIZE) {
54 + if (expirationTime >= UserBlockingUpdateTime - ADJUSTMENT_OFFSET) {
55 return UserBlockingPriority;
56 }
138 - if (msUntil <= LOW_PRIORITY_EXPIRATION + LOW_PRIORITY_BATCH_SIZE) {
57 + if (expirationTime >= LongTransition - ADJUSTMENT_OFFSET) {
58 return NormalPriority;
59 }
60
142 - // TODO: Handle LowPriority
61 + // TODO: Handle LowPriority. Maybe should give it NormalPriority since Idle is
62 + // very agressively deprioritized.
63
64 // Assume anything lower has idle priority
65 return IdlePriority;
66 }
67 +
68 +export function isSameOrHigherPriority(
69 + a: ExpirationTimeOpaque,
70 + b: ExpirationTimeOpaque,
71 +) {
72 + return a >= b;
73 +}
74 +
75 +export function isSameExpirationTime(
76 + a: ExpirationTimeOpaque,
77 + b: ExpirationTimeOpaque,
78 +) {
79 + return a === b;
80 +}
81 +
82 +export function bumpPriorityHigher(
83 + a: ExpirationTimeOpaque,
84 +): ExpirationTimeOpaque {
85 + return a + 1;
86 +}
87 +
88 +export function bumpPriorityLower(
89 + a: ExpirationTimeOpaque,
90 +): ExpirationTimeOpaque {
91 + return a - 1;
92 +}
packages/react-reconciler/src/ReactFiberHooks.new.js
+49 -32
@@ -16,7 +16,7 @@ import type {
16 ReactEventResponderListener,
17 } from 'shared/ReactTypes';
18 import type {Fiber, Dispatcher} from './ReactInternalTypes';
19 -import type {ExpirationTime} from './ReactFiberExpirationTime.new';
19 +import type {ExpirationTimeOpaque} from './ReactFiberExpirationTime.new';
20 import type {HookEffectTag} from './ReactHookEffectTags';
21 import type {SuspenseConfig} from './ReactFiberSuspenseConfig';
22 import type {ReactPriorityLevel} from './ReactInternalTypes';
@@ -26,7 +26,12 @@ import type {OpaqueIDType} from './ReactFiberHostConfig';
26 import ReactSharedInternals from 'shared/ReactSharedInternals';
27
28 import {markRootExpiredAtTime} from './ReactFiberRoot.new';
29 -import {NoWork, Sync} from './ReactFiberExpirationTime.new';
29 +import {
30 + NoWork,
31 + Sync,
32 + isSameOrHigherPriority,
33 + isSameExpirationTime,
34 +} from './ReactFiberExpirationTime.new';
35 import {NoMode, BlockingMode} from './ReactTypeOfMode';
36 import {readContext} from './ReactFiberNewContext.new';
37 import {createDeprecatedResponderListener} from './ReactFiberDeprecatedEvents.new';
@@ -42,8 +47,8 @@ import {
47 import {
48 getWorkInProgressRoot,
49 scheduleUpdateOnFiber,
45 - computeExpirationForFiber,
46 - requestCurrentTimeForUpdate,
50 + requestUpdateExpirationTime,
51 + requestEventTime,
52 warnIfNotCurrentlyActingEffectsInDEV,
53 warnIfNotCurrentlyActingUpdatesInDev,
54 warnIfNotScopedWithMatchingAct,
@@ -83,8 +88,8 @@ const {ReactCurrentDispatcher, ReactCurrentBatchConfig} = ReactSharedInternals;
88 type Update<S, A> = {|
89 // TODO: Temporary field. Will remove this by storing a map of
90 // transition -> start time on the root.
86 - eventTime: ExpirationTime,
87 - expirationTime: ExpirationTime,
91 + eventTime: number,
92 + expirationTime: ExpirationTimeOpaque,
93 suspenseConfig: null | SuspenseConfig,
94 action: A,
95 eagerReducer: ((S, A) => S) | null,
@@ -151,7 +156,7 @@ type BasicStateAction<S> = (S => S) | S;
156 type Dispatch<A> = A => void;
157
158 // These are set right before calling the component.
154 -let renderExpirationTime: ExpirationTime = NoWork;
159 +let renderExpirationTime: ExpirationTimeOpaque = NoWork;
160 // The work-in-progress fiber. I've named it differently to distinguish it from
161 // the work-in-progress hook.
162 let currentlyRenderingFiber: Fiber = (null: any);
@@ -342,7 +347,7 @@ export function renderWithHooks<Props, SecondArg>(
347 Component: (p: Props, arg: SecondArg) => any,
348 props: Props,
349 secondArg: SecondArg,
345 - nextRenderExpirationTime: ExpirationTime,
350 + nextRenderExpirationTime: ExpirationTimeOpaque,
351 ): any {
352 renderExpirationTime = nextRenderExpirationTime;
353 currentlyRenderingFiber = workInProgress;
@@ -360,7 +365,7 @@ export function renderWithHooks<Props, SecondArg>(
365
366 workInProgress.memoizedState = null;
367 workInProgress.updateQueue = null;
363 - workInProgress.expirationTime = NoWork;
368 + workInProgress.expirationTime_opaque = NoWork;
369
370 // The following should have already been reset
371 // currentHook = null;
@@ -475,12 +480,12 @@ export function renderWithHooks<Props, SecondArg>(
480 export function bailoutHooks(
481 current: Fiber,
482 workInProgress: Fiber,
478 - expirationTime: ExpirationTime,
483 + expirationTime: ExpirationTimeOpaque,
484 ) {
485 workInProgress.updateQueue = current.updateQueue;
486 workInProgress.effectTag &= ~(PassiveEffect | UpdateEffect);
482 - if (current.expirationTime <= expirationTime) {
483 - current.expirationTime = NoWork;
487 + if (isSameOrHigherPriority(expirationTime, current.expirationTime_opaque)) {
488 + current.expirationTime_opaque = NoWork;
489 }
490 }
491
@@ -705,7 +710,7 @@ function updateReducer<S, I, A>(
710 const suspenseConfig = update.suspenseConfig;
711 const updateExpirationTime = update.expirationTime;
712 const updateEventTime = update.eventTime;
708 - if (updateExpirationTime < renderExpirationTime) {
713 + if (!isSameOrHigherPriority(updateExpirationTime, renderExpirationTime)) {
714 // Priority is insufficient. Skip this update. If this is the first
715 // skipped update, the previous update/state is the new base
716 // update/state.
@@ -725,8 +730,13 @@ function updateReducer<S, I, A>(
730 newBaseQueueLast = newBaseQueueLast.next = clone;
731 }
732 // Update the remaining priority in the queue.
728 - if (updateExpirationTime > currentlyRenderingFiber.expirationTime) {
729 - currentlyRenderingFiber.expirationTime = updateExpirationTime;
733 + if (
734 + !isSameOrHigherPriority(
735 + currentlyRenderingFiber.expirationTime_opaque,
736 + updateExpirationTime,
737 + )
738 + ) {
739 + currentlyRenderingFiber.expirationTime_opaque = updateExpirationTime;
740 markUnprocessedUpdateTime(updateExpirationTime);
741 }
742 } else {
@@ -877,12 +887,20 @@ function readFromUnsubcribedMutableSource<Source, Snapshot>(
887 // If there's no version, then we should fallback to checking the update time.
888 const pendingExpirationTime = getLastPendingExpirationTime(root);
889
880 - if (pendingExpirationTime === NoWork) {
890 + if (
891 + isSameExpirationTime(
892 + pendingExpirationTime,
893 + (NoWork: ExpirationTimeOpaque),
894 + )
895 + ) {
896 isSafeToReadFromSource = true;
897 } else {
898 // If the source has pending updates, we can use the current render's expiration
899 // time to determine if it's safe to read again from the source.
885 - isSafeToReadFromSource = pendingExpirationTime >= renderExpirationTime;
900 + isSafeToReadFromSource = isSameOrHigherPriority(
901 + pendingExpirationTime,
902 + renderExpirationTime,
903 + );
904 }
905
906 if (isSafeToReadFromSource) {
@@ -975,10 +993,8 @@ function useMutableSource<Source, Snapshot>(
993 if (!is(snapshot, maybeNewSnapshot)) {
994 setSnapshot(maybeNewSnapshot);
995
978 - const currentTime = requestCurrentTimeForUpdate();
996 const suspenseConfig = requestCurrentSuspenseConfig();
980 - const expirationTime = computeExpirationForFiber(
981 - currentTime,
997 + const expirationTime = requestUpdateExpirationTime(
998 fiber,
999 suspenseConfig,
1000 );
@@ -1005,10 +1021,8 @@ function useMutableSource<Source, Snapshot>(
1021 latestSetSnapshot(latestGetSnapshot(source._source));
1022
1023 // Record a pending mutable source update with the same expiration time.
1008 - const currentTime = requestCurrentTimeForUpdate();
1024 const suspenseConfig = requestCurrentSuspenseConfig();
1010 - const expirationTime = computeExpirationForFiber(
1011 - currentTime,
1025 + const expirationTime = requestUpdateExpirationTime(
1026 fiber,
1027 suspenseConfig,
1028 );
@@ -1630,16 +1644,12 @@ function dispatchAction<S, A>(
1644 }
1645 }
1646
1633 - const currentTime = requestCurrentTimeForUpdate();
1647 + const eventTime = requestEventTime();
1648 const suspenseConfig = requestCurrentSuspenseConfig();
1635 - const expirationTime = computeExpirationForFiber(
1636 - currentTime,
1637 - fiber,
1638 - suspenseConfig,
1639 - );
1649 + const expirationTime = requestUpdateExpirationTime(fiber, suspenseConfig);
1650
1651 const update: Update<S, A> = {
1642 - eventTime: currentTime,
1652 + eventTime,
1653 expirationTime,
1654 suspenseConfig,
1655 action,
@@ -1671,8 +1681,15 @@ function dispatchAction<S, A>(
1681 update.expirationTime = renderExpirationTime;
1682 } else {
1683 if (
1674 - fiber.expirationTime === NoWork &&
1675 - (alternate === null || alternate.expirationTime === NoWork)
1684 + isSameExpirationTime(
1685 + fiber.expirationTime_opaque,
1686 + (NoWork: ExpirationTimeOpaque),
1687 + ) &&
1688 + (alternate === null ||
1689 + isSameExpirationTime(
1690 + alternate.expirationTime_opaque,
1691 + (NoWork: ExpirationTimeOpaque),
1692 + ))
1693 ) {
1694 // The queue is currently empty, which means we can eagerly compute the
1695 // next state before entering the render phase. If the new state is the
packages/react-reconciler/src/ReactFiberHotReloading.new.js
+2 -1
@@ -12,6 +12,7 @@ import type {Fiber} from './ReactInternalTypes';
12 import type {FiberRoot} from './ReactInternalTypes';
13 import type {Instance} from './ReactFiberHostConfig';
14 import type {ReactNodeList} from 'shared/ReactTypes';
15 +import type {ExpirationTimeOpaque} from './ReactFiberExpirationTime.new';
16
17 import {
18 flushSync,
@@ -319,7 +320,7 @@ function scheduleFibersWithFamiliesRecursively(
320 fiber._debugNeedsRemount = true;
321 }
322 if (needsRemount || needsRender) {
322 - scheduleUpdateOnFiber(fiber, Sync);
323 + scheduleUpdateOnFiber(fiber, (Sync: ExpirationTimeOpaque));
324 }
325 if (child !== null && !needsRemount) {
326 scheduleFibersWithFamiliesRecursively(
packages/react-reconciler/src/ReactFiberLane.js new
+22
@@ -0,0 +1,22 @@
1 +/**
2 + * Copyright (c) Facebook, Inc. and its affiliates.
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 + * @flow
8 + */
9 +
10 +export opaque type LanePriority = 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10;
11 +
12 +export const SyncLanePriority: LanePriority = 10;
13 +export const SyncBatchedLanePriority: LanePriority = 9;
14 +export const InputDiscreteLanePriority: LanePriority = 8;
15 +export const InputContinuousLanePriority: LanePriority = 7;
16 +export const DefaultLanePriority: LanePriority = 6;
17 +export const TransitionShortLanePriority: LanePriority = 5;
18 +export const TransitionLongLanePriority: LanePriority = 4;
19 +export const HydrationContinuousLanePriority: LanePriority = 3;
20 +export const IdleLanePriority: LanePriority = 2;
21 +export const OffscreenLanePriority: LanePriority = 1;
22 +export const NoLanePriority: LanePriority = 0;
packages/react-reconciler/src/ReactFiberNewContext.new.js
+59 -24
@@ -10,7 +10,7 @@
10 import type {ReactContext} from 'shared/ReactTypes';
11 import type {Fiber, ContextDependency} from './ReactInternalTypes';
12 import type {StackCursor} from './ReactFiberStack.new';
13 -import type {ExpirationTime} from './ReactFiberExpirationTime.new';
13 +import type {ExpirationTimeOpaque} from './ReactFiberExpirationTime.new';
14
15 import {isPrimaryRenderer} from './ReactFiberHostConfig';
16 import {createCursor, push, pop} from './ReactFiberStack.new';
@@ -20,6 +20,7 @@ import {
20 ClassComponent,
21 DehydratedFragment,
22 } from './ReactWorkTags';
23 +import {isSameOrHigherPriority} from './ReactFiberExpirationTime.new';
24
25 import invariant from 'shared/invariant';
26 import is from 'shared/objectIs';
@@ -147,26 +148,37 @@ export function calculateChangedBits<T>(
148
149 export function scheduleWorkOnParentPath(
150 parent: Fiber | null,
150 - renderExpirationTime: ExpirationTime,
151 + renderExpirationTime: ExpirationTimeOpaque,
152 ) {
153 // Update the child expiration time of all the ancestors, including
154 // the alternates.
155 let node = parent;
156 while (node !== null) {
157 const alternate = node.alternate;
157 - if (node.childExpirationTime < renderExpirationTime) {
158 - node.childExpirationTime = renderExpirationTime;
158 + if (
159 + !isSameOrHigherPriority(
160 + node.childExpirationTime_opaque,
161 + renderExpirationTime,
162 + )
163 + ) {
164 + node.childExpirationTime_opaque = renderExpirationTime;
165 if (
166 alternate !== null &&
161 - alternate.childExpirationTime < renderExpirationTime
167 + !isSameOrHigherPriority(
168 + alternate.childExpirationTime_opaque,
169 + renderExpirationTime,
170 + )
171 ) {
163 - alternate.childExpirationTime = renderExpirationTime;
172 + alternate.childExpirationTime_opaque = renderExpirationTime;
173 }
174 } else if (
175 alternate !== null &&
167 - alternate.childExpirationTime < renderExpirationTime
176 + !isSameOrHigherPriority(
177 + alternate.childExpirationTime_opaque,
178 + renderExpirationTime,
179 + )
180 ) {
169 - alternate.childExpirationTime = renderExpirationTime;
181 + alternate.childExpirationTime_opaque = renderExpirationTime;
182 } else {
183 // Neither alternate was updated, which means the rest of the
184 // ancestor path already has sufficient priority.
@@ -180,7 +192,7 @@ export function propagateContextChange(
192 workInProgress: Fiber,
193 context: ReactContext<mixed>,
194 changedBits: number,
183 - renderExpirationTime: ExpirationTime,
195 + renderExpirationTime: ExpirationTimeOpaque,
196 ): void {
197 let fiber = workInProgress.child;
198 if (fiber !== null) {
@@ -191,7 +203,7 @@ export function propagateContextChange(
203 let nextFiber;
204
205 // Visit this fiber.
194 - const list = fiber.dependencies;
206 + const list = fiber.dependencies_new;
207 if (list !== null) {
208 nextFiber = fiber.child;
209
@@ -206,7 +218,7 @@ export function propagateContextChange(
218
219 if (fiber.tag === ClassComponent) {
220 // Schedule a force update on the work-in-progress.
209 - const update = createUpdate(NoWork, renderExpirationTime, null);
221 + const update = createUpdate(-1, renderExpirationTime, null);
222 update.tag = ForceUpdate;
223 // TODO: Because we don't have a work-in-progress, this will add the
224 // update to the current fiber, too, which means it will persist even if
@@ -215,21 +227,31 @@ export function propagateContextChange(
227 enqueueUpdate(fiber, update);
228 }
229
218 - if (fiber.expirationTime < renderExpirationTime) {
219 - fiber.expirationTime = renderExpirationTime;
230 + if (
231 + !isSameOrHigherPriority(
232 + fiber.expirationTime_opaque,
233 + renderExpirationTime,
234 + )
235 + ) {
236 + fiber.expirationTime_opaque = renderExpirationTime;
237 }
238 const alternate = fiber.alternate;
239 if (
240 alternate !== null &&
224 - alternate.expirationTime < renderExpirationTime
241 + !isSameOrHigherPriority(
242 + alternate.expirationTime_opaque,
243 + renderExpirationTime,
244 + )
245 ) {
226 - alternate.expirationTime = renderExpirationTime;
246 + alternate.expirationTime_opaque = renderExpirationTime;
247 }
248
249 scheduleWorkOnParentPath(fiber.return, renderExpirationTime);
250
251 // Mark the expiration time on the list, too.
232 - if (list.expirationTime < renderExpirationTime) {
252 + if (
253 + !isSameOrHigherPriority(list.expirationTime, renderExpirationTime)
254 + ) {
255 list.expirationTime = renderExpirationTime;
256 }
257
@@ -254,15 +276,23 @@ export function propagateContextChange(
276 parentSuspense !== null,
277 'We just came from a parent so we must have had a parent. This is a bug in React.',
278 );
257 - if (parentSuspense.expirationTime < renderExpirationTime) {
258 - parentSuspense.expirationTime = renderExpirationTime;
279 + if (
280 + !isSameOrHigherPriority(
281 + parentSuspense.expirationTime_opaque,
282 + renderExpirationTime,
283 + )
284 + ) {
285 + parentSuspense.expirationTime_opaque = renderExpirationTime;
286 }
287 const alternate = parentSuspense.alternate;
288 if (
289 alternate !== null &&
263 - alternate.expirationTime < renderExpirationTime
290 + !isSameOrHigherPriority(
291 + alternate.expirationTime_opaque,
292 + renderExpirationTime,
293 + )
294 ) {
265 - alternate.expirationTime = renderExpirationTime;
295 + alternate.expirationTime_opaque = renderExpirationTime;
296 }
297 // This is intentionally passing this fiber as the parent
298 // because we want to schedule this fiber as having work
@@ -304,17 +334,22 @@ export function propagateContextChange(
334
335 export function prepareToReadContext(
336 workInProgress: Fiber,
307 - renderExpirationTime: ExpirationTime,
337 + renderExpirationTime: ExpirationTimeOpaque,
338 ): void {
339 currentlyRenderingFiber = workInProgress;
340 lastContextDependency = null;
341 lastContextWithAllBitsObserved = null;
342
313 - const dependencies = workInProgress.dependencies;
343 + const dependencies = workInProgress.dependencies_new;
344 if (dependencies !== null) {
345 const firstContext = dependencies.firstContext;
346 if (firstContext !== null) {
317 - if (dependencies.expirationTime >= renderExpirationTime) {
347 + if (
348 + isSameOrHigherPriority(
349 + dependencies.expirationTime,
350 + renderExpirationTime,
351 + )
352 + ) {
353 // Context list has a pending update. Mark that this fiber performed work.
354 markWorkInProgressReceivedUpdate();
355 }
@@ -375,7 +410,7 @@ export function readContext<T>(
410
411 // This is the first dependency for this component. Create a new list.
412 lastContextDependency = contextItem;
378 - currentlyRenderingFiber.dependencies = {
413 + currentlyRenderingFiber.dependencies_new = {
414 expirationTime: NoWork,
415 firstContext: contextItem,
416 responders: null,
packages/react-reconciler/src/ReactFiberNewContext.old.js
+3 -3
@@ -191,7 +191,7 @@ export function propagateContextChange(
191 let nextFiber;
192
193 // Visit this fiber.
194 - const list = fiber.dependencies;
194 + const list = fiber.dependencies_old;
195 if (list !== null) {
196 nextFiber = fiber.child;
197
@@ -310,7 +310,7 @@ export function prepareToReadContext(
310 lastContextDependency = null;
311 lastContextWithAllBitsObserved = null;
312
313 - const dependencies = workInProgress.dependencies;
313 + const dependencies = workInProgress.dependencies_old;
314 if (dependencies !== null) {
315 const firstContext = dependencies.firstContext;
316 if (firstContext !== null) {
@@ -375,7 +375,7 @@ export function readContext<T>(
375
376 // This is the first dependency for this component. Create a new list.
377 lastContextDependency = contextItem;
378 - currentlyRenderingFiber.dependencies = {
378 + currentlyRenderingFiber.dependencies_old = {
379 expirationTime: NoWork,
380 firstContext: contextItem,
381 responders: null,
packages/react-reconciler/src/ReactFiberReconciler.new.js
+30 -28
@@ -19,7 +19,7 @@ import type {
19 import type {RendererInspectionConfig} from './ReactFiberHostConfig';
20 import {FundamentalComponent} from './ReactWorkTags';
21 import type {ReactNodeList} from 'shared/ReactTypes';
22 -import type {ExpirationTime} from './ReactFiberExpirationTime.new';
22 +import type {ExpirationTimeOpaque} from './ReactFiberExpirationTime.new';
23 import type {SuspenseState} from './ReactFiberSuspenseComponent.new';
24
25 import {
@@ -46,8 +46,8 @@ import {
46 import {createFiberRoot} from './ReactFiberRoot.new';
47 import {injectInternals, onScheduleRoot} from './ReactFiberDevToolsHook.new';
48 import {
49 - requestCurrentTimeForUpdate,
50 - computeExpirationForFiber,
49 + requestEventTime,
50 + requestUpdateExpirationTime,
51 scheduleUpdateOnFiber,
52 flushRoot,
53 batchedEventUpdates,
@@ -76,7 +76,8 @@ import {StrictMode} from './ReactTypeOfMode';
76 import {
77 Sync,
78 ContinuousHydration,
79 - computeInteractiveExpiration,
79 + UserBlockingUpdateTime,
80 + isSameOrHigherPriority,
81 } from './ReactFiberExpirationTime.new';
82 import {requestCurrentSuspenseConfig} from './ReactFiberSuspenseConfig';
83 import {
@@ -234,12 +235,12 @@ export function updateContainer(
235 container: OpaqueRoot,
236 parentComponent: ?React$Component<any, any>,
237 callback: ?Function,
237 -): ExpirationTime {
238 +): ExpirationTimeOpaque {
239 if (__DEV__) {
240 onScheduleRoot(container, element);
241 }
242 const current = container.current;
242 - const currentTime = requestCurrentTimeForUpdate();
243 + const eventTime = requestEventTime();
244 if (__DEV__) {
245 // $FlowExpectedError - jest isn't a global, and isn't recognized outside of tests
246 if ('undefined' !== typeof jest) {
@@ -248,11 +249,7 @@ export function updateContainer(
249 }
250 }
251 const suspenseConfig = requestCurrentSuspenseConfig();
251 - const expirationTime = computeExpirationForFiber(
252 - currentTime,
253 - current,
254 - suspenseConfig,
255 - );
252 + const expirationTime = requestUpdateExpirationTime(current, suspenseConfig);
253
254 const context = getContextForSubtree(parentComponent);
255 if (container.context === null) {
@@ -278,7 +275,7 @@ export function updateContainer(
275 }
276 }
277
281 - const update = createUpdate(currentTime, expirationTime, suspenseConfig);
278 + const update = createUpdate(eventTime, expirationTime, suspenseConfig);
279 // Caution: React DevTools currently depends on this property
280 // being called "element".
281 update.payload = {element};
@@ -339,33 +336,36 @@ export function attemptSynchronousHydration(fiber: Fiber): void {
336 const root: FiberRoot = fiber.stateNode;
337 if (root.hydrate) {
338 // Flush the first scheduled "update".
342 - flushRoot(root, root.firstPendingTime);
339 + flushRoot(root, root.firstPendingTime_opaque);
340 }
341 break;
342 case SuspenseComponent:
346 - flushSync(() => scheduleUpdateOnFiber(fiber, Sync));
343 + flushSync(() =>
344 + scheduleUpdateOnFiber(fiber, (Sync: ExpirationTimeOpaque)),
345 + );
346 // If we're still blocked after this, we need to increase
347 // the priority of any promises resolving within this
348 // boundary so that they next attempt also has higher pri.
350 - const retryExpTime = computeInteractiveExpiration(
351 - requestCurrentTimeForUpdate(),
352 - );
349 + const retryExpTime = UserBlockingUpdateTime;
350 markRetryTimeIfNotHydrated(fiber, retryExpTime);
351 break;
352 }
353 }
354
358 -function markRetryTimeImpl(fiber: Fiber, retryTime: ExpirationTime) {
355 +function markRetryTimeImpl(fiber: Fiber, retryTime: ExpirationTimeOpaque) {
356 const suspenseState: null | SuspenseState = fiber.memoizedState;
357 if (suspenseState !== null && suspenseState.dehydrated !== null) {
361 - if (suspenseState.retryTime < retryTime) {
358 + if (!isSameOrHigherPriority(suspenseState.retryTime, retryTime)) {
359 suspenseState.retryTime = retryTime;
360 }
361 }
362 }
363
364 // Increases the priority of thennables when they resolve within this boundary.
368 -function markRetryTimeIfNotHydrated(fiber: Fiber, retryTime: ExpirationTime) {
365 +function markRetryTimeIfNotHydrated(
366 + fiber: Fiber,
367 + retryTime: ExpirationTimeOpaque,
368 +) {
369 markRetryTimeImpl(fiber, retryTime);
370 const alternate = fiber.alternate;
371 if (alternate) {
@@ -381,7 +381,7 @@ export function attemptUserBlockingHydration(fiber: Fiber): void {
381 // Suspense.
382 return;
383 }
384 - const expTime = computeInteractiveExpiration(requestCurrentTimeForUpdate());
384 + const expTime = UserBlockingUpdateTime;
385 scheduleUpdateOnFiber(fiber, expTime);
386 markRetryTimeIfNotHydrated(fiber, expTime);
387 }
@@ -394,8 +394,11 @@ export function attemptContinuousHydration(fiber: Fiber): void {
394 // Suspense.
395 return;
396 }
397 - scheduleUpdateOnFiber(fiber, ContinuousHydration);
398 - markRetryTimeIfNotHydrated(fiber, ContinuousHydration);
397 + scheduleUpdateOnFiber(fiber, (ContinuousHydration: ExpirationTimeOpaque));
398 + markRetryTimeIfNotHydrated(
399 + fiber,
400 + (ContinuousHydration: ExpirationTimeOpaque),
401 + );
402 }
403
404 export function attemptHydrationAtCurrentPriority(fiber: Fiber): void {
@@ -404,8 +407,7 @@ export function attemptHydrationAtCurrentPriority(fiber: Fiber): void {
407 // their priority other than synchronously flush it.
408 return;
409 }
407 - const currentTime = requestCurrentTimeForUpdate();
408 - const expTime = computeExpirationForFiber(currentTime, fiber, null);
410 + const expTime = requestUpdateExpirationTime(fiber, null);
411 scheduleUpdateOnFiber(fiber, expTime);
412 markRetryTimeIfNotHydrated(fiber, expTime);
413 }
@@ -489,7 +491,7 @@ if (__DEV__) {
491 // Shallow cloning props works as a workaround for now to bypass the bailout check.
492 fiber.memoizedProps = {...fiber.memoizedProps};
493
492 - scheduleUpdateOnFiber(fiber, Sync);
494 + scheduleUpdateOnFiber(fiber, (Sync: ExpirationTimeOpaque));
495 }
496 };
497
@@ -499,11 +501,11 @@ if (__DEV__) {
501 if (fiber.alternate) {
502 fiber.alternate.pendingProps = fiber.pendingProps;
503 }
502 - scheduleUpdateOnFiber(fiber, Sync);
504 + scheduleUpdateOnFiber(fiber, (Sync: ExpirationTimeOpaque));
505 };
506
507 scheduleUpdate = (fiber: Fiber) => {
506 - scheduleUpdateOnFiber(fiber, Sync);
508 + scheduleUpdateOnFiber(fiber, (Sync: ExpirationTimeOpaque));
509 };
510
511 setSuspenseHandler = (newShouldSuspendImpl: Fiber => boolean) => {
packages/react-reconciler/src/ReactFiberRoot.new.js
+105 -64
@@ -8,48 +8,55 @@
8 */
9
10 import type {FiberRoot, SuspenseHydrationCallbacks} from './ReactInternalTypes';
11 -import type {ExpirationTime} from './ReactFiberExpirationTime.new';
11 +import type {ExpirationTimeOpaque} from './ReactFiberExpirationTime.new';
12 import type {RootTag} from './ReactRootTags';
13
14 import {noTimeout} from './ReactFiberHostConfig';
15 import {createHostRootFiber} from './ReactFiber.new';
16 -import {NoWork} from './ReactFiberExpirationTime.new';
16 +import {
17 + NoWork,
18 + isSameOrHigherPriority,
19 + isSameExpirationTime,
20 + bumpPriorityHigher,
21 + bumpPriorityLower,
22 +} from './ReactFiberExpirationTime.new';
23 import {
24 enableSchedulerTracing,
25 enableSuspenseCallback,
26 } from 'shared/ReactFeatureFlags';
27 import {unstable_getThreadID} from 'scheduler/tracing';
22 -import {NoPriority} from './SchedulerWithReactIntegration.new';
28 import {initializeUpdateQueue} from './ReactUpdateQueue.new';
29 import {clearPendingUpdates as clearPendingMutableSourceUpdates} from './ReactMutableSource.new';
30
31 function FiberRootNode(containerInfo, tag, hydrate) {
32 this.tag = tag;
28 - this.current = null;
33 this.containerInfo = containerInfo;
34 this.pendingChildren = null;
35 + this.current = null;
36 this.pingCache = null;
32 - this.finishedExpirationTime = NoWork;
37 this.finishedWork = null;
38 + this.finishedExpirationTime_opaque = NoWork;
39 this.timeoutHandle = noTimeout;
40 this.context = null;
41 this.pendingContext = null;
42 this.hydrate = hydrate;
43 this.callbackNode = null;
39 - this.callbackPriority = NoPriority;
40 - this.firstPendingTime = NoWork;
41 - this.lastPendingTime = NoWork;
42 - this.firstSuspendedTime = NoWork;
43 - this.lastSuspendedTime = NoWork;
44 - this.nextKnownPendingLevel = NoWork;
45 - this.lastPingedTime = NoWork;
46 - this.lastExpiredTime = NoWork;
47 - this.mutableSourceLastPendingUpdateTime = NoWork;
44 + this.callbackId = NoWork;
45 + this.callbackIsSync = false;
46 + this.expiresAt = -1;
47 + this.firstPendingTime_opaque = NoWork;
48 + this.lastPendingTime_opaque = NoWork;
49 + this.firstSuspendedTime_opaque = NoWork;
50 + this.lastSuspendedTime_opaque = NoWork;
51 + this.nextKnownPendingLevel_opaque = NoWork;
52 + this.lastPingedTime_opaque = NoWork;
53 + this.lastExpiredTime_opaque = NoWork;
54 + this.mutableSourceLastPendingUpdateTime_opaque = NoWork;
55
56 if (enableSchedulerTracing) {
57 this.interactionThreadID = unstable_getThreadID();
58 this.memoizedInteractions = new Set();
52 - this.pendingInteractionMap = new Map();
59 + this.pendingInteractionMap_new = new Map();
60 }
61 if (enableSuspenseCallback) {
62 this.hydrationCallbacks = null;
@@ -80,105 +87,136 @@ export function createFiberRoot(
87
88 export function isRootSuspendedAtTime(
89 root: FiberRoot,
83 - expirationTime: ExpirationTime,
90 + expirationTime: ExpirationTimeOpaque,
91 ): boolean {
85 - const firstSuspendedTime = root.firstSuspendedTime;
86 - const lastSuspendedTime = root.lastSuspendedTime;
92 + const firstSuspendedTime = root.firstSuspendedTime_opaque;
93 + const lastSuspendedTime = root.lastSuspendedTime_opaque;
94 return (
88 - firstSuspendedTime !== NoWork &&
89 - firstSuspendedTime >= expirationTime &&
90 - lastSuspendedTime <= expirationTime
95 + !isSameExpirationTime(firstSuspendedTime, (NoWork: ExpirationTimeOpaque)) &&
96 + isSameOrHigherPriority(firstSuspendedTime, expirationTime) &&
97 + isSameOrHigherPriority(expirationTime, lastSuspendedTime)
98 );
99 }
100
101 export function markRootSuspendedAtTime(
102 root: FiberRoot,
96 - expirationTime: ExpirationTime,
103 + expirationTime: ExpirationTimeOpaque,
104 ): void {
98 - const firstSuspendedTime = root.firstSuspendedTime;
99 - const lastSuspendedTime = root.lastSuspendedTime;
100 - if (firstSuspendedTime < expirationTime) {
101 - root.firstSuspendedTime = expirationTime;
105 + const firstSuspendedTime = root.firstSuspendedTime_opaque;
106 + const lastSuspendedTime = root.lastSuspendedTime_opaque;
107 + if (!isSameOrHigherPriority(firstSuspendedTime, expirationTime)) {
108 + root.firstSuspendedTime_opaque = expirationTime;
109 }
103 - if (lastSuspendedTime > expirationTime || firstSuspendedTime === NoWork) {
104 - root.lastSuspendedTime = expirationTime;
110 + if (
111 + !isSameOrHigherPriority(expirationTime, lastSuspendedTime) ||
112 + isSameExpirationTime(firstSuspendedTime, (NoWork: ExpirationTimeOpaque))
113 + ) {
114 + root.lastSuspendedTime_opaque = expirationTime;
115 }
116
107 - if (expirationTime <= root.lastPingedTime) {
108 - root.lastPingedTime = NoWork;
117 + if (isSameOrHigherPriority(root.lastPingedTime_opaque, expirationTime)) {
118 + root.lastPingedTime_opaque = NoWork;
119 }
120
111 - if (expirationTime <= root.lastExpiredTime) {
112 - root.lastExpiredTime = NoWork;
121 + if (isSameOrHigherPriority(root.lastExpiredTime_opaque, expirationTime)) {
122 + root.lastExpiredTime_opaque = NoWork;
123 }
124 }
125
126 export function markRootUpdatedAtTime(
127 root: FiberRoot,
118 - expirationTime: ExpirationTime,
128 + expirationTime: ExpirationTimeOpaque,
129 ): void {
130 // Update the range of pending times
121 - const firstPendingTime = root.firstPendingTime;
122 - if (expirationTime > firstPendingTime) {
123 - root.firstPendingTime = expirationTime;
131 + const firstPendingTime = root.firstPendingTime_opaque;
132 + if (!isSameOrHigherPriority(firstPendingTime, expirationTime)) {
133 + root.firstPendingTime_opaque = expirationTime;
134 }
125 - const lastPendingTime = root.lastPendingTime;
126 - if (lastPendingTime === NoWork || expirationTime < lastPendingTime) {
127 - root.lastPendingTime = expirationTime;
135 + const lastPendingTime = root.lastPendingTime_opaque;
136 + if (
137 + isSameExpirationTime(lastPendingTime, (NoWork: ExpirationTimeOpaque)) ||
138 + !isSameOrHigherPriority(expirationTime, lastPendingTime)
139 + ) {
140 + root.lastPendingTime_opaque = expirationTime;
141 }
142
143 // Update the range of suspended times. Treat everything lower priority or
144 // equal to this update as unsuspended.
132 - const firstSuspendedTime = root.firstSuspendedTime;
133 - if (firstSuspendedTime !== NoWork) {
134 - if (expirationTime >= firstSuspendedTime) {
145 + const firstSuspendedTime = root.firstSuspendedTime_opaque;
146 + if (
147 + !isSameExpirationTime(firstSuspendedTime, (NoWork: ExpirationTimeOpaque))
148 + ) {
149 + if (isSameOrHigherPriority(expirationTime, firstSuspendedTime)) {
150 // The entire suspended range is now unsuspended.
136 - root.firstSuspendedTime = root.lastSuspendedTime = root.nextKnownPendingLevel = NoWork;
137 - } else if (expirationTime >= root.lastSuspendedTime) {
138 - root.lastSuspendedTime = expirationTime + 1;
151 + root.firstSuspendedTime_opaque = root.lastSuspendedTime_opaque = root.nextKnownPendingLevel_opaque = NoWork;
152 + } else if (
153 + isSameOrHigherPriority(expirationTime, root.lastSuspendedTime_opaque)
154 + ) {
155 + root.lastSuspendedTime_opaque = bumpPriorityHigher(expirationTime);
156 }
157
158 // This is a pending level. Check if it's higher priority than the next
159 // known pending level.
143 - if (expirationTime > root.nextKnownPendingLevel) {
144 - root.nextKnownPendingLevel = expirationTime;
160 + if (
161 + !isSameOrHigherPriority(root.nextKnownPendingLevel_opaque, expirationTime)
162 + ) {
163 + root.nextKnownPendingLevel_opaque = expirationTime;
164 }
165 }
166 }
167
168 export function markRootFinishedAtTime(
169 root: FiberRoot,
151 - finishedExpirationTime: ExpirationTime,
152 - remainingExpirationTime: ExpirationTime,
170 + finishedExpirationTime: ExpirationTimeOpaque,
171 + remainingExpirationTime: ExpirationTimeOpaque,
172 ): void {
173 // Update the range of pending times
155 - root.firstPendingTime = remainingExpirationTime;
156 - if (remainingExpirationTime < root.lastPendingTime) {
174 + root.firstPendingTime_opaque = remainingExpirationTime;
175 + if (
176 + !isSameOrHigherPriority(
177 + remainingExpirationTime,
178 + root.lastPendingTime_opaque,
179 + )
180 + ) {
181 // This usually means we've finished all the work, but it can also happen
182 // when something gets downprioritized during render, like a hidden tree.
159 - root.lastPendingTime = remainingExpirationTime;
183 + root.lastPendingTime_opaque = remainingExpirationTime;
184 }
185
186 // Update the range of suspended times. Treat everything higher priority or
187 // equal to this update as unsuspended.
164 - if (finishedExpirationTime <= root.lastSuspendedTime) {
188 + if (
189 + isSameOrHigherPriority(
190 + root.lastSuspendedTime_opaque,
191 + finishedExpirationTime,
192 + )
193 + ) {
194 // The entire suspended range is now unsuspended.
166 - root.firstSuspendedTime = root.lastSuspendedTime = root.nextKnownPendingLevel = NoWork;
167 - } else if (finishedExpirationTime <= root.firstSuspendedTime) {
195 + root.firstSuspendedTime_opaque = root.lastSuspendedTime_opaque = root.nextKnownPendingLevel_opaque = NoWork;
196 + } else if (
197 + isSameOrHigherPriority(
198 + root.firstSuspendedTime_opaque,
199 + finishedExpirationTime,
200 + )
201 + ) {
202 // Part of the suspended range is now unsuspended. Narrow the range to
203 // include everything between the unsuspended time (non-inclusive) and the
204 // last suspended time.
171 - root.firstSuspendedTime = finishedExpirationTime - 1;
205 + root.firstSuspendedTime_opaque = bumpPriorityLower(finishedExpirationTime);
206 }
207
174 - if (finishedExpirationTime <= root.lastPingedTime) {
208 + if (
209 + isSameOrHigherPriority(root.lastPingedTime_opaque, finishedExpirationTime)
210 + ) {
211 // Clear the pinged time
176 - root.lastPingedTime = NoWork;
212 + root.lastPingedTime_opaque = NoWork;
213 }
214
179 - if (finishedExpirationTime <= root.lastExpiredTime) {
215 + if (
216 + isSameOrHigherPriority(root.lastExpiredTime_opaque, finishedExpirationTime)
217 + ) {
218 // Clear the expired time
181 - root.lastExpiredTime = NoWork;
219 + root.lastExpiredTime_opaque = NoWork;
220 }
221
222 // Clear any pending updates that were just processed.
@@ -187,10 +225,13 @@ export function markRootFinishedAtTime(
225
226 export function markRootExpiredAtTime(
227 root: FiberRoot,
190 - expirationTime: ExpirationTime,
228 + expirationTime: ExpirationTimeOpaque,
229 ): void {
192 - const lastExpiredTime = root.lastExpiredTime;
193 - if (lastExpiredTime === NoWork || lastExpiredTime > expirationTime) {
194 - root.lastExpiredTime = expirationTime;
230 + const lastExpiredTime = root.lastExpiredTime_opaque;
231 + if (
232 + isSameExpirationTime(lastExpiredTime, (NoWork: ExpirationTimeOpaque)) ||
233 + !isSameOrHigherPriority(expirationTime, lastExpiredTime)
234 + ) {
235 + root.lastExpiredTime_opaque = expirationTime;
236 }
237 }
packages/react-reconciler/src/ReactFiberRoot.old.js
+1 -1
@@ -49,7 +49,7 @@ function FiberRootNode(containerInfo, tag, hydrate) {
49 if (enableSchedulerTracing) {
50 this.interactionThreadID = unstable_getThreadID();
51 this.memoizedInteractions = new Set();
52 - this.pendingInteractionMap = new Map();
52 + this.pendingInteractionMap_old = new Map();
53 }
54 if (enableSuspenseCallback) {
55 this.hydrationCallbacks = null;
packages/react-reconciler/src/ReactFiberSuspenseComponent.new.js
+3 -3
@@ -9,7 +9,7 @@
9
10 import type {Fiber} from './ReactInternalTypes';
11 import type {SuspenseInstance} from './ReactFiberHostConfig';
12 -import type {ExpirationTime} from './ReactFiberExpirationTime.new';
12 +import type {ExpirationTimeOpaque} from './ReactFiberExpirationTime.new';
13 import {SuspenseComponent, SuspenseListComponent} from './ReactWorkTags';
14 import {NoEffect, DidCapture} from './ReactSideEffectTags';
15 import {
@@ -32,12 +32,12 @@ export type SuspenseState = {|
32 // Represents the work that was deprioritized when we committed the fallback.
33 // The work outside the boundary already committed at this level, so we cannot
34 // unhide the content without including it.
35 - baseTime: ExpirationTime,
35 + baseTime: ExpirationTimeOpaque,
36 // Represents the earliest expiration time we should attempt to hydrate
37 // a dehydrated boundary at.
38 // Never is the default for dehydrated boundaries.
39 // NoWork is the default for normal boundaries, which turns into "normal" pri.
40 - retryTime: ExpirationTime,
40 + retryTime: ExpirationTimeOpaque,
41 |};
42
43 export type SuspenseListTailMode = 'collapsed' | 'hidden' | void;
packages/react-reconciler/src/ReactFiberThrow.new.js
+24 -15
@@ -9,7 +9,7 @@
9
10 import type {Fiber} from './ReactInternalTypes';
11 import type {FiberRoot} from './ReactInternalTypes';
12 -import type {ExpirationTime} from './ReactFiberExpirationTime.new';
12 +import type {ExpirationTimeOpaque} from './ReactFiberExpirationTime.new';
13 import type {CapturedValue} from './ReactCapturedValue';
14 import type {Update} from './ReactUpdateQueue.new';
15 import type {Wakeable} from 'shared/ReactTypes';
@@ -54,16 +54,16 @@ import {
54 } from './ReactFiberWorkLoop.new';
55 import {logCapturedError} from './ReactFiberErrorLogger';
56
57 -import {Sync, NoWork} from './ReactFiberExpirationTime.new';
57 +import {Sync, isSameExpirationTime} from './ReactFiberExpirationTime.new';
58
59 const PossiblyWeakMap = typeof WeakMap === 'function' ? WeakMap : Map;
60
61 function createRootErrorUpdate(
62 fiber: Fiber,
63 errorInfo: CapturedValue<mixed>,
64 - expirationTime: ExpirationTime,
64 + expirationTime: ExpirationTimeOpaque,
65 ): Update<mixed> {
66 - const update = createUpdate(NoWork, expirationTime, null);
66 + const update = createUpdate(-1, expirationTime, null);
67 // Unmount the root by rendering null.
68 update.tag = CaptureUpdate;
69 // Caution: React DevTools currently depends on this property
@@ -80,9 +80,9 @@ function createRootErrorUpdate(
80 function createClassErrorUpdate(
81 fiber: Fiber,
82 errorInfo: CapturedValue<mixed>,
83 - expirationTime: ExpirationTime,
83 + expirationTime: ExpirationTimeOpaque,
84 ): Update<mixed> {
85 - const update = createUpdate(NoWork, expirationTime, null);
85 + const update = createUpdate(-1, expirationTime, null);
86 update.tag = CaptureUpdate;
87 const getDerivedStateFromError = fiber.type.getDerivedStateFromError;
88 if (typeof getDerivedStateFromError === 'function') {
@@ -120,7 +120,12 @@ function createClassErrorUpdate(
120 // If componentDidCatch is the only error boundary method defined,
121 // then it needs to call setState to recover from errors.
122 // If no state update is scheduled then the boundary will swallow the error.
123 - if (fiber.expirationTime !== Sync) {
123 + if (
124 + !isSameExpirationTime(
125 + fiber.expirationTime_opaque,
126 + (Sync: ExpirationTimeOpaque),
127 + )
128 + ) {
129 console.error(
130 '%s: Error boundaries should implement getDerivedStateFromError(). ' +
131 'In that method, return a state update to display an error message or fallback UI.',
@@ -140,7 +145,7 @@ function createClassErrorUpdate(
145
146 function attachPingListener(
147 root: FiberRoot,
143 - renderExpirationTime: ExpirationTime,
148 + renderExpirationTime: ExpirationTimeOpaque,
149 wakeable: Wakeable,
150 ) {
151 // Attach a listener to the promise to "ping" the root and retry. But
@@ -177,7 +182,7 @@ function throwException(
182 returnFiber: Fiber,
183 sourceFiber: Fiber,
184 value: mixed,
180 - renderExpirationTime: ExpirationTime,
185 + renderExpirationTime: ExpirationTimeOpaque,
186 ) {
187 // The source fiber did not complete.
188 sourceFiber.effectTag |= Incomplete;
@@ -199,7 +204,7 @@ function throwException(
204 if (currentSource) {
205 sourceFiber.updateQueue = currentSource.updateQueue;
206 sourceFiber.memoizedState = currentSource.memoizedState;
202 - sourceFiber.expirationTime = currentSource.expirationTime;
207 + sourceFiber.expirationTime_opaque = currentSource.expirationTime_opaque;
208 } else {
209 sourceFiber.updateQueue = null;
210 sourceFiber.memoizedState = null;
@@ -258,7 +263,11 @@ function throwException(
263 // When we try rendering again, we should not reuse the current fiber,
264 // since it's known to be in an inconsistent state. Use a force update to
265 // prevent a bail out.
261 - const update = createUpdate(NoWork, Sync, null);
266 + const update = createUpdate(
267 + -1,
268 + (Sync: ExpirationTimeOpaque),
269 + null,
270 + );
271 update.tag = ForceUpdate;
272 enqueueUpdate(sourceFiber, update);
273 }
@@ -266,7 +275,7 @@ function throwException(
275
276 // The source fiber did not complete. Mark it with Sync priority to
277 // indicate that it still has pending work.
269 - sourceFiber.expirationTime = Sync;
278 + sourceFiber.expirationTime_opaque = Sync;
279
280 // Exit without suspending.
281 return;
@@ -317,7 +326,7 @@ function throwException(
326 attachPingListener(root, renderExpirationTime, wakeable);
327
328 workInProgress.effectTag |= ShouldCapture;
320 - workInProgress.expirationTime = renderExpirationTime;
329 + workInProgress.expirationTime_opaque = renderExpirationTime;
330
331 return;
332 }
@@ -347,7 +356,7 @@ function throwException(
356 case HostRoot: {
357 const errorInfo = value;
358 workInProgress.effectTag |= ShouldCapture;
350 - workInProgress.expirationTime = renderExpirationTime;
359 + workInProgress.expirationTime_opaque = renderExpirationTime;
360 const update = createRootErrorUpdate(
361 workInProgress,
362 errorInfo,
@@ -369,7 +378,7 @@ function throwException(
378 !isAlreadyFailedLegacyErrorBoundary(instance)))
379 ) {
380 workInProgress.effectTag |= ShouldCapture;
372 - workInProgress.expirationTime = renderExpirationTime;
381 + workInProgress.expirationTime_opaque = renderExpirationTime;
382 // Schedule the error boundary to re-render using updated state
383 const update = createClassErrorUpdate(
384 workInProgress,
packages/react-reconciler/src/ReactFiberUnwindWork.new.js
+2 -2
@@ -8,7 +8,7 @@
8 */
9
10 import type {Fiber} from './ReactInternalTypes';
11 -import type {ExpirationTime} from './ReactFiberExpirationTime.new';
11 +import type {ExpirationTimeOpaque} from './ReactFiberExpirationTime.new';
12 import type {SuspenseState} from './ReactFiberSuspenseComponent.new';
13
14 import {resetWorkInProgressVersions as resetMutableSourceWorkInProgressVersions} from './ReactMutableSource.new';
@@ -38,7 +38,7 @@ import invariant from 'shared/invariant';
38
39 function unwindWork(
40 workInProgress: Fiber,
41 - renderExpirationTime: ExpirationTime,
41 + renderExpirationTime: ExpirationTimeOpaque,
42 ) {
43 switch (workInProgress.tag) {
44 case ClassComponent: {
packages/react-reconciler/src/ReactFiberWorkLoop.new.js
+529 -322
@@ -9,7 +9,7 @@
9
10 import type {Thenable, Wakeable} from 'shared/ReactTypes';
11 import type {Fiber, FiberRoot} from './ReactInternalTypes';
12 -import type {ExpirationTime} from './ReactFiberExpirationTime.new';
12 +import type {ExpirationTimeOpaque} from './ReactFiberExpirationTime.new';
13 import type {ReactPriorityLevel} from './ReactInternalTypes';
14 import type {Interaction} from 'scheduler/src/Tracing';
15 import type {SuspenseConfig} from './ReactFiberSuspenseConfig';
@@ -26,7 +26,6 @@ import {
26 enableProfilerCommitHooks,
27 enableSchedulerTracing,
28 warnAboutUnmockedScheduler,
29 - disableSchedulerTimeoutBasedOnReactExpirationTime,
29 } from 'shared/ReactFeatureFlags';
30 import ReactSharedInternals from 'shared/ReactSharedInternals';
31 import invariant from 'shared/invariant';
@@ -39,12 +38,12 @@ import {
38 shouldYield,
39 requestPaint,
40 now,
42 - NoPriority,
43 - ImmediatePriority,
44 - UserBlockingPriority,
45 - NormalPriority,
46 - LowPriority,
47 - IdlePriority,
41 + NoPriority as NoSchedulerPriority,
42 + ImmediatePriority as ImmediateSchedulerPriority,
43 + UserBlockingPriority as UserBlockingSchedulerPriority,
44 + NormalPriority as NormalSchedulerPriority,
45 + LowPriority as LowSchedulerPriority,
46 + IdlePriority as IdleSchedulerPriority,
47 flushSyncCallbackQueue,
48 scheduleSyncCallback,
49 } from './SchedulerWithReactIntegration.new';
@@ -116,19 +115,31 @@ import {
115 import {
116 NoWork,
117 Sync,
118 + UserBlockingUpdateTime,
119 + DefaultUpdateTime,
120 Never,
120 - msToExpirationTime,
121 - expirationTimeToMs,
122 - computeInteractiveExpiration,
123 - computeAsyncExpiration,
124 - computeSuspenseTimeout,
125 - LOW_PRIORITY_EXPIRATION,
121 inferPriorityFromExpirationTime,
122 Batched,
123 Idle,
124 + ContinuousHydration,
125 ShortTransition,
126 LongTransition,
127 + isSameOrHigherPriority,
128 + isSameExpirationTime,
129 + bumpPriorityLower,
130 } from './ReactFiberExpirationTime.new';
131 +import {
132 + SyncLanePriority,
133 + SyncBatchedLanePriority,
134 + InputDiscreteLanePriority,
135 + InputContinuousLanePriority,
136 + DefaultLanePriority,
137 + TransitionShortLanePriority,
138 + TransitionLongLanePriority,
139 + HydrationContinuousLanePriority,
140 + IdleLanePriority,
141 + OffscreenLanePriority,
142 +} from './ReactFiberLane';
143 import {beginWork as originalBeginWork} from './ReactFiberBeginWork.new';
144 import {completeWork} from './ReactFiberCompleteWork.new';
145 import {unwindWork, unwindInterruptedWork} from './ReactFiberUnwindWork.new';
@@ -219,21 +230,21 @@ let workInProgressRoot: FiberRoot | null = null;
230 // The fiber we're working on
231 let workInProgress: Fiber | null = null;
232 // The expiration time we're rendering
222 -let renderExpirationTime: ExpirationTime = NoWork;
233 +let renderExpirationTime: ExpirationTimeOpaque = NoWork;
234 // Whether to root completed, errored, suspended, etc.
235 let workInProgressRootExitStatus: RootExitStatus = RootIncomplete;
236 // A fatal error, if one is thrown
237 let workInProgressRootFatalError: mixed = null;
238 // Most recent event time among processed updates during this render.
228 -// This is conceptually a time stamp but expressed in terms of an ExpirationTime
239 +// This is conceptually a time stamp but expressed in terms of an ExpirationTimeOpaque
240 // because we deal mostly with expiration times in the hot path, so this avoids
241 // the conversion happening in the hot path.
231 -let workInProgressRootLatestProcessedEventTime: ExpirationTime = Sync;
232 -let workInProgressRootLatestSuspenseTimeout: ExpirationTime = Sync;
242 +let workInProgressRootLatestProcessedEventTime: number = -1;
243 +let workInProgressRootLatestSuspenseTimeout: number = -1;
244 let workInProgressRootCanSuspendUsingConfig: null | SuspenseConfig = null;
245 // The work left over by components that were visited during this render. Only
246 // includes unprocessed updates, not work in bailed out children.
236 -let workInProgressRootNextUnprocessedUpdateTime: ExpirationTime = NoWork;
247 +let workInProgressRootNextUnprocessedUpdateTime: ExpirationTimeOpaque = NoWork;
248
249 // If we're pinged while rendering we don't always restart immediately.
250 // This flag determines if it might be worthwhile to restart if an opportunity
@@ -243,6 +254,7 @@ let workInProgressRootHasPendingPing: boolean = false;
254 // model where we don't commit new loading states in too quick succession.
255 let globalMostRecentFallbackTime: number = 0;
256 const FALLBACK_THROTTLE_MS: number = 500;
257 +const DEFAULT_TIMEOUT_MS: number = 5000;
258
259 let nextEffect: Fiber | null = null;
260 let hasUncaughtError = false;
@@ -251,15 +263,15 @@ let legacyErrorBoundariesThatAlreadyFailed: Set<mixed> | null = null;
263
264 let rootDoesHavePassiveEffects: boolean = false;
265 let rootWithPendingPassiveEffects: FiberRoot | null = null;
254 -let pendingPassiveEffectsRenderPriority: ReactPriorityLevel = NoPriority;
255 -let pendingPassiveEffectsExpirationTime: ExpirationTime = NoWork;
266 +let pendingPassiveEffectsRenderPriority: ReactPriorityLevel = NoSchedulerPriority;
267 +let pendingPassiveEffectsExpirationTime: ExpirationTimeOpaque = NoWork;
268 let pendingPassiveHookEffectsMount: Array<HookEffect | Fiber> = [];
269 let pendingPassiveHookEffectsUnmount: Array<HookEffect | Fiber> = [];
270 let pendingPassiveProfilerEffects: Array<Fiber> = [];
271
272 let rootsWithPendingDiscreteUpdates: Map<
273 FiberRoot,
262 - ExpirationTime,
274 + ExpirationTimeOpaque,
275 > | null = null;
276
277 // Use these to prevent an infinite loop of nested updates
@@ -274,17 +286,12 @@ let nestedPassiveUpdateCount: number = 0;
286 // during the commit phase. This enables them to be traced across components
287 // that spawn new work during render. E.g. hidden boundaries, suspended SSR
288 // hydration or SuspenseList.
277 -let spawnedWorkDuringRender: null | Array<ExpirationTime> = null;
278 -
279 -// Expiration times are computed by adding to the current time (the start
280 -// time). However, if two updates are scheduled within the same event, we
281 -// should treat their start times as simultaneous, even if the actual clock
282 -// time has advanced between the first and second call.
289 +let spawnedWorkDuringRender: null | Array<ExpirationTimeOpaque> = null;
290
284 -// In other words, because expiration times determine how updates are batched,
285 -// we want all updates of like priority that occur within the same event to
286 -// receive the same expiration time. Otherwise we get tearing.
287 -let currentEventTime: ExpirationTime = NoWork;
291 +// If two updates are scheduled within the same event, we should treat their
292 +// event times as simultaneous, even if the actual clock time has advanced
293 +// between the first and second call.
294 +let currentEventTime: number = -1;
295
296 // Dev only flag that tracks if passive effects are currently being flushed.
297 // We warn about state updates for unmounted components differently in this case.
@@ -297,89 +304,127 @@ export function getWorkInProgressRoot(): FiberRoot | null {
304 return workInProgressRoot;
305 }
306
300 -export function requestCurrentTimeForUpdate() {
307 +export function requestEventTime() {
308 if ((executionContext & (RenderContext | CommitContext)) !== NoContext) {
309 // We're inside React, so it's fine to read the actual time.
303 - return msToExpirationTime(now());
310 + return now();
311 }
312 // We're not inside React, so we may be in the middle of a browser event.
306 - if (currentEventTime !== NoWork) {
313 + if (currentEventTime !== -1) {
314 // Use the same start time for all updates until we enter React again.
315 return currentEventTime;
316 }
317 // This is the first update since React yielded. Compute a new start time.
311 - currentEventTime = msToExpirationTime(now());
318 + currentEventTime = now();
319 return currentEventTime;
320 }
321
322 export function getCurrentTime() {
316 - return msToExpirationTime(now());
323 + return now();
324 }
325
319 -export function computeExpirationForFiber(
320 - currentTime: ExpirationTime,
326 +export function requestUpdateExpirationTime(
327 fiber: Fiber,
322 - suspenseConfig: null | SuspenseConfig,
323 -): ExpirationTime {
328 + suspenseConfig: SuspenseConfig | null,
329 +): ExpirationTimeOpaque {
330 + // Special cases
331 const mode = fiber.mode;
332 if ((mode & BlockingMode) === NoMode) {
333 return Sync;
327 - }
328 -
329 - const priorityLevel = getCurrentPriorityLevel();
330 - if ((mode & ConcurrentMode) === NoMode) {
331 - return priorityLevel === ImmediatePriority ? Sync : Batched;
332 - }
333 -
334 - if ((executionContext & RenderContext) !== NoContext) {
334 + } else if ((mode & ConcurrentMode) === NoMode) {
335 + return getCurrentPriorityLevel() === ImmediateSchedulerPriority
336 + ? (Sync: ExpirationTimeOpaque)
337 + : Batched;
338 + } else if ((executionContext & RenderContext) !== NoContext) {
339 // Use whatever time we're already rendering
336 - // TODO: Should there be a way to opt out, like with `runWithPriority`?
340 + // TODO: Treat render phase updates as if they came from an
341 + // interleaved event.
342 return renderExpirationTime;
343 }
344
340 - let expirationTime;
345 + let updateLanePriority;
346 if (suspenseConfig !== null) {
347 // If there's a SuspenseConfig, choose an expiration time that's lower
348 // priority than a normal concurrent update (regardless of the current
344 - // Scheduler priority.) Timeouts larger than 10 seconds move one level lower
345 - // than that.
349 + // Scheduler priority.) Timeouts larger than 10 seconds move one level
350 + // lower than that.
351 + // TODO: This will coerce numbers larger than 31 bits to 0.
352 const timeoutMs = suspenseConfig.timeoutMs;
347 - expirationTime =
348 - // TODO: This will coerce numbers larger than 31 bits to 0.
353 + updateLanePriority =
354 timeoutMs === undefined || (timeoutMs | 0) < 10000
350 - ? ShortTransition
351 - : LongTransition;
355 + ? TransitionShortLanePriority
356 + : TransitionLongLanePriority;
357 } else {
353 - // Compute an expiration time based on the Scheduler priority.
358 + // TODO: If we're not inside `runWithPriority`, this returns the priority
359 + // of the currently running task. That's probably not what we want.
360 + const priorityLevel = getCurrentPriorityLevel();
361 switch (priorityLevel) {
355 - case ImmediatePriority:
356 - expirationTime = Sync;
362 + case ImmediateSchedulerPriority:
363 + updateLanePriority = SyncLanePriority;
364 break;
358 - case UserBlockingPriority:
359 - // TODO: Rename this to computeUserBlockingExpiration
360 - expirationTime = computeInteractiveExpiration(currentTime);
365 + case UserBlockingSchedulerPriority:
366 + updateLanePriority = InputContinuousLanePriority;
367 break;
362 - case NormalPriority:
363 - case LowPriority: // TODO: Handle LowPriority
364 - // TODO: Rename this to... something better.
365 - expirationTime = computeAsyncExpiration(currentTime);
368 + case NormalSchedulerPriority:
369 + case LowSchedulerPriority:
370 + // TODO: Handle LowSchedulerPriority, somehow. Maybe the same lane as hydration.
371 + updateLanePriority = DefaultLanePriority;
372 break;
367 - case IdlePriority:
368 - expirationTime = Idle;
373 + case IdleSchedulerPriority:
374 + updateLanePriority = IdleLanePriority;
375 break;
376 default:
377 invariant(false, 'Expected a valid priority level');
378 }
379 }
380
381 + // TODO: In the new system, what we'll do here is claim one of the bits of
382 + // the root's `pendingLanes` field, based on its priority. We'll combine this
383 + // function with `scheduleUpdateOnFiber` and `markRootUpdatedAtTime`.
384 + let expirationTime;
385 + switch (updateLanePriority) {
386 + case SyncLanePriority:
387 + return Sync;
388 + case SyncBatchedLanePriority:
389 + return Batched;
390 + case InputDiscreteLanePriority:
391 + case InputContinuousLanePriority:
392 + expirationTime = UserBlockingUpdateTime;
393 + break;
394 + case DefaultLanePriority:
395 + expirationTime = DefaultUpdateTime;
396 + break;
397 + case TransitionShortLanePriority:
398 + expirationTime = ShortTransition;
399 + break;
400 + case TransitionLongLanePriority:
401 + expirationTime = LongTransition;
402 + break;
403 + case HydrationContinuousLanePriority:
404 + expirationTime = ContinuousHydration;
405 + break;
406 + case IdleLanePriority:
407 + expirationTime = Idle;
408 + break;
409 + case OffscreenLanePriority:
410 + expirationTime = Never;
411 + break;
412 + default:
413 + invariant(false, 'Expected a valid priority level');
414 + }
415 +
416 // If we're in the middle of rendering a tree, do not update at the same
417 // expiration time that is already rendering.
418 // TODO: We shouldn't have to do this if the update is on a different root.
378 - // Refactor computeExpirationForFiber + scheduleUpdate so we have access to
379 - // the root when we check for this condition.
380 - if (workInProgressRoot !== null && expirationTime === renderExpirationTime) {
419 + // TODO: In the new system, we'll find a different bit that's not the one
420 + // we're currently rendering.
421 + if (
422 + workInProgressRoot !== null &&
423 + isSameExpirationTime(expirationTime, renderExpirationTime)
424 + ) {
425 // This is a trick to move this update into a separate batch
382 - expirationTime -= 1;
426 + // TODO: This probably causes problems with ContinuousHydration and Idle
427 + expirationTime = bumpPriorityLower(expirationTime);
428 }
429
430 return expirationTime;
@@ -387,7 +432,7 @@ export function computeExpirationForFiber(
432
433 export function scheduleUpdateOnFiber(
434 fiber: Fiber,
390 - expirationTime: ExpirationTime,
435 + expirationTime: ExpirationTimeOpaque,
436 ) {
437 checkForNestedUpdates();
438 warnAboutRenderPhaseUpdatesInDEV(fiber);
@@ -398,11 +443,11 @@ export function scheduleUpdateOnFiber(
443 return null;
444 }
445
401 - // TODO: computeExpirationForFiber also reads the priority. Pass the
446 + // TODO: requestUpdateLanePriority also reads the priority. Pass the
447 // priority as an argument to that function and this one.
448 const priorityLevel = getCurrentPriorityLevel();
449
405 - if (expirationTime === Sync) {
450 + if (isSameExpirationTime(expirationTime, (Sync: ExpirationTimeOpaque))) {
451 if (
452 // Check if we're inside unbatchedUpdates
453 (executionContext & LegacyUnbatchedContext) !== NoContext &&
@@ -437,8 +482,8 @@ export function scheduleUpdateOnFiber(
482 (executionContext & DiscreteEventContext) !== NoContext &&
483 // Only updates at user-blocking priority or greater are considered
484 // discrete, even inside a discrete event.
440 - (priorityLevel === UserBlockingPriority ||
441 - priorityLevel === ImmediatePriority)
485 + (priorityLevel === UserBlockingSchedulerPriority ||
486 + priorityLevel === ImmediateSchedulerPriority)
487 ) {
488 // This is the result of a discrete event. Track the lowest priority
489 // discrete update per root so we can flush them early, if needed.
@@ -446,7 +491,10 @@ export function scheduleUpdateOnFiber(
491 rootsWithPendingDiscreteUpdates = new Map([[root, expirationTime]]);
492 } else {
493 const lastDiscreteTime = rootsWithPendingDiscreteUpdates.get(root);
449 - if (lastDiscreteTime === undefined || lastDiscreteTime > expirationTime) {
494 + if (
495 + lastDiscreteTime === undefined ||
496 + !isSameOrHigherPriority(expirationTime, lastDiscreteTime)
497 + ) {
498 rootsWithPendingDiscreteUpdates.set(root, expirationTime);
499 }
500 }
@@ -459,12 +507,15 @@ export function scheduleUpdateOnFiber(
507 // on a fiber.
508 function markUpdateTimeFromFiberToRoot(fiber, expirationTime) {
509 // Update the source fiber's expiration time
462 - if (fiber.expirationTime < expirationTime) {
463 - fiber.expirationTime = expirationTime;
510 + if (!isSameOrHigherPriority(fiber.expirationTime_opaque, expirationTime)) {
511 + fiber.expirationTime_opaque = expirationTime;
512 }
513 let alternate = fiber.alternate;
466 - if (alternate !== null && alternate.expirationTime < expirationTime) {
467 - alternate.expirationTime = expirationTime;
514 + if (
515 + alternate !== null &&
516 + !isSameOrHigherPriority(alternate.expirationTime_opaque, expirationTime)
517 + ) {
518 + alternate.expirationTime_opaque = expirationTime;
519 }
520 // Walk the parent path to the root and update the child expiration time.
521 let node = fiber.return;
@@ -474,19 +525,27 @@ function markUpdateTimeFromFiberToRoot(fiber, expirationTime) {
525 } else {
526 while (node !== null) {
527 alternate = node.alternate;
477 - if (node.childExpirationTime < expirationTime) {
478 - node.childExpirationTime = expirationTime;
528 + if (
529 + !isSameOrHigherPriority(node.childExpirationTime_opaque, expirationTime)
530 + ) {
531 + node.childExpirationTime_opaque = expirationTime;
532 if (
533 alternate !== null &&
481 - alternate.childExpirationTime < expirationTime
534 + !isSameOrHigherPriority(
535 + alternate.childExpirationTime_opaque,
536 + expirationTime,
537 + )
538 ) {
483 - alternate.childExpirationTime = expirationTime;
539 + alternate.childExpirationTime_opaque = expirationTime;
540 }
541 } else if (
542 alternate !== null &&
487 - alternate.childExpirationTime < expirationTime
543 + !isSameOrHigherPriority(
544 + alternate.childExpirationTime_opaque,
545 + expirationTime,
546 + )
547 ) {
489 - alternate.childExpirationTime = expirationTime;
548 + alternate.childExpirationTime_opaque = expirationTime;
549 }
550 if (node.return === null && node.tag === HostRoot) {
551 root = node.stateNode;
@@ -508,7 +567,7 @@ function markUpdateTimeFromFiberToRoot(fiber, expirationTime) {
567 // suspended now, right before marking the incoming update. This has the
568 // effect of interrupting the current render and switching to the update.
569 // TODO: This happens to work when receiving an update during the render
511 - // phase, because of the trick inside computeExpirationForFiber to
570 + // phase, because of the trick inside requestUpdateExpirationTime to
571 // subtract 1 from `renderExpirationTime` to move it into a
572 // separate bucket. But we should probably model it with an exception,
573 // using the same mechanism we use to force hydration of a subtree.
@@ -526,19 +585,21 @@ function markUpdateTimeFromFiberToRoot(fiber, expirationTime) {
585 return root;
586 }
587
529 -function getNextRootExpirationTimeToWorkOn(root: FiberRoot): ExpirationTime {
588 +function getNextRootExpirationTimeToWorkOn(
589 + root: FiberRoot,
590 +): ExpirationTimeOpaque {
591 // Determines the next expiration time that the root should render, taking
592 // into account levels that may be suspended, or levels that may have
593 // received a ping.
594
534 - const lastExpiredTime = root.lastExpiredTime;
535 - if (lastExpiredTime !== NoWork) {
595 + const lastExpiredTime = root.lastExpiredTime_opaque;
596 + if (!isSameExpirationTime(lastExpiredTime, (NoWork: ExpirationTimeOpaque))) {
597 return lastExpiredTime;
598 }
599
600 // "Pending" refers to any update that hasn't committed yet, including if it
601 // suspended. The "suspended" range is therefore a subset.
541 - const firstPendingTime = root.firstPendingTime;
602 + const firstPendingTime = root.firstPendingTime_opaque;
603 if (!isRootSuspendedAtTime(root, firstPendingTime)) {
604 // The highest priority pending time is not suspended. Let's work on that.
605 return firstPendingTime;
@@ -547,13 +608,18 @@ function getNextRootExpirationTimeToWorkOn(root: FiberRoot): ExpirationTime {
608 // If the first pending time is suspended, check if there's a lower priority
609 // pending level that we know about. Or check if we received a ping. Work
610 // on whichever is higher priority.
550 - const lastPingedTime = root.lastPingedTime;
551 - const nextKnownPendingLevel = root.nextKnownPendingLevel;
552 - const nextLevel =
553 - lastPingedTime > nextKnownPendingLevel
554 - ? lastPingedTime
555 - : nextKnownPendingLevel;
556 - if (nextLevel <= Idle && firstPendingTime !== nextLevel) {
611 + const lastPingedTime = root.lastPingedTime_opaque;
612 + const nextKnownPendingLevel = root.nextKnownPendingLevel_opaque;
613 + const nextLevel = !isSameOrHigherPriority(
614 + nextKnownPendingLevel,
615 + lastPingedTime,
616 + )
617 + ? lastPingedTime
618 + : nextKnownPendingLevel;
619 + if (
620 + isSameOrHigherPriority((Idle: ExpirationTimeOpaque), nextLevel) &&
621 + !isSameExpirationTime(firstPendingTime, nextLevel)
622 + ) {
623 // Don't work on Idle/Never priority unless everything else is committed.
624 return NoWork;
625 }
@@ -566,80 +632,97 @@ function getNextRootExpirationTimeToWorkOn(root: FiberRoot): ExpirationTime {
632 // the next level that the root has work on. This function is called on every
633 // update, and right before exiting a task.
634 function ensureRootIsScheduled(root: FiberRoot) {
569 - const lastExpiredTime = root.lastExpiredTime;
570 - if (lastExpiredTime !== NoWork) {
571 - // Special case: Expired work should flush synchronously.
572 - root.callbackExpirationTime = Sync;
573 - root.callbackPriority = ImmediatePriority;
574 - root.callbackNode = scheduleSyncCallback(
575 - performSyncWorkOnRoot.bind(null, root),
576 - );
577 - return;
578 - }
579 -
580 - const expirationTime = getNextRootExpirationTimeToWorkOn(root);
635 const existingCallbackNode = root.callbackNode;
582 - if (expirationTime === NoWork) {
583 - // There's nothing to work on.
636 +
637 + const newCallbackId = getNextRootExpirationTimeToWorkOn(root);
638 + if (newCallbackId === (NoWork: ExpirationTimeOpaque)) {
639 + // Special case: There's nothing to work on.
640 if (existingCallbackNode !== null) {
641 + cancelCallback(existingCallbackNode);
642 + root.expiresAt = -1;
643 root.callbackNode = null;
586 - root.callbackExpirationTime = NoWork;
587 - root.callbackPriority = NoPriority;
644 + root.callbackIsSync = false;
645 + root.callbackId = NoWork;
646 }
647 return;
648 }
649
592 - // TODO: If this is an update, we already read the current time. Pass the
593 - // time as an argument.
594 - const currentTime = requestCurrentTimeForUpdate();
595 - const priorityLevel = inferPriorityFromExpirationTime(
596 - currentTime,
597 - expirationTime,
598 - );
650 + const newTaskIsSync =
651 + newCallbackId === (Sync: ExpirationTimeOpaque) ||
652 + !isSameExpirationTime(
653 + root.lastExpiredTime_opaque,
654 + (NoWork: ExpirationTimeOpaque),
655 + );
656
600 - // If there's an existing render task, confirm it has the correct priority and
601 - // expiration time. Otherwise, we'll cancel it and schedule a new one.
602 - if (existingCallbackNode !== null) {
603 - const existingCallbackPriority = root.callbackPriority;
604 - const existingCallbackExpirationTime = root.callbackExpirationTime;
605 - if (
606 - // Callback must have the exact same expiration time.
607 - existingCallbackExpirationTime === expirationTime &&
608 - // Callback must have greater or equal priority.
609 - existingCallbackPriority >= priorityLevel
610 - ) {
611 - // Existing callback is sufficient.
612 - return;
657 + // Check if there's an existing task. We may be able to reuse it.
658 + const existingTaskId = root.callbackId;
659 + const existingCallbackIsSync = root.callbackIsSync;
660 + if (existingTaskId !== (NoWork: ExpirationTimeOpaque)) {
661 + if (newCallbackId === existingTaskId) {
662 + // This task is already scheduled. Let's check its priority.
663 + if (
664 + (newTaskIsSync && existingCallbackIsSync) ||
665 + (!newTaskIsSync && !existingCallbackIsSync)
666 + ) {
667 + // The priority hasn't changed. Exit.
668 + return;
669 + }
670 + // The task ID is the same but the priority changed. Cancel the existing
671 + // callback. We'll schedule a new one below.
672 }
614 - // Need to schedule a new task.
615 - // TODO: Instead of scheduling a new task, we should be able to change the
616 - // priority of the existing one.
673 cancelCallback(existingCallbackNode);
674 }
675
620 - root.callbackExpirationTime = expirationTime;
621 - root.callbackPriority = priorityLevel;
622 -
623 - let callbackNode;
624 - if (expirationTime === Sync) {
625 - // Sync React callbacks are scheduled on a special internal queue
626 - callbackNode = scheduleSyncCallback(performSyncWorkOnRoot.bind(null, root));
627 - } else if (disableSchedulerTimeoutBasedOnReactExpirationTime) {
628 - callbackNode = scheduleCallback(
629 - priorityLevel,
630 - performConcurrentWorkOnRoot.bind(null, root),
676 + // Schedule a new callback.
677 + let newCallbackNode;
678 + if (newTaskIsSync) {
679 + // Special case: Sync React callbacks are scheduled on a special internal queue
680 + newCallbackNode = scheduleSyncCallback(
681 + performSyncWorkOnRoot.bind(null, root),
682 );
683 } else {
633 - callbackNode = scheduleCallback(
634 - priorityLevel,
635 - performConcurrentWorkOnRoot.bind(null, root),
636 - // Compute a task timeout based on the expiration time. This also affects
637 - // ordering because tasks are processed in timeout order.
638 - {timeout: expirationTimeToMs(expirationTime) - now()},
639 - );
684 + // TODO: Use LanePriority instead of SchedulerPriority
685 + const priorityLevel = inferPriorityFromExpirationTime(newCallbackId);
686 + if (
687 + priorityLevel === NormalSchedulerPriority ||
688 + priorityLevel === UserBlockingSchedulerPriority
689 + ) {
690 + const existingExpirationTime = root.expiresAt;
691 + const currentTimeMs = now();
692 +
693 + // Compute an expiration time based on the priority level.
694 + const expiration =
695 + priorityLevel === UserBlockingSchedulerPriority ? 250 : 5000;
696 +
697 + let msUntilExpiration;
698 + if (existingExpirationTime === -1) {
699 + // This is the first concurrent update on the root. Use the expiration
700 + // time we just computed.
701 + msUntilExpiration = expiration;
702 + root.expiresAt = msUntilExpiration + currentTimeMs;
703 + } else {
704 + // There's already an expiration time. Use the smaller of the current
705 + // expiration and the one we just computed.
706 + msUntilExpiration = existingExpirationTime - currentTimeMs;
707 + if (expiration < msUntilExpiration) {
708 + root.expiresAt = expiration;
709 + }
710 + }
711 + newCallbackNode = scheduleCallback(
712 + priorityLevel,
713 + performConcurrentWorkOnRoot.bind(null, root),
714 + );
715 + } else {
716 + newCallbackNode = scheduleCallback(
717 + priorityLevel,
718 + performConcurrentWorkOnRoot.bind(null, root),
719 + );
720 + }
721 }
722
642 - root.callbackNode = callbackNode;
723 + root.callbackId = newCallbackId;
724 + root.callbackNode = newCallbackNode;
725 + root.callbackIsSync = newTaskIsSync;
726 }
727
728 // This is the entry point for every concurrent task, i.e. anything that
@@ -647,25 +730,24 @@ function ensureRootIsScheduled(root: FiberRoot) {
730 function performConcurrentWorkOnRoot(root, didTimeout) {
731 // Since we know we're in a React event, we can clear the current
732 // event time. The next update will compute a new event time.
650 - currentEventTime = NoWork;
733 + currentEventTime = -1;
734 +
735 + // Determine the next expiration time to work on, using the fields stored
736 + // on the root.
737 + let expirationTime = getNextRootExpirationTimeToWorkOn(root);
738 + if (isSameExpirationTime(expirationTime, (NoWork: ExpirationTimeOpaque))) {
739 + return null;
740 + }
741
652 - // Check if the render expired.
742 if (didTimeout) {
654 - // The render task took too long to complete. Mark the current time as
655 - // expired to synchronously render all expired work in a single batch.
656 - const currentTime = requestCurrentTimeForUpdate();
657 - markRootExpiredAtTime(root, currentTime);
743 + // The render task took too long to complete. Mark the root as expired to
744 + // prevent yielding to other tasks until this one finishes.
745 + markRootExpiredAtTime(root, expirationTime);
746 // This will schedule a synchronous callback.
747 ensureRootIsScheduled(root);
748 return null;
749 }
750
663 - // Determine the next expiration time to work on, using the fields stored
664 - // on the root.
665 - let expirationTime = getNextRootExpirationTimeToWorkOn(root);
666 - if (expirationTime === NoWork) {
667 - return null;
668 - }
751 const originalCallbackNode = root.callbackNode;
752 invariant(
753 (executionContext & (RenderContext | CommitContext)) === NoContext,
@@ -683,7 +765,12 @@ function performConcurrentWorkOnRoot(root, didTimeout) {
765 // render at Idle (or lower) so that all pending updates are included.
766 // If it still fails after the second attempt, we'll give up and commit
767 // the resulting tree.
686 - expirationTime = expirationTime > Idle ? Idle : expirationTime;
768 + expirationTime = !isSameOrHigherPriority(
769 + (Idle: ExpirationTimeOpaque),
770 + expirationTime,
771 + )
772 + ? (Idle: ExpirationTimeOpaque)
773 + : expirationTime;
774 exitStatus = renderRootSync(root, expirationTime);
775 }
776
@@ -699,8 +786,10 @@ function performConcurrentWorkOnRoot(root, didTimeout) {
786 // or, if something suspended, wait to commit it after a timeout.
787 const finishedWork: Fiber = (root.current.alternate: any);
788 root.finishedWork = finishedWork;
702 - root.finishedExpirationTime = expirationTime;
703 - root.nextKnownPendingLevel = getRemainingExpirationTime(finishedWork);
789 + root.finishedExpirationTime_opaque = expirationTime;
790 + root.nextKnownPendingLevel_opaque = getRemainingExpirationTime(
791 + finishedWork,
792 + );
793 finishConcurrentRender(root, finishedWork, exitStatus, expirationTime);
794 }
795
@@ -735,7 +824,7 @@ function finishConcurrentRender(
824 }
825 case RootSuspended: {
826 markRootSuspendedAtTime(root, expirationTime);
738 - const lastSuspendedTime = root.lastSuspendedTime;
827 + const lastSuspendedTime = root.lastSuspendedTime_opaque;
828
829 // We have an acceptable loading state. We need to figure out if we
830 // should immediately commit it or wait a bit.
@@ -744,7 +833,7 @@ function finishConcurrentRender(
833 // have a new loading state ready. We want to ensure that we commit
834 // that as soon as possible.
835 const hasNotProcessedNewUpdates =
747 - workInProgressRootLatestProcessedEventTime === Sync;
836 + workInProgressRootLatestProcessedEventTime === -1;
837 if (
838 hasNotProcessedNewUpdates &&
839 // do not delay if we're inside an act() scope
@@ -761,29 +850,41 @@ function finishConcurrentRender(
850 // Don't bother with a very short suspense time.
851 if (msUntilTimeout > 10) {
852 if (workInProgressRootHasPendingPing) {
764 - const lastPingedTime = root.lastPingedTime;
765 - if (lastPingedTime === NoWork || lastPingedTime >= expirationTime) {
853 + const lastPingedTime = root.lastPingedTime_opaque;
854 + if (
855 + isSameExpirationTime(
856 + lastPingedTime,
857 + (NoWork: ExpirationTimeOpaque),
858 + ) ||
859 + isSameOrHigherPriority(lastPingedTime, expirationTime)
860 + ) {
861 // This render was pinged but we didn't get to restart
862 // earlier so try restarting now instead.
768 - root.lastPingedTime = expirationTime;
863 + root.lastPingedTime_opaque = expirationTime;
864 prepareFreshStack(root, expirationTime);
865 break;
866 }
867 }
868
869 const nextTime = getNextRootExpirationTimeToWorkOn(root);
775 - if (nextTime !== NoWork && nextTime !== expirationTime) {
870 + if (
871 + !isSameExpirationTime(nextTime, (NoWork: ExpirationTimeOpaque)) &&
872 + !isSameExpirationTime(nextTime, expirationTime)
873 + ) {
874 // There's additional work on this root.
875 break;
876 }
877 if (
780 - lastSuspendedTime !== NoWork &&
781 - lastSuspendedTime !== expirationTime
878 + !isSameExpirationTime(
879 + lastSuspendedTime,
880 + (NoWork: ExpirationTimeOpaque),
881 + ) &&
882 + !isSameExpirationTime(lastSuspendedTime, expirationTime)
883 ) {
884 // We should prefer to render the fallback of at the last
885 // suspended level. Ping the last suspended level to try
886 // rendering it again.
786 - root.lastPingedTime = lastSuspendedTime;
887 + root.lastPingedTime_opaque = lastSuspendedTime;
888 break;
889 }
890
@@ -803,7 +904,7 @@ function finishConcurrentRender(
904 }
905 case RootSuspendedWithDelay: {
906 markRootSuspendedAtTime(root, expirationTime);
806 - const lastSuspendedTime = root.lastSuspendedTime;
907 + const lastSuspendedTime = root.lastSuspendedTime_opaque;
908
909 if (
910 // do not delay if we're inside an act() scope
@@ -812,39 +913,50 @@ function finishConcurrentRender(
913 // We're suspended in a state that should be avoided. We'll try to
914 // avoid committing it for as long as the timeouts let us.
915 if (workInProgressRootHasPendingPing) {
815 - const lastPingedTime = root.lastPingedTime;
816 - if (lastPingedTime === NoWork || lastPingedTime >= expirationTime) {
916 + const lastPingedTime = root.lastPingedTime_opaque;
917 + if (
918 + isSameExpirationTime(
919 + lastPingedTime,
920 + (NoWork: ExpirationTimeOpaque),
921 + ) ||
922 + isSameOrHigherPriority(lastPingedTime, expirationTime)
923 + ) {
924 // This render was pinged but we didn't get to restart earlier
925 // so try restarting now instead.
819 - root.lastPingedTime = expirationTime;
926 + root.lastPingedTime_opaque = expirationTime;
927 prepareFreshStack(root, expirationTime);
928 break;
929 }
930 }
931
932 const nextTime = getNextRootExpirationTimeToWorkOn(root);
826 - if (nextTime !== NoWork && nextTime !== expirationTime) {
933 + if (
934 + !isSameExpirationTime(nextTime, (NoWork: ExpirationTimeOpaque)) &&
935 + !isSameExpirationTime(nextTime, expirationTime)
936 + ) {
937 // There's additional work on this root.
938 break;
939 }
940 if (
831 - lastSuspendedTime !== NoWork &&
832 - lastSuspendedTime !== expirationTime
941 + !isSameExpirationTime(
942 + lastSuspendedTime,
943 + (NoWork: ExpirationTimeOpaque),
944 + ) &&
945 + !isSameExpirationTime(lastSuspendedTime, expirationTime)
946 ) {
947 // We should prefer to render the fallback of at the last
948 // suspended level. Ping the last suspended level to try
949 // rendering it again.
837 - root.lastPingedTime = lastSuspendedTime;
950 + root.lastPingedTime_opaque = lastSuspendedTime;
951 break;
952 }
953
954 let msUntilTimeout;
842 - if (workInProgressRootLatestSuspenseTimeout !== Sync) {
955 + if (workInProgressRootLatestSuspenseTimeout !== -1) {
956 // We have processed a suspense config whose expiration time we
957 // can use as the timeout.
845 - msUntilTimeout =
846 - expirationTimeToMs(workInProgressRootLatestSuspenseTimeout) - now();
847 - } else if (workInProgressRootLatestProcessedEventTime === Sync) {
958 + msUntilTimeout = workInProgressRootLatestSuspenseTimeout - now();
959 + } else if (workInProgressRootLatestProcessedEventTime === -1) {
960 // This should never normally happen because only new updates
961 // cause delayed states, so we should have processed something.
962 // However, this could also happen in an offscreen tree.
@@ -852,9 +964,7 @@ function finishConcurrentRender(
964 } else {
965 // If we didn't process a suspense config, compute a JND based on
966 // the amount of time elapsed since the most recent event time.
855 - const eventTimeMs = expirationTimeToMs(
856 - workInProgressRootLatestProcessedEventTime,
857 - );
967 + const eventTimeMs = workInProgressRootLatestProcessedEventTime;
968 const timeElapsedMs = now() - eventTimeMs;
969 msUntilTimeout = jnd(timeElapsedMs) - timeElapsedMs;
970 }
@@ -880,7 +990,7 @@ function finishConcurrentRender(
990 if (
991 // do not delay if we're inside an act() scope
992 !shouldForceFlushFallbacksInDEV() &&
883 - workInProgressRootLatestProcessedEventTime !== Sync &&
993 + workInProgressRootLatestProcessedEventTime !== -1 &&
994 workInProgressRootCanSuspendUsingConfig !== null
995 ) {
996 // If we have exceeded the minimum loading delay, which probably
@@ -889,7 +999,6 @@ function finishConcurrentRender(
999 // enough time.
1000 const msUntilTimeout = computeMsUntilSuspenseLoadingDelay(
1001 workInProgressRootLatestProcessedEventTime,
892 - expirationTime,
1002 workInProgressRootCanSuspendUsingConfig,
1003 );
1004 if (msUntilTimeout > 10) {
@@ -920,15 +1029,15 @@ function performSyncWorkOnRoot(root) {
1029
1030 flushPassiveEffects();
1031
923 - const lastExpiredTime = root.lastExpiredTime;
1032 + const lastExpiredTime = root.lastExpiredTime_opaque;
1033
1034 let expirationTime;
926 - if (lastExpiredTime !== NoWork) {
1035 + if (!isSameExpirationTime(lastExpiredTime, (NoWork: ExpirationTimeOpaque))) {
1036 // There's expired work on this root. Check if we have a partial tree
1037 // that we can reuse.
1038 if (
1039 root === workInProgressRoot &&
931 - renderExpirationTime >= lastExpiredTime
1040 + isSameOrHigherPriority(renderExpirationTime, lastExpiredTime)
1041 ) {
1042 // There's a partial tree with equal or greater than priority than the
1043 // expired level. Finish rendering it before rendering the rest of the
@@ -951,7 +1060,12 @@ function performSyncWorkOnRoot(root) {
1060 // render at Idle (or lower) so that all pending updates are included.
1061 // If it still fails after the second attempt, we'll give up and commit
1062 // the resulting tree.
954 - expirationTime = expirationTime > Idle ? Idle : expirationTime;
1063 + expirationTime = !isSameOrHigherPriority(
1064 + (Idle: ExpirationTimeOpaque),
1065 + expirationTime,
1066 + )
1067 + ? (Idle: ExpirationTimeOpaque)
1068 + : expirationTime;
1069 exitStatus = renderRootSync(root, expirationTime);
1070 }
1071
@@ -967,8 +1081,8 @@ function performSyncWorkOnRoot(root) {
1081 // will commit it even if something suspended.
1082 const finishedWork: Fiber = (root.current.alternate: any);
1083 root.finishedWork = finishedWork;
970 - root.finishedExpirationTime = expirationTime;
971 - root.nextKnownPendingLevel = getRemainingExpirationTime(finishedWork);
1084 + root.finishedExpirationTime_opaque = expirationTime;
1085 + root.nextKnownPendingLevel_opaque = getRemainingExpirationTime(finishedWork);
1086 commitRoot(root);
1087
1088 // Before exiting, make sure there's a callback scheduled for the next
@@ -978,7 +1092,10 @@ function performSyncWorkOnRoot(root) {
1092 return null;
1093 }
1094
981 -export function flushRoot(root: FiberRoot, expirationTime: ExpirationTime) {
1095 +export function flushRoot(
1096 + root: FiberRoot,
1097 + expirationTime: ExpirationTimeOpaque,
1098 +) {
1099 markRootExpiredAtTime(root, expirationTime);
1100 ensureRootIsScheduled(root);
1101 if ((executionContext & (RenderContext | CommitContext)) === NoContext) {
@@ -1016,7 +1133,7 @@ export function flushDiscreteUpdates() {
1133
1134 export function deferredUpdates<A>(fn: () => A): A {
1135 // TODO: Remove in favor of Scheduler.next
1019 - return runWithPriority(NormalPriority, fn);
1136 + return runWithPriority(NormalSchedulerPriority, fn);
1137 }
1138
1139 export function syncUpdates<A, B, C, R>(
@@ -1025,7 +1142,7 @@ export function syncUpdates<A, B, C, R>(
1142 b: B,
1143 c: C,
1144 ): R {
1028 - return runWithPriority(ImmediatePriority, fn.bind(null, a, b, c));
1145 + return runWithPriority(ImmediateSchedulerPriority, fn.bind(null, a, b, c));
1146 }
1147
1148 function flushPendingDiscreteUpdates() {
@@ -1082,7 +1199,10 @@ export function discreteUpdates<A, B, C, D, R>(
1199 executionContext |= DiscreteEventContext;
1200 try {
1201 // Should this
1085 - return runWithPriority(UserBlockingPriority, fn.bind(null, a, b, c, d));
1202 + return runWithPriority(
1203 + UserBlockingSchedulerPriority,
1204 + fn.bind(null, a, b, c, d),
1205 + );
1206 } finally {
1207 executionContext = prevExecutionContext;
1208 if (executionContext === NoContext) {
@@ -1118,7 +1238,7 @@ export function flushSync<A, R>(fn: A => R, a: A): R {
1238 const prevExecutionContext = executionContext;
1239 executionContext |= BatchedContext;
1240 try {
1121 - return runWithPriority(ImmediatePriority, fn.bind(null, a));
1241 + return runWithPriority(ImmediateSchedulerPriority, fn.bind(null, a));
1242 } finally {
1243 executionContext = prevExecutionContext;
1244 // Flush the immediate callbacks that were scheduled during this batch.
@@ -1132,7 +1252,7 @@ export function flushControlled(fn: () => mixed): void {
1252 const prevExecutionContext = executionContext;
1253 executionContext |= BatchedContext;
1254 try {
1135 - runWithPriority(ImmediatePriority, fn);
1255 + runWithPriority(ImmediateSchedulerPriority, fn);
1256 } finally {
1257 executionContext = prevExecutionContext;
1258 if (executionContext === NoContext) {
@@ -1144,7 +1264,7 @@ export function flushControlled(fn: () => mixed): void {
1264
1265 function prepareFreshStack(root, expirationTime) {
1266 root.finishedWork = null;
1147 - root.finishedExpirationTime = NoWork;
1267 + root.finishedExpirationTime_opaque = NoWork;
1268
1269 const timeoutHandle = root.timeoutHandle;
1270 if (timeoutHandle !== noTimeout) {
@@ -1156,17 +1276,23 @@ function prepareFreshStack(root, expirationTime) {
1276 }
1277
1278 // Check if there's a suspended level at lower priority.
1159 - const lastSuspendedTime = root.lastSuspendedTime;
1160 - if (lastSuspendedTime !== NoWork && lastSuspendedTime < expirationTime) {
1161 - const lastPingedTime = root.lastPingedTime;
1279 + const lastSuspendedTime = root.lastSuspendedTime_opaque;
1280 + if (
1281 + !isSameExpirationTime(lastSuspendedTime, (NoWork: ExpirationTimeOpaque)) &&
1282 + !isSameOrHigherPriority(lastSuspendedTime, expirationTime)
1283 + ) {
1284 + const lastPingedTime = root.lastPingedTime_opaque;
1285 // Make sure the suspended level is marked as pinged so that we return back
1286 // to it later, in case the render we're about to start gets aborted.
1287 // Generally we only reach this path via a ping, but we shouldn't assume
1288 // that will always be the case.
1289 // Note: This is defensive coding to prevent a pending commit from
1290 // being dropped without being rescheduled. It shouldn't be necessary.
1168 - if (lastPingedTime === NoWork || lastPingedTime > lastSuspendedTime) {
1169 - root.lastPingedTime = lastSuspendedTime;
1291 + if (
1292 + isSameExpirationTime(lastPingedTime, (NoWork: ExpirationTimeOpaque)) ||
1293 + !isSameOrHigherPriority(lastSuspendedTime, lastPingedTime)
1294 + ) {
1295 + root.lastPingedTime_opaque = lastSuspendedTime;
1296 }
1297 }
1298
@@ -1182,8 +1308,8 @@ function prepareFreshStack(root, expirationTime) {
1308 renderExpirationTime = expirationTime;
1309 workInProgressRootExitStatus = RootIncomplete;
1310 workInProgressRootFatalError = null;
1185 - workInProgressRootLatestProcessedEventTime = Sync;
1186 - workInProgressRootLatestSuspenseTimeout = Sync;
1311 + workInProgressRootLatestProcessedEventTime = -1;
1312 + workInProgressRootLatestSuspenseTimeout = -1;
1313 workInProgressRootCanSuspendUsingConfig = null;
1314 workInProgressRootNextUnprocessedUpdateTime = NoWork;
1315 workInProgressRootHasPendingPing = false;
@@ -1296,41 +1422,43 @@ export function markCommitTimeOfFallback() {
1422 }
1423
1424 export function markRenderEventTimeAndConfig(
1299 - eventTime: ExpirationTime,
1425 + eventTime: number,
1426 suspenseConfig: null | SuspenseConfig,
1427 ): void {
1302 - // Anything lower pri than Idle is not an update, so we should skip it.
1303 - if (eventTime > Idle) {
1304 - // Track the most recent event time of all updates processed in this batch.
1305 - if (workInProgressRootLatestProcessedEventTime > eventTime) {
1306 - workInProgressRootLatestProcessedEventTime = eventTime;
1307 - }
1308 -
1309 - // Track the largest/latest timeout deadline in this batch.
1310 - // TODO: If there are two transitions in the same batch, shouldn't we
1311 - // choose the smaller one? Maybe this is because when an intermediate
1312 - // transition is superseded, we should ignore its suspense config, but
1313 - // we don't currently.
1314 - if (suspenseConfig !== null) {
1315 - // If `timeoutMs` is not specified, we default to 5 seconds. We have to
1316 - // resolve this default here because `suspenseConfig` is owned
1317 - // by userspace.
1318 - // TODO: Store this on the root instead (transition -> timeoutMs)
1319 - // TODO: Should this default to a JND instead?
1320 - const timeoutMs = suspenseConfig.timeoutMs | 0 || LOW_PRIORITY_EXPIRATION;
1321 - const timeoutTime = computeSuspenseTimeout(eventTime, timeoutMs);
1322 - if (timeoutTime < workInProgressRootLatestSuspenseTimeout) {
1323 - workInProgressRootLatestSuspenseTimeout = timeoutTime;
1324 - workInProgressRootCanSuspendUsingConfig = suspenseConfig;
1325 - }
1428 + // Track the most recent event time of all updates processed in this batch.
1429 + if (workInProgressRootLatestProcessedEventTime < eventTime) {
1430 + workInProgressRootLatestProcessedEventTime = eventTime;
1431 + }
1432 +
1433 + // Track the largest/latest timeout deadline in this batch.
1434 + // TODO: If there are two transitions in the same batch, shouldn't we
1435 + // choose the smaller one? Maybe this is because when an intermediate
1436 + // transition is superseded, we should ignore its suspense config, but
1437 + // we don't currently.
1438 + if (suspenseConfig !== null) {
1439 + // If `timeoutMs` is not specified, we default to 5 seconds. We have to
1440 + // resolve this default here because `suspenseConfig` is owned
1441 + // by userspace.
1442 + // TODO: Store this on the root instead (transition -> timeoutMs)
1443 + // TODO: Should this default to a JND instead?
1444 + const timeoutMs = suspenseConfig.timeoutMs | 0 || DEFAULT_TIMEOUT_MS;
1445 + const timeoutTime = eventTime + timeoutMs;
1446 + if (timeoutTime > workInProgressRootLatestSuspenseTimeout) {
1447 + workInProgressRootLatestSuspenseTimeout = timeoutTime;
1448 + workInProgressRootCanSuspendUsingConfig = suspenseConfig;
1449 }
1450 }
1451 }
1452
1453 export function markUnprocessedUpdateTime(
1331 - expirationTime: ExpirationTime,
1454 + expirationTime: ExpirationTimeOpaque,
1455 ): void {
1333 - if (expirationTime > workInProgressRootNextUnprocessedUpdateTime) {
1456 + if (
1457 + !isSameOrHigherPriority(
1458 + workInProgressRootNextUnprocessedUpdateTime,
1459 + expirationTime,
1460 + )
1461 + ) {
1462 workInProgressRootNextUnprocessedUpdateTime = expirationTime;
1463 }
1464 }
@@ -1351,7 +1479,10 @@ export function renderDidSuspendDelayIfPossible(): void {
1479
1480 // Check if there's a lower priority update somewhere else in the tree.
1481 if (
1354 - workInProgressRootNextUnprocessedUpdateTime !== NoWork &&
1482 + !isSameExpirationTime(
1483 + workInProgressRootNextUnprocessedUpdateTime,
1484 + (NoWork: ExpirationTimeOpaque),
1485 + ) &&
1486 workInProgressRoot !== null
1487 ) {
1488 // Mark the current render as suspended, and then mark that there's a
@@ -1387,7 +1518,10 @@ function renderRootSync(root, expirationTime) {
1518
1519 // If the root or expiration time have changed, throw out the existing stack
1520 // and prepare a fresh one. Otherwise we'll continue where we left off.
1390 - if (root !== workInProgressRoot || expirationTime !== renderExpirationTime) {
1521 + if (
1522 + root !== workInProgressRoot ||
1523 + !isSameExpirationTime(expirationTime, renderExpirationTime)
1524 + ) {
1525 prepareFreshStack(root, expirationTime);
1526 startWorkOnPendingInteractions(root, expirationTime);
1527 }
@@ -1441,7 +1575,10 @@ function renderRootConcurrent(root, expirationTime) {
1575
1576 // If the root or expiration time have changed, throw out the existing stack
1577 // and prepare a fresh one. Otherwise we'll continue where we left off.
1444 - if (root !== workInProgressRoot || expirationTime !== renderExpirationTime) {
1578 + if (
1579 + root !== workInProgressRoot ||
1580 + !isSameExpirationTime(expirationTime, renderExpirationTime)
1581 + ) {
1582 prepareFreshStack(root, expirationTime);
1583 startWorkOnPendingInteractions(root, expirationTime);
1584 }
@@ -1648,17 +1785,23 @@ function completeUnitOfWork(unitOfWork: Fiber): void {
1785 }
1786
1787 function getRemainingExpirationTime(fiber: Fiber) {
1651 - const updateExpirationTime = fiber.expirationTime;
1652 - const childExpirationTime = fiber.childExpirationTime;
1653 - return updateExpirationTime > childExpirationTime
1788 + const updateExpirationTime = fiber.expirationTime_opaque;
1789 + const childExpirationTime = fiber.childExpirationTime_opaque;
1790 + return !isSameOrHigherPriority(childExpirationTime, updateExpirationTime)
1791 ? updateExpirationTime
1792 : childExpirationTime;
1793 }
1794
1795 function resetChildExpirationTime(completedWork: Fiber) {
1796 if (
1660 - renderExpirationTime !== Never &&
1661 - completedWork.childExpirationTime === Never
1797 + !isSameExpirationTime(
1798 + renderExpirationTime,
1799 + (Never: ExpirationTimeOpaque),
1800 + ) &&
1801 + isSameExpirationTime(
1802 + completedWork.childExpirationTime_opaque,
1803 + (Never: ExpirationTimeOpaque),
1804 + )
1805 ) {
1806 // The children of this component are hidden. Don't bubble their
1807 // expiration times.
@@ -1687,12 +1830,22 @@ function resetChildExpirationTime(completedWork: Fiber) {
1830
1831 let child = completedWork.child;
1832 while (child !== null) {
1690 - const childUpdateExpirationTime = child.expirationTime;
1691 - const childChildExpirationTime = child.childExpirationTime;
1692 - if (childUpdateExpirationTime > newChildExpirationTime) {
1833 + const childUpdateExpirationTime = child.expirationTime_opaque;
1834 + const childChildExpirationTime = child.childExpirationTime_opaque;
1835 + if (
1836 + !isSameOrHigherPriority(
1837 + newChildExpirationTime,
1838 + childUpdateExpirationTime,
1839 + )
1840 + ) {
1841 newChildExpirationTime = childUpdateExpirationTime;
1842 }
1695 - if (childChildExpirationTime > newChildExpirationTime) {
1843 + if (
1844 + !isSameOrHigherPriority(
1845 + newChildExpirationTime,
1846 + childChildExpirationTime,
1847 + )
1848 + ) {
1849 newChildExpirationTime = childChildExpirationTime;
1850 }
1851 if (shouldBubbleActualDurations) {
@@ -1706,25 +1859,35 @@ function resetChildExpirationTime(completedWork: Fiber) {
1859 } else {
1860 let child = completedWork.child;
1861 while (child !== null) {
1709 - const childUpdateExpirationTime = child.expirationTime;
1710 - const childChildExpirationTime = child.childExpirationTime;
1711 - if (childUpdateExpirationTime > newChildExpirationTime) {
1862 + const childUpdateExpirationTime = child.expirationTime_opaque;
1863 + const childChildExpirationTime = child.childExpirationTime_opaque;
1864 + if (
1865 + !isSameOrHigherPriority(
1866 + newChildExpirationTime,
1867 + childUpdateExpirationTime,
1868 + )
1869 + ) {
1870 newChildExpirationTime = childUpdateExpirationTime;
1871 }
1714 - if (childChildExpirationTime > newChildExpirationTime) {
1872 + if (
1873 + !isSameOrHigherPriority(
1874 + newChildExpirationTime,
1875 + childChildExpirationTime,
1876 + )
1877 + ) {
1878 newChildExpirationTime = childChildExpirationTime;
1879 }
1880 child = child.sibling;
1881 }
1882 }
1883
1721 - completedWork.childExpirationTime = newChildExpirationTime;
1884 + completedWork.childExpirationTime_opaque = newChildExpirationTime;
1885 }
1886
1887 function commitRoot(root) {
1888 const renderPriorityLevel = getCurrentPriorityLevel();
1889 runWithPriority(
1727 - ImmediatePriority,
1890 + ImmediateSchedulerPriority,
1891 commitRootImpl.bind(null, root, renderPriorityLevel),
1892 );
1893 return null;
@@ -1748,12 +1911,12 @@ function commitRootImpl(root, renderPriorityLevel) {
1911 );
1912
1913 const finishedWork = root.finishedWork;
1751 - const expirationTime = root.finishedExpirationTime;
1914 + const expirationTime = root.finishedExpirationTime_opaque;
1915 if (finishedWork === null) {
1916 return null;
1917 }
1918 root.finishedWork = null;
1756 - root.finishedExpirationTime = NoWork;
1919 + root.finishedExpirationTime_opaque = NoWork;
1920
1921 invariant(
1922 finishedWork !== root.current,
@@ -1764,8 +1927,12 @@ function commitRootImpl(root, renderPriorityLevel) {
1927 // commitRoot never returns a continuation; it always finishes synchronously.
1928 // So we can clear these now to allow a new callback to be scheduled.
1929 root.callbackNode = null;
1767 - root.callbackExpirationTime = NoWork;
1768 - root.callbackPriority = NoPriority;
1930 + root.callbackId = NoWork;
1931 + // TODO: Use LanePriority instead of SchedulerPriority
1932 + if (renderPriorityLevel < ImmediateSchedulerPriority) {
1933 + // If this was a concurrent render, we can reset the expiration time.
1934 + root.expiresAt = -1;
1935 + }
1936
1937 // Update the first and last pending times on this root. The new first
1938 // pending time is whatever is left on the root fiber.
@@ -1785,7 +1952,10 @@ function commitRootImpl(root, renderPriorityLevel) {
1952 const lastDiscreteTime = rootsWithPendingDiscreteUpdates.get(root);
1953 if (
1954 lastDiscreteTime !== undefined &&
1788 - remainingExpirationTimeBeforeCommit < lastDiscreteTime
1955 + !isSameOrHigherPriority(
1956 + remainingExpirationTimeBeforeCommit,
1957 + lastDiscreteTime,
1958 + )
1959 ) {
1960 rootsWithPendingDiscreteUpdates.delete(root);
1961 }
@@ -1980,8 +2150,13 @@ function commitRootImpl(root, renderPriorityLevel) {
2150 }
2151
2152 // Check if there's remaining work on this root
1983 - const remainingExpirationTime = root.firstPendingTime;
1984 - if (remainingExpirationTime !== NoWork) {
2153 + const remainingExpirationTime = root.firstPendingTime_opaque;
2154 + if (
2155 + !isSameExpirationTime(
2156 + remainingExpirationTime,
2157 + (NoWork: ExpirationTimeOpaque),
2158 + )
2159 + ) {
2160 if (enableSchedulerTracing) {
2161 if (spawnedWorkDuringRender !== null) {
2162 const expirationTimes = spawnedWorkDuringRender;
@@ -2012,7 +2187,9 @@ function commitRootImpl(root, renderPriorityLevel) {
2187 }
2188 }
2189
2015 - if (remainingExpirationTime === Sync) {
2190 + if (
2191 + isSameExpirationTime(remainingExpirationTime, (Sync: ExpirationTimeOpaque))
2192 + ) {
2193 // Count the number of times the root synchronously re-renders without
2194 // finishing. If there are too many, it indicates an infinite update loop.
2195 if (root === rootWithNestedUpdates) {
@@ -2025,7 +2202,7 @@ function commitRootImpl(root, renderPriorityLevel) {
2202 nestedUpdateCount = 0;
2203 }
2204
2028 - onCommitRoot(finishedWork.stateNode, expirationTime);
2205 + onCommitRoot(finishedWork.stateNode, renderPriorityLevel);
2206
2207 // Always call this before exiting `commitRoot`, to ensure that any
2208 // additional work on this root is scheduled.
@@ -2076,7 +2253,7 @@ function commitBeforeMutationEffects() {
2253 // the earliest opportunity.
2254 if (!rootDoesHavePassiveEffects) {
2255 rootDoesHavePassiveEffects = true;
2079 - scheduleCallback(NormalPriority, () => {
2256 + scheduleCallback(NormalSchedulerPriority, () => {
2257 flushPassiveEffects();
2258 return null;
2259 });
@@ -2162,7 +2339,7 @@ function commitMutationEffects(root: FiberRoot, renderPriorityLevel) {
2339
2340 function commitLayoutEffects(
2341 root: FiberRoot,
2165 - committedExpirationTime: ExpirationTime,
2342 + committedExpirationTime: ExpirationTimeOpaque,
2343 ) {
2344 // TODO: Should probably move the bulk of this function to commitWork.
2345 while (nextEffect !== null) {
@@ -2190,12 +2367,12 @@ function commitLayoutEffects(
2367 }
2368
2369 export function flushPassiveEffects() {
2193 - if (pendingPassiveEffectsRenderPriority !== NoPriority) {
2370 + if (pendingPassiveEffectsRenderPriority !== NoSchedulerPriority) {
2371 const priorityLevel =
2195 - pendingPassiveEffectsRenderPriority > NormalPriority
2196 - ? NormalPriority
2372 + pendingPassiveEffectsRenderPriority > NormalSchedulerPriority
2373 + ? NormalSchedulerPriority
2374 : pendingPassiveEffectsRenderPriority;
2198 - pendingPassiveEffectsRenderPriority = NoPriority;
2375 + pendingPassiveEffectsRenderPriority = NoSchedulerPriority;
2376 return runWithPriority(priorityLevel, flushPassiveEffectsImpl);
2377 }
2378 }
@@ -2205,7 +2382,7 @@ export function enqueuePendingPassiveProfilerEffect(fiber: Fiber): void {
2382 pendingPassiveProfilerEffects.push(fiber);
2383 if (!rootDoesHavePassiveEffects) {
2384 rootDoesHavePassiveEffects = true;
2208 - scheduleCallback(NormalPriority, () => {
2385 + scheduleCallback(NormalSchedulerPriority, () => {
2386 flushPassiveEffects();
2387 return null;
2388 });
@@ -2221,7 +2398,7 @@ export function enqueuePendingPassiveHookEffectMount(
2398 pendingPassiveHookEffectsMount.push(effect, fiber);
2399 if (!rootDoesHavePassiveEffects) {
2400 rootDoesHavePassiveEffects = true;
2224 - scheduleCallback(NormalPriority, () => {
2401 + scheduleCallback(NormalSchedulerPriority, () => {
2402 flushPassiveEffects();
2403 return null;
2404 });
@@ -2246,7 +2423,7 @@ export function enqueuePendingPassiveHookEffectUnmount(
2423 }
2424 if (!rootDoesHavePassiveEffects) {
2425 rootDoesHavePassiveEffects = true;
2249 - scheduleCallback(NormalPriority, () => {
2426 + scheduleCallback(NormalSchedulerPriority, () => {
2427 flushPassiveEffects();
2428 return null;
2429 });
@@ -2493,12 +2670,19 @@ function captureCommitPhaseErrorOnRoot(
2670 error: mixed,
2671 ) {
2672 const errorInfo = createCapturedValue(error, sourceFiber);
2496 - const update = createRootErrorUpdate(rootFiber, errorInfo, Sync);
2673 + const update = createRootErrorUpdate(
2674 + rootFiber,
2675 + errorInfo,
2676 + (Sync: ExpirationTimeOpaque),
2677 + );
2678 enqueueUpdate(rootFiber, update);
2498 - const root = markUpdateTimeFromFiberToRoot(rootFiber, Sync);
2679 + const root = markUpdateTimeFromFiberToRoot(
2680 + rootFiber,
2681 + (Sync: ExpirationTimeOpaque),
2682 + );
2683 if (root !== null) {
2684 ensureRootIsScheduled(root);
2501 - schedulePendingInteractions(root, Sync);
2685 + schedulePendingInteractions(root, (Sync: ExpirationTimeOpaque));
2686 }
2687 }
2688
@@ -2527,14 +2711,16 @@ export function captureCommitPhaseError(sourceFiber: Fiber, error: mixed) {
2711 const update = createClassErrorUpdate(
2712 fiber,
2713 errorInfo,
2530 - // TODO: This is always sync
2531 - Sync,
2714 + (Sync: ExpirationTimeOpaque),
2715 );
2716 enqueueUpdate(fiber, update);
2534 - const root = markUpdateTimeFromFiberToRoot(fiber, Sync);
2717 + const root = markUpdateTimeFromFiberToRoot(
2718 + fiber,
2719 + (Sync: ExpirationTimeOpaque),
2720 + );
2721 if (root !== null) {
2722 ensureRootIsScheduled(root);
2537 - schedulePendingInteractions(root, Sync);
2723 + schedulePendingInteractions(root, (Sync: ExpirationTimeOpaque));
2724 }
2725 return;
2726 }
@@ -2546,7 +2732,7 @@ export function captureCommitPhaseError(sourceFiber: Fiber, error: mixed) {
2732 export function pingSuspendedRoot(
2733 root: FiberRoot,
2734 wakeable: Wakeable,
2549 - suspendedTime: ExpirationTime,
2735 + suspendedTime: ExpirationTimeOpaque,
2736 ) {
2737 const pingCache = root.pingCache;
2738 if (pingCache !== null) {
@@ -2555,7 +2741,10 @@ export function pingSuspendedRoot(
2741 pingCache.delete(wakeable);
2742 }
2743
2558 - if (workInProgressRoot === root && renderExpirationTime === suspendedTime) {
2744 + if (
2745 + workInProgressRoot === root &&
2746 + isSameExpirationTime(renderExpirationTime, suspendedTime)
2747 + ) {
2748 // Received a ping at the same priority level at which we're currently
2749 // rendering. We might want to restart this render. This should mirror
2750 // the logic of whether or not a root suspends once it completes.
@@ -2572,7 +2761,7 @@ export function pingSuspendedRoot(
2761 if (
2762 workInProgressRootExitStatus === RootSuspendedWithDelay ||
2763 (workInProgressRootExitStatus === RootSuspended &&
2575 - workInProgressRootLatestProcessedEventTime === Sync &&
2764 + workInProgressRootLatestProcessedEventTime === -1 &&
2765 now() - globalMostRecentFallbackTime < FALLBACK_THROTTLE_MS)
2766 ) {
2767 // Restart from the root. Don't need to schedule a ping because
@@ -2591,14 +2780,17 @@ export function pingSuspendedRoot(
2780 return;
2781 }
2782
2594 - const lastPingedTime = root.lastPingedTime;
2595 - if (lastPingedTime !== NoWork && lastPingedTime < suspendedTime) {
2783 + const lastPingedTime = root.lastPingedTime_opaque;
2784 + if (
2785 + !isSameExpirationTime(lastPingedTime, (NoWork: ExpirationTimeOpaque)) &&
2786 + !isSameOrHigherPriority(lastPingedTime, suspendedTime)
2787 + ) {
2788 // There's already a lower priority ping scheduled.
2789 return;
2790 }
2791
2792 // Mark the time at which this ping was scheduled.
2601 - root.lastPingedTime = suspendedTime;
2793 + root.lastPingedTime_opaque = suspendedTime;
2794
2795 ensureRootIsScheduled(root);
2796 schedulePendingInteractions(root, suspendedTime);
@@ -2606,20 +2798,15 @@ export function pingSuspendedRoot(
2798
2799 function retryTimedOutBoundary(
2800 boundaryFiber: Fiber,
2609 - retryTime: ExpirationTime,
2801 + retryTime: ExpirationTimeOpaque,
2802 ) {
2803 // The boundary fiber (a Suspense component or SuspenseList component)
2804 // previously was rendered in its fallback state. One of the promises that
2805 // suspended it has resolved, which means at least part of the tree was
2806 // likely unblocked. Try rendering again, at a new expiration time.
2615 - if (retryTime === NoWork) {
2807 + if (isSameExpirationTime(retryTime, (NoWork: ExpirationTimeOpaque))) {
2808 const suspenseConfig = null; // Retries don't carry over the already committed update.
2617 - const currentTime = requestCurrentTimeForUpdate();
2618 - retryTime = computeExpirationForFiber(
2619 - currentTime,
2620 - boundaryFiber,
2621 - suspenseConfig,
2622 - );
2809 + retryTime = requestUpdateExpirationTime(boundaryFiber, suspenseConfig);
2810 }
2811 // TODO: Special case idle priority?
2812 const root = markUpdateTimeFromFiberToRoot(boundaryFiber, retryTime);
@@ -2699,8 +2886,7 @@ function jnd(timeElapsed: number) {
2886 }
2887
2888 function computeMsUntilSuspenseLoadingDelay(
2702 - mostRecentEventTime: ExpirationTime,
2703 - committedExpirationTime: ExpirationTime,
2889 + mostRecentEventTime: number,
2890 suspenseConfig: SuspenseConfig,
2891 ) {
2892 const busyMinDurationMs = (suspenseConfig.busyMinDurationMs: any) | 0;
@@ -2711,7 +2897,7 @@ function computeMsUntilSuspenseLoadingDelay(
2897
2898 // Compute the time until this render pass would expire.
2899 const currentTimeMs: number = now();
2714 - const eventTimeMs: number = expirationTimeToMs(mostRecentEventTime);
2900 + const eventTimeMs: number = mostRecentEventTime;
2901 const timeElapsed = currentTimeMs - eventTimeMs;
2902 if (timeElapsed <= busyDelayMs) {
2903 // If we haven't yet waited longer than the initial delay, we don't
@@ -3101,12 +3287,18 @@ export function warnIfUnmockedScheduler(fiber: Fiber) {
3287 }
3288 }
3289
3104 -function computeThreadID(root, expirationTime) {
3290 +function computeThreadID(
3291 + root: FiberRoot,
3292 + expirationTime: ExpirationTimeOpaque,
3293 +) {
3294 // Interaction threads are unique per root and expiration time.
3106 - return expirationTime * 1000 + root.interactionThreadID;
3295 + // NOTE: Intentionally unsound cast. All that matters is that it's a number
3296 + // and it represents a batch of work. Could make a helper function instead,
3297 + // but meh this is fine for now.
3298 + return (expirationTime: any) * 1000 + root.interactionThreadID;
3299 }
3300
3109 -export function markSpawnedWork(expirationTime: ExpirationTime) {
3301 +export function markSpawnedWork(expirationTime: ExpirationTimeOpaque) {
3302 if (!enableSchedulerTracing) {
3303 return;
3304 }
@@ -3117,13 +3309,17 @@ export function markSpawnedWork(expirationTime: ExpirationTime) {
3309 }
3310 }
3311
3120 -function scheduleInteractions(root, expirationTime, interactions) {
3312 +function scheduleInteractions(
3313 + root: FiberRoot,
3314 + expirationTime: ExpirationTimeOpaque,
3315 + interactions: Set<Interaction>,
3316 +) {
3317 if (!enableSchedulerTracing) {
3318 return;
3319 }
3320
3321 if (interactions.size > 0) {
3126 - const pendingInteractionMap = root.pendingInteractionMap;
3322 + const pendingInteractionMap = root.pendingInteractionMap_new;
3323 const pendingInteractions = pendingInteractionMap.get(expirationTime);
3324 if (pendingInteractions != null) {
3325 interactions.forEach(interaction => {
@@ -3151,7 +3347,10 @@ function scheduleInteractions(root, expirationTime, interactions) {
3347 }
3348 }
3349
3154 -function schedulePendingInteractions(root, expirationTime) {
3350 +function schedulePendingInteractions(
3351 + root: FiberRoot,
3352 + expirationTime: ExpirationTimeOpaque,
3353 +) {
3354 // This is called when work is scheduled on a root.
3355 // It associates the current interactions with the newly-scheduled expiration.
3356 // They will be restored when that expiration is later committed.
@@ -3162,7 +3361,10 @@ function schedulePendingInteractions(root, expirationTime) {
3361 scheduleInteractions(root, expirationTime, __interactionsRef.current);
3362 }
3363
3165 -function startWorkOnPendingInteractions(root, expirationTime) {
3364 +function startWorkOnPendingInteractions(
3365 + root: FiberRoot,
3366 + expirationTime: ExpirationTimeOpaque,
3367 +) {
3368 // This is called when new work is started on a root.
3369 if (!enableSchedulerTracing) {
3370 return;
@@ -3172,9 +3374,9 @@ function startWorkOnPendingInteractions(root, expirationTime) {
3374 // we can accurately attribute time spent working on it, And so that cascading
3375 // work triggered during the render phase will be associated with it.
3376 const interactions: Set<Interaction> = new Set();
3175 - root.pendingInteractionMap.forEach(
3377 + root.pendingInteractionMap_new.forEach(
3378 (scheduledInteractions, scheduledExpirationTime) => {
3177 - if (scheduledExpirationTime >= expirationTime) {
3379 + if (isSameOrHigherPriority(scheduledExpirationTime, expirationTime)) {
3380 scheduledInteractions.forEach(interaction =>
3381 interactions.add(interaction),
3382 );
@@ -3197,7 +3399,7 @@ function startWorkOnPendingInteractions(root, expirationTime) {
3399 subscriber.onWorkStarted(interactions, threadID);
3400 } catch (error) {
3401 // If the subscriber throws, rethrow it in a separate task
3200 - scheduleCallback(ImmediatePriority, () => {
3402 + scheduleCallback(ImmediateSchedulerPriority, () => {
3403 throw error;
3404 });
3405 }
@@ -3210,7 +3412,7 @@ function finishPendingInteractions(root, committedExpirationTime) {
3412 return;
3413 }
3414
3213 - const earliestRemainingTimeAfterCommit = root.firstPendingTime;
3415 + const earliestRemainingTimeAfterCommit = root.firstPendingTime_opaque;
3416
3417 let subscriber;
3418
@@ -3222,20 +3424,25 @@ function finishPendingInteractions(root, committedExpirationTime) {
3424 }
3425 } catch (error) {
3426 // If the subscriber throws, rethrow it in a separate task
3225 - scheduleCallback(ImmediatePriority, () => {
3427 + scheduleCallback(ImmediateSchedulerPriority, () => {
3428 throw error;
3429 });
3430 } finally {
3431 // Clear completed interactions from the pending Map.
3432 // Unless the render was suspended or cascading work was scheduled,
3433 // In which case– leave pending interactions until the subsequent render.
3232 - const pendingInteractionMap = root.pendingInteractionMap;
3434 + const pendingInteractionMap = root.pendingInteractionMap_new;
3435 pendingInteractionMap.forEach(
3436 (scheduledInteractions, scheduledExpirationTime) => {
3437 // Only decrement the pending interaction count if we're done.
3438 // If there's still work at the current priority,
3439 // That indicates that we are waiting for suspense data.
3238 - if (scheduledExpirationTime > earliestRemainingTimeAfterCommit) {
3440 + if (
3441 + !isSameOrHigherPriority(
3442 + earliestRemainingTimeAfterCommit,
3443 + scheduledExpirationTime,
3444 + )
3445 + ) {
3446 pendingInteractionMap.delete(scheduledExpirationTime);
3447
3448 scheduledInteractions.forEach(interaction => {
@@ -3246,7 +3453,7 @@ function finishPendingInteractions(root, committedExpirationTime) {
3453 subscriber.onInteractionScheduledWorkCompleted(interaction);
3454 } catch (error) {
3455 // If the subscriber throws, rethrow it in a separate task
3249 - scheduleCallback(ImmediatePriority, () => {
3456 + scheduleCallback(ImmediateSchedulerPriority, () => {
3457 throw error;
3458 });
3459 }
packages/react-reconciler/src/ReactFiberWorkLoop.old.js
+3 -3
@@ -3258,7 +3258,7 @@ function scheduleInteractions(root, expirationTime, interactions) {
3258 }
3259
3260 if (interactions.size > 0) {
3261 - const pendingInteractionMap = root.pendingInteractionMap;
3261 + const pendingInteractionMap = root.pendingInteractionMap_old;
3262 const pendingInteractions = pendingInteractionMap.get(expirationTime);
3263 if (pendingInteractions != null) {
3264 interactions.forEach(interaction => {
@@ -3307,7 +3307,7 @@ function startWorkOnPendingInteractions(root, expirationTime) {
3307 // we can accurately attribute time spent working on it, And so that cascading
3308 // work triggered during the render phase will be associated with it.
3309 const interactions: Set<Interaction> = new Set();
3310 - root.pendingInteractionMap.forEach(
3310 + root.pendingInteractionMap_old.forEach(
3311 (scheduledInteractions, scheduledExpirationTime) => {
3312 if (scheduledExpirationTime >= expirationTime) {
3313 scheduledInteractions.forEach(interaction =>
@@ -3364,7 +3364,7 @@ function finishPendingInteractions(root, committedExpirationTime) {
3364 // Clear completed interactions from the pending Map.
3365 // Unless the render was suspended or cascading work was scheduled,
3366 // In which case– leave pending interactions until the subsequent render.
3367 - const pendingInteractionMap = root.pendingInteractionMap;
3367 + const pendingInteractionMap = root.pendingInteractionMap_old;
3368 pendingInteractionMap.forEach(
3369 (scheduledInteractions, scheduledExpirationTime) => {
3370 // Only decrement the pending interaction count if we're done.
packages/react-reconciler/src/ReactInternalTypes.js
+56 -11
@@ -23,7 +23,7 @@ import type {WorkTag} from './ReactWorkTags';
23 import type {TypeOfMode} from './ReactTypeOfMode';
24 import type {SideEffectTag} from './ReactSideEffectTags';
25 import type {ExpirationTime} from './ReactFiberExpirationTime.old';
26 -// import type {UpdateQueue} from './ReactUpdateQueue.old';
26 +import type {ExpirationTimeOpaque} from './ReactFiberExpirationTime.new';
27 import type {HookType} from './ReactFiberHooks.old';
28 import type {RootTag} from './ReactRootTags';
29 import type {TimeoutHandle, NoTimeout} from './ReactFiberHostConfig';
@@ -41,7 +41,7 @@ export type ContextDependency<T> = {
41 ...
42 };
43
44 -export type Dependencies = {
44 +export type Dependencies_old = {
45 expirationTime: ExpirationTime,
46 firstContext: ContextDependency<mixed> | null,
47 responders: Map<
@@ -51,6 +51,16 @@ export type Dependencies = {
51 ...
52 };
53
54 +export type Dependencies_new = {
55 + expirationTime: ExpirationTimeOpaque,
56 + firstContext: ContextDependency<mixed> | null,
57 + responders: Map<
58 + ReactEventResponder<any, any>,
59 + ReactEventResponderInstance<any, any>,
60 + > | null,
61 + ...
62 +};
63 +
64 // A Fiber is work on a Component that needs to be done or was done. There can
65 // be more than one per component.
66 export type Fiber = {|
@@ -115,7 +125,8 @@ export type Fiber = {|
125 memoizedState: any,
126
127 // Dependencies (contexts, events) for this fiber, if it has any
118 - dependencies: Dependencies | null,
128 + dependencies_new: Dependencies_new | null,
129 + dependencies_old: Dependencies_old | null,
130
131 // Bitfield that describes properties about the fiber and its subtree. E.g.
132 // the ConcurrentMode flag indicates whether the subtree should be async-by-
@@ -144,6 +155,10 @@ export type Fiber = {|
155 // This is used to quickly determine if a subtree has no pending changes.
156 childExpirationTime: ExpirationTime,
157
158 + // Only used by new reconciler
159 + expirationTime_opaque: ExpirationTimeOpaque,
160 + childExpirationTime_opaque: ExpirationTimeOpaque,
161 +
162 // This is a pooled version of a Fiber. Every fiber that gets updated will
163 // eventually have a pair. There are cases when we can clean up pairs to save
164 // memory if we need to.
@@ -184,8 +199,6 @@ export type Fiber = {|
199 _debugHookTypes?: Array<HookType> | null,
200 |};
201
187 -export type PendingInteractionMap = Map<ExpirationTime, Set<Interaction>>;
188 -
202 type BaseFiberRootProperties = {|
203 // The type of root (legacy, batched, concurrent, etc.)
204 tag: RootTag,
@@ -197,12 +210,8 @@ type BaseFiberRootProperties = {|
210 // The currently active root fiber. This is the mutable root of the tree.
211 current: Fiber,
212
200 - pingCache:
201 - | WeakMap<Wakeable, Set<ExpirationTime>>
202 - | Map<Wakeable, Set<ExpirationTime>>
203 - | null,
213 + pingCache: WeakMap<Wakeable, Set<mixed>> | Map<Wakeable, Set<mixed>> | null,
214
205 - finishedExpirationTime: ExpirationTime,
215 // A finished work-in-progress HostRoot that's ready to be committed.
216 finishedWork: Fiber | null,
217 // Timeout handle returned by setTimeout. Used to cancel a pending timeout, if
@@ -215,10 +224,15 @@ type BaseFiberRootProperties = {|
224 +hydrate: boolean,
225 // Node returned by Scheduler.scheduleCallback
226 callbackNode: *,
227 +
228 + // Only used by old reconciler
229 +
230 // Expiration of the callback associated with this root
231 callbackExpirationTime: ExpirationTime,
232 // Priority of the callback associated with this root
233 callbackPriority: ReactPriorityLevel,
234 +
235 + finishedExpirationTime: ExpirationTime,
236 // The earliest pending expiration time that exists in the tree
237 firstPendingTime: ExpirationTime,
238 // The latest pending expiration time that exists in the tree
@@ -236,6 +250,36 @@ type BaseFiberRootProperties = {|
250 // Used by useMutableSource hook to avoid tearing within this root
251 // when external, mutable sources are read from during render.
252 mutableSourceLastPendingUpdateTime: ExpirationTime,
253 +
254 + // Only used by new reconciler
255 +
256 + // Represents the next task that the root should work on, or the current one
257 + // if it's already working.
258 + // TODO: In the new system, this will be a Lanes bitmask.
259 + callbackId: ExpirationTimeOpaque,
260 + // Whether the currently scheduled task for this root is synchronous or
261 + // batched/concurrent. We have to track this because Scheduler does not
262 + // support synchronous tasks, so we put those on a separate queue. So you
263 + // could also think of this as "which queue is the callback scheduled with?"
264 + callbackIsSync: boolean,
265 + // Timestamp at which we will synchronously finish the current task to
266 + // prevent starvation.
267 + // TODO: There should be a separate expiration per lane.
268 + // NOTE: This is not an "ExpirationTime" as used by the old reconciler. It's a
269 + // timestamp, in milliseconds.
270 + expiresAt: number,
271 +
272 + // Same as corresponding fields in the old reconciler, but opaque. These will
273 + // become bitmasks.
274 + finishedExpirationTime_opaque: ExpirationTimeOpaque,
275 + firstPendingTime_opaque: ExpirationTimeOpaque,
276 + lastPendingTime_opaque: ExpirationTimeOpaque,
277 + firstSuspendedTime_opaque: ExpirationTimeOpaque,
278 + lastSuspendedTime_opaque: ExpirationTimeOpaque,
279 + nextKnownPendingLevel_opaque: ExpirationTimeOpaque,
280 + lastPingedTime_opaque: ExpirationTimeOpaque,
281 + lastExpiredTime_opaque: ExpirationTimeOpaque,
282 + mutableSourceLastPendingUpdateTime_opaque: ExpirationTimeOpaque,
283 |};
284
285 // The following attributes are only used by interaction tracing builds.
@@ -245,7 +289,8 @@ type BaseFiberRootProperties = {|
289 type ProfilingOnlyFiberRootProperties = {|
290 interactionThreadID: number,
291 memoizedInteractions: Set<Interaction>,
248 - pendingInteractionMap: PendingInteractionMap,
292 + pendingInteractionMap_new: Map<ExpirationTimeOpaque, Set<Interaction>>,
293 + pendingInteractionMap_old: Map<ExpirationTime, Set<Interaction>>,
294 |};
295
296 export type SuspenseHydrationCallbacks = {
packages/react-reconciler/src/ReactMutableSource.new.js
+26 -12
@@ -7,12 +7,16 @@
7 * @flow
8 */
9
10 -import type {ExpirationTime} from './ReactFiberExpirationTime.new';
10 +import type {ExpirationTimeOpaque} from './ReactFiberExpirationTime.new';
11 import type {FiberRoot} from './ReactInternalTypes';
12 import type {MutableSource, MutableSourceVersion} from 'shared/ReactTypes';
13
14 import {isPrimaryRenderer} from './ReactFiberHostConfig';
15 -import {NoWork} from './ReactFiberExpirationTime.new';
15 +import {
16 + NoWork,
17 + isSameOrHigherPriority,
18 + isSameExpirationTime,
19 +} from './ReactFiberExpirationTime.new';
20
21 // Work in progress version numbers only apply to a single render,
22 // and should be reset before starting a new render.
@@ -28,29 +32,39 @@ if (__DEV__) {
32
33 export function clearPendingUpdates(
34 root: FiberRoot,
31 - expirationTime: ExpirationTime,
35 + expirationTime: ExpirationTimeOpaque,
36 ): void {
33 - if (expirationTime <= root.mutableSourceLastPendingUpdateTime) {
37 + if (
38 + isSameOrHigherPriority(
39 + root.mutableSourceLastPendingUpdateTime_opaque,
40 + expirationTime,
41 + )
42 + ) {
43 // All updates for this source have been processed.
35 - root.mutableSourceLastPendingUpdateTime = NoWork;
44 + root.mutableSourceLastPendingUpdateTime_opaque = NoWork;
45 }
46 }
47
39 -export function getLastPendingExpirationTime(root: FiberRoot): ExpirationTime {
40 - return root.mutableSourceLastPendingUpdateTime;
48 +export function getLastPendingExpirationTime(
49 + root: FiberRoot,
50 +): ExpirationTimeOpaque {
51 + return root.mutableSourceLastPendingUpdateTime_opaque;
52 }
53
54 export function setPendingExpirationTime(
55 root: FiberRoot,
45 - expirationTime: ExpirationTime,
56 + expirationTime: ExpirationTimeOpaque,
57 ): void {
58 const mutableSourceLastPendingUpdateTime =
48 - root.mutableSourceLastPendingUpdateTime;
59 + root.mutableSourceLastPendingUpdateTime_opaque;
60 if (
50 - mutableSourceLastPendingUpdateTime === NoWork ||
51 - expirationTime < mutableSourceLastPendingUpdateTime
61 + isSameExpirationTime(
62 + mutableSourceLastPendingUpdateTime,
63 + (NoWork: ExpirationTimeOpaque),
64 + ) ||
65 + !isSameOrHigherPriority(expirationTime, mutableSourceLastPendingUpdateTime)
66 ) {
53 - root.mutableSourceLastPendingUpdateTime = expirationTime;
67 + root.mutableSourceLastPendingUpdateTime_opaque = expirationTime;
68 }
69 }
70
packages/react-reconciler/src/ReactUpdateQueue.new.js
+14 -10
@@ -85,10 +85,14 @@
85 // resources, but the final state is always the same.
86
87 import type {Fiber} from './ReactInternalTypes';
88 -import type {ExpirationTime} from './ReactFiberExpirationTime.new';
88 +import type {ExpirationTimeOpaque} from './ReactFiberExpirationTime.new';
89 import type {SuspenseConfig} from './ReactFiberSuspenseConfig';
90
91 -import {NoWork, Sync} from './ReactFiberExpirationTime.new';
91 +import {
92 + NoWork,
93 + Sync,
94 + isSameOrHigherPriority,
95 +} from './ReactFiberExpirationTime.new';
96 import {
97 enterDisallowedContextReadInDEV,
98 exitDisallowedContextReadInDEV,
@@ -110,8 +114,8 @@ import {disableLogs, reenableLogs} from 'shared/ConsolePatchingDev';
114 export type Update<State> = {|
115 // TODO: Temporary field. Will remove this by storing a map of
116 // transition -> event time on the root.
113 - eventTime: ExpirationTime,
114 - expirationTime: ExpirationTime,
117 + eventTime: number,
118 + expirationTime: ExpirationTimeOpaque,
119 suspenseConfig: null | SuspenseConfig,
120
121 tag: 0 | 1 | 2 | 3,
@@ -187,8 +191,8 @@ export function cloneUpdateQueue<State>(
191 }
192
193 export function createUpdate(
190 - eventTime: ExpirationTime,
191 - expirationTime: ExpirationTime,
194 + eventTime: number,
195 + expirationTime: ExpirationTimeOpaque,
196 suspenseConfig: null | SuspenseConfig,
197 ): Update<*> {
198 const update: Update<*> = {
@@ -406,7 +410,7 @@ export function processUpdateQueue<State>(
410 workInProgress: Fiber,
411 props: any,
412 instance: any,
409 - renderExpirationTime: ExpirationTime,
413 + renderExpirationTime: ExpirationTimeOpaque,
414 ): void {
415 // This is always non-null on a ClassComponent or HostRoot
416 const queue: UpdateQueue<State> = (workInProgress.updateQueue: any);
@@ -473,7 +477,7 @@ export function processUpdateQueue<State>(
477 do {
478 const updateEventTime = update.eventTime;
479 const updateExpirationTime = update.expirationTime;
476 - if (updateExpirationTime < renderExpirationTime) {
480 + if (!isSameOrHigherPriority(updateExpirationTime, renderExpirationTime)) {
481 // Priority is insufficient. Skip this update. If this is the first
482 // skipped update, the previous update/state is the new base
483 // update/state.
@@ -495,7 +499,7 @@ export function processUpdateQueue<State>(
499 newLastBaseUpdate = newLastBaseUpdate.next = clone;
500 }
501 // Update the remaining priority in the queue.
498 - if (updateExpirationTime > newExpirationTime) {
502 + if (!isSameOrHigherPriority(newExpirationTime, updateExpirationTime)) {
503 newExpirationTime = updateExpirationTime;
504 }
505 } else {
@@ -580,7 +584,7 @@ export function processUpdateQueue<State>(
584 // shouldComponentUpdate is tricky; but we'll have to account for
585 // that regardless.
586 markUnprocessedUpdateTime(newExpirationTime);
583 - workInProgress.expirationTime = newExpirationTime;
587 + workInProgress.expirationTime_opaque = newExpirationTime;
588 workInProgress.memoizedState = newState;
589 }
590
packages/react-reconciler/src/__tests__/ReactExpiration-test.js
+91 -34
@@ -22,6 +22,11 @@ describe('ReactExpiration', () => {
22 Scheduler = require('scheduler');
23 });
24
25 + function Text(props) {
26 + Scheduler.unstable_yieldValue(props.text);
27 + return props.text;
28 + }
29 +
30 function span(prop) {
31 return {type: 'span', children: [], prop, hidden: false};
32 }
@@ -47,7 +52,7 @@ describe('ReactExpiration', () => {
52 });
53
54 it('two updates of like priority in the same event always flush within the same batch', () => {
50 - class Text extends React.Component {
55 + class TextClass extends React.Component {
56 componentDidMount() {
57 Scheduler.unstable_yieldValue(`${this.props.text} [commit]`);
58 }
@@ -68,7 +73,7 @@ describe('ReactExpiration', () => {
73
74 // First, show what happens for updates in two separate events.
75 // Schedule an update.
71 - ReactNoop.render(<Text text="A" />);
76 + ReactNoop.render(<TextClass text="A" />);
77 // Advance the timer.
78 Scheduler.unstable_advanceTime(2000);
79 // Partially flush the first update, then interrupt it.
@@ -80,29 +85,28 @@ describe('ReactExpiration', () => {
85 expect(ReactNoop.getChildren()).toEqual([]);
86
87 // Schedule another update.
83 - ReactNoop.render(<Text text="B" />);
84 - // The updates should flush in separate batches, since sufficient time
85 - // passed in between them *and* they occurred in separate events.
86 - // Note: This isn't necessarily the ideal behavior. It might be better to
87 - // batch these two updates together. The fact that they aren't batched
88 - // is an implementation detail. The important part of this unit test is that
89 - // they are batched if it's possible that they happened in the same event.
90 - expect(Scheduler).toFlushAndYield([
91 - 'A [render]',
92 - 'A [commit]',
93 - 'B [render]',
94 - 'B [commit]',
95 - ]);
88 + ReactNoop.render(<TextClass text="B" />);
89 + expect(Scheduler).toFlushAndYield(
90 + gate(flags =>
91 + flags.new
92 + ? // In the new reconciler, both updates are batched
93 + ['B [render]', 'B [commit]']
94 + : // In the old reconciler, they are flushed separately. That's not
95 + // ideal, but for the purposes of this test it's fine since they
96 + // didn't happen in the same event,
97 + ['A [render]', 'A [commit]', 'B [render]', 'B [commit]'],
98 + ),
99 + );
100 expect(ReactNoop.getChildren()).toEqual([span('B')]);
101
102 // Now do the same thing again, except this time don't flush any work in
103 // between the two updates.
100 - ReactNoop.render(<Text text="A" />);
104 + ReactNoop.render(<TextClass text="A" />);
105 Scheduler.unstable_advanceTime(2000);
106 expect(Scheduler).toHaveYielded([]);
107 expect(ReactNoop.getChildren()).toEqual([span('B')]);
108 // Schedule another update.
105 - ReactNoop.render(<Text text="B" />);
109 + ReactNoop.render(<TextClass text="B" />);
110 // The updates should flush in the same batch, since as far as the scheduler
111 // knows, they may have occurred inside the same event.
112 expect(Scheduler).toFlushAndYield(['B [render]', 'B [commit]']);
@@ -112,7 +116,7 @@ describe('ReactExpiration', () => {
116 'two updates of like priority in the same event always flush within the ' +
117 "same batch, even if there's a sync update in between",
118 () => {
115 - class Text extends React.Component {
119 + class TextClass extends React.Component {
120 componentDidMount() {
121 Scheduler.unstable_yieldValue(`${this.props.text} [commit]`);
122 }
@@ -133,7 +137,7 @@ describe('ReactExpiration', () => {
137
138 // First, show what happens for updates in two separate events.
139 // Schedule an update.
136 - ReactNoop.render(<Text text="A" />);
140 + ReactNoop.render(<TextClass text="A" />);
141 // Advance the timer.
142 Scheduler.unstable_advanceTime(2000);
143 // Partially flush the first update, then interrupt it.
@@ -145,24 +149,23 @@ describe('ReactExpiration', () => {
149 expect(ReactNoop.getChildren()).toEqual([]);
150
151 // Schedule another update.
148 - ReactNoop.render(<Text text="B" />);
149 - // The updates should flush in separate batches, since sufficient time
150 - // passed in between them *and* they occurred in separate events.
151 - // Note: This isn't necessarily the ideal behavior. It might be better to
152 - // batch these two updates together. The fact that they aren't batched
153 - // is an implementation detail. The important part of this unit test is that
154 - // they are batched if it's possible that they happened in the same event.
155 - expect(Scheduler).toFlushAndYield([
156 - 'A [render]',
157 - 'A [commit]',
158 - 'B [render]',
159 - 'B [commit]',
160 - ]);
152 + ReactNoop.render(<TextClass text="B" />);
153 + expect(Scheduler).toFlushAndYield(
154 + gate(flags =>
155 + flags.new
156 + ? // In the new reconciler, both updates are batched
157 + ['B [render]', 'B [commit]']
158 + : // In the old reconciler, they are flushed separately. That's not
159 + // ideal, but for the purposes of this test it's fine since they
160 + // didn't happen in the same event,
161 + ['A [render]', 'A [commit]', 'B [render]', 'B [commit]'],
162 + ),
163 + );
164 expect(ReactNoop.getChildren()).toEqual([span('B')]);
165
166 // Now do the same thing again, except this time don't flush any work in
167 // between the two updates.
165 - ReactNoop.render(<Text text="A" />);
168 + ReactNoop.render(<TextClass text="A" />);
169 Scheduler.unstable_advanceTime(2000);
170 expect(Scheduler).toHaveYielded([]);
171 expect(ReactNoop.getChildren()).toEqual([span('B')]);
@@ -172,7 +175,7 @@ describe('ReactExpiration', () => {
175 interrupt();
176
177 // Schedule another update.
175 - ReactNoop.render(<Text text="B" />);
178 + ReactNoop.render(<TextClass text="B" />);
179 // The updates should flush in the same batch, since as far as the scheduler
180 // knows, they may have occurred inside the same event.
181 expect(Scheduler).toFlushAndYield(['B [render]', 'B [commit]']);
@@ -244,6 +247,60 @@ describe('ReactExpiration', () => {
247 ]);
248 });
249
250 + it('stops yielding if CPU-bound update takes too long to finish', () => {
251 + const root = ReactNoop.createRoot();
252 + function App() {
253 + return (
254 + <>
255 + <Text text="A" />
256 + <Text text="B" />
257 + <Text text="C" />
258 + <Text text="D" />
259 + <Text text="E" />
260 + </>
261 + );
262 + }
263 +
264 + root.render(<App />);
265 +
266 + expect(Scheduler).toFlushAndYieldThrough(['A']);
267 + expect(Scheduler).toFlushAndYieldThrough(['B']);
268 + expect(Scheduler).toFlushAndYieldThrough(['C']);
269 +
270 + Scheduler.unstable_advanceTime(10000);
271 +
272 + expect(Scheduler).toFlushExpired(['D', 'E']);
273 + expect(root).toMatchRenderedOutput('ABCDE');
274 + });
275 +
276 + it('root expiration is measured from the time of the first update', () => {
277 + Scheduler.unstable_advanceTime(10000);
278 +
279 + const root = ReactNoop.createRoot();
280 + function App() {
281 + return (
282 + <>
283 + <Text text="A" />
284 + <Text text="B" />
285 + <Text text="C" />
286 + <Text text="D" />
287 + <Text text="E" />
288 + </>
289 + );
290 + }
291 +
292 + root.render(<App />);
293 +
294 + expect(Scheduler).toFlushAndYieldThrough(['A']);
295 + expect(Scheduler).toFlushAndYieldThrough(['B']);
296 + expect(Scheduler).toFlushAndYieldThrough(['C']);
297 +
298 + Scheduler.unstable_advanceTime(10000);
299 +
300 + expect(Scheduler).toFlushExpired(['D', 'E']);
301 + expect(root).toMatchRenderedOutput('ABCDE');
302 + });
303 +
304 it('should measure expiration times relative to module initialization', () => {
305 // Tests an implementation detail where expiration times are computed using
306 // bitwise operations.
packages/react-reconciler/src/__tests__/ReactIncrementalErrorHandling-test.internal.js
+6 -4
@@ -239,8 +239,9 @@ describe('ReactIncrementalErrorHandling', () => {
239 });
240 }
241
242 - ReactNoop.render(<App isBroken={true} />, onCommit);
243 - Scheduler.unstable_advanceTime(1000);
242 + ReactNoop.discreteUpdates(() => {
243 + ReactNoop.render(<App isBroken={true} />, onCommit);
244 + });
245 expect(Scheduler).toFlushAndYieldThrough(['error']);
246 interrupt();
247
@@ -296,8 +297,9 @@ describe('ReactIncrementalErrorHandling', () => {
297 });
298 }
299
299 - ReactNoop.render(<App isBroken={true} />, onCommit);
300 - Scheduler.unstable_advanceTime(1000);
300 + ReactNoop.discreteUpdates(() => {
301 + ReactNoop.render(<App isBroken={true} />, onCommit);
302 + });
303 expect(Scheduler).toFlushAndYieldThrough(['error']);
304 interrupt();
305
packages/react-reconciler/src/__tests__/ReactIncrementalUpdates-test.js
+9
@@ -442,6 +442,9 @@ describe('ReactIncrementalUpdates', () => {
442 expect(ReactNoop.getChildren()).toEqual([span('derived state')]);
443 });
444
445 + // Note: This test doesn't really make sense in the new model, but we might
446 + // want to port it once lanes are implemented.
447 + // @gate old
448 it('flushes all expired updates in a single batch', () => {
449 const {useEffect} = React;
450
@@ -531,6 +534,9 @@ describe('ReactIncrementalUpdates', () => {
534 });
535 });
536
537 + // Note: This test doesn't really make sense in the new model, but we might
538 + // want to port it once lanes are implemented.
539 + // @gate old
540 it('flushes all expired updates in a single batch across multiple roots', () => {
541 // Same as previous test, but with two roots.
542 const {useEffect} = React;
@@ -640,6 +646,9 @@ describe('ReactIncrementalUpdates', () => {
646 });
647 });
648
649 + // Note: This test doesn't really make sense in the new model, but we might
650 + // want to port it once lanes are implemented.
651 + // @gate old
652 it('does not throw out partially completed tree if it expires midway through', () => {
653 function Text({text}) {
654 Scheduler.unstable_yieldValue(text);
packages/react-reconciler/src/__tests__/ReactSuspense-test.internal.js
+6 -6
@@ -357,16 +357,16 @@ describe('ReactSuspense', () => {
357 // This update will suspend.
358 root.update(<App shouldSuspend={true} step={1} />);
359
360 - // Need to move into the next async bucket.
361 - Scheduler.unstable_advanceTime(1000);
362 - // Do a bit of work, then interrupt to trigger a restart.
360 + // Do a bit of work
361 expect(Scheduler).toFlushAndYieldThrough(['A1']);
364 - interrupt();
362
366 - // Schedule another update. This will have lower priority because of
367 - // the interrupt trick above.
363 + // Schedule another update. This will get bumped into a different batch
364 + // because we're already in the middle of rendering.
365 root.update(<App shouldSuspend={false} step={2} />);
366
367 + // Interrupt to trigger a restart.
368 + interrupt();
369 +
370 expect(Scheduler).toFlushAndYieldThrough([
371 // Should have restarted the first update, because of the interruption
372 'A1',
packages/react-reconciler/src/__tests__/ReactSuspenseWithNoopRenderer-test.js
+24 -11
@@ -554,6 +554,8 @@ describe('ReactSuspenseWithNoopRenderer', () => {
554 expect(ReactNoop.getChildren()).toEqual([span('(empty)')]);
555 });
556
557 + // Note: This test was written to test a heuristic used in the expiration
558 + // times model. Might not make sense in the new model.
559 it('tries each subsequent level after suspending', async () => {
560 const root = ReactNoop.createRoot();
561
@@ -606,17 +608,28 @@ describe('ReactSuspenseWithNoopRenderer', () => {
608 root.render(<App step={4} shouldSuspend={false} />);
609 });
610
609 - // Should suspend at each distinct level
610 - expect(Scheduler).toHaveYielded([
611 - 'Sibling',
612 - 'Suspend! [Step 1]',
613 - 'Sibling',
614 - 'Suspend! [Step 2]',
615 - 'Sibling',
616 - 'Suspend! [Step 3]',
617 - 'Sibling',
618 - 'Step 4',
619 - ]);
611 + expect(Scheduler).toHaveYielded(
612 + gate(flags =>
613 + flags.new
614 + ? [
615 + // The new reconciler batches everything together, so it finishes
616 + // without suspending again.
617 + 'Sibling',
618 + 'Step 4',
619 + ]
620 + : [
621 + // The old reconciler tries at each distinct level.
622 + 'Sibling',
623 + 'Suspend! [Step 1]',
624 + 'Sibling',
625 + 'Suspend! [Step 2]',
626 + 'Sibling',
627 + 'Suspend! [Step 3]',
628 + 'Sibling',
629 + 'Step 4',
630 + ],
631 + ),
632 + );
633 });
634
635 it('forces an expiration after an update times out', async () => {