From 4d5cb64aa2beacf982cf0e01628ddda6bd92014c Mon Sep 17 00:00:00 2001 From: Andrew Clark Date: Tue, 2 Apr 2019 15:49:07 -0700 Subject: [PATCH] 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. --- packages/react-cache/src/LRU.js | 7 +- .../ReactHooksInspectionIntegration-test.js | 4 + ...test.js => ReactDOMHooks-test.internal.js} | 37 +- .../ReactDOMSuspensePlaceholder-test.js | 8 + .../ReactServerRenderingHydration-test.js | 3 + packages/react-dom/unstable-new-scheduler.js | 16 + .../src/ReactFiberExpirationTime.js | 39 + .../src/ReactFiberReconciler.js | 2 - .../react-reconciler/src/ReactFiberRoot.js | 121 +- .../src/ReactFiberScheduler.js | 128 +- .../src/ReactFiberScheduler.new.js | 2218 ++++++++++++++++- .../src/ReactFiberScheduler.old.js | 29 +- .../src/ReactFiberUnwindWork.js | 23 +- .../src/SchedulerWithReactIntegration.js | 171 ++ .../src/__tests__/ReactHooks-test.internal.js | 3 + ...tIncrementalErrorHandling-test.internal.js | 79 +- .../ReactIncrementalPerf-test.internal.js | 976 ++++---- .../src/__tests__/ReactLazy-test.internal.js | 32 +- ...ReactSchedulerIntegration-test.internal.js | 156 ++ .../__tests__/ReactSuspense-test.internal.js | 271 +- .../ReactSuspensePlaceholder-test.internal.js | 30 +- ...tSuspenseWithNoopRenderer-test.internal.js | 214 +- ...ReactIncrementalPerf-test.internal.js.snap | 543 +++- .../src/__tests__/ReactTestRenderer-test.js | 28 +- .../__tests__/ReactProfiler-test.internal.js | 52 +- packages/scheduler/src/Scheduler.js | 141 +- .../scheduler/src/__tests__/Scheduler-test.js | 262 +- .../src/__tests__/SchedulerDOM-test.js | 191 +- .../src/__tests__/SchedulerNoDOM-test.js | 70 +- .../forks/ReactFeatureFlags.new-scheduler.js | 30 + .../ReactFeatureFlags.www-new-scheduler.js | 39 + scripts/circleci/test_entry_point.sh | 1 + .../jest/matchers/schedulerTestMatchers.js | 10 + scripts/rollup/bundles.js | 15 + scripts/rollup/forks.js | 6 + scripts/shared/inlinedHostConfigs.js | 6 +- 36 files changed, 4754 insertions(+), 1207 deletions(-) rename packages/react-dom/src/__tests__/{ReactDOMHooks-test.js => ReactDOMHooks-test.internal.js} (79%) create mode 100644 packages/react-dom/unstable-new-scheduler.js create mode 100644 packages/react-reconciler/src/SchedulerWithReactIntegration.js create mode 100644 packages/react-reconciler/src/__tests__/ReactSchedulerIntegration-test.internal.js create mode 100644 packages/shared/forks/ReactFeatureFlags.new-scheduler.js create mode 100644 packages/shared/forks/ReactFeatureFlags.www-new-scheduler.js diff --git a/packages/react-cache/src/LRU.js b/packages/react-cache/src/LRU.js index c12a0f6f4bea2..0b150dee7c27b 100644 --- a/packages/react-cache/src/LRU.js +++ b/packages/react-cache/src/LRU.js @@ -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 = {| value: T, @@ -34,7 +37,7 @@ export function createLRU(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); } } diff --git a/packages/react-debug-tools/src/__tests__/ReactHooksInspectionIntegration-test.js b/packages/react-debug-tools/src/__tests__/ReactHooksInspectionIntegration-test.js index 8faffa48e5f34..5c092f5853288 100644 --- a/packages/react-debug-tools/src/__tests__/ReactHooksInspectionIntegration-test.js +++ b/packages/react-debug-tools/src/__tests__/ReactHooksInspectionIntegration-test.js @@ -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([ diff --git a/packages/react-dom/src/__tests__/ReactDOMHooks-test.js b/packages/react-dom/src/__tests__/ReactDOMHooks-test.internal.js similarity index 79% rename from packages/react-dom/src/__tests__/ReactDOMHooks-test.js rename to packages/react-dom/src/__tests__/ReactDOMHooks-test.internal.js index 5dff31ad17a60..7d58d22f41bac 100644 --- a/packages/react-dom/src/__tests__/ReactDOMHooks-test.js +++ b/packages/react-dom/src/__tests__/ReactDOMHooks-test.internal.js @@ -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,15 +101,30 @@ describe('ReactDOMHooks', () => { } ReactDOM.render(, container); - ReactDOM.unstable_batchedUpdates(() => { - _set(0); // Forces the effect to be flushed - expect(otherContainer.textContent).toBe(''); - ReactDOM.render(, otherContainer); - expect(otherContainer.textContent).toBe(''); - }); - expect(otherContainer.textContent).toBe('B'); - expect(calledA).toBe(false); // It was in a batch - expect(calledB).toBe(true); + + 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(, 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(''); + ReactDOM.render(, otherContainer); + expect(otherContainer.textContent).toBe(''); + }); + 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', () => { diff --git a/packages/react-dom/src/__tests__/ReactDOMSuspensePlaceholder-test.js b/packages/react-dom/src/__tests__/ReactDOMSuspensePlaceholder-test.js index 7a5df7629e088..de43fd44274c3 100644 --- a/packages/react-dom/src/__tests__/ReactDOMSuspensePlaceholder-test.js +++ b/packages/react-dom/src/__tests__/ReactDOMSuspensePlaceholder-test.js @@ -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( 'SiblingAsync', ); diff --git a/packages/react-dom/src/__tests__/ReactServerRenderingHydration-test.js b/packages/react-dom/src/__tests__/ReactServerRenderingHydration-test.js index e28183e1b3231..4d046af383cea 100644 --- a/packages/react-dom/src/__tests__/ReactServerRenderingHydration-test.js +++ b/packages/react-dom/src/__tests__/ReactServerRenderingHydration-test.js @@ -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'); }); }); diff --git a/packages/react-dom/unstable-new-scheduler.js b/packages/react-dom/unstable-new-scheduler.js new file mode 100644 index 0000000000000..2a016ba16e9db --- /dev/null +++ b/packages/react-dom/unstable-new-scheduler.js @@ -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; diff --git a/packages/react-reconciler/src/ReactFiberExpirationTime.js b/packages/react-reconciler/src/ReactFiberExpirationTime.js index a29dfbdf17b25..f75ca42a74a57 100644 --- a/packages/react-reconciler/src/ReactFiberExpirationTime.js +++ b/packages/react-reconciler/src/ReactFiberExpirationTime.js @@ -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; +} diff --git a/packages/react-reconciler/src/ReactFiberReconciler.js b/packages/react-reconciler/src/ReactFiberReconciler.js index eb98b19ce4f0e..b5e1182376523 100644 --- a/packages/react-reconciler/src/ReactFiberReconciler.js +++ b/packages/react-reconciler/src/ReactFiberReconciler.js @@ -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, diff --git a/packages/react-reconciler/src/ReactFiberRoot.js b/packages/react-reconciler/src/ReactFiberRoot.js index e3e445b46ef7f..2b085604ce808 100644 --- a/packages/react-reconciler/src/ReactFiberRoot.js +++ b/packages/react-reconciler/src/ReactFiberRoot.js @@ -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; } diff --git a/packages/react-reconciler/src/ReactFiberScheduler.js b/packages/react-reconciler/src/ReactFiberScheduler.js index df179b766e155..fb94b012ef904 100644 --- a/packages/react-reconciler/src/ReactFiberScheduler.js +++ b/packages/react-reconciler/src/ReactFiberScheduler.js @@ -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, diff --git a/packages/react-reconciler/src/ReactFiberScheduler.new.js b/packages/react-reconciler/src/ReactFiberScheduler.new.js index 810cbd8847d8d..e2b4517de23fe 100644 --- a/packages/react-reconciler/src/ReactFiberScheduler.new.js +++ b/packages/react-reconciler/src/ReactFiberScheduler.new.js @@ -3,34 +3,2194 @@ * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. + * + * @flow */ -function notYetImplemented() { - throw new Error('Not yet implemented.'); -} - -export const requestCurrentTime = notYetImplemented; -export const computeExpirationForFiber = notYetImplemented; -export const captureCommitPhaseError = notYetImplemented; -export const onUncaughtError = notYetImplemented; -export const renderDidSuspend = notYetImplemented; -export const renderDidError = notYetImplemented; -export const pingSuspendedRoot = notYetImplemented; -export const retryTimedOutBoundary = notYetImplemented; -export const resolveRetryThenable = notYetImplemented; -export const markLegacyErrorBoundaryAsFailed = notYetImplemented; -export const isAlreadyFailedLegacyErrorBoundary = notYetImplemented; -export const scheduleWork = notYetImplemented; -export const requestWork = notYetImplemented; -export const flushRoot = notYetImplemented; -export const batchedUpdates = notYetImplemented; -export const unbatchedUpdates = notYetImplemented; -export const flushSync = notYetImplemented; -export const flushControlled = notYetImplemented; -export const deferredUpdates = notYetImplemented; -export const syncUpdates = notYetImplemented; -export const interactiveUpdates = notYetImplemented; -export const flushInteractiveUpdates = notYetImplemented; -export const computeUniqueAsyncExpiration = notYetImplemented; -export const flushPassiveEffects = notYetImplemented; -export const warnIfNotCurrentlyActingUpdatesInDev = notYetImplemented; +import type {Fiber} from './ReactFiber'; +import type {FiberRoot} from './ReactFiberRoot'; +import type {ExpirationTime} from './ReactFiberExpirationTime'; +import type { + ReactPriorityLevel, + SchedulerCallback, +} from './SchedulerWithReactIntegration'; +import type {Interaction} from 'scheduler/src/Tracing'; + +import { + warnAboutDeprecatedLifecycles, + enableUserTimingAPI, + enableSuspenseServerRenderer, + replayFailedUnitOfWorkWithInvokeGuardedCallback, + enableProfilerTimer, + disableYielding, + enableSchedulerTracing, +} from 'shared/ReactFeatureFlags'; +import ReactSharedInternals from 'shared/ReactSharedInternals'; +import invariant from 'shared/invariant'; +import warning from 'shared/warning'; + +import { + scheduleCallback, + cancelCallback, + getCurrentPriorityLevel, + runWithPriority, + shouldYield, + now, + ImmediatePriority, + UserBlockingPriority, + NormalPriority, + LowPriority, + IdlePriority, + flushImmediateQueue, +} from './SchedulerWithReactIntegration'; + +import {__interactionsRef, __subscriberRef} from 'scheduler/tracing'; + +import { + prepareForCommit, + resetAfterCommit, + scheduleTimeout, + cancelTimeout, + noTimeout, +} from './ReactFiberHostConfig'; + +import {createWorkInProgress, assignFiberPropertiesInDEV} from './ReactFiber'; +import {NoContext, ConcurrentMode, ProfileMode} from './ReactTypeOfMode'; +import { + HostRoot, + ClassComponent, + SuspenseComponent, + DehydratedSuspenseComponent, + FunctionComponent, + ForwardRef, + MemoComponent, + SimpleMemoComponent, +} from 'shared/ReactWorkTags'; +import { + NoEffect, + PerformedWork, + Placement, + Update, + PlacementAndUpdate, + Deletion, + Ref, + ContentReset, + Snapshot, + Callback, + Passive, + Incomplete, + HostEffectMask, +} from 'shared/ReactSideEffectTags'; +import { + NoWork, + Sync, + Never, + msToExpirationTime, + expirationTimeToMs, + computeInteractiveExpiration, + computeAsyncExpiration, + inferPriorityFromExpirationTime, + LOW_PRIORITY_EXPIRATION, +} from './ReactFiberExpirationTime'; +import {beginWork as originalBeginWork} from './ReactFiberBeginWork'; +import {completeWork} from './ReactFiberCompleteWork'; +import { + throwException, + unwindWork, + unwindInterruptedWork, + createRootErrorUpdate, + createClassErrorUpdate, +} from './ReactFiberUnwindWork'; +import { + commitBeforeMutationLifeCycles as commitBeforeMutationEffectOnFiber, + commitLifeCycles as commitLayoutEffectOnFiber, + commitPassiveHookEffects, + commitPlacement, + commitWork, + commitDeletion, + commitDetachRef, + commitAttachRef, + commitResetTextContent, +} from './ReactFiberCommitWork'; +import {enqueueUpdate} from './ReactUpdateQueue'; +// TODO: Ahaha Andrew is bad at spellling +import {resetContextDependences as resetContextDependencies} from './ReactFiberNewContext'; +import {resetHooks, ContextOnlyDispatcher} from './ReactFiberHooks'; +import {createCapturedValue} from './ReactCapturedValue'; + +import { + recordCommitTime, + startProfilerTimer, + stopProfilerTimerIfRunningAndRecordDelta, +} from './ReactProfilerTimer'; + +// DEV stuff +import warningWithoutStack from 'shared/warningWithoutStack'; +import getComponentName from 'shared/getComponentName'; +import ReactStrictModeWarnings from './ReactStrictModeWarnings'; +import { + phase as ReactCurrentDebugFiberPhaseInDEV, + resetCurrentFiber as resetCurrentDebugFiberInDEV, + setCurrentFiber as setCurrentDebugFiberInDEV, + getStackByFiberInDevAndProd, +} from './ReactCurrentFiber'; +import { + recordEffect, + recordScheduleUpdate, + startRequestCallbackTimer, + stopRequestCallbackTimer, + startWorkTimer, + stopWorkTimer, + stopFailedWorkTimer, + startWorkLoopTimer, + stopWorkLoopTimer, + startCommitTimer, + stopCommitTimer, + startCommitSnapshotEffectsTimer, + stopCommitSnapshotEffectsTimer, + startCommitHostEffectsTimer, + stopCommitHostEffectsTimer, + startCommitLifeCyclesTimer, + stopCommitLifeCyclesTimer, +} from './ReactDebugFiberPerf'; +import { + invokeGuardedCallback, + hasCaughtError, + clearCaughtError, +} from 'shared/ReactErrorUtils'; +import {onCommitRoot} from './ReactFiberDevToolsHook'; + +const { + ReactCurrentDispatcher, + ReactCurrentOwner, + ReactShouldWarnActingUpdates, +} = ReactSharedInternals; + +type WorkPhase = 0 | 1 | 2 | 3 | 4 | 5; +const NotWorking = 0; +const BatchedPhase = 1; +const LegacyUnbatchedPhase = 2; +const FlushSyncPhase = 3; +const RenderPhase = 4; +const CommitPhase = 5; + +type RootExitStatus = 0 | 1 | 2 | 3; +const RootIncomplete = 0; +const RootErrored = 1; +const RootSuspended = 2; +const RootCompleted = 3; + +export type Thenable = { + then(resolve: () => mixed, reject?: () => mixed): Thenable | void, +}; + +// The phase of work we're currently in +let workPhase: WorkPhase = NotWorking; +// The root we're working on +let workInProgressRoot: FiberRoot | null = null; +// The fiber we're working on +let workInProgress: Fiber | null = null; +// The expiration time we're rendering +let renderExpirationTime: ExpirationTime = NoWork; +// Whether to root completed, errored, suspended, etc. +let workInProgressRootExitStatus: RootExitStatus = RootIncomplete; +let workInProgressRootAbsoluteTimeoutMs: number = -1; + +let nextEffect: Fiber | null = null; +let hasUncaughtError = false; +let firstUncaughtError = null; +let legacyErrorBoundariesThatAlreadyFailed: Set | null = null; + +let rootDoesHavePassiveEffects: boolean = false; +let rootWithPendingPassiveEffects: FiberRoot | null = null; +let pendingPassiveEffectsExpirationTime: ExpirationTime = NoWork; + +let rootsWithPendingDiscreteUpdates: Map< + FiberRoot, + ExpirationTime, +> | null = null; + +// Use these to prevent an infinite loop of nested updates +const NESTED_UPDATE_LIMIT = 50; +let nestedUpdateCount: number = 0; +let rootWithNestedUpdates: FiberRoot | null = null; + +const NESTED_PASSIVE_UPDATE_LIMIT = 50; +let nestedPassiveUpdateCount: number = 0; + +let interruptedBy: Fiber | null = null; + +// Expiration times are computed by adding to the current time (the start +// time). However, if two updates are scheduled within the same event, we +// should treat their start times as simultaneous, even if the actual clock +// time has advanced between the first and second call. + +// In other words, because expiration times determine how updates are batched, +// we want all updates of like priority that occur within the same event to +// receive the same expiration time. Otherwise we get tearing. +let currentEventTime: ExpirationTime = NoWork; + +export function requestCurrentTime() { + if (workPhase === RenderPhase || workPhase === CommitPhase) { + // We're inside React, so it's fine to read the actual time. + return msToExpirationTime(now()); + } + // We're not inside React, so we may be in the middle of a browser event. + if (currentEventTime !== NoWork) { + // Use the same start time for all updates until we enter React again. + return currentEventTime; + } + // This is the first update since React yielded. Compute a new start time. + currentEventTime = msToExpirationTime(now()); + return currentEventTime; +} + +export function computeExpirationForFiber( + currentTime: ExpirationTime, + fiber: Fiber, +): ExpirationTime { + if ((fiber.mode & ConcurrentMode) === NoContext) { + return Sync; + } + + if (workPhase === RenderPhase) { + // Use whatever time we're already rendering + return renderExpirationTime; + } + + // Compute an expiration time based on the Scheduler priority. + let expirationTime; + const priorityLevel = getCurrentPriorityLevel(); + switch (priorityLevel) { + case ImmediatePriority: + expirationTime = Sync; + break; + case UserBlockingPriority: + // TODO: Rename this to computeUserBlockingExpiration + expirationTime = computeInteractiveExpiration(currentTime); + break; + case NormalPriority: + case LowPriority: // TODO: Handle LowPriority + // TODO: Rename this to... something better. + expirationTime = computeAsyncExpiration(currentTime); + break; + case IdlePriority: + expirationTime = Never; + break; + default: + invariant(false, 'Expected a valid priority level'); + } + + // If we're in the middle of rendering a tree, do not update at the same + // expiration time that is already rendering. + if (workInProgressRoot !== null && expirationTime === renderExpirationTime) { + // This is a trick to move this update into a separate batch + expirationTime -= 1; + } + + return expirationTime; +} + +let lastUniqueAsyncExpiration = NoWork; +export function computeUniqueAsyncExpiration(): ExpirationTime { + const currentTime = requestCurrentTime(); + let result = computeAsyncExpiration(currentTime); + if (result <= lastUniqueAsyncExpiration) { + // Since we assume the current time monotonically increases, we only hit + // this branch when computeUniqueAsyncExpiration is fired multiple times + // within a 200ms window (or whatever the async bucket size is). + result -= 1; + } + lastUniqueAsyncExpiration = result; + return result; +} + +export function scheduleUpdateOnFiber( + fiber: Fiber, + expirationTime: ExpirationTime, +) { + checkForNestedUpdates(); + warnAboutInvalidUpdatesOnClassComponentsInDEV(fiber); + + const root = markUpdateTimeFromFiberToRoot(fiber, expirationTime); + if (root === null) { + warnAboutUpdateOnUnmountedFiberInDEV(fiber); + return; + } + + root.pingTime = NoWork; + + checkForInterruption(fiber, expirationTime); + recordScheduleUpdate(); + + if (expirationTime === Sync) { + if (workPhase === LegacyUnbatchedPhase) { + // This is a legacy edge case. The initial mount of a ReactDOM.render-ed + // root inside of batchedUpdates should be synchronous, but layout updates + // should be deferred until the end of the batch. + let callback = renderRoot(root, Sync, true); + while (callback !== null) { + callback = callback(true); + } + } else { + scheduleCallbackForRoot(root, ImmediatePriority, Sync); + if (workPhase === NotWorking) { + // Flush the synchronous work now, wnless we're already working or inside + // a batch. This is intentionally inside scheduleUpdateOnFiber instead of + // scheduleCallbackForFiber to preserve the ability to schedule a callback + // without immediately flushing it. We only do this for user-initated + // updates, to preserve historical behavior of sync mode. + flushImmediateQueue(); + } + } + } else { + // TODO: computeExpirationForFiber also reads the priority. Pass the + // priority as an argument to that function and this one. + const priorityLevel = getCurrentPriorityLevel(); + if (priorityLevel === UserBlockingPriority) { + // This is the result of a discrete event. Track the lowest priority + // discrete update per root so we can flush them early, if needed. + if (rootsWithPendingDiscreteUpdates === null) { + rootsWithPendingDiscreteUpdates = new Map([[root, expirationTime]]); + } else { + const lastDiscreteTime = rootsWithPendingDiscreteUpdates.get(root); + if ( + lastDiscreteTime === undefined || + lastDiscreteTime > expirationTime + ) { + rootsWithPendingDiscreteUpdates.set(root, expirationTime); + } + } + } + scheduleCallbackForRoot(root, priorityLevel, expirationTime); + } +} +export const scheduleWork = scheduleUpdateOnFiber; + +// This is split into a separate function so we can mark a fiber with pending +// work without treating it as a typical update that originates from an event; +// e.g. retrying a Suspense boundary isn't an update, but it does schedule work +// on a fiber. +function markUpdateTimeFromFiberToRoot(fiber, expirationTime) { + // Update the source fiber's expiration time + if (fiber.expirationTime < expirationTime) { + fiber.expirationTime = expirationTime; + } + let alternate = fiber.alternate; + if (alternate !== null && alternate.expirationTime < expirationTime) { + alternate.expirationTime = expirationTime; + } + // Walk the parent path to the root and update the child expiration time. + let node = fiber.return; + let root = null; + if (node === null && fiber.tag === HostRoot) { + root = fiber.stateNode; + } else { + while (node !== null) { + alternate = node.alternate; + if (node.childExpirationTime < expirationTime) { + node.childExpirationTime = expirationTime; + if ( + alternate !== null && + alternate.childExpirationTime < expirationTime + ) { + alternate.childExpirationTime = expirationTime; + } + } else if ( + alternate !== null && + alternate.childExpirationTime < expirationTime + ) { + alternate.childExpirationTime = expirationTime; + } + if (node.return === null && node.tag === HostRoot) { + root = node.stateNode; + break; + } + node = node.return; + } + } + + if (root !== null) { + // Update the first and last pending expiration times in this root + const firstPendingTime = root.firstPendingTime; + if (expirationTime > firstPendingTime) { + root.firstPendingTime = expirationTime; + } + const lastPendingTime = root.lastPendingTime; + if (lastPendingTime === NoWork || expirationTime < lastPendingTime) { + root.lastPendingTime = expirationTime; + } + } + + return root; +} + +// Use this function, along with runRootCallback, to ensure that only a single +// callback per root is scheduled. It's still possible to call renderRoot +// directly, but scheduling via this function helps avoid excessive callbacks. +// It works by storing the callback node and expiration time on the root. When a +// new callback comes in, it compares the expiration time to determine if it +// should cancel the previous one. It also relies on commitRoot scheduling a +// callback to render the next level, because that means we don't need a +// separate callback per expiration time. +function scheduleCallbackForRoot( + root: FiberRoot, + priorityLevel: ReactPriorityLevel, + expirationTime: ExpirationTime, +) { + const existingCallbackExpirationTime = root.callbackExpirationTime; + if (existingCallbackExpirationTime < expirationTime) { + // New callback has higher priority than the existing one. + const existingCallbackNode = root.callbackNode; + if (existingCallbackNode !== null) { + cancelCallback(existingCallbackNode); + } + root.callbackExpirationTime = expirationTime; + const options = + expirationTime === Sync + ? null + : {timeout: expirationTimeToMs(expirationTime)}; + root.callbackNode = scheduleCallback( + priorityLevel, + runRootCallback.bind( + null, + root, + renderRoot.bind(null, root, expirationTime), + ), + options, + ); + if ( + enableUserTimingAPI && + expirationTime !== Sync && + workPhase !== RenderPhase && + workPhase !== CommitPhase + ) { + // Scheduled an async callback, and we're not already working. Add an + // entry to the flamegraph that shows we're waiting for a callback + // to fire. + startRequestCallbackTimer(); + } + } + + const timeoutHandle = root.timeoutHandle; + if (timeoutHandle !== noTimeout) { + // The root previous suspended and scheduled a timeout to commit a fallback + // state. Now that we have additional work, cancel the timeout. + root.timeoutHandle = noTimeout; + // $FlowFixMe Complains noTimeout is not a TimeoutID, despite the check above + cancelTimeout(timeoutHandle); + } + + // Add the current set of interactions to the pending set associated with + // this root. + schedulePendingInteraction(root, expirationTime); +} + +function runRootCallback(root, callback, isSync) { + const prevCallbackNode = root.callbackNode; + let continuation = null; + try { + continuation = callback(isSync); + if (continuation !== null) { + return runRootCallback.bind(null, root, continuation); + } else { + return null; + } + } finally { + // If the callback exits without returning a continuation, remove the + // corresponding callback node from the root. Unless the callback node + // has changed, which implies that it was already cancelled by a high + // priority update. + if (continuation === null && prevCallbackNode === root.callbackNode) { + root.callbackNode = null; + root.callbackExpirationTime = NoWork; + } + } +} + +export function flushRoot(root: FiberRoot, expirationTime: ExpirationTime) { + if (workPhase === RenderPhase || workPhase === CommitPhase) { + invariant( + false, + 'work.commit(): Cannot commit while already rendering. This likely ' + + 'means you attempted to commit from inside a lifecycle method.', + ); + } + scheduleCallback( + ImmediatePriority, + renderRoot.bind(null, root, expirationTime), + ); + flushImmediateQueue(); +} + +export function flushInteractiveUpdates() { + if (workPhase === RenderPhase || workPhase === CommitPhase) { + // Can't synchronously flush interactive updates if React is already + // working. This is currently a no-op. + // TODO: Should we fire a warning? This happens if you synchronously invoke + // an input event inside an effect, like with `element.click()`. + return; + } + flushPendingDiscreteUpdates(); +} + +function resolveLocksOnRoot(root: FiberRoot, expirationTime: ExpirationTime) { + const firstBatch = root.firstBatch; + if ( + firstBatch !== null && + firstBatch._defer && + firstBatch._expirationTime >= expirationTime + ) { + root.finishedWork = root.current.alternate; + root.pendingCommitExpirationTime = expirationTime; + scheduleCallback(NormalPriority, () => { + firstBatch._onComplete(); + return null; + }); + return true; + } else { + return false; + } +} + +export function deferredUpdates(fn: () => A): A { + // TODO: Remove in favor of Scheduler.next + return runWithPriority(NormalPriority, fn); +} + +export function interactiveUpdates( + fn: (A, B, C) => R, + a: A, + b: B, + c: C, +): R { + if (workPhase === NotWorking) { + // TODO: Remove this call. Instead of doing this automatically, the caller + // should explicitly call flushInteractiveUpdates. + flushPendingDiscreteUpdates(); + } + return runWithPriority(UserBlockingPriority, fn.bind(null, a, b, c)); +} + +export function syncUpdates( + fn: (A, B, C) => R, + a: A, + b: B, + c: C, +): R { + return runWithPriority(ImmediatePriority, fn.bind(null, a, b, c)); +} + +function flushPendingDiscreteUpdates() { + if (rootsWithPendingDiscreteUpdates !== null) { + // For each root with pending discrete updates, schedule a callback to + // immediately flush them. + const roots = rootsWithPendingDiscreteUpdates; + rootsWithPendingDiscreteUpdates = null; + roots.forEach((expirationTime, root) => { + scheduleCallback( + ImmediatePriority, + renderRoot.bind(null, root, expirationTime), + ); + }); + // Now flush the immediate queue. + flushImmediateQueue(); + } +} + +export function batchedUpdates(fn: A => R, a: A): R { + if (workPhase !== NotWorking) { + // We're already working, or inside a batch, so batchedUpdates is a no-op. + return fn(a); + } + workPhase = BatchedPhase; + try { + return fn(a); + } finally { + workPhase = NotWorking; + // Flush the immediate callbacks that were scheduled during this batch + flushImmediateQueue(); + } +} + +export function unbatchedUpdates(fn: (a: A) => R, a: A): R { + if (workPhase !== BatchedPhase && workPhase !== FlushSyncPhase) { + // We're not inside batchedUpdates or flushSync, so unbatchedUpdates is + // a no-op. + return fn(a); + } + const prevWorkPhase = workPhase; + workPhase = LegacyUnbatchedPhase; + try { + return fn(a); + } finally { + workPhase = prevWorkPhase; + } +} + +export function flushSync(fn: A => R, a: A): R { + if (workPhase === RenderPhase || workPhase === CommitPhase) { + invariant( + false, + 'flushSync was called from inside a lifecycle method. It cannot be ' + + 'called when React is already rendering.', + ); + } + const prevWorkPhase = workPhase; + workPhase = FlushSyncPhase; + try { + return runWithPriority(ImmediatePriority, fn.bind(null, a)); + } finally { + workPhase = prevWorkPhase; + // Flush the immediate callbacks that were scheduled during this batch. + // Note that this will happen even if batchedUpdates is higher up + // the stack. + flushImmediateQueue(); + } +} + +export function flushControlled(fn: () => mixed): void { + const prevWorkPhase = workPhase; + workPhase = BatchedPhase; + try { + runWithPriority(ImmediatePriority, fn); + } finally { + workPhase = prevWorkPhase; + if (workPhase === NotWorking) { + // Flush the immediate callbacks that were scheduled during this batch + flushImmediateQueue(); + } + } +} + +function prepareFreshStack(root, expirationTime) { + root.pendingCommitExpirationTime = NoWork; + + if (workInProgress !== null) { + let interruptedWork = workInProgress.return; + while (interruptedWork !== null) { + unwindInterruptedWork(interruptedWork); + interruptedWork = interruptedWork.return; + } + } + workInProgressRoot = root; + workInProgress = createWorkInProgress(root.current, null, expirationTime); + renderExpirationTime = expirationTime; + workInProgressRootExitStatus = RootIncomplete; + workInProgressRootAbsoluteTimeoutMs = -1; + + if (__DEV__) { + ReactStrictModeWarnings.discardPendingWarnings(); + } +} + +function renderRoot( + root: FiberRoot, + expirationTime: ExpirationTime, + isSync: boolean, +): SchedulerCallback | null { + invariant( + workPhase !== RenderPhase && workPhase !== CommitPhase, + 'Should not already be working.', + ); + + if (enableUserTimingAPI && expirationTime !== Sync) { + const didExpire = isSync; + const timeoutMs = expirationTimeToMs(expirationTime); + stopRequestCallbackTimer(didExpire, timeoutMs); + } + + if (root.firstPendingTime < expirationTime) { + // If there's no work left at this expiration time, exit immediately. This + // happens when multiple callbacks are scheduled for a single root, but an + // earlier callback flushes the work of a later one. + return null; + } + + if (root.pendingCommitExpirationTime === expirationTime) { + // There's already a pending commit at this expiration time. + root.pendingCommitExpirationTime = NoWork; + return commitRoot.bind(null, root, expirationTime); + } + + flushPassiveEffects(); + + // If the root or expiration time have changed, throw out the existing stack + // and prepare a fresh one. Otherwise we'll continue where we left off. + if (root !== workInProgressRoot || expirationTime !== renderExpirationTime) { + prepareFreshStack(root, expirationTime); + startWorkOnPendingInteraction(root, expirationTime); + } + + // If we have a work-in-progress fiber, it means there's still work to do + // in this root. + if (workInProgress !== null) { + const prevWorkPhase = workPhase; + workPhase = RenderPhase; + let prevDispatcher = ReactCurrentDispatcher.current; + if (prevDispatcher === null) { + // The React isomorphic package does not include a default dispatcher. + // Instead the first renderer will lazily attach one, in order to give + // nicer error messages. + prevDispatcher = ContextOnlyDispatcher; + } + ReactCurrentDispatcher.current = ContextOnlyDispatcher; + let prevInteractions: Set | null = null; + if (enableSchedulerTracing) { + prevInteractions = __interactionsRef.current; + __interactionsRef.current = root.memoizedInteractions; + } + + startWorkLoopTimer(workInProgress); + do { + try { + if (isSync) { + if (expirationTime !== Sync) { + // An async update expired. There may be other expired updates on + // this root. We should render all the expired work in a + // single batch. + const currentTime = requestCurrentTime(); + if (currentTime < expirationTime) { + // Restart at the current time. + workPhase = prevWorkPhase; + ReactCurrentDispatcher.current = prevDispatcher; + return renderRoot.bind(null, root, currentTime); + } + } + workLoopSync(); + } else { + // Since we know we're in a React event, we can clear the current + // event time. The next update will compute a new event time. + currentEventTime = NoWork; + workLoop(); + } + break; + } catch (thrownValue) { + // Reset module-level state that was set during the render phase. + resetContextDependencies(); + resetHooks(); + + const sourceFiber = workInProgress; + if (sourceFiber === null || sourceFiber.return === null) { + // Expected to be working on a non-root fiber. This is a fatal error + // because there's no ancestor that can handle it; the root is + // supposed to capture all errors that weren't caught by an error + // boundary. + prepareFreshStack(root, expirationTime); + workPhase = prevWorkPhase; + throw thrownValue; + } + + if (enableProfilerTimer && sourceFiber.mode & ProfileMode) { + // Record the time spent rendering before an error was thrown. This + // avoids inaccurate Profiler durations in the case of a + // suspended render. + stopProfilerTimerIfRunningAndRecordDelta(sourceFiber, true); + } + + const returnFiber = sourceFiber.return; + throwException( + root, + returnFiber, + sourceFiber, + thrownValue, + renderExpirationTime, + ); + workInProgress = completeUnitOfWork(sourceFiber); + } + } while (true); + + workPhase = prevWorkPhase; + resetContextDependencies(); + ReactCurrentDispatcher.current = prevDispatcher; + if (enableSchedulerTracing) { + __interactionsRef.current = ((prevInteractions: any): Set); + } + + if (workInProgress !== null) { + // There's still work left over. Return a continuation. + stopInterruptedWorkLoopTimer(); + if (expirationTime !== Sync) { + startRequestCallbackTimer(); + } + return renderRoot.bind(null, root, expirationTime); + } + } + + // We now have a consistent tree. The next step is either to commit it, or, if + // something suspended, wait to commit it after a timeout. + stopFinishedWorkLoopTimer(); + + const isLocked = resolveLocksOnRoot(root, expirationTime); + if (isLocked) { + // This root has a lock that prevents it from committing. Exit. If we begin + // work on the root again, without any intervening updates, it will finish + // without doing additional work. + return null; + } + + // Set this to null to indicate there's no in-progress render. + workInProgressRoot = null; + + switch (workInProgressRootExitStatus) { + case RootIncomplete: { + invariant(false, 'Should have a work-in-progress.'); + } + // Flow knows about invariant, so it compains if I add a break statement, + // but eslint doesn't know about invariant, so it complains if I do. + // eslint-disable-next-line no-fallthrough + case RootErrored: { + // An error was thrown. First check if there is lower priority work + // scheduled on this root. + const lastPendingTime = root.lastPendingTime; + if (root.lastPendingTime < expirationTime) { + // There's lower priority work. Before raising the error, try rendering + // at the lower priority to see if it fixes it. Use a continuation to + // maintain the existing priority and position in the queue. + return renderRoot.bind(null, root, lastPendingTime); + } + if (!isSync) { + // If we're rendering asynchronously, it's possible the error was + // caused by tearing due to a mutation during an event. Try rendering + // one more time without yiedling to events. + prepareFreshStack(root, expirationTime); + scheduleCallback( + ImmediatePriority, + renderRoot.bind(null, root, expirationTime), + ); + return null; + } + // If we're already rendering synchronously, commit the root in its + // errored state. + return commitRoot.bind(null, root, expirationTime); + } + case RootSuspended: { + const lastPendingTime = root.lastPendingTime; + if (root.lastPendingTime < expirationTime) { + // There's lower priority work. It might be unsuspended. Try rendering + // at that level. + return renderRoot.bind(null, root, lastPendingTime); + } + if (!isSync) { + const msUntilTimeout = computeMsUntilTimeout( + root, + workInProgressRootAbsoluteTimeoutMs, + ); + if (msUntilTimeout > 0) { + // The render is suspended, it hasn't timed out, and there's no lower + // priority work to do. Instead of committing the fallback + // immediately, wait for more data to arrive. + root.timeoutHandle = scheduleTimeout( + commitRoot.bind(null, root, expirationTime), + msUntilTimeout, + ); + return null; + } + } + // The work expired. Commit immediately. + return commitRoot.bind(null, root, expirationTime); + } + case RootCompleted: { + // The work completed. Ready to commit. + return commitRoot.bind(null, root, expirationTime); + } + default: { + invariant(false, 'Unknown root exit status.'); + } + } +} + +export function renderDidSuspend( + root: FiberRoot, + absoluteTimeoutMs: number, + // TODO: Don't need this argument anymore + suspendedTime: ExpirationTime, +) { + if ( + absoluteTimeoutMs >= 0 && + workInProgressRootAbsoluteTimeoutMs < absoluteTimeoutMs + ) { + workInProgressRootAbsoluteTimeoutMs = absoluteTimeoutMs; + if (workInProgressRootExitStatus === RootIncomplete) { + workInProgressRootExitStatus = RootSuspended; + } + } +} + +export function renderDidError() { + if ( + workInProgressRootExitStatus === RootIncomplete || + workInProgressRootExitStatus === RootSuspended + ) { + workInProgressRootExitStatus = RootErrored; + } +} + +function workLoopSync() { + // Already timed out, so perform work without checking if we need to yield. + while (workInProgress !== null) { + workInProgress = performUnitOfWork(workInProgress); + } +} + +function workLoop() { + // Perform work until Scheduler asks us to yield + while (workInProgress !== null && !shouldYield()) { + workInProgress = performUnitOfWork(workInProgress); + } +} + +function performUnitOfWork(unitOfWork: Fiber): Fiber | null { + // The current, flushed, state of this fiber is the alternate. Ideally + // nothing should rely on this, but relying on it here means that we don't + // need an additional field on the work in progress. + const current = unitOfWork.alternate; + + startWorkTimer(unitOfWork); + setCurrentDebugFiberInDEV(unitOfWork); + + let next; + if (enableProfilerTimer && (unitOfWork.mode & ProfileMode) !== NoContext) { + startProfilerTimer(unitOfWork); + next = beginWork(current, unitOfWork, renderExpirationTime); + stopProfilerTimerIfRunningAndRecordDelta(unitOfWork, true); + } else { + next = beginWork(current, unitOfWork, renderExpirationTime); + } + + resetCurrentDebugFiberInDEV(); + unitOfWork.memoizedProps = unitOfWork.pendingProps; + if (next === null) { + // If this doesn't spawn new work, complete the current work. + next = completeUnitOfWork(unitOfWork); + } + + ReactCurrentOwner.current = null; + return next; +} + +function completeUnitOfWork(unitOfWork: Fiber): Fiber | null { + // Attempt to complete the current unit of work, then move to the next + // sibling. If there are no more siblings, return to the parent fiber. + workInProgress = unitOfWork; + do { + // The current, flushed, state of this fiber is the alternate. Ideally + // nothing should rely on this, but relying on it here means that we don't + // need an additional field on the work in progress. + const current = workInProgress.alternate; + const returnFiber = workInProgress.return; + + // Check if the work completed or if something threw. + if ((workInProgress.effectTag & Incomplete) === NoEffect) { + setCurrentDebugFiberInDEV(workInProgress); + let next; + if ( + !enableProfilerTimer || + (workInProgress.mode & ProfileMode) === NoContext + ) { + next = completeWork(current, workInProgress, renderExpirationTime); + } else { + startProfilerTimer(workInProgress); + next = completeWork(current, workInProgress, renderExpirationTime); + // Update render duration assuming we didn't error. + stopProfilerTimerIfRunningAndRecordDelta(workInProgress, false); + } + stopWorkTimer(workInProgress); + resetCurrentDebugFiberInDEV(); + resetChildExpirationTime(workInProgress); + + if (next !== null) { + // Completing this fiber spawned new work. Work on that next. + return next; + } + + if ( + returnFiber !== null && + // Do not append effects to parents if a sibling failed to complete + (returnFiber.effectTag & Incomplete) === NoEffect + ) { + // Append all the effects of the subtree and this fiber onto the effect + // list of the parent. The completion order of the children affects the + // side-effect order. + if (returnFiber.firstEffect === null) { + returnFiber.firstEffect = workInProgress.firstEffect; + } + if (workInProgress.lastEffect !== null) { + if (returnFiber.lastEffect !== null) { + returnFiber.lastEffect.nextEffect = workInProgress.firstEffect; + } + returnFiber.lastEffect = workInProgress.lastEffect; + } + + // If this fiber had side-effects, we append it AFTER the children's + // side-effects. We can perform certain side-effects earlier if needed, + // by doing multiple passes over the effect list. We don't want to + // schedule our own side-effect on our own list because if end up + // reusing children we'll schedule this effect onto itself since we're + // at the end. + const effectTag = workInProgress.effectTag; + + // Skip both NoWork and PerformedWork tags when creating the effect + // list. PerformedWork effect is read by React DevTools but shouldn't be + // committed. + if (effectTag > PerformedWork) { + if (returnFiber.lastEffect !== null) { + returnFiber.lastEffect.nextEffect = workInProgress; + } else { + returnFiber.firstEffect = workInProgress; + } + returnFiber.lastEffect = workInProgress; + } + } + } else { + // This fiber did not complete because something threw. Pop values off + // the stack without entering the complete phase. If this is a boundary, + // capture values if possible. + const next = unwindWork(workInProgress, renderExpirationTime); + + // Because this fiber did not complete, don't reset its expiration time. + + if ( + enableProfilerTimer && + (workInProgress.mode & ProfileMode) !== NoContext + ) { + // Record the render duration for the fiber that errored. + stopProfilerTimerIfRunningAndRecordDelta(workInProgress, false); + + // Include the time spent working on failed children before continuing. + let actualDuration = workInProgress.actualDuration; + let child = workInProgress.child; + while (child !== null) { + actualDuration += child.actualDuration; + child = child.sibling; + } + workInProgress.actualDuration = actualDuration; + } + + if (next !== null) { + // If completing this work spawned new work, do that next. We'll come + // back here again. + // Since we're restarting, remove anything that is not a host effect + // from the effect tag. + // TODO: The name stopFailedWorkTimer is misleading because Suspense + // also captures and restarts. + stopFailedWorkTimer(workInProgress); + next.effectTag &= HostEffectMask; + return next; + } + stopWorkTimer(workInProgress); + + if (returnFiber !== null) { + // Mark the parent fiber as incomplete and clear its effect list. + returnFiber.firstEffect = returnFiber.lastEffect = null; + returnFiber.effectTag |= Incomplete; + } + } + + const siblingFiber = workInProgress.sibling; + if (siblingFiber !== null) { + // If there is more work to do in this returnFiber, do that next. + return siblingFiber; + } + // Otherwise, return to the parent + workInProgress = returnFiber; + } while (workInProgress !== null); + + // We've reached the root. + if (workInProgressRootExitStatus === RootIncomplete) { + workInProgressRootExitStatus = RootCompleted; + } + return null; +} + +function resetChildExpirationTime(completedWork: Fiber) { + if ( + renderExpirationTime !== Never && + completedWork.childExpirationTime === Never + ) { + // The children of this component are hidden. Don't bubble their + // expiration times. + return; + } + + let newChildExpirationTime = NoWork; + + // Bubble up the earliest expiration time. + if (enableProfilerTimer && (completedWork.mode & ProfileMode) !== NoContext) { + // In profiling mode, resetChildExpirationTime is also used to reset + // profiler durations. + let actualDuration = completedWork.actualDuration; + let treeBaseDuration = completedWork.selfBaseDuration; + + // When a fiber is cloned, its actualDuration is reset to 0. This value will + // only be updated if work is done on the fiber (i.e. it doesn't bailout). + // When work is done, it should bubble to the parent's actualDuration. If + // the fiber has not been cloned though, (meaning no work was done), then + // this value will reflect the amount of time spent working on a previous + // render. In that case it should not bubble. We determine whether it was + // cloned by comparing the child pointer. + const shouldBubbleActualDurations = + completedWork.alternate === null || + completedWork.child !== completedWork.alternate.child; + + let child = completedWork.child; + while (child !== null) { + const childUpdateExpirationTime = child.expirationTime; + const childChildExpirationTime = child.childExpirationTime; + if (childUpdateExpirationTime > newChildExpirationTime) { + newChildExpirationTime = childUpdateExpirationTime; + } + if (childChildExpirationTime > newChildExpirationTime) { + newChildExpirationTime = childChildExpirationTime; + } + if (shouldBubbleActualDurations) { + actualDuration += child.actualDuration; + } + treeBaseDuration += child.treeBaseDuration; + child = child.sibling; + } + completedWork.actualDuration = actualDuration; + completedWork.treeBaseDuration = treeBaseDuration; + } else { + let child = completedWork.child; + while (child !== null) { + const childUpdateExpirationTime = child.expirationTime; + const childChildExpirationTime = child.childExpirationTime; + if (childUpdateExpirationTime > newChildExpirationTime) { + newChildExpirationTime = childUpdateExpirationTime; + } + if (childChildExpirationTime > newChildExpirationTime) { + newChildExpirationTime = childChildExpirationTime; + } + child = child.sibling; + } + } + + completedWork.childExpirationTime = newChildExpirationTime; +} + +function commitRoot(root, expirationTime) { + runWithPriority( + ImmediatePriority, + commitRootImpl.bind(null, root, expirationTime), + ); + // If there are passive effects, schedule a callback to flush them. This goes + // outside commitRootImpl so that it inherits the priority of the render. + if (rootWithPendingPassiveEffects !== null) { + const priorityLevel = getCurrentPriorityLevel(); + scheduleCallback(priorityLevel, () => { + flushPassiveEffects(); + return null; + }); + } + return null; +} + +function commitRootImpl(root, expirationTime) { + flushPassiveEffects(); + flushRenderPhaseStrictModeWarningsInDEV(); + + invariant( + workPhase !== RenderPhase && workPhase !== CommitPhase, + 'Should not already be working.', + ); + const finishedWork = root.current.alternate; + invariant(finishedWork !== null, 'Should have a work-in-progress root.'); + + // commitRoot never returns a continuation; it always finishes synchronously. + // So we can clear these now to allow a new callback to be scheduled. + root.callbackNode = null; + root.callbackExpirationTime = NoWork; + + startCommitTimer(); + + // Update the first and last pending times on this root. The new first + // pending time is whatever is left on the root fiber. + const updateExpirationTimeBeforeCommit = finishedWork.expirationTime; + const childExpirationTimeBeforeCommit = finishedWork.childExpirationTime; + const firstPendingTimeBeforeCommit = + childExpirationTimeBeforeCommit > updateExpirationTimeBeforeCommit + ? childExpirationTimeBeforeCommit + : updateExpirationTimeBeforeCommit; + root.firstPendingTime = firstPendingTimeBeforeCommit; + if (firstPendingTimeBeforeCommit < root.lastPendingTime) { + // This usually means we've finished all the work, but it can also happen + // when something gets downprioritized during render, like a hidden tree. + root.lastPendingTime = firstPendingTimeBeforeCommit; + } + + if (root === workInProgressRoot) { + // We can reset these now that they are finished. + workInProgressRoot = null; + workInProgress = null; + renderExpirationTime = NoWork; + } else { + // This indicates that the last root we worked on is not the same one that + // we're committing now. This most commonly happens when a suspended root + // times out. + } + + // Get the list of effects. + let firstEffect; + if (finishedWork.effectTag > PerformedWork) { + // A fiber's effect list consists only of its children, not itself. So if + // the root has an effect, we need to add it to the end of the list. The + // resulting list is the set that would belong to the root's parent, if it + // had one; that is, all the effects in the tree including the root. + if (finishedWork.lastEffect !== null) { + finishedWork.lastEffect.nextEffect = finishedWork; + firstEffect = finishedWork.firstEffect; + } else { + firstEffect = finishedWork; + } + } else { + // There is no effect on the root. + firstEffect = finishedWork.firstEffect; + } + + if (firstEffect !== null) { + const prevWorkPhase = workPhase; + workPhase = CommitPhase; + let prevInteractions: Set | null = null; + if (enableSchedulerTracing) { + prevInteractions = __interactionsRef.current; + __interactionsRef.current = root.memoizedInteractions; + } + + // Reset this to null before calling lifecycles + ReactCurrentOwner.current = null; + + // The commit phase is broken into several sub-phases. We do a separate pass + // of the effect list for each phase: all mutation effects come before all + // layout effects, and so on. + + // The first phase a "before mutation" phase. We use this phase to read the + // state of the host tree right before we mutate it. This is where + // getSnapshotBeforeUpdate is called. + startCommitSnapshotEffectsTimer(); + prepareForCommit(root.containerInfo); + nextEffect = firstEffect; + do { + if (__DEV__) { + invokeGuardedCallback(null, commitBeforeMutationEffects, null); + if (hasCaughtError()) { + invariant(nextEffect !== null, 'Should be working on an effect.'); + const error = clearCaughtError(); + captureCommitPhaseError(nextEffect, error); + nextEffect = nextEffect.nextEffect; + } + } else { + try { + commitBeforeMutationEffects(); + } catch (error) { + invariant(nextEffect !== null, 'Should be working on an effect.'); + captureCommitPhaseError(nextEffect, error); + nextEffect = nextEffect.nextEffect; + } + } + } while (nextEffect !== null); + stopCommitSnapshotEffectsTimer(); + + if (enableProfilerTimer) { + // Mark the current commit time to be shared by all Profilers in this + // batch. This enables them to be grouped later. + recordCommitTime(); + } + + // The next phase is the mutation phase, where we mutate the host tree. + startCommitHostEffectsTimer(); + nextEffect = firstEffect; + do { + if (__DEV__) { + invokeGuardedCallback(null, commitMutationEffects, null); + if (hasCaughtError()) { + invariant(nextEffect !== null, 'Should be working on an effect.'); + const error = clearCaughtError(); + captureCommitPhaseError(nextEffect, error); + nextEffect = nextEffect.nextEffect; + } + } else { + try { + commitMutationEffects(); + } catch (error) { + invariant(nextEffect !== null, 'Should be working on an effect.'); + captureCommitPhaseError(nextEffect, error); + nextEffect = nextEffect.nextEffect; + } + } + } while (nextEffect !== null); + stopCommitHostEffectsTimer(); + resetAfterCommit(root.containerInfo); + + // The work-in-progress tree is now the current tree. This must come after + // the mutation phase, so that the previous tree is still current during + // componentWillUnmount, but before the layout phase, so that the finished + // work is current during componentDidMount/Update. + root.current = finishedWork; + + // The next phase is the layout phase, where we call effects that read + // the host tree after it's been mutated. The idiomatic use case for this is + // layout, but class component lifecycles also fire here for legacy reasons. + startCommitLifeCyclesTimer(); + nextEffect = firstEffect; + do { + if (__DEV__) { + invokeGuardedCallback( + null, + commitLayoutEffects, + null, + root, + expirationTime, + ); + if (hasCaughtError()) { + invariant(nextEffect !== null, 'Should be working on an effect.'); + const error = clearCaughtError(); + captureCommitPhaseError(nextEffect, error); + nextEffect = nextEffect.nextEffect; + } + } else { + try { + commitLayoutEffects(root, expirationTime); + } catch (error) { + invariant(nextEffect !== null, 'Should be working on an effect.'); + captureCommitPhaseError(nextEffect, error); + nextEffect = nextEffect.nextEffect; + } + } + } while (nextEffect !== null); + stopCommitLifeCyclesTimer(); + + nextEffect = null; + + if (enableSchedulerTracing) { + __interactionsRef.current = ((prevInteractions: any): Set); + } + workPhase = prevWorkPhase; + } else { + // No effects. + root.current = finishedWork; + // Measure these anyway so the flamegraph explicitly shows that there were + // no effects. + // TODO: Maybe there's a better way to report this. + startCommitSnapshotEffectsTimer(); + stopCommitSnapshotEffectsTimer(); + if (enableProfilerTimer) { + recordCommitTime(); + } + startCommitHostEffectsTimer(); + stopCommitHostEffectsTimer(); + startCommitLifeCyclesTimer(); + stopCommitLifeCyclesTimer(); + } + + stopCommitTimer(); + + if (rootDoesHavePassiveEffects) { + // This commit has passive effects. Stash a reference to them. But don't + // schedule a callback until after flushing layout work. + rootDoesHavePassiveEffects = false; + rootWithPendingPassiveEffects = root; + pendingPassiveEffectsExpirationTime = expirationTime; + } else { + if (enableSchedulerTracing) { + // If there are no passive effects, then we can complete the pending + // interactions. Otherwise, we'll wait until after the passive effects + // are flushed. + finishPendingInteractions(root, expirationTime); + } + } + + // Check if there's remaining work on this root + const remainingExpirationTime = root.firstPendingTime; + if (remainingExpirationTime !== NoWork) { + const currentTime = requestCurrentTime(); + const priorityLevel = inferPriorityFromExpirationTime( + currentTime, + remainingExpirationTime, + ); + scheduleCallbackForRoot(root, priorityLevel, remainingExpirationTime); + } else { + // If there's no remaining work, we can clear the set of already failed + // error boundaries. + legacyErrorBoundariesThatAlreadyFailed = null; + } + + onCommitRoot(finishedWork.stateNode); + + if (remainingExpirationTime === Sync) { + // Count the number of times the root synchronously re-renders without + // finishing. If there are too many, it indicates an infinite update loop. + if (root === rootWithNestedUpdates) { + nestedUpdateCount++; + } else { + nestedUpdateCount = 0; + rootWithNestedUpdates = root; + } + } else { + nestedUpdateCount = 0; + } + + if (hasUncaughtError) { + hasUncaughtError = false; + const error = firstUncaughtError; + firstUncaughtError = null; + throw error; + } + + if (workPhase === LegacyUnbatchedPhase) { + // This is a legacy edge case. We just committed the initial mount of + // a ReactDOM.render-ed root inside of batchedUpdates. The commit fired + // synchronously, but layout updates should be deferred until the end + // of the batch. + return null; + } + + // If layout work was scheduled, flush it now. + flushImmediateQueue(); + return null; +} + +function commitBeforeMutationEffects() { + while (nextEffect !== null) { + if ((nextEffect.effectTag & Snapshot) !== NoEffect) { + setCurrentDebugFiberInDEV(nextEffect); + recordEffect(); + + const current = nextEffect.alternate; + commitBeforeMutationEffectOnFiber(current, nextEffect); + + resetCurrentDebugFiberInDEV(); + } + nextEffect = nextEffect.nextEffect; + } +} + +function commitMutationEffects() { + // TODO: Should probably move the bulk of this function to commitWork. + while (nextEffect !== null) { + setCurrentDebugFiberInDEV(nextEffect); + + const effectTag = nextEffect.effectTag; + + if (effectTag & ContentReset) { + commitResetTextContent(nextEffect); + } + + if (effectTag & Ref) { + const current = nextEffect.alternate; + if (current !== null) { + commitDetachRef(current); + } + } + + // The following switch statement is only concerned about placement, + // updates, and deletions. To avoid needing to add a case for every possible + // bitmap value, we remove the secondary effects from the effect tag and + // switch on that value. + let primaryEffectTag = effectTag & (Placement | Update | Deletion); + switch (primaryEffectTag) { + case Placement: { + commitPlacement(nextEffect); + // Clear the "placement" from effect tag so that we know that this is + // inserted, before any life-cycles like componentDidMount gets called. + // TODO: findDOMNode doesn't rely on this any more but isMounted does + // and isMounted is deprecated anyway so we should be able to kill this. + nextEffect.effectTag &= ~Placement; + break; + } + case PlacementAndUpdate: { + // Placement + commitPlacement(nextEffect); + // Clear the "placement" from effect tag so that we know that this is + // inserted, before any life-cycles like componentDidMount gets called. + nextEffect.effectTag &= ~Placement; + + // Update + const current = nextEffect.alternate; + commitWork(current, nextEffect); + break; + } + case Update: { + const current = nextEffect.alternate; + commitWork(current, nextEffect); + break; + } + case Deletion: { + commitDeletion(nextEffect); + break; + } + } + + // TODO: Only record a mutation effect if primaryEffectTag is non-zero. + recordEffect(); + + resetCurrentDebugFiberInDEV(); + nextEffect = nextEffect.nextEffect; + } +} + +function commitLayoutEffects( + root: FiberRoot, + committedExpirationTime: ExpirationTime, +) { + // TODO: Should probably move the bulk of this function to commitWork. + while (nextEffect !== null) { + setCurrentDebugFiberInDEV(nextEffect); + + const effectTag = nextEffect.effectTag; + + if (effectTag & (Update | Callback)) { + recordEffect(); + const current = nextEffect.alternate; + commitLayoutEffectOnFiber( + root, + current, + nextEffect, + committedExpirationTime, + ); + } + + if (effectTag & Ref) { + recordEffect(); + commitAttachRef(nextEffect); + } + + if (effectTag & Passive) { + rootDoesHavePassiveEffects = true; + } + + resetCurrentDebugFiberInDEV(); + nextEffect = nextEffect.nextEffect; + } +} + +export function flushPassiveEffects() { + if (rootWithPendingPassiveEffects === null) { + return false; + } + const root = rootWithPendingPassiveEffects; + const expirationTime = pendingPassiveEffectsExpirationTime; + rootWithPendingPassiveEffects = null; + pendingPassiveEffectsExpirationTime = NoWork; + + let prevInteractions: Set | null = null; + if (enableSchedulerTracing) { + prevInteractions = __interactionsRef.current; + __interactionsRef.current = root.memoizedInteractions; + } + + invariant( + workPhase !== RenderPhase && workPhase !== CommitPhase, + 'Cannot flush passive effects while already rendering.', + ); + const prevWorkPhase = workPhase; + workPhase = CommitPhase; + + // Note: This currently assumes there are no passive effects on the root + // fiber, because the root is not part of its own effect list. This could + // change in the future. + let effect = root.current.firstEffect; + while (effect !== null) { + if (__DEV__) { + setCurrentDebugFiberInDEV(effect); + invokeGuardedCallback(null, commitPassiveHookEffects, null, effect); + if (hasCaughtError()) { + invariant(effect !== null, 'Should be working on an effect.'); + const error = clearCaughtError(); + captureCommitPhaseError(effect, error); + } + resetCurrentDebugFiberInDEV(); + } else { + try { + commitPassiveHookEffects(effect); + } catch (error) { + invariant(effect !== null, 'Should be working on an effect.'); + captureCommitPhaseError(effect, error); + } + } + effect = effect.nextEffect; + } + + if (enableSchedulerTracing) { + __interactionsRef.current = ((prevInteractions: any): Set); + finishPendingInteractions(root, expirationTime); + } + + workPhase = prevWorkPhase; + flushImmediateQueue(); + + // If additional passive effects were scheduled, increment a counter. If this + // exceeds the limit, we'll fire a warning. + nestedPassiveUpdateCount = + rootWithPendingPassiveEffects === null ? 0 : nestedPassiveUpdateCount + 1; + + return true; +} + +export function isAlreadyFailedLegacyErrorBoundary(instance: mixed): boolean { + return ( + legacyErrorBoundariesThatAlreadyFailed !== null && + legacyErrorBoundariesThatAlreadyFailed.has(instance) + ); +} + +export function markLegacyErrorBoundaryAsFailed(instance: mixed) { + if (legacyErrorBoundariesThatAlreadyFailed === null) { + legacyErrorBoundariesThatAlreadyFailed = new Set([instance]); + } else { + legacyErrorBoundariesThatAlreadyFailed.add(instance); + } +} + +function prepareToThrowUncaughtError(error: mixed) { + if (!hasUncaughtError) { + hasUncaughtError = true; + firstUncaughtError = error; + } +} +export const onUncaughtError = prepareToThrowUncaughtError; + +function captureCommitPhaseErrorOnRoot( + rootFiber: Fiber, + sourceFiber: Fiber, + error: mixed, +) { + const errorInfo = createCapturedValue(error, sourceFiber); + const update = createRootErrorUpdate(rootFiber, errorInfo, Sync); + enqueueUpdate(rootFiber, update); + const root = markUpdateTimeFromFiberToRoot(rootFiber, Sync); + if (root !== null) { + scheduleCallbackForRoot(root, ImmediatePriority, Sync); + } +} + +export function captureCommitPhaseError(sourceFiber: Fiber, error: mixed) { + if (sourceFiber.tag === HostRoot) { + // Error was thrown at the root. There is no parent, so the root + // itself should capture it. + captureCommitPhaseErrorOnRoot(sourceFiber, sourceFiber, error); + return; + } + + let fiber = sourceFiber.return; + while (fiber !== null) { + if (fiber.tag === HostRoot) { + captureCommitPhaseErrorOnRoot(fiber, sourceFiber, error); + return; + } else if (fiber.tag === ClassComponent) { + const ctor = fiber.type; + const instance = fiber.stateNode; + if ( + typeof ctor.getDerivedStateFromError === 'function' || + (typeof instance.componentDidCatch === 'function' && + !isAlreadyFailedLegacyErrorBoundary(instance)) + ) { + const errorInfo = createCapturedValue(error, sourceFiber); + const update = createClassErrorUpdate( + fiber, + errorInfo, + // TODO: This is always sync + Sync, + ); + enqueueUpdate(fiber, update); + const root = markUpdateTimeFromFiberToRoot(fiber, Sync); + if (root !== null) { + scheduleCallbackForRoot(root, ImmediatePriority, Sync); + } + return; + } + } + fiber = fiber.return; + } +} + +export function pingSuspendedRoot( + root: FiberRoot, + thenable: Thenable, + suspendedTime: ExpirationTime, +) { + const pingCache = root.pingCache; + if (pingCache !== null) { + // The thenable resolved, so we no longer need to memoize, because it will + // never be thrown again. + pingCache.delete(thenable); + } + + if (workInProgressRoot === root && renderExpirationTime === suspendedTime) { + // Received a ping at the same priority level at which we're currently + // rendering. Restart from the root. Don't need to schedule a ping because + // we're already working on this tree. + prepareFreshStack(root, renderExpirationTime); + return; + } + + const lastPendingTime = root.lastPendingTime; + if (lastPendingTime < suspendedTime) { + // The root is no longer suspended at this time. + return; + } + + const pingTime = root.pingTime; + if (pingTime !== NoWork && pingTime < suspendedTime) { + // There's already a lower priority ping scheduled. + return; + } + + // Mark the time at which this ping was scheduled. + root.pingTime = suspendedTime; + + const currentTime = requestCurrentTime(); + const priorityLevel = inferPriorityFromExpirationTime( + currentTime, + suspendedTime, + ); + scheduleCallbackForRoot(root, priorityLevel, suspendedTime); +} + +export function retryTimedOutBoundary(boundaryFiber: Fiber) { + // The boundary fiber (a Suspense component) previously timed out and was + // rendered in its fallback state. One of the promises that suspended it has + // resolved, which means at least part of the tree was likely unblocked. Try + // rendering again, at a new expiration time. + const currentTime = requestCurrentTime(); + const retryTime = computeExpirationForFiber(currentTime, boundaryFiber); + // TODO: Special case idle priority? + const priorityLevel = inferPriorityFromExpirationTime(currentTime, retryTime); + const root = markUpdateTimeFromFiberToRoot(boundaryFiber, retryTime); + if (root !== null) { + scheduleCallbackForRoot(root, priorityLevel, retryTime); + } +} + +export function resolveRetryThenable(boundaryFiber: Fiber, thenable: Thenable) { + let retryCache: WeakSet | Set | null; + if (enableSuspenseServerRenderer) { + switch (boundaryFiber.tag) { + case SuspenseComponent: + retryCache = boundaryFiber.stateNode; + break; + case DehydratedSuspenseComponent: + retryCache = boundaryFiber.memoizedState; + break; + default: + invariant( + false, + 'Pinged unknown suspense boundary type. ' + + 'This is probably a bug in React.', + ); + } + } else { + retryCache = boundaryFiber.stateNode; + } + + if (retryCache !== null) { + // The thenable resolved, so we no longer need to memoize, because it will + // never be thrown again. + retryCache.delete(thenable); + } + + retryTimedOutBoundary(boundaryFiber); +} + +export 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. + const earliestExpirationTimeMs = expirationTimeToMs(root.firstPendingTime); + // TODO: Track this on the root instead. It's more accurate, doesn't rely on + // assumptions about priority, and isn't coupled to Scheduler details. + return earliestExpirationTimeMs - LOW_PRIORITY_EXPIRATION; +} + +function computeMsUntilTimeout(root, absoluteTimeoutMs) { + if (disableYielding) { + // Timeout immediately when yielding is disabled. + return 0; + } + + // Find the earliest uncommitted expiration time in the tree, including + // work that is suspended. The timeout threshold cannot be longer than + // the overall expiration. + const earliestExpirationTimeMs = expirationTimeToMs(root.firstPendingTime); + if (earliestExpirationTimeMs < absoluteTimeoutMs) { + absoluteTimeoutMs = earliestExpirationTimeMs; + } + + // Subtract the current time from the absolute timeout to get the number + // of milliseconds until the timeout. In other words, convert an absolute + // timestamp to a relative time. This is the value that is passed + // to `setTimeout`. + let msUntilTimeout = absoluteTimeoutMs - now(); + return msUntilTimeout < 0 ? 0 : msUntilTimeout; +} + +function checkForNestedUpdates() { + if (nestedUpdateCount > NESTED_UPDATE_LIMIT) { + nestedUpdateCount = 0; + rootWithNestedUpdates = null; + invariant( + false, + 'Maximum update depth exceeded. This can happen when a component ' + + 'repeatedly calls setState inside componentWillUpdate or ' + + 'componentDidUpdate. React limits the number of nested updates to ' + + 'prevent infinite loops.', + ); + } + + if (__DEV__) { + if (nestedPassiveUpdateCount > NESTED_PASSIVE_UPDATE_LIMIT) { + nestedPassiveUpdateCount = 0; + warning( + false, + 'Maximum update depth exceeded. This can happen when a component ' + + "calls setState inside useEffect, but useEffect either doesn't " + + 'have a dependency array, or one of the dependencies changes on ' + + 'every render.', + ); + } + } +} + +function flushRenderPhaseStrictModeWarningsInDEV() { + if (__DEV__) { + ReactStrictModeWarnings.flushPendingUnsafeLifecycleWarnings(); + ReactStrictModeWarnings.flushLegacyContextWarning(); + + if (warnAboutDeprecatedLifecycles) { + ReactStrictModeWarnings.flushPendingDeprecationWarnings(); + } + } +} + +function stopFinishedWorkLoopTimer() { + const didCompleteRoot = true; + stopWorkLoopTimer(interruptedBy, didCompleteRoot); + interruptedBy = null; +} + +function stopInterruptedWorkLoopTimer() { + // TODO: Track which fiber caused the interruption. + const didCompleteRoot = false; + stopWorkLoopTimer(interruptedBy, didCompleteRoot); + interruptedBy = null; +} + +function checkForInterruption( + fiberThatReceivedUpdate: Fiber, + updateExpirationTime: ExpirationTime, +) { + if ( + enableUserTimingAPI && + workInProgressRoot !== null && + updateExpirationTime > renderExpirationTime + ) { + interruptedBy = fiberThatReceivedUpdate; + } +} + +let didWarnStateUpdateForUnmountedComponent: Set | null = null; +function warnAboutUpdateOnUnmountedFiberInDEV(fiber) { + if (__DEV__) { + const tag = fiber.tag; + if ( + tag !== HostRoot && + tag !== ClassComponent && + tag !== FunctionComponent && + tag !== ForwardRef && + tag !== MemoComponent && + tag !== SimpleMemoComponent + ) { + // Only warn for user-defined components, not internal ones like Suspense. + return; + } + // We show the whole stack but dedupe on the top component's name because + // the problematic code almost always lies inside that component. + const componentName = getComponentName(fiber.type) || 'ReactComponent'; + if (didWarnStateUpdateForUnmountedComponent !== null) { + if (didWarnStateUpdateForUnmountedComponent.has(componentName)) { + return; + } + didWarnStateUpdateForUnmountedComponent.add(componentName); + } else { + didWarnStateUpdateForUnmountedComponent = new Set([componentName]); + } + warningWithoutStack( + false, + "Can't perform a React state update on an unmounted component. This " + + 'is a no-op, but it indicates a memory leak in your application. To ' + + 'fix, cancel all subscriptions and asynchronous tasks in %s.%s', + tag === ClassComponent + ? 'the componentWillUnmount method' + : 'a useEffect cleanup function', + getStackByFiberInDevAndProd(fiber), + ); + } +} + +let beginWork; +if (__DEV__ && replayFailedUnitOfWorkWithInvokeGuardedCallback) { + let dummyFiber = null; + beginWork = (current, unitOfWork, expirationTime) => { + // If a component throws an error, we replay it again in a synchronously + // dispatched event, so that the debugger will treat it as an uncaught + // error See ReactErrorUtils for more information. + + // Before entering the begin phase, copy the work-in-progress onto a dummy + // fiber. If beginWork throws, we'll use this to reset the state. + const originalWorkInProgressCopy = assignFiberPropertiesInDEV( + dummyFiber, + unitOfWork, + ); + try { + return originalBeginWork(current, unitOfWork, expirationTime); + } catch (originalError) { + if ( + originalError !== null && + typeof originalError === 'object' && + typeof originalError.then === 'function' + ) { + // Don't replay promises. Treat everything else like an error. + throw originalError; + } + + // Keep this code in sync with renderRoot; any changes here must have + // corresponding changes there. + resetContextDependencies(); + resetHooks(); + + // Unwind the failed stack frame + unwindInterruptedWork(unitOfWork); + + // Restore the original properties of the fiber. + assignFiberPropertiesInDEV(unitOfWork, originalWorkInProgressCopy); + + if (enableProfilerTimer && unitOfWork.mode & ProfileMode) { + // Reset the profiler timer. + startProfilerTimer(unitOfWork); + } + + // Run beginWork again. + invokeGuardedCallback( + null, + originalBeginWork, + null, + current, + unitOfWork, + expirationTime, + ); + + if (hasCaughtError()) { + const replayError = clearCaughtError(); + // `invokeGuardedCallback` sometimes sets an expando `_suppressLogging`. + // Rethrow this error instead of the original one. + throw replayError; + } else { + // This branch is reachable if the render phase is impure. + throw originalError; + } + } + }; +} else { + beginWork = originalBeginWork; +} + +let didWarnAboutUpdateInRender = false; +let didWarnAboutUpdateInGetChildContext = false; +function warnAboutInvalidUpdatesOnClassComponentsInDEV(fiber) { + if (__DEV__) { + if (fiber.tag === ClassComponent) { + switch (ReactCurrentDebugFiberPhaseInDEV) { + case 'getChildContext': + if (didWarnAboutUpdateInGetChildContext) { + return; + } + warningWithoutStack( + false, + 'setState(...): Cannot call setState() inside getChildContext()', + ); + didWarnAboutUpdateInGetChildContext = true; + break; + case 'render': + if (didWarnAboutUpdateInRender) { + return; + } + warningWithoutStack( + false, + 'Cannot update during an existing state transition (such as ' + + 'within `render`). Render methods should be a pure function of ' + + 'props and state.', + ); + didWarnAboutUpdateInRender = true; + break; + } + } + } +} + +function warnIfNotCurrentlyActingUpdatesInDEV(fiber: Fiber): void { + if (__DEV__) { + if ( + workPhase === NotWorking && + ReactShouldWarnActingUpdates.current === false + ) { + warningWithoutStack( + false, + 'An update to %s inside a test was not wrapped in act(...).\n\n' + + 'When testing, code that causes React state updates should be ' + + 'wrapped into act(...):\n\n' + + 'act(() => {\n' + + ' /* fire events that update state */\n' + + '});\n' + + '/* assert on the output */\n\n' + + "This ensures that you're testing the behavior the user would see " + + 'in the browser.' + + ' Learn more at https://fb.me/react-wrap-tests-with-act' + + '%s', + getComponentName(fiber.type), + getStackByFiberInDevAndProd(fiber), + ); + } + } +} + +export const warnIfNotCurrentlyActingUpdatesInDev = warnIfNotCurrentlyActingUpdatesInDEV; + +function computeThreadID(root, expirationTime) { + // Interaction threads are unique per root and expiration time. + return expirationTime * 1000 + root.interactionThreadID; +} + +function schedulePendingInteraction(root, expirationTime) { + // This is called when work is scheduled on a root. It sets up a pending + // interaction, which is completed once the work commits. + if (!enableSchedulerTracing) { + return; + } + + const interactions = __interactionsRef.current; + if (interactions.size > 0) { + const pendingInteractionMap = root.pendingInteractionMap; + const pendingInteractions = pendingInteractionMap.get(expirationTime); + if (pendingInteractions != null) { + interactions.forEach(interaction => { + if (!pendingInteractions.has(interaction)) { + // Update the pending async work count for previously unscheduled interaction. + interaction.__count++; + } + + pendingInteractions.add(interaction); + }); + } else { + pendingInteractionMap.set(expirationTime, new Set(interactions)); + + // Update the pending async work count for the current interactions. + interactions.forEach(interaction => { + interaction.__count++; + }); + } + + const subscriber = __subscriberRef.current; + if (subscriber !== null) { + const threadID = computeThreadID(root, expirationTime); + subscriber.onWorkScheduled(interactions, threadID); + } + } +} + +function startWorkOnPendingInteraction(root, expirationTime) { + // This is called when new work is started on a root. + if (!enableSchedulerTracing) { + return; + } + + // Determine which interactions this batch of work currently includes, So that + // we can accurately attribute time spent working on it, And so that cascading + // work triggered during the render phase will be associated with it. + const interactions: Set = new Set(); + root.pendingInteractionMap.forEach( + (scheduledInteractions, scheduledExpirationTime) => { + if (scheduledExpirationTime >= expirationTime) { + scheduledInteractions.forEach(interaction => + interactions.add(interaction), + ); + } + }, + ); + + // Store the current set of interactions on the FiberRoot for a few reasons: + // We can re-use it in hot functions like renderRoot() without having to + // recalculate it. We will also use it in commitWork() to pass to any Profiler + // onRender() hooks. This also provides DevTools with a way to access it when + // the onCommitRoot() hook is called. + root.memoizedInteractions = interactions; + + if (interactions.size > 0) { + const subscriber = __subscriberRef.current; + if (subscriber !== null) { + const threadID = computeThreadID(root, expirationTime); + try { + subscriber.onWorkStarted(interactions, threadID); + } catch (error) { + // If the subscriber throws, rethrow it in a separate task + scheduleCallback(ImmediatePriority, () => { + throw error; + }); + } + } + } +} + +function finishPendingInteractions(root, committedExpirationTime) { + if (!enableSchedulerTracing) { + return; + } + + const earliestRemainingTimeAfterCommit = root.firstPendingTime; + + let subscriber; + + try { + subscriber = __subscriberRef.current; + if (subscriber !== null && root.memoizedInteractions.size > 0) { + const threadID = computeThreadID(root, committedExpirationTime); + subscriber.onWorkStopped(root.memoizedInteractions, threadID); + } + } catch (error) { + // If the subscriber throws, rethrow it in a separate task + scheduleCallback(ImmediatePriority, () => { + throw error; + }); + } finally { + // Clear completed interactions from the pending Map. + // Unless the render was suspended or cascading work was scheduled, + // In which case– leave pending interactions until the subsequent render. + const pendingInteractionMap = root.pendingInteractionMap; + pendingInteractionMap.forEach( + (scheduledInteractions, scheduledExpirationTime) => { + // Only decrement the pending interaction count if we're done. + // If there's still work at the current priority, + // That indicates that we are waiting for suspense data. + if (scheduledExpirationTime > earliestRemainingTimeAfterCommit) { + pendingInteractionMap.delete(scheduledExpirationTime); + + scheduledInteractions.forEach(interaction => { + interaction.__count--; + + if (subscriber !== null && interaction.__count === 0) { + try { + subscriber.onInteractionScheduledWorkCompleted(interaction); + } catch (error) { + // If the subscriber throws, rethrow it in a separate task + scheduleCallback(ImmediatePriority, () => { + throw error; + }); + } + } + }); + } + }, + ); + } +} diff --git a/packages/react-reconciler/src/ReactFiberScheduler.old.js b/packages/react-reconciler/src/ReactFiberScheduler.old.js index 1a4b15b2bee99..965eaa1cdc62c 100644 --- a/packages/react-reconciler/src/ReactFiberScheduler.old.js +++ b/packages/react-reconciler/src/ReactFiberScheduler.old.js @@ -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, }; diff --git a/packages/react-reconciler/src/ReactFiberUnwindWork.js b/packages/react-reconciler/src/ReactFiberUnwindWork.js index f3b4d749df80b..4f5340857552a 100644 --- a/packages/react-reconciler/src/ReactFiberUnwindWork.js +++ b/packages/react-reconciler/src/ReactFiberUnwindWork.js @@ -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; } diff --git a/packages/react-reconciler/src/SchedulerWithReactIntegration.js b/packages/react-reconciler/src/SchedulerWithReactIntegration.js new file mode 100644 index 0000000000000..323ab5528fdbf --- /dev/null +++ b/packages/react-reconciler/src/SchedulerWithReactIntegration.js @@ -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 | 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( + 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; + } + } +} diff --git a/packages/react-reconciler/src/__tests__/ReactHooks-test.internal.js b/packages/react-reconciler/src/__tests__/ReactHooks-test.internal.js index f3f00658b5d9d..1de8540a6eeb5 100644 --- a/packages/react-reconciler/src/__tests__/ReactHooks-test.internal.js +++ b/packages/react-reconciler/src/__tests__/ReactHooks-test.internal.js @@ -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'); }); }); diff --git a/packages/react-reconciler/src/__tests__/ReactIncrementalErrorHandling-test.internal.js b/packages/react-reconciler/src/__tests__/ReactIncrementalErrorHandling-test.internal.js index d1f360592b4b7..4d6ddee647149 100644 --- a/packages/react-reconciler/src/__tests__/ReactIncrementalErrorHandling-test.internal.js +++ b/packages/react-reconciler/src/__tests__/ReactIncrementalErrorHandling-test.internal.js @@ -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 ; } function Parent() { - ops.push('Parent'); Scheduler.yieldValue('Parent'); return ( @@ -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(); 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(, 'a'); + ReactNoop.renderToRootWithID(, 'a'); expect(() => { expect(Scheduler).toFlushWithoutYielding(); - }).toThrow('Hello'); + }).toThrow('a'); expect(ReactNoop.getChildren('a')).toEqual([]); - ReactNoop.renderToRootWithID(, 'a'); + ReactNoop.renderToRootWithID(, 'a'); ReactNoop.renderToRootWithID(, '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(, 'a'); - ReactNoop.renderToRootWithID(, 'b'); + ReactNoop.renderToRootWithID(, 'b'); expect(() => { expect(Scheduler).toFlushWithoutYielding(); - }).toThrow('Hello'); + }).toThrow('b'); expect(ReactNoop.getChildren('a')).toEqual([span('a:3')]); expect(ReactNoop.getChildren('b')).toEqual([]); ReactNoop.renderToRootWithID(, 'a'); - ReactNoop.renderToRootWithID(, 'b'); + ReactNoop.renderToRootWithID(, 'b'); ReactNoop.renderToRootWithID(, '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(, 'b'); ReactNoop.renderToRootWithID(, 'c'); ReactNoop.renderToRootWithID(, 'd'); - ReactNoop.renderToRootWithID(, 'e'); + ReactNoop.renderToRootWithID(, '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(, 'a'); + ReactNoop.renderToRootWithID(, 'a'); ReactNoop.renderToRootWithID(, 'b'); - ReactNoop.renderToRootWithID(, 'c'); + ReactNoop.renderToRootWithID(, 'c'); ReactNoop.renderToRootWithID(, 'd'); - ReactNoop.renderToRootWithID(, 'e'); + ReactNoop.renderToRootWithID(, 'e'); ReactNoop.renderToRootWithID(, 'f'); - expect(() => { - expect(Scheduler).toFlushWithoutYielding(); - }).toThrow('Hello'); + + if (enableNewScheduler) { + // The new scheduler will throw all three errors. + expect(() => { + expect(Scheduler).toFlushWithoutYielding(); + }).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([]); diff --git a/packages/react-reconciler/src/__tests__/ReactIncrementalPerf-test.internal.js b/packages/react-reconciler/src/__tests__/ReactIncrementalPerf-test.internal.js index 0b460675d2721..1c6bf0f6b733c 100644 --- a/packages/react-reconciler/src/__tests__/ReactIncrementalPerf-test.internal.js +++ b/packages/react-reconciler/src/__tests__/ReactIncrementalPerf-test.internal.js @@ -110,27 +110,6 @@ describe('ReactDebugFiberPerf', () => { }; } - beforeEach(() => { - jest.resetModules(); - resetFlamechart(); - global.performance = createUserTimingPolyfill(); - - require('shared/ReactFeatureFlags').enableUserTimingAPI = true; - require('shared/ReactFeatureFlags').enableProfilerTimer = false; - require('shared/ReactFeatureFlags').replayFailedUnitOfWorkWithInvokeGuardedCallback = false; - require('shared/ReactFeatureFlags').debugRenderPhaseSideEffectsForStrictMode = false; - - // Import after the polyfill is set up: - React = require('react'); - ReactNoop = require('react-noop-renderer'); - Scheduler = require('scheduler'); - PropTypes = require('prop-types'); - }); - - afterEach(() => { - delete global.performance; - }); - function Parent(props) { return
{props.children}
; } @@ -139,533 +118,568 @@ describe('ReactDebugFiberPerf', () => { return
{props.children}
; } - it('measures a simple reconciliation', () => { - ReactNoop.render( - - - , - ); - addComment('Mount'); - expect(Scheduler).toFlushWithoutYielding(); - - ReactNoop.render( - - - , - ); - addComment('Update'); - expect(Scheduler).toFlushWithoutYielding(); - - ReactNoop.render(null); - addComment('Unmount'); - expect(Scheduler).toFlushWithoutYielding(); - - expect(getFlameChart()).toMatchSnapshot(); + describe('old scheduler', () => { + runTests(false); }); - it('properly displays the forwardRef component in measurements', () => { - const AnonymousForwardRef = React.forwardRef((props, ref) => ( - - )); - const NamedForwardRef = React.forwardRef(function refForwarder(props, ref) { - return ; - }); - function notImportant(props, ref) { - return ; - } - notImportant.displayName = 'OverriddenName'; - const DisplayNamedForwardRef = React.forwardRef(notImportant); - - ReactNoop.render( - - - - - , - ); - addComment('Mount'); - expect(Scheduler).toFlushWithoutYielding(); - - expect(getFlameChart()).toMatchSnapshot(); + describe('new scheduler', () => { + runTests(true); }); - it('does not include ConcurrentMode, StrictMode, or Profiler components in measurements', () => { - ReactNoop.render( - - - - - - - - - , - ); - addComment('Mount'); - expect(Scheduler).toFlushWithoutYielding(); + 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; + require('shared/ReactFeatureFlags').debugRenderPhaseSideEffectsForStrictMode = false; + + // Import after the polyfill is set up: + React = require('react'); + ReactNoop = require('react-noop-renderer'); + Scheduler = require('scheduler'); + PropTypes = require('prop-types'); + }); - expect(getFlameChart()).toMatchSnapshot(); - }); + afterEach(() => { + delete global.performance; + }); - it('does not include context provider or consumer in measurements', () => { - const {Consumer, Provider} = React.createContext(true); + it('measures a simple reconciliation', () => { + ReactNoop.render( + + + , + ); + addComment('Mount'); + expect(Scheduler).toFlushWithoutYielding(); - ReactNoop.render( - + ReactNoop.render( - {value => } - - , - ); - addComment('Mount'); - expect(Scheduler).toFlushWithoutYielding(); - - expect(getFlameChart()).toMatchSnapshot(); - }); + + , + ); + addComment('Update'); + expect(Scheduler).toFlushWithoutYielding(); - it('skips parents during setState', () => { - class A extends React.Component { - render() { - return
{this.props.children}
; - } - } + ReactNoop.render(null); + addComment('Unmount'); + expect(Scheduler).toFlushWithoutYielding(); - class B extends React.Component { - render() { - return
{this.props.children}
; + expect(getFlameChart()).toMatchSnapshot(); + }); + + it('properly displays the forwardRef component in measurements', () => { + const AnonymousForwardRef = React.forwardRef((props, ref) => ( + + )); + const NamedForwardRef = React.forwardRef(function refForwarder( + props, + ref, + ) { + return ; + }); + function notImportant(props, ref) { + return ; } - } + notImportant.displayName = 'OverriddenName'; + const DisplayNamedForwardRef = React.forwardRef(notImportant); - let a; - let b; - ReactNoop.render( - + ReactNoop.render( + + + + , + ); + addComment('Mount'); + expect(Scheduler).toFlushWithoutYielding(); + + expect(getFlameChart()).toMatchSnapshot(); + }); + + it('does not include ConcurrentMode, StrictMode, or Profiler components in measurements', () => { + ReactNoop.render( + + + + + + + + + , + ); + addComment('Mount'); + expect(Scheduler).toFlushWithoutYielding(); + + expect(getFlameChart()).toMatchSnapshot(); + }); + + it('does not include context provider or consumer in measurements', () => { + const {Consumer, Provider} = React.createContext(true); + + ReactNoop.render( + -
(a = inst)} /> + {value => } - - - (b = inst)} /> - - , - ); - expect(Scheduler).toFlushWithoutYielding(); - resetFlamechart(); - - a.setState({}); - b.setState({}); - addComment('Should include just A and B, no Parents'); - expect(Scheduler).toFlushWithoutYielding(); - expect(getFlameChart()).toMatchSnapshot(); - }); + , + ); + addComment('Mount'); + expect(Scheduler).toFlushWithoutYielding(); - it('warns on cascading renders from setState', () => { - class Cascading extends React.Component { - componentDidMount() { - this.setState({}); - } - render() { - return
{this.props.children}
; + expect(getFlameChart()).toMatchSnapshot(); + }); + + it('skips parents during setState', () => { + class A extends React.Component { + render() { + return
{this.props.children}
; + } } - } - - ReactNoop.render( - - - , - ); - addComment('Should print a warning'); - expect(Scheduler).toFlushWithoutYielding(); - expect(getFlameChart()).toMatchSnapshot(); - }); - it('warns on cascading renders from top-level render', () => { - class Cascading extends React.Component { - componentDidMount() { - ReactNoop.renderToRootWithID(, 'b'); - addComment('Scheduling another root from componentDidMount'); + class B extends React.Component { + render() { + return
{this.props.children}
; + } } - render() { - return
{this.props.children}
; + + let a; + let b; + ReactNoop.render( + + + +
(a = inst)} /> + + + + (b = inst)} /> + + , + ); + expect(Scheduler).toFlushWithoutYielding(); + resetFlamechart(); + + a.setState({}); + b.setState({}); + addComment('Should include just A and B, no Parents'); + expect(Scheduler).toFlushWithoutYielding(); + expect(getFlameChart()).toMatchSnapshot(); + }); + + it('warns on cascading renders from setState', () => { + class Cascading extends React.Component { + componentDidMount() { + this.setState({}); + } + render() { + return
{this.props.children}
; + } } - } - ReactNoop.renderToRootWithID(, 'a'); - addComment('Rendering the first root'); - expect(Scheduler).toFlushWithoutYielding(); - expect(getFlameChart()).toMatchSnapshot(); - }); + ReactNoop.render( + + + , + ); + addComment('Should print a warning'); + expect(Scheduler).toFlushWithoutYielding(); + expect(getFlameChart()).toMatchSnapshot(); + }); - it('does not treat setState from cWM or cWRP as cascading', () => { - class NotCascading extends React.Component { - UNSAFE_componentWillMount() { - this.setState({}); + it('warns on cascading renders from top-level render', () => { + class Cascading extends React.Component { + componentDidMount() { + ReactNoop.renderToRootWithID(, 'b'); + addComment('Scheduling another root from componentDidMount'); + } + render() { + return
{this.props.children}
; + } } - UNSAFE_componentWillReceiveProps() { - this.setState({}); + + ReactNoop.renderToRootWithID(, 'a'); + addComment('Rendering the first root'); + expect(Scheduler).toFlushWithoutYielding(); + expect(getFlameChart()).toMatchSnapshot(); + }); + + it('does not treat setState from cWM or cWRP as cascading', () => { + class NotCascading extends React.Component { + UNSAFE_componentWillMount() { + this.setState({}); + } + UNSAFE_componentWillReceiveProps() { + this.setState({}); + } + render() { + return
{this.props.children}
; + } } - render() { - return
{this.props.children}
; + + ReactNoop.render( + + + , + ); + addComment('Should not print a warning'); + expect(() => expect(Scheduler).toFlushWithoutYielding()).toWarnDev( + [ + 'componentWillMount: Please update the following components ' + + 'to use componentDidMount instead: NotCascading' + + '\n\ncomponentWillReceiveProps: Please update the following components ' + + 'to use static getDerivedStateFromProps instead: NotCascading', + ], + {withoutStack: true}, + ); + ReactNoop.render( + + + , + ); + addComment('Should not print a warning'); + expect(Scheduler).toFlushWithoutYielding(); + expect(getFlameChart()).toMatchSnapshot(); + }); + + it('captures all lifecycles', () => { + class AllLifecycles extends React.Component { + static childContextTypes = { + foo: PropTypes.any, + }; + shouldComponentUpdate() { + return true; + } + getChildContext() { + return {foo: 42}; + } + UNSAFE_componentWillMount() {} + componentDidMount() {} + UNSAFE_componentWillReceiveProps() {} + UNSAFE_componentWillUpdate() {} + componentDidUpdate() {} + componentWillUnmount() {} + render() { + return
; + } } - } - - ReactNoop.render( - - - , - ); - addComment('Should not print a warning'); - expect(() => expect(Scheduler).toFlushWithoutYielding()).toWarnDev( - [ - 'componentWillMount: Please update the following components ' + - 'to use componentDidMount instead: NotCascading' + - '\n\ncomponentWillReceiveProps: Please update the following components ' + - 'to use static getDerivedStateFromProps instead: NotCascading', - ], - {withoutStack: true}, - ); - ReactNoop.render( - - - , - ); - addComment('Should not print a warning'); - expect(Scheduler).toFlushWithoutYielding(); - expect(getFlameChart()).toMatchSnapshot(); - }); + ReactNoop.render(); + addComment('Mount'); + expect(() => expect(Scheduler).toFlushWithoutYielding()).toWarnDev( + [ + 'componentWillMount: Please update the following components ' + + 'to use componentDidMount instead: AllLifecycles' + + '\n\ncomponentWillReceiveProps: Please update the following components ' + + 'to use static getDerivedStateFromProps instead: AllLifecycles' + + '\n\ncomponentWillUpdate: Please update the following components ' + + 'to use componentDidUpdate instead: AllLifecycles', + 'Legacy context API has been detected within a strict-mode tree: \n\n' + + 'Please update the following components: AllLifecycles', + ], + {withoutStack: true}, + ); + ReactNoop.render(); + addComment('Update'); + expect(Scheduler).toFlushWithoutYielding(); + ReactNoop.render(null); + addComment('Unmount'); + expect(Scheduler).toFlushWithoutYielding(); + expect(getFlameChart()).toMatchSnapshot(); + }); - it('captures all lifecycles', () => { - class AllLifecycles extends React.Component { - static childContextTypes = { - foo: PropTypes.any, - }; - shouldComponentUpdate() { - return true; + it('measures deprioritized work', () => { + addComment('Flush the parent'); + ReactNoop.flushSync(() => { + ReactNoop.render( + + + , + ); + }); + addComment('Flush the child'); + expect(Scheduler).toFlushWithoutYielding(); + expect(getFlameChart()).toMatchSnapshot(); + }); + + it('measures deferred work in chunks', () => { + class A extends React.Component { + render() { + Scheduler.yieldValue('A'); + return
{this.props.children}
; + } } - getChildContext() { - return {foo: 42}; + + class B extends React.Component { + render() { + Scheduler.yieldValue('B'); + return
{this.props.children}
; + } } - UNSAFE_componentWillMount() {} - componentDidMount() {} - UNSAFE_componentWillReceiveProps() {} - UNSAFE_componentWillUpdate() {} - componentDidUpdate() {} - componentWillUnmount() {} - render() { - return
; + + class C extends React.Component { + render() { + Scheduler.yieldValue('C'); + return
{this.props.children}
; + } } - } - ReactNoop.render(); - addComment('Mount'); - expect(() => expect(Scheduler).toFlushWithoutYielding()).toWarnDev( - [ - 'componentWillMount: Please update the following components ' + - 'to use componentDidMount instead: AllLifecycles' + - '\n\ncomponentWillReceiveProps: Please update the following components ' + - 'to use static getDerivedStateFromProps instead: AllLifecycles' + - '\n\ncomponentWillUpdate: Please update the following components ' + - 'to use componentDidUpdate instead: AllLifecycles', - 'Legacy context API has been detected within a strict-mode tree: \n\n' + - 'Please update the following components: AllLifecycles', - ], - {withoutStack: true}, - ); - ReactNoop.render(); - addComment('Update'); - expect(Scheduler).toFlushWithoutYielding(); - ReactNoop.render(null); - addComment('Unmount'); - expect(Scheduler).toFlushWithoutYielding(); - expect(getFlameChart()).toMatchSnapshot(); - }); - it('measures deprioritized work', () => { - addComment('Flush the parent'); - ReactNoop.flushSync(() => { ReactNoop.render( -
+ , ); + addComment('Start rendering through B'); + expect(Scheduler).toFlushAndYieldThrough(['A', 'B']); + addComment('Complete the rest'); + expect(Scheduler).toFlushAndYield(['C']); + expect(getFlameChart()).toMatchSnapshot(); }); - addComment('Flush the child'); - expect(Scheduler).toFlushWithoutYielding(); - expect(getFlameChart()).toMatchSnapshot(); - }); - - it('measures deferred work in chunks', () => { - class A extends React.Component { - render() { - Scheduler.yieldValue('A'); - return
{this.props.children}
; - } - } - class B extends React.Component { - render() { - Scheduler.yieldValue('B'); - return
{this.props.children}
; + it('recovers from fatal errors', () => { + function Baddie() { + throw new Error('Game over'); } - } - class C extends React.Component { - render() { - Scheduler.yieldValue('C'); - return
{this.props.children}
; + ReactNoop.render( + + + , + ); + try { + addComment('Will fatal'); + expect(Scheduler).toFlushWithoutYielding(); + } catch (err) { + expect(err.message).toBe('Game over'); } - } - - ReactNoop.render( - - - - - - - - + ReactNoop.render( + - - , - ); - addComment('Start rendering through B'); - expect(Scheduler).toFlushAndYieldThrough(['A', 'B']); - addComment('Complete the rest'); - expect(Scheduler).toFlushAndYield(['C']); - expect(getFlameChart()).toMatchSnapshot(); - }); - - it('recovers from fatal errors', () => { - function Baddie() { - throw new Error('Game over'); - } - - ReactNoop.render( - - - , - ); - try { - addComment('Will fatal'); + , + ); + addComment('Will reconcile from a clean state'); expect(Scheduler).toFlushWithoutYielding(); - } catch (err) { - expect(err.message).toBe('Game over'); - } - ReactNoop.render( - - - , - ); - addComment('Will reconcile from a clean state'); - expect(Scheduler).toFlushWithoutYielding(); - expect(getFlameChart()).toMatchSnapshot(); - }); - - it('recovers from caught errors', () => { - function Baddie() { - throw new Error('Game over'); - } + expect(getFlameChart()).toMatchSnapshot(); + }); - function ErrorReport() { - return
; - } + it('recovers from caught errors', () => { + function Baddie() { + throw new Error('Game over'); + } - class Boundary extends React.Component { - state = {error: null}; - componentDidCatch(error) { - this.setState({error}); + function ErrorReport() { + return
; } - render() { - if (this.state.error) { - return ; + + class Boundary extends React.Component { + state = {error: null}; + componentDidCatch(error) { + this.setState({error}); + } + render() { + if (this.state.error) { + return ; + } + return this.props.children; } - return this.props.children; } - } - ReactNoop.render( - - - - - - - , - ); - addComment('Stop on Baddie and restart from Boundary'); - expect(Scheduler).toFlushWithoutYielding(); - expect(getFlameChart()).toMatchSnapshot(); - }); + ReactNoop.render( + + + + + + + , + ); + addComment('Stop on Baddie and restart from Boundary'); + expect(Scheduler).toFlushWithoutYielding(); + expect(getFlameChart()).toMatchSnapshot(); + }); - it('deduplicates lifecycle names during commit to reduce overhead', () => { - class A extends React.Component { - componentDidUpdate() {} - render() { - return
; + it('deduplicates lifecycle names during commit to reduce overhead', () => { + class A extends React.Component { + componentDidUpdate() {} + render() { + return
; + } } - } - class B extends React.Component { - componentDidUpdate(prevProps) { - if (this.props.cascade && !prevProps.cascade) { - this.setState({}); + class B extends React.Component { + componentDidUpdate(prevProps) { + if (this.props.cascade && !prevProps.cascade) { + this.setState({}); + } + } + render() { + return
; } } - render() { - return
; - } - } - - ReactNoop.render( - - - - - - , - ); - expect(Scheduler).toFlushWithoutYielding(); - resetFlamechart(); - - ReactNoop.render( - - - - - - , - ); - addComment('The commit phase should mention A and B just once'); - expect(Scheduler).toFlushWithoutYielding(); - ReactNoop.render( - - - - - - , - ); - addComment("Because of deduplication, we don't know B was cascading,"); - addComment('but we should still see the warning for the commit phase.'); - expect(Scheduler).toFlushWithoutYielding(); - expect(getFlameChart()).toMatchSnapshot(); - }); - it('supports portals', () => { - const portalContainer = ReactNoop.getOrCreateRootContainer( - 'portalContainer', - ); - ReactNoop.render( - - {ReactNoop.createPortal(, portalContainer, null)} - , - ); - expect(Scheduler).toFlushWithoutYielding(); - expect(getFlameChart()).toMatchSnapshot(); - }); + ReactNoop.render( + + + + + + , + ); + expect(Scheduler).toFlushWithoutYielding(); + resetFlamechart(); + + ReactNoop.render( + + + + + + , + ); + addComment('The commit phase should mention A and B just once'); + expect(Scheduler).toFlushWithoutYielding(); + ReactNoop.render( + + + + + + , + ); + addComment("Because of deduplication, we don't know B was cascading,"); + addComment('but we should still see the warning for the commit phase.'); + expect(Scheduler).toFlushWithoutYielding(); + expect(getFlameChart()).toMatchSnapshot(); + }); - it('supports memo', () => { - const MemoFoo = React.memo(function Foo() { - return
; + it('supports portals', () => { + const portalContainer = ReactNoop.getOrCreateRootContainer( + 'portalContainer', + ); + ReactNoop.render( + + {ReactNoop.createPortal(, portalContainer, null)} + , + ); + expect(Scheduler).toFlushWithoutYielding(); + expect(getFlameChart()).toMatchSnapshot(); }); - ReactNoop.render( - - - , - ); - expect(Scheduler).toFlushWithoutYielding(); - expect(getFlameChart()).toMatchSnapshot(); - }); - it('supports Suspense and lazy', async () => { - function Spinner() { - return ; - } + it('supports memo', () => { + const MemoFoo = React.memo(function Foo() { + return
; + }); + ReactNoop.render( + + + , + ); + expect(Scheduler).toFlushWithoutYielding(); + expect(getFlameChart()).toMatchSnapshot(); + }); - function fakeImport(result) { - return {default: result}; - } + it('supports Suspense and lazy', async () => { + function Spinner() { + return ; + } + + function fakeImport(result) { + return {default: result}; + } - let resolve; - const LazyFoo = React.lazy( - () => - new Promise(r => { - resolve = r; + let resolve; + const LazyFoo = React.lazy( + () => + new Promise(r => { + resolve = r; + }), + ); + + ReactNoop.render( + + }> + + + , + ); + expect(Scheduler).toFlushWithoutYielding(); + expect(getFlameChart()).toMatchSnapshot(); + + resolve( + fakeImport(function Foo() { + return
; }), - ); - - ReactNoop.render( - - }> - - - , - ); - expect(Scheduler).toFlushWithoutYielding(); - expect(getFlameChart()).toMatchSnapshot(); - - resolve( - fakeImport(function Foo() { - return
; - }), - ); - - await Promise.resolve(); - - ReactNoop.render( - - - - - , - ); - expect(Scheduler).toFlushWithoutYielding(); - expect(getFlameChart()).toMatchSnapshot(); - }); + ); - it('does not schedule an extra callback if setState is called during a synchronous commit phase', () => { - class Component extends React.Component { - state = {step: 1}; - componentDidMount() { - this.setState({step: 2}); - } - render() { - return ; + await Promise.resolve(); + + ReactNoop.render( + + + + + , + ); + expect(Scheduler).toFlushWithoutYielding(); + expect(getFlameChart()).toMatchSnapshot(); + }); + + it('does not schedule an extra callback if setState is called during a synchronous commit phase', () => { + class Component extends React.Component { + state = {step: 1}; + componentDidMount() { + this.setState({step: 2}); + } + render() { + return ; + } } - } - ReactNoop.flushSync(() => { - ReactNoop.render(); + ReactNoop.flushSync(() => { + ReactNoop.render(); + }); + expect(getFlameChart()).toMatchSnapshot(); }); - expect(getFlameChart()).toMatchSnapshot(); - }); - it('warns if an in-progress update is interrupted', () => { - function Foo() { - Scheduler.yieldValue('Foo'); - return ; - } + it('warns if an in-progress update is interrupted', () => { + function Foo() { + Scheduler.yieldValue('Foo'); + return ; + } - ReactNoop.render(); - ReactNoop.flushNextYield(); - ReactNoop.flushSync(() => { ReactNoop.render(); + ReactNoop.flushNextYield(); + ReactNoop.flushSync(() => { + ReactNoop.render(); + }); + expect(Scheduler).toHaveYielded(['Foo']); + expect(Scheduler).toFlushWithoutYielding(); + expect(getFlameChart()).toMatchSnapshot(); }); - expect(Scheduler).toHaveYielded(['Foo']); - expect(Scheduler).toFlushWithoutYielding(); - expect(getFlameChart()).toMatchSnapshot(); - }); - it('warns if async work expires (starvation)', () => { - function Foo() { - return ; - } + it('warns if async work expires (starvation)', () => { + function Foo() { + return ; + } - ReactNoop.render(); - ReactNoop.expire(6000); - expect(Scheduler).toFlushWithoutYielding(); - expect(getFlameChart()).toMatchSnapshot(); - }); + ReactNoop.render(); + ReactNoop.expire(6000); + expect(Scheduler).toFlushWithoutYielding(); + expect(getFlameChart()).toMatchSnapshot(); + }); + } }); diff --git a/packages/react-reconciler/src/__tests__/ReactLazy-test.internal.js b/packages/react-reconciler/src/__tests__/ReactLazy-test.internal.js index cd91c40574072..bacabb906c581 100644 --- a/packages/react-reconciler/src/__tests__/ReactLazy-test.internal.js +++ b/packages/react-reconciler/src/__tests__/ReactLazy-test.internal.js @@ -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( }> , ); - expect(Scheduler).toHaveYielded([ - 'UNSAFE_componentWillMount: A', - 'A1', - 'UNSAFE_componentWillReceiveProps: A -> A', - 'UNSAFE_componentWillUpdate: A -> A', - 'A2', - ]); - expect(Scheduler).toFlushAndYield([]); + + 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_componentWillReceiveProps: A -> A', + 'UNSAFE_componentWillUpdate: A -> A', + 'A2', + ]); + } + expect(root).toMatchRenderedOutput('A2'); root.update( diff --git a/packages/react-reconciler/src/__tests__/ReactSchedulerIntegration-test.internal.js b/packages/react-reconciler/src/__tests__/ReactSchedulerIntegration-test.internal.js new file mode 100644 index 0000000000000..153d93692a612 --- /dev/null +++ b/packages/react-reconciler/src/__tests__/ReactSchedulerIntegration-test.internal.js @@ -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(); + expect(Scheduler).toFlushAndYield(['Priority: Normal']); + + runWithPriority(UserBlockingPriority, () => { + ReactNoop.render(); + }); + expect(Scheduler).toFlushAndYield(['Priority: UserBlocking']); + + runWithPriority(IdlePriority, () => { + ReactNoop.render(); + }); + 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( + + + + + , + ); + }); + + // 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(); + 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(); + expect(Scheduler).toFlushAndYield([ + 'Render priority: Normal', + 'Passive priority: Normal', + ]); + + runWithPriority(UserBlockingPriority, () => { + ReactNoop.render(); + }); + + expect(Scheduler).toFlushAndYield([ + 'Render priority: UserBlocking', + 'Passive priority: UserBlocking', + ]); + }); + + // TODO + it.skip('passive effects have render priority even if they are flushed early', () => {}); +}); diff --git a/packages/react-reconciler/src/__tests__/ReactSuspense-test.internal.js b/packages/react-reconciler/src/__tests__/ReactSuspense-test.internal.js index 14fe319b7fe43..1a23bdf09303a 100644 --- a/packages/react-reconciler/src/__tests__/ReactSuspense-test.internal.js +++ b/packages/react-reconciler/src/__tests__/ReactSuspense-test.internal.js @@ -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; - expect(Scheduler).toHaveYielded(['Hi', 'Did mount: Hi']); + if (enableNewScheduler) { + expect(Scheduler).toFlushExpired(['Hi', 'Did mount: Hi']); + } else { + expect(Scheduler).toHaveYielded(['Hi', 'Did mount: Hi']); + } expect(root).toMatchRenderedOutput('Hi'); }); @@ -393,13 +399,24 @@ describe('ReactSuspense', () => { expect(root).toMatchRenderedOutput('Loading...'); jest.advanceTimersByTime(100); - expect(Scheduler).toHaveYielded([ - 'Promise resolved [B:1]', - 'B:1', - 'Unmount [Loading...]', - // Should be a mount, not an update - 'Mount [B:1]', - ]); + + 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', + 'Unmount [Loading...]', + // Should be a mount, not an update + 'Mount [B:1]', + ]); + } expect(root).toMatchRenderedOutput('AB:1C'); @@ -413,12 +430,21 @@ describe('ReactSuspense', () => { jest.advanceTimersByTime(100); - expect(Scheduler).toHaveYielded([ - 'Promise resolved [B:2]', - 'B:2', - 'Unmount [Loading...]', - 'Update [B:2]', - ]); + 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); - expect(Scheduler).toHaveYielded(['Promise resolved [A]', 'A']); + + 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(); @@ -466,7 +499,14 @@ describe('ReactSuspense', () => { expect(root).toMatchRenderedOutput('Loading...'); jest.advanceTimersByTime(1000); - expect(Scheduler).toHaveYielded(['Promise resolved [B]', 'B']); + + 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); - expect(Scheduler).toHaveYielded(['Promise resolved [A]', 'A']); + + 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(); @@ -529,7 +575,14 @@ describe('ReactSuspense', () => { expect(root).toMatchRenderedOutput('Loading...'); jest.advanceTimersByTime(1000); - expect(Scheduler).toHaveYielded(['Promise resolved [B]', 'B']); + + 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(); expect(Scheduler).toHaveYielded(['Suspend! [A]', 'Loading...']); jest.advanceTimersByTime(500); - expect(Scheduler).toHaveYielded([ - 'Promise resolved [A]', - 'A', - 'Did commit: A', - ]); + + 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); - expect(Scheduler).toHaveYielded([ - 'Promise resolved [Child 1]', - 'Child 1', - 'Suspend! [Child 2]', - 'Suspend! [Child 3]', - ]); + 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); - expect(Scheduler).toHaveYielded([ - 'Promise resolved [Child 2]', - 'Child 2', - 'Suspend! [Child 3]', - ]); + 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); - expect(Scheduler).toHaveYielded([ - 'Promise resolved [Child 3]', - 'Child 3', - ]); + 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); - expect(Scheduler).toHaveYielded(['Promise resolved [A:0]', 'A:0']); + + 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,34 +982,65 @@ describe('ReactSuspense', () => { // Resolve A jest.advanceTimersByTime(1000); - expect(Scheduler).toHaveYielded([ - 'Promise resolved [A]', - 'A', - // The promises for B and C have now been thrown twice - 'Suspend! [B]', - 'Suspend! [C]', - ]); + 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', + // The promises for B and C have now been thrown twice + 'Suspend! [B]', + 'Suspend! [C]', + ]); + } // Resolve B jest.advanceTimersByTime(1000); - expect(Scheduler).toHaveYielded([ - 'Promise resolved [B]', - // 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]', - ]); + 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 + // re-render once. + 'B', + // The promise for C has now been thrown three times + 'Suspend! [C]', + ]); + } // Resolve C jest.advanceTimersByTime(1000); - expect(Scheduler).toHaveYielded([ - 'Promise resolved [C]', - // Even though the promise for C was thrown three times, we should only - // re-render once. - 'C', - ]); + + 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', () => { diff --git a/packages/react-reconciler/src/__tests__/ReactSuspensePlaceholder-test.internal.js b/packages/react-reconciler/src/__tests__/ReactSuspensePlaceholder-test.internal.js index 89f25adcd3cdd..0711e244f2dce 100644 --- a/packages/react-reconciler/src/__tests__/ReactSuspensePlaceholder-test.internal.js +++ b/packages/react-reconciler/src/__tests__/ReactSuspensePlaceholder-test.internal.js @@ -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); - expect(Scheduler).toHaveYielded([ - 'Promise resolved [Loaded]', - 'Loaded', - ]); + 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); - expect(Scheduler).toHaveYielded([ - 'Promise resolved [Loaded]', - 'Loaded', - ]); + 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); diff --git a/packages/react-reconciler/src/__tests__/ReactSuspenseWithNoopRenderer-test.internal.js b/packages/react-reconciler/src/__tests__/ReactSuspenseWithNoopRenderer-test.internal.js index 18d85fadb0b1d..b68b239f80e68 100644 --- a/packages/react-reconciler/src/__tests__/ReactSuspenseWithNoopRenderer-test.internal.js +++ b/packages/react-reconciler/src/__tests__/ReactSuspenseWithNoopRenderer-test.internal.js @@ -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,15 +891,27 @@ describe('ReactSuspenseWithNoopRenderer', () => { // Initial mount. This is synchronous, because the root is sync. ReactNoop.renderLegacySyncRoot(); await advanceTimers(100); - expect(Scheduler).toHaveYielded([ - 'Suspend! [Step: 1]', - 'Sibling', - 'Loading (1)', - 'Loading (2)', - 'Loading (3)', - 'Promise resolved [Step: 1]', - 'Step: 1', - ]); + 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', + 'Loading (1)', + 'Loading (2)', + 'Loading (3)', + 'Promise resolved [Step: 1]', + 'Step: 1', + ]); + } expect(ReactNoop).toMatchRenderedOutput( @@ -920,10 +943,15 @@ describe('ReactSuspenseWithNoopRenderer', () => { ); await advanceTimers(100); - expect(Scheduler).toHaveYielded([ - 'Promise resolved [Step: 2]', - 'Step: 2', - ]); + 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( @@ -981,18 +1009,34 @@ describe('ReactSuspenseWithNoopRenderer', () => { Scheduler.yieldValue('Did mount'), ); await advanceTimers(100); - expect(Scheduler).toHaveYielded([ - 'Before', - 'Suspend! [Async: 1]', - 'After', - 'Loading...', - 'Before', - 'Sync: 1', - 'After', - 'Did mount', - 'Promise resolved [Async: 1]', - 'Async: 1', - ]); + + 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]', + 'After', + 'Loading...', + 'Before', + 'Sync: 1', + 'After', + 'Did mount', + 'Promise resolved [Async: 1]', + 'Async: 1', + ]); + } expect(ReactNoop).toMatchRenderedOutput( @@ -1046,10 +1090,16 @@ describe('ReactSuspenseWithNoopRenderer', () => { // When the placeholder is pinged, the boundary must be re-rendered // synchronously. await advanceTimers(100); - expect(Scheduler).toHaveYielded([ - 'Promise resolved [Async: 2]', - 'Async: 2', - ]); + + 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( @@ -1114,18 +1164,33 @@ describe('ReactSuspenseWithNoopRenderer', () => { Scheduler.yieldValue('Did mount'), ); await advanceTimers(100); - expect(Scheduler).toHaveYielded([ - 'Before', - 'Suspend! [Async: 1]', - 'After', - 'Loading...', - 'Before', - 'Sync: 1', - 'After', - 'Did mount', - 'Promise resolved [Async: 1]', - 'Async: 1', - ]); + 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]', + 'After', + 'Loading...', + 'Before', + 'Sync: 1', + 'After', + 'Did mount', + 'Promise resolved [Async: 1]', + 'Async: 1', + ]); + } expect(ReactNoop).toMatchRenderedOutput( @@ -1179,10 +1244,16 @@ describe('ReactSuspenseWithNoopRenderer', () => { // When the placeholder is pinged, the boundary must be re-rendered // synchronously. await advanceTimers(100); - expect(Scheduler).toHaveYielded([ - 'Promise resolved [Async: 2]', - 'Async: 2', - ]); + + 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( @@ -1260,7 +1331,13 @@ describe('ReactSuspenseWithNoopRenderer', () => { ReactNoop.expire(1000); await advanceTimers(1000); - expect(Scheduler).toHaveYielded(['Promise resolved [B]', 'B']); + + if (enableNewScheduler) { + expect(Scheduler).toHaveYielded(['Promise resolved [B]']); + expect(Scheduler).toFlushExpired(['B']); + } else { + expect(Scheduler).toHaveYielded(['Promise resolved [B]', 'B']); + } expect(ReactNoop).toMatchRenderedOutput( @@ -1312,12 +1389,22 @@ describe('ReactSuspenseWithNoopRenderer', () => { expect(ReactNoop.getChildren()).toEqual([span('Loading...')]); await advanceTimers(1000); - expect(Scheduler).toHaveYielded([ - 'Promise resolved [Hi]', - 'constructor', - 'Hi', - 'componentDidMount', - ]); + + 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); - expect(Scheduler).toHaveYielded(['Promise resolved [Hi]', 'Hi']); + 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); - expect(Scheduler).toHaveYielded(['Promise resolved [Hi]', 'Hi']); + 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); - expect(Scheduler).toHaveYielded(['Promise resolved [Hi]', 'Hi']); + if (enableNewScheduler) { + expect(Scheduler).toHaveYielded(['Promise resolved [Hi]']); + expect(Scheduler).toFlushExpired(['Hi']); + } else { + expect(Scheduler).toHaveYielded(['Promise resolved [Hi]', 'Hi']); + } }); } }); diff --git a/packages/react-reconciler/src/__tests__/__snapshots__/ReactIncrementalPerf-test.internal.js.snap b/packages/react-reconciler/src/__tests__/__snapshots__/ReactIncrementalPerf-test.internal.js.snap index 3f5f23c5a9bec..541307deeadc4 100644 --- a/packages/react-reconciler/src/__tests__/__snapshots__/ReactIncrementalPerf-test.internal.js.snap +++ b/packages/react-reconciler/src/__tests__/__snapshots__/ReactIncrementalPerf-test.internal.js.snap @@ -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,7 +443,508 @@ 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 + +⚛ (Committing Changes) + ⚛ (Committing Snapshot Effects: 0 Total) + ⚛ (Committing Host Effects: 1 Total) + ⚛ (Calling Lifecycle Methods: 0 Total) +" +`; + +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 +⚛ (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 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) @@ -456,7 +957,7 @@ exports[`ReactDebugFiberPerf warns if async work expires (starvation) 1`] = ` " `; -exports[`ReactDebugFiberPerf warns on cascading renders from setState 1`] = ` +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 @@ -480,7 +981,7 @@ exports[`ReactDebugFiberPerf warns on cascading renders from setState 1`] = ` " `; -exports[`ReactDebugFiberPerf warns on cascading renders from top-level render 1`] = ` +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 diff --git a/packages/react-test-renderer/src/__tests__/ReactTestRenderer-test.js b/packages/react-test-renderer/src/__tests__/ReactTestRenderer-test.js index 177ac9e11d89f..50de254aa3022 100644 --- a/packages/react-test-renderer/src/__tests__/ReactTestRenderer-test.js +++ b/packages/react-test-renderer/src/__tests__/ReactTestRenderer-test.js @@ -9,15 +9,25 @@ 'use strict'; -const ReactDOM = require('react-dom'); - -// Isolate test renderer. -jest.resetModules(); -const React = require('react'); -const ReactCache = require('react-cache'); -const ReactTestRenderer = require('react-test-renderer'); +let ReactDOM; +let React; +let ReactCache; +let ReactTestRenderer; +let Scheduler; describe('ReactTestRenderer', () => { + beforeEach(() => { + jest.resetModules(); + ReactDOM = require('react-dom'); + + // Isolate test renderer. + jest.resetModules(); + React = require('react'); + ReactCache = require('react-cache'); + ReactTestRenderer = require('react-test-renderer'); + Scheduler = require('scheduler'); + }); + 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(); PendingResources.initial('initial'); await Promise.resolve(); + Scheduler.flushAll(); expect(root.toJSON()).toEqual('initial'); root.update(); @@ -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(); PendingResources.initial('initial'); await Promise.resolve(); + Scheduler.flushAll(); expect(root.toJSON().children).toEqual(['initial']); root.update(); @@ -95,6 +108,7 @@ describe('ReactTestRenderer', () => { PendingResources.dynamic('dynamic'); await Promise.resolve(); + Scheduler.flushAll(); expect(root.toJSON().children).toEqual(['dynamic']); done(); diff --git a/packages/react/src/__tests__/ReactProfiler-test.internal.js b/packages/react/src/__tests__/ReactProfiler-test.internal.js index 8df35e979e8fd..95c3b54770db4 100644 --- a/packages/react/src/__tests__/ReactProfiler-test.internal.js +++ b/packages/react/src/__tests__/ReactProfiler-test.internal.js @@ -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'); - expect(Scheduler).toHaveYielded(['Component:text']); + 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); diff --git a/packages/scheduler/src/Scheduler.js b/packages/scheduler/src/Scheduler.js index ab533e9cd878b..339ed21964ef2 100644 --- a/packages/scheduler/src/Scheduler.js +++ b/packages/scheduler/src/Scheduler.js @@ -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; } - // Schedule the host callback using the earliest expiration in the list. - var expirationTime = firstCallbackNode.expirationTime; - if (!isHostCallbackScheduled) { - isHostCallbackScheduled = true; - } else { - // Cancel the existing host callback. - cancelHostCallback(); + if (firstCallbackNode !== null) { + // Schedule the host callback using the earliest expiration in the list. + var expirationTime = firstCallbackNode.expirationTime; + if (isHostCallbackScheduled) { + // Cancel the existing host callback. + cancelHostCallback(); + } else { + isHostCallbackScheduled = true; + } + requestHostCallback(flushWork, expirationTime); } - 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) { - // There's still work remaining. Request another callback. - ensureHostCallbackIsScheduled(); - } else { - isHostCallbackScheduled = false; - } - // Before exiting, flush all the immediate work that was scheduled. - flushImmediateWork(); + isPerformingWork = false; + currentHostCallbackDidTimeout = previousDidTimeout; + // There's still work remaining. Request another callback. + 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()) diff --git a/packages/scheduler/src/__tests__/Scheduler-test.js b/packages/scheduler/src/__tests__/Scheduler-test.js index 117daf69166ac..20cd67fa27adb 100644 --- a/packages/scheduler/src/__tests__/Scheduler-test.js +++ b/packages/scheduler/src/__tests__/Scheduler-test.js @@ -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,37 +66,31 @@ 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 => { - Scheduler.advanceTime(100); - Scheduler.yieldValue(`B (did timeout: ${didTimeout})`); - }); + scheduleCallback(UserBlockingPriority, didTimeout => { + Scheduler.advanceTime(100); + Scheduler.yieldValue(`B (did timeout: ${didTimeout})`); }); - runWithPriority(UserBlockingPriority, () => { - scheduleCallback(didTimeout => { - Scheduler.advanceTime(100); - Scheduler.yieldValue(`C (did timeout: ${didTimeout})`); - }); + scheduleCallback(UserBlockingPriority, didTimeout => { + Scheduler.advanceTime(100); + Scheduler.yieldValue(`C (did timeout: ${didTimeout})`); }); // Advance time, but not by enough to expire any work @@ -102,11 +98,11 @@ describe('Scheduler', () => { 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,14 +215,12 @@ describe('Scheduler', () => { } } }; - scheduleCallback(work); + scheduleCallback(NormalPriority, work); expect(Scheduler).toFlushAndYieldThrough(['A']); - runWithPriority(UserBlockingPriority, () => { - scheduleCallback(() => { - Scheduler.advanceTime(100); - Scheduler.yieldValue('High pri'); - }); + 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(() => { - Scheduler.advanceTime(100); - Scheduler.yieldValue('High pri'); - }), - ); + 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 - expect(Scheduler).toHaveYielded([]); - }); - // Nothing should have fired yet - expect(Scheduler).toHaveYielded([]); + 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']); + }); + + 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')); }); - // The callbacks were called at the end of the outer event - expect(Scheduler).toHaveYielded(['A', 'B', 'C']); + scheduleCallback(ImmediatePriority, () => Scheduler.yieldValue('D')); + expect(Scheduler).toHaveYielded([]); + // 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( + const wrappedCallback = runWithPriority(NormalPriority, () => + wrapCallback(() => { + Scheduler.yieldValue(getCurrentPriorityLevel()); + }), + ); + + const wrappedUserBlockingCallback = runWithPriority( UserBlockingPriority, () => wrapCallback(() => { - scheduleCallback(() => { - Scheduler.advanceTime(100); - Scheduler.yieldValue('User-blocking'); - }); + 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(); + ); }); - 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(); + wrappedCallback(); + expect(Scheduler).toHaveYielded([NormalPriority]); - // 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(() => { - Scheduler.yieldValue('A'); - throw new Error('Oops A'); - }); - scheduleCallback(() => { - Scheduler.yieldValue('B'); - }); - scheduleCallback(() => { - Scheduler.yieldValue('C'); - throw new Error('Oops C'); - }); - }); - }).toThrow('Oops A'); + scheduleCallback(ImmediatePriority, () => { + Scheduler.yieldValue('A'); + throw new Error('Oops A'); + }); + scheduleCallback(ImmediatePriority, () => { + Scheduler.yieldValue('B'); + }); + scheduleCallback(ImmediatePriority, () => { + Scheduler.yieldValue('C'); + throw new Error('Oops C'); + }); + 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, () => { diff --git a/packages/scheduler/src/__tests__/SchedulerDOM-test.js b/packages/scheduler/src/__tests__/SchedulerDOM-test.js index 7565e84454a1d..3a66657d17ee2 100644 --- a/packages/scheduler/src/__tests__/SchedulerDOM-test.js +++ b/packages/scheduler/src/__tests__/SchedulerDOM-test.js @@ -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([]); diff --git a/packages/scheduler/src/__tests__/SchedulerNoDOM-test.js b/packages/scheduler/src/__tests__/SchedulerNoDOM-test.js index 55d47037e0936..a8c45c0bc840a 100644 --- a/packages/scheduler/src/__tests__/SchedulerNoDOM-test.js +++ b/packages/scheduler/src/__tests__/SchedulerNoDOM-test.js @@ -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,19 +57,17 @@ 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(() => { - log.push('C'); - }); - scheduleCallback(() => { - log.push('D'); - }); + scheduleCallback(UserBlockingPriority, () => { + log.push('C'); + }); + scheduleCallback(UserBlockingPriority, () => { + log.push('D'); }); expect(log).toEqual([]); @@ -76,39 +75,22 @@ describe('SchedulerNoDOM', () => { expect(log).toEqual(['C', 'D', 'A', 'B']); }); - it('calls immediate callbacks immediately', () => { + it('handles errors', () => { let log = []; - runWithPriority(ImmediatePriority, () => { - scheduleCallback(() => { - log.push('A'); - scheduleCallback(() => { - log.push('B'); - }); - }); + scheduleCallback(ImmediatePriority, () => { + log.push('A'); + throw new Error('Oops A'); + }); + scheduleCallback(ImmediatePriority, () => { + log.push('B'); + }); + scheduleCallback(ImmediatePriority, () => { + log.push('C'); + throw new Error('Oops C'); }); - expect(log).toEqual(['A', 'B']); - }); - - it('handles errors', () => { - let log = []; - - expect(() => { - runWithPriority(ImmediatePriority, () => { - scheduleCallback(() => { - log.push('A'); - throw new Error('Oops A'); - }); - scheduleCallback(() => { - log.push('B'); - }); - scheduleCallback(() => { - log.push('C'); - throw new Error('Oops C'); - }); - }); - }).toThrow('Oops A'); + expect(() => jest.runAllTimers()).toThrow('Oops A'); expect(log).toEqual(['A']); diff --git a/packages/shared/forks/ReactFeatureFlags.new-scheduler.js b/packages/shared/forks/ReactFeatureFlags.new-scheduler.js new file mode 100644 index 0000000000000..a2a3bfdfe786d --- /dev/null +++ b/packages/shared/forks/ReactFeatureFlags.new-scheduler.js @@ -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; diff --git a/packages/shared/forks/ReactFeatureFlags.www-new-scheduler.js b/packages/shared/forks/ReactFeatureFlags.www-new-scheduler.js new file mode 100644 index 0000000000000..0d87b53fe3bd9 --- /dev/null +++ b/packages/shared/forks/ReactFeatureFlags.www-new-scheduler.js @@ -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); diff --git a/scripts/circleci/test_entry_point.sh b/scripts/circleci/test_entry_point.sh index 87bbad4aba9e8..6227b7c5dcbd0 100755 --- a/scripts/circleci/test_entry_point.sh +++ b/scripts/circleci/test_entry_point.sh @@ -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') diff --git a/scripts/jest/matchers/schedulerTestMatchers.js b/scripts/jest/matchers/schedulerTestMatchers.js index 4984ea42b50ca..b19a984904dfd 100644 --- a/scripts/jest/matchers/schedulerTestMatchers.js +++ b/scripts/jest/matchers/schedulerTestMatchers.js @@ -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, }; diff --git a/scripts/rollup/bundles.js b/scripts/rollup/bundles.js index 98bfdadcd6a9d..3ba276130135f 100644 --- a/scripts/rollup/bundles.js +++ b/scripts/rollup/bundles.js @@ -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, diff --git a/scripts/rollup/forks.js b/scripts/rollup/forks.js index c93ac87559aaa..41d0f9bce6cd9 100644 --- a/scripts/rollup/forks.js +++ b/scripts/rollup/forks.js @@ -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: diff --git a/scripts/shared/inlinedHostConfigs.js b/scripts/shared/inlinedHostConfigs.js index 66c54963c7f28..02c131a847a61 100644 --- a/scripts/shared/inlinedHostConfigs.js +++ b/scripts/shared/inlinedHostConfigs.js @@ -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, },