Skip to content

Commit

Permalink
Rewrite ReactFiberScheduler
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
acdlite committed Mar 29, 2019
1 parent 08055a6 commit 2f60c47
Show file tree
Hide file tree
Showing 30 changed files with 4,373 additions and 1,080 deletions.
7 changes: 5 additions & 2 deletions packages/react-cache/src/LRU.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,10 @@ import * as Scheduler from 'scheduler';

// Intentionally not named imports because Rollup would
// use dynamic dispatch for CommonJS interop named imports.
const {unstable_scheduleCallback: scheduleCallback} = Scheduler;
const {
unstable_scheduleCallback: scheduleCallback,
unstable_IdlePriority: IdlePriority,
} = Scheduler;

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

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@

let React;
let ReactTestRenderer;
let Scheduler;
let ReactDebugTools;
let act;

Expand All @@ -20,6 +21,7 @@ describe('ReactHooksInspectionIntegration', () => {
jest.resetModules();
React = require('react');
ReactTestRenderer = require('react-test-renderer');
Scheduler = require('scheduler');
act = ReactTestRenderer.act;
ReactDebugTools = require('react-debug-tools');
});
Expand Down Expand Up @@ -618,6 +620,8 @@ describe('ReactHooksInspectionIntegration', () => {

await LazyFoo;

Scheduler.flushAll();

let childFiber = renderer.root._currentFiber();
let tree = ReactDebugTools.inspectHooksOfFiber(childFiber);
expect(tree).toEqual([
Expand Down
3 changes: 2 additions & 1 deletion packages/react-dom/src/__tests__/ReactDOMHooks-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,8 @@ describe('ReactDOMHooks', () => {
expect(container3.textContent).toBe('6');
});

it('can batch synchronous work inside effects with other work', () => {
// TODO: This behavior is wrong. Fix this in the old implementation.
it.skip('can batch synchronous work inside effects with other work', () => {
let otherContainer = document.createElement('div');

let calledA = false;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ let ReactDOM;
let Suspense;
let ReactCache;
let ReactTestUtils;
let Scheduler;
let TextResource;
let act;

Expand All @@ -26,6 +27,7 @@ describe('ReactDOMSuspensePlaceholder', () => {
ReactDOM = require('react-dom');
ReactCache = require('react-cache');
ReactTestUtils = require('react-dom/test-utils');
Scheduler = require('scheduler');
act = ReactTestUtils.act;
Suspense = React.Suspense;
container = document.createElement('div');
Expand Down Expand Up @@ -94,6 +96,8 @@ describe('ReactDOMSuspensePlaceholder', () => {

await advanceTimers(1000);

Scheduler.flushAll();

expect(divs[0].current.style.display).toEqual('');
expect(divs[1].current.style.display).toEqual('');
// This div's display was set with a prop.
Expand All @@ -115,6 +119,8 @@ describe('ReactDOMSuspensePlaceholder', () => {

await advanceTimers(1000);

Scheduler.flushAll();

expect(container.textContent).toEqual('ABC');
});

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

await advanceTimers(1000);

Scheduler.flushAll();

expect(container.innerHTML).toEqual(
'<span style="display: inline;">Sibling</span><span style="">Async</span>',
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
let React;
let ReactDOM;
let ReactDOMServer;
let Scheduler;

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

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

jest.runAllTimers();
await Promise.resolve();
Scheduler.flushAll();
expect(element.textContent).toBe('Hello world');
});
});
2 changes: 1 addition & 1 deletion packages/react-reconciler/src/ReactDebugFiberPerf.js
Original file line number Diff line number Diff line change
Expand Up @@ -253,7 +253,7 @@ export function stopRequestCallbackTimer(
expirationTime: number,
): void {
if (enableUserTimingAPI) {
if (supportsUserTiming) {
if (supportsUserTiming && isWaitingForCallback) {
isWaitingForCallback = false;
const warning = didExpire ? 'React was blocked by main thread' : null;
endMark(
Expand Down
39 changes: 39 additions & 0 deletions packages/react-reconciler/src/ReactFiberExpirationTime.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,17 @@
* @flow
*/

import type {ReactPriorityLevel} from './SchedulerWithReactIntegration';

import MAX_SIGNED_31_BIT_INT from './maxSigned31BitInt';

import {
ImmediatePriority,
UserBlockingPriority,
NormalPriority,
IdlePriority,
} from './SchedulerWithReactIntegration';

export type ExpirationTime = number;

export const NoWork = 0;
Expand Down Expand Up @@ -46,6 +55,8 @@ function computeExpirationBucket(
);
}

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

Expand Down Expand Up @@ -80,3 +91,31 @@ export function computeInteractiveExpiration(currentTime: ExpirationTime) {
HIGH_PRIORITY_BATCH_SIZE,
);
}

export function inferPriorityFromExpirationTime(
currentTime: ExpirationTime,
expirationTime: ExpirationTime,
): ReactPriorityLevel {
if (expirationTime === Sync) {
return ImmediatePriority;
}
if (expirationTime === Never) {
return IdlePriority;
}
const msUntil =
msToExpirationTime(expirationTime) - msToExpirationTime(currentTime);
if (msUntil <= 0) {
return ImmediatePriority;
}
if (msUntil <= HIGH_PRIORITY_EXPIRATION) {
return UserBlockingPriority;
}
if (msUntil <= LOW_PRIORITY_EXPIRATION) {
return NormalPriority;
}

// TODO: Handle LowPriority

// Assume anything lower has idle priority
return IdlePriority;
}
2 changes: 0 additions & 2 deletions packages/react-reconciler/src/ReactFiberReconciler.js
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,6 @@ import {
requestCurrentTime,
computeExpirationForFiber,
scheduleWork,
requestWork,
flushRoot,
batchedUpdates,
unbatchedUpdates,
Expand Down Expand Up @@ -300,7 +299,6 @@ export function updateContainer(

export {
flushRoot,
requestWork,
computeUniqueAsyncExpiration,
batchedUpdates,
unbatchedUpdates,
Expand Down
19 changes: 19 additions & 0 deletions packages/react-reconciler/src/ReactFiberRoot.js
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,13 @@ type BaseFiberRootProperties = {|
firstBatch: Batch | null,
// Linked-list of roots
nextScheduledRoot: FiberRoot | null,

// New Scheduler fields
callbackNode: *,
callbackExpirationTime: ExpirationTime,
firstPendingTime: ExpirationTime,
lastPendingTime: ExpirationTime,
pingTime: ExpirationTime,
|};

// The following attributes are only used by interaction tracing builds.
Expand Down Expand Up @@ -145,6 +152,12 @@ export function createFiberRoot(
interactionThreadID: unstable_getThreadID(),
memoizedInteractions: new Set(),
pendingInteractionMap: new Map(),

callbackNode: null,
callbackExpirationTime: NoWork,
firstPendingTime: NoWork,
lastPendingTime: NoWork,
pingTime: NoWork,
}: FiberRoot);
} else {
root = ({
Expand Down Expand Up @@ -172,6 +185,12 @@ export function createFiberRoot(
expirationTime: NoWork,
firstBatch: null,
nextScheduledRoot: null,

callbackNode: null,
callbackExpirationTime: NoWork,
firstPendingTime: NoWork,
lastPendingTime: NoWork,
pingTime: NoWork,
}: BaseFiberRootProperties);
}

Expand Down
8 changes: 4 additions & 4 deletions packages/react-reconciler/src/ReactFiberScheduler.js
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,6 @@ import {
markLegacyErrorBoundaryAsFailed as markLegacyErrorBoundaryAsFailed_old,
isAlreadyFailedLegacyErrorBoundary as isAlreadyFailedLegacyErrorBoundary_old,
scheduleWork as scheduleWork_old,
requestWork as requestWork_old,
flushRoot as flushRoot_old,
batchedUpdates as batchedUpdates_old,
unbatchedUpdates as unbatchedUpdates_old,
Expand All @@ -35,6 +34,7 @@ import {
computeUniqueAsyncExpiration as computeUniqueAsyncExpiration_old,
flushPassiveEffects as flushPassiveEffects_old,
warnIfNotCurrentlyBatchingInDev as warnIfNotCurrentlyBatchingInDev_old,
inferStartTimeFromExpirationTime as inferStartTimeFromExpirationTime_old,
} from './ReactFiberScheduler.old';

import {
Expand All @@ -50,7 +50,6 @@ import {
markLegacyErrorBoundaryAsFailed as markLegacyErrorBoundaryAsFailed_new,
isAlreadyFailedLegacyErrorBoundary as isAlreadyFailedLegacyErrorBoundary_new,
scheduleWork as scheduleWork_new,
requestWork as requestWork_new,
flushRoot as flushRoot_new,
batchedUpdates as batchedUpdates_new,
unbatchedUpdates as unbatchedUpdates_new,
Expand All @@ -63,6 +62,7 @@ import {
computeUniqueAsyncExpiration as computeUniqueAsyncExpiration_new,
flushPassiveEffects as flushPassiveEffects_new,
warnIfNotCurrentlyBatchingInDev as warnIfNotCurrentlyBatchingInDev_new,
inferStartTimeFromExpirationTime as inferStartTimeFromExpirationTime_new,
} from './ReactFiberScheduler.new';

export let requestCurrentTime = requestCurrentTime_old;
Expand All @@ -77,7 +77,6 @@ export let resolveRetryThenable = resolveRetryThenable_old;
export let markLegacyErrorBoundaryAsFailed = markLegacyErrorBoundaryAsFailed_old;
export let isAlreadyFailedLegacyErrorBoundary = isAlreadyFailedLegacyErrorBoundary_old;
export let scheduleWork = scheduleWork_old;
export let requestWork = requestWork_old;
export let flushRoot = flushRoot_old;
export let batchedUpdates = batchedUpdates_old;
export let unbatchedUpdates = unbatchedUpdates_old;
Expand All @@ -90,6 +89,7 @@ export let flushInteractiveUpdates = flushInteractiveUpdates_old;
export let computeUniqueAsyncExpiration = computeUniqueAsyncExpiration_old;
export let flushPassiveEffects = flushPassiveEffects_old;
export let warnIfNotCurrentlyBatchingInDev = warnIfNotCurrentlyBatchingInDev_old;
export let inferStartTimeFromExpirationTime = inferStartTimeFromExpirationTime_old;

if (enableNewScheduler) {
requestCurrentTime = requestCurrentTime_new;
Expand All @@ -104,7 +104,6 @@ if (enableNewScheduler) {
markLegacyErrorBoundaryAsFailed = markLegacyErrorBoundaryAsFailed_new;
isAlreadyFailedLegacyErrorBoundary = isAlreadyFailedLegacyErrorBoundary_new;
scheduleWork = scheduleWork_new;
requestWork = requestWork_new;
flushRoot = flushRoot_new;
batchedUpdates = batchedUpdates_new;
unbatchedUpdates = unbatchedUpdates_new;
Expand All @@ -117,6 +116,7 @@ if (enableNewScheduler) {
computeUniqueAsyncExpiration = computeUniqueAsyncExpiration_new;
flushPassiveEffects = flushPassiveEffects_new;
warnIfNotCurrentlyBatchingInDev = warnIfNotCurrentlyBatchingInDev_new;
inferStartTimeFromExpirationTime = inferStartTimeFromExpirationTime_new;
}

export type Thenable = {
Expand Down
Loading

0 comments on commit 2f60c47

Please sign in to comment.