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, },