diff --git a/packages/react-reconciler/src/ReactFiberCommitWork.js b/packages/react-reconciler/src/ReactFiberCommitWork.js index 85cd0a5791b..d81c5cafeb6 100644 --- a/packages/react-reconciler/src/ReactFiberCommitWork.js +++ b/packages/react-reconciler/src/ReactFiberCommitWork.js @@ -23,11 +23,7 @@ import type {FunctionComponentUpdateQueue} from './ReactFiberHooks'; import type {Thenable} from './ReactFiberScheduler'; import {unstable_wrap as Schedule_tracing_wrap} from 'scheduler/tracing'; -import { - enableHooks, - enableSchedulerTracing, - enableProfilerTimer, -} from 'shared/ReactFeatureFlags'; +import {enableHooks, enableProfilerTimer} from 'shared/ReactFeatureFlags'; import { FunctionComponent, ForwardRef, @@ -551,26 +547,15 @@ function commitLifeCycles( if (enableProfilerTimer) { const onRender = finishedWork.memoizedProps.onRender; - if (enableSchedulerTracing) { - onRender( - finishedWork.memoizedProps.id, - current === null ? 'mount' : 'update', - finishedWork.actualDuration, - finishedWork.treeBaseDuration, - finishedWork.actualStartTime, - getCommitTime(), - finishedRoot.memoizedInteractions, - ); - } else { - onRender( - finishedWork.memoizedProps.id, - current === null ? 'mount' : 'update', - finishedWork.actualDuration, - finishedWork.treeBaseDuration, - finishedWork.actualStartTime, - getCommitTime(), - ); - } + onRender( + finishedWork.memoizedProps.id, + current === null ? 'mount' : 'update', + finishedWork.actualDuration, + finishedWork.treeBaseDuration, + finishedWork.actualStartTime, + getCommitTime(), + finishedRoot.memoizedInteractions, + ); } return; } @@ -1198,10 +1183,9 @@ function commitWork(current: Fiber | null, finishedWork: Fiber): void { } thenables.forEach(thenable => { // Memoize using the boundary fiber to prevent redundant listeners. - let retry = retryTimedOutBoundary.bind(null, finishedWork, thenable); - if (enableSchedulerTracing) { - retry = Schedule_tracing_wrap(retry); - } + let retry = Schedule_tracing_wrap( + retryTimedOutBoundary.bind(null, finishedWork, thenable), + ); if (!retryCache.has(thenable)) { retryCache.add(thenable); thenable.then(retry, retry); diff --git a/packages/react-reconciler/src/ReactFiberRoot.js b/packages/react-reconciler/src/ReactFiberRoot.js index e3e445b46ef..7998f18ab9c 100644 --- a/packages/react-reconciler/src/ReactFiberRoot.js +++ b/packages/react-reconciler/src/ReactFiberRoot.js @@ -16,7 +16,6 @@ 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 {unstable_getThreadID} from 'scheduler/tracing'; // TODO: This should be lifted into the renderer. @@ -29,7 +28,7 @@ export type Batch = { export type PendingInteractionMap = Map>; -type BaseFiberRootProperties = {| +export type FiberRoot = {| // Any additional information from the host associated with this root. containerInfo: any, // Used only by persistent updates. @@ -83,28 +82,13 @@ type BaseFiberRootProperties = {| firstBatch: Batch | null, // Linked-list of roots nextScheduledRoot: FiberRoot | null, -|}; -// The following attributes are only used by interaction tracing builds. -// They enable interactions to be associated with their async work, -// And expose interaction metadata to the React DevTools Profiler plugin. -// Note that these attributes are only defined when the enableSchedulerTracing flag is enabled. -type ProfilingOnlyFiberRootProperties = {| + // Used in support of interaction tracing interactionThreadID: number, memoizedInteractions: Set, pendingInteractionMap: PendingInteractionMap, |}; -// Exported FiberRoot type includes all properties, -// To avoid requiring potentially error-prone :any casts throughout the project. -// Profiling properties are only safe to access in profiling builds (when enableSchedulerTracing is true). -// The types are defined separately within this file to ensure they stay in sync. -// (We don't have to use an inline :any cast when enableSchedulerTracing is disabled.) -export type FiberRoot = { - ...BaseFiberRootProperties, - ...ProfilingOnlyFiberRootProperties, -}; - export function createFiberRoot( containerInfo: any, isConcurrent: boolean, @@ -115,65 +99,36 @@ export function createFiberRoot( 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, + 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); uninitializedFiber.stateNode = root; diff --git a/packages/react-reconciler/src/ReactFiberScheduler.js b/packages/react-reconciler/src/ReactFiberScheduler.js index a63bfd56b79..96d3947b8f9 100644 --- a/packages/react-reconciler/src/ReactFiberScheduler.js +++ b/packages/react-reconciler/src/ReactFiberScheduler.js @@ -57,7 +57,6 @@ import { } from 'shared/ReactWorkTags'; import { enableHooks, - enableSchedulerTracing, enableProfilerTimer, enableUserTimingAPI, replayFailedUnitOfWorkWithInvokeGuardedCallback, @@ -178,17 +177,15 @@ let didWarnSetStateChildContext; let warnAboutUpdateOnUnmounted; let warnAboutInvalidUpdates; -if (enableSchedulerTracing) { - // Provide explicit error message when production+profiling bundle of e.g. react-dom - // is used with production (non-profiling) bundle of scheduler/tracing - invariant( - __interactionsRef != null && __interactionsRef.current != null, - 'It is not supported to run the profiling version of a renderer (for example, `react-dom/profiling`) ' + - 'without also replacing the `scheduler/tracing` module with `scheduler/tracing-profiling`. ' + - 'Your bundler might have a setting for aliasing both modules. ' + - 'Learn more at http://fb.me/react-profiling', - ); -} +// Provide explicit error message when production+profiling bundle of e.g. react-dom +// is used with production (non-profiling) bundle of scheduler/tracing +invariant( + __interactionsRef != null && __interactionsRef.current != null, + 'It is not supported to run the profiling version of a renderer (for example, `react-dom/profiling`) ' + + 'without also replacing the `scheduler/tracing` module with `scheduler/tracing-profiling`. ' + + 'Your bundler might have a setting for aliasing both modules. ' + + 'Learn more at http://fb.me/react-profiling', +); if (__DEV__) { didWarnAboutStateTransition = false; @@ -613,13 +610,10 @@ function commitRoot(root: FiberRoot, finishedWork: Fiber): void { : updateExpirationTimeBeforeCommit; markCommittedPriorityLevels(root, earliestRemainingTimeBeforeCommit); - let prevInteractions: Set = (null: any); - if (enableSchedulerTracing) { - // Restore any pending interactions at this point, - // So that cascading work triggered during the render phase will be accounted for. - prevInteractions = __interactionsRef.current; - __interactionsRef.current = root.memoizedInteractions; - } + // Restore any pending interactions at this point, + // So that cascading work triggered during the render phase will be accounted for. + let prevInteractions: Set = __interactionsRef.current; + __interactionsRef.current = root.memoizedInteractions; // Reset this to null before calling lifecycles ReactCurrentOwner.current = null; @@ -780,13 +774,12 @@ function commitRoot(root: FiberRoot, finishedWork: Fiber): void { // after the next paint. Schedule an callback to fire them in an async // event. To ensure serial execution, the callback will be flushed early if // we enter rootWithPendingPassiveEffects commit phase before then. - let callback = commitPassiveEffects.bind(null, root, firstEffect); - if (enableSchedulerTracing) { - // TODO: Avoid this extra callback by mutating the tracing ref directly, - // like we do at the beginning of commitRoot. I've opted not to do that - // here because that code is still in flux. - callback = Schedule_tracing_wrap(callback); - } + // TODO: Avoid this extra callback by mutating the tracing ref directly, + // like we do at the beginning of commitRoot. I've opted not to do that + // here because that code is still in flux. + let callback = Schedule_tracing_wrap( + commitPassiveEffects.bind(null, root, firstEffect), + ); passiveEffectCallbackHandle = Schedule_scheduleCallback(callback); passiveEffectCallback = callback; } @@ -813,60 +806,58 @@ function commitRoot(root: FiberRoot, finishedWork: Fiber): void { } onCommit(root, earliestRemainingTimeAfterCommit); - if (enableSchedulerTracing) { - __interactionsRef.current = prevInteractions; + __interactionsRef.current = prevInteractions; - let subscriber; + let subscriber; - try { - subscriber = __subscriberRef.current; - if (subscriber !== null && root.memoizedInteractions.size > 0) { - const threadID = computeThreadID( - committedExpirationTime, - root.interactionThreadID, - ); - subscriber.onWorkStopped(root.memoizedInteractions, threadID); - } - } catch (error) { - // It's not safe for commitRoot() to throw. - // Store the error for now and we'll re-throw in finishRendering(). - if (!hasUnhandledError) { - hasUnhandledError = true; - unhandledError = 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) { - // It's not safe for commitRoot() to throw. - // Store the error for now and we'll re-throw in finishRendering(). - if (!hasUnhandledError) { - hasUnhandledError = true; - unhandledError = error; - } - } - } - }); - } - }, + try { + subscriber = __subscriberRef.current; + if (subscriber !== null && root.memoizedInteractions.size > 0) { + const threadID = computeThreadID( + committedExpirationTime, + root.interactionThreadID, ); + subscriber.onWorkStopped(root.memoizedInteractions, threadID); } + } catch (error) { + // It's not safe for commitRoot() to throw. + // Store the error for now and we'll re-throw in finishRendering(). + if (!hasUnhandledError) { + hasUnhandledError = true; + unhandledError = 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) { + // It's not safe for commitRoot() to throw. + // Store the error for now and we'll re-throw in finishRendering(). + if (!hasUnhandledError) { + hasUnhandledError = true; + unhandledError = error; + } + } + } + }); + } + }, + ); } } @@ -1234,57 +1225,52 @@ function renderRoot(root: FiberRoot, isYieldy: boolean): void { ); root.pendingCommitExpirationTime = NoWork; - if (enableSchedulerTracing) { - // 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), - ); - } - }, - ); + // 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; + // 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( - expirationTime, - root.interactionThreadID, - ); - try { - subscriber.onWorkStarted(interactions, threadID); - } catch (error) { - // Work thrown by an interaction tracing subscriber should be rethrown, - // But only once it's safe (to avoid leaveing the scheduler in an invalid state). - // Store the error for now and we'll re-throw in finishRendering(). - if (!hasUnhandledError) { - hasUnhandledError = true; - unhandledError = error; - } + if (interactions.size > 0) { + const subscriber = __subscriberRef.current; + if (subscriber !== null) { + const threadID = computeThreadID( + expirationTime, + root.interactionThreadID, + ); + try { + subscriber.onWorkStarted(interactions, threadID); + } catch (error) { + // Work thrown by an interaction tracing subscriber should be rethrown, + // But only once it's safe (to avoid leaveing the scheduler in an invalid state). + // Store the error for now and we'll re-throw in finishRendering(). + if (!hasUnhandledError) { + hasUnhandledError = true; + unhandledError = error; } } } } } - let prevInteractions: Set = (null: any); - if (enableSchedulerTracing) { - // We're about to start new traced work. - // Restore pending interactions so cascading work triggered during the render phase will be accounted for. - prevInteractions = __interactionsRef.current; - __interactionsRef.current = root.memoizedInteractions; - } + // We're about to start new traced work. + // Restore pending interactions so cascading work triggered during the render phase will be accounted for. + let prevInteractions: Set = __interactionsRef.current; + __interactionsRef.current = root.memoizedInteractions; let didFatal = false; @@ -1366,10 +1352,8 @@ function renderRoot(root: FiberRoot, isYieldy: boolean): void { break; } while (true); - if (enableSchedulerTracing) { - // Traced work is done for now; restore the previous interactions. - __interactionsRef.current = prevInteractions; - } + // Traced work is done for now; restore the previous interactions. + __interactionsRef.current = prevInteractions; // We're done performing work. Time to clean up. isWorking = false; @@ -1747,41 +1731,40 @@ function scheduleWorkToRoot(fiber: Fiber, expirationTime): FiberRoot | null { } } - if (enableSchedulerTracing) { - if (root !== null) { - 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++; - } + if (root !== null) { + 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)); + 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++; - }); - } + // 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( - expirationTime, - root.interactionThreadID, - ); - subscriber.onWorkScheduled(interactions, threadID); - } + const subscriber = __subscriberRef.current; + if (subscriber !== null) { + const threadID = computeThreadID( + expirationTime, + root.interactionThreadID, + ); + subscriber.onWorkScheduled(interactions, threadID); } } } + return root; } diff --git a/packages/react-reconciler/src/ReactFiberUnwindWork.js b/packages/react-reconciler/src/ReactFiberUnwindWork.js index 86e2b1a964a..7c33586e2ab 100644 --- a/packages/react-reconciler/src/ReactFiberUnwindWork.js +++ b/packages/react-reconciler/src/ReactFiberUnwindWork.js @@ -34,7 +34,6 @@ import { ShouldCapture, LifecycleEffectMask, } from 'shared/ReactSideEffectTags'; -import {enableSchedulerTracing} from 'shared/ReactFeatureFlags'; import {ConcurrentMode} from './ReactTypeOfMode'; import {shouldCaptureSuspense} from './ReactFiberSuspenseComponent'; @@ -288,9 +287,7 @@ function throwException( thenable, renderExpirationTime, ); - if (enableSchedulerTracing) { - ping = Schedule_tracing_wrap(ping); - } + ping = Schedule_tracing_wrap(ping); thenable.then(ping, ping); } diff --git a/packages/react-reconciler/src/__tests__/ReactHooksWithNoopRenderer-test.internal.js b/packages/react-reconciler/src/__tests__/ReactHooksWithNoopRenderer-test.internal.js index ea567214746..067030dadb0 100644 --- a/packages/react-reconciler/src/__tests__/ReactHooksWithNoopRenderer-test.internal.js +++ b/packages/react-reconciler/src/__tests__/ReactHooksWithNoopRenderer-test.internal.js @@ -60,7 +60,7 @@ describe('ReactHooksWithNoopRenderer', () => { ReactFeatureFlags = require('shared/ReactFeatureFlags'); ReactFeatureFlags.debugRenderPhaseSideEffectsForStrictMode = false; ReactFeatureFlags.enableHooks = true; - ReactFeatureFlags.enableSchedulerTracing = true; + React = require('react'); ReactNoop = require('react-noop-renderer'); SchedulerTracing = require('scheduler/tracing'); diff --git a/packages/react-reconciler/src/__tests__/ReactTracing-test.internal.js b/packages/react-reconciler/src/__tests__/ReactTracing-test.internal.js deleted file mode 100644 index 2f45ad48caf..00000000000 --- a/packages/react-reconciler/src/__tests__/ReactTracing-test.internal.js +++ /dev/null @@ -1,28 +0,0 @@ -/** - * 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'; - -describe('ReactTracing', () => { - it('should error if profiling renderer and non-profiling scheduler/tracing bundles are combined', () => { - jest.resetModules(); - - const ReactFeatureFlags = require('shared/ReactFeatureFlags'); - ReactFeatureFlags.enableSchedulerTracing = false; - - require('scheduler/tracing'); - - ReactFeatureFlags.enableSchedulerTracing = true; - - expect(() => require('react-dom')).toThrow( - 'Learn more at http://fb.me/react-profiling', - ); - }); -}); diff --git a/packages/react/src/__tests__/ReactProfiler-test.internal.js b/packages/react/src/__tests__/ReactProfiler-test.internal.js index f98c7fbfa33..cc234e12960 100644 --- a/packages/react/src/__tests__/ReactProfiler-test.internal.js +++ b/packages/react/src/__tests__/ReactProfiler-test.internal.js @@ -26,7 +26,6 @@ let resourcePromise; function loadModules({ enableProfilerTimer = true, - enableSchedulerTracing = true, replayFailedUnitOfWorkWithInvokeGuardedCallback = false, useNoopRenderer = false, } = {}) { @@ -40,7 +39,6 @@ function loadModules({ ReactFeatureFlags.debugRenderPhaseSideEffects = false; ReactFeatureFlags.debugRenderPhaseSideEffectsForStrictMode = false; ReactFeatureFlags.enableProfilerTimer = enableProfilerTimer; - ReactFeatureFlags.enableSchedulerTracing = enableSchedulerTracing; ReactFeatureFlags.replayFailedUnitOfWorkWithInvokeGuardedCallback = replayFailedUnitOfWorkWithInvokeGuardedCallback; React = require('react'); @@ -127,1075 +125,1067 @@ const mockDevToolsForTest = () => { describe('Profiler', () => { describe('works in profiling and non-profiling bundles', () => { - [true, false].forEach(enableSchedulerTracing => { - [true, false].forEach(enableProfilerTimer => { - describe(`enableSchedulerTracing:${ - enableSchedulerTracing ? 'enabled' : 'disabled' - } enableProfilerTimer:${ - enableProfilerTimer ? 'enabled' : 'disabled' - }`, () => { - beforeEach(() => { - jest.resetModules(); - - loadModules({enableSchedulerTracing, enableProfilerTimer}); - }); + [true, false].forEach(enableProfilerTimer => { + describe(`enableProfilerTimer:${ + enableProfilerTimer ? 'enabled' : 'disabled' + }`, () => { + beforeEach(() => { + jest.resetModules(); + + loadModules({enableProfilerTimer}); + }); - // This will throw in production too, - // But the test is only interested in verifying the DEV error message. - if (__DEV__ && enableProfilerTimer) { - it('should warn if required params are missing', () => { + // This will throw in production too, + // But the test is only interested in verifying the DEV error message. + if (__DEV__ && enableProfilerTimer) { + it('should warn if required params are missing', () => { + expect(() => { expect(() => { - expect(() => { - ReactTestRenderer.create(); - }).toThrow('onRender is not a function'); - }).toWarnDev( - 'Profiler must specify an "id" string and "onRender" function as props', - {withoutStack: true}, - ); - }); - } - - it('should support an empty Profiler (with no children)', () => { - // As root - expect( - ReactTestRenderer.create( - , - ).toJSON(), - ).toMatchSnapshot(); - - // As non-root - expect( - ReactTestRenderer.create( -
- -
, - ).toJSON(), - ).toMatchSnapshot(); + ReactTestRenderer.create(); + }).toThrow('onRender is not a function'); + }).toWarnDev( + 'Profiler must specify an "id" string and "onRender" function as props', + {withoutStack: true}, + ); }); + } - it('should render children', () => { - const FunctionComponent = ({label}) => {label}; - const renderer = ReactTestRenderer.create( + it('should support an empty Profiler (with no children)', () => { + // As root + expect( + ReactTestRenderer.create( + , + ).toJSON(), + ).toMatchSnapshot(); + + // As non-root + expect( + ReactTestRenderer.create(
- outside span - - inside span - - +
, - ); - expect(renderer.toJSON()).toMatchSnapshot(); - }); + ).toJSON(), + ).toMatchSnapshot(); + }); - it('should support nested Profilers', () => { - const FunctionComponent = ({label}) =>
{label}
; - class ClassComponent extends React.Component { - render() { - return {this.props.label}; - } + it('should render children', () => { + const FunctionComponent = ({label}) => {label}; + const renderer = ReactTestRenderer.create( +
+ outside span + + inside span + + +
, + ); + expect(renderer.toJSON()).toMatchSnapshot(); + }); + + it('should support nested Profilers', () => { + const FunctionComponent = ({label}) =>
{label}
; + class ClassComponent extends React.Component { + render() { + return {this.props.label}; } - const renderer = ReactTestRenderer.create( - - - - - inner span - - , - ); - expect(renderer.toJSON()).toMatchSnapshot(); - }); + } + const renderer = ReactTestRenderer.create( + + + + + inner span + + , + ); + expect(renderer.toJSON()).toMatchSnapshot(); }); }); }); }); - [true, false].forEach(enableSchedulerTracing => { - describe('onRender callback', () => { - beforeEach(() => { - jest.resetModules(); + describe('onRender callback', () => { + beforeEach(() => { + jest.resetModules(); - loadModules({enableSchedulerTracing}); - }); + loadModules(); + }); - it('should handle errors thrown', () => { - const callback = jest.fn(id => { - if (id === 'throw') { - throw Error('expected'); - } - }); + it('should handle errors thrown', () => { + const callback = jest.fn(id => { + if (id === 'throw') { + throw Error('expected'); + } + }); - let didMount = false; - class ClassComponent extends React.Component { - componentDidMount() { - didMount = true; - } - render() { - return this.props.children; - } + let didMount = false; + class ClassComponent extends React.Component { + componentDidMount() { + didMount = true; } + render() { + return this.props.children; + } + } - // Errors thrown from onRender should not break the commit phase, - // Or prevent other lifecycles from being called. - expect(() => - ReactTestRenderer.create( - - - -
- + // Errors thrown from onRender should not break the commit phase, + // Or prevent other lifecycles from being called. + expect(() => + ReactTestRenderer.create( + + + +
- , - ), - ).toThrow('expected'); - expect(didMount).toBe(true); - expect(callback).toHaveBeenCalledTimes(2); - }); + + , + ), + ).toThrow('expected'); + expect(didMount).toBe(true); + expect(callback).toHaveBeenCalledTimes(2); + }); - it('is not invoked until the commit phase', () => { - const callback = jest.fn(); + it('is not invoked until the commit phase', () => { + const callback = jest.fn(); - const Yield = ({value}) => { - ReactTestRenderer.unstable_yield(value); - return null; - }; + const Yield = ({value}) => { + ReactTestRenderer.unstable_yield(value); + return null; + }; - const renderer = ReactTestRenderer.create( - - - - , - { - unstable_isConcurrent: true, - }, - ); + const renderer = ReactTestRenderer.create( + + + + , + { + unstable_isConcurrent: true, + }, + ); - // Times are logged until a render is committed. - expect(renderer).toFlushAndYieldThrough(['first']); - expect(callback).toHaveBeenCalledTimes(0); - expect(renderer).toFlushAndYield(['last']); - expect(callback).toHaveBeenCalledTimes(1); - }); + // Times are logged until a render is committed. + expect(renderer).toFlushAndYieldThrough(['first']); + expect(callback).toHaveBeenCalledTimes(0); + expect(renderer).toFlushAndYield(['last']); + expect(callback).toHaveBeenCalledTimes(1); + }); - it('does not record times for components outside of Profiler tree', () => { - ReactTestRenderer.create( -
- - - - - -
, - ); + it('does not record times for components outside of Profiler tree', () => { + ReactTestRenderer.create( +
+ + + + + +
, + ); - // Should be called two times: - // 2. To compute the update expiration time - // 3. To record the commit time - // No additional calls from ProfilerTimer are expected. - expect(mockNow).toHaveBeenCalledTimes(2); - }); + // Should be called two times: + // 2. To compute the update expiration time + // 3. To record the commit time + // No additional calls from ProfilerTimer are expected. + expect(mockNow).toHaveBeenCalledTimes(2); + }); - it('logs render times for both mount and update', () => { - const callback = jest.fn(); + it('logs render times for both mount and update', () => { + const callback = jest.fn(); - advanceTimeBy(5); // 0 -> 5 + advanceTimeBy(5); // 0 -> 5 - const renderer = ReactTestRenderer.create( - - - , - ); + const renderer = ReactTestRenderer.create( + + + , + ); - expect(callback).toHaveBeenCalledTimes(1); + expect(callback).toHaveBeenCalledTimes(1); - let [call] = callback.mock.calls; + let [call] = callback.mock.calls; - expect(call).toHaveLength(enableSchedulerTracing ? 7 : 6); - expect(call[0]).toBe('test'); - expect(call[1]).toBe('mount'); - expect(call[2]).toBe(10); // actual time - expect(call[3]).toBe(10); // base time - expect(call[4]).toBe(5); // start time - expect(call[5]).toBe(15); // commit time - expect(call[6]).toEqual(enableSchedulerTracing ? new Set() : undefined); // interaction events + expect(call).toHaveLength(7); + expect(call[0]).toBe('test'); + expect(call[1]).toBe('mount'); + expect(call[2]).toBe(10); // actual time + expect(call[3]).toBe(10); // base time + expect(call[4]).toBe(5); // start time + expect(call[5]).toBe(15); // commit time + expect(call[6]).toEqual(new Set()); // interaction events - callback.mockReset(); + callback.mockReset(); - advanceTimeBy(20); // 15 -> 35 + advanceTimeBy(20); // 15 -> 35 - renderer.update( - - - , - ); + renderer.update( + + + , + ); - expect(callback).toHaveBeenCalledTimes(1); + expect(callback).toHaveBeenCalledTimes(1); - [call] = callback.mock.calls; + [call] = callback.mock.calls; - expect(call).toHaveLength(enableSchedulerTracing ? 7 : 6); - expect(call[0]).toBe('test'); - expect(call[1]).toBe('update'); - expect(call[2]).toBe(10); // actual time - expect(call[3]).toBe(10); // base time - expect(call[4]).toBe(35); // start time - expect(call[5]).toBe(45); // commit time - expect(call[6]).toEqual(enableSchedulerTracing ? new Set() : undefined); // interaction events + expect(call).toHaveLength(7); + expect(call[0]).toBe('test'); + expect(call[1]).toBe('update'); + expect(call[2]).toBe(10); // actual time + expect(call[3]).toBe(10); // base time + expect(call[4]).toBe(35); // start time + expect(call[5]).toBe(45); // commit time + expect(call[6]).toEqual(new Set()); // interaction events - callback.mockReset(); + callback.mockReset(); - advanceTimeBy(20); // 45 -> 65 + advanceTimeBy(20); // 45 -> 65 - renderer.update( + renderer.update( + + + , + ); + + expect(callback).toHaveBeenCalledTimes(1); + + [call] = callback.mock.calls; + + expect(call).toHaveLength(7); + expect(call[0]).toBe('test'); + expect(call[1]).toBe('update'); + expect(call[2]).toBe(4); // actual time + expect(call[3]).toBe(4); // base time + expect(call[4]).toBe(65); // start time + expect(call[5]).toBe(69); // commit time + expect(call[6]).toEqual(new Set()); // interaction events + }); + + it('includes render times of nested Profilers in their parent times', () => { + const callback = jest.fn(); + + advanceTimeBy(5); // 0 -> 5 + + ReactTestRenderer.create( + + + + + + + + + , + ); + + expect(callback).toHaveBeenCalledTimes(2); + + // Callbacks bubble (reverse order). + const [childCall, parentCall] = callback.mock.calls; + expect(childCall[0]).toBe('child'); + expect(parentCall[0]).toBe('parent'); + + // Parent times should include child times + expect(childCall[2]).toBe(20); // actual time + expect(childCall[3]).toBe(20); // base time + expect(childCall[4]).toBe(15); // start time + expect(childCall[5]).toBe(35); // commit time + expect(parentCall[2]).toBe(30); // actual time + expect(parentCall[3]).toBe(30); // base time + expect(parentCall[4]).toBe(5); // start time + expect(parentCall[5]).toBe(35); // commit time + }); + + it('traces sibling Profilers separately', () => { + const callback = jest.fn(); + + advanceTimeBy(5); // 0 -> 5 + + ReactTestRenderer.create( + + + + + + + + , + ); + + expect(callback).toHaveBeenCalledTimes(2); + + const [firstCall, secondCall] = callback.mock.calls; + expect(firstCall[0]).toBe('first'); + expect(secondCall[0]).toBe('second'); + + // Parent times should include child times + expect(firstCall[2]).toBe(20); // actual time + expect(firstCall[3]).toBe(20); // base time + expect(firstCall[4]).toBe(5); // start time + expect(firstCall[5]).toBe(30); // commit time + expect(secondCall[2]).toBe(5); // actual time + expect(secondCall[3]).toBe(5); // base time + expect(secondCall[4]).toBe(25); // start time + expect(secondCall[5]).toBe(30); // commit time + }); + + it('does not include time spent outside of profile root', () => { + const callback = jest.fn(); + + advanceTimeBy(5); // 0 -> 5 + + ReactTestRenderer.create( + + - - , - ); + + + + , + ); - expect(callback).toHaveBeenCalledTimes(1); + expect(callback).toHaveBeenCalledTimes(1); - [call] = callback.mock.calls; + const [call] = callback.mock.calls; + expect(call[0]).toBe('test'); + expect(call[2]).toBe(5); // actual time + expect(call[3]).toBe(5); // base time + expect(call[4]).toBe(25); // start time + expect(call[5]).toBe(50); // commit time + }); - expect(call).toHaveLength(enableSchedulerTracing ? 7 : 6); - expect(call[0]).toBe('test'); - expect(call[1]).toBe('update'); - expect(call[2]).toBe(4); // actual time - expect(call[3]).toBe(4); // base time - expect(call[4]).toBe(65); // start time - expect(call[5]).toBe(69); // commit time - expect(call[6]).toEqual(enableSchedulerTracing ? new Set() : undefined); // interaction events - }); + it('is not called when blocked by sCU false', () => { + const callback = jest.fn(); - it('includes render times of nested Profilers in their parent times', () => { - const callback = jest.fn(); + let instance; + class Updater extends React.Component { + state = {}; + render() { + instance = this; + return this.props.children; + } + } - advanceTimeBy(5); // 0 -> 5 + class Pure extends React.PureComponent { + render() { + return this.props.children; + } + } - ReactTestRenderer.create( - - - - - + const renderer = ReactTestRenderer.create( + + + + + +
- + - , - ); + + , + ); - expect(callback).toHaveBeenCalledTimes(2); + // All profile callbacks are called for initial render + expect(callback).toHaveBeenCalledTimes(3); + + callback.mockReset(); - // Callbacks bubble (reverse order). - const [childCall, parentCall] = callback.mock.calls; - expect(childCall[0]).toBe('child'); - expect(parentCall[0]).toBe('parent'); - - // Parent times should include child times - expect(childCall[2]).toBe(20); // actual time - expect(childCall[3]).toBe(20); // base time - expect(childCall[4]).toBe(15); // start time - expect(childCall[5]).toBe(35); // commit time - expect(parentCall[2]).toBe(30); // actual time - expect(parentCall[3]).toBe(30); // base time - expect(parentCall[4]).toBe(5); // start time - expect(parentCall[5]).toBe(35); // commit time + renderer.unstable_flushSync(() => { + instance.setState({ + count: 1, + }); }); - it('traces sibling Profilers separately', () => { - const callback = jest.fn(); + // Only call profile updates for paths that have re-rendered + // Since "inner" is beneath a pure component, it isn't called + expect(callback).toHaveBeenCalledTimes(2); + expect(callback.mock.calls[0][0]).toBe('middle'); + expect(callback.mock.calls[1][0]).toBe('outer'); + }); - advanceTimeBy(5); // 0 -> 5 + it('decreases actual time but not base time when sCU prevents an update', () => { + const callback = jest.fn(); - ReactTestRenderer.create( - - - - - - - - , - ); + advanceTimeBy(5); // 0 -> 5 - expect(callback).toHaveBeenCalledTimes(2); + const renderer = ReactTestRenderer.create( + + + + + , + ); - const [firstCall, secondCall] = callback.mock.calls; - expect(firstCall[0]).toBe('first'); - expect(secondCall[0]).toBe('second'); - - // Parent times should include child times - expect(firstCall[2]).toBe(20); // actual time - expect(firstCall[3]).toBe(20); // base time - expect(firstCall[4]).toBe(5); // start time - expect(firstCall[5]).toBe(30); // commit time - expect(secondCall[2]).toBe(5); // actual time - expect(secondCall[3]).toBe(5); // base time - expect(secondCall[4]).toBe(25); // start time - expect(secondCall[5]).toBe(30); // commit time - }); + expect(callback).toHaveBeenCalledTimes(1); - it('does not include time spent outside of profile root', () => { + advanceTimeBy(30); // 28 -> 58 + + renderer.update( + + + + + , + ); + + expect(callback).toHaveBeenCalledTimes(2); + + const [mountCall, updateCall] = callback.mock.calls; + + expect(mountCall[1]).toBe('mount'); + expect(mountCall[2]).toBe(23); // actual time + expect(mountCall[3]).toBe(23); // base time + expect(mountCall[4]).toBe(5); // start time + expect(mountCall[5]).toBe(28); // commit time + + expect(updateCall[1]).toBe('update'); + expect(updateCall[2]).toBe(4); // actual time + expect(updateCall[3]).toBe(17); // base time + expect(updateCall[4]).toBe(58); // start time + expect(updateCall[5]).toBe(62); // commit time + }); + + it('includes time spent in render phase lifecycles', () => { + class WithLifecycles extends React.Component { + state = {}; + static getDerivedStateFromProps() { + advanceTimeBy(3); + return null; + } + shouldComponentUpdate() { + advanceTimeBy(7); + return true; + } + render() { + advanceTimeBy(5); + return null; + } + } + + const callback = jest.fn(); + + advanceTimeBy(5); // 0 -> 5 + + const renderer = ReactTestRenderer.create( + + + , + ); + + advanceTimeBy(15); // 13 -> 28 + + renderer.update( + + + , + ); + + expect(callback).toHaveBeenCalledTimes(2); + + const [mountCall, updateCall] = callback.mock.calls; + + expect(mountCall[1]).toBe('mount'); + expect(mountCall[2]).toBe(8); // actual time + expect(mountCall[3]).toBe(8); // base time + expect(mountCall[4]).toBe(5); // start time + expect(mountCall[5]).toBe(13); // commit time + + expect(updateCall[1]).toBe('update'); + expect(updateCall[2]).toBe(15); // actual time + expect(updateCall[3]).toBe(15); // base time + expect(updateCall[4]).toBe(28); // start time + expect(updateCall[5]).toBe(43); // commit time + }); + + describe('with regard to interruptions', () => { + it('should accumulate actual time after a scheduling interruptions', () => { const callback = jest.fn(); + const Yield = ({renderTime}) => { + advanceTimeBy(renderTime); + ReactTestRenderer.unstable_yield('Yield:' + renderTime); + return null; + }; + advanceTimeBy(5); // 0 -> 5 - ReactTestRenderer.create( - - - - - - - , + // Render partially, but run out of time before completing. + const renderer = ReactTestRenderer.create( + + + + , + {unstable_isConcurrent: true}, ); + expect(renderer).toFlushAndYieldThrough(['Yield:2']); + expect(callback).toHaveBeenCalledTimes(0); - expect(callback).toHaveBeenCalledTimes(1); + // Resume render for remaining children. + expect(renderer).toFlushAndYield(['Yield:3']); + // Verify that logged times include both durations above. + expect(callback).toHaveBeenCalledTimes(1); const [call] = callback.mock.calls; - expect(call[0]).toBe('test'); expect(call[2]).toBe(5); // actual time expect(call[3]).toBe(5); // base time - expect(call[4]).toBe(25); // start time - expect(call[5]).toBe(50); // commit time + expect(call[4]).toBe(5); // start time + expect(call[5]).toBe(10); // commit time }); - it('is not called when blocked by sCU false', () => { + it('should not include time between frames', () => { const callback = jest.fn(); - let instance; - class Updater extends React.Component { - state = {}; - render() { - instance = this; - return this.props.children; - } - } + const Yield = ({renderTime}) => { + advanceTimeBy(renderTime); + ReactTestRenderer.unstable_yield('Yield:' + renderTime); + return null; + }; - class Pure extends React.PureComponent { - render() { - return this.props.children; - } - } + advanceTimeBy(5); // 0 -> 5 + // Render partially, but don't finish. + // This partial render should take 5ms of simulated time. const renderer = ReactTestRenderer.create( - - - - -
- - - - + + + + + , + {unstable_isConcurrent: true}, ); + expect(renderer).toFlushAndYieldThrough(['Yield:5']); + expect(callback).toHaveBeenCalledTimes(0); - // All profile callbacks are called for initial render - expect(callback).toHaveBeenCalledTimes(3); - - callback.mockReset(); - - renderer.unstable_flushSync(() => { - instance.setState({ - count: 1, - }); - }); + // Simulate time moving forward while frame is paused. + advanceTimeBy(50); // 10 -> 60 - // Only call profile updates for paths that have re-rendered - // Since "inner" is beneath a pure component, it isn't called + // Flush the remaining work, + // Which should take an additional 10ms of simulated time. + expect(renderer).toFlushAndYield(['Yield:10', 'Yield:17']); expect(callback).toHaveBeenCalledTimes(2); - expect(callback.mock.calls[0][0]).toBe('middle'); - expect(callback.mock.calls[1][0]).toBe('outer'); + + const [innerCall, outerCall] = callback.mock.calls; + + // Verify that the actual time includes all work times, + // But not the time that elapsed between frames. + expect(innerCall[0]).toBe('inner'); + expect(innerCall[2]).toBe(17); // actual time + expect(innerCall[3]).toBe(17); // base time + expect(innerCall[4]).toBe(70); // start time + expect(innerCall[5]).toBe(87); // commit time + expect(outerCall[0]).toBe('outer'); + expect(outerCall[2]).toBe(32); // actual time + expect(outerCall[3]).toBe(32); // base time + expect(outerCall[4]).toBe(5); // start time + expect(outerCall[5]).toBe(87); // commit time }); - it('decreases actual time but not base time when sCU prevents an update', () => { + it('should report the expected times when a high-pri update replaces a mount in-progress', () => { const callback = jest.fn(); + const Yield = ({renderTime}) => { + advanceTimeBy(renderTime); + ReactTestRenderer.unstable_yield('Yield:' + renderTime); + return null; + }; + advanceTimeBy(5); // 0 -> 5 + // Render a partially update, but don't finish. + // This partial render should take 10ms of simulated time. const renderer = ReactTestRenderer.create( - - - + + , + {unstable_isConcurrent: true}, ); + expect(renderer).toFlushAndYieldThrough(['Yield:10']); + expect(callback).toHaveBeenCalledTimes(0); - expect(callback).toHaveBeenCalledTimes(1); - - advanceTimeBy(30); // 28 -> 58 - - renderer.update( - - - - - , - ); + // Simulate time moving forward while frame is paused. + advanceTimeBy(100); // 15 -> 115 - expect(callback).toHaveBeenCalledTimes(2); + // Interrupt with higher priority work. + // The interrupted work simulates an additional 5ms of time. + renderer.unstable_flushSync(() => { + renderer.update( + + + , + ); + }); + expect(ReactTestRenderer).toHaveYielded(['Yield:5']); - const [mountCall, updateCall] = callback.mock.calls; + // The initial work was thrown away in this case, + // So the actual and base times should only include the final rendered tree times. + expect(callback).toHaveBeenCalledTimes(1); + let call = callback.mock.calls[0]; + expect(call[2]).toBe(5); // actual time + expect(call[3]).toBe(5); // base time + expect(call[4]).toBe(115); // start time + expect(call[5]).toBe(120); // commit time - expect(mountCall[1]).toBe('mount'); - expect(mountCall[2]).toBe(23); // actual time - expect(mountCall[3]).toBe(23); // base time - expect(mountCall[4]).toBe(5); // start time - expect(mountCall[5]).toBe(28); // commit time + callback.mockReset(); - expect(updateCall[1]).toBe('update'); - expect(updateCall[2]).toBe(4); // actual time - expect(updateCall[3]).toBe(17); // base time - expect(updateCall[4]).toBe(58); // start time - expect(updateCall[5]).toBe(62); // commit time + // Verify no more unexpected callbacks from low priority work + expect(renderer).toFlushWithoutYielding(); + expect(callback).toHaveBeenCalledTimes(0); }); - it('includes time spent in render phase lifecycles', () => { - class WithLifecycles extends React.Component { - state = {}; - static getDerivedStateFromProps() { - advanceTimeBy(3); - return null; - } - shouldComponentUpdate() { - advanceTimeBy(7); - return true; - } - render() { - advanceTimeBy(5); - return null; - } - } - + it('should report the expected times when a high-priority update replaces a low-priority update', () => { const callback = jest.fn(); + const Yield = ({renderTime}) => { + advanceTimeBy(renderTime); + ReactTestRenderer.unstable_yield('Yield:' + renderTime); + return null; + }; + advanceTimeBy(5); // 0 -> 5 const renderer = ReactTestRenderer.create( - + + , + {unstable_isConcurrent: true}, ); - advanceTimeBy(15); // 13 -> 28 + // Render everything initially. + // This should take 21 seconds of actual and base time. + expect(renderer).toFlushAndYield(['Yield:6', 'Yield:15']); + expect(callback).toHaveBeenCalledTimes(1); + let call = callback.mock.calls[0]; + expect(call[2]).toBe(21); // actual time + expect(call[3]).toBe(21); // base time + expect(call[4]).toBe(5); // start time + expect(call[5]).toBe(26); // commit time + + callback.mockReset(); + + advanceTimeBy(30); // 26 -> 56 + // Render a partially update, but don't finish. + // This partial render should take 3ms of simulated time. renderer.update( - + + + , ); + expect(renderer).toFlushAndYieldThrough(['Yield:3']); + expect(callback).toHaveBeenCalledTimes(0); - expect(callback).toHaveBeenCalledTimes(2); - - const [mountCall, updateCall] = callback.mock.calls; - - expect(mountCall[1]).toBe('mount'); - expect(mountCall[2]).toBe(8); // actual time - expect(mountCall[3]).toBe(8); // base time - expect(mountCall[4]).toBe(5); // start time - expect(mountCall[5]).toBe(13); // commit time - - expect(updateCall[1]).toBe('update'); - expect(updateCall[2]).toBe(15); // actual time - expect(updateCall[3]).toBe(15); // base time - expect(updateCall[4]).toBe(28); // start time - expect(updateCall[5]).toBe(43); // commit time - }); - - describe('with regard to interruptions', () => { - it('should accumulate actual time after a scheduling interruptions', () => { - const callback = jest.fn(); + // Simulate time moving forward while frame is paused. + advanceTimeBy(100); // 59 -> 159 - const Yield = ({renderTime}) => { - advanceTimeBy(renderTime); - ReactTestRenderer.unstable_yield('Yield:' + renderTime); - return null; - }; + // Render another 5ms of simulated time. + expect(renderer).toFlushAndYieldThrough(['Yield:5']); + expect(callback).toHaveBeenCalledTimes(0); - advanceTimeBy(5); // 0 -> 5 + // Simulate time moving forward while frame is paused. + advanceTimeBy(100); // 164 -> 264 - // Render partially, but run out of time before completing. - const renderer = ReactTestRenderer.create( + // Interrupt with higher priority work. + // The interrupted work simulates an additional 11ms of time. + renderer.unstable_flushSync(() => { + renderer.update( - - + , - {unstable_isConcurrent: true}, ); - expect(renderer).toFlushAndYieldThrough(['Yield:2']); - expect(callback).toHaveBeenCalledTimes(0); - - // Resume render for remaining children. - expect(renderer).toFlushAndYield(['Yield:3']); - - // Verify that logged times include both durations above. - expect(callback).toHaveBeenCalledTimes(1); - const [call] = callback.mock.calls; - expect(call[2]).toBe(5); // actual time - expect(call[3]).toBe(5); // base time - expect(call[4]).toBe(5); // start time - expect(call[5]).toBe(10); // commit time }); + expect(ReactTestRenderer).toHaveYielded(['Yield:11']); - it('should not include time between frames', () => { - const callback = jest.fn(); + // The actual time should include only the most recent render, + // Because this lets us avoid a lot of commit phase reset complexity. + // The base time includes only the final rendered tree times. + expect(callback).toHaveBeenCalledTimes(1); + call = callback.mock.calls[0]; + expect(call[2]).toBe(11); // actual time + expect(call[3]).toBe(11); // base time + expect(call[4]).toBe(264); // start time + expect(call[5]).toBe(275); // commit time - const Yield = ({renderTime}) => { - advanceTimeBy(renderTime); - ReactTestRenderer.unstable_yield('Yield:' + renderTime); - return null; - }; + // Verify no more unexpected callbacks from low priority work + expect(renderer).toFlushAndYield([]); + expect(callback).toHaveBeenCalledTimes(1); + }); - advanceTimeBy(5); // 0 -> 5 + it('should report the expected times when a high-priority update interrupts a low-priority update', () => { + const callback = jest.fn(); - // Render partially, but don't finish. - // This partial render should take 5ms of simulated time. - const renderer = ReactTestRenderer.create( - - - - - - - , - {unstable_isConcurrent: true}, - ); - expect(renderer).toFlushAndYieldThrough(['Yield:5']); - expect(callback).toHaveBeenCalledTimes(0); - - // Simulate time moving forward while frame is paused. - advanceTimeBy(50); // 10 -> 60 - - // Flush the remaining work, - // Which should take an additional 10ms of simulated time. - expect(renderer).toFlushAndYield(['Yield:10', 'Yield:17']); - expect(callback).toHaveBeenCalledTimes(2); - - const [innerCall, outerCall] = callback.mock.calls; - - // Verify that the actual time includes all work times, - // But not the time that elapsed between frames. - expect(innerCall[0]).toBe('inner'); - expect(innerCall[2]).toBe(17); // actual time - expect(innerCall[3]).toBe(17); // base time - expect(innerCall[4]).toBe(70); // start time - expect(innerCall[5]).toBe(87); // commit time - expect(outerCall[0]).toBe('outer'); - expect(outerCall[2]).toBe(32); // actual time - expect(outerCall[3]).toBe(32); // base time - expect(outerCall[4]).toBe(5); // start time - expect(outerCall[5]).toBe(87); // commit time - }); + const Yield = ({renderTime}) => { + advanceTimeBy(renderTime); + ReactTestRenderer.unstable_yield('Yield:' + renderTime); + return null; + }; - it('should report the expected times when a high-pri update replaces a mount in-progress', () => { - const callback = jest.fn(); + let first; + class FirstComponent extends React.Component { + state = {renderTime: 1}; + render() { + first = this; + advanceTimeBy(this.state.renderTime); + ReactTestRenderer.unstable_yield( + 'FirstComponent:' + this.state.renderTime, + ); + return ; + } + } + let second; + class SecondComponent extends React.Component { + state = {renderTime: 2}; + render() { + second = this; + advanceTimeBy(this.state.renderTime); + ReactTestRenderer.unstable_yield( + 'SecondComponent:' + this.state.renderTime, + ); + return ; + } + } - const Yield = ({renderTime}) => { - advanceTimeBy(renderTime); - ReactTestRenderer.unstable_yield('Yield:' + renderTime); - return null; - }; + advanceTimeBy(5); // 0 -> 5 - advanceTimeBy(5); // 0 -> 5 + const renderer = ReactTestRenderer.create( + + + + , + {unstable_isConcurrent: true}, + ); - // Render a partially update, but don't finish. - // This partial render should take 10ms of simulated time. - const renderer = ReactTestRenderer.create( - - - - , - {unstable_isConcurrent: true}, - ); - expect(renderer).toFlushAndYieldThrough(['Yield:10']); - expect(callback).toHaveBeenCalledTimes(0); + // Render everything initially. + // This simulates a total of 14ms of actual render time. + // The base render time is also 14ms for the initial render. + expect(renderer).toFlushAndYield([ + 'FirstComponent:1', + 'Yield:4', + 'SecondComponent:2', + 'Yield:7', + ]); + expect(callback).toHaveBeenCalledTimes(1); + let call = callback.mock.calls[0]; + expect(call[2]).toBe(14); // actual time + expect(call[3]).toBe(14); // base time + expect(call[4]).toBe(5); // start time + expect(call[5]).toBe(19); // commit time - // Simulate time moving forward while frame is paused. - advanceTimeBy(100); // 15 -> 115 + callback.mockClear(); - // Interrupt with higher priority work. - // The interrupted work simulates an additional 5ms of time. - renderer.unstable_flushSync(() => { - renderer.update( - - - , - ); - }); - expect(ReactTestRenderer).toHaveYielded(['Yield:5']); - - // The initial work was thrown away in this case, - // So the actual and base times should only include the final rendered tree times. - expect(callback).toHaveBeenCalledTimes(1); - let call = callback.mock.calls[0]; - expect(call[2]).toBe(5); // actual time - expect(call[3]).toBe(5); // base time - expect(call[4]).toBe(115); // start time - expect(call[5]).toBe(120); // commit time - - callback.mockReset(); - - // Verify no more unexpected callbacks from low priority work - expect(renderer).toFlushWithoutYielding(); - expect(callback).toHaveBeenCalledTimes(0); - }); + advanceTimeBy(100); // 19 -> 119 - it('should report the expected times when a high-priority update replaces a low-priority update', () => { - const callback = jest.fn(); + // Render a partially update, but don't finish. + // This partial render will take 10ms of actual render time. + first.setState({renderTime: 10}); + expect(renderer).toFlushAndYieldThrough(['FirstComponent:10']); + expect(callback).toHaveBeenCalledTimes(0); - const Yield = ({renderTime}) => { - advanceTimeBy(renderTime); - ReactTestRenderer.unstable_yield('Yield:' + renderTime); - return null; - }; + // Simulate time moving forward while frame is paused. + advanceTimeBy(100); // 129 -> 229 - advanceTimeBy(5); // 0 -> 5 + // Interrupt with higher priority work. + // This simulates a total of 37ms of actual render time. + renderer.unstable_flushSync(() => second.setState({renderTime: 30})); + expect(ReactTestRenderer).toHaveYielded([ + 'SecondComponent:30', + 'Yield:7', + ]); - const renderer = ReactTestRenderer.create( - - - - , - {unstable_isConcurrent: true}, - ); + // The actual time should include only the most recent render (37ms), + // Because this greatly simplifies the commit phase logic. + // The base time should include the more recent times for the SecondComponent subtree, + // As well as the original times for the FirstComponent subtree. + expect(callback).toHaveBeenCalledTimes(1); + call = callback.mock.calls[0]; + expect(call[2]).toBe(37); // actual time + expect(call[3]).toBe(42); // base time + expect(call[4]).toBe(229); // start time + expect(call[5]).toBe(266); // commit time - // Render everything initially. - // This should take 21 seconds of actual and base time. - expect(renderer).toFlushAndYield(['Yield:6', 'Yield:15']); - expect(callback).toHaveBeenCalledTimes(1); - let call = callback.mock.calls[0]; - expect(call[2]).toBe(21); // actual time - expect(call[3]).toBe(21); // base time - expect(call[4]).toBe(5); // start time - expect(call[5]).toBe(26); // commit time + callback.mockClear(); - callback.mockReset(); + // Simulate time moving forward while frame is paused. + advanceTimeBy(100); // 266 -> 366 - advanceTimeBy(30); // 26 -> 56 + // Resume the original low priority update, with rebased state. + // This simulates a total of 14ms of actual render time, + // And does not include the original (interrupted) 10ms. + // The tree contains 42ms of base render time at this point, + // Reflecting the most recent (longer) render durations. + // TODO: This actual time should decrease by 10ms once the scheduler supports resuming. + expect(renderer).toFlushAndYield(['FirstComponent:10', 'Yield:4']); + expect(callback).toHaveBeenCalledTimes(1); + call = callback.mock.calls[0]; + expect(call[2]).toBe(14); // actual time + expect(call[3]).toBe(51); // base time + expect(call[4]).toBe(366); // start time + expect(call[5]).toBe(380); // commit time + }); - // Render a partially update, but don't finish. - // This partial render should take 3ms of simulated time. - renderer.update( - - - - - , - ); - expect(renderer).toFlushAndYieldThrough(['Yield:3']); - expect(callback).toHaveBeenCalledTimes(0); + [true, false].forEach(replayFailedUnitOfWorkWithInvokeGuardedCallback => { + describe(`replayFailedUnitOfWorkWithInvokeGuardedCallback ${ + replayFailedUnitOfWorkWithInvokeGuardedCallback + ? 'enabled' + : 'disabled' + }`, () => { + beforeEach(() => { + jest.resetModules(); + + loadModules({ + replayFailedUnitOfWorkWithInvokeGuardedCallback, + }); + }); + + it('should accumulate actual time after an error handled by componentDidCatch()', () => { + const callback = jest.fn(); - // Simulate time moving forward while frame is paused. - advanceTimeBy(100); // 59 -> 159 + const ThrowsError = () => { + advanceTimeBy(3); + throw Error('expected error'); + }; - // Render another 5ms of simulated time. - expect(renderer).toFlushAndYieldThrough(['Yield:5']); - expect(callback).toHaveBeenCalledTimes(0); + class ErrorBoundary extends React.Component { + state = {error: null}; + componentDidCatch(error) { + this.setState({error}); + } + render() { + advanceTimeBy(2); + return this.state.error === null ? ( + this.props.children + ) : ( + + ); + } + } - // Simulate time moving forward while frame is paused. - advanceTimeBy(100); // 164 -> 264 + advanceTimeBy(5); // 0 -> 5 - // Interrupt with higher priority work. - // The interrupted work simulates an additional 11ms of time. - renderer.unstable_flushSync(() => { - renderer.update( + ReactTestRenderer.create( - + + + + , ); + + expect(callback).toHaveBeenCalledTimes(2); + + // Callbacks bubble (reverse order). + let [mountCall, updateCall] = callback.mock.calls; + + // The initial mount only includes the ErrorBoundary (which takes 2) + // But it spends time rendering all of the failed subtree also. + expect(mountCall[1]).toBe('mount'); + // actual time includes: 2 (ErrorBoundary) + 9 (AdvanceTime) + 3 (ThrowsError) + // We don't count the time spent in replaying the failed unit of work (ThrowsError) + expect(mountCall[2]).toBe(14); + // base time includes: 2 (ErrorBoundary) + // Since the tree is empty for the initial commit + expect(mountCall[3]).toBe(2); + // start time + expect(mountCall[4]).toBe(5); + // commit time: 5 initially + 14 of work + // Add an additional 3 (ThrowsError) if we replayed the failed work + expect(mountCall[5]).toBe( + __DEV__ && replayFailedUnitOfWorkWithInvokeGuardedCallback + ? 22 + : 19, + ); + + // The update includes the ErrorBoundary and its fallback child + expect(updateCall[1]).toBe('update'); + // actual time includes: 2 (ErrorBoundary) + 20 (AdvanceTime) + expect(updateCall[2]).toBe(22); + // base time includes: 2 (ErrorBoundary) + 20 (AdvanceTime) + expect(updateCall[3]).toBe(22); + // start time + expect(updateCall[4]).toBe( + __DEV__ && replayFailedUnitOfWorkWithInvokeGuardedCallback + ? 22 + : 19, + ); + // commit time: 19 (startTime) + 2 (ErrorBoundary) + 20 (AdvanceTime) + // Add an additional 3 (ThrowsError) if we replayed the failed work + expect(updateCall[5]).toBe( + __DEV__ && replayFailedUnitOfWorkWithInvokeGuardedCallback + ? 44 + : 41, + ); }); - expect(ReactTestRenderer).toHaveYielded(['Yield:11']); - - // The actual time should include only the most recent render, - // Because this lets us avoid a lot of commit phase reset complexity. - // The base time includes only the final rendered tree times. - expect(callback).toHaveBeenCalledTimes(1); - call = callback.mock.calls[0]; - expect(call[2]).toBe(11); // actual time - expect(call[3]).toBe(11); // base time - expect(call[4]).toBe(264); // start time - expect(call[5]).toBe(275); // commit time - - // Verify no more unexpected callbacks from low priority work - expect(renderer).toFlushAndYield([]); - expect(callback).toHaveBeenCalledTimes(1); - }); - it('should report the expected times when a high-priority update interrupts a low-priority update', () => { - const callback = jest.fn(); + it('should accumulate actual time after an error handled by getDerivedStateFromError()', () => { + const callback = jest.fn(); - const Yield = ({renderTime}) => { - advanceTimeBy(renderTime); - ReactTestRenderer.unstable_yield('Yield:' + renderTime); - return null; - }; + const ThrowsError = () => { + advanceTimeBy(10); + throw Error('expected error'); + }; - let first; - class FirstComponent extends React.Component { - state = {renderTime: 1}; - render() { - first = this; - advanceTimeBy(this.state.renderTime); - ReactTestRenderer.unstable_yield( - 'FirstComponent:' + this.state.renderTime, - ); - return ; - } - } - let second; - class SecondComponent extends React.Component { - state = {renderTime: 2}; - render() { - second = this; - advanceTimeBy(this.state.renderTime); - ReactTestRenderer.unstable_yield( - 'SecondComponent:' + this.state.renderTime, - ); - return ; + class ErrorBoundary extends React.Component { + state = {error: null}; + static getDerivedStateFromError(error) { + return {error}; + } + render() { + advanceTimeBy(2); + return this.state.error === null ? ( + this.props.children + ) : ( + + ); + } } - } - advanceTimeBy(5); // 0 -> 5 + advanceTimeBy(5); // 0 -> 5 - const renderer = ReactTestRenderer.create( - - - - , - {unstable_isConcurrent: true}, - ); + ReactTestRenderer.create( + + + + + + , + ); - // Render everything initially. - // This simulates a total of 14ms of actual render time. - // The base render time is also 14ms for the initial render. - expect(renderer).toFlushAndYield([ - 'FirstComponent:1', - 'Yield:4', - 'SecondComponent:2', - 'Yield:7', - ]); - expect(callback).toHaveBeenCalledTimes(1); - let call = callback.mock.calls[0]; - expect(call[2]).toBe(14); // actual time - expect(call[3]).toBe(14); // base time - expect(call[4]).toBe(5); // start time - expect(call[5]).toBe(19); // commit time - - callback.mockClear(); - - advanceTimeBy(100); // 19 -> 119 - - // Render a partially update, but don't finish. - // This partial render will take 10ms of actual render time. - first.setState({renderTime: 10}); - expect(renderer).toFlushAndYieldThrough(['FirstComponent:10']); - expect(callback).toHaveBeenCalledTimes(0); - - // Simulate time moving forward while frame is paused. - advanceTimeBy(100); // 129 -> 229 - - // Interrupt with higher priority work. - // This simulates a total of 37ms of actual render time. - renderer.unstable_flushSync(() => second.setState({renderTime: 30})); - expect(ReactTestRenderer).toHaveYielded([ - 'SecondComponent:30', - 'Yield:7', - ]); - - // The actual time should include only the most recent render (37ms), - // Because this greatly simplifies the commit phase logic. - // The base time should include the more recent times for the SecondComponent subtree, - // As well as the original times for the FirstComponent subtree. - expect(callback).toHaveBeenCalledTimes(1); - call = callback.mock.calls[0]; - expect(call[2]).toBe(37); // actual time - expect(call[3]).toBe(42); // base time - expect(call[4]).toBe(229); // start time - expect(call[5]).toBe(266); // commit time - - callback.mockClear(); - - // Simulate time moving forward while frame is paused. - advanceTimeBy(100); // 266 -> 366 - - // Resume the original low priority update, with rebased state. - // This simulates a total of 14ms of actual render time, - // And does not include the original (interrupted) 10ms. - // The tree contains 42ms of base render time at this point, - // Reflecting the most recent (longer) render durations. - // TODO: This actual time should decrease by 10ms once the scheduler supports resuming. - expect(renderer).toFlushAndYield(['FirstComponent:10', 'Yield:4']); - expect(callback).toHaveBeenCalledTimes(1); - call = callback.mock.calls[0]; - expect(call[2]).toBe(14); // actual time - expect(call[3]).toBe(51); // base time - expect(call[4]).toBe(366); // start time - expect(call[5]).toBe(380); // commit time - }); + expect(callback).toHaveBeenCalledTimes(1); + + // Callbacks bubble (reverse order). + let [mountCall] = callback.mock.calls; + + // The initial mount includes the ErrorBoundary's error state, + // But it also spends actual time rendering UI that fails and isn't included. + expect(mountCall[1]).toBe('mount'); + // actual time includes: 2 (ErrorBoundary) + 5 (AdvanceTime) + 10 (ThrowsError) + // Then the re-render: 2 (ErrorBoundary) + 20 (AdvanceTime) + // We don't count the time spent in replaying the failed unit of work (ThrowsError) + expect(mountCall[2]).toBe(39); + // base time includes: 2 (ErrorBoundary) + 20 (AdvanceTime) + expect(mountCall[3]).toBe(22); + // start time + expect(mountCall[4]).toBe(5); + // commit time + expect(mountCall[5]).toBe( + __DEV__ && replayFailedUnitOfWorkWithInvokeGuardedCallback + ? 54 + : 44, + ); + }); - [true, false].forEach( - replayFailedUnitOfWorkWithInvokeGuardedCallback => { - describe(`replayFailedUnitOfWorkWithInvokeGuardedCallback ${ - replayFailedUnitOfWorkWithInvokeGuardedCallback - ? 'enabled' - : 'disabled' - }`, () => { - beforeEach(() => { - jest.resetModules(); - - loadModules({ - replayFailedUnitOfWorkWithInvokeGuardedCallback, - }); - }); - - it('should accumulate actual time after an error handled by componentDidCatch()', () => { - const callback = jest.fn(); - - const ThrowsError = () => { - advanceTimeBy(3); - throw Error('expected error'); - }; - - class ErrorBoundary extends React.Component { - state = {error: null}; - componentDidCatch(error) { - this.setState({error}); - } - render() { - advanceTimeBy(2); - return this.state.error === null ? ( - this.props.children - ) : ( - - ); - } - } - - advanceTimeBy(5); // 0 -> 5 - - ReactTestRenderer.create( - - - - - - , - ); + it('should reset the fiber stack correct after a "complete" phase error', () => { + jest.resetModules(); - expect(callback).toHaveBeenCalledTimes(2); - - // Callbacks bubble (reverse order). - let [mountCall, updateCall] = callback.mock.calls; - - // The initial mount only includes the ErrorBoundary (which takes 2) - // But it spends time rendering all of the failed subtree also. - expect(mountCall[1]).toBe('mount'); - // actual time includes: 2 (ErrorBoundary) + 9 (AdvanceTime) + 3 (ThrowsError) - // We don't count the time spent in replaying the failed unit of work (ThrowsError) - expect(mountCall[2]).toBe(14); - // base time includes: 2 (ErrorBoundary) - // Since the tree is empty for the initial commit - expect(mountCall[3]).toBe(2); - // start time - expect(mountCall[4]).toBe(5); - // commit time: 5 initially + 14 of work - // Add an additional 3 (ThrowsError) if we replayed the failed work - expect(mountCall[5]).toBe( - __DEV__ && replayFailedUnitOfWorkWithInvokeGuardedCallback - ? 22 - : 19, - ); + loadModules({ + useNoopRenderer: true, + replayFailedUnitOfWorkWithInvokeGuardedCallback, + }); - // The update includes the ErrorBoundary and its fallback child - expect(updateCall[1]).toBe('update'); - // actual time includes: 2 (ErrorBoundary) + 20 (AdvanceTime) - expect(updateCall[2]).toBe(22); - // base time includes: 2 (ErrorBoundary) + 20 (AdvanceTime) - expect(updateCall[3]).toBe(22); - // start time - expect(updateCall[4]).toBe( - __DEV__ && replayFailedUnitOfWorkWithInvokeGuardedCallback - ? 22 - : 19, - ); - // commit time: 19 (startTime) + 2 (ErrorBoundary) + 20 (AdvanceTime) - // Add an additional 3 (ThrowsError) if we replayed the failed work - expect(updateCall[5]).toBe( - __DEV__ && replayFailedUnitOfWorkWithInvokeGuardedCallback - ? 44 - : 41, - ); - }); - - it('should accumulate actual time after an error handled by getDerivedStateFromError()', () => { - const callback = jest.fn(); - - const ThrowsError = () => { - advanceTimeBy(10); - throw Error('expected error'); - }; - - class ErrorBoundary extends React.Component { - state = {error: null}; - static getDerivedStateFromError(error) { - return {error}; - } - render() { - advanceTimeBy(2); - return this.state.error === null ? ( - this.props.children - ) : ( - - ); - } - } - - advanceTimeBy(5); // 0 -> 5 - - ReactTestRenderer.create( - - - - - - , - ); + // Simulate a renderer error during the "complete" phase. + // This mimics behavior like React Native's View/Text nesting validation. + ReactNoop.render( + + hi + , + ); + expect(ReactNoop.flush).toThrow('Error in host config.'); + + // A similar case we've seen caused by an invariant in ReactDOM. + // It didn't reproduce without a host component inside. + ReactNoop.render( + + + hi + + , + ); + expect(ReactNoop.flush).toThrow('Error in host config.'); - expect(callback).toHaveBeenCalledTimes(1); - - // Callbacks bubble (reverse order). - let [mountCall] = callback.mock.calls; - - // The initial mount includes the ErrorBoundary's error state, - // But it also spends actual time rendering UI that fails and isn't included. - expect(mountCall[1]).toBe('mount'); - // actual time includes: 2 (ErrorBoundary) + 5 (AdvanceTime) + 10 (ThrowsError) - // Then the re-render: 2 (ErrorBoundary) + 20 (AdvanceTime) - // We don't count the time spent in replaying the failed unit of work (ThrowsError) - expect(mountCall[2]).toBe(39); - // base time includes: 2 (ErrorBoundary) + 20 (AdvanceTime) - expect(mountCall[3]).toBe(22); - // start time - expect(mountCall[4]).toBe(5); - // commit time - expect(mountCall[5]).toBe( - __DEV__ && replayFailedUnitOfWorkWithInvokeGuardedCallback - ? 54 - : 44, - ); - }); - - it('should reset the fiber stack correct after a "complete" phase error', () => { - jest.resetModules(); - - loadModules({ - useNoopRenderer: true, - replayFailedUnitOfWorkWithInvokeGuardedCallback, - }); - - // Simulate a renderer error during the "complete" phase. - // This mimics behavior like React Native's View/Text nesting validation. - ReactNoop.render( - - hi - , - ); - expect(ReactNoop.flush).toThrow('Error in host config.'); - - // A similar case we've seen caused by an invariant in ReactDOM. - // It didn't reproduce without a host component inside. - ReactNoop.render( - - - hi - - , - ); - expect(ReactNoop.flush).toThrow('Error in host config.'); - - // So long as the profiler timer's fiber stack is reset correctly, - // Subsequent renders should not error. - ReactNoop.render( - - hi - , - ); - ReactNoop.flush(); - }); - }); - }, - ); + // So long as the profiler timer's fiber stack is reset correctly, + // Subsequent renders should not error. + ReactNoop.render( + + hi + , + ); + ReactNoop.flush(); + }); + }); }); + }); - it('reflects the most recently rendered id value', () => { - const callback = jest.fn(); + it('reflects the most recently rendered id value', () => { + const callback = jest.fn(); - advanceTimeBy(5); // 0 -> 5 + advanceTimeBy(5); // 0 -> 5 - const renderer = ReactTestRenderer.create( - - - , - ); + const renderer = ReactTestRenderer.create( + + + , + ); - expect(callback).toHaveBeenCalledTimes(1); + expect(callback).toHaveBeenCalledTimes(1); - advanceTimeBy(20); // 7 -> 27 + advanceTimeBy(20); // 7 -> 27 - renderer.update( - - - , - ); + renderer.update( + + + , + ); - expect(callback).toHaveBeenCalledTimes(2); + expect(callback).toHaveBeenCalledTimes(2); - const [mountCall, updateCall] = callback.mock.calls; + const [mountCall, updateCall] = callback.mock.calls; - expect(mountCall[0]).toBe('one'); - expect(mountCall[1]).toBe('mount'); - expect(mountCall[2]).toBe(2); // actual time - expect(mountCall[3]).toBe(2); // base time - expect(mountCall[4]).toBe(5); // start time + expect(mountCall[0]).toBe('one'); + expect(mountCall[1]).toBe('mount'); + expect(mountCall[2]).toBe(2); // actual time + expect(mountCall[3]).toBe(2); // base time + expect(mountCall[4]).toBe(5); // start time - expect(updateCall[0]).toBe('two'); - expect(updateCall[1]).toBe('update'); - expect(updateCall[2]).toBe(1); // actual time - expect(updateCall[3]).toBe(1); // base time - expect(updateCall[4]).toBe(27); // start time - }); + expect(updateCall[0]).toBe('two'); + expect(updateCall[1]).toBe('update'); + expect(updateCall[2]).toBe(1); // actual time + expect(updateCall[3]).toBe(1); // base time + expect(updateCall[4]).toBe(27); // start time + }); - it('should not be called until after mutations', () => { - let classComponentMounted = false; - const callback = jest.fn( - (id, phase, actualDuration, baseDuration, startTime, commitTime) => { - // Don't call this hook until after mutations - expect(classComponentMounted).toBe(true); - // But the commit time should reflect pre-mutation - expect(commitTime).toBe(2); - }, - ); + it('should not be called until after mutations', () => { + let classComponentMounted = false; + const callback = jest.fn( + (id, phase, actualDuration, baseDuration, startTime, commitTime) => { + // Don't call this hook until after mutations + expect(classComponentMounted).toBe(true); + // But the commit time should reflect pre-mutation + expect(commitTime).toBe(2); + }, + ); - class ClassComponent extends React.Component { - componentDidMount() { - advanceTimeBy(5); - classComponentMounted = true; - } - render() { - advanceTimeBy(2); - return null; - } + class ClassComponent extends React.Component { + componentDidMount() { + advanceTimeBy(5); + classComponentMounted = true; } + render() { + advanceTimeBy(2); + return null; + } + } - ReactTestRenderer.create( - - - , - ); + ReactTestRenderer.create( + + + , + ); - expect(callback).toHaveBeenCalledTimes(1); - }); + expect(callback).toHaveBeenCalledTimes(1); }); }); @@ -1277,10 +1267,7 @@ describe('Profiler', () => { function loadModulesForTracing(params) { jest.resetModules(); - loadModules({ - enableSchedulerTracing: true, - ...params, - }); + loadModules(params); throwInOnInteractionScheduledWorkCompleted = false; throwInOnWorkScheduled = false; @@ -1555,9 +1542,7 @@ describe('Profiler', () => { let call = onRender.mock.calls[0]; expect(call[0]).toEqual('test-profiler'); expect(call[5]).toEqual(mockNow()); - if (ReactFeatureFlags.enableSchedulerTracing) { - expect(call[6]).toMatchInteractions([interactionCreation]); - } + expect(call[6]).toMatchInteractions([interactionCreation]); expect(onInteractionTraced).toHaveBeenCalledTimes(1); expect(onInteractionScheduledWorkCompleted).toHaveBeenCalledTimes(1); @@ -1627,9 +1612,7 @@ describe('Profiler', () => { call = onRender.mock.calls[0]; expect(call[0]).toEqual('test-profiler'); expect(call[5]).toEqual(mockNow()); - if (ReactFeatureFlags.enableSchedulerTracing) { - expect(call[6]).toMatchInteractions([interactionOne]); - } + expect(call[6]).toMatchInteractions([interactionOne]); didRunCallback = true; @@ -1660,9 +1643,7 @@ describe('Profiler', () => { call = onRender.mock.calls[0]; expect(call[0]).toEqual('test-profiler'); expect(call[5]).toEqual(mockNow()); - if (ReactFeatureFlags.enableSchedulerTracing) { - expect(call[6]).toMatchInteractions([]); - } + expect(call[6]).toMatchInteractions([]); expect(onInteractionTraced).toHaveBeenCalledTimes(2); expect(onInteractionScheduledWorkCompleted).toHaveBeenCalledTimes(2); @@ -1716,9 +1697,7 @@ describe('Profiler', () => { call = onRender.mock.calls[0]; expect(call[0]).toEqual('test-profiler'); expect(call[5]).toEqual(mockNow()); - if (ReactFeatureFlags.enableSchedulerTracing) { - expect(call[6]).toMatchInteractions([interactionTwo]); - } + expect(call[6]).toMatchInteractions([interactionTwo]); expect(onInteractionTraced).toHaveBeenCalledTimes(3); expect(onInteractionScheduledWorkCompleted).toHaveBeenCalledTimes(3); @@ -1849,11 +1828,10 @@ describe('Profiler', () => { let call = onRender.mock.calls[0]; expect(call[0]).toEqual('test'); expect(call[5]).toEqual(mockNow()); - expect(call[6]).toMatchInteractions( - ReactFeatureFlags.enableSchedulerTracing - ? [interactionLowPri, interactionHighPri] - : [], - ); + expect(call[6]).toMatchInteractions([ + interactionLowPri, + interactionHighPri, + ]); onRender.mockClear(); @@ -1866,9 +1844,7 @@ describe('Profiler', () => { call = onRender.mock.calls[0]; expect(call[0]).toEqual('test'); expect(call[5]).toEqual(mockNow()); - expect(call[6]).toMatchInteractions( - ReactFeatureFlags.enableSchedulerTracing ? [interactionLowPri] : [], - ); + expect(call[6]).toMatchInteractions([interactionLowPri]); expect(onInteractionTraced).toHaveBeenCalledTimes(2); expect(onInteractionScheduledWorkCompleted).toHaveBeenCalledTimes(1); @@ -1976,15 +1952,11 @@ describe('Profiler', () => { let call = onRender.mock.calls[0]; expect(call[0]).toEqual('test'); expect(call[5]).toEqual(firstCommitTime); - expect(call[6]).toMatchInteractions( - ReactFeatureFlags.enableSchedulerTracing ? [interactionOne] : [], - ); + expect(call[6]).toMatchInteractions([interactionOne]); call = onRender.mock.calls[1]; expect(call[0]).toEqual('test'); expect(call[5]).toEqual(mockNow()); - expect(call[6]).toMatchInteractions( - ReactFeatureFlags.enableSchedulerTracing ? [interactionOne] : [], - ); + expect(call[6]).toMatchInteractions([interactionOne]); onRender.mockClear(); @@ -2039,15 +2011,11 @@ describe('Profiler', () => { call = onRender.mock.calls[0]; expect(call[0]).toEqual('test'); expect(call[5]).toEqual(firstCommitTime); - expect(call[6]).toMatchInteractions( - ReactFeatureFlags.enableSchedulerTracing ? [interactionTwo] : [], - ); + expect(call[6]).toMatchInteractions([interactionTwo]); call = onRender.mock.calls[1]; expect(call[0]).toEqual('test'); expect(call[5]).toEqual(mockNow()); - expect(call[6]).toMatchInteractions( - ReactFeatureFlags.enableSchedulerTracing ? [interactionTwo] : [], - ); + expect(call[6]).toMatchInteractions([interactionTwo]); onRender.mockClear(); @@ -2104,15 +2072,11 @@ describe('Profiler', () => { call = onRender.mock.calls[0]; expect(call[0]).toEqual('test'); expect(call[5]).toEqual(firstCommitTime); - expect(call[6]).toMatchInteractions( - ReactFeatureFlags.enableSchedulerTracing ? [interactionThree] : [], - ); + expect(call[6]).toMatchInteractions([interactionThree]); call = onRender.mock.calls[1]; expect(call[0]).toEqual('test'); expect(call[5]).toEqual(mockNow()); - expect(call[6]).toMatchInteractions( - ReactFeatureFlags.enableSchedulerTracing ? [interactionThree] : [], - ); + expect(call[6]).toMatchInteractions([interactionThree]); }); it('should trace interactions associated with a parent component state update', () => { @@ -2171,9 +2135,7 @@ describe('Profiler', () => { expect(onRender).toHaveBeenCalledTimes(1); let call = onRender.mock.calls[0]; expect(call[0]).toEqual('test-profiler'); - expect(call[6]).toMatchInteractions( - ReactFeatureFlags.enableSchedulerTracing ? [interaction] : [], - ); + expect(call[6]).toMatchInteractions([interaction]); expect(onInteractionTraced).toHaveBeenCalledTimes(1); expect(onInteractionScheduledWorkCompleted).toHaveBeenCalledTimes(1); @@ -2259,9 +2221,7 @@ describe('Profiler', () => { let call = onRender.mock.calls[0]; expect(call[0]).toEqual('test-profiler'); - expect(call[6]).toMatchInteractions( - ReactFeatureFlags.enableSchedulerTracing ? [interaction] : [], - ); + expect(call[6]).toMatchInteractions([interaction]); expect(onInteractionTraced).toHaveBeenCalledTimes(1); expect(onInteractionScheduledWorkCompleted).not.toHaveBeenCalled(); @@ -2282,9 +2242,7 @@ describe('Profiler', () => { call = onRender.mock.calls[2]; expect(call[0]).toEqual('test-profiler'); - expect(call[6]).toMatchInteractions( - ReactFeatureFlags.enableSchedulerTracing ? [interaction] : [], - ); + expect(call[6]).toMatchInteractions([interaction]); expect(onInteractionTraced).toHaveBeenCalledTimes(1); expect(onInteractionScheduledWorkCompleted).toHaveBeenCalledTimes(1); diff --git a/packages/react/src/__tests__/ReactProfilerDOM-test.internal.js b/packages/react/src/__tests__/ReactProfilerDOM-test.internal.js index 354ccf25b8b..25a459d1178 100644 --- a/packages/react/src/__tests__/ReactProfilerDOM-test.internal.js +++ b/packages/react/src/__tests__/ReactProfilerDOM-test.internal.js @@ -46,7 +46,6 @@ function loadModules() { ReactFeatureFlags.debugRenderPhaseSideEffects = false; ReactFeatureFlags.debugRenderPhaseSideEffectsForStrictMode = false; ReactFeatureFlags.enableProfilerTimer = true; - ReactFeatureFlags.enableSchedulerTracing = true; React = require('react'); SchedulerTracing = require('scheduler/tracing'); diff --git a/packages/react/src/__tests__/ReactProfilerDevToolsIntegration-test.internal.js b/packages/react/src/__tests__/ReactProfilerDevToolsIntegration-test.internal.js index 3e486eefa36..9bf9e8c37a4 100644 --- a/packages/react/src/__tests__/ReactProfilerDevToolsIntegration-test.internal.js +++ b/packages/react/src/__tests__/ReactProfilerDevToolsIntegration-test.internal.js @@ -38,7 +38,6 @@ describe('ReactProfiler DevTools integration', () => { ReactFeatureFlags = require('shared/ReactFeatureFlags'); ReactFeatureFlags.enableProfilerTimer = true; - ReactFeatureFlags.enableSchedulerTracing = true; SchedulerTracing = require('scheduler/tracing'); React = require('react'); ReactTestRenderer = require('react-test-renderer'); diff --git a/packages/react/src/__tests__/__snapshots__/ReactProfiler-test.internal.js.snap b/packages/react/src/__tests__/__snapshots__/ReactProfiler-test.internal.js.snap index 91d0b594dcf..35da4c29611 100644 --- a/packages/react/src/__tests__/__snapshots__/ReactProfiler-test.internal.js.snap +++ b/packages/react/src/__tests__/__snapshots__/ReactProfiler-test.internal.js.snap @@ -1,6 +1,6 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP -exports[`Profiler works in profiling and non-profiling bundles enableSchedulerTracing:disabled enableProfilerTimer:disabled should render children 1`] = ` +exports[`Profiler works in profiling and non-profiling bundles enableProfilerTimer:disabled should render children 1`] = `
outside span @@ -14,11 +14,11 @@ exports[`Profiler works in profiling and non-profiling bundles enableSchedulerTr
`; -exports[`Profiler works in profiling and non-profiling bundles enableSchedulerTracing:disabled enableProfilerTimer:disabled should support an empty Profiler (with no children) 1`] = `null`; +exports[`Profiler works in profiling and non-profiling bundles enableProfilerTimer:disabled should support an empty Profiler (with no children) 1`] = `null`; -exports[`Profiler works in profiling and non-profiling bundles enableSchedulerTracing:disabled enableProfilerTimer:disabled should support an empty Profiler (with no children) 2`] = `
`; +exports[`Profiler works in profiling and non-profiling bundles enableProfilerTimer:disabled should support an empty Profiler (with no children) 2`] = `
`; -exports[`Profiler works in profiling and non-profiling bundles enableSchedulerTracing:disabled enableProfilerTimer:disabled should support nested Profilers 1`] = ` +exports[`Profiler works in profiling and non-profiling bundles enableProfilerTimer:disabled should support nested Profilers 1`] = ` Array [
outer function component @@ -32,7 +32,7 @@ Array [ ] `; -exports[`Profiler works in profiling and non-profiling bundles enableSchedulerTracing:disabled enableProfilerTimer:enabled should render children 1`] = ` +exports[`Profiler works in profiling and non-profiling bundles enableProfilerTimer:enabled should render children 1`] = `
outside span @@ -46,75 +46,11 @@ exports[`Profiler works in profiling and non-profiling bundles enableSchedulerTr
`; -exports[`Profiler works in profiling and non-profiling bundles enableSchedulerTracing:disabled enableProfilerTimer:enabled should support an empty Profiler (with no children) 1`] = `null`; +exports[`Profiler works in profiling and non-profiling bundles enableProfilerTimer:enabled should support an empty Profiler (with no children) 1`] = `null`; -exports[`Profiler works in profiling and non-profiling bundles enableSchedulerTracing:disabled enableProfilerTimer:enabled should support an empty Profiler (with no children) 2`] = `
`; +exports[`Profiler works in profiling and non-profiling bundles enableProfilerTimer:enabled should support an empty Profiler (with no children) 2`] = `
`; -exports[`Profiler works in profiling and non-profiling bundles enableSchedulerTracing:disabled enableProfilerTimer:enabled should support nested Profilers 1`] = ` -Array [ -
- outer function component -
, - - inner class component - , - - inner span - , -] -`; - -exports[`Profiler works in profiling and non-profiling bundles enableSchedulerTracing:enabled enableProfilerTimer:disabled should render children 1`] = ` -
- - outside span - - - inside span - - - function component - -
-`; - -exports[`Profiler works in profiling and non-profiling bundles enableSchedulerTracing:enabled enableProfilerTimer:disabled should support an empty Profiler (with no children) 1`] = `null`; - -exports[`Profiler works in profiling and non-profiling bundles enableSchedulerTracing:enabled enableProfilerTimer:disabled should support an empty Profiler (with no children) 2`] = `
`; - -exports[`Profiler works in profiling and non-profiling bundles enableSchedulerTracing:enabled enableProfilerTimer:disabled should support nested Profilers 1`] = ` -Array [ -
- outer function component -
, - - inner class component - , - - inner span - , -] -`; - -exports[`Profiler works in profiling and non-profiling bundles enableSchedulerTracing:enabled enableProfilerTimer:enabled should render children 1`] = ` -
- - outside span - - - inside span - - - function component - -
-`; - -exports[`Profiler works in profiling and non-profiling bundles enableSchedulerTracing:enabled enableProfilerTimer:enabled should support an empty Profiler (with no children) 1`] = `null`; - -exports[`Profiler works in profiling and non-profiling bundles enableSchedulerTracing:enabled enableProfilerTimer:enabled should support an empty Profiler (with no children) 2`] = `
`; - -exports[`Profiler works in profiling and non-profiling bundles enableSchedulerTracing:enabled enableProfilerTimer:enabled should support nested Profilers 1`] = ` +exports[`Profiler works in profiling and non-profiling bundles enableProfilerTimer:enabled should support nested Profilers 1`] = ` Array [
outer function component diff --git a/packages/scheduler/src/Tracing.js b/packages/scheduler/src/Tracing.js index 1223d44a2cf..4db6a3a02c6 100644 --- a/packages/scheduler/src/Tracing.js +++ b/packages/scheduler/src/Tracing.js @@ -7,8 +7,6 @@ * @flow */ -import {enableSchedulerTracing} from 'shared/ReactFeatureFlags'; - export type Interaction = {| __count: number, id: number, @@ -65,27 +63,18 @@ let threadIDCounter: number = 0; // Interactions "stack"– // Meaning that newly traced interactions are appended to the previously active set. // When an interaction goes out of scope, the previous set (if any) is restored. -let interactionsRef: InteractionsRef = (null: any); +let interactionsRef: InteractionsRef = { + current: new Set(), +}; // Listener(s) to notify when interactions begin and end. -let subscriberRef: SubscriberRef = (null: any); - -if (enableSchedulerTracing) { - interactionsRef = { - current: new Set(), - }; - subscriberRef = { - current: null, - }; -} +let subscriberRef: SubscriberRef = { + current: null, +}; export {interactionsRef as __interactionsRef, subscriberRef as __subscriberRef}; export function unstable_clear(callback: Function): any { - if (!enableSchedulerTracing) { - return callback(); - } - const prevInteractions = interactionsRef.current; interactionsRef.current = new Set(); @@ -97,11 +86,7 @@ export function unstable_clear(callback: Function): any { } export function unstable_getCurrent(): Set | null { - if (!enableSchedulerTracing) { - return null; - } else { - return interactionsRef.current; - } + return interactionsRef.current; } export function unstable_getThreadID(): number { @@ -114,10 +99,6 @@ export function unstable_trace( callback: Function, threadID: number = DEFAULT_THREAD_ID, ): any { - if (!enableSchedulerTracing) { - return callback(); - } - const interaction: Interaction = { __count: 1, id: interactionIDCounter++, @@ -176,10 +157,6 @@ export function unstable_wrap( callback: Function, threadID: number = DEFAULT_THREAD_ID, ): Function { - if (!enableSchedulerTracing) { - return callback; - } - const wrappedInteractions = interactionsRef.current; let subscriber = subscriberRef.current; diff --git a/packages/scheduler/src/TracingSubscriptions.js b/packages/scheduler/src/TracingSubscriptions.js index 9fd687adf41..f5e609313cf 100644 --- a/packages/scheduler/src/TracingSubscriptions.js +++ b/packages/scheduler/src/TracingSubscriptions.js @@ -9,38 +9,30 @@ import type {Interaction, Subscriber} from './Tracing'; -import {enableSchedulerTracing} from 'shared/ReactFeatureFlags'; import {__subscriberRef} from './Tracing'; -let subscribers: Set = (null: any); -if (enableSchedulerTracing) { - subscribers = new Set(); -} +let subscribers: Set = new Set(); export function unstable_subscribe(subscriber: Subscriber): void { - if (enableSchedulerTracing) { - subscribers.add(subscriber); - - if (subscribers.size === 1) { - __subscriberRef.current = { - onInteractionScheduledWorkCompleted, - onInteractionTraced, - onWorkCanceled, - onWorkScheduled, - onWorkStarted, - onWorkStopped, - }; - } + subscribers.add(subscriber); + + if (subscribers.size === 1) { + __subscriberRef.current = { + onInteractionScheduledWorkCompleted, + onInteractionTraced, + onWorkCanceled, + onWorkScheduled, + onWorkStarted, + onWorkStopped, + }; } } export function unstable_unsubscribe(subscriber: Subscriber): void { - if (enableSchedulerTracing) { - subscribers.delete(subscriber); + subscribers.delete(subscriber); - if (subscribers.size === 0) { - __subscriberRef.current = null; - } + if (subscribers.size === 0) { + __subscriberRef.current = null; } } diff --git a/packages/scheduler/src/__tests__/Tracing-test.internal.js b/packages/scheduler/src/__tests__/Tracing-test.internal.js deleted file mode 100644 index 658b69313ca..00000000000 --- a/packages/scheduler/src/__tests__/Tracing-test.internal.js +++ /dev/null @@ -1,375 +0,0 @@ -/** - * 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. - * - * @jest-environment node - */ -'use strict'; - -describe('Tracing', () => { - let SchedulerTracing; - let ReactFeatureFlags; - - let advanceTimeBy; - let currentTime; - - function loadModules({enableSchedulerTracing}) { - jest.resetModules(); - jest.useFakeTimers(); - - currentTime = 0; - Date.now = jest.fn().mockImplementation(() => currentTime); - - advanceTimeBy = amount => { - currentTime += amount; - }; - - ReactFeatureFlags = require('shared/ReactFeatureFlags'); - ReactFeatureFlags.enableSchedulerTracing = enableSchedulerTracing; - - SchedulerTracing = require('scheduler/tracing'); - } - - describe('enableSchedulerTracing enabled', () => { - beforeEach(() => loadModules({enableSchedulerTracing: true})); - - it('should return the value of a traced function', () => { - expect( - SchedulerTracing.unstable_trace('arbitrary', currentTime, () => 123), - ).toBe(123); - }); - - it('should return the value of a clear function', () => { - expect(SchedulerTracing.unstable_clear(() => 123)).toBe(123); - }); - - it('should return the value of a wrapped function', () => { - let wrapped; - SchedulerTracing.unstable_trace('arbitrary', currentTime, () => { - wrapped = SchedulerTracing.unstable_wrap(() => 123); - }); - expect(wrapped()).toBe(123); - }); - - it('should pass arguments through to a wrapped function', done => { - let wrapped; - SchedulerTracing.unstable_trace('arbitrary', currentTime, () => { - wrapped = SchedulerTracing.unstable_wrap((param1, param2) => { - expect(param1).toBe('foo'); - expect(param2).toBe('bar'); - done(); - }); - }); - wrapped('foo', 'bar'); - }); - - it('should return an empty set when outside of a traced event', () => { - expect(SchedulerTracing.unstable_getCurrent()).toContainNoInteractions(); - }); - - it('should report the traced interaction from within the trace callback', done => { - advanceTimeBy(100); - - SchedulerTracing.unstable_trace('some event', currentTime, () => { - const interactions = SchedulerTracing.unstable_getCurrent(); - expect(interactions).toMatchInteractions([ - {name: 'some event', timestamp: 100}, - ]); - - done(); - }); - }); - - it('should report the traced interaction from within wrapped callbacks', done => { - let wrappedIndirection; - - function indirection() { - const interactions = SchedulerTracing.unstable_getCurrent(); - expect(interactions).toMatchInteractions([ - {name: 'some event', timestamp: 100}, - ]); - - done(); - } - - advanceTimeBy(100); - - SchedulerTracing.unstable_trace('some event', currentTime, () => { - wrappedIndirection = SchedulerTracing.unstable_wrap(indirection); - }); - - advanceTimeBy(50); - - wrappedIndirection(); - }); - - it('should clear the interaction stack for traced callbacks', () => { - let innerTestReached = false; - - SchedulerTracing.unstable_trace('outer event', currentTime, () => { - expect(SchedulerTracing.unstable_getCurrent()).toMatchInteractions([ - {name: 'outer event'}, - ]); - - SchedulerTracing.unstable_clear(() => { - expect(SchedulerTracing.unstable_getCurrent()).toMatchInteractions( - [], - ); - - SchedulerTracing.unstable_trace('inner event', currentTime, () => { - expect(SchedulerTracing.unstable_getCurrent()).toMatchInteractions([ - {name: 'inner event'}, - ]); - - innerTestReached = true; - }); - }); - - expect(SchedulerTracing.unstable_getCurrent()).toMatchInteractions([ - {name: 'outer event'}, - ]); - }); - - expect(innerTestReached).toBe(true); - }); - - it('should clear the interaction stack for wrapped callbacks', () => { - let innerTestReached = false; - let wrappedIndirection; - - const indirection = jest.fn(() => { - expect(SchedulerTracing.unstable_getCurrent()).toMatchInteractions([ - {name: 'outer event'}, - ]); - - SchedulerTracing.unstable_clear(() => { - expect(SchedulerTracing.unstable_getCurrent()).toMatchInteractions( - [], - ); - - SchedulerTracing.unstable_trace('inner event', currentTime, () => { - expect(SchedulerTracing.unstable_getCurrent()).toMatchInteractions([ - {name: 'inner event'}, - ]); - - innerTestReached = true; - }); - }); - - expect(SchedulerTracing.unstable_getCurrent()).toMatchInteractions([ - {name: 'outer event'}, - ]); - }); - - SchedulerTracing.unstable_trace('outer event', currentTime, () => { - wrappedIndirection = SchedulerTracing.unstable_wrap(indirection); - }); - - wrappedIndirection(); - - expect(innerTestReached).toBe(true); - }); - - it('should support nested traced events', done => { - advanceTimeBy(100); - - let innerIndirectionTraced = false; - let outerIndirectionTraced = false; - - function innerIndirection() { - const interactions = SchedulerTracing.unstable_getCurrent(); - expect(interactions).toMatchInteractions([ - {name: 'outer event', timestamp: 100}, - {name: 'inner event', timestamp: 150}, - ]); - - innerIndirectionTraced = true; - } - - function outerIndirection() { - const interactions = SchedulerTracing.unstable_getCurrent(); - expect(interactions).toMatchInteractions([ - {name: 'outer event', timestamp: 100}, - ]); - - outerIndirectionTraced = true; - } - - SchedulerTracing.unstable_trace('outer event', currentTime, () => { - // Verify the current traced event - let interactions = SchedulerTracing.unstable_getCurrent(); - expect(interactions).toMatchInteractions([ - {name: 'outer event', timestamp: 100}, - ]); - - advanceTimeBy(50); - - const wrapperOuterIndirection = SchedulerTracing.unstable_wrap( - outerIndirection, - ); - - let wrapperInnerIndirection; - let innerEventTraced = false; - - // Verify that a nested event is properly traced - SchedulerTracing.unstable_trace('inner event', currentTime, () => { - interactions = SchedulerTracing.unstable_getCurrent(); - expect(interactions).toMatchInteractions([ - {name: 'outer event', timestamp: 100}, - {name: 'inner event', timestamp: 150}, - ]); - - // Verify that a wrapped outer callback is properly traced - wrapperOuterIndirection(); - expect(outerIndirectionTraced).toBe(true); - - wrapperInnerIndirection = SchedulerTracing.unstable_wrap( - innerIndirection, - ); - - innerEventTraced = true; - }); - - expect(innerEventTraced).toBe(true); - - // Verify that the original event is restored - interactions = SchedulerTracing.unstable_getCurrent(); - expect(interactions).toMatchInteractions([ - {name: 'outer event', timestamp: 100}, - ]); - - // Verify that a wrapped nested callback is properly traced - wrapperInnerIndirection(); - expect(innerIndirectionTraced).toBe(true); - - done(); - }); - }); - - describe('error handling', () => { - it('should reset state appropriately when an error occurs in a trace callback', done => { - advanceTimeBy(100); - - SchedulerTracing.unstable_trace('outer event', currentTime, () => { - expect(() => { - SchedulerTracing.unstable_trace('inner event', currentTime, () => { - throw Error('intentional'); - }); - }).toThrow(); - - expect(SchedulerTracing.unstable_getCurrent()).toMatchInteractions([ - {name: 'outer event', timestamp: 100}, - ]); - - done(); - }); - }); - - it('should reset state appropriately when an error occurs in a wrapped callback', done => { - advanceTimeBy(100); - - SchedulerTracing.unstable_trace('outer event', currentTime, () => { - let wrappedCallback; - - SchedulerTracing.unstable_trace('inner event', currentTime, () => { - wrappedCallback = SchedulerTracing.unstable_wrap(() => { - throw Error('intentional'); - }); - }); - - expect(wrappedCallback).toThrow(); - - expect(SchedulerTracing.unstable_getCurrent()).toMatchInteractions([ - {name: 'outer event', timestamp: 100}, - ]); - - done(); - }); - }); - }); - - describe('advanced integration', () => { - it('should return a unique threadID per request', () => { - expect(SchedulerTracing.unstable_getThreadID()).not.toBe( - SchedulerTracing.unstable_getThreadID(), - ); - }); - - it('should expose the current set of interactions to be externally manipulated', () => { - SchedulerTracing.unstable_trace('outer event', currentTime, () => { - expect(SchedulerTracing.__interactionsRef.current).toBe( - SchedulerTracing.unstable_getCurrent(), - ); - - SchedulerTracing.__interactionsRef.current = new Set([ - {name: 'override event'}, - ]); - - expect(SchedulerTracing.unstable_getCurrent()).toMatchInteractions([ - {name: 'override event'}, - ]); - }); - }); - - it('should expose a subscriber ref to be externally manipulated', () => { - SchedulerTracing.unstable_trace('outer event', currentTime, () => { - expect(SchedulerTracing.__subscriberRef).toEqual({ - current: null, - }); - }); - }); - }); - }); - - describe('enableSchedulerTracing disabled', () => { - beforeEach(() => loadModules({enableSchedulerTracing: false})); - - it('should return the value of a traced function', () => { - expect( - SchedulerTracing.unstable_trace('arbitrary', currentTime, () => 123), - ).toBe(123); - }); - - it('should return the value of a wrapped function', () => { - let wrapped; - SchedulerTracing.unstable_trace('arbitrary', currentTime, () => { - wrapped = SchedulerTracing.unstable_wrap(() => 123); - }); - expect(wrapped()).toBe(123); - }); - - it('should return null for traced interactions', () => { - expect(SchedulerTracing.unstable_getCurrent()).toBe(null); - }); - - it('should execute traced callbacks', done => { - SchedulerTracing.unstable_trace('some event', currentTime, () => { - expect(SchedulerTracing.unstable_getCurrent()).toBe(null); - - done(); - }); - }); - - it('should return the value of a clear function', () => { - expect(SchedulerTracing.unstable_clear(() => 123)).toBe(123); - }); - - it('should execute wrapped callbacks', done => { - const wrappedCallback = SchedulerTracing.unstable_wrap(() => { - expect(SchedulerTracing.unstable_getCurrent()).toBe(null); - - done(); - }); - - wrappedCallback(); - }); - - describe('advanced integration', () => { - it('should not create unnecessary objects', () => { - expect(SchedulerTracing.__interactionsRef).toBe(null); - }); - }); - }); -}); diff --git a/packages/scheduler/src/__tests__/Tracing-test.js b/packages/scheduler/src/__tests__/Tracing-test.js index 5259be47925..6cd3ff826a1 100644 --- a/packages/scheduler/src/__tests__/Tracing-test.js +++ b/packages/scheduler/src/__tests__/Tracing-test.js @@ -11,8 +11,19 @@ describe('Tracing', () => { let SchedulerTracing; + let advanceTimeBy; + let currentTime; + beforeEach(() => { jest.resetModules(); + jest.useFakeTimers(); + + currentTime = 0; + Date.now = jest.fn().mockImplementation(() => currentTime); + + advanceTimeBy = amount => { + currentTime += amount; + }; SchedulerTracing = require('scheduler/tracing'); }); @@ -48,4 +59,287 @@ describe('Tracing', () => { wrappedCallback(); }); + + it('should return the value of a traced function', () => { + expect( + SchedulerTracing.unstable_trace('arbitrary', currentTime, () => 123), + ).toBe(123); + }); + + it('should return the value of a clear function', () => { + expect(SchedulerTracing.unstable_clear(() => 123)).toBe(123); + }); + + it('should return the value of a wrapped function', () => { + let wrapped; + SchedulerTracing.unstable_trace('arbitrary', currentTime, () => { + wrapped = SchedulerTracing.unstable_wrap(() => 123); + }); + expect(wrapped()).toBe(123); + }); + + it('should pass arguments through to a wrapped function', done => { + let wrapped; + SchedulerTracing.unstable_trace('arbitrary', currentTime, () => { + wrapped = SchedulerTracing.unstable_wrap((param1, param2) => { + expect(param1).toBe('foo'); + expect(param2).toBe('bar'); + done(); + }); + }); + wrapped('foo', 'bar'); + }); + + it('should return an empty set when outside of a traced event', () => { + expect(SchedulerTracing.unstable_getCurrent()).toContainNoInteractions(); + }); + + it('should report the traced interaction from within the trace callback', done => { + advanceTimeBy(100); + + SchedulerTracing.unstable_trace('some event', currentTime, () => { + const interactions = SchedulerTracing.unstable_getCurrent(); + expect(interactions).toMatchInteractions([ + {name: 'some event', timestamp: 100}, + ]); + + done(); + }); + }); + + it('should report the traced interaction from within wrapped callbacks', done => { + let wrappedIndirection; + + function indirection() { + const interactions = SchedulerTracing.unstable_getCurrent(); + expect(interactions).toMatchInteractions([ + {name: 'some event', timestamp: 100}, + ]); + + done(); + } + + advanceTimeBy(100); + + SchedulerTracing.unstable_trace('some event', currentTime, () => { + wrappedIndirection = SchedulerTracing.unstable_wrap(indirection); + }); + + advanceTimeBy(50); + + wrappedIndirection(); + }); + + it('should clear the interaction stack for traced callbacks', () => { + let innerTestReached = false; + + SchedulerTracing.unstable_trace('outer event', currentTime, () => { + expect(SchedulerTracing.unstable_getCurrent()).toMatchInteractions([ + {name: 'outer event'}, + ]); + + SchedulerTracing.unstable_clear(() => { + expect(SchedulerTracing.unstable_getCurrent()).toMatchInteractions([]); + + SchedulerTracing.unstable_trace('inner event', currentTime, () => { + expect(SchedulerTracing.unstable_getCurrent()).toMatchInteractions([ + {name: 'inner event'}, + ]); + + innerTestReached = true; + }); + }); + + expect(SchedulerTracing.unstable_getCurrent()).toMatchInteractions([ + {name: 'outer event'}, + ]); + }); + + expect(innerTestReached).toBe(true); + }); + + it('should clear the interaction stack for wrapped callbacks', () => { + let innerTestReached = false; + let wrappedIndirection; + + const indirection = jest.fn(() => { + expect(SchedulerTracing.unstable_getCurrent()).toMatchInteractions([ + {name: 'outer event'}, + ]); + + SchedulerTracing.unstable_clear(() => { + expect(SchedulerTracing.unstable_getCurrent()).toMatchInteractions([]); + + SchedulerTracing.unstable_trace('inner event', currentTime, () => { + expect(SchedulerTracing.unstable_getCurrent()).toMatchInteractions([ + {name: 'inner event'}, + ]); + + innerTestReached = true; + }); + }); + + expect(SchedulerTracing.unstable_getCurrent()).toMatchInteractions([ + {name: 'outer event'}, + ]); + }); + + SchedulerTracing.unstable_trace('outer event', currentTime, () => { + wrappedIndirection = SchedulerTracing.unstable_wrap(indirection); + }); + + wrappedIndirection(); + + expect(innerTestReached).toBe(true); + }); + + it('should support nested traced events', done => { + advanceTimeBy(100); + + let innerIndirectionTraced = false; + let outerIndirectionTraced = false; + + function innerIndirection() { + const interactions = SchedulerTracing.unstable_getCurrent(); + expect(interactions).toMatchInteractions([ + {name: 'outer event', timestamp: 100}, + {name: 'inner event', timestamp: 150}, + ]); + + innerIndirectionTraced = true; + } + + function outerIndirection() { + const interactions = SchedulerTracing.unstable_getCurrent(); + expect(interactions).toMatchInteractions([ + {name: 'outer event', timestamp: 100}, + ]); + + outerIndirectionTraced = true; + } + + SchedulerTracing.unstable_trace('outer event', currentTime, () => { + // Verify the current traced event + let interactions = SchedulerTracing.unstable_getCurrent(); + expect(interactions).toMatchInteractions([ + {name: 'outer event', timestamp: 100}, + ]); + + advanceTimeBy(50); + + const wrapperOuterIndirection = SchedulerTracing.unstable_wrap( + outerIndirection, + ); + + let wrapperInnerIndirection; + let innerEventTraced = false; + + // Verify that a nested event is properly traced + SchedulerTracing.unstable_trace('inner event', currentTime, () => { + interactions = SchedulerTracing.unstable_getCurrent(); + expect(interactions).toMatchInteractions([ + {name: 'outer event', timestamp: 100}, + {name: 'inner event', timestamp: 150}, + ]); + + // Verify that a wrapped outer callback is properly traced + wrapperOuterIndirection(); + expect(outerIndirectionTraced).toBe(true); + + wrapperInnerIndirection = SchedulerTracing.unstable_wrap( + innerIndirection, + ); + + innerEventTraced = true; + }); + + expect(innerEventTraced).toBe(true); + + // Verify that the original event is restored + interactions = SchedulerTracing.unstable_getCurrent(); + expect(interactions).toMatchInteractions([ + {name: 'outer event', timestamp: 100}, + ]); + + // Verify that a wrapped nested callback is properly traced + wrapperInnerIndirection(); + expect(innerIndirectionTraced).toBe(true); + + done(); + }); + }); + + describe('error handling', () => { + it('should reset state appropriately when an error occurs in a trace callback', done => { + advanceTimeBy(100); + + SchedulerTracing.unstable_trace('outer event', currentTime, () => { + expect(() => { + SchedulerTracing.unstable_trace('inner event', currentTime, () => { + throw Error('intentional'); + }); + }).toThrow(); + + expect(SchedulerTracing.unstable_getCurrent()).toMatchInteractions([ + {name: 'outer event', timestamp: 100}, + ]); + + done(); + }); + }); + + it('should reset state appropriately when an error occurs in a wrapped callback', done => { + advanceTimeBy(100); + + SchedulerTracing.unstable_trace('outer event', currentTime, () => { + let wrappedCallback; + + SchedulerTracing.unstable_trace('inner event', currentTime, () => { + wrappedCallback = SchedulerTracing.unstable_wrap(() => { + throw Error('intentional'); + }); + }); + + expect(wrappedCallback).toThrow(); + + expect(SchedulerTracing.unstable_getCurrent()).toMatchInteractions([ + {name: 'outer event', timestamp: 100}, + ]); + + done(); + }); + }); + }); + + describe('advanced integration', () => { + it('should return a unique threadID per request', () => { + expect(SchedulerTracing.unstable_getThreadID()).not.toBe( + SchedulerTracing.unstable_getThreadID(), + ); + }); + + it('should expose the current set of interactions to be externally manipulated', () => { + SchedulerTracing.unstable_trace('outer event', currentTime, () => { + expect(SchedulerTracing.__interactionsRef.current).toBe( + SchedulerTracing.unstable_getCurrent(), + ); + + SchedulerTracing.__interactionsRef.current = new Set([ + {name: 'override event'}, + ]); + + expect(SchedulerTracing.unstable_getCurrent()).toMatchInteractions([ + {name: 'override event'}, + ]); + }); + }); + + it('should expose a subscriber ref to be externally manipulated', () => { + SchedulerTracing.unstable_trace('outer event', currentTime, () => { + expect(SchedulerTracing.__subscriberRef).toEqual({ + current: null, + }); + }); + }); + }); }); diff --git a/packages/scheduler/src/__tests__/TracingSubscriptions-test.internal.js b/packages/scheduler/src/__tests__/TracingSubscriptions-test.js similarity index 97% rename from packages/scheduler/src/__tests__/TracingSubscriptions-test.internal.js rename to packages/scheduler/src/__tests__/TracingSubscriptions-test.js index 6eff59a6cf8..34967f2ae19 100644 --- a/packages/scheduler/src/__tests__/TracingSubscriptions-test.internal.js +++ b/packages/scheduler/src/__tests__/TracingSubscriptions-test.js @@ -10,7 +10,6 @@ describe('TracingSubscriptions', () => { let SchedulerTracing; - let ReactFeatureFlags; let currentTime; @@ -33,15 +32,12 @@ describe('TracingSubscriptions', () => { const secondEvent = {id: 1, name: 'second', timestamp: 0}; const threadID = 123; - function loadModules({enableSchedulerTracing, autoSubscribe = true}) { + function loadModules({autoSubscribe = true} = {}) { jest.resetModules(); jest.useFakeTimers(); currentTime = 0; - ReactFeatureFlags = require('shared/ReactFeatureFlags'); - ReactFeatureFlags.enableSchedulerTracing = enableSchedulerTracing; - SchedulerTracing = require('scheduler/tracing'); throwInOnInteractionScheduledWorkCompleted = false; @@ -107,10 +103,10 @@ describe('TracingSubscriptions', () => { } describe('enabled', () => { - beforeEach(() => loadModules({enableSchedulerTracing: true})); + beforeEach(loadModules); it('should lazily subscribe to tracing and unsubscribe again if there are no external subscribers', () => { - loadModules({enableSchedulerTracing: true, autoSubscribe: false}); + loadModules({autoSubscribe: false}); expect(SchedulerTracing.__subscriberRef.current).toBe(null); SchedulerTracing.unstable_subscribe(firstSubscriber); @@ -612,10 +608,4 @@ describe('TracingSubscriptions', () => { expect(onInteractionTraced).not.toHaveBeenCalled(); }); }); - - describe('disabled', () => { - beforeEach(() => loadModules({enableSchedulerTracing: false})); - - // TODO - }); }); diff --git a/packages/shared/ReactFeatureFlags.js b/packages/shared/ReactFeatureFlags.js index 27fcfb8c21d..10d7045ef19 100644 --- a/packages/shared/ReactFeatureFlags.js +++ b/packages/shared/ReactFeatureFlags.js @@ -29,9 +29,6 @@ export const warnAboutDeprecatedLifecycles = false; // Gather advanced timing metrics for Profiler subtrees. export const enableProfilerTimer = __PROFILE__; -// Trace which interactions trigger each commit. -export const enableSchedulerTracing = __PROFILE__; - // Only used in www builds. export const enableSuspenseServerRenderer = false; // TODO: __DEV__? Here it might just be false. diff --git a/packages/shared/forks/ReactFeatureFlags.native-fb.js b/packages/shared/forks/ReactFeatureFlags.native-fb.js index 943cef96e8a..7043851c895 100644 --- a/packages/shared/forks/ReactFeatureFlags.native-fb.js +++ b/packages/shared/forks/ReactFeatureFlags.native-fb.js @@ -19,7 +19,6 @@ export const {debugRenderPhaseSideEffects} = require('ReactFeatureFlags'); export const enableHooks = true; export const enableUserTimingAPI = __DEV__; export const enableProfilerTimer = __PROFILE__; -export const enableSchedulerTracing = __PROFILE__; export const enableSuspenseServerRenderer = false; export const enableStableConcurrentModeAPIs = false; export const warnAboutShorthandPropertyCollision = false; diff --git a/packages/shared/forks/ReactFeatureFlags.native-oss.js b/packages/shared/forks/ReactFeatureFlags.native-oss.js index 7e24ece3674..6564cd4189b 100644 --- a/packages/shared/forks/ReactFeatureFlags.native-oss.js +++ b/packages/shared/forks/ReactFeatureFlags.native-oss.js @@ -19,7 +19,6 @@ export const enableUserTimingAPI = __DEV__; export const replayFailedUnitOfWorkWithInvokeGuardedCallback = __DEV__; export const warnAboutDeprecatedLifecycles = false; export const enableProfilerTimer = __PROFILE__; -export const enableSchedulerTracing = __PROFILE__; export const enableSuspenseServerRenderer = false; export const disableInputAttributeSyncing = false; export const enableStableConcurrentModeAPIs = false; diff --git a/packages/shared/forks/ReactFeatureFlags.persistent.js b/packages/shared/forks/ReactFeatureFlags.persistent.js index caa763c94c7..fc049ea9158 100644 --- a/packages/shared/forks/ReactFeatureFlags.persistent.js +++ b/packages/shared/forks/ReactFeatureFlags.persistent.js @@ -19,7 +19,6 @@ export const enableHooks = false; export const warnAboutDeprecatedLifecycles = false; export const replayFailedUnitOfWorkWithInvokeGuardedCallback = __DEV__; export const enableProfilerTimer = __PROFILE__; -export const enableSchedulerTracing = __PROFILE__; export const enableSuspenseServerRenderer = false; export const disableInputAttributeSyncing = false; export const enableStableConcurrentModeAPIs = false; diff --git a/packages/shared/forks/ReactFeatureFlags.test-renderer.js b/packages/shared/forks/ReactFeatureFlags.test-renderer.js index f55f419a6d4..5d43fbf3203 100644 --- a/packages/shared/forks/ReactFeatureFlags.test-renderer.js +++ b/packages/shared/forks/ReactFeatureFlags.test-renderer.js @@ -19,7 +19,6 @@ export const enableHooks = false; export const warnAboutDeprecatedLifecycles = false; export const replayFailedUnitOfWorkWithInvokeGuardedCallback = false; export const enableProfilerTimer = false; -export const enableSchedulerTracing = false; export const enableSuspenseServerRenderer = false; export const disableInputAttributeSyncing = false; export const enableStableConcurrentModeAPIs = false; diff --git a/packages/shared/forks/ReactFeatureFlags.test-renderer.www.js b/packages/shared/forks/ReactFeatureFlags.test-renderer.www.js index 516f5738d5a..c446b0c932c 100644 --- a/packages/shared/forks/ReactFeatureFlags.test-renderer.www.js +++ b/packages/shared/forks/ReactFeatureFlags.test-renderer.www.js @@ -19,7 +19,6 @@ export const enableHooks = true; export const warnAboutDeprecatedLifecycles = false; export const replayFailedUnitOfWorkWithInvokeGuardedCallback = false; export const enableProfilerTimer = false; -export const enableSchedulerTracing = false; export const enableSuspenseServerRenderer = false; export const enableStableConcurrentModeAPIs = false; export const enableSchedulerDebugging = false; diff --git a/packages/shared/forks/ReactFeatureFlags.www.js b/packages/shared/forks/ReactFeatureFlags.www.js index 225dee98a36..a6e7f3091c5 100644 --- a/packages/shared/forks/ReactFeatureFlags.www.js +++ b/packages/shared/forks/ReactFeatureFlags.www.js @@ -33,7 +33,6 @@ export const enableHooks = true; export let enableUserTimingAPI = __DEV__; export const enableProfilerTimer = __PROFILE__; -export const enableSchedulerTracing = __PROFILE__; export const enableSchedulerDebugging = __DEV__; // TODO or just true export const enableStableConcurrentModeAPIs = false;