Skip to content

Commit 4d5cb64

Browse files
authored
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.
1 parent aed0e1c commit 4d5cb64

36 files changed

+4754
-1207
lines changed

packages/react-cache/src/LRU.js

+5-2
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,10 @@ import * as Scheduler from 'scheduler';
1111

1212
// Intentionally not named imports because Rollup would
1313
// 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;
1518

1619
type Entry<T> = {|
1720
value: T,
@@ -34,7 +37,7 @@ export function createLRU<T>(limit: number) {
3437
// The cache size exceeds the limit. Schedule a callback to delete the
3538
// least recently used entries.
3639
cleanUpIsScheduled = true;
37-
scheduleCallback(cleanUp);
40+
scheduleCallback(IdlePriority, cleanUp);
3841
}
3942
}
4043

packages/react-debug-tools/src/__tests__/ReactHooksInspectionIntegration-test.js

+4
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212

1313
let React;
1414
let ReactTestRenderer;
15+
let Scheduler;
1516
let ReactDebugTools;
1617
let act;
1718

@@ -20,6 +21,7 @@ describe('ReactHooksInspectionIntegration', () => {
2021
jest.resetModules();
2122
React = require('react');
2223
ReactTestRenderer = require('react-test-renderer');
24+
Scheduler = require('scheduler');
2325
act = ReactTestRenderer.act;
2426
ReactDebugTools = require('react-debug-tools');
2527
});
@@ -618,6 +620,8 @@ describe('ReactHooksInspectionIntegration', () => {
618620

619621
await LazyFoo;
620622

623+
Scheduler.flushAll();
624+
621625
let childFiber = renderer.root._currentFiber();
622626
let tree = ReactDebugTools.inspectHooksOfFiber(childFiber);
623627
expect(tree).toEqual([

packages/react-dom/src/__tests__/ReactDOMHooks-test.js packages/react-dom/src/__tests__/ReactDOMHooks-test.internal.js

+28-9
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,8 @@
99

1010
'use strict';
1111

12+
let ReactFeatureFlags;
13+
let enableNewScheduler;
1214
let React;
1315
let ReactDOM;
1416
let Scheduler;
@@ -19,6 +21,8 @@ describe('ReactDOMHooks', () => {
1921
beforeEach(() => {
2022
jest.resetModules();
2123

24+
ReactFeatureFlags = require('shared/ReactFeatureFlags');
25+
enableNewScheduler = ReactFeatureFlags.enableNewScheduler;
2226
React = require('react');
2327
ReactDOM = require('react-dom');
2428
Scheduler = require('scheduler');
@@ -97,15 +101,30 @@ describe('ReactDOMHooks', () => {
97101
}
98102

99103
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+
}
109128
});
110129

111130
it('should not bail out when an update is scheduled from within an event handler', () => {

packages/react-dom/src/__tests__/ReactDOMSuspensePlaceholder-test.js

+8
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ let ReactDOM;
1414
let Suspense;
1515
let ReactCache;
1616
let ReactTestUtils;
17+
let Scheduler;
1718
let TextResource;
1819
let act;
1920

@@ -26,6 +27,7 @@ describe('ReactDOMSuspensePlaceholder', () => {
2627
ReactDOM = require('react-dom');
2728
ReactCache = require('react-cache');
2829
ReactTestUtils = require('react-dom/test-utils');
30+
Scheduler = require('scheduler');
2931
act = ReactTestUtils.act;
3032
Suspense = React.Suspense;
3133
container = document.createElement('div');
@@ -94,6 +96,8 @@ describe('ReactDOMSuspensePlaceholder', () => {
9496

9597
await advanceTimers(500);
9698

99+
Scheduler.flushAll();
100+
97101
expect(divs[0].current.style.display).toEqual('');
98102
expect(divs[1].current.style.display).toEqual('');
99103
// This div's display was set with a prop.
@@ -115,6 +119,8 @@ describe('ReactDOMSuspensePlaceholder', () => {
115119

116120
await advanceTimers(500);
117121

122+
Scheduler.flushAll();
123+
118124
expect(container.textContent).toEqual('ABC');
119125
});
120126

@@ -160,6 +166,8 @@ describe('ReactDOMSuspensePlaceholder', () => {
160166

161167
await advanceTimers(500);
162168

169+
Scheduler.flushAll();
170+
163171
expect(container.innerHTML).toEqual(
164172
'<span style="display: inline;">Sibling</span><span style="">Async</span>',
165173
);

packages/react-dom/src/__tests__/ReactServerRenderingHydration-test.js

+3
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
let React;
1313
let ReactDOM;
1414
let ReactDOMServer;
15+
let Scheduler;
1516

1617
// These tests rely both on ReactDOMServer and ReactDOM.
1718
// If a test only needs ReactDOMServer, put it in ReactServerRendering-test instead.
@@ -21,6 +22,7 @@ describe('ReactDOMServerHydration', () => {
2122
React = require('react');
2223
ReactDOM = require('react-dom');
2324
ReactDOMServer = require('react-dom/server');
25+
Scheduler = require('scheduler');
2426
});
2527

2628
it('should have the correct mounting behavior (old hydrate API)', () => {
@@ -498,6 +500,7 @@ describe('ReactDOMServerHydration', () => {
498500

499501
jest.runAllTimers();
500502
await Promise.resolve();
503+
Scheduler.flushAll();
501504
expect(element.textContent).toBe('Hello world');
502505
});
503506
});
Original file line numberDiff line numberDiff line change
@@ -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
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,17 @@
77
* @flow
88
*/
99

10+
import type {ReactPriorityLevel} from './SchedulerWithReactIntegration';
11+
1012
import MAX_SIGNED_31_BIT_INT from './maxSigned31BitInt';
1113

14+
import {
15+
ImmediatePriority,
16+
UserBlockingPriority,
17+
NormalPriority,
18+
IdlePriority,
19+
} from './SchedulerWithReactIntegration';
20+
1221
export type ExpirationTime = number;
1322

1423
export const NoWork = 0;
@@ -46,6 +55,8 @@ function computeExpirationBucket(
4655
);
4756
}
4857

58+
// TODO: This corresponds to Scheduler's NormalPriority, not LowPriority. Update
59+
// the names to reflect.
4960
export const LOW_PRIORITY_EXPIRATION = 5000;
5061
export const LOW_PRIORITY_BATCH_SIZE = 250;
5162

@@ -80,3 +91,31 @@ export function computeInteractiveExpiration(currentTime: ExpirationTime) {
8091
HIGH_PRIORITY_BATCH_SIZE,
8192
);
8293
}
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
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,6 @@ import {
4343
requestCurrentTime,
4444
computeExpirationForFiber,
4545
scheduleWork,
46-
requestWork,
4746
flushRoot,
4847
batchedUpdates,
4948
unbatchedUpdates,
@@ -300,7 +299,6 @@ export function updateContainer(
300299

301300
export {
302301
flushRoot,
303-
requestWork,
304302
computeUniqueAsyncExpiration,
305303
batchedUpdates,
306304
unbatchedUpdates,

0 commit comments

Comments
 (0)