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.
This commit is contained in:
Andrew Clark 2019-04-02 15:49:07 -07:00 committed by GitHub
parent aed0e1c30c
commit 4d5cb64aa2
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
36 changed files with 4784 additions and 1237 deletions

View File

@ -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,
@ -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);
}
}

View File

@ -12,6 +12,7 @@
let React;
let ReactTestRenderer;
let Scheduler;
let ReactDebugTools;
let act;
@ -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');
});
@ -618,6 +620,8 @@ describe('ReactHooksInspectionIntegration', () => {
await LazyFoo;
Scheduler.flushAll();
let childFiber = renderer.root._currentFiber();
let tree = ReactDebugTools.inspectHooksOfFiber(childFiber);
expect(tree).toEqual([

View File

@ -9,6 +9,8 @@
'use strict';
let ReactFeatureFlags;
let enableNewScheduler;
let React;
let ReactDOM;
let Scheduler;
@ -19,6 +21,8 @@ describe('ReactDOMHooks', () => {
beforeEach(() => {
jest.resetModules();
ReactFeatureFlags = require('shared/ReactFeatureFlags');
enableNewScheduler = ReactFeatureFlags.enableNewScheduler;
React = require('react');
ReactDOM = require('react-dom');
Scheduler = require('scheduler');
@ -97,6 +101,20 @@ describe('ReactDOMHooks', () => {
}
ReactDOM.render(<Foo />, container);
if (enableNewScheduler) {
// The old behavior was accidental; in the new scheduler, flushing passive
// effects also flushes synchronous work, even inside batchedUpdates.
ReactDOM.unstable_batchedUpdates(() => {
_set(0); // Forces the effect to be flushed
expect(otherContainer.textContent).toBe('A');
ReactDOM.render(<B />, otherContainer);
expect(otherContainer.textContent).toBe('A');
});
expect(otherContainer.textContent).toBe('B');
expect(calledA).toBe(true);
expect(calledB).toBe(true);
} else {
ReactDOM.unstable_batchedUpdates(() => {
_set(0); // Forces the effect to be flushed
expect(otherContainer.textContent).toBe('');
@ -106,6 +124,7 @@ describe('ReactDOMHooks', () => {
expect(otherContainer.textContent).toBe('B');
expect(calledA).toBe(false); // It was in a batch
expect(calledB).toBe(true);
}
});
it('should not bail out when an update is scheduled from within an event handler', () => {

View File

@ -14,6 +14,7 @@ let ReactDOM;
let Suspense;
let ReactCache;
let ReactTestUtils;
let Scheduler;
let TextResource;
let act;
@ -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');
@ -94,6 +96,8 @@ describe('ReactDOMSuspensePlaceholder', () => {
await advanceTimers(500);
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.
@ -115,6 +119,8 @@ describe('ReactDOMSuspensePlaceholder', () => {
await advanceTimers(500);
Scheduler.flushAll();
expect(container.textContent).toEqual('ABC');
});
@ -160,6 +166,8 @@ describe('ReactDOMSuspensePlaceholder', () => {
await advanceTimers(500);
Scheduler.flushAll();
expect(container.innerHTML).toEqual(
'<span style="display: inline;">Sibling</span><span style="">Async</span>',
);

View File

@ -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.
@ -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)', () => {
@ -498,6 +500,7 @@ describe('ReactDOMServerHydration', () => {
jest.runAllTimers();
await Promise.resolve();
Scheduler.flushAll();
expect(element.textContent).toBe('Hello world');
});
});

View File

@ -0,0 +1,16 @@
/**
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @flow
*/
'use strict';
const ReactDOM = require('./src/client/ReactDOM');
// TODO: decide on the top-level export form.
// This is hacky but makes it work with both Rollup and Jest.
module.exports = ReactDOM.default || ReactDOM;

View File

@ -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;
@ -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;
@ -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;
}

View File

@ -43,7 +43,6 @@ import {
requestCurrentTime,
computeExpirationForFiber,
scheduleWork,
requestWork,
flushRoot,
batchedUpdates,
unbatchedUpdates,
@ -300,7 +299,6 @@ export function updateContainer(
export {
flushRoot,
requestWork,
computeUniqueAsyncExpiration,
batchedUpdates,
unbatchedUpdates,

View File

@ -16,7 +16,10 @@ import type {Interaction} from 'scheduler/src/Tracing';
import {noTimeout} from './ReactFiberHostConfig';
import {createHostRootFiber} from './ReactFiber';
import {NoWork} from './ReactFiberExpirationTime';
import {enableSchedulerTracing} from 'shared/ReactFeatureFlags';
import {
enableSchedulerTracing,
enableNewScheduler,
} from 'shared/ReactFeatureFlags';
import {unstable_getThreadID} from 'scheduler/tracing';
// TODO: This should be lifted into the renderer.
@ -83,6 +86,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.
@ -105,81 +115,56 @@ export type FiberRoot = {
...ProfilingOnlyFiberRootProperties,
};
function FiberRootNode(containerInfo, hydrate) {
this.current = null;
this.containerInfo = containerInfo;
this.pendingChildren = null;
this.pingCache = null;
this.pendingCommitExpirationTime = NoWork;
this.finishedWork = null;
this.timeoutHandle = noTimeout;
this.context = null;
this.pendingContext = null;
this.hydrate = hydrate;
this.firstBatch = null;
if (enableNewScheduler) {
this.callbackNode = null;
this.callbackExpirationTime = NoWork;
this.firstPendingTime = NoWork;
this.lastPendingTime = NoWork;
this.pingTime = NoWork;
} else {
this.earliestPendingTime = NoWork;
this.latestPendingTime = NoWork;
this.earliestSuspendedTime = NoWork;
this.latestSuspendedTime = NoWork;
this.latestPingedTime = NoWork;
this.didError = false;
this.nextExpirationTimeToWorkOn = NoWork;
this.expirationTime = NoWork;
this.nextScheduledRoot = null;
}
if (enableSchedulerTracing) {
this.interactionThreadID = unstable_getThreadID();
this.memoizedInteractions = new Set();
this.pendingInteractionMap = new Map();
}
}
export function createFiberRoot(
containerInfo: any,
isConcurrent: boolean,
hydrate: boolean,
): FiberRoot {
const root: FiberRoot = (new FiberRootNode(containerInfo, hydrate): any);
// Cyclic construction. This cheats the type system right now because
// stateNode is any.
const uninitializedFiber = createHostRootFiber(isConcurrent);
let root;
if (enableSchedulerTracing) {
root = ({
current: uninitializedFiber,
containerInfo: containerInfo,
pendingChildren: null,
earliestPendingTime: NoWork,
latestPendingTime: NoWork,
earliestSuspendedTime: NoWork,
latestSuspendedTime: NoWork,
latestPingedTime: NoWork,
pingCache: null,
didError: false,
pendingCommitExpirationTime: NoWork,
finishedWork: null,
timeoutHandle: noTimeout,
context: null,
pendingContext: null,
hydrate,
nextExpirationTimeToWorkOn: NoWork,
expirationTime: NoWork,
firstBatch: null,
nextScheduledRoot: null,
interactionThreadID: unstable_getThreadID(),
memoizedInteractions: new Set(),
pendingInteractionMap: new Map(),
}: FiberRoot);
} else {
root = ({
current: uninitializedFiber,
containerInfo: containerInfo,
pendingChildren: null,
pingCache: null,
earliestPendingTime: NoWork,
latestPendingTime: NoWork,
earliestSuspendedTime: NoWork,
latestSuspendedTime: NoWork,
latestPingedTime: NoWork,
didError: false,
pendingCommitExpirationTime: NoWork,
finishedWork: null,
timeoutHandle: noTimeout,
context: null,
pendingContext: null,
hydrate,
nextExpirationTimeToWorkOn: NoWork,
expirationTime: NoWork,
firstBatch: null,
nextScheduledRoot: null,
}: BaseFiberRootProperties);
}
root.current = uninitializedFiber;
uninitializedFiber.stateNode = root;
// The reason for the way the Flow types are structured in this file,
// Is to avoid needing :any casts everywhere interaction tracing fields are used.
// Unfortunately that requires an :any cast for non-interaction tracing capable builds.
// $FlowFixMe Remove this :any cast and replace it with something better.
return ((root: any): FiberRoot);
return root;
}

View File

@ -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,
@ -35,6 +34,7 @@ import {
computeUniqueAsyncExpiration as computeUniqueAsyncExpiration_old,
flushPassiveEffects as flushPassiveEffects_old,
warnIfNotCurrentlyActingUpdatesInDev as warnIfNotCurrentlyActingUpdatesInDev_old,
inferStartTimeFromExpirationTime as inferStartTimeFromExpirationTime_old,
} from './ReactFiberScheduler.old';
import {
@ -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,
@ -63,61 +62,80 @@ import {
computeUniqueAsyncExpiration as computeUniqueAsyncExpiration_new,
flushPassiveEffects as flushPassiveEffects_new,
warnIfNotCurrentlyActingUpdatesInDev as warnIfNotCurrentlyActingUpdatesInDev_new,
inferStartTimeFromExpirationTime as inferStartTimeFromExpirationTime_new,
} from './ReactFiberScheduler.new';
export let requestCurrentTime = requestCurrentTime_old;
export let computeExpirationForFiber = computeExpirationForFiber_old;
export let captureCommitPhaseError = captureCommitPhaseError_old;
export let onUncaughtError = onUncaughtError_old;
export let renderDidSuspend = renderDidSuspend_old;
export let renderDidError = renderDidError_old;
export let pingSuspendedRoot = pingSuspendedRoot_old;
export let retryTimedOutBoundary = retryTimedOutBoundary_old;
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;
export let flushSync = flushSync_old;
export let flushControlled = flushControlled_old;
export let deferredUpdates = deferredUpdates_old;
export let syncUpdates = syncUpdates_old;
export let interactiveUpdates = interactiveUpdates_old;
export let flushInteractiveUpdates = flushInteractiveUpdates_old;
export let computeUniqueAsyncExpiration = computeUniqueAsyncExpiration_old;
export let flushPassiveEffects = flushPassiveEffects_old;
export let warnIfNotCurrentlyActingUpdatesInDev = warnIfNotCurrentlyActingUpdatesInDev_old;
if (enableNewScheduler) {
requestCurrentTime = requestCurrentTime_new;
computeExpirationForFiber = computeExpirationForFiber_new;
captureCommitPhaseError = captureCommitPhaseError_new;
onUncaughtError = onUncaughtError_new;
renderDidSuspend = renderDidSuspend_new;
renderDidError = renderDidError_new;
pingSuspendedRoot = pingSuspendedRoot_new;
retryTimedOutBoundary = retryTimedOutBoundary_new;
resolveRetryThenable = resolveRetryThenable_new;
markLegacyErrorBoundaryAsFailed = markLegacyErrorBoundaryAsFailed_new;
isAlreadyFailedLegacyErrorBoundary = isAlreadyFailedLegacyErrorBoundary_new;
scheduleWork = scheduleWork_new;
requestWork = requestWork_new;
flushRoot = flushRoot_new;
batchedUpdates = batchedUpdates_new;
unbatchedUpdates = unbatchedUpdates_new;
flushSync = flushSync_new;
flushControlled = flushControlled_new;
deferredUpdates = deferredUpdates_new;
syncUpdates = syncUpdates_new;
interactiveUpdates = interactiveUpdates_new;
flushInteractiveUpdates = flushInteractiveUpdates_new;
computeUniqueAsyncExpiration = computeUniqueAsyncExpiration_new;
flushPassiveEffects = flushPassiveEffects_new;
warnIfNotCurrentlyActingUpdatesInDev = warnIfNotCurrentlyActingUpdatesInDev_new;
}
export const requestCurrentTime = enableNewScheduler
? requestCurrentTime_new
: requestCurrentTime_old;
export const computeExpirationForFiber = enableNewScheduler
? computeExpirationForFiber_new
: computeExpirationForFiber_old;
export const captureCommitPhaseError = enableNewScheduler
? captureCommitPhaseError_new
: captureCommitPhaseError_old;
export const onUncaughtError = enableNewScheduler
? onUncaughtError_new
: onUncaughtError_old;
export const renderDidSuspend = enableNewScheduler
? renderDidSuspend_new
: renderDidSuspend_old;
export const renderDidError = enableNewScheduler
? renderDidError_new
: renderDidError_old;
export const pingSuspendedRoot = enableNewScheduler
? pingSuspendedRoot_new
: pingSuspendedRoot_old;
export const retryTimedOutBoundary = enableNewScheduler
? retryTimedOutBoundary_new
: retryTimedOutBoundary_old;
export const resolveRetryThenable = enableNewScheduler
? resolveRetryThenable_new
: resolveRetryThenable_old;
export const markLegacyErrorBoundaryAsFailed = enableNewScheduler
? markLegacyErrorBoundaryAsFailed_new
: markLegacyErrorBoundaryAsFailed_old;
export const isAlreadyFailedLegacyErrorBoundary = enableNewScheduler
? isAlreadyFailedLegacyErrorBoundary_new
: isAlreadyFailedLegacyErrorBoundary_old;
export const scheduleWork = enableNewScheduler
? scheduleWork_new
: scheduleWork_old;
export const flushRoot = enableNewScheduler ? flushRoot_new : flushRoot_old;
export const batchedUpdates = enableNewScheduler
? batchedUpdates_new
: batchedUpdates_old;
export const unbatchedUpdates = enableNewScheduler
? unbatchedUpdates_new
: unbatchedUpdates_old;
export const flushSync = enableNewScheduler ? flushSync_new : flushSync_old;
export const flushControlled = enableNewScheduler
? flushControlled_new
: flushControlled_old;
export const deferredUpdates = enableNewScheduler
? deferredUpdates_new
: deferredUpdates_old;
export const syncUpdates = enableNewScheduler
? syncUpdates_new
: syncUpdates_old;
export const interactiveUpdates = enableNewScheduler
? interactiveUpdates_new
: interactiveUpdates_old;
export const flushInteractiveUpdates = enableNewScheduler
? flushInteractiveUpdates_new
: flushInteractiveUpdates_old;
export const computeUniqueAsyncExpiration = enableNewScheduler
? computeUniqueAsyncExpiration_new
: computeUniqueAsyncExpiration_old;
export const flushPassiveEffects = enableNewScheduler
? flushPassiveEffects_new
: flushPassiveEffects_old;
export const warnIfNotCurrentlyActingUpdatesInDev = enableNewScheduler
? warnIfNotCurrentlyActingUpdatesInDev_new
: warnIfNotCurrentlyActingUpdatesInDev_old;
export const inferStartTimeFromExpirationTime = enableNewScheduler
? inferStartTimeFromExpirationTime_new
: inferStartTimeFromExpirationTime_old;
export type Thenable = {
then(resolve: () => mixed, reject?: () => mixed): void | Thenable,

File diff suppressed because it is too large Load Diff

View File

@ -123,6 +123,7 @@ import {
expirationTimeToMs,
computeAsyncExpiration,
computeInteractiveExpiration,
LOW_PRIORITY_EXPIRATION,
} from './ReactFiberExpirationTime';
import {ConcurrentMode, ProfileMode} from './ReactTypeOfMode';
import {enqueueUpdate, resetCurrentlyProcessingQueue} from './ReactUpdateQueue';
@ -173,6 +174,8 @@ const {
unstable_cancelCallback: cancelCallback,
unstable_shouldYield: shouldYield,
unstable_now: now,
unstable_getCurrentPriorityLevel: getCurrentPriorityLevel,
unstable_NormalPriority: NormalPriority,
} = Scheduler;
export type Thenable = {
@ -826,7 +829,7 @@ function commitRoot(root: FiberRoot, finishedWork: Fiber): void {
// here because that code is still in flux.
callback = Scheduler_tracing_wrap(callback);
}
passiveEffectCallbackHandle = scheduleCallback(callback);
passiveEffectCallbackHandle = scheduleCallback(NormalPriority, callback);
passiveEffectCallback = callback;
}
@ -1677,6 +1680,25 @@ function renderDidError() {
nextRenderDidError = true;
}
function inferStartTimeFromExpirationTime(
root: FiberRoot,
expirationTime: ExpirationTime,
) {
// We don't know exactly when the update was scheduled, but we can infer an
// approximate start time from the expiration time. First, find the earliest
// uncommitted expiration time in the tree, including work that is suspended.
// Then subtract the offset used to compute an async update's expiration time.
// This will cause high priority (interactive) work to expire earlier than
// necessary, but we can account for this by adjusting for the Just
// Noticeable Difference.
const earliestExpirationTime = findEarliestOutstandingPriorityLevel(
root,
expirationTime,
);
const earliestExpirationTimeMs = expirationTimeToMs(earliestExpirationTime);
return earliestExpirationTimeMs - LOW_PRIORITY_EXPIRATION;
}
function pingSuspendedRoot(
root: FiberRoot,
thenable: Thenable,
@ -2044,7 +2066,8 @@ function scheduleCallbackWithExpirationTime(
const currentMs = now() - originalStartTimeMs;
const expirationTimeMs = expirationTimeToMs(expirationTime);
const timeout = expirationTimeMs - currentMs;
callbackID = scheduleCallback(performAsyncWork, {timeout});
const priorityLevel = getCurrentPriorityLevel();
callbackID = scheduleCallback(priorityLevel, performAsyncWork, {timeout});
}
// For every call to renderRoot, one of onFatal, onComplete, onSuspend, and
@ -2677,7 +2700,6 @@ export {
markLegacyErrorBoundaryAsFailed,
isAlreadyFailedLegacyErrorBoundary,
scheduleWork,
requestWork,
flushRoot,
batchedUpdates,
unbatchedUpdates,
@ -2689,4 +2711,5 @@ export {
flushInteractiveUpdates,
computeUniqueAsyncExpiration,
flushPassiveEffects,
inferStartTimeFromExpirationTime,
};

View File

@ -70,16 +70,12 @@ import {
isAlreadyFailedLegacyErrorBoundary,
pingSuspendedRoot,
resolveRetryThenable,
inferStartTimeFromExpirationTime,
} from './ReactFiberScheduler';
import invariant from 'shared/invariant';
import maxSigned31BitInt from './maxSigned31BitInt';
import {
Sync,
expirationTimeToMs,
LOW_PRIORITY_EXPIRATION,
} from './ReactFiberExpirationTime';
import {findEarliestOutstandingPriorityLevel} from './ReactFiberPendingPriority';
import {Sync, expirationTimeToMs} from './ReactFiberExpirationTime';
const PossiblyWeakSet = typeof WeakSet === 'function' ? WeakSet : Set;
const PossiblyWeakMap = typeof WeakMap === 'function' ? WeakMap : Map;
@ -322,21 +318,12 @@ function throwException(
if (startTimeMs === -1) {
// This suspend happened outside of any already timed-out
// placeholders. We don't know exactly when the update was
// scheduled, but we can infer an approximate start time from the
// expiration time. First, find the earliest uncommitted expiration
// time in the tree, including work that is suspended. Then subtract
// the offset used to compute an async update's expiration time.
// This will cause high priority (interactive) work to expire
// earlier than necessary, but we can account for this by adjusting
// for the Just Noticeable Difference.
const earliestExpirationTime = findEarliestOutstandingPriorityLevel(
// scheduled, but we can infer an approximate start time based on
// the expiration time and the priority.
startTimeMs = inferStartTimeFromExpirationTime(
root,
renderExpirationTime,
);
const earliestExpirationTimeMs = expirationTimeToMs(
earliestExpirationTime,
);
startTimeMs = earliestExpirationTimeMs - LOW_PRIORITY_EXPIRATION;
}
absoluteTimeoutMs = startTimeMs + earliestTimeoutMs;
}

View File

@ -0,0 +1,171 @@
/**
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @flow
*/
// Intentionally not named imports because Rollup would use dynamic dispatch for
// CommonJS interop named imports.
import * as Scheduler from 'scheduler';
import {disableYielding} from 'shared/ReactFeatureFlags';
import invariant from 'shared/invariant';
const {
unstable_runWithPriority: Scheduler_runWithPriority,
unstable_scheduleCallback: Scheduler_scheduleCallback,
unstable_cancelCallback: Scheduler_cancelCallback,
unstable_shouldYield: Scheduler_shouldYield,
unstable_now: Scheduler_now,
unstable_getCurrentPriorityLevel: Scheduler_getCurrentPriorityLevel,
unstable_ImmediatePriority: Scheduler_ImmediatePriority,
unstable_UserBlockingPriority: Scheduler_UserBlockingPriority,
unstable_NormalPriority: Scheduler_NormalPriority,
unstable_LowPriority: Scheduler_LowPriority,
unstable_IdlePriority: Scheduler_IdlePriority,
} = Scheduler;
export opaque type ReactPriorityLevel = 99 | 98 | 97 | 96 | 95 | 90;
export type SchedulerCallback = (isSync: boolean) => SchedulerCallback | null;
type SchedulerCallbackOptions = {
timeout?: number,
};
const fakeCallbackNode = {};
// Except for NoPriority, these correspond to Scheduler priorities. We use
// ascending numbers so we can compare them like numbers. They start at 90 to
// avoid clashing with Scheduler's priorities.
export const ImmediatePriority: ReactPriorityLevel = 99;
export const UserBlockingPriority: ReactPriorityLevel = 98;
export const NormalPriority: ReactPriorityLevel = 97;
export const LowPriority: ReactPriorityLevel = 96;
export const IdlePriority: ReactPriorityLevel = 95;
// NoPriority is the absence of priority. Also React-only.
export const NoPriority: ReactPriorityLevel = 90;
export const now = Scheduler_now;
export const shouldYield = disableYielding
? () => false // Never yield when `disableYielding` is on
: Scheduler_shouldYield;
let immediateQueue: Array<SchedulerCallback> | null = null;
let immediateQueueCallbackNode: mixed | null = null;
let isFlushingImmediate: boolean = false;
export function getCurrentPriorityLevel(): ReactPriorityLevel {
switch (Scheduler_getCurrentPriorityLevel()) {
case Scheduler_ImmediatePriority:
return ImmediatePriority;
case Scheduler_UserBlockingPriority:
return UserBlockingPriority;
case Scheduler_NormalPriority:
return NormalPriority;
case Scheduler_LowPriority:
return LowPriority;
case Scheduler_IdlePriority:
return IdlePriority;
default:
invariant(false, 'Unknown priority level.');
}
}
function reactPriorityToSchedulerPriority(reactPriorityLevel) {
switch (reactPriorityLevel) {
case ImmediatePriority:
return Scheduler_ImmediatePriority;
case UserBlockingPriority:
return Scheduler_UserBlockingPriority;
case NormalPriority:
return Scheduler_NormalPriority;
case LowPriority:
return Scheduler_LowPriority;
case IdlePriority:
return Scheduler_IdlePriority;
default:
invariant(false, 'Unknown priority level.');
}
}
export function runWithPriority<T>(
reactPriorityLevel: ReactPriorityLevel,
fn: () => T,
): T {
const priorityLevel = reactPriorityToSchedulerPriority(reactPriorityLevel);
return Scheduler_runWithPriority(priorityLevel, fn);
}
export function scheduleCallback(
reactPriorityLevel: ReactPriorityLevel,
callback: SchedulerCallback,
options: SchedulerCallbackOptions | void | null,
) {
if (reactPriorityLevel === ImmediatePriority) {
// Push this callback into an internal queue. We'll flush these either in
// the next tick, or earlier if something calls `flushImmediateQueue`.
if (immediateQueue === null) {
immediateQueue = [callback];
// Flush the queue in the next tick, at the earliest.
immediateQueueCallbackNode = Scheduler_scheduleCallback(
Scheduler_ImmediatePriority,
flushImmediateQueueImpl,
);
} else {
// Push onto existing queue. Don't need to schedule a callback because
// we already scheduled one when we created the queue.
immediateQueue.push(callback);
}
return fakeCallbackNode;
}
// Otherwise pass through to Scheduler.
const priorityLevel = reactPriorityToSchedulerPriority(reactPriorityLevel);
return Scheduler_scheduleCallback(priorityLevel, callback, options);
}
export function cancelCallback(callbackNode: mixed) {
if (callbackNode !== fakeCallbackNode) {
Scheduler_cancelCallback(callbackNode);
}
}
export function flushImmediateQueue() {
if (immediateQueueCallbackNode !== null) {
Scheduler_cancelCallback(immediateQueueCallbackNode);
}
flushImmediateQueueImpl();
}
function flushImmediateQueueImpl() {
if (!isFlushingImmediate && immediateQueue !== null) {
// Prevent re-entrancy.
isFlushingImmediate = true;
let i = 0;
try {
const isSync = true;
for (; i < immediateQueue.length; i++) {
let callback = immediateQueue[i];
do {
callback = callback(isSync);
} while (callback !== null);
}
immediateQueue = null;
} catch (error) {
// If something throws, leave the remaining callbacks on the queue.
if (immediateQueue !== null) {
immediateQueue = immediateQueue.slice(i + 1);
}
// Resume flushing in the next tick
Scheduler_scheduleCallback(
Scheduler_ImmediatePriority,
flushImmediateQueue,
);
throw error;
} finally {
isFlushingImmediate = false;
}
}
}

View File

@ -1747,6 +1747,7 @@ describe('ReactHooks', () => {
);
expect(root).toMatchRenderedOutput('loading');
await Promise.resolve();
Scheduler.flushAll();
expect(root).toMatchRenderedOutput('hello');
});
@ -1778,6 +1779,7 @@ describe('ReactHooks', () => {
);
expect(root).toMatchRenderedOutput('loading');
await Promise.resolve();
Scheduler.flushAll();
expect(root).toMatchRenderedOutput('hello');
});
@ -1809,6 +1811,7 @@ describe('ReactHooks', () => {
);
expect(root).toMatchRenderedOutput('loading');
await Promise.resolve();
Scheduler.flushAll();
expect(root).toMatchRenderedOutput('hello');
});
});

View File

@ -15,6 +15,7 @@ let ReactFeatureFlags;
let React;
let ReactNoop;
let Scheduler;
let enableNewScheduler;
describe('ReactIncrementalErrorHandling', () => {
beforeEach(() => {
@ -22,6 +23,7 @@ describe('ReactIncrementalErrorHandling', () => {
ReactFeatureFlags = require('shared/ReactFeatureFlags');
ReactFeatureFlags.debugRenderPhaseSideEffectsForStrictMode = false;
ReactFeatureFlags.replayFailedUnitOfWorkWithInvokeGuardedCallback = false;
enableNewScheduler = ReactFeatureFlags.enableNewScheduler;
PropTypes = require('prop-types');
React = require('react');
ReactNoop = require('react-noop-renderer');
@ -131,6 +133,7 @@ describe('ReactIncrementalErrorHandling', () => {
'ErrorBoundary (catch)',
'ErrorMessage',
]);
expect(ReactNoop.getChildren()).toEqual([span('Caught an error: oops!')]);
});
@ -306,21 +309,17 @@ describe('ReactIncrementalErrorHandling', () => {
});
it('retries one more time before handling error', () => {
let ops = [];
function BadRender() {
ops.push('BadRender');
Scheduler.yieldValue('BadRender');
throw new Error('oops');
}
function Sibling() {
ops.push('Sibling');
Scheduler.yieldValue('Sibling');
return <span prop="Sibling" />;
}
function Parent() {
ops.push('Parent');
Scheduler.yieldValue('Parent');
return (
<React.Fragment>
@ -338,10 +337,15 @@ describe('ReactIncrementalErrorHandling', () => {
// Finish the rest of the async work
expect(Scheduler).toFlushAndYieldThrough(['Sibling']);
// React retries once, synchronously, before throwing.
ops = [];
expect(() => ReactNoop.flushNextYield()).toThrow('oops');
expect(ops).toEqual(['Parent', 'BadRender', 'Sibling']);
// Old scheduler renders, commits, and throws synchronously
expect(() => Scheduler.unstable_flushNumberOfYields(1)).toThrow('oops');
expect(Scheduler).toHaveYielded([
'Parent',
'BadRender',
'Sibling',
'commit',
]);
expect(ReactNoop.getChildren()).toEqual([]);
});
// TODO: This is currently unobservable, but will be once we lift renderRoot
@ -744,7 +748,8 @@ describe('ReactIncrementalErrorHandling', () => {
expect(ReactNoop.getChildren()).toEqual([span('a:5')]);
});
it('applies sync updates regardless despite errors in scheduling', () => {
// TODO: Is this a breaking change?
it('defers additional sync work to a separate event after an error', () => {
ReactNoop.render(<span prop="a:1" />);
expect(() => {
ReactNoop.flushSync(() => {
@ -755,6 +760,7 @@ describe('ReactIncrementalErrorHandling', () => {
});
});
}).toThrow('Hello');
Scheduler.flushAll();
expect(ReactNoop.getChildren()).toEqual([span('a:3')]);
});
@ -962,43 +968,46 @@ describe('ReactIncrementalErrorHandling', () => {
expect(ReactNoop.getChildren('a')).toEqual([
span('Caught an error: Hello.'),
]);
expect(Scheduler).toFlushWithoutYielding();
expect(ReactNoop.getChildren('b')).toEqual([span('b:1')]);
});
it('continues work on other roots despite uncaught errors', () => {
function BrokenRender(props) {
throw new Error('Hello');
throw new Error(props.label);
}
ReactNoop.renderToRootWithID(<BrokenRender />, 'a');
ReactNoop.renderToRootWithID(<BrokenRender label="a" />, 'a');
expect(() => {
expect(Scheduler).toFlushWithoutYielding();
}).toThrow('Hello');
}).toThrow('a');
expect(ReactNoop.getChildren('a')).toEqual([]);
ReactNoop.renderToRootWithID(<BrokenRender />, 'a');
ReactNoop.renderToRootWithID(<BrokenRender label="a" />, 'a');
ReactNoop.renderToRootWithID(<span prop="b:2" />, 'b');
expect(() => {
expect(Scheduler).toFlushWithoutYielding();
}).toThrow('Hello');
}).toThrow('a');
expect(Scheduler).toFlushWithoutYielding();
expect(ReactNoop.getChildren('a')).toEqual([]);
expect(ReactNoop.getChildren('b')).toEqual([span('b:2')]);
ReactNoop.renderToRootWithID(<span prop="a:3" />, 'a');
ReactNoop.renderToRootWithID(<BrokenRender />, 'b');
ReactNoop.renderToRootWithID(<BrokenRender label="b" />, 'b');
expect(() => {
expect(Scheduler).toFlushWithoutYielding();
}).toThrow('Hello');
}).toThrow('b');
expect(ReactNoop.getChildren('a')).toEqual([span('a:3')]);
expect(ReactNoop.getChildren('b')).toEqual([]);
ReactNoop.renderToRootWithID(<span prop="a:4" />, 'a');
ReactNoop.renderToRootWithID(<BrokenRender />, 'b');
ReactNoop.renderToRootWithID(<BrokenRender label="b" />, 'b');
ReactNoop.renderToRootWithID(<span prop="c:4" />, 'c');
expect(() => {
expect(Scheduler).toFlushWithoutYielding();
}).toThrow('Hello');
}).toThrow('b');
expect(Scheduler).toFlushWithoutYielding();
expect(ReactNoop.getChildren('a')).toEqual([span('a:4')]);
expect(ReactNoop.getChildren('b')).toEqual([]);
expect(ReactNoop.getChildren('c')).toEqual([span('c:4')]);
@ -1007,25 +1016,43 @@ describe('ReactIncrementalErrorHandling', () => {
ReactNoop.renderToRootWithID(<span prop="b:5" />, 'b');
ReactNoop.renderToRootWithID(<span prop="c:5" />, 'c');
ReactNoop.renderToRootWithID(<span prop="d:5" />, 'd');
ReactNoop.renderToRootWithID(<BrokenRender />, 'e');
ReactNoop.renderToRootWithID(<BrokenRender label="e" />, 'e');
expect(() => {
expect(Scheduler).toFlushWithoutYielding();
}).toThrow('Hello');
}).toThrow('e');
expect(Scheduler).toFlushWithoutYielding();
expect(ReactNoop.getChildren('a')).toEqual([span('a:5')]);
expect(ReactNoop.getChildren('b')).toEqual([span('b:5')]);
expect(ReactNoop.getChildren('c')).toEqual([span('c:5')]);
expect(ReactNoop.getChildren('d')).toEqual([span('d:5')]);
expect(ReactNoop.getChildren('e')).toEqual([]);
ReactNoop.renderToRootWithID(<BrokenRender />, 'a');
ReactNoop.renderToRootWithID(<BrokenRender label="a" />, 'a');
ReactNoop.renderToRootWithID(<span prop="b:6" />, 'b');
ReactNoop.renderToRootWithID(<BrokenRender />, 'c');
ReactNoop.renderToRootWithID(<BrokenRender label="c" />, 'c');
ReactNoop.renderToRootWithID(<span prop="d:6" />, 'd');
ReactNoop.renderToRootWithID(<BrokenRender />, 'e');
ReactNoop.renderToRootWithID(<BrokenRender label="e" />, 'e');
ReactNoop.renderToRootWithID(<span prop="f:6" />, 'f');
if (enableNewScheduler) {
// The new scheduler will throw all three errors.
expect(() => {
expect(Scheduler).toFlushWithoutYielding();
}).toThrow('Hello');
}).toThrow('a');
expect(() => {
expect(Scheduler).toFlushWithoutYielding();
}).toThrow('c');
expect(() => {
expect(Scheduler).toFlushWithoutYielding();
}).toThrow('e');
} else {
// The old scheduler only throws the first one.
expect(() => {
expect(Scheduler).toFlushWithoutYielding();
}).toThrow('a');
}
expect(Scheduler).toFlushWithoutYielding();
expect(ReactNoop.getChildren('a')).toEqual([]);
expect(ReactNoop.getChildren('b')).toEqual([span('b:6')]);
expect(ReactNoop.getChildren('c')).toEqual([]);

View File

@ -110,11 +110,29 @@ describe('ReactDebugFiberPerf', () => {
};
}
function Parent(props) {
return <div>{props.children}</div>;
}
function Child(props) {
return <div>{props.children}</div>;
}
describe('old scheduler', () => {
runTests(false);
});
describe('new scheduler', () => {
runTests(true);
});
function runTests(enableNewScheduler) {
beforeEach(() => {
jest.resetModules();
resetFlamechart();
global.performance = createUserTimingPolyfill();
require('shared/ReactFeatureFlags').enableNewScheduler = enableNewScheduler;
require('shared/ReactFeatureFlags').enableUserTimingAPI = true;
require('shared/ReactFeatureFlags').enableProfilerTimer = false;
require('shared/ReactFeatureFlags').replayFailedUnitOfWorkWithInvokeGuardedCallback = false;
@ -131,14 +149,6 @@ describe('ReactDebugFiberPerf', () => {
delete global.performance;
});
function Parent(props) {
return <div>{props.children}</div>;
}
function Child(props) {
return <div>{props.children}</div>;
}
it('measures a simple reconciliation', () => {
ReactNoop.render(
<Parent>
@ -167,7 +177,10 @@ describe('ReactDebugFiberPerf', () => {
const AnonymousForwardRef = React.forwardRef((props, ref) => (
<Child {...props} ref={ref} />
));
const NamedForwardRef = React.forwardRef(function refForwarder(props, ref) {
const NamedForwardRef = React.forwardRef(function refForwarder(
props,
ref,
) {
return <Child {...props} ref={ref} />;
});
function notImportant(props, ref) {
@ -668,4 +681,5 @@ describe('ReactDebugFiberPerf', () => {
expect(Scheduler).toFlushWithoutYielding();
expect(getFlameChart()).toMatchSnapshot();
});
}
});

View File

@ -5,6 +5,7 @@ let Scheduler;
let ReactFeatureFlags;
let Suspense;
let lazy;
let enableNewScheduler;
describe('ReactLazy', () => {
beforeEach(() => {
@ -18,6 +19,7 @@ describe('ReactLazy', () => {
lazy = React.lazy;
ReactTestRenderer = require('react-test-renderer');
Scheduler = require('scheduler');
enableNewScheduler = ReactFeatureFlags.enableNewScheduler;
});
function Text(props) {
@ -485,19 +487,33 @@ describe('ReactLazy', () => {
await Promise.resolve();
if (enableNewScheduler) {
// The new scheduler pings in a separate task
expect(Scheduler).toHaveYielded([]);
} else {
// The old scheduler pings synchronously
expect(Scheduler).toHaveYielded(['UNSAFE_componentWillMount: A', 'A1']);
}
root.update(
<Suspense fallback={<Text text="Loading..." />}>
<LazyClass num={2} />
</Suspense>,
);
if (enableNewScheduler) {
// Because this ping happens in a new task, the ping and the update
// are batched together
expect(Scheduler).toHaveYielded(['UNSAFE_componentWillMount: A', 'A2']);
} else {
// The old scheduler must do two separate renders, no batching.
expect(Scheduler).toHaveYielded([
'UNSAFE_componentWillMount: A',
'A1',
'UNSAFE_componentWillReceiveProps: A -> A',
'UNSAFE_componentWillUpdate: A -> A',
'A2',
]);
expect(Scheduler).toFlushAndYield([]);
}
expect(root).toMatchRenderedOutput('A2');
root.update(

View File

@ -0,0 +1,156 @@
/**
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @emails react-core
* @jest-environment node
*/
'use strict';
let React;
let ReactFeatureFlags;
let ReactNoop;
let Scheduler;
let ImmediatePriority;
let UserBlockingPriority;
let NormalPriority;
let LowPriority;
let IdlePriority;
let runWithPriority;
describe('ReactSchedulerIntegration', () => {
beforeEach(() => {
jest.resetModules();
ReactFeatureFlags = require('shared/ReactFeatureFlags');
ReactFeatureFlags.debugRenderPhaseSideEffectsForStrictMode = false;
ReactFeatureFlags.enableNewScheduler = true;
React = require('react');
ReactNoop = require('react-noop-renderer');
Scheduler = require('scheduler');
ImmediatePriority = Scheduler.unstable_ImmediatePriority;
UserBlockingPriority = Scheduler.unstable_UserBlockingPriority;
NormalPriority = Scheduler.unstable_NormalPriority;
LowPriority = Scheduler.unstable_LowPriority;
IdlePriority = Scheduler.unstable_IdlePriority;
runWithPriority = Scheduler.unstable_runWithPriority;
});
function getCurrentPriorityAsString() {
const priorityLevel = Scheduler.unstable_getCurrentPriorityLevel();
switch (priorityLevel) {
case ImmediatePriority:
return 'Immediate';
case UserBlockingPriority:
return 'UserBlocking';
case NormalPriority:
return 'Normal';
case LowPriority:
return 'Low';
case IdlePriority:
return 'Idle';
default:
throw Error('Unknown priority level: ' + priorityLevel);
}
}
it('has correct priority during rendering', () => {
function ReadPriority() {
Scheduler.yieldValue('Priority: ' + getCurrentPriorityAsString());
return null;
}
ReactNoop.render(<ReadPriority />);
expect(Scheduler).toFlushAndYield(['Priority: Normal']);
runWithPriority(UserBlockingPriority, () => {
ReactNoop.render(<ReadPriority />);
});
expect(Scheduler).toFlushAndYield(['Priority: UserBlocking']);
runWithPriority(IdlePriority, () => {
ReactNoop.render(<ReadPriority />);
});
expect(Scheduler).toFlushAndYield(['Priority: Idle']);
});
it('has correct priority when continuing a render after yielding', () => {
function ReadPriority() {
Scheduler.yieldValue('Priority: ' + getCurrentPriorityAsString());
return null;
}
runWithPriority(UserBlockingPriority, () => {
ReactNoop.render(
<React.Fragment>
<ReadPriority />
<ReadPriority />
<ReadPriority />
</React.Fragment>,
);
});
// Render part of the tree
expect(Scheduler).toFlushAndYieldThrough(['Priority: UserBlocking']);
// Priority is set back to normal when yielding
expect(getCurrentPriorityAsString()).toEqual('Normal');
// Priority is restored to user-blocking when continuing
expect(Scheduler).toFlushAndYield([
'Priority: UserBlocking',
'Priority: UserBlocking',
]);
});
it('layout effects have immediate priority', () => {
const {useLayoutEffect} = React;
function ReadPriority() {
Scheduler.yieldValue('Render priority: ' + getCurrentPriorityAsString());
useLayoutEffect(() => {
Scheduler.yieldValue(
'Layout priority: ' + getCurrentPriorityAsString(),
);
});
return null;
}
ReactNoop.render(<ReadPriority />);
expect(Scheduler).toFlushAndYield([
'Render priority: Normal',
'Layout priority: Immediate',
]);
});
it('passive effects have the same priority as render', () => {
const {useEffect} = React;
function ReadPriority() {
Scheduler.yieldValue('Render priority: ' + getCurrentPriorityAsString());
useEffect(() => {
Scheduler.yieldValue(
'Passive priority: ' + getCurrentPriorityAsString(),
);
});
return null;
}
ReactNoop.render(<ReadPriority />);
expect(Scheduler).toFlushAndYield([
'Render priority: Normal',
'Passive priority: Normal',
]);
runWithPriority(UserBlockingPriority, () => {
ReactNoop.render(<ReadPriority />);
});
expect(Scheduler).toFlushAndYield([
'Render priority: UserBlocking',
'Passive priority: UserBlocking',
]);
});
// TODO
it.skip('passive effects have render priority even if they are flushed early', () => {});
});

View File

@ -5,6 +5,7 @@ let Scheduler;
let ReactCache;
let Suspense;
let act;
let enableNewScheduler;
let TextResource;
let textResourceShouldFail;
@ -22,6 +23,7 @@ describe('ReactSuspense', () => {
act = ReactTestRenderer.act;
Scheduler = require('scheduler');
ReactCache = require('react-cache');
enableNewScheduler = ReactFeatureFlags.enableNewScheduler;
Suspense = React.Suspense;
@ -265,7 +267,11 @@ describe('ReactSuspense', () => {
await LazyClass;
if (enableNewScheduler) {
expect(Scheduler).toFlushExpired(['Hi', 'Did mount: Hi']);
} else {
expect(Scheduler).toHaveYielded(['Hi', 'Did mount: Hi']);
}
expect(root).toMatchRenderedOutput('Hi');
});
@ -393,6 +399,16 @@ describe('ReactSuspense', () => {
expect(root).toMatchRenderedOutput('Loading...');
jest.advanceTimersByTime(100);
if (enableNewScheduler) {
expect(Scheduler).toHaveYielded(['Promise resolved [B:1]']);
expect(Scheduler).toFlushExpired([
'B:1',
'Unmount [Loading...]',
// Should be a mount, not an update
'Mount [B:1]',
]);
} else {
expect(Scheduler).toHaveYielded([
'Promise resolved [B:1]',
'B:1',
@ -400,6 +416,7 @@ describe('ReactSuspense', () => {
// Should be a mount, not an update
'Mount [B:1]',
]);
}
expect(root).toMatchRenderedOutput('AB:1C');
@ -413,12 +430,21 @@ describe('ReactSuspense', () => {
jest.advanceTimersByTime(100);
if (enableNewScheduler) {
expect(Scheduler).toHaveYielded(['Promise resolved [B:2]']);
expect(Scheduler).toFlushExpired([
'B:2',
'Unmount [Loading...]',
'Update [B:2]',
]);
} else {
expect(Scheduler).toHaveYielded([
'Promise resolved [B:2]',
'B:2',
'Unmount [Loading...]',
'Update [B:2]',
]);
}
expect(root).toMatchRenderedOutput('AB:2C');
});
@ -450,7 +476,14 @@ describe('ReactSuspense', () => {
]);
jest.advanceTimersByTime(1000);
if (enableNewScheduler) {
expect(Scheduler).toHaveYielded(['Promise resolved [A]']);
expect(Scheduler).toFlushExpired(['A']);
} else {
expect(Scheduler).toHaveYielded(['Promise resolved [A]', 'A']);
}
expect(root).toMatchRenderedOutput('Stateful: 1A');
root.update(<App text="B" />);
@ -466,7 +499,14 @@ describe('ReactSuspense', () => {
expect(root).toMatchRenderedOutput('Loading...');
jest.advanceTimersByTime(1000);
if (enableNewScheduler) {
expect(Scheduler).toHaveYielded(['Promise resolved [B]']);
expect(Scheduler).toFlushExpired(['B']);
} else {
expect(Scheduler).toHaveYielded(['Promise resolved [B]', 'B']);
}
expect(root).toMatchRenderedOutput('Stateful: 2B');
});
@ -506,7 +546,13 @@ describe('ReactSuspense', () => {
]);
jest.advanceTimersByTime(1000);
if (enableNewScheduler) {
expect(Scheduler).toHaveYielded(['Promise resolved [A]']);
expect(Scheduler).toFlushExpired(['A']);
} else {
expect(Scheduler).toHaveYielded(['Promise resolved [A]', 'A']);
}
expect(root).toMatchRenderedOutput('Stateful: 1A');
root.update(<App text="B" />);
@ -529,7 +575,14 @@ describe('ReactSuspense', () => {
expect(root).toMatchRenderedOutput('Loading...');
jest.advanceTimersByTime(1000);
if (enableNewScheduler) {
expect(Scheduler).toHaveYielded(['Promise resolved [B]']);
expect(Scheduler).toFlushExpired(['B']);
} else {
expect(Scheduler).toHaveYielded(['Promise resolved [B]', 'B']);
}
expect(root).toMatchRenderedOutput('Stateful: 2B');
});
@ -610,11 +663,17 @@ describe('ReactSuspense', () => {
ReactTestRenderer.create(<App text="A" />);
expect(Scheduler).toHaveYielded(['Suspend! [A]', 'Loading...']);
jest.advanceTimersByTime(500);
if (enableNewScheduler) {
expect(Scheduler).toHaveYielded(['Promise resolved [A]']);
expect(Scheduler).toFlushExpired(['A', 'Did commit: A']);
} else {
expect(Scheduler).toHaveYielded([
'Promise resolved [A]',
'A',
'Did commit: A',
]);
}
});
it('retries when an update is scheduled on a timed out tree', () => {
@ -698,23 +757,42 @@ describe('ReactSuspense', () => {
]);
expect(Scheduler).toFlushAndYield([]);
jest.advanceTimersByTime(1000);
if (enableNewScheduler) {
expect(Scheduler).toHaveYielded(['Promise resolved [Child 1]']);
expect(Scheduler).toFlushExpired([
'Child 1',
'Suspend! [Child 2]',
'Suspend! [Child 3]',
]);
} else {
expect(Scheduler).toHaveYielded([
'Promise resolved [Child 1]',
'Child 1',
'Suspend! [Child 2]',
'Suspend! [Child 3]',
]);
}
jest.advanceTimersByTime(1000);
if (enableNewScheduler) {
expect(Scheduler).toHaveYielded(['Promise resolved [Child 2]']);
expect(Scheduler).toFlushExpired(['Child 2', 'Suspend! [Child 3]']);
} else {
expect(Scheduler).toHaveYielded([
'Promise resolved [Child 2]',
'Child 2',
'Suspend! [Child 3]',
]);
}
jest.advanceTimersByTime(1000);
if (enableNewScheduler) {
expect(Scheduler).toHaveYielded(['Promise resolved [Child 3]']);
expect(Scheduler).toFlushExpired(['Child 3']);
} else {
expect(Scheduler).toHaveYielded([
'Promise resolved [Child 3]',
'Child 3',
]);
}
expect(root).toMatchRenderedOutput(
['Child 1', 'Child 2', 'Child 3'].join(''),
);
@ -773,7 +851,16 @@ describe('ReactSuspense', () => {
]);
expect(root).toMatchRenderedOutput('Loading...');
jest.advanceTimersByTime(1000);
expect(Scheduler).toHaveYielded(['Promise resolved [Tab: 0]', 'Tab: 0']);
if (enableNewScheduler) {
expect(Scheduler).toHaveYielded(['Promise resolved [Tab: 0]']);
expect(Scheduler).toFlushExpired(['Tab: 0']);
} else {
expect(Scheduler).toHaveYielded([
'Promise resolved [Tab: 0]',
'Tab: 0',
]);
}
expect(root).toMatchRenderedOutput('Tab: 0 + sibling');
act(() => setTab(1));
@ -784,7 +871,17 @@ describe('ReactSuspense', () => {
]);
expect(root).toMatchRenderedOutput('Loading...');
jest.advanceTimersByTime(1000);
expect(Scheduler).toHaveYielded(['Promise resolved [Tab: 1]', 'Tab: 1']);
if (enableNewScheduler) {
expect(Scheduler).toHaveYielded(['Promise resolved [Tab: 1]']);
expect(Scheduler).toFlushExpired(['Tab: 1']);
} else {
expect(Scheduler).toHaveYielded([
'Promise resolved [Tab: 1]',
'Tab: 1',
]);
}
expect(root).toMatchRenderedOutput('Tab: 1 + sibling');
act(() => setTab(2));
@ -795,7 +892,17 @@ describe('ReactSuspense', () => {
]);
expect(root).toMatchRenderedOutput('Loading...');
jest.advanceTimersByTime(1000);
expect(Scheduler).toHaveYielded(['Promise resolved [Tab: 2]', 'Tab: 2']);
if (enableNewScheduler) {
expect(Scheduler).toHaveYielded(['Promise resolved [Tab: 2]']);
expect(Scheduler).toFlushExpired(['Tab: 2']);
} else {
expect(Scheduler).toHaveYielded([
'Promise resolved [Tab: 2]',
'Tab: 2',
]);
}
expect(root).toMatchRenderedOutput('Tab: 2 + sibling');
});
@ -831,7 +938,14 @@ describe('ReactSuspense', () => {
expect(Scheduler).toHaveYielded(['Suspend! [A:0]', 'Loading...']);
jest.advanceTimersByTime(1000);
if (enableNewScheduler) {
expect(Scheduler).toHaveYielded(['Promise resolved [A:0]']);
expect(Scheduler).toFlushExpired(['A:0']);
} else {
expect(Scheduler).toHaveYielded(['Promise resolved [A:0]', 'A:0']);
}
expect(root).toMatchRenderedOutput('A:0');
act(() => setStep(1));
@ -868,6 +982,15 @@ describe('ReactSuspense', () => {
// Resolve A
jest.advanceTimersByTime(1000);
if (enableNewScheduler) {
expect(Scheduler).toHaveYielded(['Promise resolved [A]']);
expect(Scheduler).toFlushExpired([
'A',
// The promises for B and C have now been thrown twice
'Suspend! [B]',
'Suspend! [C]',
]);
} else {
expect(Scheduler).toHaveYielded([
'Promise resolved [A]',
'A',
@ -875,10 +998,21 @@ describe('ReactSuspense', () => {
'Suspend! [B]',
'Suspend! [C]',
]);
}
// Resolve B
jest.advanceTimersByTime(1000);
if (enableNewScheduler) {
expect(Scheduler).toHaveYielded(['Promise resolved [B]']);
expect(Scheduler).toFlushExpired([
// Even though the promise for B was thrown twice, we should only
// re-render once.
'B',
// The promise for C has now been thrown three times
'Suspend! [C]',
]);
} else {
expect(Scheduler).toHaveYielded([
'Promise resolved [B]',
// Even though the promise for B was thrown twice, we should only
@ -887,15 +1021,26 @@ describe('ReactSuspense', () => {
// The promise for C has now been thrown three times
'Suspend! [C]',
]);
}
// Resolve C
jest.advanceTimersByTime(1000);
if (enableNewScheduler) {
expect(Scheduler).toHaveYielded(['Promise resolved [C]']);
expect(Scheduler).toFlushExpired([
// Even though the promise for C was thrown three times, we should only
// re-render once.
'C',
]);
} else {
expect(Scheduler).toHaveYielded([
'Promise resolved [C]',
// Even though the promise for C was thrown three times, we should only
// re-render once.
'C',
]);
}
});
it('#14162', () => {

View File

@ -17,6 +17,7 @@ let ReactCache;
let Suspense;
let TextResource;
let textResourceShouldFail;
let enableNewScheduler;
describe('ReactSuspensePlaceholder', () => {
beforeEach(() => {
@ -30,6 +31,7 @@ describe('ReactSuspensePlaceholder', () => {
ReactNoop = require('react-noop-renderer');
Scheduler = require('scheduler');
ReactCache = require('react-cache');
enableNewScheduler = ReactFeatureFlags.enableNewScheduler;
Profiler = React.Profiler;
Suspense = React.Suspense;
@ -323,10 +325,16 @@ describe('ReactSuspensePlaceholder', () => {
jest.advanceTimersByTime(1000);
if (enableNewScheduler) {
expect(Scheduler).toHaveYielded(['Promise resolved [Loaded]']);
expect(Scheduler).toFlushExpired(['Loaded']);
} else {
expect(Scheduler).toHaveYielded([
'Promise resolved [Loaded]',
'Loaded',
]);
}
expect(ReactNoop).toMatchRenderedOutput('LoadedText');
expect(onRender).toHaveBeenCalledTimes(2);
@ -426,10 +434,16 @@ describe('ReactSuspensePlaceholder', () => {
jest.advanceTimersByTime(1000);
if (enableNewScheduler) {
expect(Scheduler).toHaveYielded(['Promise resolved [Loaded]']);
expect(Scheduler).toFlushExpired(['Loaded']);
} else {
expect(Scheduler).toHaveYielded([
'Promise resolved [Loaded]',
'Loaded',
]);
}
expect(ReactNoop).toMatchRenderedOutput('LoadedNew');
expect(onRender).toHaveBeenCalledTimes(4);

View File

@ -7,6 +7,7 @@ let ReactCache;
let Suspense;
let StrictMode;
let ConcurrentMode;
let enableNewScheduler;
let TextResource;
let textResourceShouldFail;
@ -28,6 +29,7 @@ describe('ReactSuspenseWithNoopRenderer', () => {
Suspense = React.Suspense;
StrictMode = React.StrictMode;
ConcurrentMode = React.unstable_ConcurrentMode;
enableNewScheduler = ReactFeatureFlags.enableNewScheduler;
TextResource = ReactCache.unstable_createResource(([text, ms = 0]) => {
return new Promise((resolve, reject) =>
@ -842,7 +844,16 @@ describe('ReactSuspenseWithNoopRenderer', () => {
ReactNoop.expire(100);
await advanceTimers(100);
expect(Scheduler).toHaveYielded(['Promise resolved [Result]', 'Result']);
if (enableNewScheduler) {
expect(Scheduler).toHaveYielded(['Promise resolved [Result]']);
expect(Scheduler).toFlushExpired(['Result']);
} else {
expect(Scheduler).toHaveYielded([
'Promise resolved [Result]',
'Result',
]);
}
expect(ReactNoop.getChildren()).toEqual([span('Result')]);
});
@ -880,6 +891,17 @@ describe('ReactSuspenseWithNoopRenderer', () => {
// Initial mount. This is synchronous, because the root is sync.
ReactNoop.renderLegacySyncRoot(<App />);
await advanceTimers(100);
if (enableNewScheduler) {
expect(Scheduler).toHaveYielded([
'Suspend! [Step: 1]',
'Sibling',
'Loading (1)',
'Loading (2)',
'Loading (3)',
'Promise resolved [Step: 1]',
]);
expect(Scheduler).toFlushExpired(['Step: 1']);
} else {
expect(Scheduler).toHaveYielded([
'Suspend! [Step: 1]',
'Sibling',
@ -889,6 +911,7 @@ describe('ReactSuspenseWithNoopRenderer', () => {
'Promise resolved [Step: 1]',
'Step: 1',
]);
}
expect(ReactNoop).toMatchRenderedOutput(
<React.Fragment>
<span prop="Step: 1" />
@ -920,10 +943,15 @@ describe('ReactSuspenseWithNoopRenderer', () => {
);
await advanceTimers(100);
if (enableNewScheduler) {
expect(Scheduler).toHaveYielded(['Promise resolved [Step: 2]']);
expect(Scheduler).toFlushExpired(['Step: 2']);
} else {
expect(Scheduler).toHaveYielded([
'Promise resolved [Step: 2]',
'Step: 2',
]);
}
expect(ReactNoop).toMatchRenderedOutput(
<React.Fragment>
<span prop="Step: 2" />
@ -981,6 +1009,21 @@ describe('ReactSuspenseWithNoopRenderer', () => {
Scheduler.yieldValue('Did mount'),
);
await advanceTimers(100);
if (enableNewScheduler) {
expect(Scheduler).toHaveYielded([
'Before',
'Suspend! [Async: 1]',
'After',
'Loading...',
'Before',
'Sync: 1',
'After',
'Did mount',
'Promise resolved [Async: 1]',
]);
expect(Scheduler).toFlushExpired(['Async: 1']);
} else {
expect(Scheduler).toHaveYielded([
'Before',
'Suspend! [Async: 1]',
@ -993,6 +1036,7 @@ describe('ReactSuspenseWithNoopRenderer', () => {
'Promise resolved [Async: 1]',
'Async: 1',
]);
}
expect(ReactNoop).toMatchRenderedOutput(
<React.Fragment>
<span prop="Before" />
@ -1046,10 +1090,16 @@ describe('ReactSuspenseWithNoopRenderer', () => {
// When the placeholder is pinged, the boundary must be re-rendered
// synchronously.
await advanceTimers(100);
if (enableNewScheduler) {
expect(Scheduler).toHaveYielded(['Promise resolved [Async: 2]']);
expect(Scheduler).toFlushExpired(['Async: 2']);
} else {
expect(Scheduler).toHaveYielded([
'Promise resolved [Async: 2]',
'Async: 2',
]);
}
expect(ReactNoop).toMatchRenderedOutput(
<React.Fragment>
@ -1114,6 +1164,20 @@ describe('ReactSuspenseWithNoopRenderer', () => {
Scheduler.yieldValue('Did mount'),
);
await advanceTimers(100);
if (enableNewScheduler) {
expect(Scheduler).toHaveYielded([
'Before',
'Suspend! [Async: 1]',
'After',
'Loading...',
'Before',
'Sync: 1',
'After',
'Did mount',
'Promise resolved [Async: 1]',
]);
expect(Scheduler).toFlushExpired(['Async: 1']);
} else {
expect(Scheduler).toHaveYielded([
'Before',
'Suspend! [Async: 1]',
@ -1126,6 +1190,7 @@ describe('ReactSuspenseWithNoopRenderer', () => {
'Promise resolved [Async: 1]',
'Async: 1',
]);
}
expect(ReactNoop).toMatchRenderedOutput(
<React.Fragment>
<span prop="Before" />
@ -1179,10 +1244,16 @@ describe('ReactSuspenseWithNoopRenderer', () => {
// When the placeholder is pinged, the boundary must be re-rendered
// synchronously.
await advanceTimers(100);
if (enableNewScheduler) {
expect(Scheduler).toHaveYielded(['Promise resolved [Async: 2]']);
expect(Scheduler).toFlushExpired(['Async: 2']);
} else {
expect(Scheduler).toHaveYielded([
'Promise resolved [Async: 2]',
'Async: 2',
]);
}
expect(ReactNoop).toMatchRenderedOutput(
<React.Fragment>
@ -1260,7 +1331,13 @@ describe('ReactSuspenseWithNoopRenderer', () => {
ReactNoop.expire(1000);
await advanceTimers(1000);
if (enableNewScheduler) {
expect(Scheduler).toHaveYielded(['Promise resolved [B]']);
expect(Scheduler).toFlushExpired(['B']);
} else {
expect(Scheduler).toHaveYielded(['Promise resolved [B]', 'B']);
}
expect(ReactNoop).toMatchRenderedOutput(
<React.Fragment>
@ -1312,12 +1389,22 @@ describe('ReactSuspenseWithNoopRenderer', () => {
expect(ReactNoop.getChildren()).toEqual([span('Loading...')]);
await advanceTimers(1000);
if (enableNewScheduler) {
expect(Scheduler).toHaveYielded(['Promise resolved [Hi]']);
expect(Scheduler).toFlushExpired([
'constructor',
'Hi',
'componentDidMount',
]);
} else {
expect(Scheduler).toHaveYielded([
'Promise resolved [Hi]',
'constructor',
'Hi',
'componentDidMount',
]);
}
expect(ReactNoop.getChildren()).toEqual([span('Hi')]);
});
@ -1356,7 +1443,12 @@ describe('ReactSuspenseWithNoopRenderer', () => {
]);
expect(ReactNoop.getChildren()).toEqual([span('Loading...')]);
await advanceTimers(100);
if (enableNewScheduler) {
expect(Scheduler).toHaveYielded(['Promise resolved [Hi]']);
expect(Scheduler).toFlushExpired(['Hi']);
} else {
expect(Scheduler).toHaveYielded(['Promise resolved [Hi]', 'Hi']);
}
expect(ReactNoop.getChildren()).toEqual([span('Hi')]);
});
@ -1400,7 +1492,12 @@ describe('ReactSuspenseWithNoopRenderer', () => {
await advanceTimers(1000);
if (enableNewScheduler) {
expect(Scheduler).toHaveYielded(['Promise resolved [Hi]']);
expect(Scheduler).toFlushExpired(['Hi']);
} else {
expect(Scheduler).toHaveYielded(['Promise resolved [Hi]', 'Hi']);
}
});
} else {
it('hides/unhides suspended children before layout effects fire (mutation)', async () => {
@ -1439,7 +1536,12 @@ describe('ReactSuspenseWithNoopRenderer', () => {
await advanceTimers(1000);
if (enableNewScheduler) {
expect(Scheduler).toHaveYielded(['Promise resolved [Hi]']);
expect(Scheduler).toFlushExpired(['Hi']);
} else {
expect(Scheduler).toHaveYielded(['Promise resolved [Hi]', 'Hi']);
}
});
}
});

View File

@ -1,6 +1,6 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`ReactDebugFiberPerf captures all lifecycles 1`] = `
exports[`ReactDebugFiberPerf new scheduler captures all lifecycles 1`] = `
"⚛ (Waiting for async callback... will force flush in 5250 ms)
// Mount
@ -44,7 +44,7 @@ exports[`ReactDebugFiberPerf captures all lifecycles 1`] = `
"
`;
exports[`ReactDebugFiberPerf deduplicates lifecycle names during commit to reduce overhead 1`] = `
exports[`ReactDebugFiberPerf new scheduler deduplicates lifecycle names during commit to reduce overhead 1`] = `
"⚛ (Waiting for async callback... will force flush in 5250 ms)
// The commit phase should mention A and B just once
@ -91,7 +91,7 @@ exports[`ReactDebugFiberPerf deduplicates lifecycle names during commit to reduc
"
`;
exports[`ReactDebugFiberPerf does not include ConcurrentMode, StrictMode, or Profiler components in measurements 1`] = `
exports[`ReactDebugFiberPerf new scheduler does not include ConcurrentMode, StrictMode, or Profiler components in measurements 1`] = `
"⚛ (Waiting for async callback... will force flush in 5250 ms)
// Mount
@ -107,7 +107,7 @@ exports[`ReactDebugFiberPerf does not include ConcurrentMode, StrictMode, or Pro
"
`;
exports[`ReactDebugFiberPerf does not include context provider or consumer in measurements 1`] = `
exports[`ReactDebugFiberPerf new scheduler does not include context provider or consumer in measurements 1`] = `
"⚛ (Waiting for async callback... will force flush in 5250 ms)
// Mount
@ -122,7 +122,7 @@ exports[`ReactDebugFiberPerf does not include context provider or consumer in me
"
`;
exports[`ReactDebugFiberPerf does not schedule an extra callback if setState is called during a synchronous commit phase 1`] = `
exports[`ReactDebugFiberPerf new scheduler does not schedule an extra callback if setState is called during a synchronous commit phase 1`] = `
"⚛ (React Tree Reconciliation: Completed Root)
⚛ Component [mount]
@ -142,7 +142,7 @@ exports[`ReactDebugFiberPerf does not schedule an extra callback if setState is
"
`;
exports[`ReactDebugFiberPerf does not treat setState from cWM or cWRP as cascading 1`] = `
exports[`ReactDebugFiberPerf new scheduler does not treat setState from cWM or cWRP as cascading 1`] = `
"⚛ (Waiting for async callback... will force flush in 5250 ms)
// Should not print a warning
@ -171,7 +171,7 @@ exports[`ReactDebugFiberPerf does not treat setState from cWM or cWRP as cascadi
"
`;
exports[`ReactDebugFiberPerf measures a simple reconciliation 1`] = `
exports[`ReactDebugFiberPerf new scheduler measures a simple reconciliation 1`] = `
"⚛ (Waiting for async callback... will force flush in 5250 ms)
// Mount
@ -208,7 +208,7 @@ exports[`ReactDebugFiberPerf measures a simple reconciliation 1`] = `
"
`;
exports[`ReactDebugFiberPerf measures deferred work in chunks 1`] = `
exports[`ReactDebugFiberPerf new scheduler measures deferred work in chunks 1`] = `
"⚛ (Waiting for async callback... will force flush in 5250 ms)
// Start rendering through B
@ -235,7 +235,7 @@ exports[`ReactDebugFiberPerf measures deferred work in chunks 1`] = `
"
`;
exports[`ReactDebugFiberPerf measures deprioritized work 1`] = `
exports[`ReactDebugFiberPerf new scheduler measures deprioritized work 1`] = `
"// Flush the parent
⚛ (React Tree Reconciliation: Completed Root)
⚛ Parent [mount]
@ -258,7 +258,7 @@ exports[`ReactDebugFiberPerf measures deprioritized work 1`] = `
"
`;
exports[`ReactDebugFiberPerf properly displays the forwardRef component in measurements 1`] = `
exports[`ReactDebugFiberPerf new scheduler properly displays the forwardRef component in measurements 1`] = `
"⚛ (Waiting for async callback... will force flush in 5250 ms)
// Mount
@ -278,7 +278,7 @@ exports[`ReactDebugFiberPerf properly displays the forwardRef component in measu
"
`;
exports[`ReactDebugFiberPerf recovers from caught errors 1`] = `
exports[`ReactDebugFiberPerf new scheduler recovers from caught errors 1`] = `
"⚛ (Waiting for async callback... will force flush in 5250 ms)
// Stop on Baddie and restart from Boundary
@ -312,7 +312,7 @@ exports[`ReactDebugFiberPerf recovers from caught errors 1`] = `
"
`;
exports[`ReactDebugFiberPerf recovers from fatal errors 1`] = `
exports[`ReactDebugFiberPerf new scheduler recovers from fatal errors 1`] = `
"⚛ (Waiting for async callback... will force flush in 5250 ms)
// Will fatal
@ -343,7 +343,7 @@ exports[`ReactDebugFiberPerf recovers from fatal errors 1`] = `
"
`;
exports[`ReactDebugFiberPerf skips parents during setState 1`] = `
exports[`ReactDebugFiberPerf new scheduler skips parents during setState 1`] = `
"⚛ (Waiting for async callback... will force flush in 5250 ms)
// Should include just A and B, no Parents
@ -358,7 +358,7 @@ exports[`ReactDebugFiberPerf skips parents during setState 1`] = `
"
`;
exports[`ReactDebugFiberPerf supports Suspense and lazy 1`] = `
exports[`ReactDebugFiberPerf new scheduler supports Suspense and lazy 1`] = `
"⚛ (Waiting for async callback... will force flush in 5250 ms)
⚛ (React Tree Reconciliation: Completed Root)
@ -369,7 +369,7 @@ exports[`ReactDebugFiberPerf supports Suspense and lazy 1`] = `
"
`;
exports[`ReactDebugFiberPerf supports Suspense and lazy 2`] = `
exports[`ReactDebugFiberPerf new scheduler supports Suspense and lazy 2`] = `
"⚛ (Waiting for async callback... will force flush in 5250 ms)
⚛ (React Tree Reconciliation: Completed Root)
@ -392,7 +392,7 @@ exports[`ReactDebugFiberPerf supports Suspense and lazy 2`] = `
"
`;
exports[`ReactDebugFiberPerf supports memo 1`] = `
exports[`ReactDebugFiberPerf new scheduler supports memo 1`] = `
"⚛ (Waiting for async callback... will force flush in 5250 ms)
⚛ (React Tree Reconciliation: Completed Root)
@ -406,7 +406,7 @@ exports[`ReactDebugFiberPerf supports memo 1`] = `
"
`;
exports[`ReactDebugFiberPerf supports portals 1`] = `
exports[`ReactDebugFiberPerf new scheduler supports portals 1`] = `
"⚛ (Waiting for async callback... will force flush in 5250 ms)
⚛ (React Tree Reconciliation: Completed Root)
@ -420,7 +420,7 @@ exports[`ReactDebugFiberPerf supports portals 1`] = `
"
`;
exports[`ReactDebugFiberPerf warns if an in-progress update is interrupted 1`] = `
exports[`ReactDebugFiberPerf new scheduler warns if an in-progress update is interrupted 1`] = `
"⚛ (Waiting for async callback... will force flush in 5250 ms)
⚛ (React Tree Reconciliation: Yielded)
@ -443,12 +443,9 @@ exports[`ReactDebugFiberPerf warns if an in-progress update is interrupted 1`] =
"
`;
exports[`ReactDebugFiberPerf warns if async work expires (starvation) 1`] = `
exports[`ReactDebugFiberPerf new scheduler warns if async work expires (starvation) 1`] = `
"⛔ (Waiting for async callback... will force flush in 5250 ms) Warning: React was blocked by main thread
⚛ (React Tree Reconciliation: Completed Root)
⚛ Foo [mount]
⚛ (Committing Changes)
⚛ (Committing Snapshot Effects: 0 Total)
⚛ (Committing Host Effects: 1 Total)
@ -456,7 +453,7 @@ exports[`ReactDebugFiberPerf warns if async work expires (starvation) 1`] = `
"
`;
exports[`ReactDebugFiberPerf warns on cascading renders from setState 1`] = `
exports[`ReactDebugFiberPerf new scheduler warns on cascading renders from setState 1`] = `
"⚛ (Waiting for async callback... will force flush in 5250 ms)
// Should print a warning
@ -480,7 +477,511 @@ exports[`ReactDebugFiberPerf warns on cascading renders from setState 1`] = `
"
`;
exports[`ReactDebugFiberPerf warns on cascading renders from top-level render 1`] = `
exports[`ReactDebugFiberPerf new scheduler warns on cascading renders from top-level render 1`] = `
"⚛ (Waiting for async callback... will force flush in 5250 ms)
// Rendering the first root
⚛ (React Tree Reconciliation: Completed Root)
⚛ Cascading [mount]
⛔ (Committing Changes) Warning: Lifecycle hook scheduled a cascading update
⚛ (Committing Snapshot Effects: 0 Total)
⚛ (Committing Host Effects: 1 Total)
⚛ (Calling Lifecycle Methods: 1 Total)
⛔ Cascading.componentDidMount Warning: Scheduled a cascading update
// Scheduling another root from componentDidMount
⚛ (React Tree Reconciliation: Completed Root)
⚛ Child [mount]
⚛ (Committing Changes)
⚛ (Committing Snapshot Effects: 0 Total)
⚛ (Committing Host Effects: 1 Total)
⚛ (Calling Lifecycle Methods: 0 Total)
"
`;
exports[`ReactDebugFiberPerf old scheduler captures all lifecycles 1`] = `
"⚛ (Waiting for async callback... will force flush in 5250 ms)
// Mount
⚛ (React Tree Reconciliation: Completed Root)
⚛ AllLifecycles [mount]
⚛ AllLifecycles.componentWillMount
⚛ AllLifecycles.getChildContext
⚛ (Committing Changes)
⚛ (Committing Snapshot Effects: 0 Total)
⚛ (Committing Host Effects: 1 Total)
⚛ (Calling Lifecycle Methods: 1 Total)
⚛ AllLifecycles.componentDidMount
⚛ (Waiting for async callback... will force flush in 5250 ms)
// Update
⚛ (React Tree Reconciliation: Completed Root)
⚛ AllLifecycles [update]
⚛ AllLifecycles.componentWillReceiveProps
⚛ AllLifecycles.shouldComponentUpdate
⚛ AllLifecycles.componentWillUpdate
⚛ AllLifecycles.getChildContext
⚛ (Committing Changes)
⚛ (Committing Snapshot Effects: 0 Total)
⚛ (Committing Host Effects: 2 Total)
⚛ (Calling Lifecycle Methods: 2 Total)
⚛ AllLifecycles.componentDidUpdate
⚛ (Waiting for async callback... will force flush in 5250 ms)
// Unmount
⚛ (React Tree Reconciliation: Completed Root)
⚛ (Committing Changes)
⚛ (Committing Snapshot Effects: 0 Total)
⚛ (Committing Host Effects: 1 Total)
⚛ AllLifecycles.componentWillUnmount
⚛ (Calling Lifecycle Methods: 0 Total)
"
`;
exports[`ReactDebugFiberPerf old scheduler deduplicates lifecycle names during commit to reduce overhead 1`] = `
"⚛ (Waiting for async callback... will force flush in 5250 ms)
// The commit phase should mention A and B just once
⚛ (React Tree Reconciliation: Completed Root)
⚛ Parent [update]
⚛ A [update]
⚛ B [update]
⚛ A [update]
⚛ B [update]
⚛ (Committing Changes)
⚛ (Committing Snapshot Effects: 0 Total)
⚛ (Committing Host Effects: 9 Total)
⚛ (Calling Lifecycle Methods: 9 Total)
⚛ A.componentDidUpdate
⚛ B.componentDidUpdate
⚛ (Waiting for async callback... will force flush in 5250 ms)
// Because of deduplication, we don't know B was cascading,
// but we should still see the warning for the commit phase.
⚛ (React Tree Reconciliation: Completed Root)
⚛ Parent [update]
⚛ A [update]
⚛ B [update]
⚛ A [update]
⚛ B [update]
⛔ (Committing Changes) Warning: Lifecycle hook scheduled a cascading update
⚛ (Committing Snapshot Effects: 0 Total)
⚛ (Committing Host Effects: 9 Total)
⚛ (Calling Lifecycle Methods: 9 Total)
⚛ A.componentDidUpdate
⚛ B.componentDidUpdate
⚛ (React Tree Reconciliation: Completed Root)
⚛ B [update]
⚛ (Committing Changes)
⚛ (Committing Snapshot Effects: 0 Total)
⚛ (Committing Host Effects: 2 Total)
⚛ (Calling Lifecycle Methods: 2 Total)
⚛ B.componentDidUpdate
"
`;
exports[`ReactDebugFiberPerf old scheduler does not include ConcurrentMode, StrictMode, or Profiler components in measurements 1`] = `
"⚛ (Waiting for async callback... will force flush in 5250 ms)
// Mount
⚛ (React Tree Reconciliation: Completed Root)
⚛ Profiler [mount]
⚛ Parent [mount]
⚛ Child [mount]
⚛ (Committing Changes)
⚛ (Committing Snapshot Effects: 0 Total)
⚛ (Committing Host Effects: 1 Total)
⚛ (Calling Lifecycle Methods: 0 Total)
"
`;
exports[`ReactDebugFiberPerf old scheduler does not include context provider or consumer in measurements 1`] = `
"⚛ (Waiting for async callback... will force flush in 5250 ms)
// Mount
⚛ (React Tree Reconciliation: Completed Root)
⚛ Parent [mount]
⚛ Child [mount]
⚛ (Committing Changes)
⚛ (Committing Snapshot Effects: 0 Total)
⚛ (Committing Host Effects: 1 Total)
⚛ (Calling Lifecycle Methods: 0 Total)
"
`;
exports[`ReactDebugFiberPerf old scheduler does not schedule an extra callback if setState is called during a synchronous commit phase 1`] = `
"⚛ (React Tree Reconciliation: Completed Root)
⚛ Component [mount]
⛔ (Committing Changes) Warning: Lifecycle hook scheduled a cascading update
⚛ (Committing Snapshot Effects: 0 Total)
⚛ (Committing Host Effects: 1 Total)
⚛ (Calling Lifecycle Methods: 1 Total)
⛔ Component.componentDidMount Warning: Scheduled a cascading update
⚛ (React Tree Reconciliation: Completed Root)
⚛ Component [update]
⚛ (Committing Changes)
⚛ (Committing Snapshot Effects: 0 Total)
⚛ (Committing Host Effects: 1 Total)
⚛ (Calling Lifecycle Methods: 1 Total)
"
`;
exports[`ReactDebugFiberPerf old scheduler does not treat setState from cWM or cWRP as cascading 1`] = `
"⚛ (Waiting for async callback... will force flush in 5250 ms)
// Should not print a warning
⚛ (React Tree Reconciliation: Completed Root)
⚛ Parent [mount]
⚛ NotCascading [mount]
⚛ NotCascading.componentWillMount
⚛ (Committing Changes)
⚛ (Committing Snapshot Effects: 0 Total)
⚛ (Committing Host Effects: 1 Total)
⚛ (Calling Lifecycle Methods: 0 Total)
⚛ (Waiting for async callback... will force flush in 5250 ms)
// Should not print a warning
⚛ (React Tree Reconciliation: Completed Root)
⚛ Parent [update]
⚛ NotCascading [update]
⚛ NotCascading.componentWillReceiveProps
⚛ (Committing Changes)
⚛ (Committing Snapshot Effects: 0 Total)
⚛ (Committing Host Effects: 2 Total)
⚛ (Calling Lifecycle Methods: 2 Total)
"
`;
exports[`ReactDebugFiberPerf old scheduler measures a simple reconciliation 1`] = `
"⚛ (Waiting for async callback... will force flush in 5250 ms)
// Mount
⚛ (React Tree Reconciliation: Completed Root)
⚛ Parent [mount]
⚛ Child [mount]
⚛ (Committing Changes)
⚛ (Committing Snapshot Effects: 0 Total)
⚛ (Committing Host Effects: 1 Total)
⚛ (Calling Lifecycle Methods: 0 Total)
⚛ (Waiting for async callback... will force flush in 5250 ms)
// Update
⚛ (React Tree Reconciliation: Completed Root)
⚛ Parent [update]
⚛ Child [update]
⚛ (Committing Changes)
⚛ (Committing Snapshot Effects: 0 Total)
⚛ (Committing Host Effects: 2 Total)
⚛ (Calling Lifecycle Methods: 2 Total)
⚛ (Waiting for async callback... will force flush in 5250 ms)
// Unmount
⚛ (React Tree Reconciliation: Completed Root)
⚛ (Committing Changes)
⚛ (Committing Snapshot Effects: 0 Total)
⚛ (Committing Host Effects: 1 Total)
⚛ (Calling Lifecycle Methods: 0 Total)
"
`;
exports[`ReactDebugFiberPerf old scheduler measures deferred work in chunks 1`] = `
"⚛ (Waiting for async callback... will force flush in 5250 ms)
// Start rendering through B
⚛ (React Tree Reconciliation: Yielded)
⚛ Parent [mount]
⚛ A [mount]
⚛ Child [mount]
⚛ B [mount]
⚛ (Waiting for async callback... will force flush in 5250 ms)
// Complete the rest
⚛ (React Tree Reconciliation: Completed Root)
⚛ Parent [mount]
⚛ B [mount]
⚛ Child [mount]
⚛ C [mount]
⚛ Child [mount]
⚛ (Committing Changes)
⚛ (Committing Snapshot Effects: 0 Total)
⚛ (Committing Host Effects: 1 Total)
⚛ (Calling Lifecycle Methods: 0 Total)
"
`;
exports[`ReactDebugFiberPerf old scheduler measures deprioritized work 1`] = `
"// Flush the parent
⚛ (React Tree Reconciliation: Completed Root)
⚛ Parent [mount]
⚛ (Committing Changes)
⚛ (Committing Snapshot Effects: 0 Total)
⚛ (Committing Host Effects: 1 Total)
⚛ (Calling Lifecycle Methods: 0 Total)
⚛ (Waiting for async callback... will force flush in 10737418210 ms)
// Flush the child
⚛ (React Tree Reconciliation: Completed Root)
⚛ Child [mount]
⚛ (Committing Changes)
⚛ (Committing Snapshot Effects: 0 Total)
⚛ (Committing Host Effects: 1 Total)
⚛ (Calling Lifecycle Methods: 0 Total)
"
`;
exports[`ReactDebugFiberPerf old scheduler properly displays the forwardRef component in measurements 1`] = `
"⚛ (Waiting for async callback... will force flush in 5250 ms)
// Mount
⚛ (React Tree Reconciliation: Completed Root)
⚛ Parent [mount]
⚛ ForwardRef [mount]
⚛ Child [mount]
⚛ ForwardRef(refForwarder) [mount]
⚛ Child [mount]
⚛ ForwardRef(OverriddenName) [mount]
⚛ Child [mount]
⚛ (Committing Changes)
⚛ (Committing Snapshot Effects: 0 Total)
⚛ (Committing Host Effects: 1 Total)
⚛ (Calling Lifecycle Methods: 0 Total)
"
`;
exports[`ReactDebugFiberPerf old scheduler recovers from caught errors 1`] = `
"⚛ (Waiting for async callback... will force flush in 5250 ms)
// Stop on Baddie and restart from Boundary
⚛ (React Tree Reconciliation: Completed Root)
⚛ Parent [mount]
⛔ Boundary [mount] Warning: An error was thrown inside this error boundary
⚛ Parent [mount]
⚛ Baddie [mount]
⚛ Boundary [mount]
⚛ (React Tree Reconciliation: Completed Root)
⚛ Parent [mount]
⛔ Boundary [mount] Warning: An error was thrown inside this error boundary
⚛ Parent [mount]
⚛ Baddie [mount]
⚛ Boundary [mount]
⛔ (Committing Changes) Warning: Lifecycle hook scheduled a cascading update
⚛ (Committing Snapshot Effects: 0 Total)
⚛ (Committing Host Effects: 2 Total)
⚛ (Calling Lifecycle Methods: 1 Total)
⚛ (React Tree Reconciliation: Completed Root)
⚛ Boundary [update]
⚛ ErrorReport [mount]
⚛ (Committing Changes)
⚛ (Committing Snapshot Effects: 0 Total)
⚛ (Committing Host Effects: 1 Total)
⚛ (Calling Lifecycle Methods: 0 Total)
"
`;
exports[`ReactDebugFiberPerf old scheduler recovers from fatal errors 1`] = `
"⚛ (Waiting for async callback... will force flush in 5250 ms)
// Will fatal
⚛ (React Tree Reconciliation: Completed Root)
⚛ Parent [mount]
⚛ Baddie [mount]
⚛ (React Tree Reconciliation: Completed Root)
⚛ Parent [mount]
⚛ Baddie [mount]
⚛ (Committing Changes)
⚛ (Committing Snapshot Effects: 0 Total)
⚛ (Committing Host Effects: 1 Total)
⚛ (Calling Lifecycle Methods: 1 Total)
⚛ (Waiting for async callback... will force flush in 5250 ms)
// Will reconcile from a clean state
⚛ (React Tree Reconciliation: Completed Root)
⚛ Parent [mount]
⚛ Child [mount]
⚛ (Committing Changes)
⚛ (Committing Snapshot Effects: 0 Total)
⚛ (Committing Host Effects: 1 Total)
⚛ (Calling Lifecycle Methods: 0 Total)
"
`;
exports[`ReactDebugFiberPerf old scheduler skips parents during setState 1`] = `
"⚛ (Waiting for async callback... will force flush in 5250 ms)
// Should include just A and B, no Parents
⚛ (React Tree Reconciliation: Completed Root)
⚛ A [update]
⚛ B [update]
⚛ (Committing Changes)
⚛ (Committing Snapshot Effects: 0 Total)
⚛ (Committing Host Effects: 2 Total)
⚛ (Calling Lifecycle Methods: 2 Total)
"
`;
exports[`ReactDebugFiberPerf old scheduler supports Suspense and lazy 1`] = `
"⚛ (Waiting for async callback... will force flush in 5250 ms)
⚛ (React Tree Reconciliation: Completed Root)
⚛ Parent [mount]
⛔ Suspense [mount] Warning: Rendering was suspended
⚛ Suspense [mount]
⚛ Spinner [mount]
"
`;
exports[`ReactDebugFiberPerf old scheduler supports Suspense and lazy 2`] = `
"⚛ (Waiting for async callback... will force flush in 5250 ms)
⚛ (React Tree Reconciliation: Completed Root)
⚛ Parent [mount]
⛔ Suspense [mount] Warning: Rendering was suspended
⚛ Suspense [mount]
⚛ Spinner [mount]
⚛ (Waiting for async callback... will force flush in 5250 ms)
⚛ (React Tree Reconciliation: Completed Root)
⚛ Parent [mount]
⚛ Suspense [mount]
⚛ Foo [mount]
⚛ (Committing Changes)
⚛ (Committing Snapshot Effects: 0 Total)
⚛ (Committing Host Effects: 1 Total)
⚛ (Calling Lifecycle Methods: 0 Total)
"
`;
exports[`ReactDebugFiberPerf old scheduler supports memo 1`] = `
"⚛ (Waiting for async callback... will force flush in 5250 ms)
⚛ (React Tree Reconciliation: Completed Root)
⚛ Parent [mount]
⚛ Foo [mount]
⚛ (Committing Changes)
⚛ (Committing Snapshot Effects: 0 Total)
⚛ (Committing Host Effects: 1 Total)
⚛ (Calling Lifecycle Methods: 0 Total)
"
`;
exports[`ReactDebugFiberPerf old scheduler supports portals 1`] = `
"⚛ (Waiting for async callback... will force flush in 5250 ms)
⚛ (React Tree Reconciliation: Completed Root)
⚛ Parent [mount]
⚛ Child [mount]
⚛ (Committing Changes)
⚛ (Committing Snapshot Effects: 0 Total)
⚛ (Committing Host Effects: 2 Total)
⚛ (Calling Lifecycle Methods: 0 Total)
"
`;
exports[`ReactDebugFiberPerf old scheduler warns if an in-progress update is interrupted 1`] = `
"⚛ (Waiting for async callback... will force flush in 5250 ms)
⚛ (React Tree Reconciliation: Yielded)
⚛ Foo [mount]
⚛ (Waiting for async callback... will force flush in 5250 ms)
⛔ (React Tree Reconciliation: Completed Root) Warning: A top-level update interrupted the previous render
⚛ Foo [mount]
⚛ (Committing Changes)
⚛ (Committing Snapshot Effects: 0 Total)
⚛ (Committing Host Effects: 1 Total)
⚛ (Calling Lifecycle Methods: 0 Total)
⚛ (React Tree Reconciliation: Completed Root)
⚛ (Committing Changes)
⚛ (Committing Snapshot Effects: 0 Total)
⚛ (Committing Host Effects: 0 Total)
⚛ (Calling Lifecycle Methods: 0 Total)
"
`;
exports[`ReactDebugFiberPerf old scheduler warns if async work expires (starvation) 1`] = `
"⛔ (Waiting for async callback... will force flush in 5250 ms) Warning: React was blocked by main thread
⚛ (React Tree Reconciliation: Completed Root)
⚛ Foo [mount]
⚛ (Committing Changes)
⚛ (Committing Snapshot Effects: 0 Total)
⚛ (Committing Host Effects: 1 Total)
⚛ (Calling Lifecycle Methods: 0 Total)
"
`;
exports[`ReactDebugFiberPerf old scheduler warns on cascading renders from setState 1`] = `
"⚛ (Waiting for async callback... will force flush in 5250 ms)
// Should print a warning
⚛ (React Tree Reconciliation: Completed Root)
⚛ Parent [mount]
⚛ Cascading [mount]
⛔ (Committing Changes) Warning: Lifecycle hook scheduled a cascading update
⚛ (Committing Snapshot Effects: 0 Total)
⚛ (Committing Host Effects: 2 Total)
⚛ (Calling Lifecycle Methods: 1 Total)
⛔ Cascading.componentDidMount Warning: Scheduled a cascading update
⚛ (React Tree Reconciliation: Completed Root)
⚛ Cascading [update]
⚛ (Committing Changes)
⚛ (Committing Snapshot Effects: 0 Total)
⚛ (Committing Host Effects: 1 Total)
⚛ (Calling Lifecycle Methods: 1 Total)
"
`;
exports[`ReactDebugFiberPerf old scheduler warns on cascading renders from top-level render 1`] = `
"⚛ (Waiting for async callback... will force flush in 5250 ms)
// Rendering the first root

View File

@ -9,15 +9,25 @@
'use strict';
const ReactDOM = require('react-dom');
let ReactDOM;
let React;
let ReactCache;
let ReactTestRenderer;
let Scheduler;
describe('ReactTestRenderer', () => {
beforeEach(() => {
jest.resetModules();
ReactDOM = require('react-dom');
// Isolate test renderer.
jest.resetModules();
const React = require('react');
const ReactCache = require('react-cache');
const ReactTestRenderer = require('react-test-renderer');
React = require('react');
ReactCache = require('react-cache');
ReactTestRenderer = require('react-test-renderer');
Scheduler = require('scheduler');
});
describe('ReactTestRenderer', () => {
it('should warn if used to render a ReactDOM portal', () => {
const container = document.createElement('div');
expect(() => {
@ -62,6 +72,7 @@ describe('ReactTestRenderer', () => {
const root = ReactTestRenderer.create(<App text="initial" />);
PendingResources.initial('initial');
await Promise.resolve();
Scheduler.flushAll();
expect(root.toJSON()).toEqual('initial');
root.update(<App text="dynamic" />);
@ -69,6 +80,7 @@ describe('ReactTestRenderer', () => {
PendingResources.dynamic('dynamic');
await Promise.resolve();
Scheduler.flushAll();
expect(root.toJSON()).toEqual('dynamic');
done();
@ -88,6 +100,7 @@ describe('ReactTestRenderer', () => {
const root = ReactTestRenderer.create(<App text="initial" />);
PendingResources.initial('initial');
await Promise.resolve();
Scheduler.flushAll();
expect(root.toJSON().children).toEqual(['initial']);
root.update(<App text="dynamic" />);
@ -95,6 +108,7 @@ describe('ReactTestRenderer', () => {
PendingResources.dynamic('dynamic');
await Promise.resolve();
Scheduler.flushAll();
expect(root.toJSON().children).toEqual(['dynamic']);
done();

View File

@ -12,6 +12,7 @@
let React;
let ReactFeatureFlags;
let enableNewScheduler;
let ReactNoop;
let Scheduler;
let ReactCache;
@ -35,6 +36,7 @@ function loadModules({
ReactFeatureFlags.enableProfilerTimer = enableProfilerTimer;
ReactFeatureFlags.enableSchedulerTracing = enableSchedulerTracing;
ReactFeatureFlags.replayFailedUnitOfWorkWithInvokeGuardedCallback = replayFailedUnitOfWorkWithInvokeGuardedCallback;
enableNewScheduler = ReactFeatureFlags.enableNewScheduler;
React = require('react');
Scheduler = require('scheduler');
@ -1352,6 +1354,9 @@ describe('Profiler', () => {
},
);
}).toThrow('Expected error onWorkScheduled');
if (enableNewScheduler) {
expect(Scheduler).toFlushAndYield(['Component:fail']);
}
throwInOnWorkScheduled = false;
expect(onWorkScheduled).toHaveBeenCalled();
@ -1386,7 +1391,14 @@ describe('Profiler', () => {
// Errors that happen inside of a subscriber should throw,
throwInOnWorkStarted = true;
expect(Scheduler).toFlushAndThrow('Expected error onWorkStarted');
if (enableNewScheduler) {
// Rendering was interrupted by the error that was thrown
expect(Scheduler).toHaveYielded([]);
// Rendering continues in the next task
expect(Scheduler).toFlushAndYield(['Component:text']);
} else {
expect(Scheduler).toHaveYielded(['Component:text']);
}
throwInOnWorkStarted = false;
expect(onWorkStarted).toHaveBeenCalled();
@ -2370,11 +2382,23 @@ describe('Profiler', () => {
},
);
expect(Scheduler).toHaveYielded(['Suspend [loaded]', 'Text [loading]']);
expect(onInteractionScheduledWorkCompleted).not.toHaveBeenCalled();
jest.runAllTimers();
await resourcePromise;
if (enableNewScheduler) {
expect(Scheduler).toHaveYielded(['Promise resolved [loaded]']);
expect(Scheduler).toFlushExpired(['AsyncText [loaded]']);
} else {
expect(Scheduler).toHaveYielded([
'Promise resolved [loaded]',
'AsyncText [loaded]',
]);
}
expect(onInteractionScheduledWorkCompleted).toHaveBeenCalledTimes(1);
expect(
onInteractionScheduledWorkCompleted,
@ -2424,9 +2448,16 @@ describe('Profiler', () => {
expect(onInteractionScheduledWorkCompleted).not.toHaveBeenCalled();
expect(Scheduler).toHaveYielded(['Text [loading]']);
jest.runAllTimers();
await resourcePromise;
expect(Scheduler).toHaveYielded(['Promise resolved [loaded]']);
if (enableNewScheduler) {
expect(Scheduler).toFlushExpired([]);
}
expect(onInteractionScheduledWorkCompleted).not.toHaveBeenCalled();
wrappedCascadingFn();
@ -2578,6 +2609,14 @@ describe('Profiler', () => {
},
);
});
expect(Scheduler).toHaveYielded([
'Suspend [loaded]',
'Text [loading]',
'Text [initial]',
'Suspend [loaded]',
'Text [loading]',
'Text [updated]',
]);
expect(renderer.toJSON()).toEqual(['loading', 'updated']);
expect(onRender).toHaveBeenCalledTimes(1);
@ -2591,6 +2630,17 @@ describe('Profiler', () => {
Scheduler.advanceTime(100);
jest.advanceTimersByTime(100);
await originalPromise;
if (enableNewScheduler) {
expect(Scheduler).toHaveYielded(['Promise resolved [loaded]']);
expect(Scheduler).toFlushExpired(['AsyncText [loaded]']);
} else {
expect(Scheduler).toHaveYielded([
'Promise resolved [loaded]',
'AsyncText [loaded]',
]);
}
expect(renderer.toJSON()).toEqual(['loaded', 'updated']);
expect(onRender).toHaveBeenCalledTimes(1);

View File

@ -40,7 +40,7 @@ var IDLE_PRIORITY = maxSigned31BitInt;
// Callbacks are stored as a circular, doubly linked list.
var firstCallbackNode = null;
var currentDidTimeout = false;
var currentHostCallbackDidTimeout = false;
// Pausing the scheduler is useful for debugging.
var isSchedulerPaused = false;
@ -48,29 +48,31 @@ var currentPriorityLevel = NormalPriority;
var currentEventStartTime = -1;
var currentExpirationTime = -1;
// This is set when a callback is being executed, to prevent re-entrancy.
var isExecutingCallback = false;
// This is set while performing work, to prevent re-entrancy.
var isPerformingWork = false;
var isHostCallbackScheduled = false;
function ensureHostCallbackIsScheduled() {
if (isExecutingCallback) {
function scheduleHostCallbackIfNeeded() {
if (isPerformingWork) {
// Don't schedule work yet; wait until the next time we yield.
return;
}
if (firstCallbackNode !== null) {
// Schedule the host callback using the earliest expiration in the list.
var expirationTime = firstCallbackNode.expirationTime;
if (!isHostCallbackScheduled) {
isHostCallbackScheduled = true;
} else {
if (isHostCallbackScheduled) {
// Cancel the existing host callback.
cancelHostCallback();
} else {
isHostCallbackScheduled = true;
}
requestHostCallback(flushWork, expirationTime);
}
}
function flushFirstCallback() {
var flushedNode = firstCallbackNode;
const currentlyFlushingCallback = firstCallbackNode;
// Remove the node from the list before calling the callback. That way the
// list is in a consistent state even if the callback throws.
@ -85,19 +87,25 @@ function flushFirstCallback() {
next.previous = lastCallbackNode;
}
flushedNode.next = flushedNode.previous = null;
currentlyFlushingCallback.next = currentlyFlushingCallback.previous = null;
// Now it's safe to call the callback.
var callback = flushedNode.callback;
var expirationTime = flushedNode.expirationTime;
var priorityLevel = flushedNode.priorityLevel;
var callback = currentlyFlushingCallback.callback;
var expirationTime = currentlyFlushingCallback.expirationTime;
var priorityLevel = currentlyFlushingCallback.priorityLevel;
var previousPriorityLevel = currentPriorityLevel;
var previousExpirationTime = currentExpirationTime;
currentPriorityLevel = priorityLevel;
currentExpirationTime = expirationTime;
var continuationCallback;
try {
continuationCallback = callback(currentDidTimeout);
const didUserCallbackTimeout =
currentHostCallbackDidTimeout ||
// Immediate priority callbacks are always called as if they timed out
priorityLevel === ImmediatePriority;
continuationCallback = callback(didUserCallbackTimeout);
} catch (error) {
throw error;
} finally {
currentPriorityLevel = previousPriorityLevel;
currentExpirationTime = previousExpirationTime;
@ -141,7 +149,7 @@ function flushFirstCallback() {
} else if (nextAfterContinuation === firstCallbackNode) {
// The new callback is the highest priority callback in the list.
firstCallbackNode = continuationNode;
ensureHostCallbackIsScheduled();
scheduleHostCallbackIfNeeded();
}
var previous = nextAfterContinuation.previous;
@ -152,46 +160,20 @@ function flushFirstCallback() {
}
}
function flushImmediateWork() {
if (
// Confirm we've exited the outer most event handler
currentEventStartTime === -1 &&
firstCallbackNode !== null &&
firstCallbackNode.priorityLevel === ImmediatePriority
) {
isExecutingCallback = true;
try {
do {
flushFirstCallback();
} while (
// Keep flushing until there are no more immediate callbacks
firstCallbackNode !== null &&
firstCallbackNode.priorityLevel === ImmediatePriority
);
} finally {
isExecutingCallback = false;
if (firstCallbackNode !== null) {
// There's still work remaining. Request another callback.
ensureHostCallbackIsScheduled();
} else {
isHostCallbackScheduled = false;
}
}
}
}
function flushWork(didTimeout) {
function flushWork(didUserCallbackTimeout) {
// Exit right away if we're currently paused
if (enableSchedulerDebugging && isSchedulerPaused) {
return;
}
isExecutingCallback = true;
const previousDidTimeout = currentDidTimeout;
currentDidTimeout = didTimeout;
// We'll need a new host callback the next time work is scheduled.
isHostCallbackScheduled = false;
isPerformingWork = true;
const previousDidTimeout = currentHostCallbackDidTimeout;
currentHostCallbackDidTimeout = didUserCallbackTimeout;
try {
if (didTimeout) {
if (didUserCallbackTimeout) {
// Flush all the expired callbacks without yielding.
while (
firstCallbackNode !== null &&
@ -226,16 +208,10 @@ function flushWork(didTimeout) {
}
}
} finally {
isExecutingCallback = false;
currentDidTimeout = previousDidTimeout;
if (firstCallbackNode !== null) {
isPerformingWork = false;
currentHostCallbackDidTimeout = previousDidTimeout;
// There's still work remaining. Request another callback.
ensureHostCallbackIsScheduled();
} else {
isHostCallbackScheduled = false;
}
// Before exiting, flush all the immediate work that was scheduled.
flushImmediateWork();
scheduleHostCallbackIfNeeded();
}
}
@ -258,12 +234,13 @@ function unstable_runWithPriority(priorityLevel, eventHandler) {
try {
return eventHandler();
} catch (error) {
// There's still work remaining. Request another callback.
scheduleHostCallbackIfNeeded();
throw error;
} finally {
currentPriorityLevel = previousPriorityLevel;
currentEventStartTime = previousEventStartTime;
// Before exiting, flush all the immediate work that was scheduled.
flushImmediateWork();
}
}
@ -289,12 +266,13 @@ function unstable_next(eventHandler) {
try {
return eventHandler();
} catch (error) {
// There's still work remaining. Request another callback.
scheduleHostCallbackIfNeeded();
throw error;
} finally {
currentPriorityLevel = previousPriorityLevel;
currentEventStartTime = previousEventStartTime;
// Before exiting, flush all the immediate work that was scheduled.
flushImmediateWork();
}
}
@ -309,15 +287,22 @@ function unstable_wrapCallback(callback) {
try {
return callback.apply(this, arguments);
} catch (error) {
// There's still work remaining. Request another callback.
scheduleHostCallbackIfNeeded();
throw error;
} finally {
currentPriorityLevel = previousPriorityLevel;
currentEventStartTime = previousEventStartTime;
flushImmediateWork();
}
};
}
function unstable_scheduleCallback(callback, deprecated_options) {
function unstable_scheduleCallback(
priorityLevel,
callback,
deprecated_options,
) {
var startTime =
currentEventStartTime !== -1 ? currentEventStartTime : getCurrentTime();
@ -330,7 +315,7 @@ function unstable_scheduleCallback(callback, deprecated_options) {
// FIXME: Remove this branch once we lift expiration times out of React.
expirationTime = startTime + deprecated_options.timeout;
} else {
switch (currentPriorityLevel) {
switch (priorityLevel) {
case ImmediatePriority:
expirationTime = startTime + IMMEDIATE_PRIORITY_TIMEOUT;
break;
@ -351,7 +336,7 @@ function unstable_scheduleCallback(callback, deprecated_options) {
var newNode = {
callback,
priorityLevel: currentPriorityLevel,
priorityLevel: priorityLevel,
expirationTime,
next: null,
previous: null,
@ -363,7 +348,7 @@ function unstable_scheduleCallback(callback, deprecated_options) {
if (firstCallbackNode === null) {
// This is the first callback in the list.
firstCallbackNode = newNode.next = newNode.previous = newNode;
ensureHostCallbackIsScheduled();
scheduleHostCallbackIfNeeded();
} else {
var next = null;
var node = firstCallbackNode;
@ -383,7 +368,7 @@ function unstable_scheduleCallback(callback, deprecated_options) {
} else if (next === firstCallbackNode) {
// The new callback has the earliest expiration in the entire list.
firstCallbackNode = newNode;
ensureHostCallbackIsScheduled();
scheduleHostCallbackIfNeeded();
}
var previous = next.previous;
@ -402,7 +387,7 @@ function unstable_pauseExecution() {
function unstable_continueExecution() {
isSchedulerPaused = false;
if (firstCallbackNode !== null) {
ensureHostCallbackIsScheduled();
scheduleHostCallbackIfNeeded();
}
}
@ -439,7 +424,7 @@ function unstable_getCurrentPriorityLevel() {
function unstable_shouldYield() {
return (
!currentDidTimeout &&
!currentHostCallbackDidTimeout &&
((firstCallbackNode !== null &&
firstCallbackNode.expirationTime < currentExpirationTime) ||
shouldYieldToHost())

View File

@ -39,10 +39,10 @@ describe('Scheduler', () => {
});
it('flushes work incrementally', () => {
scheduleCallback(() => Scheduler.yieldValue('A'));
scheduleCallback(() => Scheduler.yieldValue('B'));
scheduleCallback(() => Scheduler.yieldValue('C'));
scheduleCallback(() => Scheduler.yieldValue('D'));
scheduleCallback(NormalPriority, () => Scheduler.yieldValue('A'));
scheduleCallback(NormalPriority, () => Scheduler.yieldValue('B'));
scheduleCallback(NormalPriority, () => Scheduler.yieldValue('C'));
scheduleCallback(NormalPriority, () => Scheduler.yieldValue('D'));
expect(Scheduler).toFlushAndYieldThrough(['A', 'B']);
expect(Scheduler).toFlushAndYieldThrough(['C']);
@ -50,9 +50,11 @@ describe('Scheduler', () => {
});
it('cancels work', () => {
scheduleCallback(() => Scheduler.yieldValue('A'));
const callbackHandleB = scheduleCallback(() => Scheduler.yieldValue('B'));
scheduleCallback(() => Scheduler.yieldValue('C'));
scheduleCallback(NormalPriority, () => Scheduler.yieldValue('A'));
const callbackHandleB = scheduleCallback(NormalPriority, () =>
Scheduler.yieldValue('B'),
);
scheduleCallback(NormalPriority, () => Scheduler.yieldValue('C'));
cancelCallback(callbackHandleB);
@ -64,49 +66,43 @@ describe('Scheduler', () => {
});
it('executes the highest priority callbacks first', () => {
scheduleCallback(() => Scheduler.yieldValue('A'));
scheduleCallback(() => Scheduler.yieldValue('B'));
scheduleCallback(NormalPriority, () => Scheduler.yieldValue('A'));
scheduleCallback(NormalPriority, () => Scheduler.yieldValue('B'));
// Yield before B is flushed
expect(Scheduler).toFlushAndYieldThrough(['A']);
runWithPriority(UserBlockingPriority, () => {
scheduleCallback(() => Scheduler.yieldValue('C'));
scheduleCallback(() => Scheduler.yieldValue('D'));
});
scheduleCallback(UserBlockingPriority, () => Scheduler.yieldValue('C'));
scheduleCallback(UserBlockingPriority, () => Scheduler.yieldValue('D'));
// C and D should come first, because they are higher priority
expect(Scheduler).toFlushAndYield(['C', 'D', 'B']);
});
it('expires work', () => {
scheduleCallback(didTimeout => {
scheduleCallback(NormalPriority, didTimeout => {
Scheduler.advanceTime(100);
Scheduler.yieldValue(`A (did timeout: ${didTimeout})`);
});
runWithPriority(UserBlockingPriority, () => {
scheduleCallback(didTimeout => {
scheduleCallback(UserBlockingPriority, didTimeout => {
Scheduler.advanceTime(100);
Scheduler.yieldValue(`B (did timeout: ${didTimeout})`);
});
});
runWithPriority(UserBlockingPriority, () => {
scheduleCallback(didTimeout => {
scheduleCallback(UserBlockingPriority, didTimeout => {
Scheduler.advanceTime(100);
Scheduler.yieldValue(`C (did timeout: ${didTimeout})`);
});
});
// Advance time, but not by enough to expire any work
Scheduler.advanceTime(249);
expect(Scheduler).toHaveYielded([]);
// Schedule a few more callbacks
scheduleCallback(didTimeout => {
scheduleCallback(NormalPriority, didTimeout => {
Scheduler.advanceTime(100);
Scheduler.yieldValue(`D (did timeout: ${didTimeout})`);
});
scheduleCallback(didTimeout => {
scheduleCallback(NormalPriority, didTimeout => {
Scheduler.advanceTime(100);
Scheduler.yieldValue(`E (did timeout: ${didTimeout})`);
});
@ -130,7 +126,7 @@ describe('Scheduler', () => {
});
it('has a default expiration of ~5 seconds', () => {
scheduleCallback(() => Scheduler.yieldValue('A'));
scheduleCallback(NormalPriority, () => Scheduler.yieldValue('A'));
Scheduler.advanceTime(4999);
expect(Scheduler).toHaveYielded([]);
@ -140,11 +136,11 @@ describe('Scheduler', () => {
});
it('continues working on same task after yielding', () => {
scheduleCallback(() => {
scheduleCallback(NormalPriority, () => {
Scheduler.advanceTime(100);
Scheduler.yieldValue('A');
});
scheduleCallback(() => {
scheduleCallback(NormalPriority, () => {
Scheduler.advanceTime(100);
Scheduler.yieldValue('B');
});
@ -163,13 +159,13 @@ describe('Scheduler', () => {
}
};
scheduleCallback(C);
scheduleCallback(NormalPriority, C);
scheduleCallback(() => {
scheduleCallback(NormalPriority, () => {
Scheduler.advanceTime(100);
Scheduler.yieldValue('D');
});
scheduleCallback(() => {
scheduleCallback(NormalPriority, () => {
Scheduler.advanceTime(100);
Scheduler.yieldValue('E');
});
@ -197,7 +193,7 @@ describe('Scheduler', () => {
};
// Schedule a high priority callback
runWithPriority(UserBlockingPriority, () => scheduleCallback(work));
scheduleCallback(UserBlockingPriority, work);
// Flush until just before the expiration time
expect(Scheduler).toFlushAndYieldThrough(['A', 'B']);
@ -207,26 +203,6 @@ describe('Scheduler', () => {
expect(Scheduler).toHaveYielded(['C', 'D']);
});
it('nested callbacks inherit the priority of the currently executing callback', () => {
runWithPriority(UserBlockingPriority, () => {
scheduleCallback(() => {
Scheduler.advanceTime(100);
Scheduler.yieldValue('Parent callback');
scheduleCallback(() => {
Scheduler.advanceTime(100);
Scheduler.yieldValue('Nested callback');
});
});
});
expect(Scheduler).toFlushAndYieldThrough(['Parent callback']);
// The nested callback has user-blocking priority, so it should
// expire quickly.
Scheduler.advanceTime(250 + 100);
expect(Scheduler).toHaveYielded(['Nested callback']);
});
it('continuations are interrupted by higher priority work', () => {
const tasks = [['A', 100], ['B', 100], ['C', 100], ['D', 100]];
const work = () => {
@ -239,15 +215,13 @@ describe('Scheduler', () => {
}
}
};
scheduleCallback(work);
scheduleCallback(NormalPriority, work);
expect(Scheduler).toFlushAndYieldThrough(['A']);
runWithPriority(UserBlockingPriority, () => {
scheduleCallback(() => {
scheduleCallback(UserBlockingPriority, () => {
Scheduler.advanceTime(100);
Scheduler.yieldValue('High pri');
});
});
expect(Scheduler).toFlushAndYield(['High pri', 'B', 'C', 'D']);
});
@ -266,12 +240,10 @@ describe('Scheduler', () => {
if (task[0] === 'B') {
// Schedule high pri work from inside another callback
Scheduler.yieldValue('Schedule high pri');
runWithPriority(UserBlockingPriority, () =>
scheduleCallback(() => {
scheduleCallback(UserBlockingPriority, () => {
Scheduler.advanceTime(100);
Scheduler.yieldValue('High pri');
}),
);
});
}
if (tasks.length > 0 && shouldYield()) {
Scheduler.yieldValue('Yield!');
@ -279,7 +251,7 @@ describe('Scheduler', () => {
}
}
};
scheduleCallback(work);
scheduleCallback(NormalPriority, work);
expect(Scheduler).toFlushAndYield([
'A',
'B',
@ -295,21 +267,28 @@ describe('Scheduler', () => {
},
);
it('immediate callbacks fire at the end of outermost event', () => {
runWithPriority(ImmediatePriority, () => {
scheduleCallback(() => Scheduler.yieldValue('A'));
scheduleCallback(() => Scheduler.yieldValue('B'));
// Nested event
runWithPriority(ImmediatePriority, () => {
scheduleCallback(() => Scheduler.yieldValue('C'));
// Nothing should have fired yet
it('top-level immediate callbacks fire in a subsequent task', () => {
scheduleCallback(ImmediatePriority, () => Scheduler.yieldValue('A'));
scheduleCallback(ImmediatePriority, () => Scheduler.yieldValue('B'));
scheduleCallback(ImmediatePriority, () => Scheduler.yieldValue('C'));
scheduleCallback(ImmediatePriority, () => Scheduler.yieldValue('D'));
// Immediate callback hasn't fired, yet.
expect(Scheduler).toHaveYielded([]);
// They all flush immediately within the subsequent task.
expect(Scheduler).toFlushExpired(['A', 'B', 'C', 'D']);
});
// Nothing should have fired yet
it('nested immediate callbacks are added to the queue of immediate callbacks', () => {
scheduleCallback(ImmediatePriority, () => Scheduler.yieldValue('A'));
scheduleCallback(ImmediatePriority, () => {
Scheduler.yieldValue('B');
// This callback should go to the end of the queue
scheduleCallback(ImmediatePriority, () => Scheduler.yieldValue('C'));
});
scheduleCallback(ImmediatePriority, () => Scheduler.yieldValue('D'));
expect(Scheduler).toHaveYielded([]);
});
// The callbacks were called at the end of the outer event
expect(Scheduler).toHaveYielded(['A', 'B', 'C']);
// C should flush at the end
expect(Scheduler).toFlushExpired(['A', 'B', 'D', 'C']);
});
it('wrapped callbacks have same signature as original callback', () => {
@ -318,108 +297,83 @@ describe('Scheduler', () => {
});
it('wrapped callbacks inherit the current priority', () => {
const wrappedCallback = wrapCallback(() => {
scheduleCallback(() => {
Scheduler.advanceTime(100);
Scheduler.yieldValue('Normal');
});
});
const wrappedInteractiveCallback = runWithPriority(
UserBlockingPriority,
() =>
const wrappedCallback = runWithPriority(NormalPriority, () =>
wrapCallback(() => {
scheduleCallback(() => {
Scheduler.advanceTime(100);
Scheduler.yieldValue('User-blocking');
});
Scheduler.yieldValue(getCurrentPriorityLevel());
}),
);
const wrappedUserBlockingCallback = runWithPriority(
UserBlockingPriority,
() =>
wrapCallback(() => {
Scheduler.yieldValue(getCurrentPriorityLevel());
}),
);
// This should schedule a normal callback
wrappedCallback();
// This should schedule an user-blocking callback
wrappedInteractiveCallback();
expect(Scheduler).toHaveYielded([NormalPriority]);
Scheduler.advanceTime(249);
expect(Scheduler).toHaveYielded([]);
Scheduler.advanceTime(1);
expect(Scheduler).toHaveYielded(['User-blocking']);
Scheduler.advanceTime(10000);
expect(Scheduler).toHaveYielded(['Normal']);
wrappedUserBlockingCallback();
expect(Scheduler).toHaveYielded([UserBlockingPriority]);
});
it('wrapped callbacks inherit the current priority even when nested', () => {
const wrappedCallback = wrapCallback(() => {
scheduleCallback(() => {
Scheduler.advanceTime(100);
Scheduler.yieldValue('Normal');
let wrappedCallback;
let wrappedUserBlockingCallback;
runWithPriority(NormalPriority, () => {
wrappedCallback = wrapCallback(() => {
Scheduler.yieldValue(getCurrentPriorityLevel());
});
});
const wrappedInteractiveCallback = runWithPriority(
UserBlockingPriority,
() =>
wrappedUserBlockingCallback = runWithPriority(UserBlockingPriority, () =>
wrapCallback(() => {
scheduleCallback(() => {
Scheduler.advanceTime(100);
Scheduler.yieldValue('User-blocking');
});
Scheduler.yieldValue(getCurrentPriorityLevel());
}),
);
});
runWithPriority(UserBlockingPriority, () => {
// This should schedule a normal callback
wrappedCallback();
// This should schedule an user-blocking callback
wrappedInteractiveCallback();
});
expect(Scheduler).toHaveYielded([NormalPriority]);
Scheduler.advanceTime(249);
expect(Scheduler).toHaveYielded([]);
Scheduler.advanceTime(1);
expect(Scheduler).toHaveYielded(['User-blocking']);
Scheduler.advanceTime(10000);
expect(Scheduler).toHaveYielded(['Normal']);
});
it('immediate callbacks fire at the end of callback', () => {
const immediateCallback = runWithPriority(ImmediatePriority, () =>
wrapCallback(() => {
scheduleCallback(() => Scheduler.yieldValue('callback'));
}),
);
immediateCallback();
// The callback was called at the end of the outer event
expect(Scheduler).toHaveYielded(['callback']);
wrappedUserBlockingCallback();
expect(Scheduler).toHaveYielded([UserBlockingPriority]);
});
it("immediate callbacks fire even if there's an error", () => {
expect(() => {
runWithPriority(ImmediatePriority, () => {
scheduleCallback(() => {
scheduleCallback(ImmediatePriority, () => {
Scheduler.yieldValue('A');
throw new Error('Oops A');
});
scheduleCallback(() => {
scheduleCallback(ImmediatePriority, () => {
Scheduler.yieldValue('B');
});
scheduleCallback(() => {
scheduleCallback(ImmediatePriority, () => {
Scheduler.yieldValue('C');
throw new Error('Oops C');
});
});
}).toThrow('Oops A');
expect(() => expect(Scheduler).toFlushExpired()).toThrow('Oops A');
expect(Scheduler).toHaveYielded(['A']);
// B and C flush in a subsequent event. That way, the second error is not
// swallowed.
expect(() => Scheduler.unstable_flushExpired()).toThrow('Oops C');
expect(() => expect(Scheduler).toFlushExpired()).toThrow('Oops C');
expect(Scheduler).toHaveYielded(['B', 'C']);
});
it('multiple immediate callbacks can throw and there will be an error for each one', () => {
scheduleCallback(ImmediatePriority, () => {
throw new Error('First error');
});
scheduleCallback(ImmediatePriority, () => {
throw new Error('Second error');
});
expect(() => Scheduler.flushAll()).toThrow('First error');
// The next error is thrown in the subsequent event
expect(() => Scheduler.flushAll()).toThrow('Second error');
});
it('exposes the current priority level', () => {
Scheduler.yieldValue(getCurrentPriorityLevel());
runWithPriority(ImmediatePriority, () => {

View File

@ -102,20 +102,26 @@ describe('SchedulerDOM', () => {
describe('scheduleCallback', () => {
it('calls the callback within the frame when not blocked', () => {
const {unstable_scheduleCallback: scheduleCallback} = Scheduler;
const {
unstable_scheduleCallback: scheduleCallback,
unstable_NormalPriority: NormalPriority,
} = Scheduler;
const cb = jest.fn();
scheduleCallback(cb);
scheduleCallback(NormalPriority, cb);
advanceOneFrame({timeLeftInFrame: 15});
expect(cb).toHaveBeenCalledTimes(1);
});
it('inserts its rAF callback as early into the queue as possible', () => {
const {unstable_scheduleCallback: scheduleCallback} = Scheduler;
const {
unstable_scheduleCallback: scheduleCallback,
unstable_NormalPriority: NormalPriority,
} = Scheduler;
const log = [];
const useRAFCallback = () => {
log.push('userRAFCallback');
};
scheduleCallback(() => {
scheduleCallback(NormalPriority, () => {
// Call rAF while idle work is being flushed.
requestAnimationFrame(useRAFCallback);
});
@ -130,15 +136,18 @@ describe('SchedulerDOM', () => {
describe('with multiple callbacks', () => {
it('accepts multiple callbacks and calls within frame when not blocked', () => {
const {unstable_scheduleCallback: scheduleCallback} = Scheduler;
const {
unstable_scheduleCallback: scheduleCallback,
unstable_NormalPriority: NormalPriority,
} = Scheduler;
const callbackLog = [];
const callbackA = jest.fn(() => callbackLog.push('A'));
const callbackB = jest.fn(() => callbackLog.push('B'));
scheduleCallback(callbackA);
scheduleCallback(NormalPriority, callbackA);
// initially waits to call the callback
expect(callbackLog).toEqual([]);
// waits while second callback is passed
scheduleCallback(callbackB);
scheduleCallback(NormalPriority, callbackB);
expect(callbackLog).toEqual([]);
// after a delay, calls as many callbacks as it has time for
advanceOneFrame({timeLeftInFrame: 15});
@ -146,17 +155,20 @@ describe('SchedulerDOM', () => {
});
it("accepts callbacks between animationFrame and postMessage and doesn't stall", () => {
const {unstable_scheduleCallback: scheduleCallback} = Scheduler;
const {
unstable_scheduleCallback: scheduleCallback,
unstable_NormalPriority: NormalPriority,
} = Scheduler;
const callbackLog = [];
const callbackA = jest.fn(() => callbackLog.push('A'));
const callbackB = jest.fn(() => callbackLog.push('B'));
const callbackC = jest.fn(() => callbackLog.push('C'));
scheduleCallback(callbackA);
scheduleCallback(NormalPriority, callbackA);
// initially waits to call the callback
expect(callbackLog).toEqual([]);
runRAFCallbacks();
// this should schedule work *after* the requestAnimationFrame but before the message handler
scheduleCallback(callbackB);
scheduleCallback(NormalPriority, callbackB);
expect(callbackLog).toEqual([]);
// now it should drain the message queue and do all scheduled work
runPostMessageCallbacks({timeLeftInFrame: 15});
@ -166,7 +178,7 @@ describe('SchedulerDOM', () => {
advanceOneFrame({timeLeftInFrame: 15});
// see if more work can be done now.
scheduleCallback(callbackC);
scheduleCallback(NormalPriority, callbackC);
expect(callbackLog).toEqual(['A', 'B']);
advanceOneFrame({timeLeftInFrame: 15});
expect(callbackLog).toEqual(['A', 'B', 'C']);
@ -176,11 +188,14 @@ describe('SchedulerDOM', () => {
'schedules callbacks in correct order and' +
'keeps calling them if there is time',
() => {
const {unstable_scheduleCallback: scheduleCallback} = Scheduler;
const {
unstable_scheduleCallback: scheduleCallback,
unstable_NormalPriority: NormalPriority,
} = Scheduler;
const callbackLog = [];
const callbackA = jest.fn(() => {
callbackLog.push('A');
scheduleCallback(callbackC);
scheduleCallback(NormalPriority, callbackC);
});
const callbackB = jest.fn(() => {
callbackLog.push('B');
@ -189,11 +204,11 @@ describe('SchedulerDOM', () => {
callbackLog.push('C');
});
scheduleCallback(callbackA);
scheduleCallback(NormalPriority, callbackA);
// initially waits to call the callback
expect(callbackLog).toEqual([]);
// continues waiting while B is scheduled
scheduleCallback(callbackB);
scheduleCallback(NormalPriority, callbackB);
expect(callbackLog).toEqual([]);
// after a delay, calls the scheduled callbacks,
// and also calls new callbacks scheduled by current callbacks
@ -203,17 +218,20 @@ describe('SchedulerDOM', () => {
);
it('schedules callbacks in correct order when callbacks have many nested scheduleCallback calls', () => {
const {unstable_scheduleCallback: scheduleCallback} = Scheduler;
const {
unstable_scheduleCallback: scheduleCallback,
unstable_NormalPriority: NormalPriority,
} = Scheduler;
const callbackLog = [];
const callbackA = jest.fn(() => {
callbackLog.push('A');
scheduleCallback(callbackC);
scheduleCallback(callbackD);
scheduleCallback(NormalPriority, callbackC);
scheduleCallback(NormalPriority, callbackD);
});
const callbackB = jest.fn(() => {
callbackLog.push('B');
scheduleCallback(callbackE);
scheduleCallback(callbackF);
scheduleCallback(NormalPriority, callbackE);
scheduleCallback(NormalPriority, callbackF);
});
const callbackC = jest.fn(() => {
callbackLog.push('C');
@ -228,8 +246,8 @@ describe('SchedulerDOM', () => {
callbackLog.push('F');
});
scheduleCallback(callbackA);
scheduleCallback(callbackB);
scheduleCallback(NormalPriority, callbackA);
scheduleCallback(NormalPriority, callbackB);
// initially waits to call the callback
expect(callbackLog).toEqual([]);
// while flushing callbacks, calls as many as it has time for
@ -238,22 +256,25 @@ describe('SchedulerDOM', () => {
});
it('schedules callbacks in correct order when they use scheduleCallback to schedule themselves', () => {
const {unstable_scheduleCallback: scheduleCallback} = Scheduler;
const {
unstable_scheduleCallback: scheduleCallback,
unstable_NormalPriority: NormalPriority,
} = Scheduler;
const callbackLog = [];
let callbackAIterations = 0;
const callbackA = jest.fn(() => {
if (callbackAIterations < 1) {
scheduleCallback(callbackA);
scheduleCallback(NormalPriority, callbackA);
}
callbackLog.push('A' + callbackAIterations);
callbackAIterations++;
});
const callbackB = jest.fn(() => callbackLog.push('B'));
scheduleCallback(callbackA);
scheduleCallback(NormalPriority, callbackA);
// initially waits to call the callback
expect(callbackLog).toEqual([]);
scheduleCallback(callbackB);
scheduleCallback(NormalPriority, callbackB);
expect(callbackLog).toEqual([]);
// after a delay, calls the latest callback passed
advanceOneFrame({timeLeftInFrame: 15});
@ -269,7 +290,10 @@ describe('SchedulerDOM', () => {
describe('when there is no more time left in the frame', () => {
it('calls any callback which has timed out, waits for others', () => {
const {unstable_scheduleCallback: scheduleCallback} = Scheduler;
const {
unstable_scheduleCallback: scheduleCallback,
unstable_NormalPriority: NormalPriority,
} = Scheduler;
startOfLatestFrame = 1000000000000;
currentTime = startOfLatestFrame - 10;
const callbackLog = [];
@ -278,9 +302,9 @@ describe('SchedulerDOM', () => {
const callbackB = jest.fn(() => callbackLog.push('B'));
const callbackC = jest.fn(() => callbackLog.push('C'));
scheduleCallback(callbackA); // won't time out
scheduleCallback(callbackB, {timeout: 100}); // times out later
scheduleCallback(callbackC, {timeout: 2}); // will time out fast
scheduleCallback(NormalPriority, callbackA); // won't time out
scheduleCallback(NormalPriority, callbackB, {timeout: 100}); // times out later
scheduleCallback(NormalPriority, callbackC, {timeout: 2}); // will time out fast
// push time ahead a bit so that we have no idle time
advanceOneFrame({timePastFrameDeadline: 16});
@ -304,7 +328,10 @@ describe('SchedulerDOM', () => {
describe('when there is some time left in the frame', () => {
it('calls timed out callbacks and then any more pending callbacks, defers others if time runs out', () => {
const {unstable_scheduleCallback: scheduleCallback} = Scheduler;
const {
unstable_scheduleCallback: scheduleCallback,
unstable_NormalPriority: NormalPriority,
} = Scheduler;
startOfLatestFrame = 1000000000000;
currentTime = startOfLatestFrame - 10;
const callbackLog = [];
@ -318,10 +345,10 @@ describe('SchedulerDOM', () => {
const callbackC = jest.fn(() => callbackLog.push('C'));
const callbackD = jest.fn(() => callbackLog.push('D'));
scheduleCallback(callbackA, {timeout: 100}); // won't time out
scheduleCallback(callbackB, {timeout: 100}); // times out later
scheduleCallback(callbackC, {timeout: 2}); // will time out fast
scheduleCallback(callbackD, {timeout: 200}); // won't time out
scheduleCallback(NormalPriority, callbackA, {timeout: 100}); // won't time out
scheduleCallback(NormalPriority, callbackB, {timeout: 100}); // times out later
scheduleCallback(NormalPriority, callbackC, {timeout: 2}); // will time out fast
scheduleCallback(NormalPriority, callbackD, {timeout: 200}); // won't time out
advanceOneFrame({timeLeftInFrame: 15}); // runs rAF and postMessage callbacks
@ -355,9 +382,10 @@ describe('SchedulerDOM', () => {
const {
unstable_scheduleCallback: scheduleCallback,
unstable_cancelCallback: cancelCallback,
unstable_NormalPriority: NormalPriority,
} = Scheduler;
const cb = jest.fn();
const callbackId = scheduleCallback(cb);
const callbackId = scheduleCallback(NormalPriority, cb);
expect(cb).toHaveBeenCalledTimes(0);
cancelCallback(callbackId);
advanceOneFrame({timeLeftInFrame: 15});
@ -369,14 +397,15 @@ describe('SchedulerDOM', () => {
const {
unstable_scheduleCallback: scheduleCallback,
unstable_cancelCallback: cancelCallback,
unstable_NormalPriority: NormalPriority,
} = Scheduler;
const callbackLog = [];
const callbackA = jest.fn(() => callbackLog.push('A'));
const callbackB = jest.fn(() => callbackLog.push('B'));
const callbackC = jest.fn(() => callbackLog.push('C'));
scheduleCallback(callbackA);
const callbackId = scheduleCallback(callbackB);
scheduleCallback(callbackC);
scheduleCallback(NormalPriority, callbackA);
const callbackId = scheduleCallback(NormalPriority, callbackB);
scheduleCallback(NormalPriority, callbackC);
cancelCallback(callbackId);
cancelCallback(callbackId);
cancelCallback(callbackId);
@ -393,6 +422,7 @@ describe('SchedulerDOM', () => {
const {
unstable_scheduleCallback: scheduleCallback,
unstable_cancelCallback: cancelCallback,
unstable_NormalPriority: NormalPriority,
} = Scheduler;
const callbackLog = [];
let callbackBId;
@ -401,8 +431,8 @@ describe('SchedulerDOM', () => {
cancelCallback(callbackBId);
});
const callbackB = jest.fn(() => callbackLog.push('B'));
scheduleCallback(callbackA);
callbackBId = scheduleCallback(callbackB);
scheduleCallback(NormalPriority, callbackA);
callbackBId = scheduleCallback(NormalPriority, callbackB);
// Initially doesn't call anything
expect(callbackLog).toEqual([]);
advanceOneFrame({timeLeftInFrame: 15});
@ -430,7 +460,10 @@ describe('SchedulerDOM', () => {
*
*/
it('still calls all callbacks within same frame', () => {
const {unstable_scheduleCallback: scheduleCallback} = Scheduler;
const {
unstable_scheduleCallback: scheduleCallback,
unstable_NormalPriority: NormalPriority,
} = Scheduler;
const callbackLog = [];
const callbackA = jest.fn(() => callbackLog.push('A'));
const callbackB = jest.fn(() => {
@ -443,11 +476,11 @@ describe('SchedulerDOM', () => {
throw new Error('D error');
});
const callbackE = jest.fn(() => callbackLog.push('E'));
scheduleCallback(callbackA);
scheduleCallback(callbackB);
scheduleCallback(callbackC);
scheduleCallback(callbackD);
scheduleCallback(callbackE);
scheduleCallback(NormalPriority, callbackA);
scheduleCallback(NormalPriority, callbackB);
scheduleCallback(NormalPriority, callbackC);
scheduleCallback(NormalPriority, callbackD);
scheduleCallback(NormalPriority, callbackE);
// Initially doesn't call anything
expect(callbackLog).toEqual([]);
catchPostMessageErrors = true;
@ -476,7 +509,10 @@ describe('SchedulerDOM', () => {
*
*/
it('and with some timed out callbacks, still calls all callbacks within same frame', () => {
const {unstable_scheduleCallback: scheduleCallback} = Scheduler;
const {
unstable_scheduleCallback: scheduleCallback,
unstable_NormalPriority: NormalPriority,
} = Scheduler;
const callbackLog = [];
const callbackA = jest.fn(() => {
callbackLog.push('A');
@ -489,11 +525,11 @@ describe('SchedulerDOM', () => {
throw new Error('D error');
});
const callbackE = jest.fn(() => callbackLog.push('E'));
scheduleCallback(callbackA);
scheduleCallback(callbackB);
scheduleCallback(callbackC, {timeout: 2}); // times out fast
scheduleCallback(callbackD, {timeout: 2}); // times out fast
scheduleCallback(callbackE, {timeout: 2}); // times out fast
scheduleCallback(NormalPriority, callbackA);
scheduleCallback(NormalPriority, callbackB);
scheduleCallback(NormalPriority, callbackC, {timeout: 2}); // times out fast
scheduleCallback(NormalPriority, callbackD, {timeout: 2}); // times out fast
scheduleCallback(NormalPriority, callbackE, {timeout: 2}); // times out fast
// Initially doesn't call anything
expect(callbackLog).toEqual([]);
catchPostMessageErrors = true;
@ -522,7 +558,10 @@ describe('SchedulerDOM', () => {
*
*/
it('still calls all callbacks within same frame', () => {
const {unstable_scheduleCallback: scheduleCallback} = Scheduler;
const {
unstable_scheduleCallback: scheduleCallback,
unstable_NormalPriority: NormalPriority,
} = Scheduler;
const callbackLog = [];
const callbackA = jest.fn(() => {
callbackLog.push('A');
@ -544,11 +583,11 @@ describe('SchedulerDOM', () => {
callbackLog.push('E');
throw new Error('E error');
});
scheduleCallback(callbackA);
scheduleCallback(callbackB);
scheduleCallback(callbackC);
scheduleCallback(callbackD);
scheduleCallback(callbackE);
scheduleCallback(NormalPriority, callbackA);
scheduleCallback(NormalPriority, callbackB);
scheduleCallback(NormalPriority, callbackC);
scheduleCallback(NormalPriority, callbackD);
scheduleCallback(NormalPriority, callbackE);
// Initially doesn't call anything
expect(callbackLog).toEqual([]);
catchPostMessageErrors = true;
@ -583,7 +622,10 @@ describe('SchedulerDOM', () => {
*
*/
it('and with all timed out callbacks, still calls all callbacks within same frame', () => {
const {unstable_scheduleCallback: scheduleCallback} = Scheduler;
const {
unstable_scheduleCallback: scheduleCallback,
unstable_NormalPriority: NormalPriority,
} = Scheduler;
const callbackLog = [];
const callbackA = jest.fn(() => {
callbackLog.push('A');
@ -605,11 +647,11 @@ describe('SchedulerDOM', () => {
callbackLog.push('E');
throw new Error('E error');
});
scheduleCallback(callbackA, {timeout: 2}); // times out fast
scheduleCallback(callbackB, {timeout: 2}); // times out fast
scheduleCallback(callbackC, {timeout: 2}); // times out fast
scheduleCallback(callbackD, {timeout: 2}); // times out fast
scheduleCallback(callbackE, {timeout: 2}); // times out fast
scheduleCallback(NormalPriority, callbackA, {timeout: 2}); // times out fast
scheduleCallback(NormalPriority, callbackB, {timeout: 2}); // times out fast
scheduleCallback(NormalPriority, callbackC, {timeout: 2}); // times out fast
scheduleCallback(NormalPriority, callbackD, {timeout: 2}); // times out fast
scheduleCallback(NormalPriority, callbackE, {timeout: 2}); // times out fast
// Initially doesn't call anything
expect(callbackLog).toEqual([]);
catchPostMessageErrors = true;
@ -664,7 +706,10 @@ describe('SchedulerDOM', () => {
*
*/
it('still calls all callbacks within same frame', () => {
const {unstable_scheduleCallback: scheduleCallback} = Scheduler;
const {
unstable_scheduleCallback: scheduleCallback,
unstable_NormalPriority: NormalPriority,
} = Scheduler;
startOfLatestFrame = 1000000000000;
currentTime = startOfLatestFrame - 10;
catchPostMessageErrors = true;
@ -698,13 +743,13 @@ describe('SchedulerDOM', () => {
});
const callbackG = jest.fn(() => callbackLog.push('G'));
scheduleCallback(callbackA);
scheduleCallback(callbackB);
scheduleCallback(callbackC);
scheduleCallback(callbackD);
scheduleCallback(callbackE);
scheduleCallback(callbackF);
scheduleCallback(callbackG);
scheduleCallback(NormalPriority, callbackA);
scheduleCallback(NormalPriority, callbackB);
scheduleCallback(NormalPriority, callbackC);
scheduleCallback(NormalPriority, callbackD);
scheduleCallback(NormalPriority, callbackE);
scheduleCallback(NormalPriority, callbackF);
scheduleCallback(NormalPriority, callbackG);
// does nothing initially
expect(callbackLog).toEqual([]);

View File

@ -9,10 +9,11 @@
'use strict';
let Scheduler;
let scheduleCallback;
let runWithPriority;
let ImmediatePriority;
let UserBlockingPriority;
let NormalPriority;
describe('SchedulerNoDOM', () => {
// If Scheduler runs in a non-DOM environment, it falls back to a naive
@ -30,22 +31,22 @@ describe('SchedulerNoDOM', () => {
),
);
const Scheduler = require('scheduler');
Scheduler = require('scheduler');
scheduleCallback = Scheduler.unstable_scheduleCallback;
runWithPriority = Scheduler.unstable_runWithPriority;
ImmediatePriority = Scheduler.unstable_ImmediatePriority;
UserBlockingPriority = Scheduler.unstable_UserBlockingPriority;
NormalPriority = Scheduler.unstable_NormalPriority;
});
it('runAllTimers flushes all scheduled callbacks', () => {
let log = [];
scheduleCallback(() => {
scheduleCallback(NormalPriority, () => {
log.push('A');
});
scheduleCallback(() => {
scheduleCallback(NormalPriority, () => {
log.push('B');
});
scheduleCallback(() => {
scheduleCallback(NormalPriority, () => {
log.push('C');
});
expect(log).toEqual([]);
@ -56,59 +57,40 @@ describe('SchedulerNoDOM', () => {
it('executes callbacks in order of priority', () => {
let log = [];
scheduleCallback(() => {
scheduleCallback(NormalPriority, () => {
log.push('A');
});
scheduleCallback(() => {
scheduleCallback(NormalPriority, () => {
log.push('B');
});
runWithPriority(UserBlockingPriority, () => {
scheduleCallback(() => {
scheduleCallback(UserBlockingPriority, () => {
log.push('C');
});
scheduleCallback(() => {
scheduleCallback(UserBlockingPriority, () => {
log.push('D');
});
});
expect(log).toEqual([]);
jest.runAllTimers();
expect(log).toEqual(['C', 'D', 'A', 'B']);
});
it('calls immediate callbacks immediately', () => {
let log = [];
runWithPriority(ImmediatePriority, () => {
scheduleCallback(() => {
log.push('A');
scheduleCallback(() => {
log.push('B');
});
});
});
expect(log).toEqual(['A', 'B']);
});
it('handles errors', () => {
let log = [];
expect(() => {
runWithPriority(ImmediatePriority, () => {
scheduleCallback(() => {
scheduleCallback(ImmediatePriority, () => {
log.push('A');
throw new Error('Oops A');
});
scheduleCallback(() => {
scheduleCallback(ImmediatePriority, () => {
log.push('B');
});
scheduleCallback(() => {
scheduleCallback(ImmediatePriority, () => {
log.push('C');
throw new Error('Oops C');
});
});
}).toThrow('Oops A');
expect(() => jest.runAllTimers()).toThrow('Oops A');
expect(log).toEqual(['A']);

View File

@ -0,0 +1,30 @@
/**
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @flow strict
*/
export const enableUserTimingAPI = __DEV__;
export const debugRenderPhaseSideEffects = false;
export const debugRenderPhaseSideEffectsForStrictMode = __DEV__;
export const replayFailedUnitOfWorkWithInvokeGuardedCallback = __DEV__;
export const warnAboutDeprecatedLifecycles = true;
export const enableProfilerTimer = __PROFILE__;
export const enableSchedulerTracing = __PROFILE__;
export const enableSuspenseServerRenderer = false; // TODO: __DEV__? Here it might just be false.
export const enableSchedulerDebugging = false;
export function addUserTimingListener() {
throw new Error('Not implemented.');
}
export const disableJavaScriptURLs = false;
export const disableYielding = false;
export const disableInputAttributeSyncing = false;
export const enableStableConcurrentModeAPIs = false;
export const warnAboutShorthandPropertyCollision = false;
export const warnAboutDeprecatedSetNativeProps = false;
export const enableEventAPI = false;
export const enableNewScheduler = true;

View File

@ -0,0 +1,39 @@
/**
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @flow
*/
import typeof * as FeatureFlagsType from 'shared/ReactFeatureFlags';
import typeof * as FeatureFlagsShimType from './ReactFeatureFlags.www-new-scheduler';
export {
enableUserTimingAPI,
debugRenderPhaseSideEffects,
debugRenderPhaseSideEffectsForStrictMode,
replayFailedUnitOfWorkWithInvokeGuardedCallback,
warnAboutDeprecatedLifecycles,
enableProfilerTimer,
enableSchedulerTracing,
enableSuspenseServerRenderer,
enableSchedulerDebugging,
addUserTimingListener,
disableJavaScriptURLs,
disableYielding,
disableInputAttributeSyncing,
enableStableConcurrentModeAPIs,
warnAboutShorthandPropertyCollision,
warnAboutDeprecatedSetNativeProps,
enableEventAPI,
} from './ReactFeatureFlags.www';
export const enableNewScheduler = true;
// Flow magic to verify the exports of this file match the original version.
// eslint-disable-next-line no-unused-vars
type Check<_X, Y: _X, X: Y = _X> = null;
// eslint-disable-next-line no-unused-expressions
(null: Check<FeatureFlagsShimType, FeatureFlagsType>);

View File

@ -11,6 +11,7 @@ if [ $((0 % CIRCLE_NODE_TOTAL)) -eq "$CIRCLE_NODE_INDEX" ]; then
COMMANDS_TO_RUN+=('node ./scripts/tasks/flow-ci')
COMMANDS_TO_RUN+=('node ./scripts/tasks/eslint')
COMMANDS_TO_RUN+=('yarn test --maxWorkers=2')
COMMANDS_TO_RUN+=('yarn test-new-scheduler --maxWorkers=2')
COMMANDS_TO_RUN+=('yarn test-persistent --maxWorkers=2')
COMMANDS_TO_RUN+=('./scripts/circleci/check_license.sh')
COMMANDS_TO_RUN+=('./scripts/circleci/check_modules.sh')

View File

@ -48,6 +48,15 @@ function toFlushWithoutYielding(Scheduler) {
return toFlushAndYield(Scheduler, []);
}
function toFlushExpired(Scheduler, expectedYields) {
assertYieldsWereCleared(Scheduler);
Scheduler.unstable_flushExpired();
const actualYields = Scheduler.unstable_clearYields();
return captureAssertion(() => {
expect(actualYields).toEqual(expectedYields);
});
}
function toHaveYielded(Scheduler, expectedYields) {
return captureAssertion(() => {
const actualYields = Scheduler.unstable_clearYields();
@ -68,6 +77,7 @@ module.exports = {
toFlushAndYield,
toFlushAndYieldThrough,
toFlushWithoutYielding,
toFlushExpired,
toHaveYielded,
toFlushAndThrow,
};

View File

@ -110,6 +110,21 @@ const bundles = [
externals: ['react'],
},
/******* React DOM (new scheduler) *******/
{
bundleTypes: [
FB_WWW_DEV,
FB_WWW_PROD,
FB_WWW_PROFILING,
NODE_DEV,
NODE_PROD,
],
moduleType: RENDERER,
entry: 'react-dom/unstable-new-scheduler',
global: 'ReactDOMNewScheduler',
externals: ['react'],
},
/******* Test Utils *******/
{
moduleType: RENDERER_UTILS,

View File

@ -68,6 +68,12 @@ const forks = Object.freeze({
// We have a few forks for different environments.
'shared/ReactFeatureFlags': (bundleType, entry) => {
switch (entry) {
case 'react-dom/unstable-new-scheduler': {
if (entry === 'react-dom/unstable-new-scheduler') {
return 'shared/forks/ReactFeatureFlags.www-new-scheduler.js';
}
return null;
}
case 'react-native-renderer':
switch (bundleType) {
case RN_FB_DEV:

View File

@ -9,7 +9,11 @@
module.exports = [
{
shortName: 'dom',
entryPoints: ['react-dom', 'react-dom/unstable-fizz.node'],
entryPoints: [
'react-dom',
'react-dom/unstable-fizz.node',
'react-dom/unstable-new-scheduler',
],
isFlowTyped: true,
isFizzSupported: true,
},