diff --git a/packages/react-reconciler/src/ReactFiber.js b/packages/react-reconciler/src/ReactFiber.js
index 792cd1def4db5..370c559b57014 100644
--- a/packages/react-reconciler/src/ReactFiber.js
+++ b/packages/react-reconciler/src/ReactFiber.js
@@ -815,13 +815,8 @@ function createFiberFromProfiler(
key: null | string,
): Fiber {
if (__DEV__) {
- if (
- typeof pendingProps.id !== 'string' ||
- typeof pendingProps.onRender !== 'function'
- ) {
- console.error(
- 'Profiler must specify an "id" string and "onRender" function as props',
- );
+ if (typeof pendingProps.id !== 'string') {
+ console.error('Profiler must specify an "id" as a prop');
}
}
@@ -831,6 +826,13 @@ function createFiberFromProfiler(
fiber.type = REACT_PROFILER_TYPE;
fiber.expirationTime = expirationTime;
+ if (enableProfilerTimer) {
+ fiber.stateNode = {
+ effectDuration: 0,
+ passiveEffectDuration: 0,
+ };
+ }
+
return fiber;
}
diff --git a/packages/react-reconciler/src/ReactFiberBeginWork.js b/packages/react-reconciler/src/ReactFiberBeginWork.js
index 9db5886a6c2b4..3738219bfc57c 100644
--- a/packages/react-reconciler/src/ReactFiberBeginWork.js
+++ b/packages/react-reconciler/src/ReactFiberBeginWork.js
@@ -577,6 +577,12 @@ function updateProfiler(
) {
if (enableProfilerTimer) {
workInProgress.effectTag |= Update;
+
+ // Reset effect durations for the next eventual effect phase.
+ // These are reset during render to allow the DevTools commit hook a chance to read them,
+ const stateNode = workInProgress.stateNode;
+ stateNode.effectDuration = 0;
+ stateNode.passiveEffectDuration = 0;
}
const nextProps = workInProgress.pendingProps;
const nextChildren = nextProps.children;
@@ -2974,6 +2980,12 @@ function beginWork(
if (hasChildWork) {
workInProgress.effectTag |= Update;
}
+
+ // Reset effect durations for the next eventual effect phase.
+ // These are reset during render to allow the DevTools commit hook a chance to read them,
+ const stateNode = workInProgress.stateNode;
+ stateNode.effectDuration = 0;
+ stateNode.passiveEffectDuration = 0;
}
break;
case SuspenseComponent: {
diff --git a/packages/react-reconciler/src/ReactFiberCommitWork.js b/packages/react-reconciler/src/ReactFiberCommitWork.js
index d22f2d296a733..6dd0e9bfe4306 100644
--- a/packages/react-reconciler/src/ReactFiberCommitWork.js
+++ b/packages/react-reconciler/src/ReactFiberCommitWork.js
@@ -75,7 +75,14 @@ import {startPhaseTimer, stopPhaseTimer} from './ReactDebugFiberPerf';
import {getStackByFiberInDevAndProd} from './ReactCurrentFiber';
import {logCapturedError} from './ReactFiberErrorLogger';
import {resolveDefaultProps} from './ReactFiberLazyComponent';
-import {getCommitTime} from './ReactProfilerTimer';
+import {
+ getCommitTime,
+ recordLayoutEffectDuration,
+ recordPassiveEffectDuration,
+ startLayoutEffectTimer,
+ startPassiveEffectTimer,
+} from './ReactProfilerTimer';
+import {ProfileMode} from './ReactTypeOfMode';
import {commitUpdateQueue} from './ReactUpdateQueue';
import {
getPublicInstance,
@@ -401,11 +408,18 @@ export function commitPassiveHookEffects(finishedWork: Fiber): void {
case ForwardRef:
case SimpleMemoComponent:
case Chunk: {
- // TODO (#17945) We should call all passive destroy functions (for all fibers)
- // before calling any create functions. The current approach only serializes
- // these for a single fiber.
- commitHookEffectList(HookPassive, NoHookEffect, finishedWork);
- commitHookEffectList(NoHookEffect, HookPassive, finishedWork);
+ if (enableProfilerTimer && finishedWork.mode & ProfileMode) {
+ try {
+ startPassiveEffectTimer();
+ commitHookEffectList(HookPassive, NoHookEffect, finishedWork);
+ commitHookEffectList(NoHookEffect, HookPassive, finishedWork);
+ } finally {
+ recordPassiveEffectDuration(finishedWork);
+ }
+ } else {
+ commitHookEffectList(HookPassive, NoHookEffect, finishedWork);
+ commitHookEffectList(NoHookEffect, HookPassive, finishedWork);
+ }
break;
}
default:
@@ -421,7 +435,16 @@ export function commitPassiveHookUnmountEffects(finishedWork: Fiber): void {
case ForwardRef:
case SimpleMemoComponent:
case Chunk: {
- commitHookEffectList(HookPassive, NoHookEffect, finishedWork);
+ if (enableProfilerTimer && finishedWork.mode & ProfileMode) {
+ try {
+ startPassiveEffectTimer();
+ commitHookEffectList(HookPassive, NoHookEffect, finishedWork);
+ } finally {
+ recordPassiveEffectDuration(finishedWork);
+ }
+ } else {
+ commitHookEffectList(HookPassive, NoHookEffect, finishedWork);
+ }
break;
}
default:
@@ -437,7 +460,16 @@ export function commitPassiveHookMountEffects(finishedWork: Fiber): void {
case ForwardRef:
case SimpleMemoComponent:
case Chunk: {
- commitHookEffectList(NoHookEffect, HookPassive, finishedWork);
+ if (enableProfilerTimer && finishedWork.mode & ProfileMode) {
+ try {
+ startPassiveEffectTimer();
+ commitHookEffectList(NoHookEffect, HookPassive, finishedWork);
+ } finally {
+ recordPassiveEffectDuration(finishedWork);
+ }
+ } else {
+ commitHookEffectList(NoHookEffect, HookPassive, finishedWork);
+ }
break;
}
default:
@@ -446,6 +478,61 @@ export function commitPassiveHookMountEffects(finishedWork: Fiber): void {
}
}
+export function commitPassiveEffectDurations(
+ finishedRoot: FiberRoot,
+ finishedWork: Fiber,
+): void {
+ if (enableProfilerTimer) {
+ // Only Profilers with work in their subtree will have an Update effect scheduled.
+ if ((finishedWork.effectTag & Update) !== NoEffect) {
+ switch (finishedWork.tag) {
+ case Profiler: {
+ const {passiveEffectDuration} = finishedWork.stateNode;
+ const {id, onPostCommit} = finishedWork.memoizedProps;
+
+ // This value will still reflect the previous commit phase.
+ // It does not get reset until the start of the next commit phase.
+ const commitTime = getCommitTime();
+
+ if (typeof onPostCommit === 'function') {
+ if (enableSchedulerTracing) {
+ onPostCommit(
+ id,
+ finishedWork.alternate === null ? 'mount' : 'update',
+ passiveEffectDuration,
+ commitTime,
+ finishedRoot.memoizedInteractions,
+ );
+ } else {
+ onPostCommit(
+ id,
+ finishedWork.alternate === null ? 'mount' : 'update',
+ passiveEffectDuration,
+ commitTime,
+ );
+ }
+ }
+
+ // Bubble times to the next nearest ancestor Profiler.
+ // After we process that Profiler, we'll bubble further up.
+ let parentFiber = finishedWork.return;
+ while (parentFiber !== null) {
+ if (parentFiber.tag === Profiler) {
+ const parentStateNode = parentFiber.stateNode;
+ parentStateNode.passiveEffectDuration += passiveEffectDuration;
+ break;
+ }
+ parentFiber = parentFiber.return;
+ }
+ break;
+ }
+ default:
+ break;
+ }
+ }
+ }
+}
+
function commitLifeCycles(
finishedRoot: FiberRoot,
current: Fiber | null,
@@ -461,7 +548,16 @@ function commitLifeCycles(
// This is done to prevent sibling component effects from interfering with each other,
// e.g. a destroy function in one component should never override a ref set
// by a create function in another component during the same commit.
- commitHookEffectList(NoHookEffect, HookLayout, finishedWork);
+ if (enableProfilerTimer && finishedWork.mode & ProfileMode) {
+ try {
+ startLayoutEffectTimer();
+ commitHookEffectList(NoHookEffect, HookLayout, finishedWork);
+ } finally {
+ recordLayoutEffectDuration(finishedWork);
+ }
+ } else {
+ commitHookEffectList(NoHookEffect, HookLayout, finishedWork);
+ }
return;
}
case ClassComponent: {
@@ -499,7 +595,16 @@ function commitLifeCycles(
}
}
}
- instance.componentDidMount();
+ if (enableProfilerTimer && finishedWork.mode & ProfileMode) {
+ try {
+ startLayoutEffectTimer();
+ instance.componentDidMount();
+ } finally {
+ recordLayoutEffectDuration(finishedWork);
+ }
+ } else {
+ instance.componentDidMount();
+ }
stopPhaseTimer();
} else {
const prevProps =
@@ -538,11 +643,24 @@ function commitLifeCycles(
}
}
}
- instance.componentDidUpdate(
- prevProps,
- prevState,
- instance.__reactInternalSnapshotBeforeUpdate,
- );
+ if (enableProfilerTimer && finishedWork.mode & ProfileMode) {
+ try {
+ startLayoutEffectTimer();
+ instance.componentDidUpdate(
+ prevProps,
+ prevState,
+ instance.__reactInternalSnapshotBeforeUpdate,
+ );
+ } finally {
+ recordLayoutEffectDuration(finishedWork);
+ }
+ } else {
+ instance.componentDidUpdate(
+ prevProps,
+ prevState,
+ instance.__reactInternalSnapshotBeforeUpdate,
+ );
+ }
stopPhaseTimer();
}
}
@@ -635,7 +753,10 @@ function commitLifeCycles(
}
case Profiler: {
if (enableProfilerTimer) {
- const onRender = finishedWork.memoizedProps.onRender;
+ const {onCommit, onRender} = finishedWork.memoizedProps;
+ const {effectDuration} = finishedWork.stateNode;
+
+ const commitTime = getCommitTime();
if (typeof onRender === 'function') {
if (enableSchedulerTracing) {
@@ -645,7 +766,7 @@ function commitLifeCycles(
finishedWork.actualDuration,
finishedWork.treeBaseDuration,
finishedWork.actualStartTime,
- getCommitTime(),
+ commitTime,
finishedRoot.memoizedInteractions,
);
} else {
@@ -655,10 +776,41 @@ function commitLifeCycles(
finishedWork.actualDuration,
finishedWork.treeBaseDuration,
finishedWork.actualStartTime,
- getCommitTime(),
+ commitTime,
+ );
+ }
+ }
+
+ if (typeof onCommit === 'function') {
+ if (enableSchedulerTracing) {
+ onCommit(
+ finishedWork.memoizedProps.id,
+ current === null ? 'mount' : 'update',
+ effectDuration,
+ commitTime,
+ finishedRoot.memoizedInteractions,
+ );
+ } else {
+ onCommit(
+ finishedWork.memoizedProps.id,
+ current === null ? 'mount' : 'update',
+ effectDuration,
+ commitTime,
);
}
}
+
+ // Propagate layout effect durations to the next nearest Profiler ancestor.
+ // Do not reset these values until the next render so DevTools has a chance to read them first.
+ let parentFiber = finishedWork.return;
+ while (parentFiber !== null) {
+ if (parentFiber.tag === Profiler) {
+ const parentStateNode = parentFiber.stateNode;
+ parentStateNode.effectDuration += effectDuration;
+ break;
+ }
+ parentFiber = parentFiber.return;
+ }
}
return;
}
@@ -806,7 +958,13 @@ function commitUnmount(
if ((tag & HookPassive) !== NoHookEffect) {
enqueuePendingPassiveEffectDestroyFn(destroy);
} else {
- safelyCallDestroy(current, destroy);
+ if (enableProfilerTimer && current.mode & ProfileMode) {
+ startLayoutEffectTimer();
+ safelyCallDestroy(current, destroy);
+ recordLayoutEffectDuration(current);
+ } else {
+ safelyCallDestroy(current, destroy);
+ }
}
}
effect = effect.next;
@@ -847,7 +1005,13 @@ function commitUnmount(
safelyDetachRef(current);
const instance = current.stateNode;
if (typeof instance.componentWillUnmount === 'function') {
- safelyCallComponentWillUnmount(current, instance);
+ if (enableProfilerTimer && current.mode & ProfileMode) {
+ startLayoutEffectTimer();
+ safelyCallComponentWillUnmount(current, instance);
+ recordLayoutEffectDuration(current);
+ } else {
+ safelyCallComponentWillUnmount(current, instance);
+ }
}
return;
}
@@ -1347,7 +1511,16 @@ function commitWork(current: Fiber | null, finishedWork: Fiber): void {
// This prevents sibling component effects from interfering with each other,
// e.g. a destroy function in one component should never override a ref set
// by a create function in another component during the same commit.
- commitHookEffectList(HookLayout, NoHookEffect, finishedWork);
+ if (enableProfilerTimer && finishedWork.mode & ProfileMode) {
+ try {
+ startLayoutEffectTimer();
+ commitHookEffectList(HookLayout, NoHookEffect, finishedWork);
+ } finally {
+ recordLayoutEffectDuration(finishedWork);
+ }
+ } else {
+ commitHookEffectList(HookLayout, NoHookEffect, finishedWork);
+ }
return;
}
case Profiler: {
@@ -1390,7 +1563,16 @@ function commitWork(current: Fiber | null, finishedWork: Fiber): void {
// This prevents sibling component effects from interfering with each other,
// e.g. a destroy function in one component should never override a ref set
// by a create function in another component during the same commit.
- commitHookEffectList(HookLayout, NoHookEffect, finishedWork);
+ if (enableProfilerTimer && finishedWork.mode & ProfileMode) {
+ try {
+ startLayoutEffectTimer();
+ commitHookEffectList(HookLayout, NoHookEffect, finishedWork);
+ } finally {
+ recordLayoutEffectDuration(finishedWork);
+ }
+ } else {
+ commitHookEffectList(HookLayout, NoHookEffect, finishedWork);
+ }
return;
}
case ClassComponent: {
diff --git a/packages/react-reconciler/src/ReactFiberWorkLoop.js b/packages/react-reconciler/src/ReactFiberWorkLoop.js
index dd25a2cee256c..d84b590af46ef 100644
--- a/packages/react-reconciler/src/ReactFiberWorkLoop.js
+++ b/packages/react-reconciler/src/ReactFiberWorkLoop.js
@@ -141,6 +141,7 @@ import {
commitDetachRef,
commitAttachRef,
commitResetTextContent,
+ commitPassiveEffectDurations,
} from './ReactFiberCommitWork';
import {enqueueUpdate} from './ReactUpdateQueue';
import {resetContextDependencies} from './ReactFiberNewContext';
@@ -149,6 +150,8 @@ import {createCapturedValue} from './ReactCapturedValue';
import {
recordCommitTime,
+ recordPassiveEffectDuration,
+ startPassiveEffectTimer,
startProfilerTimer,
stopProfilerTimerIfRunningAndRecordDelta,
} from './ReactProfilerTimer';
@@ -2199,6 +2202,7 @@ function flushPassiveEffectsImpl() {
if (rootWithPendingPassiveEffects === null) {
return false;
}
+
const root = rootWithPendingPassiveEffects;
const expirationTime = pendingPassiveEffectsExpirationTime;
rootWithPendingPassiveEffects = null;
@@ -2220,6 +2224,13 @@ function flushPassiveEffectsImpl() {
i < pendingUnmountedPassiveEffectDestroyFunctions.length;
i++
) {
+ // TODO (bvaughn) If we are in a profiling build, within a Profiled subtree,
+ // we should measure the duration of passive destroy functions.
+ // However by the time we are flushing passive effects for an unmount,
+ // the effect's Fiber is no longer in the tree-
+ // so we don't know if there was a Profiler above us.
+ // Let's revisit this if we refactor to preserve the unmounted Fiber tree.
+
const destroy = pendingUnmountedPassiveEffectDestroyFunctions[i];
invokeGuardedCallback(null, destroy, null);
}
@@ -2242,12 +2253,23 @@ function flushPassiveEffectsImpl() {
while (effect !== null) {
if (__DEV__) {
setCurrentDebugFiberInDEV(effect);
- invokeGuardedCallback(
- null,
- commitPassiveHookUnmountEffects,
- null,
- effect,
- );
+ if (enableProfilerTimer && effect.mode & ProfileMode) {
+ startPassiveEffectTimer();
+ invokeGuardedCallback(
+ null,
+ commitPassiveHookUnmountEffects,
+ null,
+ effect,
+ );
+ recordPassiveEffectDuration(effect);
+ } else {
+ invokeGuardedCallback(
+ null,
+ commitPassiveHookUnmountEffects,
+ null,
+ effect,
+ );
+ }
if (hasCaughtError()) {
effectWithErrorDuringUnmount = effect;
invariant(effect !== null, 'Should be working on an effect.');
@@ -2257,7 +2279,16 @@ function flushPassiveEffectsImpl() {
resetCurrentDebugFiberInDEV();
} else {
try {
- commitPassiveHookUnmountEffects(effect);
+ if (enableProfilerTimer && effect.mode & ProfileMode) {
+ try {
+ startPassiveEffectTimer();
+ commitPassiveHookUnmountEffects(effect);
+ } finally {
+ recordPassiveEffectDuration(effect);
+ }
+ } else {
+ commitPassiveHookUnmountEffects(effect);
+ }
} catch (error) {
effectWithErrorDuringUnmount = effect;
invariant(effect !== null, 'Should be working on an effect.');
@@ -2280,12 +2311,23 @@ function flushPassiveEffectsImpl() {
if (effectWithErrorDuringUnmount !== effect) {
if (__DEV__) {
setCurrentDebugFiberInDEV(effect);
- invokeGuardedCallback(
- null,
- commitPassiveHookMountEffects,
- null,
- effect,
- );
+ if (enableProfilerTimer && effect.mode & ProfileMode) {
+ startPassiveEffectTimer();
+ invokeGuardedCallback(
+ null,
+ commitPassiveHookMountEffects,
+ null,
+ effect,
+ );
+ recordPassiveEffectDuration(effect);
+ } else {
+ invokeGuardedCallback(
+ null,
+ commitPassiveHookMountEffects,
+ null,
+ effect,
+ );
+ }
if (hasCaughtError()) {
invariant(effect !== null, 'Should be working on an effect.');
const error = clearCaughtError();
@@ -2294,13 +2336,27 @@ function flushPassiveEffectsImpl() {
resetCurrentDebugFiberInDEV();
} else {
try {
- commitPassiveHookMountEffects(effect);
+ if (enableProfilerTimer && effect.mode & ProfileMode) {
+ try {
+ startPassiveEffectTimer();
+ commitPassiveHookMountEffects(effect);
+ } finally {
+ recordPassiveEffectDuration(effect);
+ }
+ } else {
+ commitPassiveHookMountEffects(effect);
+ }
} catch (error) {
invariant(effect !== null, 'Should be working on an effect.');
captureCommitPhaseError(effect, error);
}
}
}
+
+ if (enableProfilerTimer) {
+ commitPassiveEffectDurations(root, effect);
+ }
+
const nextNextEffect = effect.nextEffect;
// Remove nextEffect pointer to assist GC
effect.nextEffect = null;
@@ -2314,7 +2370,13 @@ function flushPassiveEffectsImpl() {
while (effect !== null) {
if (__DEV__) {
setCurrentDebugFiberInDEV(effect);
- invokeGuardedCallback(null, commitPassiveHookEffects, null, effect);
+ if (enableProfilerTimer && effect.mode & ProfileMode) {
+ startPassiveEffectTimer();
+ invokeGuardedCallback(null, commitPassiveHookEffects, null, effect);
+ recordPassiveEffectDuration(effect);
+ } else {
+ invokeGuardedCallback(null, commitPassiveHookEffects, null, effect);
+ }
if (hasCaughtError()) {
invariant(effect !== null, 'Should be working on an effect.');
const error = clearCaughtError();
@@ -2323,12 +2385,26 @@ function flushPassiveEffectsImpl() {
resetCurrentDebugFiberInDEV();
} else {
try {
- commitPassiveHookEffects(effect);
+ if (enableProfilerTimer && effect.mode & ProfileMode) {
+ try {
+ startPassiveEffectTimer();
+ commitPassiveHookEffects(effect);
+ } finally {
+ recordPassiveEffectDuration(effect);
+ }
+ } else {
+ commitPassiveHookEffects(effect);
+ }
} catch (error) {
invariant(effect !== null, 'Should be working on an effect.');
captureCommitPhaseError(effect, error);
}
}
+
+ if (enableProfilerTimer) {
+ commitPassiveEffectDurations(root, effect);
+ }
+
const nextNextEffect = effect.nextEffect;
// Remove nextEffect pointer to assist GC
effect.nextEffect = null;
diff --git a/packages/react-reconciler/src/ReactProfilerTimer.js b/packages/react-reconciler/src/ReactProfilerTimer.js
index c26f66d969244..3ea8ba9dbd206 100644
--- a/packages/react-reconciler/src/ReactProfilerTimer.js
+++ b/packages/react-reconciler/src/ReactProfilerTimer.js
@@ -10,6 +10,7 @@
import type {Fiber} from './ReactFiber';
import {enableProfilerTimer} from 'shared/ReactFeatureFlags';
+import {Profiler} from 'shared/ReactWorkTags';
// Intentionally not named imports because Rollup would use dynamic dispatch for
// CommonJS interop named imports.
@@ -27,7 +28,9 @@ export type ProfilerTimer = {
};
let commitTime: number = 0;
+let layoutEffectStartTime: number = -1;
let profilerStartTime: number = -1;
+let passiveEffectStartTime: number = -1;
function getCommitTime(): number {
return commitTime;
@@ -77,9 +80,73 @@ function stopProfilerTimerIfRunningAndRecordDelta(
}
}
+function recordLayoutEffectDuration(fiber: Fiber): void {
+ if (!enableProfilerTimer) {
+ return;
+ }
+
+ if (layoutEffectStartTime >= 0) {
+ const elapsedTime = now() - layoutEffectStartTime;
+
+ layoutEffectStartTime = -1;
+
+ // Store duration on the next nearest Profiler ancestor.
+ let parentFiber = fiber.return;
+ while (parentFiber !== null) {
+ if (parentFiber.tag === Profiler) {
+ const parentStateNode = parentFiber.stateNode;
+ parentStateNode.effectDuration += elapsedTime;
+ break;
+ }
+ parentFiber = parentFiber.return;
+ }
+ }
+}
+
+function recordPassiveEffectDuration(fiber: Fiber): void {
+ if (!enableProfilerTimer) {
+ return;
+ }
+
+ if (passiveEffectStartTime >= 0) {
+ const elapsedTime = now() - passiveEffectStartTime;
+
+ passiveEffectStartTime = -1;
+
+ // Store duration on the next nearest Profiler ancestor.
+ let parentFiber = fiber.return;
+ while (parentFiber !== null) {
+ if (parentFiber.tag === Profiler) {
+ const parentStateNode = parentFiber.stateNode;
+ parentStateNode.passiveEffectDuration += elapsedTime;
+ break;
+ }
+ parentFiber = parentFiber.return;
+ }
+ }
+}
+
+function startLayoutEffectTimer(): void {
+ if (!enableProfilerTimer) {
+ return;
+ }
+ layoutEffectStartTime = now();
+}
+
+function startPassiveEffectTimer(): void {
+ if (!enableProfilerTimer) {
+ return;
+ }
+ passiveEffectStartTime = now();
+}
+
export {
getCommitTime,
recordCommitTime,
+ recordLayoutEffectDuration,
+ recordPassiveEffectDuration,
+ startLayoutEffectTimer,
+ startPassiveEffectTimer,
startProfilerTimer,
stopProfilerTimerIfRunning,
stopProfilerTimerIfRunningAndRecordDelta,
diff --git a/packages/react/src/__tests__/ReactProfiler-test.internal.js b/packages/react/src/__tests__/ReactProfiler-test.internal.js
index ad47793c8259a..1dec14c3675b3 100644
--- a/packages/react/src/__tests__/ReactProfiler-test.internal.js
+++ b/packages/react/src/__tests__/ReactProfiler-test.internal.js
@@ -120,10 +120,9 @@ describe('Profiler', () => {
it('should warn if required params are missing', () => {
expect(() => {
ReactTestRenderer.create();
- }).toErrorDev(
- 'Profiler must specify an "id" string and "onRender" function as props',
- {withoutStack: true},
- );
+ }).toErrorDev('Profiler must specify an "id" as a prop', {
+ withoutStack: true,
+ });
});
}
@@ -1262,6 +1261,1077 @@ describe('Profiler', () => {
expect(callback).toHaveBeenCalledTimes(1);
});
});
+
+ describe('onCommit callback', () => {
+ beforeEach(() => {
+ jest.resetModules();
+
+ loadModules({enableSchedulerTracing});
+ });
+
+ it('should report time spent in layout effects and commit lifecycles', () => {
+ const callback = jest.fn();
+
+ const ComponetWithEffects = () => {
+ React.useLayoutEffect(() => {
+ Scheduler.unstable_advanceTime(10);
+ return () => {
+ Scheduler.unstable_advanceTime(100);
+ };
+ }, []);
+ React.useLayoutEffect(() => {
+ Scheduler.unstable_advanceTime(1000);
+ return () => {
+ Scheduler.unstable_advanceTime(10000);
+ };
+ });
+ React.useEffect(() => {
+ // This passive effect is here to verify that its time isn't reported.
+ Scheduler.unstable_advanceTime(5);
+ });
+ return null;
+ };
+
+ class ComponentWithCommitHooks extends React.Component {
+ componentDidMount() {
+ Scheduler.unstable_advanceTime(100000);
+ }
+ componentDidUpdate() {
+ Scheduler.unstable_advanceTime(1000000);
+ }
+ render() {
+ return null;
+ }
+ }
+
+ Scheduler.unstable_advanceTime(1);
+
+ const renderer = ReactTestRenderer.create(
+
+
+
+ ,
+ );
+
+ expect(callback).toHaveBeenCalledTimes(1);
+
+ let call = callback.mock.calls[0];
+
+ expect(call).toHaveLength(enableSchedulerTracing ? 5 : 4);
+ expect(call[0]).toBe('mount-test');
+ expect(call[1]).toBe('mount');
+ expect(call[2]).toBe(101010); // durations
+ expect(call[3]).toBe(1); // commit start time (before mutations or effects)
+ expect(call[4]).toEqual(enableSchedulerTracing ? new Set() : undefined); // interaction events
+
+ Scheduler.unstable_advanceTime(1);
+
+ renderer.update(
+
+
+
+ ,
+ );
+
+ expect(callback).toHaveBeenCalledTimes(2);
+
+ call = callback.mock.calls[1];
+
+ expect(call).toHaveLength(enableSchedulerTracing ? 5 : 4);
+ expect(call[0]).toBe('update-test');
+ expect(call[1]).toBe('update');
+ expect(call[2]).toBe(1011000); // durations
+ expect(call[3]).toBe(101017); // commit start time (before mutations or effects)
+ expect(call[4]).toEqual(enableSchedulerTracing ? new Set() : undefined); // interaction events
+
+ Scheduler.unstable_advanceTime(1);
+
+ renderer.update(
+ ,
+ );
+
+ expect(callback).toHaveBeenCalledTimes(3);
+
+ call = callback.mock.calls[2];
+
+ expect(call).toHaveLength(enableSchedulerTracing ? 5 : 4);
+ expect(call[0]).toBe('unmount-test');
+ expect(call[1]).toBe('update');
+ expect(call[2]).toBe(10100); // durations
+ expect(call[3]).toBe(1112023); // commit start time (before mutations or effects)
+ expect(call[4]).toEqual(enableSchedulerTracing ? new Set() : undefined); // interaction events
+ });
+
+ it('should report time spent in layout effects and commit lifecycles with cascading renders', () => {
+ const callback = jest.fn();
+
+ const ComponetWithEffects = ({shouldCascade}) => {
+ const [didCascade, setDidCascade] = React.useState(false);
+ React.useLayoutEffect(() => {
+ if (shouldCascade && !didCascade) {
+ setDidCascade(true);
+ }
+ Scheduler.unstable_advanceTime(didCascade ? 30 : 10);
+ return () => {
+ Scheduler.unstable_advanceTime(100);
+ };
+ }, [didCascade, shouldCascade]);
+ return null;
+ };
+
+ class ComponentWithCommitHooks extends React.Component {
+ state = {
+ didCascade: false,
+ };
+ componentDidMount() {
+ Scheduler.unstable_advanceTime(1000);
+ }
+ componentDidUpdate() {
+ Scheduler.unstable_advanceTime(10000);
+ if (this.props.shouldCascade && !this.state.didCascade) {
+ this.setState({didCascade: true});
+ }
+ }
+ render() {
+ return null;
+ }
+ }
+
+ Scheduler.unstable_advanceTime(1);
+
+ const renderer = ReactTestRenderer.create(
+
+
+
+ ,
+ );
+
+ expect(callback).toHaveBeenCalledTimes(2);
+
+ let call = callback.mock.calls[0];
+
+ expect(call).toHaveLength(enableSchedulerTracing ? 5 : 4);
+ expect(call[0]).toBe('mount-test');
+ expect(call[1]).toBe('mount');
+ expect(call[2]).toBe(1010); // durations
+ expect(call[3]).toBe(1); // commit start time (before mutations or effects)
+ expect(call[4]).toEqual(enableSchedulerTracing ? new Set() : undefined); // interaction events
+
+ call = callback.mock.calls[1];
+
+ expect(call).toHaveLength(enableSchedulerTracing ? 5 : 4);
+ expect(call[0]).toBe('mount-test');
+ expect(call[1]).toBe('update');
+ expect(call[2]).toBe(130); // durations
+ expect(call[3]).toBe(1011); // commit start time (before mutations or effects)
+ expect(call[4]).toEqual(enableSchedulerTracing ? new Set() : undefined); // interaction events
+
+ Scheduler.unstable_advanceTime(1);
+
+ renderer.update(
+
+
+
+ ,
+ );
+
+ expect(callback).toHaveBeenCalledTimes(4);
+
+ call = callback.mock.calls[2];
+
+ expect(call).toHaveLength(enableSchedulerTracing ? 5 : 4);
+ expect(call[0]).toBe('update-test');
+ expect(call[1]).toBe('update');
+ expect(call[2]).toBe(10130); // durations
+ expect(call[3]).toBe(1142); // commit start time (before mutations or effects)
+ expect(call[4]).toEqual(enableSchedulerTracing ? new Set() : undefined); // interaction events
+
+ call = callback.mock.calls[3];
+
+ expect(call).toHaveLength(enableSchedulerTracing ? 5 : 4);
+ expect(call[0]).toBe('update-test');
+ expect(call[1]).toBe('update');
+ expect(call[2]).toBe(10000); // durations
+ expect(call[3]).toBe(11272); // commit start time (before mutations or effects)
+ expect(call[4]).toEqual(enableSchedulerTracing ? new Set() : undefined); // interaction events
+ });
+
+ it('should bubble time spent in layout effects to higher profilers', () => {
+ const callback = jest.fn();
+
+ const ComponetWithEffects = ({
+ cleanupDuration,
+ duration,
+ setCountRef,
+ }) => {
+ const setCount = React.useState(0)[1];
+ if (setCountRef != null) {
+ setCountRef.current = setCount;
+ }
+ React.useLayoutEffect(() => {
+ Scheduler.unstable_advanceTime(duration);
+ return () => {
+ Scheduler.unstable_advanceTime(cleanupDuration);
+ };
+ });
+ Scheduler.unstable_advanceTime(1);
+ return null;
+ };
+
+ const setCountRef = React.createRef(null);
+
+ let renderer = null;
+ ReactTestRenderer.act(() => {
+ renderer = ReactTestRenderer.create(
+
+
+
+
+
+
+
+ ,
+ );
+ });
+
+ expect(callback).toHaveBeenCalledTimes(1);
+
+ let call = callback.mock.calls[0];
+
+ expect(call).toHaveLength(enableSchedulerTracing ? 5 : 4);
+ expect(call[0]).toBe('root-mount');
+ expect(call[1]).toBe('mount');
+ expect(call[2]).toBe(1010); // durations
+ expect(call[3]).toBe(2); // commit start time (before mutations or effects)
+ expect(call[4]).toEqual(enableSchedulerTracing ? new Set() : undefined); // interaction events
+
+ ReactTestRenderer.act(() => setCountRef.current(count => count + 1));
+
+ expect(callback).toHaveBeenCalledTimes(2);
+
+ call = callback.mock.calls[1];
+
+ expect(call).toHaveLength(enableSchedulerTracing ? 5 : 4);
+ expect(call[0]).toBe('root-mount');
+ expect(call[1]).toBe('update');
+ expect(call[2]).toBe(110); // durations
+ expect(call[3]).toBe(1013); // commit start time (before mutations or effects)
+ expect(call[4]).toEqual(enableSchedulerTracing ? new Set() : undefined); // interaction events
+
+ ReactTestRenderer.act(() => {
+ renderer.update(
+
+
+
+
+ ,
+ );
+ });
+
+ expect(callback).toHaveBeenCalledTimes(3);
+
+ call = callback.mock.calls[2];
+
+ expect(call).toHaveLength(enableSchedulerTracing ? 5 : 4);
+ expect(call[0]).toBe('root-update');
+ expect(call[1]).toBe('update');
+ expect(call[2]).toBe(1100); // durations
+ expect(call[3]).toBe(1124); // commit start time (before mutations or effects)
+ expect(call[4]).toEqual(enableSchedulerTracing ? new Set() : undefined); // interaction events
+ });
+
+ it('should properly report time in layout effects even when there are errors', () => {
+ const callback = jest.fn();
+
+ class ErrorBoundary extends React.Component {
+ state = {error: null};
+ static getDerivedStateFromError(error) {
+ return {error};
+ }
+ render() {
+ return this.state.error === null
+ ? this.props.children
+ : this.props.fallback;
+ }
+ }
+
+ const ComponetWithEffects = ({
+ cleanupDuration,
+ duration,
+ effectDuration,
+ shouldThrow,
+ }) => {
+ React.useLayoutEffect(() => {
+ Scheduler.unstable_advanceTime(effectDuration);
+ if (shouldThrow) {
+ throw Error('expected');
+ }
+ return () => {
+ Scheduler.unstable_advanceTime(cleanupDuration);
+ };
+ });
+ Scheduler.unstable_advanceTime(duration);
+ return null;
+ };
+
+ Scheduler.unstable_advanceTime(1);
+
+ // Test an error that happens during an effect
+
+ ReactTestRenderer.act(() => {
+ ReactTestRenderer.create(
+
+
+ }>
+
+
+
+ ,
+ );
+ });
+
+ expect(callback).toHaveBeenCalledTimes(2);
+
+ let call = callback.mock.calls[0];
+
+ // Initial render (with error)
+ expect(call).toHaveLength(enableSchedulerTracing ? 5 : 4);
+ expect(call[0]).toBe('root');
+ expect(call[1]).toBe('mount');
+ expect(call[2]).toBe(100100); // durations
+ expect(call[3]).toBe(10011); // commit start time (before mutations or effects)
+ expect(call[4]).toEqual(enableSchedulerTracing ? new Set() : undefined); // interaction events
+
+ call = callback.mock.calls[1];
+
+ // Cleanup render from error boundary
+ expect(call).toHaveLength(enableSchedulerTracing ? 5 : 4);
+ expect(call[0]).toBe('root');
+ expect(call[1]).toBe('update');
+ expect(call[2]).toBe(100000000); // durations
+ expect(call[3]).toBe(10110111); // commit start time (before mutations or effects)
+ expect(call[4]).toEqual(enableSchedulerTracing ? new Set() : undefined); // interaction events
+ });
+
+ it('should properly report time in layout effect cleanup functions even when there are errors', () => {
+ const callback = jest.fn();
+
+ class ErrorBoundary extends React.Component {
+ state = {error: null};
+ static getDerivedStateFromError(error) {
+ return {error};
+ }
+ render() {
+ return this.state.error === null
+ ? this.props.children
+ : this.props.fallback;
+ }
+ }
+
+ const ComponetWithEffects = ({
+ cleanupDuration,
+ duration,
+ effectDuration,
+ shouldThrow = false,
+ }) => {
+ React.useLayoutEffect(() => {
+ Scheduler.unstable_advanceTime(effectDuration);
+ return () => {
+ Scheduler.unstable_advanceTime(cleanupDuration);
+ if (shouldThrow) {
+ throw Error('expected');
+ }
+ };
+ });
+ Scheduler.unstable_advanceTime(duration);
+ return null;
+ };
+
+ Scheduler.unstable_advanceTime(1);
+
+ let renderer = null;
+
+ ReactTestRenderer.act(() => {
+ renderer = ReactTestRenderer.create(
+
+
+ }>
+
+
+
+ ,
+ );
+ });
+
+ expect(callback).toHaveBeenCalledTimes(1);
+
+ let call = callback.mock.calls[0];
+
+ // Initial render
+ expect(call).toHaveLength(enableSchedulerTracing ? 5 : 4);
+ expect(call[0]).toBe('root');
+ expect(call[1]).toBe('mount');
+ expect(call[2]).toBe(100100); // durations
+ expect(call[3]).toBe(10011); // commit start time (before mutations or effects)
+ expect(call[4]).toEqual(enableSchedulerTracing ? new Set() : undefined); // interaction events
+
+ callback.mockClear();
+
+ // Test an error that happens during an cleanup function
+
+ ReactTestRenderer.act(() => {
+ renderer.update(
+
+
+ }>
+
+
+
+ ,
+ );
+ });
+
+ expect(callback).toHaveBeenCalledTimes(2);
+
+ call = callback.mock.calls[0];
+
+ // Update (that throws)
+ expect(call).toHaveLength(enableSchedulerTracing ? 5 : 4);
+ expect(call[0]).toBe('root');
+ expect(call[1]).toBe('update');
+ expect(call[2]).toBe(1101100); // durations
+ expect(call[3]).toBe(120121); // commit start time (before mutations or effects)
+ expect(call[4]).toEqual(enableSchedulerTracing ? new Set() : undefined); // interaction events
+
+ call = callback.mock.calls[1];
+
+ // Cleanup render from error boundary
+ expect(call).toHaveLength(enableSchedulerTracing ? 5 : 4);
+ expect(call[0]).toBe('root');
+ expect(call[1]).toBe('update');
+ expect(call[2]).toBe(100001000); // durations
+ expect(call[3]).toBe(11221221); // commit start time (before mutations or effects)
+ expect(call[4]).toEqual(enableSchedulerTracing ? new Set() : undefined); // interaction events
+ });
+
+ if (enableSchedulerTracing) {
+ it('should report interactions that were active', () => {
+ const callback = jest.fn();
+
+ const ComponetWithEffects = () => {
+ const [didMount, setDidMount] = React.useState(false);
+ React.useLayoutEffect(() => {
+ Scheduler.unstable_advanceTime(didMount ? 1000 : 100);
+ if (!didMount) {
+ setDidMount(true);
+ }
+ return () => {
+ Scheduler.unstable_advanceTime(10000);
+ };
+ }, [didMount]);
+ Scheduler.unstable_advanceTime(10);
+ return null;
+ };
+
+ const interaction = {
+ id: 0,
+ name: 'mount',
+ timestamp: Scheduler.unstable_now(),
+ };
+
+ Scheduler.unstable_advanceTime(1);
+
+ SchedulerTracing.unstable_trace(
+ interaction.name,
+ interaction.timestamp,
+ () => {
+ ReactTestRenderer.create(
+
+
+ ,
+ );
+ },
+ );
+
+ expect(callback).toHaveBeenCalledTimes(2);
+
+ let call = callback.mock.calls[0];
+
+ expect(call).toHaveLength(enableSchedulerTracing ? 5 : 4);
+ expect(call[0]).toBe('root');
+ expect(call[1]).toBe('mount');
+ expect(call[4]).toMatchInteractions([interaction]);
+
+ call = callback.mock.calls[1];
+
+ expect(call).toHaveLength(enableSchedulerTracing ? 5 : 4);
+ expect(call[0]).toBe('root');
+ expect(call[1]).toBe('update');
+ expect(call[4]).toMatchInteractions([interaction]);
+ });
+ }
+ });
+
+ describe('onPostCommit callback', () => {
+ beforeEach(() => {
+ jest.resetModules();
+
+ loadModules({enableSchedulerTracing});
+ });
+
+ it('should report time spent in passive effects', () => {
+ const callback = jest.fn();
+
+ const ComponetWithEffects = () => {
+ React.useLayoutEffect(() => {
+ // This layout effect is here to verify that its time isn't reported.
+ Scheduler.unstable_advanceTime(5);
+ });
+ React.useEffect(() => {
+ Scheduler.unstable_advanceTime(10);
+ return () => {
+ Scheduler.unstable_advanceTime(100);
+ };
+ }, []);
+ React.useEffect(() => {
+ Scheduler.unstable_advanceTime(1000);
+ return () => {
+ Scheduler.unstable_advanceTime(10000);
+ };
+ });
+ return null;
+ };
+
+ Scheduler.unstable_advanceTime(1);
+
+ let renderer;
+ ReactTestRenderer.act(() => {
+ renderer = ReactTestRenderer.create(
+
+
+ ,
+ );
+ });
+
+ expect(callback).toHaveBeenCalledTimes(1);
+
+ let call = callback.mock.calls[0];
+
+ expect(call).toHaveLength(enableSchedulerTracing ? 5 : 4);
+ expect(call[0]).toBe('mount-test');
+ expect(call[1]).toBe('mount');
+ expect(call[2]).toBe(1010); // durations
+ expect(call[3]).toBe(1); // commit start time (before mutations or effects)
+ expect(call[4]).toEqual(enableSchedulerTracing ? new Set() : undefined); // interaction events
+
+ Scheduler.unstable_advanceTime(1);
+
+ ReactTestRenderer.act(() => {
+ renderer.update(
+
+
+ ,
+ );
+ });
+
+ expect(callback).toHaveBeenCalledTimes(2);
+
+ call = callback.mock.calls[1];
+
+ expect(call).toHaveLength(enableSchedulerTracing ? 5 : 4);
+ expect(call[0]).toBe('update-test');
+ expect(call[1]).toBe('update');
+ expect(call[2]).toBe(11000); // durations
+ expect(call[3]).toBe(1017); // commit start time (before mutations or effects)
+ expect(call[4]).toEqual(enableSchedulerTracing ? new Set() : undefined); // interaction events
+
+ Scheduler.unstable_advanceTime(1);
+
+ ReactTestRenderer.act(() => {
+ renderer.update(
+ ,
+ );
+ });
+
+ expect(callback).toHaveBeenCalledTimes(3);
+
+ call = callback.mock.calls[2];
+
+ expect(call).toHaveLength(enableSchedulerTracing ? 5 : 4);
+ expect(call[0]).toBe('unmount-test');
+ expect(call[1]).toBe('update');
+ // TODO (bvaughn) The duration reported below should be 10100, but is 0
+ // by the time the passive effect is flushed its parent Fiber pointer is gone.
+ // If we refactor to preserve the unmounted Fiber tree we could fix this.
+ // The current implementation would require too much extra overhead to track this.
+ expect(call[2]).toBe(0); // durations
+ expect(call[3]).toBe(12023); // commit start time (before mutations or effects)
+ expect(call[4]).toEqual(enableSchedulerTracing ? new Set() : undefined); // interaction events
+ });
+
+ it('should report time spent in passive effects with cascading renders', () => {
+ const callback = jest.fn();
+
+ const ComponetWithEffects = () => {
+ const [didMount, setDidMount] = React.useState(false);
+ React.useEffect(() => {
+ if (!didMount) {
+ setDidMount(true);
+ }
+ Scheduler.unstable_advanceTime(didMount ? 30 : 10);
+ return () => {
+ Scheduler.unstable_advanceTime(100);
+ };
+ }, [didMount]);
+ return null;
+ };
+
+ Scheduler.unstable_advanceTime(1);
+
+ ReactTestRenderer.act(() => {
+ ReactTestRenderer.create(
+
+
+ ,
+ );
+ });
+
+ expect(callback).toHaveBeenCalledTimes(2);
+
+ let call = callback.mock.calls[0];
+
+ expect(call).toHaveLength(enableSchedulerTracing ? 5 : 4);
+ expect(call[0]).toBe('mount-test');
+ expect(call[1]).toBe('mount');
+ expect(call[2]).toBe(10); // durations
+ expect(call[3]).toBe(1); // commit start time (before mutations or effects)
+ expect(call[4]).toEqual(enableSchedulerTracing ? new Set() : undefined); // interaction events
+
+ call = callback.mock.calls[1];
+
+ expect(call).toHaveLength(enableSchedulerTracing ? 5 : 4);
+ expect(call[0]).toBe('mount-test');
+ expect(call[1]).toBe('update');
+ expect(call[2]).toBe(130); // durations
+ expect(call[3]).toBe(11); // commit start time (before mutations or effects)
+ expect(call[4]).toEqual(enableSchedulerTracing ? new Set() : undefined); // interaction events
+ });
+
+ it('should bubble time spent in effects to higher profilers', () => {
+ const callback = jest.fn();
+
+ const ComponetWithEffects = ({
+ cleanupDuration,
+ duration,
+ setCountRef,
+ }) => {
+ const setCount = React.useState(0)[1];
+ if (setCountRef != null) {
+ setCountRef.current = setCount;
+ }
+ React.useEffect(() => {
+ Scheduler.unstable_advanceTime(duration);
+ return () => {
+ Scheduler.unstable_advanceTime(cleanupDuration);
+ };
+ });
+ Scheduler.unstable_advanceTime(1);
+ return null;
+ };
+
+ const setCountRef = React.createRef(null);
+
+ let renderer = null;
+ ReactTestRenderer.act(() => {
+ renderer = ReactTestRenderer.create(
+
+
+
+
+
+
+
+ ,
+ );
+ });
+
+ expect(callback).toHaveBeenCalledTimes(1);
+
+ let call = callback.mock.calls[0];
+
+ expect(call).toHaveLength(enableSchedulerTracing ? 5 : 4);
+ expect(call[0]).toBe('root-mount');
+ expect(call[1]).toBe('mount');
+ expect(call[2]).toBe(1010); // durations
+ expect(call[3]).toBe(2); // commit start time (before mutations or effects)
+ expect(call[4]).toEqual(enableSchedulerTracing ? new Set() : undefined); // interaction events
+
+ ReactTestRenderer.act(() => setCountRef.current(count => count + 1));
+
+ expect(callback).toHaveBeenCalledTimes(2);
+
+ call = callback.mock.calls[1];
+
+ expect(call).toHaveLength(enableSchedulerTracing ? 5 : 4);
+ expect(call[0]).toBe('root-mount');
+ expect(call[1]).toBe('update');
+ expect(call[2]).toBe(110); // durations
+ expect(call[3]).toBe(1013); // commit start time (before mutations or effects)
+ expect(call[4]).toEqual(enableSchedulerTracing ? new Set() : undefined); // interaction events
+
+ ReactTestRenderer.act(() => {
+ renderer.update(
+
+
+
+
+ ,
+ );
+ });
+
+ expect(callback).toHaveBeenCalledTimes(3);
+
+ call = callback.mock.calls[2];
+
+ expect(call).toHaveLength(enableSchedulerTracing ? 5 : 4);
+ expect(call[0]).toBe('root-update');
+ expect(call[1]).toBe('update');
+ expect(call[2]).toBe(1100); // durations
+ expect(call[3]).toBe(1124); // commit start time (before mutations or effects)
+ expect(call[4]).toEqual(enableSchedulerTracing ? new Set() : undefined); // interaction events
+ });
+
+ it('should properly report time in passive effects even when there are errors', () => {
+ const callback = jest.fn();
+
+ class ErrorBoundary extends React.Component {
+ state = {error: null};
+ static getDerivedStateFromError(error) {
+ return {error};
+ }
+ render() {
+ return this.state.error === null
+ ? this.props.children
+ : this.props.fallback;
+ }
+ }
+
+ const ComponetWithEffects = ({
+ cleanupDuration,
+ duration,
+ effectDuration,
+ shouldThrow,
+ }) => {
+ React.useEffect(() => {
+ Scheduler.unstable_advanceTime(effectDuration);
+ if (shouldThrow) {
+ throw Error('expected');
+ }
+ return () => {
+ Scheduler.unstable_advanceTime(cleanupDuration);
+ };
+ });
+ Scheduler.unstable_advanceTime(duration);
+ return null;
+ };
+
+ Scheduler.unstable_advanceTime(1);
+
+ // Test an error that happens during an effect
+
+ ReactTestRenderer.act(() => {
+ ReactTestRenderer.create(
+
+
+ }>
+
+
+
+ ,
+ );
+ });
+
+ expect(callback).toHaveBeenCalledTimes(2);
+
+ let call = callback.mock.calls[0];
+
+ // Initial render (with error)
+ expect(call).toHaveLength(enableSchedulerTracing ? 5 : 4);
+ expect(call[0]).toBe('root');
+ expect(call[1]).toBe('mount');
+ expect(call[2]).toBe(100100); // durations
+ expect(call[3]).toBe(10011); // commit start time (before mutations or effects)
+ expect(call[4]).toEqual(enableSchedulerTracing ? new Set() : undefined); // interaction events
+
+ call = callback.mock.calls[1];
+
+ // Cleanup render from error boundary
+ expect(call).toHaveLength(enableSchedulerTracing ? 5 : 4);
+ expect(call[0]).toBe('root');
+ expect(call[1]).toBe('update');
+ expect(call[2]).toBe(100000000); // durations
+ expect(call[3]).toBe(10110111); // commit start time (before mutations or effects)
+ expect(call[4]).toEqual(enableSchedulerTracing ? new Set() : undefined); // interaction events
+ });
+
+ it('should properly report time in passive effect cleanup functions even when there are errors', () => {
+ const callback = jest.fn();
+
+ class ErrorBoundary extends React.Component {
+ state = {error: null};
+ static getDerivedStateFromError(error) {
+ return {error};
+ }
+ render() {
+ return this.state.error === null
+ ? this.props.children
+ : this.props.fallback;
+ }
+ }
+
+ const ComponetWithEffects = ({
+ cleanupDuration,
+ duration,
+ effectDuration,
+ shouldThrow = false,
+ }) => {
+ React.useEffect(() => {
+ Scheduler.unstable_advanceTime(effectDuration);
+ return () => {
+ Scheduler.unstable_advanceTime(cleanupDuration);
+ if (shouldThrow) {
+ throw Error('expected');
+ }
+ };
+ });
+ Scheduler.unstable_advanceTime(duration);
+ return null;
+ };
+
+ Scheduler.unstable_advanceTime(1);
+
+ let renderer = null;
+
+ ReactTestRenderer.act(() => {
+ renderer = ReactTestRenderer.create(
+
+
+ }>
+
+
+
+ ,
+ );
+ });
+
+ expect(callback).toHaveBeenCalledTimes(1);
+
+ let call = callback.mock.calls[0];
+
+ // Initial render
+ expect(call).toHaveLength(enableSchedulerTracing ? 5 : 4);
+ expect(call[0]).toBe('root');
+ expect(call[1]).toBe('mount');
+ expect(call[2]).toBe(100100); // durations
+ expect(call[3]).toBe(10011); // commit start time (before mutations or effects)
+ expect(call[4]).toEqual(enableSchedulerTracing ? new Set() : undefined); // interaction events
+
+ callback.mockClear();
+
+ // Test an error that happens during an cleanup function
+
+ ReactTestRenderer.act(() => {
+ renderer.update(
+
+
+ }>
+
+
+
+ ,
+ );
+ });
+
+ expect(callback).toHaveBeenCalledTimes(2);
+
+ call = callback.mock.calls[0];
+
+ // Update (that throws)
+ expect(call).toHaveLength(enableSchedulerTracing ? 5 : 4);
+ expect(call[0]).toBe('root');
+ expect(call[1]).toBe('update');
+ expect(call[2]).toBe(1101000); // durations
+ expect(call[3]).toBe(120121); // commit start time (before mutations or effects)
+ expect(call[4]).toEqual(enableSchedulerTracing ? new Set() : undefined); // interaction events
+
+ call = callback.mock.calls[1];
+
+ // Cleanup render from error boundary
+ expect(call).toHaveLength(enableSchedulerTracing ? 5 : 4);
+ expect(call[0]).toBe('root');
+ expect(call[1]).toBe('update');
+ expect(call[2]).toBe(100000000); // durations
+ expect(call[3]).toBe(11221121); // commit start time (before mutations or effects)
+ expect(call[4]).toEqual(enableSchedulerTracing ? new Set() : undefined); // interaction events
+ });
+
+ if (enableSchedulerTracing) {
+ it('should report interactions that were active', () => {
+ const callback = jest.fn();
+
+ const ComponetWithEffects = () => {
+ const [didMount, setDidMount] = React.useState(false);
+ React.useEffect(() => {
+ Scheduler.unstable_advanceTime(didMount ? 1000 : 100);
+ if (!didMount) {
+ setDidMount(true);
+ }
+ return () => {
+ Scheduler.unstable_advanceTime(10000);
+ };
+ }, [didMount]);
+ Scheduler.unstable_advanceTime(10);
+ return null;
+ };
+
+ const interaction = {
+ id: 0,
+ name: 'mount',
+ timestamp: Scheduler.unstable_now(),
+ };
+
+ Scheduler.unstable_advanceTime(1);
+
+ ReactTestRenderer.act(() => {
+ SchedulerTracing.unstable_trace(
+ interaction.name,
+ interaction.timestamp,
+ () => {
+ ReactTestRenderer.create(
+
+
+ ,
+ );
+ },
+ );
+ });
+
+ expect(callback).toHaveBeenCalledTimes(2);
+
+ let call = callback.mock.calls[0];
+
+ expect(call).toHaveLength(enableSchedulerTracing ? 5 : 4);
+ expect(call[0]).toBe('root');
+ expect(call[1]).toBe('mount');
+ expect(call[4]).toMatchInteractions([interaction]);
+
+ call = callback.mock.calls[1];
+
+ expect(call).toHaveLength(enableSchedulerTracing ? 5 : 4);
+ expect(call[0]).toBe('root');
+ expect(call[1]).toBe('update');
+ expect(call[4]).toMatchInteractions([interaction]);
+ });
+ }
+ });
});
describe('interaction tracing', () => {
@@ -1771,6 +2841,45 @@ describe('Profiler', () => {
]);
});
+ it('should not mark an interaction complete while passive effects are outstanding', () => {
+ const onCommit = jest.fn();
+ const onPostCommit = jest.fn();
+
+ const ComponetWithEffects = () => {
+ React.useEffect(() => {
+ Scheduler.unstable_yieldValue('passive effect');
+ });
+ React.useLayoutEffect(() => {
+ Scheduler.unstable_yieldValue('layout effect');
+ });
+ Scheduler.unstable_yieldValue('render');
+ return null;
+ };
+
+ SchedulerTracing.unstable_trace('mount', Scheduler.unstable_now(), () => {
+ ReactTestRenderer.create(
+
+
+ ,
+ );
+ });
+
+ expect(Scheduler).toHaveYielded(['render', 'layout effect']);
+
+ expect(onCommit).toHaveBeenCalled();
+ expect(onPostCommit).not.toHaveBeenCalled();
+ expect(onInteractionScheduledWorkCompleted).not.toHaveBeenCalled();
+
+ expect(Scheduler).toFlushAndYieldThrough(['passive effect']);
+
+ expect(onCommit).toHaveBeenCalled();
+ expect(onPostCommit).toHaveBeenCalled();
+ expect(onInteractionScheduledWorkCompleted).toHaveBeenCalledTimes(1);
+ });
+
it('should report the expected times when a high-priority update interrupts a low-priority update', () => {
const onRender = jest.fn();