@samitouri / QOS-React / commits / 4d5cb64aa2

Rewrite ReactFiberScheduler for better integration with Scheduler package (#15151)

* Rewrite ReactFiberScheduler Adds a new implementation of ReactFiberScheduler behind a feature flag. We will maintain both implementations in parallel until the new one is proven stable enough to replace the old one. The main difference between the implementations is that the new one is integrated with the Scheduler package's priority levels. * Conditionally add fields to FiberRoot Some fields only used by the old scheduler, and some by the new. * Add separate build that enables new scheduler * Re-enable skipped test If synchronous updates are scheduled by a passive effect, that work should be flushed synchronously, even if flushPassiveEffects is called inside batchedUpdates. * Passive effects have same priority as render * Revert ability to cancel the current callback React doesn't need this anyway because it never schedules callbacks if it's already rendering. * Revert change to FiberDebugPerf Turns out this isn't neccessary. * Fix ReactFiberScheduler dead code elimination Should initialize to nothing, then assign the exports conditionally, instead of initializing to the old exports and then reassigning to the new ones. * Don't yield before commit during sync error retry * Call Scheduler.flushAll unconditionally in tests Instead of wrapping in enableNewScheduler flag.

Andrew Clark committed Apr 2, 2019 at 15:49 UTC 4d5cb64aa2beacf982cf0e01628ddda6bd92014c
36 files changed +4754 -1207
packages/react-cache/src/LRU.js
+5 -2
@@ -11,7 +11,10 @@ import * as Scheduler from 'scheduler';
11
12 // Intentionally not named imports because Rollup would
13 // use dynamic dispatch for CommonJS interop named imports.
14 -const {unstable_scheduleCallback: scheduleCallback} = Scheduler;
14 +const {
15 + unstable_scheduleCallback: scheduleCallback,
16 + unstable_IdlePriority: IdlePriority,
17 +} = Scheduler;
18
19 type Entry<T> = {|
20 value: T,
@@ -34,7 +37,7 @@ export function createLRU<T>(limit: number) {
37 // The cache size exceeds the limit. Schedule a callback to delete the
38 // least recently used entries.
39 cleanUpIsScheduled = true;
37 - scheduleCallback(cleanUp);
40 + scheduleCallback(IdlePriority, cleanUp);
41 }
42 }
43
packages/react-debug-tools/src/__tests__/ReactHooksInspectionIntegration-test.js
+4
@@ -12,6 +12,7 @@
12
13 let React;
14 let ReactTestRenderer;
15 +let Scheduler;
16 let ReactDebugTools;
17 let act;
18
@@ -20,6 +21,7 @@ describe('ReactHooksInspectionIntegration', () => {
21 jest.resetModules();
22 React = require('react');
23 ReactTestRenderer = require('react-test-renderer');
24 + Scheduler = require('scheduler');
25 act = ReactTestRenderer.act;
26 ReactDebugTools = require('react-debug-tools');
27 });
@@ -618,6 +620,8 @@ describe('ReactHooksInspectionIntegration', () => {
620
621 await LazyFoo;
622
623 + Scheduler.flushAll();
624 +
625 let childFiber = renderer.root._currentFiber();
626 let tree = ReactDebugTools.inspectHooksOfFiber(childFiber);
627 expect(tree).toEqual([
packages/react-dom/src/__tests__/ReactDOMHooks-test.internal.js renamed
+28 -9
@@ -9,6 +9,8 @@
9
10 'use strict';
11
12 +let ReactFeatureFlags;
13 +let enableNewScheduler;
14 let React;
15 let ReactDOM;
16 let Scheduler;
@@ -19,6 +21,8 @@ describe('ReactDOMHooks', () => {
21 beforeEach(() => {
22 jest.resetModules();
23
24 + ReactFeatureFlags = require('shared/ReactFeatureFlags');
25 + enableNewScheduler = ReactFeatureFlags.enableNewScheduler;
26 React = require('react');
27 ReactDOM = require('react-dom');
28 Scheduler = require('scheduler');
@@ -97,15 +101,30 @@ describe('ReactDOMHooks', () => {
101 }
102
103 ReactDOM.render(<Foo />, container);
100 - ReactDOM.unstable_batchedUpdates(() => {
101 - _set(0); // Forces the effect to be flushed
102 - expect(otherContainer.textContent).toBe('');
103 - ReactDOM.render(<B />, otherContainer);
104 - expect(otherContainer.textContent).toBe('');
105 - });
106 - expect(otherContainer.textContent).toBe('B');
107 - expect(calledA).toBe(false); // It was in a batch
108 - expect(calledB).toBe(true);
104 +
105 + if (enableNewScheduler) {
106 + // The old behavior was accidental; in the new scheduler, flushing passive
107 + // effects also flushes synchronous work, even inside batchedUpdates.
108 + ReactDOM.unstable_batchedUpdates(() => {
109 + _set(0); // Forces the effect to be flushed
110 + expect(otherContainer.textContent).toBe('A');
111 + ReactDOM.render(<B />, otherContainer);
112 + expect(otherContainer.textContent).toBe('A');
113 + });
114 + expect(otherContainer.textContent).toBe('B');
115 + expect(calledA).toBe(true);
116 + expect(calledB).toBe(true);
117 + } else {
118 + ReactDOM.unstable_batchedUpdates(() => {
119 + _set(0); // Forces the effect to be flushed
120 + expect(otherContainer.textContent).toBe('');
121 + ReactDOM.render(<B />, otherContainer);
122 + expect(otherContainer.textContent).toBe('');
123 + });
124 + expect(otherContainer.textContent).toBe('B');
125 + expect(calledA).toBe(false); // It was in a batch
126 + expect(calledB).toBe(true);
127 + }
128 });
129
130 it('should not bail out when an update is scheduled from within an event handler', () => {
packages/react-dom/src/__tests__/ReactDOMSuspensePlaceholder-test.js
+8
@@ -14,6 +14,7 @@ let ReactDOM;
14 let Suspense;
15 let ReactCache;
16 let ReactTestUtils;
17 +let Scheduler;
18 let TextResource;
19 let act;
20
@@ -26,6 +27,7 @@ describe('ReactDOMSuspensePlaceholder', () => {
27 ReactDOM = require('react-dom');
28 ReactCache = require('react-cache');
29 ReactTestUtils = require('react-dom/test-utils');
30 + Scheduler = require('scheduler');
31 act = ReactTestUtils.act;
32 Suspense = React.Suspense;
33 container = document.createElement('div');
@@ -94,6 +96,8 @@ describe('ReactDOMSuspensePlaceholder', () => {
96
97 await advanceTimers(500);
98
99 + Scheduler.flushAll();
100 +
101 expect(divs[0].current.style.display).toEqual('');
102 expect(divs[1].current.style.display).toEqual('');
103 // This div's display was set with a prop.
@@ -115,6 +119,8 @@ describe('ReactDOMSuspensePlaceholder', () => {
119
120 await advanceTimers(500);
121
122 + Scheduler.flushAll();
123 +
124 expect(container.textContent).toEqual('ABC');
125 });
126
@@ -160,6 +166,8 @@ describe('ReactDOMSuspensePlaceholder', () => {
166
167 await advanceTimers(500);
168
169 + Scheduler.flushAll();
170 +
171 expect(container.innerHTML).toEqual(
172 '<span style="display: inline;">Sibling</span><span style="">Async</span>',
173 );
packages/react-dom/src/__tests__/ReactServerRenderingHydration-test.js
+3
@@ -12,6 +12,7 @@
12 let React;
13 let ReactDOM;
14 let ReactDOMServer;
15 +let Scheduler;
16
17 // These tests rely both on ReactDOMServer and ReactDOM.
18 // If a test only needs ReactDOMServer, put it in ReactServerRendering-test instead.
@@ -21,6 +22,7 @@ describe('ReactDOMServerHydration', () => {
22 React = require('react');
23 ReactDOM = require('react-dom');
24 ReactDOMServer = require('react-dom/server');
25 + Scheduler = require('scheduler');
26 });
27
28 it('should have the correct mounting behavior (old hydrate API)', () => {
@@ -498,6 +500,7 @@ describe('ReactDOMServerHydration', () => {
500
501 jest.runAllTimers();
502 await Promise.resolve();
503 + Scheduler.flushAll();
504 expect(element.textContent).toBe('Hello world');
505 });
506 });
packages/react-dom/unstable-new-scheduler.js new
+16
@@ -0,0 +1,16 @@
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 +'use strict';
11 +
12 +const ReactDOM = require('./src/client/ReactDOM');
13 +
14 +// TODO: decide on the top-level export form.
15 +// This is hacky but makes it work with both Rollup and Jest.
16 +module.exports = ReactDOM.default || ReactDOM;
packages/react-reconciler/src/ReactFiberExpirationTime.js
+39
@@ -7,8 +7,17 @@
7 * @flow
8 */
9
10 +import type {ReactPriorityLevel} from './SchedulerWithReactIntegration';
11 +
12 import MAX_SIGNED_31_BIT_INT from './maxSigned31BitInt';
13
14 +import {
15 + ImmediatePriority,
16 + UserBlockingPriority,
17 + NormalPriority,
18 + IdlePriority,
19 +} from './SchedulerWithReactIntegration';
20 +
21 export type ExpirationTime = number;
22
23 export const NoWork = 0;
@@ -46,6 +55,8 @@ function computeExpirationBucket(
55 );
56 }
57
58 +// TODO: This corresponds to Scheduler's NormalPriority, not LowPriority. Update
59 +// the names to reflect.
60 export const LOW_PRIORITY_EXPIRATION = 5000;
61 export const LOW_PRIORITY_BATCH_SIZE = 250;
62
@@ -80,3 +91,31 @@ export function computeInteractiveExpiration(currentTime: ExpirationTime) {
91 HIGH_PRIORITY_BATCH_SIZE,
92 );
93 }
94 +
95 +export function inferPriorityFromExpirationTime(
96 + currentTime: ExpirationTime,
97 + expirationTime: ExpirationTime,
98 +): ReactPriorityLevel {
99 + if (expirationTime === Sync) {
100 + return ImmediatePriority;
101 + }
102 + if (expirationTime === Never) {
103 + return IdlePriority;
104 + }
105 + const msUntil =
106 + msToExpirationTime(expirationTime) - msToExpirationTime(currentTime);
107 + if (msUntil <= 0) {
108 + return ImmediatePriority;
109 + }
110 + if (msUntil <= HIGH_PRIORITY_EXPIRATION) {
111 + return UserBlockingPriority;
112 + }
113 + if (msUntil <= LOW_PRIORITY_EXPIRATION) {
114 + return NormalPriority;
115 + }
116 +
117 + // TODO: Handle LowPriority
118 +
119 + // Assume anything lower has idle priority
120 + return IdlePriority;
121 +}
packages/react-reconciler/src/ReactFiberReconciler.js
-2
@@ -43,7 +43,6 @@ import {
43 requestCurrentTime,
44 computeExpirationForFiber,
45 scheduleWork,
46 - requestWork,
46 flushRoot,
47 batchedUpdates,
48 unbatchedUpdates,
@@ -300,7 +299,6 @@ export function updateContainer(
299
300 export {
301 flushRoot,
303 - requestWork,
302 computeUniqueAsyncExpiration,
303 batchedUpdates,
304 unbatchedUpdates,
packages/react-reconciler/src/ReactFiberRoot.js
+53 -68
@@ -16,7 +16,10 @@ import type {Interaction} from 'scheduler/src/Tracing';
16 import {noTimeout} from './ReactFiberHostConfig';
17 import {createHostRootFiber} from './ReactFiber';
18 import {NoWork} from './ReactFiberExpirationTime';
19 -import {enableSchedulerTracing} from 'shared/ReactFeatureFlags';
19 +import {
20 + enableSchedulerTracing,
21 + enableNewScheduler,
22 +} from 'shared/ReactFeatureFlags';
23 import {unstable_getThreadID} from 'scheduler/tracing';
24
25 // TODO: This should be lifted into the renderer.
@@ -83,6 +86,13 @@ type BaseFiberRootProperties = {|
86 firstBatch: Batch | null,
87 // Linked-list of roots
88 nextScheduledRoot: FiberRoot | null,
89 +
90 + // New Scheduler fields
91 + callbackNode: *,
92 + callbackExpirationTime: ExpirationTime,
93 + firstPendingTime: ExpirationTime,
94 + lastPendingTime: ExpirationTime,
95 + pingTime: ExpirationTime,
96 |};
97
98 // The following attributes are only used by interaction tracing builds.
@@ -105,81 +115,56 @@ export type FiberRoot = {
115 ...ProfilingOnlyFiberRootProperties,
116 };
117
118 +function FiberRootNode(containerInfo, hydrate) {
119 + this.current = null;
120 + this.containerInfo = containerInfo;
121 + this.pendingChildren = null;
122 + this.pingCache = null;
123 + this.pendingCommitExpirationTime = NoWork;
124 + this.finishedWork = null;
125 + this.timeoutHandle = noTimeout;
126 + this.context = null;
127 + this.pendingContext = null;
128 + this.hydrate = hydrate;
129 + this.firstBatch = null;
130 +
131 + if (enableNewScheduler) {
132 + this.callbackNode = null;
133 + this.callbackExpirationTime = NoWork;
134 + this.firstPendingTime = NoWork;
135 + this.lastPendingTime = NoWork;
136 + this.pingTime = NoWork;
137 + } else {
138 + this.earliestPendingTime = NoWork;
139 + this.latestPendingTime = NoWork;
140 + this.earliestSuspendedTime = NoWork;
141 + this.latestSuspendedTime = NoWork;
142 + this.latestPingedTime = NoWork;
143 + this.didError = false;
144 + this.nextExpirationTimeToWorkOn = NoWork;
145 + this.expirationTime = NoWork;
146 + this.nextScheduledRoot = null;
147 + }
148 +
149 + if (enableSchedulerTracing) {
150 + this.interactionThreadID = unstable_getThreadID();
151 + this.memoizedInteractions = new Set();
152 + this.pendingInteractionMap = new Map();
153 + }
154 +}
155 +
156 export function createFiberRoot(
157 containerInfo: any,
158 isConcurrent: boolean,
159 hydrate: boolean,
160 ): FiberRoot {
161 + const root: FiberRoot = (new FiberRootNode(containerInfo, hydrate): any);
162 +
163 // Cyclic construction. This cheats the type system right now because
164 // stateNode is any.
165 const uninitializedFiber = createHostRootFiber(isConcurrent);
116 -
117 - let root;
118 - if (enableSchedulerTracing) {
119 - root = ({
120 - current: uninitializedFiber,
121 - containerInfo: containerInfo,
122 - pendingChildren: null,
123 -
124 - earliestPendingTime: NoWork,
125 - latestPendingTime: NoWork,
126 - earliestSuspendedTime: NoWork,
127 - latestSuspendedTime: NoWork,
128 - latestPingedTime: NoWork,
129 -
130 - pingCache: null,
131 -
132 - didError: false,
133 -
134 - pendingCommitExpirationTime: NoWork,
135 - finishedWork: null,
136 - timeoutHandle: noTimeout,
137 - context: null,
138 - pendingContext: null,
139 - hydrate,
140 - nextExpirationTimeToWorkOn: NoWork,
141 - expirationTime: NoWork,
142 - firstBatch: null,
143 - nextScheduledRoot: null,
144 -
145 - interactionThreadID: unstable_getThreadID(),
146 - memoizedInteractions: new Set(),
147 - pendingInteractionMap: new Map(),
148 - }: FiberRoot);
149 - } else {
150 - root = ({
151 - current: uninitializedFiber,
152 - containerInfo: containerInfo,
153 - pendingChildren: null,
154 -
155 - pingCache: null,
156 -
157 - earliestPendingTime: NoWork,
158 - latestPendingTime: NoWork,
159 - earliestSuspendedTime: NoWork,
160 - latestSuspendedTime: NoWork,
161 - latestPingedTime: NoWork,
162 -
163 - didError: false,
164 -
165 - pendingCommitExpirationTime: NoWork,
166 - finishedWork: null,
167 - timeoutHandle: noTimeout,
168 - context: null,
169 - pendingContext: null,
170 - hydrate,
171 - nextExpirationTimeToWorkOn: NoWork,
172 - expirationTime: NoWork,
173 - firstBatch: null,
174 - nextScheduledRoot: null,
175 - }: BaseFiberRootProperties);
176 - }
177 -
166 + root.current = uninitializedFiber;
167 uninitializedFiber.stateNode = root;
168
180 - // The reason for the way the Flow types are structured in this file,
181 - // Is to avoid needing :any casts everywhere interaction tracing fields are used.
182 - // Unfortunately that requires an :any cast for non-interaction tracing capable builds.
183 - // $FlowFixMe Remove this :any cast and replace it with something better.
184 - return ((root: any): FiberRoot);
169 + return root;
170 }
packages/react-reconciler/src/ReactFiberScheduler.js
+73 -55
@@ -22,7 +22,6 @@ import {
22 markLegacyErrorBoundaryAsFailed as markLegacyErrorBoundaryAsFailed_old,
23 isAlreadyFailedLegacyErrorBoundary as isAlreadyFailedLegacyErrorBoundary_old,
24 scheduleWork as scheduleWork_old,
25 - requestWork as requestWork_old,
25 flushRoot as flushRoot_old,
26 batchedUpdates as batchedUpdates_old,
27 unbatchedUpdates as unbatchedUpdates_old,
@@ -35,6 +34,7 @@ import {
34 computeUniqueAsyncExpiration as computeUniqueAsyncExpiration_old,
35 flushPassiveEffects as flushPassiveEffects_old,
36 warnIfNotCurrentlyActingUpdatesInDev as warnIfNotCurrentlyActingUpdatesInDev_old,
37 + inferStartTimeFromExpirationTime as inferStartTimeFromExpirationTime_old,
38 } from './ReactFiberScheduler.old';
39
40 import {
@@ -50,7 +50,6 @@ import {
50 markLegacyErrorBoundaryAsFailed as markLegacyErrorBoundaryAsFailed_new,
51 isAlreadyFailedLegacyErrorBoundary as isAlreadyFailedLegacyErrorBoundary_new,
52 scheduleWork as scheduleWork_new,
53 - requestWork as requestWork_new,
53 flushRoot as flushRoot_new,
54 batchedUpdates as batchedUpdates_new,
55 unbatchedUpdates as unbatchedUpdates_new,
@@ -63,61 +62,80 @@ import {
62 computeUniqueAsyncExpiration as computeUniqueAsyncExpiration_new,
63 flushPassiveEffects as flushPassiveEffects_new,
64 warnIfNotCurrentlyActingUpdatesInDev as warnIfNotCurrentlyActingUpdatesInDev_new,
65 + inferStartTimeFromExpirationTime as inferStartTimeFromExpirationTime_new,
66 } from './ReactFiberScheduler.new';
67
68 -export let requestCurrentTime = requestCurrentTime_old;
69 -export let computeExpirationForFiber = computeExpirationForFiber_old;
70 -export let captureCommitPhaseError = captureCommitPhaseError_old;
71 -export let onUncaughtError = onUncaughtError_old;
72 -export let renderDidSuspend = renderDidSuspend_old;
73 -export let renderDidError = renderDidError_old;
74 -export let pingSuspendedRoot = pingSuspendedRoot_old;
75 -export let retryTimedOutBoundary = retryTimedOutBoundary_old;
76 -export let resolveRetryThenable = resolveRetryThenable_old;
77 -export let markLegacyErrorBoundaryAsFailed = markLegacyErrorBoundaryAsFailed_old;
78 -export let isAlreadyFailedLegacyErrorBoundary = isAlreadyFailedLegacyErrorBoundary_old;
79 -export let scheduleWork = scheduleWork_old;
80 -export let requestWork = requestWork_old;
81 -export let flushRoot = flushRoot_old;
82 -export let batchedUpdates = batchedUpdates_old;
83 -export let unbatchedUpdates = unbatchedUpdates_old;
84 -export let flushSync = flushSync_old;
85 -export let flushControlled = flushControlled_old;
86 -export let deferredUpdates = deferredUpdates_old;
87 -export let syncUpdates = syncUpdates_old;
88 -export let interactiveUpdates = interactiveUpdates_old;
89 -export let flushInteractiveUpdates = flushInteractiveUpdates_old;
90 -export let computeUniqueAsyncExpiration = computeUniqueAsyncExpiration_old;
91 -export let flushPassiveEffects = flushPassiveEffects_old;
92 -export let warnIfNotCurrentlyActingUpdatesInDev = warnIfNotCurrentlyActingUpdatesInDev_old;
93 -
94 -if (enableNewScheduler) {
95 - requestCurrentTime = requestCurrentTime_new;
96 - computeExpirationForFiber = computeExpirationForFiber_new;
97 - captureCommitPhaseError = captureCommitPhaseError_new;
98 - onUncaughtError = onUncaughtError_new;
99 - renderDidSuspend = renderDidSuspend_new;
100 - renderDidError = renderDidError_new;
101 - pingSuspendedRoot = pingSuspendedRoot_new;
102 - retryTimedOutBoundary = retryTimedOutBoundary_new;
103 - resolveRetryThenable = resolveRetryThenable_new;
104 - markLegacyErrorBoundaryAsFailed = markLegacyErrorBoundaryAsFailed_new;
105 - isAlreadyFailedLegacyErrorBoundary = isAlreadyFailedLegacyErrorBoundary_new;
106 - scheduleWork = scheduleWork_new;
107 - requestWork = requestWork_new;
108 - flushRoot = flushRoot_new;
109 - batchedUpdates = batchedUpdates_new;
110 - unbatchedUpdates = unbatchedUpdates_new;
111 - flushSync = flushSync_new;
112 - flushControlled = flushControlled_new;
113 - deferredUpdates = deferredUpdates_new;
114 - syncUpdates = syncUpdates_new;
115 - interactiveUpdates = interactiveUpdates_new;
116 - flushInteractiveUpdates = flushInteractiveUpdates_new;
117 - computeUniqueAsyncExpiration = computeUniqueAsyncExpiration_new;
118 - flushPassiveEffects = flushPassiveEffects_new;
119 - warnIfNotCurrentlyActingUpdatesInDev = warnIfNotCurrentlyActingUpdatesInDev_new;
120 -}
68 +export const requestCurrentTime = enableNewScheduler
69 + ? requestCurrentTime_new
70 + : requestCurrentTime_old;
71 +export const computeExpirationForFiber = enableNewScheduler
72 + ? computeExpirationForFiber_new
73 + : computeExpirationForFiber_old;
74 +export const captureCommitPhaseError = enableNewScheduler
75 + ? captureCommitPhaseError_new
76 + : captureCommitPhaseError_old;
77 +export const onUncaughtError = enableNewScheduler
78 + ? onUncaughtError_new
79 + : onUncaughtError_old;
80 +export const renderDidSuspend = enableNewScheduler
81 + ? renderDidSuspend_new
82 + : renderDidSuspend_old;
83 +export const renderDidError = enableNewScheduler
84 + ? renderDidError_new
85 + : renderDidError_old;
86 +export const pingSuspendedRoot = enableNewScheduler
87 + ? pingSuspendedRoot_new
88 + : pingSuspendedRoot_old;
89 +export const retryTimedOutBoundary = enableNewScheduler
90 + ? retryTimedOutBoundary_new
91 + : retryTimedOutBoundary_old;
92 +export const resolveRetryThenable = enableNewScheduler
93 + ? resolveRetryThenable_new
94 + : resolveRetryThenable_old;
95 +export const markLegacyErrorBoundaryAsFailed = enableNewScheduler
96 + ? markLegacyErrorBoundaryAsFailed_new
97 + : markLegacyErrorBoundaryAsFailed_old;
98 +export const isAlreadyFailedLegacyErrorBoundary = enableNewScheduler
99 + ? isAlreadyFailedLegacyErrorBoundary_new
100 + : isAlreadyFailedLegacyErrorBoundary_old;
101 +export const scheduleWork = enableNewScheduler
102 + ? scheduleWork_new
103 + : scheduleWork_old;
104 +export const flushRoot = enableNewScheduler ? flushRoot_new : flushRoot_old;
105 +export const batchedUpdates = enableNewScheduler
106 + ? batchedUpdates_new
107 + : batchedUpdates_old;
108 +export const unbatchedUpdates = enableNewScheduler
109 + ? unbatchedUpdates_new
110 + : unbatchedUpdates_old;
111 +export const flushSync = enableNewScheduler ? flushSync_new : flushSync_old;
112 +export const flushControlled = enableNewScheduler
113 + ? flushControlled_new
114 + : flushControlled_old;
115 +export const deferredUpdates = enableNewScheduler
116 + ? deferredUpdates_new
117 + : deferredUpdates_old;
118 +export const syncUpdates = enableNewScheduler
119 + ? syncUpdates_new
120 + : syncUpdates_old;
121 +export const interactiveUpdates = enableNewScheduler
122 + ? interactiveUpdates_new
123 + : interactiveUpdates_old;
124 +export const flushInteractiveUpdates = enableNewScheduler
125 + ? flushInteractiveUpdates_new
126 + : flushInteractiveUpdates_old;
127 +export const computeUniqueAsyncExpiration = enableNewScheduler
128 + ? computeUniqueAsyncExpiration_new
129 + : computeUniqueAsyncExpiration_old;
130 +export const flushPassiveEffects = enableNewScheduler
131 + ? flushPassiveEffects_new
132 + : flushPassiveEffects_old;
133 +export const warnIfNotCurrentlyActingUpdatesInDev = enableNewScheduler
134 + ? warnIfNotCurrentlyActingUpdatesInDev_new
135 + : warnIfNotCurrentlyActingUpdatesInDev_old;
136 +export const inferStartTimeFromExpirationTime = enableNewScheduler
137 + ? inferStartTimeFromExpirationTime_new
138 + : inferStartTimeFromExpirationTime_old;
139
140 export type Thenable = {
141 then(resolve: () => mixed, reject?: () => mixed): void | Thenable,
packages/react-reconciler/src/ReactFiberScheduler.new.js
+2189 -29
@@ -3,34 +3,2194 @@
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
8 -function notYetImplemented() {
9 - throw new Error('Not yet implemented.');
10 -}
11 -
12 -export const requestCurrentTime = notYetImplemented;
13 -export const computeExpirationForFiber = notYetImplemented;
14 -export const captureCommitPhaseError = notYetImplemented;
15 -export const onUncaughtError = notYetImplemented;
16 -export const renderDidSuspend = notYetImplemented;
17 -export const renderDidError = notYetImplemented;
18 -export const pingSuspendedRoot = notYetImplemented;
19 -export const retryTimedOutBoundary = notYetImplemented;
20 -export const resolveRetryThenable = notYetImplemented;
21 -export const markLegacyErrorBoundaryAsFailed = notYetImplemented;
22 -export const isAlreadyFailedLegacyErrorBoundary = notYetImplemented;
23 -export const scheduleWork = notYetImplemented;
24 -export const requestWork = notYetImplemented;
25 -export const flushRoot = notYetImplemented;
26 -export const batchedUpdates = notYetImplemented;
27 -export const unbatchedUpdates = notYetImplemented;
28 -export const flushSync = notYetImplemented;
29 -export const flushControlled = notYetImplemented;
30 -export const deferredUpdates = notYetImplemented;
31 -export const syncUpdates = notYetImplemented;
32 -export const interactiveUpdates = notYetImplemented;
33 -export const flushInteractiveUpdates = notYetImplemented;
34 -export const computeUniqueAsyncExpiration = notYetImplemented;
35 -export const flushPassiveEffects = notYetImplemented;
36 -export const warnIfNotCurrentlyActingUpdatesInDev = notYetImplemented;
10 +import type {Fiber} from './ReactFiber';
11 +import type {FiberRoot} from './ReactFiberRoot';
12 +import type {ExpirationTime} from './ReactFiberExpirationTime';
13 +import type {
14 + ReactPriorityLevel,
15 + SchedulerCallback,
16 +} from './SchedulerWithReactIntegration';
17 +import type {Interaction} from 'scheduler/src/Tracing';
18 +
19 +import {
20 + warnAboutDeprecatedLifecycles,
21 + enableUserTimingAPI,
22 + enableSuspenseServerRenderer,
23 + replayFailedUnitOfWorkWithInvokeGuardedCallback,
24 + enableProfilerTimer,
25 + disableYielding,
26 + enableSchedulerTracing,
27 +} from 'shared/ReactFeatureFlags';
28 +import ReactSharedInternals from 'shared/ReactSharedInternals';
29 +import invariant from 'shared/invariant';
30 +import warning from 'shared/warning';
31 +
32 +import {
33 + scheduleCallback,
34 + cancelCallback,
35 + getCurrentPriorityLevel,
36 + runWithPriority,
37 + shouldYield,
38 + now,
39 + ImmediatePriority,
40 + UserBlockingPriority,
41 + NormalPriority,
42 + LowPriority,
43 + IdlePriority,
44 + flushImmediateQueue,
45 +} from './SchedulerWithReactIntegration';
46 +
47 +import {__interactionsRef, __subscriberRef} from 'scheduler/tracing';
48 +
49 +import {
50 + prepareForCommit,
51 + resetAfterCommit,
52 + scheduleTimeout,
53 + cancelTimeout,
54 + noTimeout,
55 +} from './ReactFiberHostConfig';
56 +
57 +import {createWorkInProgress, assignFiberPropertiesInDEV} from './ReactFiber';
58 +import {NoContext, ConcurrentMode, ProfileMode} from './ReactTypeOfMode';
59 +import {
60 + HostRoot,
61 + ClassComponent,
62 + SuspenseComponent,
63 + DehydratedSuspenseComponent,
64 + FunctionComponent,
65 + ForwardRef,
66 + MemoComponent,
67 + SimpleMemoComponent,
68 +} from 'shared/ReactWorkTags';
69 +import {
70 + NoEffect,
71 + PerformedWork,
72 + Placement,
73 + Update,
74 + PlacementAndUpdate,
75 + Deletion,
76 + Ref,
77 + ContentReset,
78 + Snapshot,
79 + Callback,
80 + Passive,
81 + Incomplete,
82 + HostEffectMask,
83 +} from 'shared/ReactSideEffectTags';
84 +import {
85 + NoWork,
86 + Sync,
87 + Never,
88 + msToExpirationTime,
89 + expirationTimeToMs,
90 + computeInteractiveExpiration,
91 + computeAsyncExpiration,
92 + inferPriorityFromExpirationTime,
93 + LOW_PRIORITY_EXPIRATION,
94 +} from './ReactFiberExpirationTime';
95 +import {beginWork as originalBeginWork} from './ReactFiberBeginWork';
96 +import {completeWork} from './ReactFiberCompleteWork';
97 +import {
98 + throwException,
99 + unwindWork,
100 + unwindInterruptedWork,
101 + createRootErrorUpdate,
102 + createClassErrorUpdate,
103 +} from './ReactFiberUnwindWork';
104 +import {
105 + commitBeforeMutationLifeCycles as commitBeforeMutationEffectOnFiber,
106 + commitLifeCycles as commitLayoutEffectOnFiber,
107 + commitPassiveHookEffects,
108 + commitPlacement,
109 + commitWork,
110 + commitDeletion,
111 + commitDetachRef,
112 + commitAttachRef,
113 + commitResetTextContent,
114 +} from './ReactFiberCommitWork';
115 +import {enqueueUpdate} from './ReactUpdateQueue';
116 +// TODO: Ahaha Andrew is bad at spellling
117 +import {resetContextDependences as resetContextDependencies} from './ReactFiberNewContext';
118 +import {resetHooks, ContextOnlyDispatcher} from './ReactFiberHooks';
119 +import {createCapturedValue} from './ReactCapturedValue';
120 +
121 +import {
122 + recordCommitTime,
123 + startProfilerTimer,
124 + stopProfilerTimerIfRunningAndRecordDelta,
125 +} from './ReactProfilerTimer';
126 +
127 +// DEV stuff
128 +import warningWithoutStack from 'shared/warningWithoutStack';
129 +import getComponentName from 'shared/getComponentName';
130 +import ReactStrictModeWarnings from './ReactStrictModeWarnings';
131 +import {
132 + phase as ReactCurrentDebugFiberPhaseInDEV,
133 + resetCurrentFiber as resetCurrentDebugFiberInDEV,
134 + setCurrentFiber as setCurrentDebugFiberInDEV,
135 + getStackByFiberInDevAndProd,
136 +} from './ReactCurrentFiber';
137 +import {
138 + recordEffect,
139 + recordScheduleUpdate,
140 + startRequestCallbackTimer,
141 + stopRequestCallbackTimer,
142 + startWorkTimer,
143 + stopWorkTimer,
144 + stopFailedWorkTimer,
145 + startWorkLoopTimer,
146 + stopWorkLoopTimer,
147 + startCommitTimer,
148 + stopCommitTimer,
149 + startCommitSnapshotEffectsTimer,
150 + stopCommitSnapshotEffectsTimer,
151 + startCommitHostEffectsTimer,
152 + stopCommitHostEffectsTimer,
153 + startCommitLifeCyclesTimer,
154 + stopCommitLifeCyclesTimer,
155 +} from './ReactDebugFiberPerf';
156 +import {
157 + invokeGuardedCallback,
158 + hasCaughtError,
159 + clearCaughtError,
160 +} from 'shared/ReactErrorUtils';
161 +import {onCommitRoot} from './ReactFiberDevToolsHook';
162 +
163 +const {
164 + ReactCurrentDispatcher,
165 + ReactCurrentOwner,
166 + ReactShouldWarnActingUpdates,
167 +} = ReactSharedInternals;
168 +
169 +type WorkPhase = 0 | 1 | 2 | 3 | 4 | 5;
170 +const NotWorking = 0;
171 +const BatchedPhase = 1;
172 +const LegacyUnbatchedPhase = 2;
173 +const FlushSyncPhase = 3;
174 +const RenderPhase = 4;
175 +const CommitPhase = 5;
176 +
177 +type RootExitStatus = 0 | 1 | 2 | 3;
178 +const RootIncomplete = 0;
179 +const RootErrored = 1;
180 +const RootSuspended = 2;
181 +const RootCompleted = 3;
182 +
183 +export type Thenable = {
184 + then(resolve: () => mixed, reject?: () => mixed): Thenable | void,
185 +};
186 +
187 +// The phase of work we're currently in
188 +let workPhase: WorkPhase = NotWorking;
189 +// The root we're working on
190 +let workInProgressRoot: FiberRoot | null = null;
191 +// The fiber we're working on
192 +let workInProgress: Fiber | null = null;
193 +// The expiration time we're rendering
194 +let renderExpirationTime: ExpirationTime = NoWork;
195 +// Whether to root completed, errored, suspended, etc.
196 +let workInProgressRootExitStatus: RootExitStatus = RootIncomplete;
197 +let workInProgressRootAbsoluteTimeoutMs: number = -1;
198 +
199 +let nextEffect: Fiber | null = null;
200 +let hasUncaughtError = false;
201 +let firstUncaughtError = null;
202 +let legacyErrorBoundariesThatAlreadyFailed: Set<mixed> | null = null;
203 +
204 +let rootDoesHavePassiveEffects: boolean = false;
205 +let rootWithPendingPassiveEffects: FiberRoot | null = null;
206 +let pendingPassiveEffectsExpirationTime: ExpirationTime = NoWork;
207 +
208 +let rootsWithPendingDiscreteUpdates: Map<
209 + FiberRoot,
210 + ExpirationTime,
211 +> | null = null;
212 +
213 +// Use these to prevent an infinite loop of nested updates
214 +const NESTED_UPDATE_LIMIT = 50;
215 +let nestedUpdateCount: number = 0;
216 +let rootWithNestedUpdates: FiberRoot | null = null;
217 +
218 +const NESTED_PASSIVE_UPDATE_LIMIT = 50;
219 +let nestedPassiveUpdateCount: number = 0;
220 +
221 +let interruptedBy: Fiber | null = null;
222 +
223 +// Expiration times are computed by adding to the current time (the start
224 +// time). However, if two updates are scheduled within the same event, we
225 +// should treat their start times as simultaneous, even if the actual clock
226 +// time has advanced between the first and second call.
227 +
228 +// In other words, because expiration times determine how updates are batched,
229 +// we want all updates of like priority that occur within the same event to
230 +// receive the same expiration time. Otherwise we get tearing.
231 +let currentEventTime: ExpirationTime = NoWork;
232 +
233 +export function requestCurrentTime() {
234 + if (workPhase === RenderPhase || workPhase === CommitPhase) {
235 + // We're inside React, so it's fine to read the actual time.
236 + return msToExpirationTime(now());
237 + }
238 + // We're not inside React, so we may be in the middle of a browser event.
239 + if (currentEventTime !== NoWork) {
240 + // Use the same start time for all updates until we enter React again.
241 + return currentEventTime;
242 + }
243 + // This is the first update since React yielded. Compute a new start time.
244 + currentEventTime = msToExpirationTime(now());
245 + return currentEventTime;
246 +}
247 +
248 +export function computeExpirationForFiber(
249 + currentTime: ExpirationTime,
250 + fiber: Fiber,
251 +): ExpirationTime {
252 + if ((fiber.mode & ConcurrentMode) === NoContext) {
253 + return Sync;
254 + }
255 +
256 + if (workPhase === RenderPhase) {
257 + // Use whatever time we're already rendering
258 + return renderExpirationTime;
259 + }
260 +
261 + // Compute an expiration time based on the Scheduler priority.
262 + let expirationTime;
263 + const priorityLevel = getCurrentPriorityLevel();
264 + switch (priorityLevel) {
265 + case ImmediatePriority:
266 + expirationTime = Sync;
267 + break;
268 + case UserBlockingPriority:
269 + // TODO: Rename this to computeUserBlockingExpiration
270 + expirationTime = computeInteractiveExpiration(currentTime);
271 + break;
272 + case NormalPriority:
273 + case LowPriority: // TODO: Handle LowPriority
274 + // TODO: Rename this to... something better.
275 + expirationTime = computeAsyncExpiration(currentTime);
276 + break;
277 + case IdlePriority:
278 + expirationTime = Never;
279 + break;
280 + default:
281 + invariant(false, 'Expected a valid priority level');
282 + }
283 +
284 + // If we're in the middle of rendering a tree, do not update at the same
285 + // expiration time that is already rendering.
286 + if (workInProgressRoot !== null && expirationTime === renderExpirationTime) {
287 + // This is a trick to move this update into a separate batch
288 + expirationTime -= 1;
289 + }
290 +
291 + return expirationTime;
292 +}
293 +
294 +let lastUniqueAsyncExpiration = NoWork;
295 +export function computeUniqueAsyncExpiration(): ExpirationTime {
296 + const currentTime = requestCurrentTime();
297 + let result = computeAsyncExpiration(currentTime);
298 + if (result <= lastUniqueAsyncExpiration) {
299 + // Since we assume the current time monotonically increases, we only hit
300 + // this branch when computeUniqueAsyncExpiration is fired multiple times
301 + // within a 200ms window (or whatever the async bucket size is).
302 + result -= 1;
303 + }
304 + lastUniqueAsyncExpiration = result;
305 + return result;
306 +}
307 +
308 +export function scheduleUpdateOnFiber(
309 + fiber: Fiber,
310 + expirationTime: ExpirationTime,
311 +) {
312 + checkForNestedUpdates();
313 + warnAboutInvalidUpdatesOnClassComponentsInDEV(fiber);
314 +
315 + const root = markUpdateTimeFromFiberToRoot(fiber, expirationTime);
316 + if (root === null) {
317 + warnAboutUpdateOnUnmountedFiberInDEV(fiber);
318 + return;
319 + }
320 +
321 + root.pingTime = NoWork;
322 +
323 + checkForInterruption(fiber, expirationTime);
324 + recordScheduleUpdate();
325 +
326 + if (expirationTime === Sync) {
327 + if (workPhase === LegacyUnbatchedPhase) {
328 + // This is a legacy edge case. The initial mount of a ReactDOM.render-ed
329 + // root inside of batchedUpdates should be synchronous, but layout updates
330 + // should be deferred until the end of the batch.
331 + let callback = renderRoot(root, Sync, true);
332 + while (callback !== null) {
333 + callback = callback(true);
334 + }
335 + } else {
336 + scheduleCallbackForRoot(root, ImmediatePriority, Sync);
337 + if (workPhase === NotWorking) {
338 + // Flush the synchronous work now, wnless we're already working or inside
339 + // a batch. This is intentionally inside scheduleUpdateOnFiber instead of
340 + // scheduleCallbackForFiber to preserve the ability to schedule a callback
341 + // without immediately flushing it. We only do this for user-initated
342 + // updates, to preserve historical behavior of sync mode.
343 + flushImmediateQueue();
344 + }
345 + }
346 + } else {
347 + // TODO: computeExpirationForFiber also reads the priority. Pass the
348 + // priority as an argument to that function and this one.
349 + const priorityLevel = getCurrentPriorityLevel();
350 + if (priorityLevel === UserBlockingPriority) {
351 + // This is the result of a discrete event. Track the lowest priority
352 + // discrete update per root so we can flush them early, if needed.
353 + if (rootsWithPendingDiscreteUpdates === null) {
354 + rootsWithPendingDiscreteUpdates = new Map([[root, expirationTime]]);
355 + } else {
356 + const lastDiscreteTime = rootsWithPendingDiscreteUpdates.get(root);
357 + if (
358 + lastDiscreteTime === undefined ||
359 + lastDiscreteTime > expirationTime
360 + ) {
361 + rootsWithPendingDiscreteUpdates.set(root, expirationTime);
362 + }
363 + }
364 + }
365 + scheduleCallbackForRoot(root, priorityLevel, expirationTime);
366 + }
367 +}
368 +export const scheduleWork = scheduleUpdateOnFiber;
369 +
370 +// This is split into a separate function so we can mark a fiber with pending
371 +// work without treating it as a typical update that originates from an event;
372 +// e.g. retrying a Suspense boundary isn't an update, but it does schedule work
373 +// on a fiber.
374 +function markUpdateTimeFromFiberToRoot(fiber, expirationTime) {
375 + // Update the source fiber's expiration time
376 + if (fiber.expirationTime < expirationTime) {
377 + fiber.expirationTime = expirationTime;
378 + }
379 + let alternate = fiber.alternate;
380 + if (alternate !== null && alternate.expirationTime < expirationTime) {
381 + alternate.expirationTime = expirationTime;
382 + }
383 + // Walk the parent path to the root and update the child expiration time.
384 + let node = fiber.return;
385 + let root = null;
386 + if (node === null && fiber.tag === HostRoot) {
387 + root = fiber.stateNode;
388 + } else {
389 + while (node !== null) {
390 + alternate = node.alternate;
391 + if (node.childExpirationTime < expirationTime) {
392 + node.childExpirationTime = expirationTime;
393 + if (
394 + alternate !== null &&
395 + alternate.childExpirationTime < expirationTime
396 + ) {
397 + alternate.childExpirationTime = expirationTime;
398 + }
399 + } else if (
400 + alternate !== null &&
401 + alternate.childExpirationTime < expirationTime
402 + ) {
403 + alternate.childExpirationTime = expirationTime;
404 + }
405 + if (node.return === null && node.tag === HostRoot) {
406 + root = node.stateNode;
407 + break;
408 + }
409 + node = node.return;
410 + }
411 + }
412 +
413 + if (root !== null) {
414 + // Update the first and last pending expiration times in this root
415 + const firstPendingTime = root.firstPendingTime;
416 + if (expirationTime > firstPendingTime) {
417 + root.firstPendingTime = expirationTime;
418 + }
419 + const lastPendingTime = root.lastPendingTime;
420 + if (lastPendingTime === NoWork || expirationTime < lastPendingTime) {
421 + root.lastPendingTime = expirationTime;
422 + }
423 + }
424 +
425 + return root;
426 +}
427 +
428 +// Use this function, along with runRootCallback, to ensure that only a single
429 +// callback per root is scheduled. It's still possible to call renderRoot
430 +// directly, but scheduling via this function helps avoid excessive callbacks.
431 +// It works by storing the callback node and expiration time on the root. When a
432 +// new callback comes in, it compares the expiration time to determine if it
433 +// should cancel the previous one. It also relies on commitRoot scheduling a
434 +// callback to render the next level, because that means we don't need a
435 +// separate callback per expiration time.
436 +function scheduleCallbackForRoot(
437 + root: FiberRoot,
438 + priorityLevel: ReactPriorityLevel,
439 + expirationTime: ExpirationTime,
440 +) {
441 + const existingCallbackExpirationTime = root.callbackExpirationTime;
442 + if (existingCallbackExpirationTime < expirationTime) {
443 + // New callback has higher priority than the existing one.
444 + const existingCallbackNode = root.callbackNode;
445 + if (existingCallbackNode !== null) {
446 + cancelCallback(existingCallbackNode);
447 + }
448 + root.callbackExpirationTime = expirationTime;
449 + const options =
450 + expirationTime === Sync
451 + ? null
452 + : {timeout: expirationTimeToMs(expirationTime)};
453 + root.callbackNode = scheduleCallback(
454 + priorityLevel,
455 + runRootCallback.bind(
456 + null,
457 + root,
458 + renderRoot.bind(null, root, expirationTime),
459 + ),
460 + options,
461 + );
462 + if (
463 + enableUserTimingAPI &&
464 + expirationTime !== Sync &&
465 + workPhase !== RenderPhase &&
466 + workPhase !== CommitPhase
467 + ) {
468 + // Scheduled an async callback, and we're not already working. Add an
469 + // entry to the flamegraph that shows we're waiting for a callback
470 + // to fire.
471 + startRequestCallbackTimer();
472 + }
473 + }
474 +
475 + const timeoutHandle = root.timeoutHandle;
476 + if (timeoutHandle !== noTimeout) {
477 + // The root previous suspended and scheduled a timeout to commit a fallback
478 + // state. Now that we have additional work, cancel the timeout.
479 + root.timeoutHandle = noTimeout;
480 + // $FlowFixMe Complains noTimeout is not a TimeoutID, despite the check above
481 + cancelTimeout(timeoutHandle);
482 + }
483 +
484 + // Add the current set of interactions to the pending set associated with
485 + // this root.
486 + schedulePendingInteraction(root, expirationTime);
487 +}
488 +
489 +function runRootCallback(root, callback, isSync) {
490 + const prevCallbackNode = root.callbackNode;
491 + let continuation = null;
492 + try {
493 + continuation = callback(isSync);
494 + if (continuation !== null) {
495 + return runRootCallback.bind(null, root, continuation);
496 + } else {
497 + return null;
498 + }
499 + } finally {
500 + // If the callback exits without returning a continuation, remove the
501 + // corresponding callback node from the root. Unless the callback node
502 + // has changed, which implies that it was already cancelled by a high
503 + // priority update.
504 + if (continuation === null && prevCallbackNode === root.callbackNode) {
505 + root.callbackNode = null;
506 + root.callbackExpirationTime = NoWork;
507 + }
508 + }
509 +}
510 +
511 +export function flushRoot(root: FiberRoot, expirationTime: ExpirationTime) {
512 + if (workPhase === RenderPhase || workPhase === CommitPhase) {
513 + invariant(
514 + false,
515 + 'work.commit(): Cannot commit while already rendering. This likely ' +
516 + 'means you attempted to commit from inside a lifecycle method.',
517 + );
518 + }
519 + scheduleCallback(
520 + ImmediatePriority,
521 + renderRoot.bind(null, root, expirationTime),
522 + );
523 + flushImmediateQueue();
524 +}
525 +
526 +export function flushInteractiveUpdates() {
527 + if (workPhase === RenderPhase || workPhase === CommitPhase) {
528 + // Can't synchronously flush interactive updates if React is already
529 + // working. This is currently a no-op.
530 + // TODO: Should we fire a warning? This happens if you synchronously invoke
531 + // an input event inside an effect, like with `element.click()`.
532 + return;
533 + }
534 + flushPendingDiscreteUpdates();
535 +}
536 +
537 +function resolveLocksOnRoot(root: FiberRoot, expirationTime: ExpirationTime) {
538 + const firstBatch = root.firstBatch;
539 + if (
540 + firstBatch !== null &&
541 + firstBatch._defer &&
542 + firstBatch._expirationTime >= expirationTime
543 + ) {
544 + root.finishedWork = root.current.alternate;
545 + root.pendingCommitExpirationTime = expirationTime;
546 + scheduleCallback(NormalPriority, () => {
547 + firstBatch._onComplete();
548 + return null;
549 + });
550 + return true;
551 + } else {
552 + return false;
553 + }
554 +}
555 +
556 +export function deferredUpdates<A>(fn: () => A): A {
557 + // TODO: Remove in favor of Scheduler.next
558 + return runWithPriority(NormalPriority, fn);
559 +}
560 +
561 +export function interactiveUpdates<A, B, C, R>(
562 + fn: (A, B, C) => R,
563 + a: A,
564 + b: B,
565 + c: C,
566 +): R {
567 + if (workPhase === NotWorking) {
568 + // TODO: Remove this call. Instead of doing this automatically, the caller
569 + // should explicitly call flushInteractiveUpdates.
570 + flushPendingDiscreteUpdates();
571 + }
572 + return runWithPriority(UserBlockingPriority, fn.bind(null, a, b, c));
573 +}
574 +
575 +export function syncUpdates<A, B, C, R>(
576 + fn: (A, B, C) => R,
577 + a: A,
578 + b: B,
579 + c: C,
580 +): R {
581 + return runWithPriority(ImmediatePriority, fn.bind(null, a, b, c));
582 +}
583 +
584 +function flushPendingDiscreteUpdates() {
585 + if (rootsWithPendingDiscreteUpdates !== null) {
586 + // For each root with pending discrete updates, schedule a callback to
587 + // immediately flush them.
588 + const roots = rootsWithPendingDiscreteUpdates;
589 + rootsWithPendingDiscreteUpdates = null;
590 + roots.forEach((expirationTime, root) => {
591 + scheduleCallback(
592 + ImmediatePriority,
593 + renderRoot.bind(null, root, expirationTime),
594 + );
595 + });
596 + // Now flush the immediate queue.
597 + flushImmediateQueue();
598 + }
599 +}
600 +
601 +export function batchedUpdates<A, R>(fn: A => R, a: A): R {
602 + if (workPhase !== NotWorking) {
603 + // We're already working, or inside a batch, so batchedUpdates is a no-op.
604 + return fn(a);
605 + }
606 + workPhase = BatchedPhase;
607 + try {
608 + return fn(a);
609 + } finally {
610 + workPhase = NotWorking;
611 + // Flush the immediate callbacks that were scheduled during this batch
612 + flushImmediateQueue();
613 + }
614 +}
615 +
616 +export function unbatchedUpdates<A, R>(fn: (a: A) => R, a: A): R {
617 + if (workPhase !== BatchedPhase && workPhase !== FlushSyncPhase) {
618 + // We're not inside batchedUpdates or flushSync, so unbatchedUpdates is
619 + // a no-op.
620 + return fn(a);
621 + }
622 + const prevWorkPhase = workPhase;
623 + workPhase = LegacyUnbatchedPhase;
624 + try {
625 + return fn(a);
626 + } finally {
627 + workPhase = prevWorkPhase;
628 + }
629 +}
630 +
631 +export function flushSync<A, R>(fn: A => R, a: A): R {
632 + if (workPhase === RenderPhase || workPhase === CommitPhase) {
633 + invariant(
634 + false,
635 + 'flushSync was called from inside a lifecycle method. It cannot be ' +
636 + 'called when React is already rendering.',
637 + );
638 + }
639 + const prevWorkPhase = workPhase;
640 + workPhase = FlushSyncPhase;
641 + try {
642 + return runWithPriority(ImmediatePriority, fn.bind(null, a));
643 + } finally {
644 + workPhase = prevWorkPhase;
645 + // Flush the immediate callbacks that were scheduled during this batch.
646 + // Note that this will happen even if batchedUpdates is higher up
647 + // the stack.
648 + flushImmediateQueue();
649 + }
650 +}
651 +
652 +export function flushControlled(fn: () => mixed): void {
653 + const prevWorkPhase = workPhase;
654 + workPhase = BatchedPhase;
655 + try {
656 + runWithPriority(ImmediatePriority, fn);
657 + } finally {
658 + workPhase = prevWorkPhase;
659 + if (workPhase === NotWorking) {
660 + // Flush the immediate callbacks that were scheduled during this batch
661 + flushImmediateQueue();
662 + }
663 + }
664 +}
665 +
666 +function prepareFreshStack(root, expirationTime) {
667 + root.pendingCommitExpirationTime = NoWork;
668 +
669 + if (workInProgress !== null) {
670 + let interruptedWork = workInProgress.return;
671 + while (interruptedWork !== null) {
672 + unwindInterruptedWork(interruptedWork);
673 + interruptedWork = interruptedWork.return;
674 + }
675 + }
676 + workInProgressRoot = root;
677 + workInProgress = createWorkInProgress(root.current, null, expirationTime);
678 + renderExpirationTime = expirationTime;
679 + workInProgressRootExitStatus = RootIncomplete;
680 + workInProgressRootAbsoluteTimeoutMs = -1;
681 +
682 + if (__DEV__) {
683 + ReactStrictModeWarnings.discardPendingWarnings();
684 + }
685 +}
686 +
687 +function renderRoot(
688 + root: FiberRoot,
689 + expirationTime: ExpirationTime,
690 + isSync: boolean,
691 +): SchedulerCallback | null {
692 + invariant(
693 + workPhase !== RenderPhase && workPhase !== CommitPhase,
694 + 'Should not already be working.',
695 + );
696 +
697 + if (enableUserTimingAPI && expirationTime !== Sync) {
698 + const didExpire = isSync;
699 + const timeoutMs = expirationTimeToMs(expirationTime);
700 + stopRequestCallbackTimer(didExpire, timeoutMs);
701 + }
702 +
703 + if (root.firstPendingTime < expirationTime) {
704 + // If there's no work left at this expiration time, exit immediately. This
705 + // happens when multiple callbacks are scheduled for a single root, but an
706 + // earlier callback flushes the work of a later one.
707 + return null;
708 + }
709 +
710 + if (root.pendingCommitExpirationTime === expirationTime) {
711 + // There's already a pending commit at this expiration time.
712 + root.pendingCommitExpirationTime = NoWork;
713 + return commitRoot.bind(null, root, expirationTime);
714 + }
715 +
716 + flushPassiveEffects();
717 +
718 + // If the root or expiration time have changed, throw out the existing stack
719 + // and prepare a fresh one. Otherwise we'll continue where we left off.
720 + if (root !== workInProgressRoot || expirationTime !== renderExpirationTime) {
721 + prepareFreshStack(root, expirationTime);
722 + startWorkOnPendingInteraction(root, expirationTime);
723 + }
724 +
725 + // If we have a work-in-progress fiber, it means there's still work to do
726 + // in this root.
727 + if (workInProgress !== null) {
728 + const prevWorkPhase = workPhase;
729 + workPhase = RenderPhase;
730 + let prevDispatcher = ReactCurrentDispatcher.current;
731 + if (prevDispatcher === null) {
732 + // The React isomorphic package does not include a default dispatcher.
733 + // Instead the first renderer will lazily attach one, in order to give
734 + // nicer error messages.
735 + prevDispatcher = ContextOnlyDispatcher;
736 + }
737 + ReactCurrentDispatcher.current = ContextOnlyDispatcher;
738 + let prevInteractions: Set<Interaction> | null = null;
739 + if (enableSchedulerTracing) {
740 + prevInteractions = __interactionsRef.current;
741 + __interactionsRef.current = root.memoizedInteractions;
742 + }
743 +
744 + startWorkLoopTimer(workInProgress);
745 + do {
746 + try {
747 + if (isSync) {
748 + if (expirationTime !== Sync) {
749 + // An async update expired. There may be other expired updates on
750 + // this root. We should render all the expired work in a
751 + // single batch.
752 + const currentTime = requestCurrentTime();
753 + if (currentTime < expirationTime) {
754 + // Restart at the current time.
755 + workPhase = prevWorkPhase;
756 + ReactCurrentDispatcher.current = prevDispatcher;
757 + return renderRoot.bind(null, root, currentTime);
758 + }
759 + }
760 + workLoopSync();
761 + } else {
762 + // Since we know we're in a React event, we can clear the current
763 + // event time. The next update will compute a new event time.
764 + currentEventTime = NoWork;
765 + workLoop();
766 + }
767 + break;
768 + } catch (thrownValue) {
769 + // Reset module-level state that was set during the render phase.
770 + resetContextDependencies();
771 + resetHooks();
772 +
773 + const sourceFiber = workInProgress;
774 + if (sourceFiber === null || sourceFiber.return === null) {
775 + // Expected to be working on a non-root fiber. This is a fatal error
776 + // because there's no ancestor that can handle it; the root is
777 + // supposed to capture all errors that weren't caught by an error
778 + // boundary.
779 + prepareFreshStack(root, expirationTime);
780 + workPhase = prevWorkPhase;
781 + throw thrownValue;
782 + }
783 +
784 + if (enableProfilerTimer && sourceFiber.mode & ProfileMode) {
785 + // Record the time spent rendering before an error was thrown. This
786 + // avoids inaccurate Profiler durations in the case of a
787 + // suspended render.
788 + stopProfilerTimerIfRunningAndRecordDelta(sourceFiber, true);
789 + }
790 +
791 + const returnFiber = sourceFiber.return;
792 + throwException(
793 + root,
794 + returnFiber,
795 + sourceFiber,
796 + thrownValue,
797 + renderExpirationTime,
798 + );
799 + workInProgress = completeUnitOfWork(sourceFiber);
800 + }
801 + } while (true);
802 +
803 + workPhase = prevWorkPhase;
804 + resetContextDependencies();
805 + ReactCurrentDispatcher.current = prevDispatcher;
806 + if (enableSchedulerTracing) {
807 + __interactionsRef.current = ((prevInteractions: any): Set<Interaction>);
808 + }
809 +
810 + if (workInProgress !== null) {
811 + // There's still work left over. Return a continuation.
812 + stopInterruptedWorkLoopTimer();
813 + if (expirationTime !== Sync) {
814 + startRequestCallbackTimer();
815 + }
816 + return renderRoot.bind(null, root, expirationTime);
817 + }
818 + }
819 +
820 + // We now have a consistent tree. The next step is either to commit it, or, if
821 + // something suspended, wait to commit it after a timeout.
822 + stopFinishedWorkLoopTimer();
823 +
824 + const isLocked = resolveLocksOnRoot(root, expirationTime);
825 + if (isLocked) {
826 + // This root has a lock that prevents it from committing. Exit. If we begin
827 + // work on the root again, without any intervening updates, it will finish
828 + // without doing additional work.
829 + return null;
830 + }
831 +
832 + // Set this to null to indicate there's no in-progress render.
833 + workInProgressRoot = null;
834 +
835 + switch (workInProgressRootExitStatus) {
836 + case RootIncomplete: {
837 + invariant(false, 'Should have a work-in-progress.');
838 + }
839 + // Flow knows about invariant, so it compains if I add a break statement,
840 + // but eslint doesn't know about invariant, so it complains if I do.
841 + // eslint-disable-next-line no-fallthrough
842 + case RootErrored: {
843 + // An error was thrown. First check if there is lower priority work
844 + // scheduled on this root.
845 + const lastPendingTime = root.lastPendingTime;
846 + if (root.lastPendingTime < expirationTime) {
847 + // There's lower priority work. Before raising the error, try rendering
848 + // at the lower priority to see if it fixes it. Use a continuation to
849 + // maintain the existing priority and position in the queue.
850 + return renderRoot.bind(null, root, lastPendingTime);
851 + }
852 + if (!isSync) {
853 + // If we're rendering asynchronously, it's possible the error was
854 + // caused by tearing due to a mutation during an event. Try rendering
855 + // one more time without yiedling to events.
856 + prepareFreshStack(root, expirationTime);
857 + scheduleCallback(
858 + ImmediatePriority,
859 + renderRoot.bind(null, root, expirationTime),
860 + );
861 + return null;
862 + }
863 + // If we're already rendering synchronously, commit the root in its
864 + // errored state.
865 + return commitRoot.bind(null, root, expirationTime);
866 + }
867 + case RootSuspended: {
868 + const lastPendingTime = root.lastPendingTime;
869 + if (root.lastPendingTime < expirationTime) {
870 + // There's lower priority work. It might be unsuspended. Try rendering
871 + // at that level.
872 + return renderRoot.bind(null, root, lastPendingTime);
873 + }
874 + if (!isSync) {
875 + const msUntilTimeout = computeMsUntilTimeout(
876 + root,
877 + workInProgressRootAbsoluteTimeoutMs,
878 + );
879 + if (msUntilTimeout > 0) {
880 + // The render is suspended, it hasn't timed out, and there's no lower
881 + // priority work to do. Instead of committing the fallback
882 + // immediately, wait for more data to arrive.
883 + root.timeoutHandle = scheduleTimeout(
884 + commitRoot.bind(null, root, expirationTime),
885 + msUntilTimeout,
886 + );
887 + return null;
888 + }
889 + }
890 + // The work expired. Commit immediately.
891 + return commitRoot.bind(null, root, expirationTime);
892 + }
893 + case RootCompleted: {
894 + // The work completed. Ready to commit.
895 + return commitRoot.bind(null, root, expirationTime);
896 + }
897 + default: {
898 + invariant(false, 'Unknown root exit status.');
899 + }
900 + }
901 +}
902 +
903 +export function renderDidSuspend(
904 + root: FiberRoot,
905 + absoluteTimeoutMs: number,
906 + // TODO: Don't need this argument anymore
907 + suspendedTime: ExpirationTime,
908 +) {
909 + if (
910 + absoluteTimeoutMs >= 0 &&
911 + workInProgressRootAbsoluteTimeoutMs < absoluteTimeoutMs
912 + ) {
913 + workInProgressRootAbsoluteTimeoutMs = absoluteTimeoutMs;
914 + if (workInProgressRootExitStatus === RootIncomplete) {
915 + workInProgressRootExitStatus = RootSuspended;
916 + }
917 + }
918 +}
919 +
920 +export function renderDidError() {
921 + if (
922 + workInProgressRootExitStatus === RootIncomplete ||
923 + workInProgressRootExitStatus === RootSuspended
924 + ) {
925 + workInProgressRootExitStatus = RootErrored;
926 + }
927 +}
928 +
929 +function workLoopSync() {
930 + // Already timed out, so perform work without checking if we need to yield.
931 + while (workInProgress !== null) {
932 + workInProgress = performUnitOfWork(workInProgress);
933 + }
934 +}
935 +
936 +function workLoop() {
937 + // Perform work until Scheduler asks us to yield
938 + while (workInProgress !== null && !shouldYield()) {
939 + workInProgress = performUnitOfWork(workInProgress);
940 + }
941 +}
942 +
943 +function performUnitOfWork(unitOfWork: Fiber): Fiber | null {
944 + // The current, flushed, state of this fiber is the alternate. Ideally
945 + // nothing should rely on this, but relying on it here means that we don't
946 + // need an additional field on the work in progress.
947 + const current = unitOfWork.alternate;
948 +
949 + startWorkTimer(unitOfWork);
950 + setCurrentDebugFiberInDEV(unitOfWork);
951 +
952 + let next;
953 + if (enableProfilerTimer && (unitOfWork.mode & ProfileMode) !== NoContext) {
954 + startProfilerTimer(unitOfWork);
955 + next = beginWork(current, unitOfWork, renderExpirationTime);
956 + stopProfilerTimerIfRunningAndRecordDelta(unitOfWork, true);
957 + } else {
958 + next = beginWork(current, unitOfWork, renderExpirationTime);
959 + }
960 +
961 + resetCurrentDebugFiberInDEV();
962 + unitOfWork.memoizedProps = unitOfWork.pendingProps;
963 + if (next === null) {
964 + // If this doesn't spawn new work, complete the current work.
965 + next = completeUnitOfWork(unitOfWork);
966 + }
967 +
968 + ReactCurrentOwner.current = null;
969 + return next;
970 +}
971 +
972 +function completeUnitOfWork(unitOfWork: Fiber): Fiber | null {
973 + // Attempt to complete the current unit of work, then move to the next
974 + // sibling. If there are no more siblings, return to the parent fiber.
975 + workInProgress = unitOfWork;
976 + do {
977 + // The current, flushed, state of this fiber is the alternate. Ideally
978 + // nothing should rely on this, but relying on it here means that we don't
979 + // need an additional field on the work in progress.
980 + const current = workInProgress.alternate;
981 + const returnFiber = workInProgress.return;
982 +
983 + // Check if the work completed or if something threw.
984 + if ((workInProgress.effectTag & Incomplete) === NoEffect) {
985 + setCurrentDebugFiberInDEV(workInProgress);
986 + let next;
987 + if (
988 + !enableProfilerTimer ||
989 + (workInProgress.mode & ProfileMode) === NoContext
990 + ) {
991 + next = completeWork(current, workInProgress, renderExpirationTime);
992 + } else {
993 + startProfilerTimer(workInProgress);
994 + next = completeWork(current, workInProgress, renderExpirationTime);
995 + // Update render duration assuming we didn't error.
996 + stopProfilerTimerIfRunningAndRecordDelta(workInProgress, false);
997 + }
998 + stopWorkTimer(workInProgress);
999 + resetCurrentDebugFiberInDEV();
1000 + resetChildExpirationTime(workInProgress);
1001 +
1002 + if (next !== null) {
1003 + // Completing this fiber spawned new work. Work on that next.
1004 + return next;
1005 + }
1006 +
1007 + if (
1008 + returnFiber !== null &&
1009 + // Do not append effects to parents if a sibling failed to complete
1010 + (returnFiber.effectTag & Incomplete) === NoEffect
1011 + ) {
1012 + // Append all the effects of the subtree and this fiber onto the effect
1013 + // list of the parent. The completion order of the children affects the
1014 + // side-effect order.
1015 + if (returnFiber.firstEffect === null) {
1016 + returnFiber.firstEffect = workInProgress.firstEffect;
1017 + }
1018 + if (workInProgress.lastEffect !== null) {
1019 + if (returnFiber.lastEffect !== null) {
1020 + returnFiber.lastEffect.nextEffect = workInProgress.firstEffect;
1021 + }
1022 + returnFiber.lastEffect = workInProgress.lastEffect;
1023 + }
1024 +
1025 + // If this fiber had side-effects, we append it AFTER the children's
1026 + // side-effects. We can perform certain side-effects earlier if needed,
1027 + // by doing multiple passes over the effect list. We don't want to
1028 + // schedule our own side-effect on our own list because if end up
1029 + // reusing children we'll schedule this effect onto itself since we're
1030 + // at the end.
1031 + const effectTag = workInProgress.effectTag;
1032 +
1033 + // Skip both NoWork and PerformedWork tags when creating the effect
1034 + // list. PerformedWork effect is read by React DevTools but shouldn't be
1035 + // committed.
1036 + if (effectTag > PerformedWork) {
1037 + if (returnFiber.lastEffect !== null) {
1038 + returnFiber.lastEffect.nextEffect = workInProgress;
1039 + } else {
1040 + returnFiber.firstEffect = workInProgress;
1041 + }
1042 + returnFiber.lastEffect = workInProgress;
1043 + }
1044 + }
1045 + } else {
1046 + // This fiber did not complete because something threw. Pop values off
1047 + // the stack without entering the complete phase. If this is a boundary,
1048 + // capture values if possible.
1049 + const next = unwindWork(workInProgress, renderExpirationTime);
1050 +
1051 + // Because this fiber did not complete, don't reset its expiration time.
1052 +
1053 + if (
1054 + enableProfilerTimer &&
1055 + (workInProgress.mode & ProfileMode) !== NoContext
1056 + ) {
1057 + // Record the render duration for the fiber that errored.
1058 + stopProfilerTimerIfRunningAndRecordDelta(workInProgress, false);
1059 +
1060 + // Include the time spent working on failed children before continuing.
1061 + let actualDuration = workInProgress.actualDuration;
1062 + let child = workInProgress.child;
1063 + while (child !== null) {
1064 + actualDuration += child.actualDuration;
1065 + child = child.sibling;
1066 + }
1067 + workInProgress.actualDuration = actualDuration;
1068 + }
1069 +
1070 + if (next !== null) {
1071 + // If completing this work spawned new work, do that next. We'll come
1072 + // back here again.
1073 + // Since we're restarting, remove anything that is not a host effect
1074 + // from the effect tag.
1075 + // TODO: The name stopFailedWorkTimer is misleading because Suspense
1076 + // also captures and restarts.
1077 + stopFailedWorkTimer(workInProgress);
1078 + next.effectTag &= HostEffectMask;
1079 + return next;
1080 + }
1081 + stopWorkTimer(workInProgress);
1082 +
1083 + if (returnFiber !== null) {
1084 + // Mark the parent fiber as incomplete and clear its effect list.
1085 + returnFiber.firstEffect = returnFiber.lastEffect = null;
1086 + returnFiber.effectTag |= Incomplete;
1087 + }
1088 + }
1089 +
1090 + const siblingFiber = workInProgress.sibling;
1091 + if (siblingFiber !== null) {
1092 + // If there is more work to do in this returnFiber, do that next.
1093 + return siblingFiber;
1094 + }
1095 + // Otherwise, return to the parent
1096 + workInProgress = returnFiber;
1097 + } while (workInProgress !== null);
1098 +
1099 + // We've reached the root.
1100 + if (workInProgressRootExitStatus === RootIncomplete) {
1101 + workInProgressRootExitStatus = RootCompleted;
1102 + }
1103 + return null;
1104 +}
1105 +
1106 +function resetChildExpirationTime(completedWork: Fiber) {
1107 + if (
1108 + renderExpirationTime !== Never &&
1109 + completedWork.childExpirationTime === Never
1110 + ) {
1111 + // The children of this component are hidden. Don't bubble their
1112 + // expiration times.
1113 + return;
1114 + }
1115 +
1116 + let newChildExpirationTime = NoWork;
1117 +
1118 + // Bubble up the earliest expiration time.
1119 + if (enableProfilerTimer && (completedWork.mode & ProfileMode) !== NoContext) {
1120 + // In profiling mode, resetChildExpirationTime is also used to reset
1121 + // profiler durations.
1122 + let actualDuration = completedWork.actualDuration;
1123 + let treeBaseDuration = completedWork.selfBaseDuration;
1124 +
1125 + // When a fiber is cloned, its actualDuration is reset to 0. This value will
1126 + // only be updated if work is done on the fiber (i.e. it doesn't bailout).
1127 + // When work is done, it should bubble to the parent's actualDuration. If
1128 + // the fiber has not been cloned though, (meaning no work was done), then
1129 + // this value will reflect the amount of time spent working on a previous
1130 + // render. In that case it should not bubble. We determine whether it was
1131 + // cloned by comparing the child pointer.
1132 + const shouldBubbleActualDurations =
1133 + completedWork.alternate === null ||
1134 + completedWork.child !== completedWork.alternate.child;
1135 +
1136 + let child = completedWork.child;
1137 + while (child !== null) {
1138 + const childUpdateExpirationTime = child.expirationTime;
1139 + const childChildExpirationTime = child.childExpirationTime;
1140 + if (childUpdateExpirationTime > newChildExpirationTime) {
1141 + newChildExpirationTime = childUpdateExpirationTime;
1142 + }
1143 + if (childChildExpirationTime > newChildExpirationTime) {
1144 + newChildExpirationTime = childChildExpirationTime;
1145 + }
1146 + if (shouldBubbleActualDurations) {
1147 + actualDuration += child.actualDuration;
1148 + }
1149 + treeBaseDuration += child.treeBaseDuration;
1150 + child = child.sibling;
1151 + }
1152 + completedWork.actualDuration = actualDuration;
1153 + completedWork.treeBaseDuration = treeBaseDuration;
1154 + } else {
1155 + let child = completedWork.child;
1156 + while (child !== null) {
1157 + const childUpdateExpirationTime = child.expirationTime;
1158 + const childChildExpirationTime = child.childExpirationTime;
1159 + if (childUpdateExpirationTime > newChildExpirationTime) {
1160 + newChildExpirationTime = childUpdateExpirationTime;
1161 + }
1162 + if (childChildExpirationTime > newChildExpirationTime) {
1163 + newChildExpirationTime = childChildExpirationTime;
1164 + }
1165 + child = child.sibling;
1166 + }
1167 + }
1168 +
1169 + completedWork.childExpirationTime = newChildExpirationTime;
1170 +}
1171 +
1172 +function commitRoot(root, expirationTime) {
1173 + runWithPriority(
1174 + ImmediatePriority,
1175 + commitRootImpl.bind(null, root, expirationTime),
1176 + );
1177 + // If there are passive effects, schedule a callback to flush them. This goes
1178 + // outside commitRootImpl so that it inherits the priority of the render.
1179 + if (rootWithPendingPassiveEffects !== null) {
1180 + const priorityLevel = getCurrentPriorityLevel();
1181 + scheduleCallback(priorityLevel, () => {
1182 + flushPassiveEffects();
1183 + return null;
1184 + });
1185 + }
1186 + return null;
1187 +}
1188 +
1189 +function commitRootImpl(root, expirationTime) {
1190 + flushPassiveEffects();
1191 + flushRenderPhaseStrictModeWarningsInDEV();
1192 +
1193 + invariant(
1194 + workPhase !== RenderPhase && workPhase !== CommitPhase,
1195 + 'Should not already be working.',
1196 + );
1197 + const finishedWork = root.current.alternate;
1198 + invariant(finishedWork !== null, 'Should have a work-in-progress root.');
1199 +
1200 + // commitRoot never returns a continuation; it always finishes synchronously.
1201 + // So we can clear these now to allow a new callback to be scheduled.
1202 + root.callbackNode = null;
1203 + root.callbackExpirationTime = NoWork;
1204 +
1205 + startCommitTimer();
1206 +
1207 + // Update the first and last pending times on this root. The new first
1208 + // pending time is whatever is left on the root fiber.
1209 + const updateExpirationTimeBeforeCommit = finishedWork.expirationTime;
1210 + const childExpirationTimeBeforeCommit = finishedWork.childExpirationTime;
1211 + const firstPendingTimeBeforeCommit =
1212 + childExpirationTimeBeforeCommit > updateExpirationTimeBeforeCommit
1213 + ? childExpirationTimeBeforeCommit
1214 + : updateExpirationTimeBeforeCommit;
1215 + root.firstPendingTime = firstPendingTimeBeforeCommit;
1216 + if (firstPendingTimeBeforeCommit < root.lastPendingTime) {
1217 + // This usually means we've finished all the work, but it can also happen
1218 + // when something gets downprioritized during render, like a hidden tree.
1219 + root.lastPendingTime = firstPendingTimeBeforeCommit;
1220 + }
1221 +
1222 + if (root === workInProgressRoot) {
1223 + // We can reset these now that they are finished.
1224 + workInProgressRoot = null;
1225 + workInProgress = null;
1226 + renderExpirationTime = NoWork;
1227 + } else {
1228 + // This indicates that the last root we worked on is not the same one that
1229 + // we're committing now. This most commonly happens when a suspended root
1230 + // times out.
1231 + }
1232 +
1233 + // Get the list of effects.
1234 + let firstEffect;
1235 + if (finishedWork.effectTag > PerformedWork) {
1236 + // A fiber's effect list consists only of its children, not itself. So if
1237 + // the root has an effect, we need to add it to the end of the list. The
1238 + // resulting list is the set that would belong to the root's parent, if it
1239 + // had one; that is, all the effects in the tree including the root.
1240 + if (finishedWork.lastEffect !== null) {
1241 + finishedWork.lastEffect.nextEffect = finishedWork;
1242 + firstEffect = finishedWork.firstEffect;
1243 + } else {
1244 + firstEffect = finishedWork;
1245 + }
1246 + } else {
1247 + // There is no effect on the root.
1248 + firstEffect = finishedWork.firstEffect;
1249 + }
1250 +
1251 + if (firstEffect !== null) {
1252 + const prevWorkPhase = workPhase;
1253 + workPhase = CommitPhase;
1254 + let prevInteractions: Set<Interaction> | null = null;
1255 + if (enableSchedulerTracing) {
1256 + prevInteractions = __interactionsRef.current;
1257 + __interactionsRef.current = root.memoizedInteractions;
1258 + }
1259 +
1260 + // Reset this to null before calling lifecycles
1261 + ReactCurrentOwner.current = null;
1262 +
1263 + // The commit phase is broken into several sub-phases. We do a separate pass
1264 + // of the effect list for each phase: all mutation effects come before all
1265 + // layout effects, and so on.
1266 +
1267 + // The first phase a "before mutation" phase. We use this phase to read the
1268 + // state of the host tree right before we mutate it. This is where
1269 + // getSnapshotBeforeUpdate is called.
1270 + startCommitSnapshotEffectsTimer();
1271 + prepareForCommit(root.containerInfo);
1272 + nextEffect = firstEffect;
1273 + do {
1274 + if (__DEV__) {
1275 + invokeGuardedCallback(null, commitBeforeMutationEffects, null);
1276 + if (hasCaughtError()) {
1277 + invariant(nextEffect !== null, 'Should be working on an effect.');
1278 + const error = clearCaughtError();
1279 + captureCommitPhaseError(nextEffect, error);
1280 + nextEffect = nextEffect.nextEffect;
1281 + }
1282 + } else {
1283 + try {
1284 + commitBeforeMutationEffects();
1285 + } catch (error) {
1286 + invariant(nextEffect !== null, 'Should be working on an effect.');
1287 + captureCommitPhaseError(nextEffect, error);
1288 + nextEffect = nextEffect.nextEffect;
1289 + }
1290 + }
1291 + } while (nextEffect !== null);
1292 + stopCommitSnapshotEffectsTimer();
1293 +
1294 + if (enableProfilerTimer) {
1295 + // Mark the current commit time to be shared by all Profilers in this
1296 + // batch. This enables them to be grouped later.
1297 + recordCommitTime();
1298 + }
1299 +
1300 + // The next phase is the mutation phase, where we mutate the host tree.
1301 + startCommitHostEffectsTimer();
1302 + nextEffect = firstEffect;
1303 + do {
1304 + if (__DEV__) {
1305 + invokeGuardedCallback(null, commitMutationEffects, null);
1306 + if (hasCaughtError()) {
1307 + invariant(nextEffect !== null, 'Should be working on an effect.');
1308 + const error = clearCaughtError();
1309 + captureCommitPhaseError(nextEffect, error);
1310 + nextEffect = nextEffect.nextEffect;
1311 + }
1312 + } else {
1313 + try {
1314 + commitMutationEffects();
1315 + } catch (error) {
1316 + invariant(nextEffect !== null, 'Should be working on an effect.');
1317 + captureCommitPhaseError(nextEffect, error);
1318 + nextEffect = nextEffect.nextEffect;
1319 + }
1320 + }
1321 + } while (nextEffect !== null);
1322 + stopCommitHostEffectsTimer();
1323 + resetAfterCommit(root.containerInfo);
1324 +
1325 + // The work-in-progress tree is now the current tree. This must come after
1326 + // the mutation phase, so that the previous tree is still current during
1327 + // componentWillUnmount, but before the layout phase, so that the finished
1328 + // work is current during componentDidMount/Update.
1329 + root.current = finishedWork;
1330 +
1331 + // The next phase is the layout phase, where we call effects that read
1332 + // the host tree after it's been mutated. The idiomatic use case for this is
1333 + // layout, but class component lifecycles also fire here for legacy reasons.
1334 + startCommitLifeCyclesTimer();
1335 + nextEffect = firstEffect;
1336 + do {
1337 + if (__DEV__) {
1338 + invokeGuardedCallback(
1339 + null,
1340 + commitLayoutEffects,
1341 + null,
1342 + root,
1343 + expirationTime,
1344 + );
1345 + if (hasCaughtError()) {
1346 + invariant(nextEffect !== null, 'Should be working on an effect.');
1347 + const error = clearCaughtError();
1348 + captureCommitPhaseError(nextEffect, error);
1349 + nextEffect = nextEffect.nextEffect;
1350 + }
1351 + } else {
1352 + try {
1353 + commitLayoutEffects(root, expirationTime);
1354 + } catch (error) {
1355 + invariant(nextEffect !== null, 'Should be working on an effect.');
1356 + captureCommitPhaseError(nextEffect, error);
1357 + nextEffect = nextEffect.nextEffect;
1358 + }
1359 + }
1360 + } while (nextEffect !== null);
1361 + stopCommitLifeCyclesTimer();
1362 +
1363 + nextEffect = null;
1364 +
1365 + if (enableSchedulerTracing) {
1366 + __interactionsRef.current = ((prevInteractions: any): Set<Interaction>);
1367 + }
1368 + workPhase = prevWorkPhase;
1369 + } else {
1370 + // No effects.
1371 + root.current = finishedWork;
1372 + // Measure these anyway so the flamegraph explicitly shows that there were
1373 + // no effects.
1374 + // TODO: Maybe there's a better way to report this.
1375 + startCommitSnapshotEffectsTimer();
1376 + stopCommitSnapshotEffectsTimer();
1377 + if (enableProfilerTimer) {
1378 + recordCommitTime();
1379 + }
1380 + startCommitHostEffectsTimer();
1381 + stopCommitHostEffectsTimer();
1382 + startCommitLifeCyclesTimer();
1383 + stopCommitLifeCyclesTimer();
1384 + }
1385 +
1386 + stopCommitTimer();
1387 +
1388 + if (rootDoesHavePassiveEffects) {
1389 + // This commit has passive effects. Stash a reference to them. But don't
1390 + // schedule a callback until after flushing layout work.
1391 + rootDoesHavePassiveEffects = false;
1392 + rootWithPendingPassiveEffects = root;
1393 + pendingPassiveEffectsExpirationTime = expirationTime;
1394 + } else {
1395 + if (enableSchedulerTracing) {
1396 + // If there are no passive effects, then we can complete the pending
1397 + // interactions. Otherwise, we'll wait until after the passive effects
1398 + // are flushed.
1399 + finishPendingInteractions(root, expirationTime);
1400 + }
1401 + }
1402 +
1403 + // Check if there's remaining work on this root
1404 + const remainingExpirationTime = root.firstPendingTime;
1405 + if (remainingExpirationTime !== NoWork) {
1406 + const currentTime = requestCurrentTime();
1407 + const priorityLevel = inferPriorityFromExpirationTime(
1408 + currentTime,
1409 + remainingExpirationTime,
1410 + );
1411 + scheduleCallbackForRoot(root, priorityLevel, remainingExpirationTime);
1412 + } else {
1413 + // If there's no remaining work, we can clear the set of already failed
1414 + // error boundaries.
1415 + legacyErrorBoundariesThatAlreadyFailed = null;
1416 + }
1417 +
1418 + onCommitRoot(finishedWork.stateNode);
1419 +
1420 + if (remainingExpirationTime === Sync) {
1421 + // Count the number of times the root synchronously re-renders without
1422 + // finishing. If there are too many, it indicates an infinite update loop.
1423 + if (root === rootWithNestedUpdates) {
1424 + nestedUpdateCount++;
1425 + } else {
1426 + nestedUpdateCount = 0;
1427 + rootWithNestedUpdates = root;
1428 + }
1429 + } else {
1430 + nestedUpdateCount = 0;
1431 + }
1432 +
1433 + if (hasUncaughtError) {
1434 + hasUncaughtError = false;
1435 + const error = firstUncaughtError;
1436 + firstUncaughtError = null;
1437 + throw error;
1438 + }
1439 +
1440 + if (workPhase === LegacyUnbatchedPhase) {
1441 + // This is a legacy edge case. We just committed the initial mount of
1442 + // a ReactDOM.render-ed root inside of batchedUpdates. The commit fired
1443 + // synchronously, but layout updates should be deferred until the end
1444 + // of the batch.
1445 + return null;
1446 + }
1447 +
1448 + // If layout work was scheduled, flush it now.
1449 + flushImmediateQueue();
1450 + return null;
1451 +}
1452 +
1453 +function commitBeforeMutationEffects() {
1454 + while (nextEffect !== null) {
1455 + if ((nextEffect.effectTag & Snapshot) !== NoEffect) {
1456 + setCurrentDebugFiberInDEV(nextEffect);
1457 + recordEffect();
1458 +
1459 + const current = nextEffect.alternate;
1460 + commitBeforeMutationEffectOnFiber(current, nextEffect);
1461 +
1462 + resetCurrentDebugFiberInDEV();
1463 + }
1464 + nextEffect = nextEffect.nextEffect;
1465 + }
1466 +}
1467 +
1468 +function commitMutationEffects() {
1469 + // TODO: Should probably move the bulk of this function to commitWork.
1470 + while (nextEffect !== null) {
1471 + setCurrentDebugFiberInDEV(nextEffect);
1472 +
1473 + const effectTag = nextEffect.effectTag;
1474 +
1475 + if (effectTag & ContentReset) {
1476 + commitResetTextContent(nextEffect);
1477 + }
1478 +
1479 + if (effectTag & Ref) {
1480 + const current = nextEffect.alternate;
1481 + if (current !== null) {
1482 + commitDetachRef(current);
1483 + }
1484 + }
1485 +
1486 + // The following switch statement is only concerned about placement,
1487 + // updates, and deletions. To avoid needing to add a case for every possible
1488 + // bitmap value, we remove the secondary effects from the effect tag and
1489 + // switch on that value.
1490 + let primaryEffectTag = effectTag & (Placement | Update | Deletion);
1491 + switch (primaryEffectTag) {
1492 + case Placement: {
1493 + commitPlacement(nextEffect);
1494 + // Clear the "placement" from effect tag so that we know that this is
1495 + // inserted, before any life-cycles like componentDidMount gets called.
1496 + // TODO: findDOMNode doesn't rely on this any more but isMounted does
1497 + // and isMounted is deprecated anyway so we should be able to kill this.
1498 + nextEffect.effectTag &= ~Placement;
1499 + break;
1500 + }
1501 + case PlacementAndUpdate: {
1502 + // Placement
1503 + commitPlacement(nextEffect);
1504 + // Clear the "placement" from effect tag so that we know that this is
1505 + // inserted, before any life-cycles like componentDidMount gets called.
1506 + nextEffect.effectTag &= ~Placement;
1507 +
1508 + // Update
1509 + const current = nextEffect.alternate;
1510 + commitWork(current, nextEffect);
1511 + break;
1512 + }
1513 + case Update: {
1514 + const current = nextEffect.alternate;
1515 + commitWork(current, nextEffect);
1516 + break;
1517 + }
1518 + case Deletion: {
1519 + commitDeletion(nextEffect);
1520 + break;
1521 + }
1522 + }
1523 +
1524 + // TODO: Only record a mutation effect if primaryEffectTag is non-zero.
1525 + recordEffect();
1526 +
1527 + resetCurrentDebugFiberInDEV();
1528 + nextEffect = nextEffect.nextEffect;
1529 + }
1530 +}
1531 +
1532 +function commitLayoutEffects(
1533 + root: FiberRoot,
1534 + committedExpirationTime: ExpirationTime,
1535 +) {
1536 + // TODO: Should probably move the bulk of this function to commitWork.
1537 + while (nextEffect !== null) {
1538 + setCurrentDebugFiberInDEV(nextEffect);
1539 +
1540 + const effectTag = nextEffect.effectTag;
1541 +
1542 + if (effectTag & (Update | Callback)) {
1543 + recordEffect();
1544 + const current = nextEffect.alternate;
1545 + commitLayoutEffectOnFiber(
1546 + root,
1547 + current,
1548 + nextEffect,
1549 + committedExpirationTime,
1550 + );
1551 + }
1552 +
1553 + if (effectTag & Ref) {
1554 + recordEffect();
1555 + commitAttachRef(nextEffect);
1556 + }
1557 +
1558 + if (effectTag & Passive) {
1559 + rootDoesHavePassiveEffects = true;
1560 + }
1561 +
1562 + resetCurrentDebugFiberInDEV();
1563 + nextEffect = nextEffect.nextEffect;
1564 + }
1565 +}
1566 +
1567 +export function flushPassiveEffects() {
1568 + if (rootWithPendingPassiveEffects === null) {
1569 + return false;
1570 + }
1571 + const root = rootWithPendingPassiveEffects;
1572 + const expirationTime = pendingPassiveEffectsExpirationTime;
1573 + rootWithPendingPassiveEffects = null;
1574 + pendingPassiveEffectsExpirationTime = NoWork;
1575 +
1576 + let prevInteractions: Set<Interaction> | null = null;
1577 + if (enableSchedulerTracing) {
1578 + prevInteractions = __interactionsRef.current;
1579 + __interactionsRef.current = root.memoizedInteractions;
1580 + }
1581 +
1582 + invariant(
1583 + workPhase !== RenderPhase && workPhase !== CommitPhase,
1584 + 'Cannot flush passive effects while already rendering.',
1585 + );
1586 + const prevWorkPhase = workPhase;
1587 + workPhase = CommitPhase;
1588 +
1589 + // Note: This currently assumes there are no passive effects on the root
1590 + // fiber, because the root is not part of its own effect list. This could
1591 + // change in the future.
1592 + let effect = root.current.firstEffect;
1593 + while (effect !== null) {
1594 + if (__DEV__) {
1595 + setCurrentDebugFiberInDEV(effect);
1596 + invokeGuardedCallback(null, commitPassiveHookEffects, null, effect);
1597 + if (hasCaughtError()) {
1598 + invariant(effect !== null, 'Should be working on an effect.');
1599 + const error = clearCaughtError();
1600 + captureCommitPhaseError(effect, error);
1601 + }
1602 + resetCurrentDebugFiberInDEV();
1603 + } else {
1604 + try {
1605 + commitPassiveHookEffects(effect);
1606 + } catch (error) {
1607 + invariant(effect !== null, 'Should be working on an effect.');
1608 + captureCommitPhaseError(effect, error);
1609 + }
1610 + }
1611 + effect = effect.nextEffect;
1612 + }
1613 +
1614 + if (enableSchedulerTracing) {
1615 + __interactionsRef.current = ((prevInteractions: any): Set<Interaction>);
1616 + finishPendingInteractions(root, expirationTime);
1617 + }
1618 +
1619 + workPhase = prevWorkPhase;
1620 + flushImmediateQueue();
1621 +
1622 + // If additional passive effects were scheduled, increment a counter. If this
1623 + // exceeds the limit, we'll fire a warning.
1624 + nestedPassiveUpdateCount =
1625 + rootWithPendingPassiveEffects === null ? 0 : nestedPassiveUpdateCount + 1;
1626 +
1627 + return true;
1628 +}
1629 +
1630 +export function isAlreadyFailedLegacyErrorBoundary(instance: mixed): boolean {
1631 + return (
1632 + legacyErrorBoundariesThatAlreadyFailed !== null &&
1633 + legacyErrorBoundariesThatAlreadyFailed.has(instance)
1634 + );
1635 +}
1636 +
1637 +export function markLegacyErrorBoundaryAsFailed(instance: mixed) {
1638 + if (legacyErrorBoundariesThatAlreadyFailed === null) {
1639 + legacyErrorBoundariesThatAlreadyFailed = new Set([instance]);
1640 + } else {
1641 + legacyErrorBoundariesThatAlreadyFailed.add(instance);
1642 + }
1643 +}
1644 +
1645 +function prepareToThrowUncaughtError(error: mixed) {
1646 + if (!hasUncaughtError) {
1647 + hasUncaughtError = true;
1648 + firstUncaughtError = error;
1649 + }
1650 +}
1651 +export const onUncaughtError = prepareToThrowUncaughtError;
1652 +
1653 +function captureCommitPhaseErrorOnRoot(
1654 + rootFiber: Fiber,
1655 + sourceFiber: Fiber,
1656 + error: mixed,
1657 +) {
1658 + const errorInfo = createCapturedValue(error, sourceFiber);
1659 + const update = createRootErrorUpdate(rootFiber, errorInfo, Sync);
1660 + enqueueUpdate(rootFiber, update);
1661 + const root = markUpdateTimeFromFiberToRoot(rootFiber, Sync);
1662 + if (root !== null) {
1663 + scheduleCallbackForRoot(root, ImmediatePriority, Sync);
1664 + }
1665 +}
1666 +
1667 +export function captureCommitPhaseError(sourceFiber: Fiber, error: mixed) {
1668 + if (sourceFiber.tag === HostRoot) {
1669 + // Error was thrown at the root. There is no parent, so the root
1670 + // itself should capture it.
1671 + captureCommitPhaseErrorOnRoot(sourceFiber, sourceFiber, error);
1672 + return;
1673 + }
1674 +
1675 + let fiber = sourceFiber.return;
1676 + while (fiber !== null) {
1677 + if (fiber.tag === HostRoot) {
1678 + captureCommitPhaseErrorOnRoot(fiber, sourceFiber, error);
1679 + return;
1680 + } else if (fiber.tag === ClassComponent) {
1681 + const ctor = fiber.type;
1682 + const instance = fiber.stateNode;
1683 + if (
1684 + typeof ctor.getDerivedStateFromError === 'function' ||
1685 + (typeof instance.componentDidCatch === 'function' &&
1686 + !isAlreadyFailedLegacyErrorBoundary(instance))
1687 + ) {
1688 + const errorInfo = createCapturedValue(error, sourceFiber);
1689 + const update = createClassErrorUpdate(
1690 + fiber,
1691 + errorInfo,
1692 + // TODO: This is always sync
1693 + Sync,
1694 + );
1695 + enqueueUpdate(fiber, update);
1696 + const root = markUpdateTimeFromFiberToRoot(fiber, Sync);
1697 + if (root !== null) {
1698 + scheduleCallbackForRoot(root, ImmediatePriority, Sync);
1699 + }
1700 + return;
1701 + }
1702 + }
1703 + fiber = fiber.return;
1704 + }
1705 +}
1706 +
1707 +export function pingSuspendedRoot(
1708 + root: FiberRoot,
1709 + thenable: Thenable,
1710 + suspendedTime: ExpirationTime,
1711 +) {
1712 + const pingCache = root.pingCache;
1713 + if (pingCache !== null) {
1714 + // The thenable resolved, so we no longer need to memoize, because it will
1715 + // never be thrown again.
1716 + pingCache.delete(thenable);
1717 + }
1718 +
1719 + if (workInProgressRoot === root && renderExpirationTime === suspendedTime) {
1720 + // Received a ping at the same priority level at which we're currently
1721 + // rendering. Restart from the root. Don't need to schedule a ping because
1722 + // we're already working on this tree.
1723 + prepareFreshStack(root, renderExpirationTime);
1724 + return;
1725 + }
1726 +
1727 + const lastPendingTime = root.lastPendingTime;
1728 + if (lastPendingTime < suspendedTime) {
1729 + // The root is no longer suspended at this time.
1730 + return;
1731 + }
1732 +
1733 + const pingTime = root.pingTime;
1734 + if (pingTime !== NoWork && pingTime < suspendedTime) {
1735 + // There's already a lower priority ping scheduled.
1736 + return;
1737 + }
1738 +
1739 + // Mark the time at which this ping was scheduled.
1740 + root.pingTime = suspendedTime;
1741 +
1742 + const currentTime = requestCurrentTime();
1743 + const priorityLevel = inferPriorityFromExpirationTime(
1744 + currentTime,
1745 + suspendedTime,
1746 + );
1747 + scheduleCallbackForRoot(root, priorityLevel, suspendedTime);
1748 +}
1749 +
1750 +export function retryTimedOutBoundary(boundaryFiber: Fiber) {
1751 + // The boundary fiber (a Suspense component) previously timed out and was
1752 + // rendered in its fallback state. One of the promises that suspended it has
1753 + // resolved, which means at least part of the tree was likely unblocked. Try
1754 + // rendering again, at a new expiration time.
1755 + const currentTime = requestCurrentTime();
1756 + const retryTime = computeExpirationForFiber(currentTime, boundaryFiber);
1757 + // TODO: Special case idle priority?
1758 + const priorityLevel = inferPriorityFromExpirationTime(currentTime, retryTime);
1759 + const root = markUpdateTimeFromFiberToRoot(boundaryFiber, retryTime);
1760 + if (root !== null) {
1761 + scheduleCallbackForRoot(root, priorityLevel, retryTime);
1762 + }
1763 +}
1764 +
1765 +export function resolveRetryThenable(boundaryFiber: Fiber, thenable: Thenable) {
1766 + let retryCache: WeakSet<Thenable> | Set<Thenable> | null;
1767 + if (enableSuspenseServerRenderer) {
1768 + switch (boundaryFiber.tag) {
1769 + case SuspenseComponent:
1770 + retryCache = boundaryFiber.stateNode;
1771 + break;
1772 + case DehydratedSuspenseComponent:
1773 + retryCache = boundaryFiber.memoizedState;
1774 + break;
1775 + default:
1776 + invariant(
1777 + false,
1778 + 'Pinged unknown suspense boundary type. ' +
1779 + 'This is probably a bug in React.',
1780 + );
1781 + }
1782 + } else {
1783 + retryCache = boundaryFiber.stateNode;
1784 + }
1785 +
1786 + if (retryCache !== null) {
1787 + // The thenable resolved, so we no longer need to memoize, because it will
1788 + // never be thrown again.
1789 + retryCache.delete(thenable);
1790 + }
1791 +
1792 + retryTimedOutBoundary(boundaryFiber);
1793 +}
1794 +
1795 +export function inferStartTimeFromExpirationTime(
1796 + root: FiberRoot,
1797 + expirationTime: ExpirationTime,
1798 +) {
1799 + // We don't know exactly when the update was scheduled, but we can infer an
1800 + // approximate start time from the expiration time.
1801 + const earliestExpirationTimeMs = expirationTimeToMs(root.firstPendingTime);
1802 + // TODO: Track this on the root instead. It's more accurate, doesn't rely on
1803 + // assumptions about priority, and isn't coupled to Scheduler details.
1804 + return earliestExpirationTimeMs - LOW_PRIORITY_EXPIRATION;
1805 +}
1806 +
1807 +function computeMsUntilTimeout(root, absoluteTimeoutMs) {
1808 + if (disableYielding) {
1809 + // Timeout immediately when yielding is disabled.
1810 + return 0;
1811 + }
1812 +
1813 + // Find the earliest uncommitted expiration time in the tree, including
1814 + // work that is suspended. The timeout threshold cannot be longer than
1815 + // the overall expiration.
1816 + const earliestExpirationTimeMs = expirationTimeToMs(root.firstPendingTime);
1817 + if (earliestExpirationTimeMs < absoluteTimeoutMs) {
1818 + absoluteTimeoutMs = earliestExpirationTimeMs;
1819 + }
1820 +
1821 + // Subtract the current time from the absolute timeout to get the number
1822 + // of milliseconds until the timeout. In other words, convert an absolute
1823 + // timestamp to a relative time. This is the value that is passed
1824 + // to `setTimeout`.
1825 + let msUntilTimeout = absoluteTimeoutMs - now();
1826 + return msUntilTimeout < 0 ? 0 : msUntilTimeout;
1827 +}
1828 +
1829 +function checkForNestedUpdates() {
1830 + if (nestedUpdateCount > NESTED_UPDATE_LIMIT) {
1831 + nestedUpdateCount = 0;
1832 + rootWithNestedUpdates = null;
1833 + invariant(
1834 + false,
1835 + 'Maximum update depth exceeded. This can happen when a component ' +
1836 + 'repeatedly calls setState inside componentWillUpdate or ' +
1837 + 'componentDidUpdate. React limits the number of nested updates to ' +
1838 + 'prevent infinite loops.',
1839 + );
1840 + }
1841 +
1842 + if (__DEV__) {
1843 + if (nestedPassiveUpdateCount > NESTED_PASSIVE_UPDATE_LIMIT) {
1844 + nestedPassiveUpdateCount = 0;
1845 + warning(
1846 + false,
1847 + 'Maximum update depth exceeded. This can happen when a component ' +
1848 + "calls setState inside useEffect, but useEffect either doesn't " +
1849 + 'have a dependency array, or one of the dependencies changes on ' +
1850 + 'every render.',
1851 + );
1852 + }
1853 + }
1854 +}
1855 +
1856 +function flushRenderPhaseStrictModeWarningsInDEV() {
1857 + if (__DEV__) {
1858 + ReactStrictModeWarnings.flushPendingUnsafeLifecycleWarnings();
1859 + ReactStrictModeWarnings.flushLegacyContextWarning();
1860 +
1861 + if (warnAboutDeprecatedLifecycles) {
1862 + ReactStrictModeWarnings.flushPendingDeprecationWarnings();
1863 + }
1864 + }
1865 +}
1866 +
1867 +function stopFinishedWorkLoopTimer() {
1868 + const didCompleteRoot = true;
1869 + stopWorkLoopTimer(interruptedBy, didCompleteRoot);
1870 + interruptedBy = null;
1871 +}
1872 +
1873 +function stopInterruptedWorkLoopTimer() {
1874 + // TODO: Track which fiber caused the interruption.
1875 + const didCompleteRoot = false;
1876 + stopWorkLoopTimer(interruptedBy, didCompleteRoot);
1877 + interruptedBy = null;
1878 +}
1879 +
1880 +function checkForInterruption(
1881 + fiberThatReceivedUpdate: Fiber,
1882 + updateExpirationTime: ExpirationTime,
1883 +) {
1884 + if (
1885 + enableUserTimingAPI &&
1886 + workInProgressRoot !== null &&
1887 + updateExpirationTime > renderExpirationTime
1888 + ) {
1889 + interruptedBy = fiberThatReceivedUpdate;
1890 + }
1891 +}
1892 +
1893 +let didWarnStateUpdateForUnmountedComponent: Set<string> | null = null;
1894 +function warnAboutUpdateOnUnmountedFiberInDEV(fiber) {
1895 + if (__DEV__) {
1896 + const tag = fiber.tag;
1897 + if (
1898 + tag !== HostRoot &&
1899 + tag !== ClassComponent &&
1900 + tag !== FunctionComponent &&
1901 + tag !== ForwardRef &&
1902 + tag !== MemoComponent &&
1903 + tag !== SimpleMemoComponent
1904 + ) {
1905 + // Only warn for user-defined components, not internal ones like Suspense.
1906 + return;
1907 + }
1908 + // We show the whole stack but dedupe on the top component's name because
1909 + // the problematic code almost always lies inside that component.
1910 + const componentName = getComponentName(fiber.type) || 'ReactComponent';
1911 + if (didWarnStateUpdateForUnmountedComponent !== null) {
1912 + if (didWarnStateUpdateForUnmountedComponent.has(componentName)) {
1913 + return;
1914 + }
1915 + didWarnStateUpdateForUnmountedComponent.add(componentName);
1916 + } else {
1917 + didWarnStateUpdateForUnmountedComponent = new Set([componentName]);
1918 + }
1919 + warningWithoutStack(
1920 + false,
1921 + "Can't perform a React state update on an unmounted component. This " +
1922 + 'is a no-op, but it indicates a memory leak in your application. To ' +
1923 + 'fix, cancel all subscriptions and asynchronous tasks in %s.%s',
1924 + tag === ClassComponent
1925 + ? 'the componentWillUnmount method'
1926 + : 'a useEffect cleanup function',
1927 + getStackByFiberInDevAndProd(fiber),
1928 + );
1929 + }
1930 +}
1931 +
1932 +let beginWork;
1933 +if (__DEV__ && replayFailedUnitOfWorkWithInvokeGuardedCallback) {
1934 + let dummyFiber = null;
1935 + beginWork = (current, unitOfWork, expirationTime) => {
1936 + // If a component throws an error, we replay it again in a synchronously
1937 + // dispatched event, so that the debugger will treat it as an uncaught
1938 + // error See ReactErrorUtils for more information.
1939 +
1940 + // Before entering the begin phase, copy the work-in-progress onto a dummy
1941 + // fiber. If beginWork throws, we'll use this to reset the state.
1942 + const originalWorkInProgressCopy = assignFiberPropertiesInDEV(
1943 + dummyFiber,
1944 + unitOfWork,
1945 + );
1946 + try {
1947 + return originalBeginWork(current, unitOfWork, expirationTime);
1948 + } catch (originalError) {
1949 + if (
1950 + originalError !== null &&
1951 + typeof originalError === 'object' &&
1952 + typeof originalError.then === 'function'
1953 + ) {
1954 + // Don't replay promises. Treat everything else like an error.
1955 + throw originalError;
1956 + }
1957 +
1958 + // Keep this code in sync with renderRoot; any changes here must have
1959 + // corresponding changes there.
1960 + resetContextDependencies();
1961 + resetHooks();
1962 +
1963 + // Unwind the failed stack frame
1964 + unwindInterruptedWork(unitOfWork);
1965 +
1966 + // Restore the original properties of the fiber.
1967 + assignFiberPropertiesInDEV(unitOfWork, originalWorkInProgressCopy);
1968 +
1969 + if (enableProfilerTimer && unitOfWork.mode & ProfileMode) {
1970 + // Reset the profiler timer.
1971 + startProfilerTimer(unitOfWork);
1972 + }
1973 +
1974 + // Run beginWork again.
1975 + invokeGuardedCallback(
1976 + null,
1977 + originalBeginWork,
1978 + null,
1979 + current,
1980 + unitOfWork,
1981 + expirationTime,
1982 + );
1983 +
1984 + if (hasCaughtError()) {
1985 + const replayError = clearCaughtError();
1986 + // `invokeGuardedCallback` sometimes sets an expando `_suppressLogging`.
1987 + // Rethrow this error instead of the original one.
1988 + throw replayError;
1989 + } else {
1990 + // This branch is reachable if the render phase is impure.
1991 + throw originalError;
1992 + }
1993 + }
1994 + };
1995 +} else {
1996 + beginWork = originalBeginWork;
1997 +}
1998 +
1999 +let didWarnAboutUpdateInRender = false;
2000 +let didWarnAboutUpdateInGetChildContext = false;
2001 +function warnAboutInvalidUpdatesOnClassComponentsInDEV(fiber) {
2002 + if (__DEV__) {
2003 + if (fiber.tag === ClassComponent) {
2004 + switch (ReactCurrentDebugFiberPhaseInDEV) {
2005 + case 'getChildContext':
2006 + if (didWarnAboutUpdateInGetChildContext) {
2007 + return;
2008 + }
2009 + warningWithoutStack(
2010 + false,
2011 + 'setState(...): Cannot call setState() inside getChildContext()',
2012 + );
2013 + didWarnAboutUpdateInGetChildContext = true;
2014 + break;
2015 + case 'render':
2016 + if (didWarnAboutUpdateInRender) {
2017 + return;
2018 + }
2019 + warningWithoutStack(
2020 + false,
2021 + 'Cannot update during an existing state transition (such as ' +
2022 + 'within `render`). Render methods should be a pure function of ' +
2023 + 'props and state.',
2024 + );
2025 + didWarnAboutUpdateInRender = true;
2026 + break;
2027 + }
2028 + }
2029 + }
2030 +}
2031 +
2032 +function warnIfNotCurrentlyActingUpdatesInDEV(fiber: Fiber): void {
2033 + if (__DEV__) {
2034 + if (
2035 + workPhase === NotWorking &&
2036 + ReactShouldWarnActingUpdates.current === false
2037 + ) {
2038 + warningWithoutStack(
2039 + false,
2040 + 'An update to %s inside a test was not wrapped in act(...).\n\n' +
2041 + 'When testing, code that causes React state updates should be ' +
2042 + 'wrapped into act(...):\n\n' +
2043 + 'act(() => {\n' +
2044 + ' /* fire events that update state */\n' +
2045 + '});\n' +
2046 + '/* assert on the output */\n\n' +
2047 + "This ensures that you're testing the behavior the user would see " +
2048 + 'in the browser.' +
2049 + ' Learn more at https://fb.me/react-wrap-tests-with-act' +
2050 + '%s',
2051 + getComponentName(fiber.type),
2052 + getStackByFiberInDevAndProd(fiber),
2053 + );
2054 + }
2055 + }
2056 +}
2057 +
2058 +export const warnIfNotCurrentlyActingUpdatesInDev = warnIfNotCurrentlyActingUpdatesInDEV;
2059 +
2060 +function computeThreadID(root, expirationTime) {
2061 + // Interaction threads are unique per root and expiration time.
2062 + return expirationTime * 1000 + root.interactionThreadID;
2063 +}
2064 +
2065 +function schedulePendingInteraction(root, expirationTime) {
2066 + // This is called when work is scheduled on a root. It sets up a pending
2067 + // interaction, which is completed once the work commits.
2068 + if (!enableSchedulerTracing) {
2069 + return;
2070 + }
2071 +
2072 + const interactions = __interactionsRef.current;
2073 + if (interactions.size > 0) {
2074 + const pendingInteractionMap = root.pendingInteractionMap;
2075 + const pendingInteractions = pendingInteractionMap.get(expirationTime);
2076 + if (pendingInteractions != null) {
2077 + interactions.forEach(interaction => {
2078 + if (!pendingInteractions.has(interaction)) {
2079 + // Update the pending async work count for previously unscheduled interaction.
2080 + interaction.__count++;
2081 + }
2082 +
2083 + pendingInteractions.add(interaction);
2084 + });
2085 + } else {
2086 + pendingInteractionMap.set(expirationTime, new Set(interactions));
2087 +
2088 + // Update the pending async work count for the current interactions.
2089 + interactions.forEach(interaction => {
2090 + interaction.__count++;
2091 + });
2092 + }
2093 +
2094 + const subscriber = __subscriberRef.current;
2095 + if (subscriber !== null) {
2096 + const threadID = computeThreadID(root, expirationTime);
2097 + subscriber.onWorkScheduled(interactions, threadID);
2098 + }
2099 + }
2100 +}
2101 +
2102 +function startWorkOnPendingInteraction(root, expirationTime) {
2103 + // This is called when new work is started on a root.
2104 + if (!enableSchedulerTracing) {
2105 + return;
2106 + }
2107 +
2108 + // Determine which interactions this batch of work currently includes, So that
2109 + // we can accurately attribute time spent working on it, And so that cascading
2110 + // work triggered during the render phase will be associated with it.
2111 + const interactions: Set<Interaction> = new Set();
2112 + root.pendingInteractionMap.forEach(
2113 + (scheduledInteractions, scheduledExpirationTime) => {
2114 + if (scheduledExpirationTime >= expirationTime) {
2115 + scheduledInteractions.forEach(interaction =>
2116 + interactions.add(interaction),
2117 + );
2118 + }
2119 + },
2120 + );
2121 +
2122 + // Store the current set of interactions on the FiberRoot for a few reasons:
2123 + // We can re-use it in hot functions like renderRoot() without having to
2124 + // recalculate it. We will also use it in commitWork() to pass to any Profiler
2125 + // onRender() hooks. This also provides DevTools with a way to access it when
2126 + // the onCommitRoot() hook is called.
2127 + root.memoizedInteractions = interactions;
2128 +
2129 + if (interactions.size > 0) {
2130 + const subscriber = __subscriberRef.current;
2131 + if (subscriber !== null) {
2132 + const threadID = computeThreadID(root, expirationTime);
2133 + try {
2134 + subscriber.onWorkStarted(interactions, threadID);
2135 + } catch (error) {
2136 + // If the subscriber throws, rethrow it in a separate task
2137 + scheduleCallback(ImmediatePriority, () => {
2138 + throw error;
2139 + });
2140 + }
2141 + }
2142 + }
2143 +}
2144 +
2145 +function finishPendingInteractions(root, committedExpirationTime) {
2146 + if (!enableSchedulerTracing) {
2147 + return;
2148 + }
2149 +
2150 + const earliestRemainingTimeAfterCommit = root.firstPendingTime;
2151 +
2152 + let subscriber;
2153 +
2154 + try {
2155 + subscriber = __subscriberRef.current;
2156 + if (subscriber !== null && root.memoizedInteractions.size > 0) {
2157 + const threadID = computeThreadID(root, committedExpirationTime);
2158 + subscriber.onWorkStopped(root.memoizedInteractions, threadID);
2159 + }
2160 + } catch (error) {
2161 + // If the subscriber throws, rethrow it in a separate task
2162 + scheduleCallback(ImmediatePriority, () => {
2163 + throw error;
2164 + });
2165 + } finally {
2166 + // Clear completed interactions from the pending Map.
2167 + // Unless the render was suspended or cascading work was scheduled,
2168 + // In which case– leave pending interactions until the subsequent render.
2169 + const pendingInteractionMap = root.pendingInteractionMap;
2170 + pendingInteractionMap.forEach(
2171 + (scheduledInteractions, scheduledExpirationTime) => {
2172 + // Only decrement the pending interaction count if we're done.
2173 + // If there's still work at the current priority,
2174 + // That indicates that we are waiting for suspense data.
2175 + if (scheduledExpirationTime > earliestRemainingTimeAfterCommit) {
2176 + pendingInteractionMap.delete(scheduledExpirationTime);
2177 +
2178 + scheduledInteractions.forEach(interaction => {
2179 + interaction.__count--;
2180 +
2181 + if (subscriber !== null && interaction.__count === 0) {
2182 + try {
2183 + subscriber.onInteractionScheduledWorkCompleted(interaction);
2184 + } catch (error) {
2185 + // If the subscriber throws, rethrow it in a separate task
2186 + scheduleCallback(ImmediatePriority, () => {
2187 + throw error;
2188 + });
2189 + }
2190 + }
2191 + });
2192 + }
2193 + },
2194 + );
2195 + }
2196 +}
packages/react-reconciler/src/ReactFiberScheduler.old.js
+26 -3
@@ -123,6 +123,7 @@ import {
123 expirationTimeToMs,
124 computeAsyncExpiration,
125 computeInteractiveExpiration,
126 + LOW_PRIORITY_EXPIRATION,
127 } from './ReactFiberExpirationTime';
128 import {ConcurrentMode, ProfileMode} from './ReactTypeOfMode';
129 import {enqueueUpdate, resetCurrentlyProcessingQueue} from './ReactUpdateQueue';
@@ -173,6 +174,8 @@ const {
174 unstable_cancelCallback: cancelCallback,
175 unstable_shouldYield: shouldYield,
176 unstable_now: now,
177 + unstable_getCurrentPriorityLevel: getCurrentPriorityLevel,
178 + unstable_NormalPriority: NormalPriority,
179 } = Scheduler;
180
181 export type Thenable = {
@@ -826,7 +829,7 @@ function commitRoot(root: FiberRoot, finishedWork: Fiber): void {
829 // here because that code is still in flux.
830 callback = Scheduler_tracing_wrap(callback);
831 }
829 - passiveEffectCallbackHandle = scheduleCallback(callback);
832 + passiveEffectCallbackHandle = scheduleCallback(NormalPriority, callback);
833 passiveEffectCallback = callback;
834 }
835
@@ -1677,6 +1680,25 @@ function renderDidError() {
1680 nextRenderDidError = true;
1681 }
1682
1683 +function inferStartTimeFromExpirationTime(
1684 + root: FiberRoot,
1685 + expirationTime: ExpirationTime,
1686 +) {
1687 + // We don't know exactly when the update was scheduled, but we can infer an
1688 + // approximate start time from the expiration time. First, find the earliest
1689 + // uncommitted expiration time in the tree, including work that is suspended.
1690 + // Then subtract the offset used to compute an async update's expiration time.
1691 + // This will cause high priority (interactive) work to expire earlier than
1692 + // necessary, but we can account for this by adjusting for the Just
1693 + // Noticeable Difference.
1694 + const earliestExpirationTime = findEarliestOutstandingPriorityLevel(
1695 + root,
1696 + expirationTime,
1697 + );
1698 + const earliestExpirationTimeMs = expirationTimeToMs(earliestExpirationTime);
1699 + return earliestExpirationTimeMs - LOW_PRIORITY_EXPIRATION;
1700 +}
1701 +
1702 function pingSuspendedRoot(
1703 root: FiberRoot,
1704 thenable: Thenable,
@@ -2044,7 +2066,8 @@ function scheduleCallbackWithExpirationTime(
2066 const currentMs = now() - originalStartTimeMs;
2067 const expirationTimeMs = expirationTimeToMs(expirationTime);
2068 const timeout = expirationTimeMs - currentMs;
2047 - callbackID = scheduleCallback(performAsyncWork, {timeout});
2069 + const priorityLevel = getCurrentPriorityLevel();
2070 + callbackID = scheduleCallback(priorityLevel, performAsyncWork, {timeout});
2071 }
2072
2073 // For every call to renderRoot, one of onFatal, onComplete, onSuspend, and
@@ -2677,7 +2700,6 @@ export {
2700 markLegacyErrorBoundaryAsFailed,
2701 isAlreadyFailedLegacyErrorBoundary,
2702 scheduleWork,
2680 - requestWork,
2703 flushRoot,
2704 batchedUpdates,
2705 unbatchedUpdates,
@@ -2689,4 +2711,5 @@ export {
2711 flushInteractiveUpdates,
2712 computeUniqueAsyncExpiration,
2713 flushPassiveEffects,
2714 + inferStartTimeFromExpirationTime,
2715 };
packages/react-reconciler/src/ReactFiberUnwindWork.js
+5 -18
@@ -70,16 +70,12 @@ import {
70 isAlreadyFailedLegacyErrorBoundary,
71 pingSuspendedRoot,
72 resolveRetryThenable,
73 + inferStartTimeFromExpirationTime,
74 } from './ReactFiberScheduler';
75
76 import invariant from 'shared/invariant';
77 import maxSigned31BitInt from './maxSigned31BitInt';
77 -import {
78 - Sync,
79 - expirationTimeToMs,
80 - LOW_PRIORITY_EXPIRATION,
81 -} from './ReactFiberExpirationTime';
82 -import {findEarliestOutstandingPriorityLevel} from './ReactFiberPendingPriority';
78 +import {Sync, expirationTimeToMs} from './ReactFiberExpirationTime';
79
80 const PossiblyWeakSet = typeof WeakSet === 'function' ? WeakSet : Set;
81 const PossiblyWeakMap = typeof WeakMap === 'function' ? WeakMap : Map;
@@ -322,21 +318,12 @@ function throwException(
318 if (startTimeMs === -1) {
319 // This suspend happened outside of any already timed-out
320 // placeholders. We don't know exactly when the update was
325 - // scheduled, but we can infer an approximate start time from the
326 - // expiration time. First, find the earliest uncommitted expiration
327 - // time in the tree, including work that is suspended. Then subtract
328 - // the offset used to compute an async update's expiration time.
329 - // This will cause high priority (interactive) work to expire
330 - // earlier than necessary, but we can account for this by adjusting
331 - // for the Just Noticeable Difference.
332 - const earliestExpirationTime = findEarliestOutstandingPriorityLevel(
321 + // scheduled, but we can infer an approximate start time based on
322 + // the expiration time and the priority.
323 + startTimeMs = inferStartTimeFromExpirationTime(
324 root,
325 renderExpirationTime,
326 );
336 - const earliestExpirationTimeMs = expirationTimeToMs(
337 - earliestExpirationTime,
338 - );
339 - startTimeMs = earliestExpirationTimeMs - LOW_PRIORITY_EXPIRATION;
327 }
328 absoluteTimeoutMs = startTimeMs + earliestTimeoutMs;
329 }
packages/react-reconciler/src/SchedulerWithReactIntegration.js new
+171
@@ -0,0 +1,171 @@
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 +// Intentionally not named imports because Rollup would use dynamic dispatch for
11 +// CommonJS interop named imports.
12 +import * as Scheduler from 'scheduler';
13 +
14 +import {disableYielding} from 'shared/ReactFeatureFlags';
15 +import invariant from 'shared/invariant';
16 +
17 +const {
18 + unstable_runWithPriority: Scheduler_runWithPriority,
19 + unstable_scheduleCallback: Scheduler_scheduleCallback,
20 + unstable_cancelCallback: Scheduler_cancelCallback,
21 + unstable_shouldYield: Scheduler_shouldYield,
22 + unstable_now: Scheduler_now,
23 + unstable_getCurrentPriorityLevel: Scheduler_getCurrentPriorityLevel,
24 + unstable_ImmediatePriority: Scheduler_ImmediatePriority,
25 + unstable_UserBlockingPriority: Scheduler_UserBlockingPriority,
26 + unstable_NormalPriority: Scheduler_NormalPriority,
27 + unstable_LowPriority: Scheduler_LowPriority,
28 + unstable_IdlePriority: Scheduler_IdlePriority,
29 +} = Scheduler;
30 +
31 +export opaque type ReactPriorityLevel = 99 | 98 | 97 | 96 | 95 | 90;
32 +export type SchedulerCallback = (isSync: boolean) => SchedulerCallback | null;
33 +
34 +type SchedulerCallbackOptions = {
35 + timeout?: number,
36 +};
37 +
38 +const fakeCallbackNode = {};
39 +
40 +// Except for NoPriority, these correspond to Scheduler priorities. We use
41 +// ascending numbers so we can compare them like numbers. They start at 90 to
42 +// avoid clashing with Scheduler's priorities.
43 +export const ImmediatePriority: ReactPriorityLevel = 99;
44 +export const UserBlockingPriority: ReactPriorityLevel = 98;
45 +export const NormalPriority: ReactPriorityLevel = 97;
46 +export const LowPriority: ReactPriorityLevel = 96;
47 +export const IdlePriority: ReactPriorityLevel = 95;
48 +// NoPriority is the absence of priority. Also React-only.
49 +export const NoPriority: ReactPriorityLevel = 90;
50 +
51 +export const now = Scheduler_now;
52 +export const shouldYield = disableYielding
53 + ? () => false // Never yield when `disableYielding` is on
54 + : Scheduler_shouldYield;
55 +
56 +let immediateQueue: Array<SchedulerCallback> | null = null;
57 +let immediateQueueCallbackNode: mixed | null = null;
58 +let isFlushingImmediate: boolean = false;
59 +
60 +export function getCurrentPriorityLevel(): ReactPriorityLevel {
61 + switch (Scheduler_getCurrentPriorityLevel()) {
62 + case Scheduler_ImmediatePriority:
63 + return ImmediatePriority;
64 + case Scheduler_UserBlockingPriority:
65 + return UserBlockingPriority;
66 + case Scheduler_NormalPriority:
67 + return NormalPriority;
68 + case Scheduler_LowPriority:
69 + return LowPriority;
70 + case Scheduler_IdlePriority:
71 + return IdlePriority;
72 + default:
73 + invariant(false, 'Unknown priority level.');
74 + }
75 +}
76 +
77 +function reactPriorityToSchedulerPriority(reactPriorityLevel) {
78 + switch (reactPriorityLevel) {
79 + case ImmediatePriority:
80 + return Scheduler_ImmediatePriority;
81 + case UserBlockingPriority:
82 + return Scheduler_UserBlockingPriority;
83 + case NormalPriority:
84 + return Scheduler_NormalPriority;
85 + case LowPriority:
86 + return Scheduler_LowPriority;
87 + case IdlePriority:
88 + return Scheduler_IdlePriority;
89 + default:
90 + invariant(false, 'Unknown priority level.');
91 + }
92 +}
93 +
94 +export function runWithPriority<T>(
95 + reactPriorityLevel: ReactPriorityLevel,
96 + fn: () => T,
97 +): T {
98 + const priorityLevel = reactPriorityToSchedulerPriority(reactPriorityLevel);
99 + return Scheduler_runWithPriority(priorityLevel, fn);
100 +}
101 +
102 +export function scheduleCallback(
103 + reactPriorityLevel: ReactPriorityLevel,
104 + callback: SchedulerCallback,
105 + options: SchedulerCallbackOptions | void | null,
106 +) {
107 + if (reactPriorityLevel === ImmediatePriority) {
108 + // Push this callback into an internal queue. We'll flush these either in
109 + // the next tick, or earlier if something calls `flushImmediateQueue`.
110 + if (immediateQueue === null) {
111 + immediateQueue = [callback];
112 + // Flush the queue in the next tick, at the earliest.
113 + immediateQueueCallbackNode = Scheduler_scheduleCallback(
114 + Scheduler_ImmediatePriority,
115 + flushImmediateQueueImpl,
116 + );
117 + } else {
118 + // Push onto existing queue. Don't need to schedule a callback because
119 + // we already scheduled one when we created the queue.
120 + immediateQueue.push(callback);
121 + }
122 + return fakeCallbackNode;
123 + }
124 + // Otherwise pass through to Scheduler.
125 + const priorityLevel = reactPriorityToSchedulerPriority(reactPriorityLevel);
126 + return Scheduler_scheduleCallback(priorityLevel, callback, options);
127 +}
128 +
129 +export function cancelCallback(callbackNode: mixed) {
130 + if (callbackNode !== fakeCallbackNode) {
131 + Scheduler_cancelCallback(callbackNode);
132 + }
133 +}
134 +
135 +export function flushImmediateQueue() {
136 + if (immediateQueueCallbackNode !== null) {
137 + Scheduler_cancelCallback(immediateQueueCallbackNode);
138 + }
139 + flushImmediateQueueImpl();
140 +}
141 +
142 +function flushImmediateQueueImpl() {
143 + if (!isFlushingImmediate && immediateQueue !== null) {
144 + // Prevent re-entrancy.
145 + isFlushingImmediate = true;
146 + let i = 0;
147 + try {
148 + const isSync = true;
149 + for (; i < immediateQueue.length; i++) {
150 + let callback = immediateQueue[i];
151 + do {
152 + callback = callback(isSync);
153 + } while (callback !== null);
154 + }
155 + immediateQueue = null;
156 + } catch (error) {
157 + // If something throws, leave the remaining callbacks on the queue.
158 + if (immediateQueue !== null) {
159 + immediateQueue = immediateQueue.slice(i + 1);
160 + }
161 + // Resume flushing in the next tick
162 + Scheduler_scheduleCallback(
163 + Scheduler_ImmediatePriority,
164 + flushImmediateQueue,
165 + );
166 + throw error;
167 + } finally {
168 + isFlushingImmediate = false;
169 + }
170 + }
171 +}
packages/react-reconciler/src/__tests__/ReactHooks-test.internal.js
+3
@@ -1747,6 +1747,7 @@ describe('ReactHooks', () => {
1747 );
1748 expect(root).toMatchRenderedOutput('loading');
1749 await Promise.resolve();
1750 + Scheduler.flushAll();
1751 expect(root).toMatchRenderedOutput('hello');
1752 });
1753
@@ -1778,6 +1779,7 @@ describe('ReactHooks', () => {
1779 );
1780 expect(root).toMatchRenderedOutput('loading');
1781 await Promise.resolve();
1782 + Scheduler.flushAll();
1783 expect(root).toMatchRenderedOutput('hello');
1784 });
1785
@@ -1809,6 +1811,7 @@ describe('ReactHooks', () => {
1811 );
1812 expect(root).toMatchRenderedOutput('loading');
1813 await Promise.resolve();
1814 + Scheduler.flushAll();
1815 expect(root).toMatchRenderedOutput('hello');
1816 });
1817 });
packages/react-reconciler/src/__tests__/ReactIncrementalErrorHandling-test.internal.js
+53 -26
@@ -15,6 +15,7 @@ let ReactFeatureFlags;
15 let React;
16 let ReactNoop;
17 let Scheduler;
18 +let enableNewScheduler;
19
20 describe('ReactIncrementalErrorHandling', () => {
21 beforeEach(() => {
@@ -22,6 +23,7 @@ describe('ReactIncrementalErrorHandling', () => {
23 ReactFeatureFlags = require('shared/ReactFeatureFlags');
24 ReactFeatureFlags.debugRenderPhaseSideEffectsForStrictMode = false;
25 ReactFeatureFlags.replayFailedUnitOfWorkWithInvokeGuardedCallback = false;
26 + enableNewScheduler = ReactFeatureFlags.enableNewScheduler;
27 PropTypes = require('prop-types');
28 React = require('react');
29 ReactNoop = require('react-noop-renderer');
@@ -131,6 +133,7 @@ describe('ReactIncrementalErrorHandling', () => {
133 'ErrorBoundary (catch)',
134 'ErrorMessage',
135 ]);
136 +
137 expect(ReactNoop.getChildren()).toEqual([span('Caught an error: oops!')]);
138 });
139
@@ -306,21 +309,17 @@ describe('ReactIncrementalErrorHandling', () => {
309 });
310
311 it('retries one more time before handling error', () => {
309 - let ops = [];
312 function BadRender() {
311 - ops.push('BadRender');
313 Scheduler.yieldValue('BadRender');
314 throw new Error('oops');
315 }
316
317 function Sibling() {
317 - ops.push('Sibling');
318 Scheduler.yieldValue('Sibling');
319 return <span prop="Sibling" />;
320 }
321
322 function Parent() {
323 - ops.push('Parent');
323 Scheduler.yieldValue('Parent');
324 return (
325 <React.Fragment>
@@ -338,10 +337,15 @@ describe('ReactIncrementalErrorHandling', () => {
337 // Finish the rest of the async work
338 expect(Scheduler).toFlushAndYieldThrough(['Sibling']);
339
341 - // React retries once, synchronously, before throwing.
342 - ops = [];
343 - expect(() => ReactNoop.flushNextYield()).toThrow('oops');
344 - expect(ops).toEqual(['Parent', 'BadRender', 'Sibling']);
340 + // Old scheduler renders, commits, and throws synchronously
341 + expect(() => Scheduler.unstable_flushNumberOfYields(1)).toThrow('oops');
342 + expect(Scheduler).toHaveYielded([
343 + 'Parent',
344 + 'BadRender',
345 + 'Sibling',
346 + 'commit',
347 + ]);
348 + expect(ReactNoop.getChildren()).toEqual([]);
349 });
350
351 // TODO: This is currently unobservable, but will be once we lift renderRoot
@@ -744,7 +748,8 @@ describe('ReactIncrementalErrorHandling', () => {
748 expect(ReactNoop.getChildren()).toEqual([span('a:5')]);
749 });
750
747 - it('applies sync updates regardless despite errors in scheduling', () => {
751 + // TODO: Is this a breaking change?
752 + it('defers additional sync work to a separate event after an error', () => {
753 ReactNoop.render(<span prop="a:1" />);
754 expect(() => {
755 ReactNoop.flushSync(() => {
@@ -755,6 +760,7 @@ describe('ReactIncrementalErrorHandling', () => {
760 });
761 });
762 }).toThrow('Hello');
763 + Scheduler.flushAll();
764 expect(ReactNoop.getChildren()).toEqual([span('a:3')]);
765 });
766
@@ -962,43 +968,46 @@ describe('ReactIncrementalErrorHandling', () => {
968 expect(ReactNoop.getChildren('a')).toEqual([
969 span('Caught an error: Hello.'),
970 ]);
971 + expect(Scheduler).toFlushWithoutYielding();
972 expect(ReactNoop.getChildren('b')).toEqual([span('b:1')]);
973 });
974
975 it('continues work on other roots despite uncaught errors', () => {
976 function BrokenRender(props) {
970 - throw new Error('Hello');
977 + throw new Error(props.label);
978 }
979
973 - ReactNoop.renderToRootWithID(<BrokenRender />, 'a');
980 + ReactNoop.renderToRootWithID(<BrokenRender label="a" />, 'a');
981 expect(() => {
982 expect(Scheduler).toFlushWithoutYielding();
976 - }).toThrow('Hello');
983 + }).toThrow('a');
984 expect(ReactNoop.getChildren('a')).toEqual([]);
985
979 - ReactNoop.renderToRootWithID(<BrokenRender />, 'a');
986 + ReactNoop.renderToRootWithID(<BrokenRender label="a" />, 'a');
987 ReactNoop.renderToRootWithID(<span prop="b:2" />, 'b');
988 expect(() => {
989 expect(Scheduler).toFlushWithoutYielding();
983 - }).toThrow('Hello');
990 + }).toThrow('a');
991
992 + expect(Scheduler).toFlushWithoutYielding();
993 expect(ReactNoop.getChildren('a')).toEqual([]);
994 expect(ReactNoop.getChildren('b')).toEqual([span('b:2')]);
995
996 ReactNoop.renderToRootWithID(<span prop="a:3" />, 'a');
989 - ReactNoop.renderToRootWithID(<BrokenRender />, 'b');
997 + ReactNoop.renderToRootWithID(<BrokenRender label="b" />, 'b');
998 expect(() => {
999 expect(Scheduler).toFlushWithoutYielding();
992 - }).toThrow('Hello');
1000 + }).toThrow('b');
1001 expect(ReactNoop.getChildren('a')).toEqual([span('a:3')]);
1002 expect(ReactNoop.getChildren('b')).toEqual([]);
1003
1004 ReactNoop.renderToRootWithID(<span prop="a:4" />, 'a');
997 - ReactNoop.renderToRootWithID(<BrokenRender />, 'b');
1005 + ReactNoop.renderToRootWithID(<BrokenRender label="b" />, 'b');
1006 ReactNoop.renderToRootWithID(<span prop="c:4" />, 'c');
1007 expect(() => {
1008 expect(Scheduler).toFlushWithoutYielding();
1001 - }).toThrow('Hello');
1009 + }).toThrow('b');
1010 + expect(Scheduler).toFlushWithoutYielding();
1011 expect(ReactNoop.getChildren('a')).toEqual([span('a:4')]);
1012 expect(ReactNoop.getChildren('b')).toEqual([]);
1013 expect(ReactNoop.getChildren('c')).toEqual([span('c:4')]);
@@ -1007,25 +1016,43 @@ describe('ReactIncrementalErrorHandling', () => {
1016 ReactNoop.renderToRootWithID(<span prop="b:5" />, 'b');
1017 ReactNoop.renderToRootWithID(<span prop="c:5" />, 'c');
1018 ReactNoop.renderToRootWithID(<span prop="d:5" />, 'd');
1010 - ReactNoop.renderToRootWithID(<BrokenRender />, 'e');
1019 + ReactNoop.renderToRootWithID(<BrokenRender label="e" />, 'e');
1020 expect(() => {
1021 expect(Scheduler).toFlushWithoutYielding();
1013 - }).toThrow('Hello');
1022 + }).toThrow('e');
1023 + expect(Scheduler).toFlushWithoutYielding();
1024 expect(ReactNoop.getChildren('a')).toEqual([span('a:5')]);
1025 expect(ReactNoop.getChildren('b')).toEqual([span('b:5')]);
1026 expect(ReactNoop.getChildren('c')).toEqual([span('c:5')]);
1027 expect(ReactNoop.getChildren('d')).toEqual([span('d:5')]);
1028 expect(ReactNoop.getChildren('e')).toEqual([]);
1029
1020 - ReactNoop.renderToRootWithID(<BrokenRender />, 'a');
1030 + ReactNoop.renderToRootWithID(<BrokenRender label="a" />, 'a');
1031 ReactNoop.renderToRootWithID(<span prop="b:6" />, 'b');
1022 - ReactNoop.renderToRootWithID(<BrokenRender />, 'c');
1032 + ReactNoop.renderToRootWithID(<BrokenRender label="c" />, 'c');
1033 ReactNoop.renderToRootWithID(<span prop="d:6" />, 'd');
1024 - ReactNoop.renderToRootWithID(<BrokenRender />, 'e');
1034 + ReactNoop.renderToRootWithID(<BrokenRender label="e" />, 'e');
1035 ReactNoop.renderToRootWithID(<span prop="f:6" />, 'f');
1026 - expect(() => {
1027 - expect(Scheduler).toFlushWithoutYielding();
1028 - }).toThrow('Hello');
1036 +
1037 + if (enableNewScheduler) {
1038 + // The new scheduler will throw all three errors.
1039 + expect(() => {
1040 + expect(Scheduler).toFlushWithoutYielding();
1041 + }).toThrow('a');
1042 + expect(() => {
1043 + expect(Scheduler).toFlushWithoutYielding();
1044 + }).toThrow('c');
1045 + expect(() => {
1046 + expect(Scheduler).toFlushWithoutYielding();
1047 + }).toThrow('e');
1048 + } else {
1049 + // The old scheduler only throws the first one.
1050 + expect(() => {
1051 + expect(Scheduler).toFlushWithoutYielding();
1052 + }).toThrow('a');
1053 + }
1054 +
1055 + expect(Scheduler).toFlushWithoutYielding();
1056 expect(ReactNoop.getChildren('a')).toEqual([]);
1057 expect(ReactNoop.getChildren('b')).toEqual([span('b:6')]);
1058 expect(ReactNoop.getChildren('c')).toEqual([]);
packages/react-reconciler/src/__tests__/ReactIncrementalPerf-test.internal.js
+495 -481
@@ -110,27 +110,6 @@ describe('ReactDebugFiberPerf', () => {
110 };
111 }
112
113 - beforeEach(() => {
114 - jest.resetModules();
115 - resetFlamechart();
116 - global.performance = createUserTimingPolyfill();
117 -
118 - require('shared/ReactFeatureFlags').enableUserTimingAPI = true;
119 - require('shared/ReactFeatureFlags').enableProfilerTimer = false;
120 - require('shared/ReactFeatureFlags').replayFailedUnitOfWorkWithInvokeGuardedCallback = false;
121 - require('shared/ReactFeatureFlags').debugRenderPhaseSideEffectsForStrictMode = false;
122 -
123 - // Import after the polyfill is set up:
124 - React = require('react');
125 - ReactNoop = require('react-noop-renderer');
126 - Scheduler = require('scheduler');
127 - PropTypes = require('prop-types');
128 - });
129 -
130 - afterEach(() => {
131 - delete global.performance;
132 - });
133 -
113 function Parent(props) {
114 return <div>{props.children}</div>;
115 }
@@ -139,533 +118,568 @@ describe('ReactDebugFiberPerf', () => {
118 return <div>{props.children}</div>;
119 }
120
142 - it('measures a simple reconciliation', () => {
143 - ReactNoop.render(
144 - <Parent>
145 - <Child />
146 - </Parent>,
147 - );
148 - addComment('Mount');
149 - expect(Scheduler).toFlushWithoutYielding();
150 -
151 - ReactNoop.render(
152 - <Parent>
153 - <Child />
154 - </Parent>,
155 - );
156 - addComment('Update');
157 - expect(Scheduler).toFlushWithoutYielding();
158 -
159 - ReactNoop.render(null);
160 - addComment('Unmount');
161 - expect(Scheduler).toFlushWithoutYielding();
162 -
163 - expect(getFlameChart()).toMatchSnapshot();
121 + describe('old scheduler', () => {
122 + runTests(false);
123 });
124
166 - it('properly displays the forwardRef component in measurements', () => {
167 - const AnonymousForwardRef = React.forwardRef((props, ref) => (
168 - <Child {...props} ref={ref} />
169 - ));
170 - const NamedForwardRef = React.forwardRef(function refForwarder(props, ref) {
171 - return <Child {...props} ref={ref} />;
172 - });
173 - function notImportant(props, ref) {
174 - return <Child {...props} ref={ref} />;
175 - }
176 - notImportant.displayName = 'OverriddenName';
177 - const DisplayNamedForwardRef = React.forwardRef(notImportant);
178 -
179 - ReactNoop.render(
180 - <Parent>
181 - <AnonymousForwardRef />
182 - <NamedForwardRef />
183 - <DisplayNamedForwardRef />
184 - </Parent>,
185 - );
186 - addComment('Mount');
187 - expect(Scheduler).toFlushWithoutYielding();
188 -
189 - expect(getFlameChart()).toMatchSnapshot();
125 + describe('new scheduler', () => {
126 + runTests(true);
127 });
128
192 - it('does not include ConcurrentMode, StrictMode, or Profiler components in measurements', () => {
193 - ReactNoop.render(
194 - <React.Profiler id="test" onRender={jest.fn()}>
195 - <React.StrictMode>
196 - <Parent>
197 - <React.unstable_ConcurrentMode>
198 - <Child />
199 - </React.unstable_ConcurrentMode>
200 - </Parent>
201 - </React.StrictMode>
202 - </React.Profiler>,
203 - );
204 - addComment('Mount');
205 - expect(Scheduler).toFlushWithoutYielding();
129 + function runTests(enableNewScheduler) {
130 + beforeEach(() => {
131 + jest.resetModules();
132 + resetFlamechart();
133 + global.performance = createUserTimingPolyfill();
134 +
135 + require('shared/ReactFeatureFlags').enableNewScheduler = enableNewScheduler;
136 + require('shared/ReactFeatureFlags').enableUserTimingAPI = true;
137 + require('shared/ReactFeatureFlags').enableProfilerTimer = false;
138 + require('shared/ReactFeatureFlags').replayFailedUnitOfWorkWithInvokeGuardedCallback = false;
139 + require('shared/ReactFeatureFlags').debugRenderPhaseSideEffectsForStrictMode = false;
140 +
141 + // Import after the polyfill is set up:
142 + React = require('react');
143 + ReactNoop = require('react-noop-renderer');
144 + Scheduler = require('scheduler');
145 + PropTypes = require('prop-types');
146 + });
147
207 - expect(getFlameChart()).toMatchSnapshot();
208 - });
148 + afterEach(() => {
149 + delete global.performance;
150 + });
151
210 - it('does not include context provider or consumer in measurements', () => {
211 - const {Consumer, Provider} = React.createContext(true);
152 + it('measures a simple reconciliation', () => {
153 + ReactNoop.render(
154 + <Parent>
155 + <Child />
156 + </Parent>,
157 + );
158 + addComment('Mount');
159 + expect(Scheduler).toFlushWithoutYielding();
160
213 - ReactNoop.render(
214 - <Provider value={false}>
161 + ReactNoop.render(
162 <Parent>
216 - <Consumer>{value => <Child value={value} />}</Consumer>
217 - </Parent>
218 - </Provider>,
219 - );
220 - addComment('Mount');
221 - expect(Scheduler).toFlushWithoutYielding();
222 -
223 - expect(getFlameChart()).toMatchSnapshot();
224 - });
163 + <Child />
164 + </Parent>,
165 + );
166 + addComment('Update');
167 + expect(Scheduler).toFlushWithoutYielding();
168
226 - it('skips parents during setState', () => {
227 - class A extends React.Component {
228 - render() {
229 - return <div>{this.props.children}</div>;
230 - }
231 - }
169 + ReactNoop.render(null);
170 + addComment('Unmount');
171 + expect(Scheduler).toFlushWithoutYielding();
172
233 - class B extends React.Component {
234 - render() {
235 - return <div>{this.props.children}</div>;
173 + expect(getFlameChart()).toMatchSnapshot();
174 + });
175 +
176 + it('properly displays the forwardRef component in measurements', () => {
177 + const AnonymousForwardRef = React.forwardRef((props, ref) => (
178 + <Child {...props} ref={ref} />
179 + ));
180 + const NamedForwardRef = React.forwardRef(function refForwarder(
181 + props,
182 + ref,
183 + ) {
184 + return <Child {...props} ref={ref} />;
185 + });
186 + function notImportant(props, ref) {
187 + return <Child {...props} ref={ref} />;
188 }
237 - }
189 + notImportant.displayName = 'OverriddenName';
190 + const DisplayNamedForwardRef = React.forwardRef(notImportant);
191
239 - let a;
240 - let b;
241 - ReactNoop.render(
242 - <Parent>
192 + ReactNoop.render(
193 <Parent>
194 + <AnonymousForwardRef />
195 + <NamedForwardRef />
196 + <DisplayNamedForwardRef />
197 + </Parent>,
198 + );
199 + addComment('Mount');
200 + expect(Scheduler).toFlushWithoutYielding();
201 +
202 + expect(getFlameChart()).toMatchSnapshot();
203 + });
204 +
205 + it('does not include ConcurrentMode, StrictMode, or Profiler components in measurements', () => {
206 + ReactNoop.render(
207 + <React.Profiler id="test" onRender={jest.fn()}>
208 + <React.StrictMode>
209 + <Parent>
210 + <React.unstable_ConcurrentMode>
211 + <Child />
212 + </React.unstable_ConcurrentMode>
213 + </Parent>
214 + </React.StrictMode>
215 + </React.Profiler>,
216 + );
217 + addComment('Mount');
218 + expect(Scheduler).toFlushWithoutYielding();
219 +
220 + expect(getFlameChart()).toMatchSnapshot();
221 + });
222 +
223 + it('does not include context provider or consumer in measurements', () => {
224 + const {Consumer, Provider} = React.createContext(true);
225 +
226 + ReactNoop.render(
227 + <Provider value={false}>
228 <Parent>
245 - <A ref={inst => (a = inst)} />
229 + <Consumer>{value => <Child value={value} />}</Consumer>
230 </Parent>
247 - </Parent>
248 - <Parent>
249 - <B ref={inst => (b = inst)} />
250 - </Parent>
251 - </Parent>,
252 - );
253 - expect(Scheduler).toFlushWithoutYielding();
254 - resetFlamechart();
255 -
256 - a.setState({});
257 - b.setState({});
258 - addComment('Should include just A and B, no Parents');
259 - expect(Scheduler).toFlushWithoutYielding();
260 - expect(getFlameChart()).toMatchSnapshot();
261 - });
231 + </Provider>,
232 + );
233 + addComment('Mount');
234 + expect(Scheduler).toFlushWithoutYielding();
235
263 - it('warns on cascading renders from setState', () => {
264 - class Cascading extends React.Component {
265 - componentDidMount() {
266 - this.setState({});
267 - }
268 - render() {
269 - return <div>{this.props.children}</div>;
236 + expect(getFlameChart()).toMatchSnapshot();
237 + });
238 +
239 + it('skips parents during setState', () => {
240 + class A extends React.Component {
241 + render() {
242 + return <div>{this.props.children}</div>;
243 + }
244 }
271 - }
272 -
273 - ReactNoop.render(
274 - <Parent>
275 - <Cascading />
276 - </Parent>,
277 - );
278 - addComment('Should print a warning');
279 - expect(Scheduler).toFlushWithoutYielding();
280 - expect(getFlameChart()).toMatchSnapshot();
281 - });
245
283 - it('warns on cascading renders from top-level render', () => {
284 - class Cascading extends React.Component {
285 - componentDidMount() {
286 - ReactNoop.renderToRootWithID(<Child />, 'b');
287 - addComment('Scheduling another root from componentDidMount');
246 + class B extends React.Component {
247 + render() {
248 + return <div>{this.props.children}</div>;
249 + }
250 }
289 - render() {
290 - return <div>{this.props.children}</div>;
251 +
252 + let a;
253 + let b;
254 + ReactNoop.render(
255 + <Parent>
256 + <Parent>
257 + <Parent>
258 + <A ref={inst => (a = inst)} />
259 + </Parent>
260 + </Parent>
261 + <Parent>
262 + <B ref={inst => (b = inst)} />
263 + </Parent>
264 + </Parent>,
265 + );
266 + expect(Scheduler).toFlushWithoutYielding();
267 + resetFlamechart();
268 +
269 + a.setState({});
270 + b.setState({});
271 + addComment('Should include just A and B, no Parents');
272 + expect(Scheduler).toFlushWithoutYielding();
273 + expect(getFlameChart()).toMatchSnapshot();
274 + });
275 +
276 + it('warns on cascading renders from setState', () => {
277 + class Cascading extends React.Component {
278 + componentDidMount() {
279 + this.setState({});
280 + }
281 + render() {
282 + return <div>{this.props.children}</div>;
283 + }
284 }
292 - }
285
294 - ReactNoop.renderToRootWithID(<Cascading />, 'a');
295 - addComment('Rendering the first root');
296 - expect(Scheduler).toFlushWithoutYielding();
297 - expect(getFlameChart()).toMatchSnapshot();
298 - });
286 + ReactNoop.render(
287 + <Parent>
288 + <Cascading />
289 + </Parent>,
290 + );
291 + addComment('Should print a warning');
292 + expect(Scheduler).toFlushWithoutYielding();
293 + expect(getFlameChart()).toMatchSnapshot();
294 + });
295
300 - it('does not treat setState from cWM or cWRP as cascading', () => {
301 - class NotCascading extends React.Component {
302 - UNSAFE_componentWillMount() {
303 - this.setState({});
296 + it('warns on cascading renders from top-level render', () => {
297 + class Cascading extends React.Component {
298 + componentDidMount() {
299 + ReactNoop.renderToRootWithID(<Child />, 'b');
300 + addComment('Scheduling another root from componentDidMount');
301 + }
302 + render() {
303 + return <div>{this.props.children}</div>;
304 + }
305 }
305 - UNSAFE_componentWillReceiveProps() {
306 - this.setState({});
306 +
307 + ReactNoop.renderToRootWithID(<Cascading />, 'a');
308 + addComment('Rendering the first root');
309 + expect(Scheduler).toFlushWithoutYielding();
310 + expect(getFlameChart()).toMatchSnapshot();
311 + });
312 +
313 + it('does not treat setState from cWM or cWRP as cascading', () => {
314 + class NotCascading extends React.Component {
315 + UNSAFE_componentWillMount() {
316 + this.setState({});
317 + }
318 + UNSAFE_componentWillReceiveProps() {
319 + this.setState({});
320 + }
321 + render() {
322 + return <div>{this.props.children}</div>;
323 + }
324 }
308 - render() {
309 - return <div>{this.props.children}</div>;
325 +
326 + ReactNoop.render(
327 + <Parent>
328 + <NotCascading />
329 + </Parent>,
330 + );
331 + addComment('Should not print a warning');
332 + expect(() => expect(Scheduler).toFlushWithoutYielding()).toWarnDev(
333 + [
334 + 'componentWillMount: Please update the following components ' +
335 + 'to use componentDidMount instead: NotCascading' +
336 + '\n\ncomponentWillReceiveProps: Please update the following components ' +
337 + 'to use static getDerivedStateFromProps instead: NotCascading',
338 + ],
339 + {withoutStack: true},
340 + );
341 + ReactNoop.render(
342 + <Parent>
343 + <NotCascading />
344 + </Parent>,
345 + );
346 + addComment('Should not print a warning');
347 + expect(Scheduler).toFlushWithoutYielding();
348 + expect(getFlameChart()).toMatchSnapshot();
349 + });
350 +
351 + it('captures all lifecycles', () => {
352 + class AllLifecycles extends React.Component {
353 + static childContextTypes = {
354 + foo: PropTypes.any,
355 + };
356 + shouldComponentUpdate() {
357 + return true;
358 + }
359 + getChildContext() {
360 + return {foo: 42};
361 + }
362 + UNSAFE_componentWillMount() {}
363 + componentDidMount() {}
364 + UNSAFE_componentWillReceiveProps() {}
365 + UNSAFE_componentWillUpdate() {}
366 + componentDidUpdate() {}
367 + componentWillUnmount() {}
368 + render() {
369 + return <div />;
370 + }
371 }
311 - }
312 -
313 - ReactNoop.render(
314 - <Parent>
315 - <NotCascading />
316 - </Parent>,
317 - );
318 - addComment('Should not print a warning');
319 - expect(() => expect(Scheduler).toFlushWithoutYielding()).toWarnDev(
320 - [
321 - 'componentWillMount: Please update the following components ' +
322 - 'to use componentDidMount instead: NotCascading' +
323 - '\n\ncomponentWillReceiveProps: Please update the following components ' +
324 - 'to use static getDerivedStateFromProps instead: NotCascading',
325 - ],
326 - {withoutStack: true},
327 - );
328 - ReactNoop.render(
329 - <Parent>
330 - <NotCascading />
331 - </Parent>,
332 - );
333 - addComment('Should not print a warning');
334 - expect(Scheduler).toFlushWithoutYielding();
335 - expect(getFlameChart()).toMatchSnapshot();
336 - });
372 + ReactNoop.render(<AllLifecycles />);
373 + addComment('Mount');
374 + expect(() => expect(Scheduler).toFlushWithoutYielding()).toWarnDev(
375 + [
376 + 'componentWillMount: Please update the following components ' +
377 + 'to use componentDidMount instead: AllLifecycles' +
378 + '\n\ncomponentWillReceiveProps: Please update the following components ' +
379 + 'to use static getDerivedStateFromProps instead: AllLifecycles' +
380 + '\n\ncomponentWillUpdate: Please update the following components ' +
381 + 'to use componentDidUpdate instead: AllLifecycles',
382 + 'Legacy context API has been detected within a strict-mode tree: \n\n' +
383 + 'Please update the following components: AllLifecycles',
384 + ],
385 + {withoutStack: true},
386 + );
387 + ReactNoop.render(<AllLifecycles />);
388 + addComment('Update');
389 + expect(Scheduler).toFlushWithoutYielding();
390 + ReactNoop.render(null);
391 + addComment('Unmount');
392 + expect(Scheduler).toFlushWithoutYielding();
393 + expect(getFlameChart()).toMatchSnapshot();
394 + });
395
338 - it('captures all lifecycles', () => {
339 - class AllLifecycles extends React.Component {
340 - static childContextTypes = {
341 - foo: PropTypes.any,
342 - };
343 - shouldComponentUpdate() {
344 - return true;
396 + it('measures deprioritized work', () => {
397 + addComment('Flush the parent');
398 + ReactNoop.flushSync(() => {
399 + ReactNoop.render(
400 + <Parent>
401 + <div hidden={true}>
402 + <Child />
403 + </div>
404 + </Parent>,
405 + );
406 + });
407 + addComment('Flush the child');
408 + expect(Scheduler).toFlushWithoutYielding();
409 + expect(getFlameChart()).toMatchSnapshot();
410 + });
411 +
412 + it('measures deferred work in chunks', () => {
413 + class A extends React.Component {
414 + render() {
415 + Scheduler.yieldValue('A');
416 + return <div>{this.props.children}</div>;
417 + }
418 }
346 - getChildContext() {
347 - return {foo: 42};
419 +
420 + class B extends React.Component {
421 + render() {
422 + Scheduler.yieldValue('B');
423 + return <div>{this.props.children}</div>;
424 + }
425 }
349 - UNSAFE_componentWillMount() {}
350 - componentDidMount() {}
351 - UNSAFE_componentWillReceiveProps() {}
352 - UNSAFE_componentWillUpdate() {}
353 - componentDidUpdate() {}
354 - componentWillUnmount() {}
355 - render() {
356 - return <div />;
426 +
427 + class C extends React.Component {
428 + render() {
429 + Scheduler.yieldValue('C');
430 + return <div>{this.props.children}</div>;
431 + }
432 }
358 - }
359 - ReactNoop.render(<AllLifecycles />);
360 - addComment('Mount');
361 - expect(() => expect(Scheduler).toFlushWithoutYielding()).toWarnDev(
362 - [
363 - 'componentWillMount: Please update the following components ' +
364 - 'to use componentDidMount instead: AllLifecycles' +
365 - '\n\ncomponentWillReceiveProps: Please update the following components ' +
366 - 'to use static getDerivedStateFromProps instead: AllLifecycles' +
367 - '\n\ncomponentWillUpdate: Please update the following components ' +
368 - 'to use componentDidUpdate instead: AllLifecycles',
369 - 'Legacy context API has been detected within a strict-mode tree: \n\n' +
370 - 'Please update the following components: AllLifecycles',
371 - ],
372 - {withoutStack: true},
373 - );
374 - ReactNoop.render(<AllLifecycles />);
375 - addComment('Update');
376 - expect(Scheduler).toFlushWithoutYielding();
377 - ReactNoop.render(null);
378 - addComment('Unmount');
379 - expect(Scheduler).toFlushWithoutYielding();
380 - expect(getFlameChart()).toMatchSnapshot();
381 - });
433
383 - it('measures deprioritized work', () => {
384 - addComment('Flush the parent');
385 - ReactNoop.flushSync(() => {
434 ReactNoop.render(
435 <Parent>
388 - <div hidden={true}>
436 + <A>
437 + <Child />
438 + </A>
439 + <B>
440 + <Child />
441 + </B>
442 + <C>
443 <Child />
390 - </div>
444 + </C>
445 </Parent>,
446 );
447 + addComment('Start rendering through B');
448 + expect(Scheduler).toFlushAndYieldThrough(['A', 'B']);
449 + addComment('Complete the rest');
450 + expect(Scheduler).toFlushAndYield(['C']);
451 + expect(getFlameChart()).toMatchSnapshot();
452 });
394 - addComment('Flush the child');
395 - expect(Scheduler).toFlushWithoutYielding();
396 - expect(getFlameChart()).toMatchSnapshot();
397 - });
398 -
399 - it('measures deferred work in chunks', () => {
400 - class A extends React.Component {
401 - render() {
402 - Scheduler.yieldValue('A');
403 - return <div>{this.props.children}</div>;
404 - }
405 - }
453
407 - class B extends React.Component {
408 - render() {
409 - Scheduler.yieldValue('B');
410 - return <div>{this.props.children}</div>;
454 + it('recovers from fatal errors', () => {
455 + function Baddie() {
456 + throw new Error('Game over');
457 }
412 - }
458
414 - class C extends React.Component {
415 - render() {
416 - Scheduler.yieldValue('C');
417 - return <div>{this.props.children}</div>;
459 + ReactNoop.render(
460 + <Parent>
461 + <Baddie />
462 + </Parent>,
463 + );
464 + try {
465 + addComment('Will fatal');
466 + expect(Scheduler).toFlushWithoutYielding();
467 + } catch (err) {
468 + expect(err.message).toBe('Game over');
469 }
419 - }
420 -
421 - ReactNoop.render(
422 - <Parent>
423 - <A>
424 - <Child />
425 - </A>
426 - <B>
427 - <Child />
428 - </B>
429 - <C>
470 + ReactNoop.render(
471 + <Parent>
472 <Child />
431 - </C>
432 - </Parent>,
433 - );
434 - addComment('Start rendering through B');
435 - expect(Scheduler).toFlushAndYieldThrough(['A', 'B']);
436 - addComment('Complete the rest');
437 - expect(Scheduler).toFlushAndYield(['C']);
438 - expect(getFlameChart()).toMatchSnapshot();
439 - });
440 -
441 - it('recovers from fatal errors', () => {
442 - function Baddie() {
443 - throw new Error('Game over');
444 - }
445 -
446 - ReactNoop.render(
447 - <Parent>
448 - <Baddie />
449 - </Parent>,
450 - );
451 - try {
452 - addComment('Will fatal');
473 + </Parent>,
474 + );
475 + addComment('Will reconcile from a clean state');
476 expect(Scheduler).toFlushWithoutYielding();
454 - } catch (err) {
455 - expect(err.message).toBe('Game over');
456 - }
457 - ReactNoop.render(
458 - <Parent>
459 - <Child />
460 - </Parent>,
461 - );
462 - addComment('Will reconcile from a clean state');
463 - expect(Scheduler).toFlushWithoutYielding();
464 - expect(getFlameChart()).toMatchSnapshot();
465 - });
466 -
467 - it('recovers from caught errors', () => {
468 - function Baddie() {
469 - throw new Error('Game over');
470 - }
477 + expect(getFlameChart()).toMatchSnapshot();
478 + });
479
472 - function ErrorReport() {
473 - return <div />;
474 - }
480 + it('recovers from caught errors', () => {
481 + function Baddie() {
482 + throw new Error('Game over');
483 + }
484
476 - class Boundary extends React.Component {
477 - state = {error: null};
478 - componentDidCatch(error) {
479 - this.setState({error});
485 + function ErrorReport() {
486 + return <div />;
487 }
481 - render() {
482 - if (this.state.error) {
483 - return <ErrorReport />;
488 +
489 + class Boundary extends React.Component {
490 + state = {error: null};
491 + componentDidCatch(error) {
492 + this.setState({error});
493 + }
494 + render() {
495 + if (this.state.error) {
496 + return <ErrorReport />;
497 + }
498 + return this.props.children;
499 }
485 - return this.props.children;
500 }
487 - }
501
489 - ReactNoop.render(
490 - <Parent>
491 - <Boundary>
492 - <Parent>
493 - <Baddie />
494 - </Parent>
495 - </Boundary>
496 - </Parent>,
497 - );
498 - addComment('Stop on Baddie and restart from Boundary');
499 - expect(Scheduler).toFlushWithoutYielding();
500 - expect(getFlameChart()).toMatchSnapshot();
501 - });
502 + ReactNoop.render(
503 + <Parent>
504 + <Boundary>
505 + <Parent>
506 + <Baddie />
507 + </Parent>
508 + </Boundary>
509 + </Parent>,
510 + );
511 + addComment('Stop on Baddie and restart from Boundary');
512 + expect(Scheduler).toFlushWithoutYielding();
513 + expect(getFlameChart()).toMatchSnapshot();
514 + });
515
503 - it('deduplicates lifecycle names during commit to reduce overhead', () => {
504 - class A extends React.Component {
505 - componentDidUpdate() {}
506 - render() {
507 - return <div />;
516 + it('deduplicates lifecycle names during commit to reduce overhead', () => {
517 + class A extends React.Component {
518 + componentDidUpdate() {}
519 + render() {
520 + return <div />;
521 + }
522 }
509 - }
523
511 - class B extends React.Component {
512 - componentDidUpdate(prevProps) {
513 - if (this.props.cascade && !prevProps.cascade) {
514 - this.setState({});
524 + class B extends React.Component {
525 + componentDidUpdate(prevProps) {
526 + if (this.props.cascade && !prevProps.cascade) {
527 + this.setState({});
528 + }
529 + }
530 + render() {
531 + return <div />;
532 }
533 }
517 - render() {
518 - return <div />;
519 - }
520 - }
521 -
522 - ReactNoop.render(
523 - <Parent>
524 - <A />
525 - <B />
526 - <A />
527 - <B />
528 - </Parent>,
529 - );
530 - expect(Scheduler).toFlushWithoutYielding();
531 - resetFlamechart();
532 -
533 - ReactNoop.render(
534 - <Parent>
535 - <A />
536 - <B />
537 - <A />
538 - <B />
539 - </Parent>,
540 - );
541 - addComment('The commit phase should mention A and B just once');
542 - expect(Scheduler).toFlushWithoutYielding();
543 - ReactNoop.render(
544 - <Parent>
545 - <A />
546 - <B />
547 - <A />
548 - <B cascade={true} />
549 - </Parent>,
550 - );
551 - addComment("Because of deduplication, we don't know B was cascading,");
552 - addComment('but we should still see the warning for the commit phase.');
553 - expect(Scheduler).toFlushWithoutYielding();
554 - expect(getFlameChart()).toMatchSnapshot();
555 - });
534
557 - it('supports portals', () => {
558 - const portalContainer = ReactNoop.getOrCreateRootContainer(
559 - 'portalContainer',
560 - );
561 - ReactNoop.render(
562 - <Parent>
563 - {ReactNoop.createPortal(<Child />, portalContainer, null)}
564 - </Parent>,
565 - );
566 - expect(Scheduler).toFlushWithoutYielding();
567 - expect(getFlameChart()).toMatchSnapshot();
568 - });
535 + ReactNoop.render(
536 + <Parent>
537 + <A />
538 + <B />
539 + <A />
540 + <B />
541 + </Parent>,
542 + );
543 + expect(Scheduler).toFlushWithoutYielding();
544 + resetFlamechart();
545 +
546 + ReactNoop.render(
547 + <Parent>
548 + <A />
549 + <B />
550 + <A />
551 + <B />
552 + </Parent>,
553 + );
554 + addComment('The commit phase should mention A and B just once');
555 + expect(Scheduler).toFlushWithoutYielding();
556 + ReactNoop.render(
557 + <Parent>
558 + <A />
559 + <B />
560 + <A />
561 + <B cascade={true} />
562 + </Parent>,
563 + );
564 + addComment("Because of deduplication, we don't know B was cascading,");
565 + addComment('but we should still see the warning for the commit phase.');
566 + expect(Scheduler).toFlushWithoutYielding();
567 + expect(getFlameChart()).toMatchSnapshot();
568 + });
569
570 - it('supports memo', () => {
571 - const MemoFoo = React.memo(function Foo() {
572 - return <div />;
570 + it('supports portals', () => {
571 + const portalContainer = ReactNoop.getOrCreateRootContainer(
572 + 'portalContainer',
573 + );
574 + ReactNoop.render(
575 + <Parent>
576 + {ReactNoop.createPortal(<Child />, portalContainer, null)}
577 + </Parent>,
578 + );
579 + expect(Scheduler).toFlushWithoutYielding();
580 + expect(getFlameChart()).toMatchSnapshot();
581 });
574 - ReactNoop.render(
575 - <Parent>
576 - <MemoFoo />
577 - </Parent>,
578 - );
579 - expect(Scheduler).toFlushWithoutYielding();
580 - expect(getFlameChart()).toMatchSnapshot();
581 - });
582
583 - it('supports Suspense and lazy', async () => {
584 - function Spinner() {
585 - return <span />;
586 - }
583 + it('supports memo', () => {
584 + const MemoFoo = React.memo(function Foo() {
585 + return <div />;
586 + });
587 + ReactNoop.render(
588 + <Parent>
589 + <MemoFoo />
590 + </Parent>,
591 + );
592 + expect(Scheduler).toFlushWithoutYielding();
593 + expect(getFlameChart()).toMatchSnapshot();
594 + });
595
588 - function fakeImport(result) {
589 - return {default: result};
590 - }
596 + it('supports Suspense and lazy', async () => {
597 + function Spinner() {
598 + return <span />;
599 + }
600 +
601 + function fakeImport(result) {
602 + return {default: result};
603 + }
604
592 - let resolve;
593 - const LazyFoo = React.lazy(
594 - () =>
595 - new Promise(r => {
596 - resolve = r;
605 + let resolve;
606 + const LazyFoo = React.lazy(
607 + () =>
608 + new Promise(r => {
609 + resolve = r;
610 + }),
611 + );
612 +
613 + ReactNoop.render(
614 + <Parent>
615 + <React.Suspense fallback={<Spinner />}>
616 + <LazyFoo />
617 + </React.Suspense>
618 + </Parent>,
619 + );
620 + expect(Scheduler).toFlushWithoutYielding();
621 + expect(getFlameChart()).toMatchSnapshot();
622 +
623 + resolve(
624 + fakeImport(function Foo() {
625 + return <div />;
626 }),
598 - );
599 -
600 - ReactNoop.render(
601 - <Parent>
602 - <React.Suspense fallback={<Spinner />}>
603 - <LazyFoo />
604 - </React.Suspense>
605 - </Parent>,
606 - );
607 - expect(Scheduler).toFlushWithoutYielding();
608 - expect(getFlameChart()).toMatchSnapshot();
609 -
610 - resolve(
611 - fakeImport(function Foo() {
612 - return <div />;
613 - }),
614 - );
615 -
616 - await Promise.resolve();
617 -
618 - ReactNoop.render(
619 - <Parent>
620 - <React.Suspense>
621 - <LazyFoo />
622 - </React.Suspense>
623 - </Parent>,
624 - );
625 - expect(Scheduler).toFlushWithoutYielding();
626 - expect(getFlameChart()).toMatchSnapshot();
627 - });
627 + );
628
629 - it('does not schedule an extra callback if setState is called during a synchronous commit phase', () => {
630 - class Component extends React.Component {
631 - state = {step: 1};
632 - componentDidMount() {
633 - this.setState({step: 2});
634 - }
635 - render() {
636 - return <span prop={this.state.step} />;
629 + await Promise.resolve();
630 +
631 + ReactNoop.render(
632 + <Parent>
633 + <React.Suspense>
634 + <LazyFoo />
635 + </React.Suspense>
636 + </Parent>,
637 + );
638 + expect(Scheduler).toFlushWithoutYielding();
639 + expect(getFlameChart()).toMatchSnapshot();
640 + });
641 +
642 + it('does not schedule an extra callback if setState is called during a synchronous commit phase', () => {
643 + class Component extends React.Component {
644 + state = {step: 1};
645 + componentDidMount() {
646 + this.setState({step: 2});
647 + }
648 + render() {
649 + return <span prop={this.state.step} />;
650 + }
651 }
638 - }
639 - ReactNoop.flushSync(() => {
640 - ReactNoop.render(<Component />);
652 + ReactNoop.flushSync(() => {
653 + ReactNoop.render(<Component />);
654 + });
655 + expect(getFlameChart()).toMatchSnapshot();
656 });
642 - expect(getFlameChart()).toMatchSnapshot();
643 - });
657
645 - it('warns if an in-progress update is interrupted', () => {
646 - function Foo() {
647 - Scheduler.yieldValue('Foo');
648 - return <span />;
649 - }
658 + it('warns if an in-progress update is interrupted', () => {
659 + function Foo() {
660 + Scheduler.yieldValue('Foo');
661 + return <span />;
662 + }
663
651 - ReactNoop.render(<Foo />);
652 - ReactNoop.flushNextYield();
653 - ReactNoop.flushSync(() => {
664 ReactNoop.render(<Foo />);
665 + ReactNoop.flushNextYield();
666 + ReactNoop.flushSync(() => {
667 + ReactNoop.render(<Foo />);
668 + });
669 + expect(Scheduler).toHaveYielded(['Foo']);
670 + expect(Scheduler).toFlushWithoutYielding();
671 + expect(getFlameChart()).toMatchSnapshot();
672 });
656 - expect(Scheduler).toHaveYielded(['Foo']);
657 - expect(Scheduler).toFlushWithoutYielding();
658 - expect(getFlameChart()).toMatchSnapshot();
659 - });
673
661 - it('warns if async work expires (starvation)', () => {
662 - function Foo() {
663 - return <span />;
664 - }
674 + it('warns if async work expires (starvation)', () => {
675 + function Foo() {
676 + return <span />;
677 + }
678
666 - ReactNoop.render(<Foo />);
667 - ReactNoop.expire(6000);
668 - expect(Scheduler).toFlushWithoutYielding();
669 - expect(getFlameChart()).toMatchSnapshot();
670 - });
679 + ReactNoop.render(<Foo />);
680 + ReactNoop.expire(6000);
681 + expect(Scheduler).toFlushWithoutYielding();
682 + expect(getFlameChart()).toMatchSnapshot();
683 + });
684 + }
685 });
packages/react-reconciler/src/__tests__/ReactLazy-test.internal.js
+24 -8
@@ -5,6 +5,7 @@ let Scheduler;
5 let ReactFeatureFlags;
6 let Suspense;
7 let lazy;
8 +let enableNewScheduler;
9
10 describe('ReactLazy', () => {
11 beforeEach(() => {
@@ -18,6 +19,7 @@ describe('ReactLazy', () => {
19 lazy = React.lazy;
20 ReactTestRenderer = require('react-test-renderer');
21 Scheduler = require('scheduler');
22 + enableNewScheduler = ReactFeatureFlags.enableNewScheduler;
23 });
24
25 function Text(props) {
@@ -485,19 +487,33 @@ describe('ReactLazy', () => {
487
488 await Promise.resolve();
489
490 + if (enableNewScheduler) {
491 + // The new scheduler pings in a separate task
492 + expect(Scheduler).toHaveYielded([]);
493 + } else {
494 + // The old scheduler pings synchronously
495 + expect(Scheduler).toHaveYielded(['UNSAFE_componentWillMount: A', 'A1']);
496 + }
497 +
498 root.update(
499 <Suspense fallback={<Text text="Loading..." />}>
500 <LazyClass num={2} />
501 </Suspense>,
502 );
493 - expect(Scheduler).toHaveYielded([
494 - 'UNSAFE_componentWillMount: A',
495 - 'A1',
496 - 'UNSAFE_componentWillReceiveProps: A -> A',
497 - 'UNSAFE_componentWillUpdate: A -> A',
498 - 'A2',
499 - ]);
500 - expect(Scheduler).toFlushAndYield([]);
503 +
504 + if (enableNewScheduler) {
505 + // Because this ping happens in a new task, the ping and the update
506 + // are batched together
507 + expect(Scheduler).toHaveYielded(['UNSAFE_componentWillMount: A', 'A2']);
508 + } else {
509 + // The old scheduler must do two separate renders, no batching.
510 + expect(Scheduler).toHaveYielded([
511 + 'UNSAFE_componentWillReceiveProps: A -> A',
512 + 'UNSAFE_componentWillUpdate: A -> A',
513 + 'A2',
514 + ]);
515 + }
516 +
517 expect(root).toMatchRenderedOutput('A2');
518
519 root.update(
packages/react-reconciler/src/__tests__/ReactSchedulerIntegration-test.internal.js new
+156
@@ -0,0 +1,156 @@
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 + * @emails react-core
8 + * @jest-environment node
9 + */
10 +
11 +'use strict';
12 +
13 +let React;
14 +let ReactFeatureFlags;
15 +let ReactNoop;
16 +let Scheduler;
17 +let ImmediatePriority;
18 +let UserBlockingPriority;
19 +let NormalPriority;
20 +let LowPriority;
21 +let IdlePriority;
22 +let runWithPriority;
23 +
24 +describe('ReactSchedulerIntegration', () => {
25 + beforeEach(() => {
26 + jest.resetModules();
27 + ReactFeatureFlags = require('shared/ReactFeatureFlags');
28 + ReactFeatureFlags.debugRenderPhaseSideEffectsForStrictMode = false;
29 + ReactFeatureFlags.enableNewScheduler = true;
30 + React = require('react');
31 + ReactNoop = require('react-noop-renderer');
32 + Scheduler = require('scheduler');
33 + ImmediatePriority = Scheduler.unstable_ImmediatePriority;
34 + UserBlockingPriority = Scheduler.unstable_UserBlockingPriority;
35 + NormalPriority = Scheduler.unstable_NormalPriority;
36 + LowPriority = Scheduler.unstable_LowPriority;
37 + IdlePriority = Scheduler.unstable_IdlePriority;
38 + runWithPriority = Scheduler.unstable_runWithPriority;
39 + });
40 +
41 + function getCurrentPriorityAsString() {
42 + const priorityLevel = Scheduler.unstable_getCurrentPriorityLevel();
43 + switch (priorityLevel) {
44 + case ImmediatePriority:
45 + return 'Immediate';
46 + case UserBlockingPriority:
47 + return 'UserBlocking';
48 + case NormalPriority:
49 + return 'Normal';
50 + case LowPriority:
51 + return 'Low';
52 + case IdlePriority:
53 + return 'Idle';
54 + default:
55 + throw Error('Unknown priority level: ' + priorityLevel);
56 + }
57 + }
58 +
59 + it('has correct priority during rendering', () => {
60 + function ReadPriority() {
61 + Scheduler.yieldValue('Priority: ' + getCurrentPriorityAsString());
62 + return null;
63 + }
64 + ReactNoop.render(<ReadPriority />);
65 + expect(Scheduler).toFlushAndYield(['Priority: Normal']);
66 +
67 + runWithPriority(UserBlockingPriority, () => {
68 + ReactNoop.render(<ReadPriority />);
69 + });
70 + expect(Scheduler).toFlushAndYield(['Priority: UserBlocking']);
71 +
72 + runWithPriority(IdlePriority, () => {
73 + ReactNoop.render(<ReadPriority />);
74 + });
75 + expect(Scheduler).toFlushAndYield(['Priority: Idle']);
76 + });
77 +
78 + it('has correct priority when continuing a render after yielding', () => {
79 + function ReadPriority() {
80 + Scheduler.yieldValue('Priority: ' + getCurrentPriorityAsString());
81 + return null;
82 + }
83 +
84 + runWithPriority(UserBlockingPriority, () => {
85 + ReactNoop.render(
86 + <React.Fragment>
87 + <ReadPriority />
88 + <ReadPriority />
89 + <ReadPriority />
90 + </React.Fragment>,
91 + );
92 + });
93 +
94 + // Render part of the tree
95 + expect(Scheduler).toFlushAndYieldThrough(['Priority: UserBlocking']);
96 +
97 + // Priority is set back to normal when yielding
98 + expect(getCurrentPriorityAsString()).toEqual('Normal');
99 +
100 + // Priority is restored to user-blocking when continuing
101 + expect(Scheduler).toFlushAndYield([
102 + 'Priority: UserBlocking',
103 + 'Priority: UserBlocking',
104 + ]);
105 + });
106 +
107 + it('layout effects have immediate priority', () => {
108 + const {useLayoutEffect} = React;
109 + function ReadPriority() {
110 + Scheduler.yieldValue('Render priority: ' + getCurrentPriorityAsString());
111 + useLayoutEffect(() => {
112 + Scheduler.yieldValue(
113 + 'Layout priority: ' + getCurrentPriorityAsString(),
114 + );
115 + });
116 + return null;
117 + }
118 +
119 + ReactNoop.render(<ReadPriority />);
120 + expect(Scheduler).toFlushAndYield([
121 + 'Render priority: Normal',
122 + 'Layout priority: Immediate',
123 + ]);
124 + });
125 +
126 + it('passive effects have the same priority as render', () => {
127 + const {useEffect} = React;
128 + function ReadPriority() {
129 + Scheduler.yieldValue('Render priority: ' + getCurrentPriorityAsString());
130 + useEffect(() => {
131 + Scheduler.yieldValue(
132 + 'Passive priority: ' + getCurrentPriorityAsString(),
133 + );
134 + });
135 + return null;
136 + }
137 +
138 + ReactNoop.render(<ReadPriority />);
139 + expect(Scheduler).toFlushAndYield([
140 + 'Render priority: Normal',
141 + 'Passive priority: Normal',
142 + ]);
143 +
144 + runWithPriority(UserBlockingPriority, () => {
145 + ReactNoop.render(<ReadPriority />);
146 + });
147 +
148 + expect(Scheduler).toFlushAndYield([
149 + 'Render priority: UserBlocking',
150 + 'Passive priority: UserBlocking',
151 + ]);
152 + });
153 +
154 + // TODO
155 + it.skip('passive effects have render priority even if they are flushed early', () => {});
156 +});
packages/react-reconciler/src/__tests__/ReactSuspense-test.internal.js
+208 -63
@@ -5,6 +5,7 @@ let Scheduler;
5 let ReactCache;
6 let Suspense;
7 let act;
8 +let enableNewScheduler;
9
10 let TextResource;
11 let textResourceShouldFail;
@@ -22,6 +23,7 @@ describe('ReactSuspense', () => {
23 act = ReactTestRenderer.act;
24 Scheduler = require('scheduler');
25 ReactCache = require('react-cache');
26 + enableNewScheduler = ReactFeatureFlags.enableNewScheduler;
27
28 Suspense = React.Suspense;
29
@@ -265,7 +267,11 @@ describe('ReactSuspense', () => {
267
268 await LazyClass;
269
268 - expect(Scheduler).toHaveYielded(['Hi', 'Did mount: Hi']);
270 + if (enableNewScheduler) {
271 + expect(Scheduler).toFlushExpired(['Hi', 'Did mount: Hi']);
272 + } else {
273 + expect(Scheduler).toHaveYielded(['Hi', 'Did mount: Hi']);
274 + }
275 expect(root).toMatchRenderedOutput('Hi');
276 });
277
@@ -393,13 +399,24 @@ describe('ReactSuspense', () => {
399 expect(root).toMatchRenderedOutput('Loading...');
400
401 jest.advanceTimersByTime(100);
396 - expect(Scheduler).toHaveYielded([
397 - 'Promise resolved [B:1]',
398 - 'B:1',
399 - 'Unmount [Loading...]',
400 - // Should be a mount, not an update
401 - 'Mount [B:1]',
402 - ]);
402 +
403 + if (enableNewScheduler) {
404 + expect(Scheduler).toHaveYielded(['Promise resolved [B:1]']);
405 + expect(Scheduler).toFlushExpired([
406 + 'B:1',
407 + 'Unmount [Loading...]',
408 + // Should be a mount, not an update
409 + 'Mount [B:1]',
410 + ]);
411 + } else {
412 + expect(Scheduler).toHaveYielded([
413 + 'Promise resolved [B:1]',
414 + 'B:1',
415 + 'Unmount [Loading...]',
416 + // Should be a mount, not an update
417 + 'Mount [B:1]',
418 + ]);
419 + }
420
421 expect(root).toMatchRenderedOutput('AB:1C');
422
@@ -413,12 +430,21 @@ describe('ReactSuspense', () => {
430
431 jest.advanceTimersByTime(100);
432
416 - expect(Scheduler).toHaveYielded([
417 - 'Promise resolved [B:2]',
418 - 'B:2',
419 - 'Unmount [Loading...]',
420 - 'Update [B:2]',
421 - ]);
433 + if (enableNewScheduler) {
434 + expect(Scheduler).toHaveYielded(['Promise resolved [B:2]']);
435 + expect(Scheduler).toFlushExpired([
436 + 'B:2',
437 + 'Unmount [Loading...]',
438 + 'Update [B:2]',
439 + ]);
440 + } else {
441 + expect(Scheduler).toHaveYielded([
442 + 'Promise resolved [B:2]',
443 + 'B:2',
444 + 'Unmount [Loading...]',
445 + 'Update [B:2]',
446 + ]);
447 + }
448 expect(root).toMatchRenderedOutput('AB:2C');
449 });
450
@@ -450,7 +476,14 @@ describe('ReactSuspense', () => {
476 ]);
477
478 jest.advanceTimersByTime(1000);
453 - expect(Scheduler).toHaveYielded(['Promise resolved [A]', 'A']);
479 +
480 + if (enableNewScheduler) {
481 + expect(Scheduler).toHaveYielded(['Promise resolved [A]']);
482 + expect(Scheduler).toFlushExpired(['A']);
483 + } else {
484 + expect(Scheduler).toHaveYielded(['Promise resolved [A]', 'A']);
485 + }
486 +
487 expect(root).toMatchRenderedOutput('Stateful: 1A');
488
489 root.update(<App text="B" />);
@@ -466,7 +499,14 @@ describe('ReactSuspense', () => {
499 expect(root).toMatchRenderedOutput('Loading...');
500
501 jest.advanceTimersByTime(1000);
469 - expect(Scheduler).toHaveYielded(['Promise resolved [B]', 'B']);
502 +
503 + if (enableNewScheduler) {
504 + expect(Scheduler).toHaveYielded(['Promise resolved [B]']);
505 + expect(Scheduler).toFlushExpired(['B']);
506 + } else {
507 + expect(Scheduler).toHaveYielded(['Promise resolved [B]', 'B']);
508 + }
509 +
510 expect(root).toMatchRenderedOutput('Stateful: 2B');
511 });
512
@@ -506,7 +546,13 @@ describe('ReactSuspense', () => {
546 ]);
547
548 jest.advanceTimersByTime(1000);
509 - expect(Scheduler).toHaveYielded(['Promise resolved [A]', 'A']);
549 +
550 + if (enableNewScheduler) {
551 + expect(Scheduler).toHaveYielded(['Promise resolved [A]']);
552 + expect(Scheduler).toFlushExpired(['A']);
553 + } else {
554 + expect(Scheduler).toHaveYielded(['Promise resolved [A]', 'A']);
555 + }
556 expect(root).toMatchRenderedOutput('Stateful: 1A');
557
558 root.update(<App text="B" />);
@@ -529,7 +575,14 @@ describe('ReactSuspense', () => {
575 expect(root).toMatchRenderedOutput('Loading...');
576
577 jest.advanceTimersByTime(1000);
532 - expect(Scheduler).toHaveYielded(['Promise resolved [B]', 'B']);
578 +
579 + if (enableNewScheduler) {
580 + expect(Scheduler).toHaveYielded(['Promise resolved [B]']);
581 + expect(Scheduler).toFlushExpired(['B']);
582 + } else {
583 + expect(Scheduler).toHaveYielded(['Promise resolved [B]', 'B']);
584 + }
585 +
586 expect(root).toMatchRenderedOutput('Stateful: 2B');
587 });
588
@@ -610,11 +663,17 @@ describe('ReactSuspense', () => {
663 ReactTestRenderer.create(<App text="A" />);
664 expect(Scheduler).toHaveYielded(['Suspend! [A]', 'Loading...']);
665 jest.advanceTimersByTime(500);
613 - expect(Scheduler).toHaveYielded([
614 - 'Promise resolved [A]',
615 - 'A',
616 - 'Did commit: A',
617 - ]);
666 +
667 + if (enableNewScheduler) {
668 + expect(Scheduler).toHaveYielded(['Promise resolved [A]']);
669 + expect(Scheduler).toFlushExpired(['A', 'Did commit: A']);
670 + } else {
671 + expect(Scheduler).toHaveYielded([
672 + 'Promise resolved [A]',
673 + 'A',
674 + 'Did commit: A',
675 + ]);
676 + }
677 });
678
679 it('retries when an update is scheduled on a timed out tree', () => {
@@ -698,23 +757,42 @@ describe('ReactSuspense', () => {
757 ]);
758 expect(Scheduler).toFlushAndYield([]);
759 jest.advanceTimersByTime(1000);
701 - expect(Scheduler).toHaveYielded([
702 - 'Promise resolved [Child 1]',
703 - 'Child 1',
704 - 'Suspend! [Child 2]',
705 - 'Suspend! [Child 3]',
706 - ]);
760 + if (enableNewScheduler) {
761 + expect(Scheduler).toHaveYielded(['Promise resolved [Child 1]']);
762 + expect(Scheduler).toFlushExpired([
763 + 'Child 1',
764 + 'Suspend! [Child 2]',
765 + 'Suspend! [Child 3]',
766 + ]);
767 + } else {
768 + expect(Scheduler).toHaveYielded([
769 + 'Promise resolved [Child 1]',
770 + 'Child 1',
771 + 'Suspend! [Child 2]',
772 + 'Suspend! [Child 3]',
773 + ]);
774 + }
775 jest.advanceTimersByTime(1000);
708 - expect(Scheduler).toHaveYielded([
709 - 'Promise resolved [Child 2]',
710 - 'Child 2',
711 - 'Suspend! [Child 3]',
712 - ]);
776 + if (enableNewScheduler) {
777 + expect(Scheduler).toHaveYielded(['Promise resolved [Child 2]']);
778 + expect(Scheduler).toFlushExpired(['Child 2', 'Suspend! [Child 3]']);
779 + } else {
780 + expect(Scheduler).toHaveYielded([
781 + 'Promise resolved [Child 2]',
782 + 'Child 2',
783 + 'Suspend! [Child 3]',
784 + ]);
785 + }
786 jest.advanceTimersByTime(1000);
714 - expect(Scheduler).toHaveYielded([
715 - 'Promise resolved [Child 3]',
716 - 'Child 3',
717 - ]);
787 + if (enableNewScheduler) {
788 + expect(Scheduler).toHaveYielded(['Promise resolved [Child 3]']);
789 + expect(Scheduler).toFlushExpired(['Child 3']);
790 + } else {
791 + expect(Scheduler).toHaveYielded([
792 + 'Promise resolved [Child 3]',
793 + 'Child 3',
794 + ]);
795 + }
796 expect(root).toMatchRenderedOutput(
797 ['Child 1', 'Child 2', 'Child 3'].join(''),
798 );
@@ -773,7 +851,16 @@ describe('ReactSuspense', () => {
851 ]);
852 expect(root).toMatchRenderedOutput('Loading...');
853 jest.advanceTimersByTime(1000);
776 - expect(Scheduler).toHaveYielded(['Promise resolved [Tab: 0]', 'Tab: 0']);
854 +
855 + if (enableNewScheduler) {
856 + expect(Scheduler).toHaveYielded(['Promise resolved [Tab: 0]']);
857 + expect(Scheduler).toFlushExpired(['Tab: 0']);
858 + } else {
859 + expect(Scheduler).toHaveYielded([
860 + 'Promise resolved [Tab: 0]',
861 + 'Tab: 0',
862 + ]);
863 + }
864 expect(root).toMatchRenderedOutput('Tab: 0 + sibling');
865
866 act(() => setTab(1));
@@ -784,7 +871,17 @@ describe('ReactSuspense', () => {
871 ]);
872 expect(root).toMatchRenderedOutput('Loading...');
873 jest.advanceTimersByTime(1000);
787 - expect(Scheduler).toHaveYielded(['Promise resolved [Tab: 1]', 'Tab: 1']);
874 +
875 + if (enableNewScheduler) {
876 + expect(Scheduler).toHaveYielded(['Promise resolved [Tab: 1]']);
877 + expect(Scheduler).toFlushExpired(['Tab: 1']);
878 + } else {
879 + expect(Scheduler).toHaveYielded([
880 + 'Promise resolved [Tab: 1]',
881 + 'Tab: 1',
882 + ]);
883 + }
884 +
885 expect(root).toMatchRenderedOutput('Tab: 1 + sibling');
886
887 act(() => setTab(2));
@@ -795,7 +892,17 @@ describe('ReactSuspense', () => {
892 ]);
893 expect(root).toMatchRenderedOutput('Loading...');
894 jest.advanceTimersByTime(1000);
798 - expect(Scheduler).toHaveYielded(['Promise resolved [Tab: 2]', 'Tab: 2']);
895 +
896 + if (enableNewScheduler) {
897 + expect(Scheduler).toHaveYielded(['Promise resolved [Tab: 2]']);
898 + expect(Scheduler).toFlushExpired(['Tab: 2']);
899 + } else {
900 + expect(Scheduler).toHaveYielded([
901 + 'Promise resolved [Tab: 2]',
902 + 'Tab: 2',
903 + ]);
904 + }
905 +
906 expect(root).toMatchRenderedOutput('Tab: 2 + sibling');
907 });
908
@@ -831,7 +938,14 @@ describe('ReactSuspense', () => {
938
939 expect(Scheduler).toHaveYielded(['Suspend! [A:0]', 'Loading...']);
940 jest.advanceTimersByTime(1000);
834 - expect(Scheduler).toHaveYielded(['Promise resolved [A:0]', 'A:0']);
941 +
942 + if (enableNewScheduler) {
943 + expect(Scheduler).toHaveYielded(['Promise resolved [A:0]']);
944 + expect(Scheduler).toFlushExpired(['A:0']);
945 + } else {
946 + expect(Scheduler).toHaveYielded(['Promise resolved [A:0]', 'A:0']);
947 + }
948 +
949 expect(root).toMatchRenderedOutput('A:0');
950
951 act(() => setStep(1));
@@ -868,34 +982,65 @@ describe('ReactSuspense', () => {
982 // Resolve A
983 jest.advanceTimersByTime(1000);
984
871 - expect(Scheduler).toHaveYielded([
872 - 'Promise resolved [A]',
873 - 'A',
874 - // The promises for B and C have now been thrown twice
875 - 'Suspend! [B]',
876 - 'Suspend! [C]',
877 - ]);
985 + if (enableNewScheduler) {
986 + expect(Scheduler).toHaveYielded(['Promise resolved [A]']);
987 + expect(Scheduler).toFlushExpired([
988 + 'A',
989 + // The promises for B and C have now been thrown twice
990 + 'Suspend! [B]',
991 + 'Suspend! [C]',
992 + ]);
993 + } else {
994 + expect(Scheduler).toHaveYielded([
995 + 'Promise resolved [A]',
996 + 'A',
997 + // The promises for B and C have now been thrown twice
998 + 'Suspend! [B]',
999 + 'Suspend! [C]',
1000 + ]);
1001 + }
1002
1003 // Resolve B
1004 jest.advanceTimersByTime(1000);
1005
882 - expect(Scheduler).toHaveYielded([
883 - 'Promise resolved [B]',
884 - // Even though the promise for B was thrown twice, we should only
885 - // re-render once.
886 - 'B',
887 - // The promise for C has now been thrown three times
888 - 'Suspend! [C]',
889 - ]);
1006 + if (enableNewScheduler) {
1007 + expect(Scheduler).toHaveYielded(['Promise resolved [B]']);
1008 + expect(Scheduler).toFlushExpired([
1009 + // Even though the promise for B was thrown twice, we should only
1010 + // re-render once.
1011 + 'B',
1012 + // The promise for C has now been thrown three times
1013 + 'Suspend! [C]',
1014 + ]);
1015 + } else {
1016 + expect(Scheduler).toHaveYielded([
1017 + 'Promise resolved [B]',
1018 + // Even though the promise for B was thrown twice, we should only
1019 + // re-render once.
1020 + 'B',
1021 + // The promise for C has now been thrown three times
1022 + 'Suspend! [C]',
1023 + ]);
1024 + }
1025
1026 // Resolve C
1027 jest.advanceTimersByTime(1000);
893 - expect(Scheduler).toHaveYielded([
894 - 'Promise resolved [C]',
895 - // Even though the promise for C was thrown three times, we should only
896 - // re-render once.
897 - 'C',
898 - ]);
1028 +
1029 + if (enableNewScheduler) {
1030 + expect(Scheduler).toHaveYielded(['Promise resolved [C]']);
1031 + expect(Scheduler).toFlushExpired([
1032 + // Even though the promise for C was thrown three times, we should only
1033 + // re-render once.
1034 + 'C',
1035 + ]);
1036 + } else {
1037 + expect(Scheduler).toHaveYielded([
1038 + 'Promise resolved [C]',
1039 + // Even though the promise for C was thrown three times, we should only
1040 + // re-render once.
1041 + 'C',
1042 + ]);
1043 + }
1044 });
1045
1046 it('#14162', () => {
packages/react-reconciler/src/__tests__/ReactSuspensePlaceholder-test.internal.js
+22 -8
@@ -17,6 +17,7 @@ let ReactCache;
17 let Suspense;
18 let TextResource;
19 let textResourceShouldFail;
20 +let enableNewScheduler;
21
22 describe('ReactSuspensePlaceholder', () => {
23 beforeEach(() => {
@@ -30,6 +31,7 @@ describe('ReactSuspensePlaceholder', () => {
31 ReactNoop = require('react-noop-renderer');
32 Scheduler = require('scheduler');
33 ReactCache = require('react-cache');
34 + enableNewScheduler = ReactFeatureFlags.enableNewScheduler;
35
36 Profiler = React.Profiler;
37 Suspense = React.Suspense;
@@ -323,10 +325,16 @@ describe('ReactSuspensePlaceholder', () => {
325
326 jest.advanceTimersByTime(1000);
327
326 - expect(Scheduler).toHaveYielded([
327 - 'Promise resolved [Loaded]',
328 - 'Loaded',
329 - ]);
328 + if (enableNewScheduler) {
329 + expect(Scheduler).toHaveYielded(['Promise resolved [Loaded]']);
330 + expect(Scheduler).toFlushExpired(['Loaded']);
331 + } else {
332 + expect(Scheduler).toHaveYielded([
333 + 'Promise resolved [Loaded]',
334 + 'Loaded',
335 + ]);
336 + }
337 +
338 expect(ReactNoop).toMatchRenderedOutput('LoadedText');
339 expect(onRender).toHaveBeenCalledTimes(2);
340
@@ -426,10 +434,16 @@ describe('ReactSuspensePlaceholder', () => {
434
435 jest.advanceTimersByTime(1000);
436
429 - expect(Scheduler).toHaveYielded([
430 - 'Promise resolved [Loaded]',
431 - 'Loaded',
432 - ]);
437 + if (enableNewScheduler) {
438 + expect(Scheduler).toHaveYielded(['Promise resolved [Loaded]']);
439 + expect(Scheduler).toFlushExpired(['Loaded']);
440 + } else {
441 + expect(Scheduler).toHaveYielded([
442 + 'Promise resolved [Loaded]',
443 + 'Loaded',
444 + ]);
445 + }
446 +
447 expect(ReactNoop).toMatchRenderedOutput('LoadedNew');
448 expect(onRender).toHaveBeenCalledTimes(4);
449
packages/react-reconciler/src/__tests__/ReactSuspenseWithNoopRenderer-test.internal.js
+158 -56
@@ -7,6 +7,7 @@ let ReactCache;
7 let Suspense;
8 let StrictMode;
9 let ConcurrentMode;
10 +let enableNewScheduler;
11
12 let TextResource;
13 let textResourceShouldFail;
@@ -28,6 +29,7 @@ describe('ReactSuspenseWithNoopRenderer', () => {
29 Suspense = React.Suspense;
30 StrictMode = React.StrictMode;
31 ConcurrentMode = React.unstable_ConcurrentMode;
32 + enableNewScheduler = ReactFeatureFlags.enableNewScheduler;
33
34 TextResource = ReactCache.unstable_createResource(([text, ms = 0]) => {
35 return new Promise((resolve, reject) =>
@@ -842,7 +844,16 @@ describe('ReactSuspenseWithNoopRenderer', () => {
844
845 ReactNoop.expire(100);
846 await advanceTimers(100);
845 - expect(Scheduler).toHaveYielded(['Promise resolved [Result]', 'Result']);
847 +
848 + if (enableNewScheduler) {
849 + expect(Scheduler).toHaveYielded(['Promise resolved [Result]']);
850 + expect(Scheduler).toFlushExpired(['Result']);
851 + } else {
852 + expect(Scheduler).toHaveYielded([
853 + 'Promise resolved [Result]',
854 + 'Result',
855 + ]);
856 + }
857
858 expect(ReactNoop.getChildren()).toEqual([span('Result')]);
859 });
@@ -880,15 +891,27 @@ describe('ReactSuspenseWithNoopRenderer', () => {
891 // Initial mount. This is synchronous, because the root is sync.
892 ReactNoop.renderLegacySyncRoot(<App />);
893 await advanceTimers(100);
883 - expect(Scheduler).toHaveYielded([
884 - 'Suspend! [Step: 1]',
885 - 'Sibling',
886 - 'Loading (1)',
887 - 'Loading (2)',
888 - 'Loading (3)',
889 - 'Promise resolved [Step: 1]',
890 - 'Step: 1',
891 - ]);
894 + if (enableNewScheduler) {
895 + expect(Scheduler).toHaveYielded([
896 + 'Suspend! [Step: 1]',
897 + 'Sibling',
898 + 'Loading (1)',
899 + 'Loading (2)',
900 + 'Loading (3)',
901 + 'Promise resolved [Step: 1]',
902 + ]);
903 + expect(Scheduler).toFlushExpired(['Step: 1']);
904 + } else {
905 + expect(Scheduler).toHaveYielded([
906 + 'Suspend! [Step: 1]',
907 + 'Sibling',
908 + 'Loading (1)',
909 + 'Loading (2)',
910 + 'Loading (3)',
911 + 'Promise resolved [Step: 1]',
912 + 'Step: 1',
913 + ]);
914 + }
915 expect(ReactNoop).toMatchRenderedOutput(
916 <React.Fragment>
917 <span prop="Step: 1" />
@@ -920,10 +943,15 @@ describe('ReactSuspenseWithNoopRenderer', () => {
943 );
944
945 await advanceTimers(100);
923 - expect(Scheduler).toHaveYielded([
924 - 'Promise resolved [Step: 2]',
925 - 'Step: 2',
926 - ]);
946 + if (enableNewScheduler) {
947 + expect(Scheduler).toHaveYielded(['Promise resolved [Step: 2]']);
948 + expect(Scheduler).toFlushExpired(['Step: 2']);
949 + } else {
950 + expect(Scheduler).toHaveYielded([
951 + 'Promise resolved [Step: 2]',
952 + 'Step: 2',
953 + ]);
954 + }
955 expect(ReactNoop).toMatchRenderedOutput(
956 <React.Fragment>
957 <span prop="Step: 2" />
@@ -981,18 +1009,34 @@ describe('ReactSuspenseWithNoopRenderer', () => {
1009 Scheduler.yieldValue('Did mount'),
1010 );
1011 await advanceTimers(100);
984 - expect(Scheduler).toHaveYielded([
985 - 'Before',
986 - 'Suspend! [Async: 1]',
987 - 'After',
988 - 'Loading...',
989 - 'Before',
990 - 'Sync: 1',
991 - 'After',
992 - 'Did mount',
993 - 'Promise resolved [Async: 1]',
994 - 'Async: 1',
995 - ]);
1012 +
1013 + if (enableNewScheduler) {
1014 + expect(Scheduler).toHaveYielded([
1015 + 'Before',
1016 + 'Suspend! [Async: 1]',
1017 + 'After',
1018 + 'Loading...',
1019 + 'Before',
1020 + 'Sync: 1',
1021 + 'After',
1022 + 'Did mount',
1023 + 'Promise resolved [Async: 1]',
1024 + ]);
1025 + expect(Scheduler).toFlushExpired(['Async: 1']);
1026 + } else {
1027 + expect(Scheduler).toHaveYielded([
1028 + 'Before',
1029 + 'Suspend! [Async: 1]',
1030 + 'After',
1031 + 'Loading...',
1032 + 'Before',
1033 + 'Sync: 1',
1034 + 'After',
1035 + 'Did mount',
1036 + 'Promise resolved [Async: 1]',
1037 + 'Async: 1',
1038 + ]);
1039 + }
1040 expect(ReactNoop).toMatchRenderedOutput(
1041 <React.Fragment>
1042 <span prop="Before" />
@@ -1046,10 +1090,16 @@ describe('ReactSuspenseWithNoopRenderer', () => {
1090 // When the placeholder is pinged, the boundary must be re-rendered
1091 // synchronously.
1092 await advanceTimers(100);
1049 - expect(Scheduler).toHaveYielded([
1050 - 'Promise resolved [Async: 2]',
1051 - 'Async: 2',
1052 - ]);
1093 +
1094 + if (enableNewScheduler) {
1095 + expect(Scheduler).toHaveYielded(['Promise resolved [Async: 2]']);
1096 + expect(Scheduler).toFlushExpired(['Async: 2']);
1097 + } else {
1098 + expect(Scheduler).toHaveYielded([
1099 + 'Promise resolved [Async: 2]',
1100 + 'Async: 2',
1101 + ]);
1102 + }
1103
1104 expect(ReactNoop).toMatchRenderedOutput(
1105 <React.Fragment>
@@ -1114,18 +1164,33 @@ describe('ReactSuspenseWithNoopRenderer', () => {
1164 Scheduler.yieldValue('Did mount'),
1165 );
1166 await advanceTimers(100);
1117 - expect(Scheduler).toHaveYielded([
1118 - 'Before',
1119 - 'Suspend! [Async: 1]',
1120 - 'After',
1121 - 'Loading...',
1122 - 'Before',
1123 - 'Sync: 1',
1124 - 'After',
1125 - 'Did mount',
1126 - 'Promise resolved [Async: 1]',
1127 - 'Async: 1',
1128 - ]);
1167 + if (enableNewScheduler) {
1168 + expect(Scheduler).toHaveYielded([
1169 + 'Before',
1170 + 'Suspend! [Async: 1]',
1171 + 'After',
1172 + 'Loading...',
1173 + 'Before',
1174 + 'Sync: 1',
1175 + 'After',
1176 + 'Did mount',
1177 + 'Promise resolved [Async: 1]',
1178 + ]);
1179 + expect(Scheduler).toFlushExpired(['Async: 1']);
1180 + } else {
1181 + expect(Scheduler).toHaveYielded([
1182 + 'Before',
1183 + 'Suspend! [Async: 1]',
1184 + 'After',
1185 + 'Loading...',
1186 + 'Before',
1187 + 'Sync: 1',
1188 + 'After',
1189 + 'Did mount',
1190 + 'Promise resolved [Async: 1]',
1191 + 'Async: 1',
1192 + ]);
1193 + }
1194 expect(ReactNoop).toMatchRenderedOutput(
1195 <React.Fragment>
1196 <span prop="Before" />
@@ -1179,10 +1244,16 @@ describe('ReactSuspenseWithNoopRenderer', () => {
1244 // When the placeholder is pinged, the boundary must be re-rendered
1245 // synchronously.
1246 await advanceTimers(100);
1182 - expect(Scheduler).toHaveYielded([
1183 - 'Promise resolved [Async: 2]',
1184 - 'Async: 2',
1185 - ]);
1247 +
1248 + if (enableNewScheduler) {
1249 + expect(Scheduler).toHaveYielded(['Promise resolved [Async: 2]']);
1250 + expect(Scheduler).toFlushExpired(['Async: 2']);
1251 + } else {
1252 + expect(Scheduler).toHaveYielded([
1253 + 'Promise resolved [Async: 2]',
1254 + 'Async: 2',
1255 + ]);
1256 + }
1257
1258 expect(ReactNoop).toMatchRenderedOutput(
1259 <React.Fragment>
@@ -1260,7 +1331,13 @@ describe('ReactSuspenseWithNoopRenderer', () => {
1331
1332 ReactNoop.expire(1000);
1333 await advanceTimers(1000);
1263 - expect(Scheduler).toHaveYielded(['Promise resolved [B]', 'B']);
1334 +
1335 + if (enableNewScheduler) {
1336 + expect(Scheduler).toHaveYielded(['Promise resolved [B]']);
1337 + expect(Scheduler).toFlushExpired(['B']);
1338 + } else {
1339 + expect(Scheduler).toHaveYielded(['Promise resolved [B]', 'B']);
1340 + }
1341
1342 expect(ReactNoop).toMatchRenderedOutput(
1343 <React.Fragment>
@@ -1312,12 +1389,22 @@ describe('ReactSuspenseWithNoopRenderer', () => {
1389 expect(ReactNoop.getChildren()).toEqual([span('Loading...')]);
1390
1391 await advanceTimers(1000);
1315 - expect(Scheduler).toHaveYielded([
1316 - 'Promise resolved [Hi]',
1317 - 'constructor',
1318 - 'Hi',
1319 - 'componentDidMount',
1320 - ]);
1392 +
1393 + if (enableNewScheduler) {
1394 + expect(Scheduler).toHaveYielded(['Promise resolved [Hi]']);
1395 + expect(Scheduler).toFlushExpired([
1396 + 'constructor',
1397 + 'Hi',
1398 + 'componentDidMount',
1399 + ]);
1400 + } else {
1401 + expect(Scheduler).toHaveYielded([
1402 + 'Promise resolved [Hi]',
1403 + 'constructor',
1404 + 'Hi',
1405 + 'componentDidMount',
1406 + ]);
1407 + }
1408 expect(ReactNoop.getChildren()).toEqual([span('Hi')]);
1409 });
1410
@@ -1356,7 +1443,12 @@ describe('ReactSuspenseWithNoopRenderer', () => {
1443 ]);
1444 expect(ReactNoop.getChildren()).toEqual([span('Loading...')]);
1445 await advanceTimers(100);
1359 - expect(Scheduler).toHaveYielded(['Promise resolved [Hi]', 'Hi']);
1446 + if (enableNewScheduler) {
1447 + expect(Scheduler).toHaveYielded(['Promise resolved [Hi]']);
1448 + expect(Scheduler).toFlushExpired(['Hi']);
1449 + } else {
1450 + expect(Scheduler).toHaveYielded(['Promise resolved [Hi]', 'Hi']);
1451 + }
1452 expect(ReactNoop.getChildren()).toEqual([span('Hi')]);
1453 });
1454
@@ -1400,7 +1492,12 @@ describe('ReactSuspenseWithNoopRenderer', () => {
1492
1493 await advanceTimers(1000);
1494
1403 - expect(Scheduler).toHaveYielded(['Promise resolved [Hi]', 'Hi']);
1495 + if (enableNewScheduler) {
1496 + expect(Scheduler).toHaveYielded(['Promise resolved [Hi]']);
1497 + expect(Scheduler).toFlushExpired(['Hi']);
1498 + } else {
1499 + expect(Scheduler).toHaveYielded(['Promise resolved [Hi]', 'Hi']);
1500 + }
1501 });
1502 } else {
1503 it('hides/unhides suspended children before layout effects fire (mutation)', async () => {
@@ -1439,7 +1536,12 @@ describe('ReactSuspenseWithNoopRenderer', () => {
1536
1537 await advanceTimers(1000);
1538
1442 - expect(Scheduler).toHaveYielded(['Promise resolved [Hi]', 'Hi']);
1539 + if (enableNewScheduler) {
1540 + expect(Scheduler).toHaveYielded(['Promise resolved [Hi]']);
1541 + expect(Scheduler).toFlushExpired(['Hi']);
1542 + } else {
1543 + expect(Scheduler).toHaveYielded(['Promise resolved [Hi]', 'Hi']);
1544 + }
1545 });
1546 }
1547 });
packages/react-reconciler/src/__tests__/__snapshots__/ReactIncrementalPerf-test.internal.js.snap
+522 -21
@@ -1,6 +1,6 @@
1 // Jest Snapshot v1, https://goo.gl/fbAQLP
2
3 -exports[`ReactDebugFiberPerf captures all lifecycles 1`] = `
3 +exports[`ReactDebugFiberPerf new scheduler captures all lifecycles 1`] = `
4 "⚛ (Waiting for async callback... will force flush in 5250 ms)
5
6 // Mount
@@ -44,7 +44,7 @@ exports[`ReactDebugFiberPerf captures all lifecycles 1`] = `
44 "
45 `;
46
47 -exports[`ReactDebugFiberPerf deduplicates lifecycle names during commit to reduce overhead 1`] = `
47 +exports[`ReactDebugFiberPerf new scheduler deduplicates lifecycle names during commit to reduce overhead 1`] = `
48 "⚛ (Waiting for async callback... will force flush in 5250 ms)
49
50 // The commit phase should mention A and B just once
@@ -91,7 +91,7 @@ exports[`ReactDebugFiberPerf deduplicates lifecycle names during commit to reduc
91 "
92 `;
93
94 -exports[`ReactDebugFiberPerf does not include ConcurrentMode, StrictMode, or Profiler components in measurements 1`] = `
94 +exports[`ReactDebugFiberPerf new scheduler does not include ConcurrentMode, StrictMode, or Profiler components in measurements 1`] = `
95 "⚛ (Waiting for async callback... will force flush in 5250 ms)
96
97 // Mount
@@ -107,7 +107,7 @@ exports[`ReactDebugFiberPerf does not include ConcurrentMode, StrictMode, or Pro
107 "
108 `;
109
110 -exports[`ReactDebugFiberPerf does not include context provider or consumer in measurements 1`] = `
110 +exports[`ReactDebugFiberPerf new scheduler does not include context provider or consumer in measurements 1`] = `
111 "⚛ (Waiting for async callback... will force flush in 5250 ms)
112
113 // Mount
@@ -122,7 +122,7 @@ exports[`ReactDebugFiberPerf does not include context provider or consumer in me
122 "
123 `;
124
125 -exports[`ReactDebugFiberPerf does not schedule an extra callback if setState is called during a synchronous commit phase 1`] = `
125 +exports[`ReactDebugFiberPerf new scheduler does not schedule an extra callback if setState is called during a synchronous commit phase 1`] = `
126 "⚛ (React Tree Reconciliation: Completed Root)
127 ⚛ Component [mount]
128
@@ -142,7 +142,7 @@ exports[`ReactDebugFiberPerf does not schedule an extra callback if setState is
142 "
143 `;
144
145 -exports[`ReactDebugFiberPerf does not treat setState from cWM or cWRP as cascading 1`] = `
145 +exports[`ReactDebugFiberPerf new scheduler does not treat setState from cWM or cWRP as cascading 1`] = `
146 "⚛ (Waiting for async callback... will force flush in 5250 ms)
147
148 // Should not print a warning
@@ -171,7 +171,7 @@ exports[`ReactDebugFiberPerf does not treat setState from cWM or cWRP as cascadi
171 "
172 `;
173
174 -exports[`ReactDebugFiberPerf measures a simple reconciliation 1`] = `
174 +exports[`ReactDebugFiberPerf new scheduler measures a simple reconciliation 1`] = `
175 "⚛ (Waiting for async callback... will force flush in 5250 ms)
176
177 // Mount
@@ -208,7 +208,7 @@ exports[`ReactDebugFiberPerf measures a simple reconciliation 1`] = `
208 "
209 `;
210
211 -exports[`ReactDebugFiberPerf measures deferred work in chunks 1`] = `
211 +exports[`ReactDebugFiberPerf new scheduler measures deferred work in chunks 1`] = `
212 "⚛ (Waiting for async callback... will force flush in 5250 ms)
213
214 // Start rendering through B
@@ -235,7 +235,7 @@ exports[`ReactDebugFiberPerf measures deferred work in chunks 1`] = `
235 "
236 `;
237
238 -exports[`ReactDebugFiberPerf measures deprioritized work 1`] = `
238 +exports[`ReactDebugFiberPerf new scheduler measures deprioritized work 1`] = `
239 "// Flush the parent
240 ⚛ (React Tree Reconciliation: Completed Root)
241 ⚛ Parent [mount]
@@ -258,7 +258,7 @@ exports[`ReactDebugFiberPerf measures deprioritized work 1`] = `
258 "
259 `;
260
261 -exports[`ReactDebugFiberPerf properly displays the forwardRef component in measurements 1`] = `
261 +exports[`ReactDebugFiberPerf new scheduler properly displays the forwardRef component in measurements 1`] = `
262 "⚛ (Waiting for async callback... will force flush in 5250 ms)
263
264 // Mount
@@ -278,7 +278,7 @@ exports[`ReactDebugFiberPerf properly displays the forwardRef component in measu
278 "
279 `;
280
281 -exports[`ReactDebugFiberPerf recovers from caught errors 1`] = `
281 +exports[`ReactDebugFiberPerf new scheduler recovers from caught errors 1`] = `
282 "⚛ (Waiting for async callback... will force flush in 5250 ms)
283
284 // Stop on Baddie and restart from Boundary
@@ -312,7 +312,7 @@ exports[`ReactDebugFiberPerf recovers from caught errors 1`] = `
312 "
313 `;
314
315 -exports[`ReactDebugFiberPerf recovers from fatal errors 1`] = `
315 +exports[`ReactDebugFiberPerf new scheduler recovers from fatal errors 1`] = `
316 "⚛ (Waiting for async callback... will force flush in 5250 ms)
317
318 // Will fatal
@@ -343,7 +343,7 @@ exports[`ReactDebugFiberPerf recovers from fatal errors 1`] = `
343 "
344 `;
345
346 -exports[`ReactDebugFiberPerf skips parents during setState 1`] = `
346 +exports[`ReactDebugFiberPerf new scheduler skips parents during setState 1`] = `
347 "⚛ (Waiting for async callback... will force flush in 5250 ms)
348
349 // Should include just A and B, no Parents
@@ -358,7 +358,7 @@ exports[`ReactDebugFiberPerf skips parents during setState 1`] = `
358 "
359 `;
360
361 -exports[`ReactDebugFiberPerf supports Suspense and lazy 1`] = `
361 +exports[`ReactDebugFiberPerf new scheduler supports Suspense and lazy 1`] = `
362 "⚛ (Waiting for async callback... will force flush in 5250 ms)
363
364 ⚛ (React Tree Reconciliation: Completed Root)
@@ -369,7 +369,7 @@ exports[`ReactDebugFiberPerf supports Suspense and lazy 1`] = `
369 "
370 `;
371
372 -exports[`ReactDebugFiberPerf supports Suspense and lazy 2`] = `
372 +exports[`ReactDebugFiberPerf new scheduler supports Suspense and lazy 2`] = `
373 "⚛ (Waiting for async callback... will force flush in 5250 ms)
374
375 ⚛ (React Tree Reconciliation: Completed Root)
@@ -392,7 +392,7 @@ exports[`ReactDebugFiberPerf supports Suspense and lazy 2`] = `
392 "
393 `;
394
395 -exports[`ReactDebugFiberPerf supports memo 1`] = `
395 +exports[`ReactDebugFiberPerf new scheduler supports memo 1`] = `
396 "⚛ (Waiting for async callback... will force flush in 5250 ms)
397
398 ⚛ (React Tree Reconciliation: Completed Root)
@@ -406,7 +406,7 @@ exports[`ReactDebugFiberPerf supports memo 1`] = `
406 "
407 `;
408
409 -exports[`ReactDebugFiberPerf supports portals 1`] = `
409 +exports[`ReactDebugFiberPerf new scheduler supports portals 1`] = `
410 "⚛ (Waiting for async callback... will force flush in 5250 ms)
411
412 ⚛ (React Tree Reconciliation: Completed Root)
@@ -420,7 +420,7 @@ exports[`ReactDebugFiberPerf supports portals 1`] = `
420 "
421 `;
422
423 -exports[`ReactDebugFiberPerf warns if an in-progress update is interrupted 1`] = `
423 +exports[`ReactDebugFiberPerf new scheduler warns if an in-progress update is interrupted 1`] = `
424 "⚛ (Waiting for async callback... will force flush in 5250 ms)
425
426 ⚛ (React Tree Reconciliation: Yielded)
@@ -443,7 +443,508 @@ exports[`ReactDebugFiberPerf warns if an in-progress update is interrupted 1`] =
443 "
444 `;
445
446 -exports[`ReactDebugFiberPerf warns if async work expires (starvation) 1`] = `
446 +exports[`ReactDebugFiberPerf new scheduler warns if async work expires (starvation) 1`] = `
447 +"⛔ (Waiting for async callback... will force flush in 5250 ms) Warning: React was blocked by main thread
448 +
449 +⚛ (Committing Changes)
450 + ⚛ (Committing Snapshot Effects: 0 Total)
451 + ⚛ (Committing Host Effects: 1 Total)
452 + ⚛ (Calling Lifecycle Methods: 0 Total)
453 +"
454 +`;
455 +
456 +exports[`ReactDebugFiberPerf new scheduler warns on cascading renders from setState 1`] = `
457 +"⚛ (Waiting for async callback... will force flush in 5250 ms)
458 +
459 +// Should print a warning
460 +⚛ (React Tree Reconciliation: Completed Root)
461 + ⚛ Parent [mount]
462 + ⚛ Cascading [mount]
463 +
464 +⛔ (Committing Changes) Warning: Lifecycle hook scheduled a cascading update
465 + ⚛ (Committing Snapshot Effects: 0 Total)
466 + ⚛ (Committing Host Effects: 2 Total)
467 + ⚛ (Calling Lifecycle Methods: 1 Total)
468 + ⛔ Cascading.componentDidMount Warning: Scheduled a cascading update
469 +
470 +⚛ (React Tree Reconciliation: Completed Root)
471 + ⚛ Cascading [update]
472 +
473 +⚛ (Committing Changes)
474 + ⚛ (Committing Snapshot Effects: 0 Total)
475 + ⚛ (Committing Host Effects: 1 Total)
476 + ⚛ (Calling Lifecycle Methods: 1 Total)
477 +"
478 +`;
479 +
480 +exports[`ReactDebugFiberPerf new scheduler warns on cascading renders from top-level render 1`] = `
481 +"⚛ (Waiting for async callback... will force flush in 5250 ms)
482 +
483 +// Rendering the first root
484 +⚛ (React Tree Reconciliation: Completed Root)
485 + ⚛ Cascading [mount]
486 +
487 +⛔ (Committing Changes) Warning: Lifecycle hook scheduled a cascading update
488 + ⚛ (Committing Snapshot Effects: 0 Total)
489 + ⚛ (Committing Host Effects: 1 Total)
490 + ⚛ (Calling Lifecycle Methods: 1 Total)
491 + ⛔ Cascading.componentDidMount Warning: Scheduled a cascading update
492 +
493 +// Scheduling another root from componentDidMount
494 +⚛ (React Tree Reconciliation: Completed Root)
495 + ⚛ Child [mount]
496 +
497 +⚛ (Committing Changes)
498 + ⚛ (Committing Snapshot Effects: 0 Total)
499 + ⚛ (Committing Host Effects: 1 Total)
500 + ⚛ (Calling Lifecycle Methods: 0 Total)
501 +"
502 +`;
503 +
504 +exports[`ReactDebugFiberPerf old scheduler captures all lifecycles 1`] = `
505 +"⚛ (Waiting for async callback... will force flush in 5250 ms)
506 +
507 +// Mount
508 +⚛ (React Tree Reconciliation: Completed Root)
509 + ⚛ AllLifecycles [mount]
510 + ⚛ AllLifecycles.componentWillMount
511 + ⚛ AllLifecycles.getChildContext
512 +
513 +⚛ (Committing Changes)
514 + ⚛ (Committing Snapshot Effects: 0 Total)
515 + ⚛ (Committing Host Effects: 1 Total)
516 + ⚛ (Calling Lifecycle Methods: 1 Total)
517 + ⚛ AllLifecycles.componentDidMount
518 +
519 +⚛ (Waiting for async callback... will force flush in 5250 ms)
520 +
521 +// Update
522 +⚛ (React Tree Reconciliation: Completed Root)
523 + ⚛ AllLifecycles [update]
524 + ⚛ AllLifecycles.componentWillReceiveProps
525 + ⚛ AllLifecycles.shouldComponentUpdate
526 + ⚛ AllLifecycles.componentWillUpdate
527 + ⚛ AllLifecycles.getChildContext
528 +
529 +⚛ (Committing Changes)
530 + ⚛ (Committing Snapshot Effects: 0 Total)
531 + ⚛ (Committing Host Effects: 2 Total)
532 + ⚛ (Calling Lifecycle Methods: 2 Total)
533 + ⚛ AllLifecycles.componentDidUpdate
534 +
535 +⚛ (Waiting for async callback... will force flush in 5250 ms)
536 +
537 +// Unmount
538 +⚛ (React Tree Reconciliation: Completed Root)
539 +
540 +⚛ (Committing Changes)
541 + ⚛ (Committing Snapshot Effects: 0 Total)
542 + ⚛ (Committing Host Effects: 1 Total)
543 + ⚛ AllLifecycles.componentWillUnmount
544 + ⚛ (Calling Lifecycle Methods: 0 Total)
545 +"
546 +`;
547 +
548 +exports[`ReactDebugFiberPerf old scheduler deduplicates lifecycle names during commit to reduce overhead 1`] = `
549 +"⚛ (Waiting for async callback... will force flush in 5250 ms)
550 +
551 +// The commit phase should mention A and B just once
552 +⚛ (React Tree Reconciliation: Completed Root)
553 + ⚛ Parent [update]
554 + ⚛ A [update]
555 + ⚛ B [update]
556 + ⚛ A [update]
557 + ⚛ B [update]
558 +
559 +⚛ (Committing Changes)
560 + ⚛ (Committing Snapshot Effects: 0 Total)
561 + ⚛ (Committing Host Effects: 9 Total)
562 + ⚛ (Calling Lifecycle Methods: 9 Total)
563 + ⚛ A.componentDidUpdate
564 + ⚛ B.componentDidUpdate
565 +
566 +⚛ (Waiting for async callback... will force flush in 5250 ms)
567 +
568 +// Because of deduplication, we don't know B was cascading,
569 +// but we should still see the warning for the commit phase.
570 +⚛ (React Tree Reconciliation: Completed Root)
571 + ⚛ Parent [update]
572 + ⚛ A [update]
573 + ⚛ B [update]
574 + ⚛ A [update]
575 + ⚛ B [update]
576 +
577 +⛔ (Committing Changes) Warning: Lifecycle hook scheduled a cascading update
578 + ⚛ (Committing Snapshot Effects: 0 Total)
579 + ⚛ (Committing Host Effects: 9 Total)
580 + ⚛ (Calling Lifecycle Methods: 9 Total)
581 + ⚛ A.componentDidUpdate
582 + ⚛ B.componentDidUpdate
583 +
584 +⚛ (React Tree Reconciliation: Completed Root)
585 + ⚛ B [update]
586 +
587 +⚛ (Committing Changes)
588 + ⚛ (Committing Snapshot Effects: 0 Total)
589 + ⚛ (Committing Host Effects: 2 Total)
590 + ⚛ (Calling Lifecycle Methods: 2 Total)
591 + ⚛ B.componentDidUpdate
592 +"
593 +`;
594 +
595 +exports[`ReactDebugFiberPerf old scheduler does not include ConcurrentMode, StrictMode, or Profiler components in measurements 1`] = `
596 +"⚛ (Waiting for async callback... will force flush in 5250 ms)
597 +
598 +// Mount
599 +⚛ (React Tree Reconciliation: Completed Root)
600 + ⚛ Profiler [mount]
601 + ⚛ Parent [mount]
602 + ⚛ Child [mount]
603 +
604 +⚛ (Committing Changes)
605 + ⚛ (Committing Snapshot Effects: 0 Total)
606 + ⚛ (Committing Host Effects: 1 Total)
607 + ⚛ (Calling Lifecycle Methods: 0 Total)
608 +"
609 +`;
610 +
611 +exports[`ReactDebugFiberPerf old scheduler does not include context provider or consumer in measurements 1`] = `
612 +"⚛ (Waiting for async callback... will force flush in 5250 ms)
613 +
614 +// Mount
615 +⚛ (React Tree Reconciliation: Completed Root)
616 + ⚛ Parent [mount]
617 + ⚛ Child [mount]
618 +
619 +⚛ (Committing Changes)
620 + ⚛ (Committing Snapshot Effects: 0 Total)
621 + ⚛ (Committing Host Effects: 1 Total)
622 + ⚛ (Calling Lifecycle Methods: 0 Total)
623 +"
624 +`;
625 +
626 +exports[`ReactDebugFiberPerf old scheduler does not schedule an extra callback if setState is called during a synchronous commit phase 1`] = `
627 +"⚛ (React Tree Reconciliation: Completed Root)
628 + ⚛ Component [mount]
629 +
630 +⛔ (Committing Changes) Warning: Lifecycle hook scheduled a cascading update
631 + ⚛ (Committing Snapshot Effects: 0 Total)
632 + ⚛ (Committing Host Effects: 1 Total)
633 + ⚛ (Calling Lifecycle Methods: 1 Total)
634 + ⛔ Component.componentDidMount Warning: Scheduled a cascading update
635 +
636 +⚛ (React Tree Reconciliation: Completed Root)
637 + ⚛ Component [update]
638 +
639 +⚛ (Committing Changes)
640 + ⚛ (Committing Snapshot Effects: 0 Total)
641 + ⚛ (Committing Host Effects: 1 Total)
642 + ⚛ (Calling Lifecycle Methods: 1 Total)
643 +"
644 +`;
645 +
646 +exports[`ReactDebugFiberPerf old scheduler does not treat setState from cWM or cWRP as cascading 1`] = `
647 +"⚛ (Waiting for async callback... will force flush in 5250 ms)
648 +
649 +// Should not print a warning
650 +⚛ (React Tree Reconciliation: Completed Root)
651 + ⚛ Parent [mount]
652 + ⚛ NotCascading [mount]
653 + ⚛ NotCascading.componentWillMount
654 +
655 +⚛ (Committing Changes)
656 + ⚛ (Committing Snapshot Effects: 0 Total)
657 + ⚛ (Committing Host Effects: 1 Total)
658 + ⚛ (Calling Lifecycle Methods: 0 Total)
659 +
660 +⚛ (Waiting for async callback... will force flush in 5250 ms)
661 +
662 +// Should not print a warning
663 +⚛ (React Tree Reconciliation: Completed Root)
664 + ⚛ Parent [update]
665 + ⚛ NotCascading [update]
666 + ⚛ NotCascading.componentWillReceiveProps
667 +
668 +⚛ (Committing Changes)
669 + ⚛ (Committing Snapshot Effects: 0 Total)
670 + ⚛ (Committing Host Effects: 2 Total)
671 + ⚛ (Calling Lifecycle Methods: 2 Total)
672 +"
673 +`;
674 +
675 +exports[`ReactDebugFiberPerf old scheduler measures a simple reconciliation 1`] = `
676 +"⚛ (Waiting for async callback... will force flush in 5250 ms)
677 +
678 +// Mount
679 +⚛ (React Tree Reconciliation: Completed Root)
680 + ⚛ Parent [mount]
681 + ⚛ Child [mount]
682 +
683 +⚛ (Committing Changes)
684 + ⚛ (Committing Snapshot Effects: 0 Total)
685 + ⚛ (Committing Host Effects: 1 Total)
686 + ⚛ (Calling Lifecycle Methods: 0 Total)
687 +
688 +⚛ (Waiting for async callback... will force flush in 5250 ms)
689 +
690 +// Update
691 +⚛ (React Tree Reconciliation: Completed Root)
692 + ⚛ Parent [update]
693 + ⚛ Child [update]
694 +
695 +⚛ (Committing Changes)
696 + ⚛ (Committing Snapshot Effects: 0 Total)
697 + ⚛ (Committing Host Effects: 2 Total)
698 + ⚛ (Calling Lifecycle Methods: 2 Total)
699 +
700 +⚛ (Waiting for async callback... will force flush in 5250 ms)
701 +
702 +// Unmount
703 +⚛ (React Tree Reconciliation: Completed Root)
704 +
705 +⚛ (Committing Changes)
706 + ⚛ (Committing Snapshot Effects: 0 Total)
707 + ⚛ (Committing Host Effects: 1 Total)
708 + ⚛ (Calling Lifecycle Methods: 0 Total)
709 +"
710 +`;
711 +
712 +exports[`ReactDebugFiberPerf old scheduler measures deferred work in chunks 1`] = `
713 +"⚛ (Waiting for async callback... will force flush in 5250 ms)
714 +
715 +// Start rendering through B
716 +⚛ (React Tree Reconciliation: Yielded)
717 + ⚛ Parent [mount]
718 + ⚛ A [mount]
719 + ⚛ Child [mount]
720 + ⚛ B [mount]
721 +
722 +⚛ (Waiting for async callback... will force flush in 5250 ms)
723 +
724 +// Complete the rest
725 +⚛ (React Tree Reconciliation: Completed Root)
726 + ⚛ Parent [mount]
727 + ⚛ B [mount]
728 + ⚛ Child [mount]
729 + ⚛ C [mount]
730 + ⚛ Child [mount]
731 +
732 +⚛ (Committing Changes)
733 + ⚛ (Committing Snapshot Effects: 0 Total)
734 + ⚛ (Committing Host Effects: 1 Total)
735 + ⚛ (Calling Lifecycle Methods: 0 Total)
736 +"
737 +`;
738 +
739 +exports[`ReactDebugFiberPerf old scheduler measures deprioritized work 1`] = `
740 +"// Flush the parent
741 +⚛ (React Tree Reconciliation: Completed Root)
742 + ⚛ Parent [mount]
743 +
744 +⚛ (Committing Changes)
745 + ⚛ (Committing Snapshot Effects: 0 Total)
746 + ⚛ (Committing Host Effects: 1 Total)
747 + ⚛ (Calling Lifecycle Methods: 0 Total)
748 +
749 +⚛ (Waiting for async callback... will force flush in 10737418210 ms)
750 +
751 +// Flush the child
752 +⚛ (React Tree Reconciliation: Completed Root)
753 + ⚛ Child [mount]
754 +
755 +⚛ (Committing Changes)
756 + ⚛ (Committing Snapshot Effects: 0 Total)
757 + ⚛ (Committing Host Effects: 1 Total)
758 + ⚛ (Calling Lifecycle Methods: 0 Total)
759 +"
760 +`;
761 +
762 +exports[`ReactDebugFiberPerf old scheduler properly displays the forwardRef component in measurements 1`] = `
763 +"⚛ (Waiting for async callback... will force flush in 5250 ms)
764 +
765 +// Mount
766 +⚛ (React Tree Reconciliation: Completed Root)
767 + ⚛ Parent [mount]
768 + ⚛ ForwardRef [mount]
769 + ⚛ Child [mount]
770 + ⚛ ForwardRef(refForwarder) [mount]
771 + ⚛ Child [mount]
772 + ⚛ ForwardRef(OverriddenName) [mount]
773 + ⚛ Child [mount]
774 +
775 +⚛ (Committing Changes)
776 + ⚛ (Committing Snapshot Effects: 0 Total)
777 + ⚛ (Committing Host Effects: 1 Total)
778 + ⚛ (Calling Lifecycle Methods: 0 Total)
779 +"
780 +`;
781 +
782 +exports[`ReactDebugFiberPerf old scheduler recovers from caught errors 1`] = `
783 +"⚛ (Waiting for async callback... will force flush in 5250 ms)
784 +
785 +// Stop on Baddie and restart from Boundary
786 +⚛ (React Tree Reconciliation: Completed Root)
787 + ⚛ Parent [mount]
788 + ⛔ Boundary [mount] Warning: An error was thrown inside this error boundary
789 + ⚛ Parent [mount]
790 + ⚛ Baddie [mount]
791 + ⚛ Boundary [mount]
792 +
793 +⚛ (React Tree Reconciliation: Completed Root)
794 + ⚛ Parent [mount]
795 + ⛔ Boundary [mount] Warning: An error was thrown inside this error boundary
796 + ⚛ Parent [mount]
797 + ⚛ Baddie [mount]
798 + ⚛ Boundary [mount]
799 +
800 +⛔ (Committing Changes) Warning: Lifecycle hook scheduled a cascading update
801 + ⚛ (Committing Snapshot Effects: 0 Total)
802 + ⚛ (Committing Host Effects: 2 Total)
803 + ⚛ (Calling Lifecycle Methods: 1 Total)
804 +
805 +⚛ (React Tree Reconciliation: Completed Root)
806 + ⚛ Boundary [update]
807 + ⚛ ErrorReport [mount]
808 +
809 +⚛ (Committing Changes)
810 + ⚛ (Committing Snapshot Effects: 0 Total)
811 + ⚛ (Committing Host Effects: 1 Total)
812 + ⚛ (Calling Lifecycle Methods: 0 Total)
813 +"
814 +`;
815 +
816 +exports[`ReactDebugFiberPerf old scheduler recovers from fatal errors 1`] = `
817 +"⚛ (Waiting for async callback... will force flush in 5250 ms)
818 +
819 +// Will fatal
820 +⚛ (React Tree Reconciliation: Completed Root)
821 + ⚛ Parent [mount]
822 + ⚛ Baddie [mount]
823 +
824 +⚛ (React Tree Reconciliation: Completed Root)
825 + ⚛ Parent [mount]
826 + ⚛ Baddie [mount]
827 +
828 +⚛ (Committing Changes)
829 + ⚛ (Committing Snapshot Effects: 0 Total)
830 + ⚛ (Committing Host Effects: 1 Total)
831 + ⚛ (Calling Lifecycle Methods: 1 Total)
832 +
833 +⚛ (Waiting for async callback... will force flush in 5250 ms)
834 +
835 +// Will reconcile from a clean state
836 +⚛ (React Tree Reconciliation: Completed Root)
837 + ⚛ Parent [mount]
838 + ⚛ Child [mount]
839 +
840 +⚛ (Committing Changes)
841 + ⚛ (Committing Snapshot Effects: 0 Total)
842 + ⚛ (Committing Host Effects: 1 Total)
843 + ⚛ (Calling Lifecycle Methods: 0 Total)
844 +"
845 +`;
846 +
847 +exports[`ReactDebugFiberPerf old scheduler skips parents during setState 1`] = `
848 +"⚛ (Waiting for async callback... will force flush in 5250 ms)
849 +
850 +// Should include just A and B, no Parents
851 +⚛ (React Tree Reconciliation: Completed Root)
852 + ⚛ A [update]
853 + ⚛ B [update]
854 +
855 +⚛ (Committing Changes)
856 + ⚛ (Committing Snapshot Effects: 0 Total)
857 + ⚛ (Committing Host Effects: 2 Total)
858 + ⚛ (Calling Lifecycle Methods: 2 Total)
859 +"
860 +`;
861 +
862 +exports[`ReactDebugFiberPerf old scheduler supports Suspense and lazy 1`] = `
863 +"⚛ (Waiting for async callback... will force flush in 5250 ms)
864 +
865 +⚛ (React Tree Reconciliation: Completed Root)
866 + ⚛ Parent [mount]
867 + ⛔ Suspense [mount] Warning: Rendering was suspended
868 + ⚛ Suspense [mount]
869 + ⚛ Spinner [mount]
870 +"
871 +`;
872 +
873 +exports[`ReactDebugFiberPerf old scheduler supports Suspense and lazy 2`] = `
874 +"⚛ (Waiting for async callback... will force flush in 5250 ms)
875 +
876 +⚛ (React Tree Reconciliation: Completed Root)
877 + ⚛ Parent [mount]
878 + ⛔ Suspense [mount] Warning: Rendering was suspended
879 + ⚛ Suspense [mount]
880 + ⚛ Spinner [mount]
881 +
882 +⚛ (Waiting for async callback... will force flush in 5250 ms)
883 +
884 +⚛ (React Tree Reconciliation: Completed Root)
885 + ⚛ Parent [mount]
886 + ⚛ Suspense [mount]
887 + ⚛ Foo [mount]
888 +
889 +⚛ (Committing Changes)
890 + ⚛ (Committing Snapshot Effects: 0 Total)
891 + ⚛ (Committing Host Effects: 1 Total)
892 + ⚛ (Calling Lifecycle Methods: 0 Total)
893 +"
894 +`;
895 +
896 +exports[`ReactDebugFiberPerf old scheduler supports memo 1`] = `
897 +"⚛ (Waiting for async callback... will force flush in 5250 ms)
898 +
899 +⚛ (React Tree Reconciliation: Completed Root)
900 + ⚛ Parent [mount]
901 + ⚛ Foo [mount]
902 +
903 +⚛ (Committing Changes)
904 + ⚛ (Committing Snapshot Effects: 0 Total)
905 + ⚛ (Committing Host Effects: 1 Total)
906 + ⚛ (Calling Lifecycle Methods: 0 Total)
907 +"
908 +`;
909 +
910 +exports[`ReactDebugFiberPerf old scheduler supports portals 1`] = `
911 +"⚛ (Waiting for async callback... will force flush in 5250 ms)
912 +
913 +⚛ (React Tree Reconciliation: Completed Root)
914 + ⚛ Parent [mount]
915 + ⚛ Child [mount]
916 +
917 +⚛ (Committing Changes)
918 + ⚛ (Committing Snapshot Effects: 0 Total)
919 + ⚛ (Committing Host Effects: 2 Total)
920 + ⚛ (Calling Lifecycle Methods: 0 Total)
921 +"
922 +`;
923 +
924 +exports[`ReactDebugFiberPerf old scheduler warns if an in-progress update is interrupted 1`] = `
925 +"⚛ (Waiting for async callback... will force flush in 5250 ms)
926 +
927 +⚛ (React Tree Reconciliation: Yielded)
928 + ⚛ Foo [mount]
929 +
930 +⚛ (Waiting for async callback... will force flush in 5250 ms)
931 + ⛔ (React Tree Reconciliation: Completed Root) Warning: A top-level update interrupted the previous render
932 + ⚛ Foo [mount]
933 + ⚛ (Committing Changes)
934 + ⚛ (Committing Snapshot Effects: 0 Total)
935 + ⚛ (Committing Host Effects: 1 Total)
936 + ⚛ (Calling Lifecycle Methods: 0 Total)
937 +
938 +⚛ (React Tree Reconciliation: Completed Root)
939 +
940 +⚛ (Committing Changes)
941 + ⚛ (Committing Snapshot Effects: 0 Total)
942 + ⚛ (Committing Host Effects: 0 Total)
943 + ⚛ (Calling Lifecycle Methods: 0 Total)
944 +"
945 +`;
946 +
947 +exports[`ReactDebugFiberPerf old scheduler warns if async work expires (starvation) 1`] = `
948 "⛔ (Waiting for async callback... will force flush in 5250 ms) Warning: React was blocked by main thread
949
950 ⚛ (React Tree Reconciliation: Completed Root)
@@ -456,7 +957,7 @@ exports[`ReactDebugFiberPerf warns if async work expires (starvation) 1`] = `
957 "
958 `;
959
459 -exports[`ReactDebugFiberPerf warns on cascading renders from setState 1`] = `
960 +exports[`ReactDebugFiberPerf old scheduler warns on cascading renders from setState 1`] = `
961 "⚛ (Waiting for async callback... will force flush in 5250 ms)
962
963 // Should print a warning
@@ -480,7 +981,7 @@ exports[`ReactDebugFiberPerf warns on cascading renders from setState 1`] = `
981 "
982 `;
983
483 -exports[`ReactDebugFiberPerf warns on cascading renders from top-level render 1`] = `
984 +exports[`ReactDebugFiberPerf old scheduler warns on cascading renders from top-level render 1`] = `
985 "⚛ (Waiting for async callback... will force flush in 5250 ms)
986
987 // Rendering the first root
packages/react-test-renderer/src/__tests__/ReactTestRenderer-test.js
+21 -7
@@ -9,15 +9,25 @@
9
10 'use strict';
11
12 -const ReactDOM = require('react-dom');
13 -
14 -// Isolate test renderer.
15 -jest.resetModules();
16 -const React = require('react');
17 -const ReactCache = require('react-cache');
18 -const ReactTestRenderer = require('react-test-renderer');
12 +let ReactDOM;
13 +let React;
14 +let ReactCache;
15 +let ReactTestRenderer;
16 +let Scheduler;
17
18 describe('ReactTestRenderer', () => {
19 + beforeEach(() => {
20 + jest.resetModules();
21 + ReactDOM = require('react-dom');
22 +
23 + // Isolate test renderer.
24 + jest.resetModules();
25 + React = require('react');
26 + ReactCache = require('react-cache');
27 + ReactTestRenderer = require('react-test-renderer');
28 + Scheduler = require('scheduler');
29 + });
30 +
31 it('should warn if used to render a ReactDOM portal', () => {
32 const container = document.createElement('div');
33 expect(() => {
@@ -62,6 +72,7 @@ describe('ReactTestRenderer', () => {
72 const root = ReactTestRenderer.create(<App text="initial" />);
73 PendingResources.initial('initial');
74 await Promise.resolve();
75 + Scheduler.flushAll();
76 expect(root.toJSON()).toEqual('initial');
77
78 root.update(<App text="dynamic" />);
@@ -69,6 +80,7 @@ describe('ReactTestRenderer', () => {
80
81 PendingResources.dynamic('dynamic');
82 await Promise.resolve();
83 + Scheduler.flushAll();
84 expect(root.toJSON()).toEqual('dynamic');
85
86 done();
@@ -88,6 +100,7 @@ describe('ReactTestRenderer', () => {
100 const root = ReactTestRenderer.create(<App text="initial" />);
101 PendingResources.initial('initial');
102 await Promise.resolve();
103 + Scheduler.flushAll();
104 expect(root.toJSON().children).toEqual(['initial']);
105
106 root.update(<App text="dynamic" />);
@@ -95,6 +108,7 @@ describe('ReactTestRenderer', () => {
108
109 PendingResources.dynamic('dynamic');
110 await Promise.resolve();
111 + Scheduler.flushAll();
112 expect(root.toJSON().children).toEqual(['dynamic']);
113
114 done();
packages/react/src/__tests__/ReactProfiler-test.internal.js
+51 -1
@@ -12,6 +12,7 @@
12
13 let React;
14 let ReactFeatureFlags;
15 +let enableNewScheduler;
16 let ReactNoop;
17 let Scheduler;
18 let ReactCache;
@@ -35,6 +36,7 @@ function loadModules({
36 ReactFeatureFlags.enableProfilerTimer = enableProfilerTimer;
37 ReactFeatureFlags.enableSchedulerTracing = enableSchedulerTracing;
38 ReactFeatureFlags.replayFailedUnitOfWorkWithInvokeGuardedCallback = replayFailedUnitOfWorkWithInvokeGuardedCallback;
39 + enableNewScheduler = ReactFeatureFlags.enableNewScheduler;
40
41 React = require('react');
42 Scheduler = require('scheduler');
@@ -1352,6 +1354,9 @@ describe('Profiler', () => {
1354 },
1355 );
1356 }).toThrow('Expected error onWorkScheduled');
1357 + if (enableNewScheduler) {
1358 + expect(Scheduler).toFlushAndYield(['Component:fail']);
1359 + }
1360 throwInOnWorkScheduled = false;
1361 expect(onWorkScheduled).toHaveBeenCalled();
1362
@@ -1386,7 +1391,14 @@ describe('Profiler', () => {
1391 // Errors that happen inside of a subscriber should throw,
1392 throwInOnWorkStarted = true;
1393 expect(Scheduler).toFlushAndThrow('Expected error onWorkStarted');
1389 - expect(Scheduler).toHaveYielded(['Component:text']);
1394 + if (enableNewScheduler) {
1395 + // Rendering was interrupted by the error that was thrown
1396 + expect(Scheduler).toHaveYielded([]);
1397 + // Rendering continues in the next task
1398 + expect(Scheduler).toFlushAndYield(['Component:text']);
1399 + } else {
1400 + expect(Scheduler).toHaveYielded(['Component:text']);
1401 + }
1402 throwInOnWorkStarted = false;
1403 expect(onWorkStarted).toHaveBeenCalled();
1404
@@ -2370,11 +2382,23 @@ describe('Profiler', () => {
2382 },
2383 );
2384
2385 + expect(Scheduler).toHaveYielded(['Suspend [loaded]', 'Text [loading]']);
2386 +
2387 expect(onInteractionScheduledWorkCompleted).not.toHaveBeenCalled();
2388
2389 jest.runAllTimers();
2390 await resourcePromise;
2391
2392 + if (enableNewScheduler) {
2393 + expect(Scheduler).toHaveYielded(['Promise resolved [loaded]']);
2394 + expect(Scheduler).toFlushExpired(['AsyncText [loaded]']);
2395 + } else {
2396 + expect(Scheduler).toHaveYielded([
2397 + 'Promise resolved [loaded]',
2398 + 'AsyncText [loaded]',
2399 + ]);
2400 + }
2401 +
2402 expect(onInteractionScheduledWorkCompleted).toHaveBeenCalledTimes(1);
2403 expect(
2404 onInteractionScheduledWorkCompleted,
@@ -2424,9 +2448,16 @@ describe('Profiler', () => {
2448
2449 expect(onInteractionScheduledWorkCompleted).not.toHaveBeenCalled();
2450
2451 + expect(Scheduler).toHaveYielded(['Text [loading]']);
2452 +
2453 jest.runAllTimers();
2454 await resourcePromise;
2455
2456 + expect(Scheduler).toHaveYielded(['Promise resolved [loaded]']);
2457 + if (enableNewScheduler) {
2458 + expect(Scheduler).toFlushExpired([]);
2459 + }
2460 +
2461 expect(onInteractionScheduledWorkCompleted).not.toHaveBeenCalled();
2462
2463 wrappedCascadingFn();
@@ -2578,6 +2609,14 @@ describe('Profiler', () => {
2609 },
2610 );
2611 });
2612 + expect(Scheduler).toHaveYielded([
2613 + 'Suspend [loaded]',
2614 + 'Text [loading]',
2615 + 'Text [initial]',
2616 + 'Suspend [loaded]',
2617 + 'Text [loading]',
2618 + 'Text [updated]',
2619 + ]);
2620 expect(renderer.toJSON()).toEqual(['loading', 'updated']);
2621
2622 expect(onRender).toHaveBeenCalledTimes(1);
@@ -2591,6 +2630,17 @@ describe('Profiler', () => {
2630 Scheduler.advanceTime(100);
2631 jest.advanceTimersByTime(100);
2632 await originalPromise;
2633 +
2634 + if (enableNewScheduler) {
2635 + expect(Scheduler).toHaveYielded(['Promise resolved [loaded]']);
2636 + expect(Scheduler).toFlushExpired(['AsyncText [loaded]']);
2637 + } else {
2638 + expect(Scheduler).toHaveYielded([
2639 + 'Promise resolved [loaded]',
2640 + 'AsyncText [loaded]',
2641 + ]);
2642 + }
2643 +
2644 expect(renderer.toJSON()).toEqual(['loaded', 'updated']);
2645
2646 expect(onRender).toHaveBeenCalledTimes(1);
packages/scheduler/src/Scheduler.js
+63 -78
@@ -40,7 +40,7 @@ var IDLE_PRIORITY = maxSigned31BitInt;
40 // Callbacks are stored as a circular, doubly linked list.
41 var firstCallbackNode = null;
42
43 -var currentDidTimeout = false;
43 +var currentHostCallbackDidTimeout = false;
44 // Pausing the scheduler is useful for debugging.
45 var isSchedulerPaused = false;
46
@@ -48,29 +48,31 @@ var currentPriorityLevel = NormalPriority;
48 var currentEventStartTime = -1;
49 var currentExpirationTime = -1;
50
51 -// This is set when a callback is being executed, to prevent re-entrancy.
52 -var isExecutingCallback = false;
51 +// This is set while performing work, to prevent re-entrancy.
52 +var isPerformingWork = false;
53
54 var isHostCallbackScheduled = false;
55
56 -function ensureHostCallbackIsScheduled() {
57 - if (isExecutingCallback) {
56 +function scheduleHostCallbackIfNeeded() {
57 + if (isPerformingWork) {
58 // Don't schedule work yet; wait until the next time we yield.
59 return;
60 }
61 - // Schedule the host callback using the earliest expiration in the list.
62 - var expirationTime = firstCallbackNode.expirationTime;
63 - if (!isHostCallbackScheduled) {
64 - isHostCallbackScheduled = true;
65 - } else {
66 - // Cancel the existing host callback.
67 - cancelHostCallback();
61 + if (firstCallbackNode !== null) {
62 + // Schedule the host callback using the earliest expiration in the list.
63 + var expirationTime = firstCallbackNode.expirationTime;
64 + if (isHostCallbackScheduled) {
65 + // Cancel the existing host callback.
66 + cancelHostCallback();
67 + } else {
68 + isHostCallbackScheduled = true;
69 + }
70 + requestHostCallback(flushWork, expirationTime);
71 }
69 - requestHostCallback(flushWork, expirationTime);
72 }
73
74 function flushFirstCallback() {
73 - var flushedNode = firstCallbackNode;
75 + const currentlyFlushingCallback = firstCallbackNode;
76
77 // Remove the node from the list before calling the callback. That way the
78 // list is in a consistent state even if the callback throws.
@@ -85,19 +87,25 @@ function flushFirstCallback() {
87 next.previous = lastCallbackNode;
88 }
89
88 - flushedNode.next = flushedNode.previous = null;
90 + currentlyFlushingCallback.next = currentlyFlushingCallback.previous = null;
91
92 // Now it's safe to call the callback.
91 - var callback = flushedNode.callback;
92 - var expirationTime = flushedNode.expirationTime;
93 - var priorityLevel = flushedNode.priorityLevel;
93 + var callback = currentlyFlushingCallback.callback;
94 + var expirationTime = currentlyFlushingCallback.expirationTime;
95 + var priorityLevel = currentlyFlushingCallback.priorityLevel;
96 var previousPriorityLevel = currentPriorityLevel;
97 var previousExpirationTime = currentExpirationTime;
98 currentPriorityLevel = priorityLevel;
99 currentExpirationTime = expirationTime;
100 var continuationCallback;
101 try {
100 - continuationCallback = callback(currentDidTimeout);
102 + const didUserCallbackTimeout =
103 + currentHostCallbackDidTimeout ||
104 + // Immediate priority callbacks are always called as if they timed out
105 + priorityLevel === ImmediatePriority;
106 + continuationCallback = callback(didUserCallbackTimeout);
107 + } catch (error) {
108 + throw error;
109 } finally {
110 currentPriorityLevel = previousPriorityLevel;
111 currentExpirationTime = previousExpirationTime;
@@ -141,7 +149,7 @@ function flushFirstCallback() {
149 } else if (nextAfterContinuation === firstCallbackNode) {
150 // The new callback is the highest priority callback in the list.
151 firstCallbackNode = continuationNode;
144 - ensureHostCallbackIsScheduled();
152 + scheduleHostCallbackIfNeeded();
153 }
154
155 var previous = nextAfterContinuation.previous;
@@ -152,46 +160,20 @@ function flushFirstCallback() {
160 }
161 }
162
155 -function flushImmediateWork() {
156 - if (
157 - // Confirm we've exited the outer most event handler
158 - currentEventStartTime === -1 &&
159 - firstCallbackNode !== null &&
160 - firstCallbackNode.priorityLevel === ImmediatePriority
161 - ) {
162 - isExecutingCallback = true;
163 - try {
164 - do {
165 - flushFirstCallback();
166 - } while (
167 - // Keep flushing until there are no more immediate callbacks
168 - firstCallbackNode !== null &&
169 - firstCallbackNode.priorityLevel === ImmediatePriority
170 - );
171 - } finally {
172 - isExecutingCallback = false;
173 - if (firstCallbackNode !== null) {
174 - // There's still work remaining. Request another callback.
175 - ensureHostCallbackIsScheduled();
176 - } else {
177 - isHostCallbackScheduled = false;
178 - }
179 - }
180 - }
181 -}
182 -
183 -function flushWork(didTimeout) {
163 +function flushWork(didUserCallbackTimeout) {
164 // Exit right away if we're currently paused
185 -
165 if (enableSchedulerDebugging && isSchedulerPaused) {
166 return;
167 }
168
190 - isExecutingCallback = true;
191 - const previousDidTimeout = currentDidTimeout;
192 - currentDidTimeout = didTimeout;
169 + // We'll need a new host callback the next time work is scheduled.
170 + isHostCallbackScheduled = false;
171 +
172 + isPerformingWork = true;
173 + const previousDidTimeout = currentHostCallbackDidTimeout;
174 + currentHostCallbackDidTimeout = didUserCallbackTimeout;
175 try {
194 - if (didTimeout) {
176 + if (didUserCallbackTimeout) {
177 // Flush all the expired callbacks without yielding.
178 while (
179 firstCallbackNode !== null &&
@@ -226,16 +208,10 @@ function flushWork(didTimeout) {
208 }
209 }
210 } finally {
229 - isExecutingCallback = false;
230 - currentDidTimeout = previousDidTimeout;
231 - if (firstCallbackNode !== null) {
232 - // There's still work remaining. Request another callback.
233 - ensureHostCallbackIsScheduled();
234 - } else {
235 - isHostCallbackScheduled = false;
236 - }
237 - // Before exiting, flush all the immediate work that was scheduled.
238 - flushImmediateWork();
211 + isPerformingWork = false;
212 + currentHostCallbackDidTimeout = previousDidTimeout;
213 + // There's still work remaining. Request another callback.
214 + scheduleHostCallbackIfNeeded();
215 }
216 }
217
@@ -258,12 +234,13 @@ function unstable_runWithPriority(priorityLevel, eventHandler) {
234
235 try {
236 return eventHandler();
237 + } catch (error) {
238 + // There's still work remaining. Request another callback.
239 + scheduleHostCallbackIfNeeded();
240 + throw error;
241 } finally {
242 currentPriorityLevel = previousPriorityLevel;
243 currentEventStartTime = previousEventStartTime;
264 -
265 - // Before exiting, flush all the immediate work that was scheduled.
266 - flushImmediateWork();
244 }
245 }
246
@@ -289,12 +266,13 @@ function unstable_next(eventHandler) {
266
267 try {
268 return eventHandler();
269 + } catch (error) {
270 + // There's still work remaining. Request another callback.
271 + scheduleHostCallbackIfNeeded();
272 + throw error;
273 } finally {
274 currentPriorityLevel = previousPriorityLevel;
275 currentEventStartTime = previousEventStartTime;
295 -
296 - // Before exiting, flush all the immediate work that was scheduled.
297 - flushImmediateWork();
276 }
277 }
278
@@ -309,15 +287,22 @@ function unstable_wrapCallback(callback) {
287
288 try {
289 return callback.apply(this, arguments);
290 + } catch (error) {
291 + // There's still work remaining. Request another callback.
292 + scheduleHostCallbackIfNeeded();
293 + throw error;
294 } finally {
295 currentPriorityLevel = previousPriorityLevel;
296 currentEventStartTime = previousEventStartTime;
315 - flushImmediateWork();
297 }
298 };
299 }
300
320 -function unstable_scheduleCallback(callback, deprecated_options) {
301 +function unstable_scheduleCallback(
302 + priorityLevel,
303 + callback,
304 + deprecated_options,
305 +) {
306 var startTime =
307 currentEventStartTime !== -1 ? currentEventStartTime : getCurrentTime();
308
@@ -330,7 +315,7 @@ function unstable_scheduleCallback(callback, deprecated_options) {
315 // FIXME: Remove this branch once we lift expiration times out of React.
316 expirationTime = startTime + deprecated_options.timeout;
317 } else {
333 - switch (currentPriorityLevel) {
318 + switch (priorityLevel) {
319 case ImmediatePriority:
320 expirationTime = startTime + IMMEDIATE_PRIORITY_TIMEOUT;
321 break;
@@ -351,7 +336,7 @@ function unstable_scheduleCallback(callback, deprecated_options) {
336
337 var newNode = {
338 callback,
354 - priorityLevel: currentPriorityLevel,
339 + priorityLevel: priorityLevel,
340 expirationTime,
341 next: null,
342 previous: null,
@@ -363,7 +348,7 @@ function unstable_scheduleCallback(callback, deprecated_options) {
348 if (firstCallbackNode === null) {
349 // This is the first callback in the list.
350 firstCallbackNode = newNode.next = newNode.previous = newNode;
366 - ensureHostCallbackIsScheduled();
351 + scheduleHostCallbackIfNeeded();
352 } else {
353 var next = null;
354 var node = firstCallbackNode;
@@ -383,7 +368,7 @@ function unstable_scheduleCallback(callback, deprecated_options) {
368 } else if (next === firstCallbackNode) {
369 // The new callback has the earliest expiration in the entire list.
370 firstCallbackNode = newNode;
386 - ensureHostCallbackIsScheduled();
371 + scheduleHostCallbackIfNeeded();
372 }
373
374 var previous = next.previous;
@@ -402,7 +387,7 @@ function unstable_pauseExecution() {
387 function unstable_continueExecution() {
388 isSchedulerPaused = false;
389 if (firstCallbackNode !== null) {
405 - ensureHostCallbackIsScheduled();
390 + scheduleHostCallbackIfNeeded();
391 }
392 }
393
@@ -439,7 +424,7 @@ function unstable_getCurrentPriorityLevel() {
424
425 function unstable_shouldYield() {
426 return (
442 - !currentDidTimeout &&
427 + !currentHostCallbackDidTimeout &&
428 ((firstCallbackNode !== null &&
429 firstCallbackNode.expirationTime < currentExpirationTime) ||
430 shouldYieldToHost())
packages/scheduler/src/__tests__/Scheduler-test.js
+108 -154
@@ -39,10 +39,10 @@ describe('Scheduler', () => {
39 });
40
41 it('flushes work incrementally', () => {
42 - scheduleCallback(() => Scheduler.yieldValue('A'));
43 - scheduleCallback(() => Scheduler.yieldValue('B'));
44 - scheduleCallback(() => Scheduler.yieldValue('C'));
45 - scheduleCallback(() => Scheduler.yieldValue('D'));
42 + scheduleCallback(NormalPriority, () => Scheduler.yieldValue('A'));
43 + scheduleCallback(NormalPriority, () => Scheduler.yieldValue('B'));
44 + scheduleCallback(NormalPriority, () => Scheduler.yieldValue('C'));
45 + scheduleCallback(NormalPriority, () => Scheduler.yieldValue('D'));
46
47 expect(Scheduler).toFlushAndYieldThrough(['A', 'B']);
48 expect(Scheduler).toFlushAndYieldThrough(['C']);
@@ -50,9 +50,11 @@ describe('Scheduler', () => {
50 });
51
52 it('cancels work', () => {
53 - scheduleCallback(() => Scheduler.yieldValue('A'));
54 - const callbackHandleB = scheduleCallback(() => Scheduler.yieldValue('B'));
55 - scheduleCallback(() => Scheduler.yieldValue('C'));
53 + scheduleCallback(NormalPriority, () => Scheduler.yieldValue('A'));
54 + const callbackHandleB = scheduleCallback(NormalPriority, () =>
55 + Scheduler.yieldValue('B'),
56 + );
57 + scheduleCallback(NormalPriority, () => Scheduler.yieldValue('C'));
58
59 cancelCallback(callbackHandleB);
60
@@ -64,37 +66,31 @@ describe('Scheduler', () => {
66 });
67
68 it('executes the highest priority callbacks first', () => {
67 - scheduleCallback(() => Scheduler.yieldValue('A'));
68 - scheduleCallback(() => Scheduler.yieldValue('B'));
69 + scheduleCallback(NormalPriority, () => Scheduler.yieldValue('A'));
70 + scheduleCallback(NormalPriority, () => Scheduler.yieldValue('B'));
71
72 // Yield before B is flushed
73 expect(Scheduler).toFlushAndYieldThrough(['A']);
74
73 - runWithPriority(UserBlockingPriority, () => {
74 - scheduleCallback(() => Scheduler.yieldValue('C'));
75 - scheduleCallback(() => Scheduler.yieldValue('D'));
76 - });
75 + scheduleCallback(UserBlockingPriority, () => Scheduler.yieldValue('C'));
76 + scheduleCallback(UserBlockingPriority, () => Scheduler.yieldValue('D'));
77
78 // C and D should come first, because they are higher priority
79 expect(Scheduler).toFlushAndYield(['C', 'D', 'B']);
80 });
81
82 it('expires work', () => {
83 - scheduleCallback(didTimeout => {
83 + scheduleCallback(NormalPriority, didTimeout => {
84 Scheduler.advanceTime(100);
85 Scheduler.yieldValue(`A (did timeout: ${didTimeout})`);
86 });
87 - runWithPriority(UserBlockingPriority, () => {
88 - scheduleCallback(didTimeout => {
89 - Scheduler.advanceTime(100);
90 - Scheduler.yieldValue(`B (did timeout: ${didTimeout})`);
91 - });
87 + scheduleCallback(UserBlockingPriority, didTimeout => {
88 + Scheduler.advanceTime(100);
89 + Scheduler.yieldValue(`B (did timeout: ${didTimeout})`);
90 });
93 - runWithPriority(UserBlockingPriority, () => {
94 - scheduleCallback(didTimeout => {
95 - Scheduler.advanceTime(100);
96 - Scheduler.yieldValue(`C (did timeout: ${didTimeout})`);
97 - });
91 + scheduleCallback(UserBlockingPriority, didTimeout => {
92 + Scheduler.advanceTime(100);
93 + Scheduler.yieldValue(`C (did timeout: ${didTimeout})`);
94 });
95
96 // Advance time, but not by enough to expire any work
@@ -102,11 +98,11 @@ describe('Scheduler', () => {
98 expect(Scheduler).toHaveYielded([]);
99
100 // Schedule a few more callbacks
105 - scheduleCallback(didTimeout => {
101 + scheduleCallback(NormalPriority, didTimeout => {
102 Scheduler.advanceTime(100);
103 Scheduler.yieldValue(`D (did timeout: ${didTimeout})`);
104 });
109 - scheduleCallback(didTimeout => {
105 + scheduleCallback(NormalPriority, didTimeout => {
106 Scheduler.advanceTime(100);
107 Scheduler.yieldValue(`E (did timeout: ${didTimeout})`);
108 });
@@ -130,7 +126,7 @@ describe('Scheduler', () => {
126 });
127
128 it('has a default expiration of ~5 seconds', () => {
133 - scheduleCallback(() => Scheduler.yieldValue('A'));
129 + scheduleCallback(NormalPriority, () => Scheduler.yieldValue('A'));
130
131 Scheduler.advanceTime(4999);
132 expect(Scheduler).toHaveYielded([]);
@@ -140,11 +136,11 @@ describe('Scheduler', () => {
136 });
137
138 it('continues working on same task after yielding', () => {
143 - scheduleCallback(() => {
139 + scheduleCallback(NormalPriority, () => {
140 Scheduler.advanceTime(100);
141 Scheduler.yieldValue('A');
142 });
147 - scheduleCallback(() => {
143 + scheduleCallback(NormalPriority, () => {
144 Scheduler.advanceTime(100);
145 Scheduler.yieldValue('B');
146 });
@@ -163,13 +159,13 @@ describe('Scheduler', () => {
159 }
160 };
161
166 - scheduleCallback(C);
162 + scheduleCallback(NormalPriority, C);
163
168 - scheduleCallback(() => {
164 + scheduleCallback(NormalPriority, () => {
165 Scheduler.advanceTime(100);
166 Scheduler.yieldValue('D');
167 });
172 - scheduleCallback(() => {
168 + scheduleCallback(NormalPriority, () => {
169 Scheduler.advanceTime(100);
170 Scheduler.yieldValue('E');
171 });
@@ -197,7 +193,7 @@ describe('Scheduler', () => {
193 };
194
195 // Schedule a high priority callback
200 - runWithPriority(UserBlockingPriority, () => scheduleCallback(work));
196 + scheduleCallback(UserBlockingPriority, work);
197
198 // Flush until just before the expiration time
199 expect(Scheduler).toFlushAndYieldThrough(['A', 'B']);
@@ -207,26 +203,6 @@ describe('Scheduler', () => {
203 expect(Scheduler).toHaveYielded(['C', 'D']);
204 });
205
210 - it('nested callbacks inherit the priority of the currently executing callback', () => {
211 - runWithPriority(UserBlockingPriority, () => {
212 - scheduleCallback(() => {
213 - Scheduler.advanceTime(100);
214 - Scheduler.yieldValue('Parent callback');
215 - scheduleCallback(() => {
216 - Scheduler.advanceTime(100);
217 - Scheduler.yieldValue('Nested callback');
218 - });
219 - });
220 - });
221 -
222 - expect(Scheduler).toFlushAndYieldThrough(['Parent callback']);
223 -
224 - // The nested callback has user-blocking priority, so it should
225 - // expire quickly.
226 - Scheduler.advanceTime(250 + 100);
227 - expect(Scheduler).toHaveYielded(['Nested callback']);
228 - });
229 -
206 it('continuations are interrupted by higher priority work', () => {
207 const tasks = [['A', 100], ['B', 100], ['C', 100], ['D', 100]];
208 const work = () => {
@@ -239,14 +215,12 @@ describe('Scheduler', () => {
215 }
216 }
217 };
242 - scheduleCallback(work);
218 + scheduleCallback(NormalPriority, work);
219 expect(Scheduler).toFlushAndYieldThrough(['A']);
220
245 - runWithPriority(UserBlockingPriority, () => {
246 - scheduleCallback(() => {
247 - Scheduler.advanceTime(100);
248 - Scheduler.yieldValue('High pri');
249 - });
221 + scheduleCallback(UserBlockingPriority, () => {
222 + Scheduler.advanceTime(100);
223 + Scheduler.yieldValue('High pri');
224 });
225
226 expect(Scheduler).toFlushAndYield(['High pri', 'B', 'C', 'D']);
@@ -266,12 +240,10 @@ describe('Scheduler', () => {
240 if (task[0] === 'B') {
241 // Schedule high pri work from inside another callback
242 Scheduler.yieldValue('Schedule high pri');
269 - runWithPriority(UserBlockingPriority, () =>
270 - scheduleCallback(() => {
271 - Scheduler.advanceTime(100);
272 - Scheduler.yieldValue('High pri');
273 - }),
274 - );
243 + scheduleCallback(UserBlockingPriority, () => {
244 + Scheduler.advanceTime(100);
245 + Scheduler.yieldValue('High pri');
246 + });
247 }
248 if (tasks.length > 0 && shouldYield()) {
249 Scheduler.yieldValue('Yield!');
@@ -279,7 +251,7 @@ describe('Scheduler', () => {
251 }
252 }
253 };
282 - scheduleCallback(work);
254 + scheduleCallback(NormalPriority, work);
255 expect(Scheduler).toFlushAndYield([
256 'A',
257 'B',
@@ -295,21 +267,28 @@ describe('Scheduler', () => {
267 },
268 );
269
298 - it('immediate callbacks fire at the end of outermost event', () => {
299 - runWithPriority(ImmediatePriority, () => {
300 - scheduleCallback(() => Scheduler.yieldValue('A'));
301 - scheduleCallback(() => Scheduler.yieldValue('B'));
302 - // Nested event
303 - runWithPriority(ImmediatePriority, () => {
304 - scheduleCallback(() => Scheduler.yieldValue('C'));
305 - // Nothing should have fired yet
306 - expect(Scheduler).toHaveYielded([]);
307 - });
308 - // Nothing should have fired yet
309 - expect(Scheduler).toHaveYielded([]);
270 + it('top-level immediate callbacks fire in a subsequent task', () => {
271 + scheduleCallback(ImmediatePriority, () => Scheduler.yieldValue('A'));
272 + scheduleCallback(ImmediatePriority, () => Scheduler.yieldValue('B'));
273 + scheduleCallback(ImmediatePriority, () => Scheduler.yieldValue('C'));
274 + scheduleCallback(ImmediatePriority, () => Scheduler.yieldValue('D'));
275 + // Immediate callback hasn't fired, yet.
276 + expect(Scheduler).toHaveYielded([]);
277 + // They all flush immediately within the subsequent task.
278 + expect(Scheduler).toFlushExpired(['A', 'B', 'C', 'D']);
279 + });
280 +
281 + it('nested immediate callbacks are added to the queue of immediate callbacks', () => {
282 + scheduleCallback(ImmediatePriority, () => Scheduler.yieldValue('A'));
283 + scheduleCallback(ImmediatePriority, () => {
284 + Scheduler.yieldValue('B');
285 + // This callback should go to the end of the queue
286 + scheduleCallback(ImmediatePriority, () => Scheduler.yieldValue('C'));
287 });
311 - // The callbacks were called at the end of the outer event
312 - expect(Scheduler).toHaveYielded(['A', 'B', 'C']);
288 + scheduleCallback(ImmediatePriority, () => Scheduler.yieldValue('D'));
289 + expect(Scheduler).toHaveYielded([]);
290 + // C should flush at the end
291 + expect(Scheduler).toFlushExpired(['A', 'B', 'D', 'C']);
292 });
293
294 it('wrapped callbacks have same signature as original callback', () => {
@@ -318,108 +297,83 @@ describe('Scheduler', () => {
297 });
298
299 it('wrapped callbacks inherit the current priority', () => {
321 - const wrappedCallback = wrapCallback(() => {
322 - scheduleCallback(() => {
323 - Scheduler.advanceTime(100);
324 - Scheduler.yieldValue('Normal');
325 - });
326 - });
327 - const wrappedInteractiveCallback = runWithPriority(
300 + const wrappedCallback = runWithPriority(NormalPriority, () =>
301 + wrapCallback(() => {
302 + Scheduler.yieldValue(getCurrentPriorityLevel());
303 + }),
304 + );
305 +
306 + const wrappedUserBlockingCallback = runWithPriority(
307 UserBlockingPriority,
308 () =>
309 wrapCallback(() => {
331 - scheduleCallback(() => {
332 - Scheduler.advanceTime(100);
333 - Scheduler.yieldValue('User-blocking');
334 - });
310 + Scheduler.yieldValue(getCurrentPriorityLevel());
311 }),
312 );
313
338 - // This should schedule a normal callback
314 wrappedCallback();
340 - // This should schedule an user-blocking callback
341 - wrappedInteractiveCallback();
315 + expect(Scheduler).toHaveYielded([NormalPriority]);
316
343 - Scheduler.advanceTime(249);
344 - expect(Scheduler).toHaveYielded([]);
345 - Scheduler.advanceTime(1);
346 - expect(Scheduler).toHaveYielded(['User-blocking']);
347 -
348 - Scheduler.advanceTime(10000);
349 - expect(Scheduler).toHaveYielded(['Normal']);
317 + wrappedUserBlockingCallback();
318 + expect(Scheduler).toHaveYielded([UserBlockingPriority]);
319 });
320
321 it('wrapped callbacks inherit the current priority even when nested', () => {
353 - const wrappedCallback = wrapCallback(() => {
354 - scheduleCallback(() => {
355 - Scheduler.advanceTime(100);
356 - Scheduler.yieldValue('Normal');
322 + let wrappedCallback;
323 + let wrappedUserBlockingCallback;
324 +
325 + runWithPriority(NormalPriority, () => {
326 + wrappedCallback = wrapCallback(() => {
327 + Scheduler.yieldValue(getCurrentPriorityLevel());
328 });
358 - });
359 - const wrappedInteractiveCallback = runWithPriority(
360 - UserBlockingPriority,
361 - () =>
329 + wrappedUserBlockingCallback = runWithPriority(UserBlockingPriority, () =>
330 wrapCallback(() => {
363 - scheduleCallback(() => {
364 - Scheduler.advanceTime(100);
365 - Scheduler.yieldValue('User-blocking');
366 - });
331 + Scheduler.yieldValue(getCurrentPriorityLevel());
332 }),
368 - );
369 -
370 - runWithPriority(UserBlockingPriority, () => {
371 - // This should schedule a normal callback
372 - wrappedCallback();
373 - // This should schedule an user-blocking callback
374 - wrappedInteractiveCallback();
333 + );
334 });
335
377 - Scheduler.advanceTime(249);
378 - expect(Scheduler).toHaveYielded([]);
379 - Scheduler.advanceTime(1);
380 - expect(Scheduler).toHaveYielded(['User-blocking']);
381 -
382 - Scheduler.advanceTime(10000);
383 - expect(Scheduler).toHaveYielded(['Normal']);
384 - });
385 -
386 - it('immediate callbacks fire at the end of callback', () => {
387 - const immediateCallback = runWithPriority(ImmediatePriority, () =>
388 - wrapCallback(() => {
389 - scheduleCallback(() => Scheduler.yieldValue('callback'));
390 - }),
391 - );
392 - immediateCallback();
336 + wrappedCallback();
337 + expect(Scheduler).toHaveYielded([NormalPriority]);
338
394 - // The callback was called at the end of the outer event
395 - expect(Scheduler).toHaveYielded(['callback']);
339 + wrappedUserBlockingCallback();
340 + expect(Scheduler).toHaveYielded([UserBlockingPriority]);
341 });
342
343 it("immediate callbacks fire even if there's an error", () => {
399 - expect(() => {
400 - runWithPriority(ImmediatePriority, () => {
401 - scheduleCallback(() => {
402 - Scheduler.yieldValue('A');
403 - throw new Error('Oops A');
404 - });
405 - scheduleCallback(() => {
406 - Scheduler.yieldValue('B');
407 - });
408 - scheduleCallback(() => {
409 - Scheduler.yieldValue('C');
410 - throw new Error('Oops C');
411 - });
412 - });
413 - }).toThrow('Oops A');
344 + scheduleCallback(ImmediatePriority, () => {
345 + Scheduler.yieldValue('A');
346 + throw new Error('Oops A');
347 + });
348 + scheduleCallback(ImmediatePriority, () => {
349 + Scheduler.yieldValue('B');
350 + });
351 + scheduleCallback(ImmediatePriority, () => {
352 + Scheduler.yieldValue('C');
353 + throw new Error('Oops C');
354 + });
355
356 + expect(() => expect(Scheduler).toFlushExpired()).toThrow('Oops A');
357 expect(Scheduler).toHaveYielded(['A']);
358
359 // B and C flush in a subsequent event. That way, the second error is not
360 // swallowed.
419 - expect(() => Scheduler.unstable_flushExpired()).toThrow('Oops C');
361 + expect(() => expect(Scheduler).toFlushExpired()).toThrow('Oops C');
362 expect(Scheduler).toHaveYielded(['B', 'C']);
363 });
364
365 + it('multiple immediate callbacks can throw and there will be an error for each one', () => {
366 + scheduleCallback(ImmediatePriority, () => {
367 + throw new Error('First error');
368 + });
369 + scheduleCallback(ImmediatePriority, () => {
370 + throw new Error('Second error');
371 + });
372 + expect(() => Scheduler.flushAll()).toThrow('First error');
373 + // The next error is thrown in the subsequent event
374 + expect(() => Scheduler.flushAll()).toThrow('Second error');
375 + });
376 +
377 it('exposes the current priority level', () => {
378 Scheduler.yieldValue(getCurrentPriorityLevel());
379 runWithPriority(ImmediatePriority, () => {
packages/scheduler/src/__tests__/SchedulerDOM-test.js
+118 -73
@@ -102,20 +102,26 @@ describe('SchedulerDOM', () => {
102
103 describe('scheduleCallback', () => {
104 it('calls the callback within the frame when not blocked', () => {
105 - const {unstable_scheduleCallback: scheduleCallback} = Scheduler;
105 + const {
106 + unstable_scheduleCallback: scheduleCallback,
107 + unstable_NormalPriority: NormalPriority,
108 + } = Scheduler;
109 const cb = jest.fn();
107 - scheduleCallback(cb);
110 + scheduleCallback(NormalPriority, cb);
111 advanceOneFrame({timeLeftInFrame: 15});
112 expect(cb).toHaveBeenCalledTimes(1);
113 });
114
115 it('inserts its rAF callback as early into the queue as possible', () => {
113 - const {unstable_scheduleCallback: scheduleCallback} = Scheduler;
116 + const {
117 + unstable_scheduleCallback: scheduleCallback,
118 + unstable_NormalPriority: NormalPriority,
119 + } = Scheduler;
120 const log = [];
121 const useRAFCallback = () => {
122 log.push('userRAFCallback');
123 };
118 - scheduleCallback(() => {
124 + scheduleCallback(NormalPriority, () => {
125 // Call rAF while idle work is being flushed.
126 requestAnimationFrame(useRAFCallback);
127 });
@@ -130,15 +136,18 @@ describe('SchedulerDOM', () => {
136
137 describe('with multiple callbacks', () => {
138 it('accepts multiple callbacks and calls within frame when not blocked', () => {
133 - const {unstable_scheduleCallback: scheduleCallback} = Scheduler;
139 + const {
140 + unstable_scheduleCallback: scheduleCallback,
141 + unstable_NormalPriority: NormalPriority,
142 + } = Scheduler;
143 const callbackLog = [];
144 const callbackA = jest.fn(() => callbackLog.push('A'));
145 const callbackB = jest.fn(() => callbackLog.push('B'));
137 - scheduleCallback(callbackA);
146 + scheduleCallback(NormalPriority, callbackA);
147 // initially waits to call the callback
148 expect(callbackLog).toEqual([]);
149 // waits while second callback is passed
141 - scheduleCallback(callbackB);
150 + scheduleCallback(NormalPriority, callbackB);
151 expect(callbackLog).toEqual([]);
152 // after a delay, calls as many callbacks as it has time for
153 advanceOneFrame({timeLeftInFrame: 15});
@@ -146,17 +155,20 @@ describe('SchedulerDOM', () => {
155 });
156
157 it("accepts callbacks between animationFrame and postMessage and doesn't stall", () => {
149 - const {unstable_scheduleCallback: scheduleCallback} = Scheduler;
158 + const {
159 + unstable_scheduleCallback: scheduleCallback,
160 + unstable_NormalPriority: NormalPriority,
161 + } = Scheduler;
162 const callbackLog = [];
163 const callbackA = jest.fn(() => callbackLog.push('A'));
164 const callbackB = jest.fn(() => callbackLog.push('B'));
165 const callbackC = jest.fn(() => callbackLog.push('C'));
154 - scheduleCallback(callbackA);
166 + scheduleCallback(NormalPriority, callbackA);
167 // initially waits to call the callback
168 expect(callbackLog).toEqual([]);
169 runRAFCallbacks();
170 // this should schedule work *after* the requestAnimationFrame but before the message handler
159 - scheduleCallback(callbackB);
171 + scheduleCallback(NormalPriority, callbackB);
172 expect(callbackLog).toEqual([]);
173 // now it should drain the message queue and do all scheduled work
174 runPostMessageCallbacks({timeLeftInFrame: 15});
@@ -166,7 +178,7 @@ describe('SchedulerDOM', () => {
178 advanceOneFrame({timeLeftInFrame: 15});
179
180 // see if more work can be done now.
169 - scheduleCallback(callbackC);
181 + scheduleCallback(NormalPriority, callbackC);
182 expect(callbackLog).toEqual(['A', 'B']);
183 advanceOneFrame({timeLeftInFrame: 15});
184 expect(callbackLog).toEqual(['A', 'B', 'C']);
@@ -176,11 +188,14 @@ describe('SchedulerDOM', () => {
188 'schedules callbacks in correct order and' +
189 'keeps calling them if there is time',
190 () => {
179 - const {unstable_scheduleCallback: scheduleCallback} = Scheduler;
191 + const {
192 + unstable_scheduleCallback: scheduleCallback,
193 + unstable_NormalPriority: NormalPriority,
194 + } = Scheduler;
195 const callbackLog = [];
196 const callbackA = jest.fn(() => {
197 callbackLog.push('A');
183 - scheduleCallback(callbackC);
198 + scheduleCallback(NormalPriority, callbackC);
199 });
200 const callbackB = jest.fn(() => {
201 callbackLog.push('B');
@@ -189,11 +204,11 @@ describe('SchedulerDOM', () => {
204 callbackLog.push('C');
205 });
206
192 - scheduleCallback(callbackA);
207 + scheduleCallback(NormalPriority, callbackA);
208 // initially waits to call the callback
209 expect(callbackLog).toEqual([]);
210 // continues waiting while B is scheduled
196 - scheduleCallback(callbackB);
211 + scheduleCallback(NormalPriority, callbackB);
212 expect(callbackLog).toEqual([]);
213 // after a delay, calls the scheduled callbacks,
214 // and also calls new callbacks scheduled by current callbacks
@@ -203,17 +218,20 @@ describe('SchedulerDOM', () => {
218 );
219
220 it('schedules callbacks in correct order when callbacks have many nested scheduleCallback calls', () => {
206 - const {unstable_scheduleCallback: scheduleCallback} = Scheduler;
221 + const {
222 + unstable_scheduleCallback: scheduleCallback,
223 + unstable_NormalPriority: NormalPriority,
224 + } = Scheduler;
225 const callbackLog = [];
226 const callbackA = jest.fn(() => {
227 callbackLog.push('A');
210 - scheduleCallback(callbackC);
211 - scheduleCallback(callbackD);
228 + scheduleCallback(NormalPriority, callbackC);
229 + scheduleCallback(NormalPriority, callbackD);
230 });
231 const callbackB = jest.fn(() => {
232 callbackLog.push('B');
215 - scheduleCallback(callbackE);
216 - scheduleCallback(callbackF);
233 + scheduleCallback(NormalPriority, callbackE);
234 + scheduleCallback(NormalPriority, callbackF);
235 });
236 const callbackC = jest.fn(() => {
237 callbackLog.push('C');
@@ -228,8 +246,8 @@ describe('SchedulerDOM', () => {
246 callbackLog.push('F');
247 });
248
231 - scheduleCallback(callbackA);
232 - scheduleCallback(callbackB);
249 + scheduleCallback(NormalPriority, callbackA);
250 + scheduleCallback(NormalPriority, callbackB);
251 // initially waits to call the callback
252 expect(callbackLog).toEqual([]);
253 // while flushing callbacks, calls as many as it has time for
@@ -238,22 +256,25 @@ describe('SchedulerDOM', () => {
256 });
257
258 it('schedules callbacks in correct order when they use scheduleCallback to schedule themselves', () => {
241 - const {unstable_scheduleCallback: scheduleCallback} = Scheduler;
259 + const {
260 + unstable_scheduleCallback: scheduleCallback,
261 + unstable_NormalPriority: NormalPriority,
262 + } = Scheduler;
263 const callbackLog = [];
264 let callbackAIterations = 0;
265 const callbackA = jest.fn(() => {
266 if (callbackAIterations < 1) {
246 - scheduleCallback(callbackA);
267 + scheduleCallback(NormalPriority, callbackA);
268 }
269 callbackLog.push('A' + callbackAIterations);
270 callbackAIterations++;
271 });
272 const callbackB = jest.fn(() => callbackLog.push('B'));
273
253 - scheduleCallback(callbackA);
274 + scheduleCallback(NormalPriority, callbackA);
275 // initially waits to call the callback
276 expect(callbackLog).toEqual([]);
256 - scheduleCallback(callbackB);
277 + scheduleCallback(NormalPriority, callbackB);
278 expect(callbackLog).toEqual([]);
279 // after a delay, calls the latest callback passed
280 advanceOneFrame({timeLeftInFrame: 15});
@@ -269,7 +290,10 @@ describe('SchedulerDOM', () => {
290
291 describe('when there is no more time left in the frame', () => {
292 it('calls any callback which has timed out, waits for others', () => {
272 - const {unstable_scheduleCallback: scheduleCallback} = Scheduler;
293 + const {
294 + unstable_scheduleCallback: scheduleCallback,
295 + unstable_NormalPriority: NormalPriority,
296 + } = Scheduler;
297 startOfLatestFrame = 1000000000000;
298 currentTime = startOfLatestFrame - 10;
299 const callbackLog = [];
@@ -278,9 +302,9 @@ describe('SchedulerDOM', () => {
302 const callbackB = jest.fn(() => callbackLog.push('B'));
303 const callbackC = jest.fn(() => callbackLog.push('C'));
304
281 - scheduleCallback(callbackA); // won't time out
282 - scheduleCallback(callbackB, {timeout: 100}); // times out later
283 - scheduleCallback(callbackC, {timeout: 2}); // will time out fast
305 + scheduleCallback(NormalPriority, callbackA); // won't time out
306 + scheduleCallback(NormalPriority, callbackB, {timeout: 100}); // times out later
307 + scheduleCallback(NormalPriority, callbackC, {timeout: 2}); // will time out fast
308
309 // push time ahead a bit so that we have no idle time
310 advanceOneFrame({timePastFrameDeadline: 16});
@@ -304,7 +328,10 @@ describe('SchedulerDOM', () => {
328
329 describe('when there is some time left in the frame', () => {
330 it('calls timed out callbacks and then any more pending callbacks, defers others if time runs out', () => {
307 - const {unstable_scheduleCallback: scheduleCallback} = Scheduler;
331 + const {
332 + unstable_scheduleCallback: scheduleCallback,
333 + unstable_NormalPriority: NormalPriority,
334 + } = Scheduler;
335 startOfLatestFrame = 1000000000000;
336 currentTime = startOfLatestFrame - 10;
337 const callbackLog = [];
@@ -318,10 +345,10 @@ describe('SchedulerDOM', () => {
345 const callbackC = jest.fn(() => callbackLog.push('C'));
346 const callbackD = jest.fn(() => callbackLog.push('D'));
347
321 - scheduleCallback(callbackA, {timeout: 100}); // won't time out
322 - scheduleCallback(callbackB, {timeout: 100}); // times out later
323 - scheduleCallback(callbackC, {timeout: 2}); // will time out fast
324 - scheduleCallback(callbackD, {timeout: 200}); // won't time out
348 + scheduleCallback(NormalPriority, callbackA, {timeout: 100}); // won't time out
349 + scheduleCallback(NormalPriority, callbackB, {timeout: 100}); // times out later
350 + scheduleCallback(NormalPriority, callbackC, {timeout: 2}); // will time out fast
351 + scheduleCallback(NormalPriority, callbackD, {timeout: 200}); // won't time out
352
353 advanceOneFrame({timeLeftInFrame: 15}); // runs rAF and postMessage callbacks
354
@@ -355,9 +382,10 @@ describe('SchedulerDOM', () => {
382 const {
383 unstable_scheduleCallback: scheduleCallback,
384 unstable_cancelCallback: cancelCallback,
385 + unstable_NormalPriority: NormalPriority,
386 } = Scheduler;
387 const cb = jest.fn();
360 - const callbackId = scheduleCallback(cb);
388 + const callbackId = scheduleCallback(NormalPriority, cb);
389 expect(cb).toHaveBeenCalledTimes(0);
390 cancelCallback(callbackId);
391 advanceOneFrame({timeLeftInFrame: 15});
@@ -369,14 +397,15 @@ describe('SchedulerDOM', () => {
397 const {
398 unstable_scheduleCallback: scheduleCallback,
399 unstable_cancelCallback: cancelCallback,
400 + unstable_NormalPriority: NormalPriority,
401 } = Scheduler;
402 const callbackLog = [];
403 const callbackA = jest.fn(() => callbackLog.push('A'));
404 const callbackB = jest.fn(() => callbackLog.push('B'));
405 const callbackC = jest.fn(() => callbackLog.push('C'));
377 - scheduleCallback(callbackA);
378 - const callbackId = scheduleCallback(callbackB);
379 - scheduleCallback(callbackC);
406 + scheduleCallback(NormalPriority, callbackA);
407 + const callbackId = scheduleCallback(NormalPriority, callbackB);
408 + scheduleCallback(NormalPriority, callbackC);
409 cancelCallback(callbackId);
410 cancelCallback(callbackId);
411 cancelCallback(callbackId);
@@ -393,6 +422,7 @@ describe('SchedulerDOM', () => {
422 const {
423 unstable_scheduleCallback: scheduleCallback,
424 unstable_cancelCallback: cancelCallback,
425 + unstable_NormalPriority: NormalPriority,
426 } = Scheduler;
427 const callbackLog = [];
428 let callbackBId;
@@ -401,8 +431,8 @@ describe('SchedulerDOM', () => {
431 cancelCallback(callbackBId);
432 });
433 const callbackB = jest.fn(() => callbackLog.push('B'));
404 - scheduleCallback(callbackA);
405 - callbackBId = scheduleCallback(callbackB);
434 + scheduleCallback(NormalPriority, callbackA);
435 + callbackBId = scheduleCallback(NormalPriority, callbackB);
436 // Initially doesn't call anything
437 expect(callbackLog).toEqual([]);
438 advanceOneFrame({timeLeftInFrame: 15});
@@ -430,7 +460,10 @@ describe('SchedulerDOM', () => {
460 *
461 */
462 it('still calls all callbacks within same frame', () => {
433 - const {unstable_scheduleCallback: scheduleCallback} = Scheduler;
463 + const {
464 + unstable_scheduleCallback: scheduleCallback,
465 + unstable_NormalPriority: NormalPriority,
466 + } = Scheduler;
467 const callbackLog = [];
468 const callbackA = jest.fn(() => callbackLog.push('A'));
469 const callbackB = jest.fn(() => {
@@ -443,11 +476,11 @@ describe('SchedulerDOM', () => {
476 throw new Error('D error');
477 });
478 const callbackE = jest.fn(() => callbackLog.push('E'));
446 - scheduleCallback(callbackA);
447 - scheduleCallback(callbackB);
448 - scheduleCallback(callbackC);
449 - scheduleCallback(callbackD);
450 - scheduleCallback(callbackE);
479 + scheduleCallback(NormalPriority, callbackA);
480 + scheduleCallback(NormalPriority, callbackB);
481 + scheduleCallback(NormalPriority, callbackC);
482 + scheduleCallback(NormalPriority, callbackD);
483 + scheduleCallback(NormalPriority, callbackE);
484 // Initially doesn't call anything
485 expect(callbackLog).toEqual([]);
486 catchPostMessageErrors = true;
@@ -476,7 +509,10 @@ describe('SchedulerDOM', () => {
509 *
510 */
511 it('and with some timed out callbacks, still calls all callbacks within same frame', () => {
479 - const {unstable_scheduleCallback: scheduleCallback} = Scheduler;
512 + const {
513 + unstable_scheduleCallback: scheduleCallback,
514 + unstable_NormalPriority: NormalPriority,
515 + } = Scheduler;
516 const callbackLog = [];
517 const callbackA = jest.fn(() => {
518 callbackLog.push('A');
@@ -489,11 +525,11 @@ describe('SchedulerDOM', () => {
525 throw new Error('D error');
526 });
527 const callbackE = jest.fn(() => callbackLog.push('E'));
492 - scheduleCallback(callbackA);
493 - scheduleCallback(callbackB);
494 - scheduleCallback(callbackC, {timeout: 2}); // times out fast
495 - scheduleCallback(callbackD, {timeout: 2}); // times out fast
496 - scheduleCallback(callbackE, {timeout: 2}); // times out fast
528 + scheduleCallback(NormalPriority, callbackA);
529 + scheduleCallback(NormalPriority, callbackB);
530 + scheduleCallback(NormalPriority, callbackC, {timeout: 2}); // times out fast
531 + scheduleCallback(NormalPriority, callbackD, {timeout: 2}); // times out fast
532 + scheduleCallback(NormalPriority, callbackE, {timeout: 2}); // times out fast
533 // Initially doesn't call anything
534 expect(callbackLog).toEqual([]);
535 catchPostMessageErrors = true;
@@ -522,7 +558,10 @@ describe('SchedulerDOM', () => {
558 *
559 */
560 it('still calls all callbacks within same frame', () => {
525 - const {unstable_scheduleCallback: scheduleCallback} = Scheduler;
561 + const {
562 + unstable_scheduleCallback: scheduleCallback,
563 + unstable_NormalPriority: NormalPriority,
564 + } = Scheduler;
565 const callbackLog = [];
566 const callbackA = jest.fn(() => {
567 callbackLog.push('A');
@@ -544,11 +583,11 @@ describe('SchedulerDOM', () => {
583 callbackLog.push('E');
584 throw new Error('E error');
585 });
547 - scheduleCallback(callbackA);
548 - scheduleCallback(callbackB);
549 - scheduleCallback(callbackC);
550 - scheduleCallback(callbackD);
551 - scheduleCallback(callbackE);
586 + scheduleCallback(NormalPriority, callbackA);
587 + scheduleCallback(NormalPriority, callbackB);
588 + scheduleCallback(NormalPriority, callbackC);
589 + scheduleCallback(NormalPriority, callbackD);
590 + scheduleCallback(NormalPriority, callbackE);
591 // Initially doesn't call anything
592 expect(callbackLog).toEqual([]);
593 catchPostMessageErrors = true;
@@ -583,7 +622,10 @@ describe('SchedulerDOM', () => {
622 *
623 */
624 it('and with all timed out callbacks, still calls all callbacks within same frame', () => {
586 - const {unstable_scheduleCallback: scheduleCallback} = Scheduler;
625 + const {
626 + unstable_scheduleCallback: scheduleCallback,
627 + unstable_NormalPriority: NormalPriority,
628 + } = Scheduler;
629 const callbackLog = [];
630 const callbackA = jest.fn(() => {
631 callbackLog.push('A');
@@ -605,11 +647,11 @@ describe('SchedulerDOM', () => {
647 callbackLog.push('E');
648 throw new Error('E error');
649 });
608 - scheduleCallback(callbackA, {timeout: 2}); // times out fast
609 - scheduleCallback(callbackB, {timeout: 2}); // times out fast
610 - scheduleCallback(callbackC, {timeout: 2}); // times out fast
611 - scheduleCallback(callbackD, {timeout: 2}); // times out fast
612 - scheduleCallback(callbackE, {timeout: 2}); // times out fast
650 + scheduleCallback(NormalPriority, callbackA, {timeout: 2}); // times out fast
651 + scheduleCallback(NormalPriority, callbackB, {timeout: 2}); // times out fast
652 + scheduleCallback(NormalPriority, callbackC, {timeout: 2}); // times out fast
653 + scheduleCallback(NormalPriority, callbackD, {timeout: 2}); // times out fast
654 + scheduleCallback(NormalPriority, callbackE, {timeout: 2}); // times out fast
655 // Initially doesn't call anything
656 expect(callbackLog).toEqual([]);
657 catchPostMessageErrors = true;
@@ -664,7 +706,10 @@ describe('SchedulerDOM', () => {
706 *
707 */
708 it('still calls all callbacks within same frame', () => {
667 - const {unstable_scheduleCallback: scheduleCallback} = Scheduler;
709 + const {
710 + unstable_scheduleCallback: scheduleCallback,
711 + unstable_NormalPriority: NormalPriority,
712 + } = Scheduler;
713 startOfLatestFrame = 1000000000000;
714 currentTime = startOfLatestFrame - 10;
715 catchPostMessageErrors = true;
@@ -698,13 +743,13 @@ describe('SchedulerDOM', () => {
743 });
744 const callbackG = jest.fn(() => callbackLog.push('G'));
745
701 - scheduleCallback(callbackA);
702 - scheduleCallback(callbackB);
703 - scheduleCallback(callbackC);
704 - scheduleCallback(callbackD);
705 - scheduleCallback(callbackE);
706 - scheduleCallback(callbackF);
707 - scheduleCallback(callbackG);
746 + scheduleCallback(NormalPriority, callbackA);
747 + scheduleCallback(NormalPriority, callbackB);
748 + scheduleCallback(NormalPriority, callbackC);
749 + scheduleCallback(NormalPriority, callbackD);
750 + scheduleCallback(NormalPriority, callbackE);
751 + scheduleCallback(NormalPriority, callbackF);
752 + scheduleCallback(NormalPriority, callbackG);
753
754 // does nothing initially
755 expect(callbackLog).toEqual([]);
packages/scheduler/src/__tests__/SchedulerNoDOM-test.js
+26 -44
@@ -9,10 +9,11 @@
9
10 'use strict';
11
12 +let Scheduler;
13 let scheduleCallback;
13 -let runWithPriority;
14 let ImmediatePriority;
15 let UserBlockingPriority;
16 +let NormalPriority;
17
18 describe('SchedulerNoDOM', () => {
19 // If Scheduler runs in a non-DOM environment, it falls back to a naive
@@ -30,22 +31,22 @@ describe('SchedulerNoDOM', () => {
31 ),
32 );
33
33 - const Scheduler = require('scheduler');
34 + Scheduler = require('scheduler');
35 scheduleCallback = Scheduler.unstable_scheduleCallback;
35 - runWithPriority = Scheduler.unstable_runWithPriority;
36 ImmediatePriority = Scheduler.unstable_ImmediatePriority;
37 UserBlockingPriority = Scheduler.unstable_UserBlockingPriority;
38 + NormalPriority = Scheduler.unstable_NormalPriority;
39 });
40
41 it('runAllTimers flushes all scheduled callbacks', () => {
42 let log = [];
42 - scheduleCallback(() => {
43 + scheduleCallback(NormalPriority, () => {
44 log.push('A');
45 });
45 - scheduleCallback(() => {
46 + scheduleCallback(NormalPriority, () => {
47 log.push('B');
48 });
48 - scheduleCallback(() => {
49 + scheduleCallback(NormalPriority, () => {
50 log.push('C');
51 });
52 expect(log).toEqual([]);
@@ -56,19 +57,17 @@ describe('SchedulerNoDOM', () => {
57 it('executes callbacks in order of priority', () => {
58 let log = [];
59
59 - scheduleCallback(() => {
60 + scheduleCallback(NormalPriority, () => {
61 log.push('A');
62 });
62 - scheduleCallback(() => {
63 + scheduleCallback(NormalPriority, () => {
64 log.push('B');
65 });
65 - runWithPriority(UserBlockingPriority, () => {
66 - scheduleCallback(() => {
67 - log.push('C');
68 - });
69 - scheduleCallback(() => {
70 - log.push('D');
71 - });
66 + scheduleCallback(UserBlockingPriority, () => {
67 + log.push('C');
68 + });
69 + scheduleCallback(UserBlockingPriority, () => {
70 + log.push('D');
71 });
72
73 expect(log).toEqual([]);
@@ -76,39 +75,22 @@ describe('SchedulerNoDOM', () => {
75 expect(log).toEqual(['C', 'D', 'A', 'B']);
76 });
77
79 - it('calls immediate callbacks immediately', () => {
78 + it('handles errors', () => {
79 let log = [];
80
82 - runWithPriority(ImmediatePriority, () => {
83 - scheduleCallback(() => {
84 - log.push('A');
85 - scheduleCallback(() => {
86 - log.push('B');
87 - });
88 - });
81 + scheduleCallback(ImmediatePriority, () => {
82 + log.push('A');
83 + throw new Error('Oops A');
84 + });
85 + scheduleCallback(ImmediatePriority, () => {
86 + log.push('B');
87 + });
88 + scheduleCallback(ImmediatePriority, () => {
89 + log.push('C');
90 + throw new Error('Oops C');
91 });
92
91 - expect(log).toEqual(['A', 'B']);
92 - });
93 -
94 - it('handles errors', () => {
95 - let log = [];
96 -
97 - expect(() => {
98 - runWithPriority(ImmediatePriority, () => {
99 - scheduleCallback(() => {
100 - log.push('A');
101 - throw new Error('Oops A');
102 - });
103 - scheduleCallback(() => {
104 - log.push('B');
105 - });
106 - scheduleCallback(() => {
107 - log.push('C');
108 - throw new Error('Oops C');
109 - });
110 - });
111 - }).toThrow('Oops A');
93 + expect(() => jest.runAllTimers()).toThrow('Oops A');
94
95 expect(log).toEqual(['A']);
96
packages/shared/forks/ReactFeatureFlags.new-scheduler.js new
+30
@@ -0,0 +1,30 @@
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 strict
8 + */
9 +
10 +export const enableUserTimingAPI = __DEV__;
11 +export const debugRenderPhaseSideEffects = false;
12 +export const debugRenderPhaseSideEffectsForStrictMode = __DEV__;
13 +export const replayFailedUnitOfWorkWithInvokeGuardedCallback = __DEV__;
14 +export const warnAboutDeprecatedLifecycles = true;
15 +export const enableProfilerTimer = __PROFILE__;
16 +export const enableSchedulerTracing = __PROFILE__;
17 +export const enableSuspenseServerRenderer = false; // TODO: __DEV__? Here it might just be false.
18 +export const enableSchedulerDebugging = false;
19 +export function addUserTimingListener() {
20 + throw new Error('Not implemented.');
21 +}
22 +export const disableJavaScriptURLs = false;
23 +export const disableYielding = false;
24 +export const disableInputAttributeSyncing = false;
25 +export const enableStableConcurrentModeAPIs = false;
26 +export const warnAboutShorthandPropertyCollision = false;
27 +export const warnAboutDeprecatedSetNativeProps = false;
28 +export const enableEventAPI = false;
29 +
30 +export const enableNewScheduler = true;
packages/shared/forks/ReactFeatureFlags.www-new-scheduler.js new
+39
@@ -0,0 +1,39 @@
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 +import typeof * as FeatureFlagsType from 'shared/ReactFeatureFlags';
11 +import typeof * as FeatureFlagsShimType from './ReactFeatureFlags.www-new-scheduler';
12 +
13 +export {
14 + enableUserTimingAPI,
15 + debugRenderPhaseSideEffects,
16 + debugRenderPhaseSideEffectsForStrictMode,
17 + replayFailedUnitOfWorkWithInvokeGuardedCallback,
18 + warnAboutDeprecatedLifecycles,
19 + enableProfilerTimer,
20 + enableSchedulerTracing,
21 + enableSuspenseServerRenderer,
22 + enableSchedulerDebugging,
23 + addUserTimingListener,
24 + disableJavaScriptURLs,
25 + disableYielding,
26 + disableInputAttributeSyncing,
27 + enableStableConcurrentModeAPIs,
28 + warnAboutShorthandPropertyCollision,
29 + warnAboutDeprecatedSetNativeProps,
30 + enableEventAPI,
31 +} from './ReactFeatureFlags.www';
32 +
33 +export const enableNewScheduler = true;
34 +
35 +// Flow magic to verify the exports of this file match the original version.
36 +// eslint-disable-next-line no-unused-vars
37 +type Check<_X, Y: _X, X: Y = _X> = null;
38 +// eslint-disable-next-line no-unused-expressions
39 +(null: Check<FeatureFlagsShimType, FeatureFlagsType>);
scripts/circleci/test_entry_point.sh
+1
@@ -11,6 +11,7 @@ if [ $((0 % CIRCLE_NODE_TOTAL)) -eq "$CIRCLE_NODE_INDEX" ]; then
11 COMMANDS_TO_RUN+=('node ./scripts/tasks/flow-ci')
12 COMMANDS_TO_RUN+=('node ./scripts/tasks/eslint')
13 COMMANDS_TO_RUN+=('yarn test --maxWorkers=2')
14 + COMMANDS_TO_RUN+=('yarn test-new-scheduler --maxWorkers=2')
15 COMMANDS_TO_RUN+=('yarn test-persistent --maxWorkers=2')
16 COMMANDS_TO_RUN+=('./scripts/circleci/check_license.sh')
17 COMMANDS_TO_RUN+=('./scripts/circleci/check_modules.sh')
scripts/jest/matchers/schedulerTestMatchers.js
+10
@@ -48,6 +48,15 @@ function toFlushWithoutYielding(Scheduler) {
48 return toFlushAndYield(Scheduler, []);
49 }
50
51 +function toFlushExpired(Scheduler, expectedYields) {
52 + assertYieldsWereCleared(Scheduler);
53 + Scheduler.unstable_flushExpired();
54 + const actualYields = Scheduler.unstable_clearYields();
55 + return captureAssertion(() => {
56 + expect(actualYields).toEqual(expectedYields);
57 + });
58 +}
59 +
60 function toHaveYielded(Scheduler, expectedYields) {
61 return captureAssertion(() => {
62 const actualYields = Scheduler.unstable_clearYields();
@@ -68,6 +77,7 @@ module.exports = {
77 toFlushAndYield,
78 toFlushAndYieldThrough,
79 toFlushWithoutYielding,
80 + toFlushExpired,
81 toHaveYielded,
82 toFlushAndThrow,
83 };
scripts/rollup/bundles.js
+15
@@ -110,6 +110,21 @@ const bundles = [
110 externals: ['react'],
111 },
112
113 + /******* React DOM (new scheduler) *******/
114 + {
115 + bundleTypes: [
116 + FB_WWW_DEV,
117 + FB_WWW_PROD,
118 + FB_WWW_PROFILING,
119 + NODE_DEV,
120 + NODE_PROD,
121 + ],
122 + moduleType: RENDERER,
123 + entry: 'react-dom/unstable-new-scheduler',
124 + global: 'ReactDOMNewScheduler',
125 + externals: ['react'],
126 + },
127 +
128 /******* Test Utils *******/
129 {
130 moduleType: RENDERER_UTILS,
scripts/rollup/forks.js
+6
@@ -68,6 +68,12 @@ const forks = Object.freeze({
68 // We have a few forks for different environments.
69 'shared/ReactFeatureFlags': (bundleType, entry) => {
70 switch (entry) {
71 + case 'react-dom/unstable-new-scheduler': {
72 + if (entry === 'react-dom/unstable-new-scheduler') {
73 + return 'shared/forks/ReactFeatureFlags.www-new-scheduler.js';
74 + }
75 + return null;
76 + }
77 case 'react-native-renderer':
78 switch (bundleType) {
79 case RN_FB_DEV:
scripts/shared/inlinedHostConfigs.js
+5 -1
@@ -9,7 +9,11 @@
9 module.exports = [
10 {
11 shortName: 'dom',
12 - entryPoints: ['react-dom', 'react-dom/unstable-fizz.node'],
12 + entryPoints: [
13 + 'react-dom',
14 + 'react-dom/unstable-fizz.node',
15 + 'react-dom/unstable-new-scheduler',
16 + ],
17 isFlowTyped: true,
18 isFizzSupported: true,
19 },