diff --git a/packages/react-reconciler/src/ReactFiberCommitWork.new.js b/packages/react-reconciler/src/ReactFiberCommitWork.new.js index 59ed975cd4..5e843a70d8 100644 --- a/packages/react-reconciler/src/ReactFiberCommitWork.new.js +++ b/packages/react-reconciler/src/ReactFiberCommitWork.new.js @@ -27,7 +27,6 @@ import type {OffscreenState} from './ReactFiberOffscreenComponent'; import {unstable_wrap as Schedule_tracing_wrap} from 'scheduler/tracing'; import { - deferPassiveEffectCleanupDuringUnmount, enableSchedulerTracing, enableProfilerTimer, enableProfilerCommitHooks, @@ -36,7 +35,6 @@ import { enableFundamentalAPI, enableSuspenseCallback, enableScopeAPI, - runAllPassiveEffectDestroysBeforeCreates, enableCreateEventHandleAPI, } from 'shared/ReactFeatureFlags'; import { @@ -71,7 +69,6 @@ import { Placement, Snapshot, Update, - Passive, } from './ReactSideEffectTags'; import getComponentName from 'shared/getComponentName'; import invariant from 'shared/invariant'; @@ -81,9 +78,7 @@ import {resolveDefaultProps} from './ReactFiberLazyComponent.new'; import { getCommitTime, recordLayoutEffectDuration, - recordPassiveEffectDuration, startLayoutEffectTimer, - startPassiveEffectTimer, } from './ReactProfilerTimer.new'; import {ProfileMode} from './ReactTypeOfMode'; import {commitUpdateQueue} from './ReactUpdateQueue.new'; @@ -134,10 +129,6 @@ import { Passive as HookPassive, } from './ReactHookEffectTags'; import {didWarnAboutReassigningProps} from './ReactFiberBeginWork.new'; -import { - runWithPriority, - NormalPriority, -} from './SchedulerWithReactIntegration.new'; import { updateDeprecatedEventListeners, unmountDeprecatedResponderListeners, @@ -394,67 +385,22 @@ function commitHookEffectListMount(tag: number, finishedWork: Fiber) { } function schedulePassiveEffects(finishedWork: Fiber) { - if (runAllPassiveEffectDestroysBeforeCreates) { - const updateQueue: FunctionComponentUpdateQueue | null = (finishedWork.updateQueue: any); - const lastEffect = updateQueue !== null ? updateQueue.lastEffect : null; - if (lastEffect !== null) { - const firstEffect = lastEffect.next; - let effect = firstEffect; - do { - const {next, tag} = effect; - if ( - (tag & HookPassive) !== NoHookEffect && - (tag & HookHasEffect) !== NoHookEffect - ) { - enqueuePendingPassiveHookEffectUnmount(finishedWork, effect); - enqueuePendingPassiveHookEffectMount(finishedWork, effect); - } - effect = next; - } while (effect !== firstEffect); - } - } -} - -export function commitPassiveHookEffects(finishedWork: Fiber): void { - if ((finishedWork.effectTag & Passive) !== NoEffect) { - switch (finishedWork.tag) { - case FunctionComponent: - case ForwardRef: - case SimpleMemoComponent: - case Block: { - // 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. - if ( - enableProfilerTimer && - enableProfilerCommitHooks && - finishedWork.mode & ProfileMode - ) { - try { - startPassiveEffectTimer(); - commitHookEffectListUnmount( - HookPassive | HookHasEffect, - finishedWork, - ); - commitHookEffectListMount( - HookPassive | HookHasEffect, - finishedWork, - ); - } finally { - recordPassiveEffectDuration(finishedWork); - } - } else { - commitHookEffectListUnmount( - HookPassive | HookHasEffect, - finishedWork, - ); - commitHookEffectListMount(HookPassive | HookHasEffect, finishedWork); - } - break; + const updateQueue: FunctionComponentUpdateQueue | null = (finishedWork.updateQueue: any); + const lastEffect = updateQueue !== null ? updateQueue.lastEffect : null; + if (lastEffect !== null) { + const firstEffect = lastEffect.next; + let effect = firstEffect; + do { + const {next, tag} = effect; + if ( + (tag & HookPassive) !== NoHookEffect && + (tag & HookHasEffect) !== NoHookEffect + ) { + enqueuePendingPassiveHookEffectUnmount(finishedWork, effect); + enqueuePendingPassiveHookEffectMount(finishedWork, effect); } - default: - break; - } + effect = next; + } while (effect !== firstEffect); } } @@ -543,9 +489,7 @@ function commitLifeCycles( commitHookEffectListMount(HookLayout | HookHasEffect, finishedWork); } - if (runAllPassiveEffectDestroysBeforeCreates) { - schedulePassiveEffects(finishedWork); - } + schedulePassiveEffects(finishedWork); return; } case ClassComponent: { @@ -946,74 +890,28 @@ function commitUnmount( if (lastEffect !== null) { const firstEffect = lastEffect.next; - if ( - deferPassiveEffectCleanupDuringUnmount && - runAllPassiveEffectDestroysBeforeCreates - ) { - let effect = firstEffect; - do { - const {destroy, tag} = effect; - if (destroy !== undefined) { - if ((tag & HookPassive) !== NoHookEffect) { - enqueuePendingPassiveHookEffectUnmount(current, effect); + let effect = firstEffect; + do { + const {destroy, tag} = effect; + if (destroy !== undefined) { + if ((tag & HookPassive) !== NoHookEffect) { + enqueuePendingPassiveHookEffectUnmount(current, effect); + } else { + if ( + enableProfilerTimer && + enableProfilerCommitHooks && + current.mode & ProfileMode + ) { + startLayoutEffectTimer(); + safelyCallDestroy(current, destroy); + recordLayoutEffectDuration(current); } else { - if ( - enableProfilerTimer && - enableProfilerCommitHooks && - current.mode & ProfileMode - ) { - startLayoutEffectTimer(); - safelyCallDestroy(current, destroy); - recordLayoutEffectDuration(current); - } else { - safelyCallDestroy(current, destroy); - } + safelyCallDestroy(current, destroy); } } - effect = effect.next; - } while (effect !== firstEffect); - } else { - // When the owner fiber is deleted, the destroy function of a passive - // effect hook is called during the synchronous commit phase. This is - // a concession to implementation complexity. Calling it in the - // passive effect phase (like they usually are, when dependencies - // change during an update) would require either traversing the - // children of the deleted fiber again, or including unmount effects - // as part of the fiber effect list. - // - // Because this is during the sync commit phase, we need to change - // the priority. - // - // TODO: Reconsider this implementation trade off. - const priorityLevel = - renderPriorityLevel > NormalPriority - ? NormalPriority - : renderPriorityLevel; - runWithPriority(priorityLevel, () => { - let effect = firstEffect; - do { - const {destroy, tag} = effect; - if (destroy !== undefined) { - if ( - enableProfilerTimer && - enableProfilerCommitHooks && - current.mode & ProfileMode - ) { - if ((tag & HookPassive) !== NoHookEffect) { - safelyCallDestroy(current, destroy); - } else { - startLayoutEffectTimer(); - safelyCallDestroy(current, destroy); - recordLayoutEffectDuration(current); - } - } else { - safelyCallDestroy(current, destroy); - } - } - effect = effect.next; - } while (effect !== firstEffect); - }); - } + } + effect = effect.next; + } while (effect !== firstEffect); } } return; diff --git a/packages/react-reconciler/src/ReactFiberCommitWork.old.js b/packages/react-reconciler/src/ReactFiberCommitWork.old.js index 5017ed66fb..229eab40ee 100644 --- a/packages/react-reconciler/src/ReactFiberCommitWork.old.js +++ b/packages/react-reconciler/src/ReactFiberCommitWork.old.js @@ -26,7 +26,6 @@ import type {ReactPriorityLevel} from './ReactInternalTypes'; import {unstable_wrap as Schedule_tracing_wrap} from 'scheduler/tracing'; import { - deferPassiveEffectCleanupDuringUnmount, enableSchedulerTracing, enableProfilerTimer, enableProfilerCommitHooks, @@ -35,7 +34,6 @@ import { enableFundamentalAPI, enableSuspenseCallback, enableScopeAPI, - runAllPassiveEffectDestroysBeforeCreates, enableCreateEventHandleAPI, } from 'shared/ReactFeatureFlags'; import { @@ -68,7 +66,6 @@ import { Placement, Snapshot, Update, - Passive, } from './ReactSideEffectTags'; import getComponentName from 'shared/getComponentName'; import invariant from 'shared/invariant'; @@ -78,9 +75,7 @@ import {resolveDefaultProps} from './ReactFiberLazyComponent.old'; import { getCommitTime, recordLayoutEffectDuration, - recordPassiveEffectDuration, startLayoutEffectTimer, - startPassiveEffectTimer, } from './ReactProfilerTimer.old'; import {ProfileMode} from './ReactTypeOfMode'; import {commitUpdateQueue} from './ReactUpdateQueue.old'; @@ -131,10 +126,6 @@ import { Passive as HookPassive, } from './ReactHookEffectTags'; import {didWarnAboutReassigningProps} from './ReactFiberBeginWork.old'; -import { - runWithPriority, - NormalPriority, -} from './SchedulerWithReactIntegration.old'; import { updateDeprecatedEventListeners, unmountDeprecatedResponderListeners, @@ -391,67 +382,22 @@ function commitHookEffectListMount(tag: number, finishedWork: Fiber) { } function schedulePassiveEffects(finishedWork: Fiber) { - if (runAllPassiveEffectDestroysBeforeCreates) { - const updateQueue: FunctionComponentUpdateQueue | null = (finishedWork.updateQueue: any); - const lastEffect = updateQueue !== null ? updateQueue.lastEffect : null; - if (lastEffect !== null) { - const firstEffect = lastEffect.next; - let effect = firstEffect; - do { - const {next, tag} = effect; - if ( - (tag & HookPassive) !== NoHookEffect && - (tag & HookHasEffect) !== NoHookEffect - ) { - enqueuePendingPassiveHookEffectUnmount(finishedWork, effect); - enqueuePendingPassiveHookEffectMount(finishedWork, effect); - } - effect = next; - } while (effect !== firstEffect); - } - } -} - -export function commitPassiveHookEffects(finishedWork: Fiber): void { - if ((finishedWork.effectTag & Passive) !== NoEffect) { - switch (finishedWork.tag) { - case FunctionComponent: - case ForwardRef: - case SimpleMemoComponent: - case Block: { - // 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. - if ( - enableProfilerTimer && - enableProfilerCommitHooks && - finishedWork.mode & ProfileMode - ) { - try { - startPassiveEffectTimer(); - commitHookEffectListUnmount( - HookPassive | HookHasEffect, - finishedWork, - ); - commitHookEffectListMount( - HookPassive | HookHasEffect, - finishedWork, - ); - } finally { - recordPassiveEffectDuration(finishedWork); - } - } else { - commitHookEffectListUnmount( - HookPassive | HookHasEffect, - finishedWork, - ); - commitHookEffectListMount(HookPassive | HookHasEffect, finishedWork); - } - break; + const updateQueue: FunctionComponentUpdateQueue | null = (finishedWork.updateQueue: any); + const lastEffect = updateQueue !== null ? updateQueue.lastEffect : null; + if (lastEffect !== null) { + const firstEffect = lastEffect.next; + let effect = firstEffect; + do { + const {next, tag} = effect; + if ( + (tag & HookPassive) !== NoHookEffect && + (tag & HookHasEffect) !== NoHookEffect + ) { + enqueuePendingPassiveHookEffectUnmount(finishedWork, effect); + enqueuePendingPassiveHookEffectMount(finishedWork, effect); } - default: - break; - } + effect = next; + } while (effect !== firstEffect); } } @@ -540,9 +486,7 @@ function commitLifeCycles( commitHookEffectListMount(HookLayout | HookHasEffect, finishedWork); } - if (runAllPassiveEffectDestroysBeforeCreates) { - schedulePassiveEffects(finishedWork); - } + schedulePassiveEffects(finishedWork); return; } case ClassComponent: { @@ -944,74 +888,28 @@ function commitUnmount( if (lastEffect !== null) { const firstEffect = lastEffect.next; - if ( - deferPassiveEffectCleanupDuringUnmount && - runAllPassiveEffectDestroysBeforeCreates - ) { - let effect = firstEffect; - do { - const {destroy, tag} = effect; - if (destroy !== undefined) { - if ((tag & HookPassive) !== NoHookEffect) { - enqueuePendingPassiveHookEffectUnmount(current, effect); + let effect = firstEffect; + do { + const {destroy, tag} = effect; + if (destroy !== undefined) { + if ((tag & HookPassive) !== NoHookEffect) { + enqueuePendingPassiveHookEffectUnmount(current, effect); + } else { + if ( + enableProfilerTimer && + enableProfilerCommitHooks && + current.mode & ProfileMode + ) { + startLayoutEffectTimer(); + safelyCallDestroy(current, destroy); + recordLayoutEffectDuration(current); } else { - if ( - enableProfilerTimer && - enableProfilerCommitHooks && - current.mode & ProfileMode - ) { - startLayoutEffectTimer(); - safelyCallDestroy(current, destroy); - recordLayoutEffectDuration(current); - } else { - safelyCallDestroy(current, destroy); - } + safelyCallDestroy(current, destroy); } } - effect = effect.next; - } while (effect !== firstEffect); - } else { - // When the owner fiber is deleted, the destroy function of a passive - // effect hook is called during the synchronous commit phase. This is - // a concession to implementation complexity. Calling it in the - // passive effect phase (like they usually are, when dependencies - // change during an update) would require either traversing the - // children of the deleted fiber again, or including unmount effects - // as part of the fiber effect list. - // - // Because this is during the sync commit phase, we need to change - // the priority. - // - // TODO: Reconsider this implementation trade off. - const priorityLevel = - renderPriorityLevel > NormalPriority - ? NormalPriority - : renderPriorityLevel; - runWithPriority(priorityLevel, () => { - let effect = firstEffect; - do { - const {destroy, tag} = effect; - if (destroy !== undefined) { - if ( - enableProfilerTimer && - enableProfilerCommitHooks && - current.mode & ProfileMode - ) { - if ((tag & HookPassive) !== NoHookEffect) { - safelyCallDestroy(current, destroy); - } else { - startLayoutEffectTimer(); - safelyCallDestroy(current, destroy); - recordLayoutEffectDuration(current); - } - } else { - safelyCallDestroy(current, destroy); - } - } - effect = effect.next; - } while (effect !== firstEffect); - }); - } + } + effect = effect.next; + } while (effect !== firstEffect); } } return; diff --git a/packages/react-reconciler/src/ReactFiberWorkLoop.new.js b/packages/react-reconciler/src/ReactFiberWorkLoop.new.js index 438c93cdcf..39ddbe53f8 100644 --- a/packages/react-reconciler/src/ReactFiberWorkLoop.new.js +++ b/packages/react-reconciler/src/ReactFiberWorkLoop.new.js @@ -19,8 +19,6 @@ import type {StackCursor} from './ReactFiberStack.new'; import { warnAboutDeprecatedLifecycles, - deferPassiveEffectCleanupDuringUnmount, - runAllPassiveEffectDestroysBeforeCreates, enableSuspenseServerRenderer, replayFailedUnitOfWorkWithInvokeGuardedCallback, enableProfilerTimer, @@ -154,7 +152,6 @@ import { import { commitBeforeMutationLifeCycles as commitBeforeMutationEffectOnFiber, commitLifeCycles as commitLayoutEffectOnFiber, - commitPassiveHookEffects, commitPlacement, commitWork, commitDeletion, @@ -2260,15 +2257,13 @@ export function enqueuePendingPassiveHookEffectMount( fiber: Fiber, effect: HookEffect, ): void { - if (runAllPassiveEffectDestroysBeforeCreates) { - pendingPassiveHookEffectsMount.push(effect, fiber); - if (!rootDoesHavePassiveEffects) { - rootDoesHavePassiveEffects = true; - scheduleCallback(NormalSchedulerPriority, () => { - flushPassiveEffects(); - return null; - }); - } + pendingPassiveHookEffectsMount.push(effect, fiber); + if (!rootDoesHavePassiveEffects) { + rootDoesHavePassiveEffects = true; + scheduleCallback(NormalSchedulerPriority, () => { + flushPassiveEffects(); + return null; + }); } } @@ -2276,25 +2271,21 @@ export function enqueuePendingPassiveHookEffectUnmount( fiber: Fiber, effect: HookEffect, ): void { - if (runAllPassiveEffectDestroysBeforeCreates) { - pendingPassiveHookEffectsUnmount.push(effect, fiber); - if (__DEV__) { - if (deferPassiveEffectCleanupDuringUnmount) { - fiber.effectTag |= PassiveUnmountPendingDev; - const alternate = fiber.alternate; - if (alternate !== null) { - alternate.effectTag |= PassiveUnmountPendingDev; - } - } - } - if (!rootDoesHavePassiveEffects) { - rootDoesHavePassiveEffects = true; - scheduleCallback(NormalSchedulerPriority, () => { - flushPassiveEffects(); - return null; - }); + pendingPassiveHookEffectsUnmount.push(effect, fiber); + if (__DEV__) { + fiber.effectTag |= PassiveUnmountPendingDev; + const alternate = fiber.alternate; + if (alternate !== null) { + alternate.effectTag |= PassiveUnmountPendingDev; } } + if (!rootDoesHavePassiveEffects) { + rootDoesHavePassiveEffects = true; + scheduleCallback(NormalSchedulerPriority, () => { + flushPassiveEffects(); + return null; + }); + } } function invokePassiveEffectCreate(effect: HookEffect): void { @@ -2325,82 +2316,31 @@ function flushPassiveEffectsImpl() { executionContext |= CommitContext; const prevInteractions = pushInteractions(root); - if (runAllPassiveEffectDestroysBeforeCreates) { - // It's important that ALL pending passive effect destroy functions are called - // before ANY passive effect create functions are called. - // Otherwise effects in sibling components might interfere with each other. - // e.g. a destroy function in one component may unintentionally override a ref - // value set by a create function in another component. - // Layout effects have the same constraint. + // It's important that ALL pending passive effect destroy functions are called + // before ANY passive effect create functions are called. + // Otherwise effects in sibling components might interfere with each other. + // e.g. a destroy function in one component may unintentionally override a ref + // value set by a create function in another component. + // Layout effects have the same constraint. - // First pass: Destroy stale passive effects. - const unmountEffects = pendingPassiveHookEffectsUnmount; - pendingPassiveHookEffectsUnmount = []; - for (let i = 0; i < unmountEffects.length; i += 2) { - const effect = ((unmountEffects[i]: any): HookEffect); - const fiber = ((unmountEffects[i + 1]: any): Fiber); - const destroy = effect.destroy; - effect.destroy = undefined; + // First pass: Destroy stale passive effects. + const unmountEffects = pendingPassiveHookEffectsUnmount; + pendingPassiveHookEffectsUnmount = []; + for (let i = 0; i < unmountEffects.length; i += 2) { + const effect = ((unmountEffects[i]: any): HookEffect); + const fiber = ((unmountEffects[i + 1]: any): Fiber); + const destroy = effect.destroy; + effect.destroy = undefined; - if (__DEV__) { - if (deferPassiveEffectCleanupDuringUnmount) { - fiber.effectTag &= ~PassiveUnmountPendingDev; - const alternate = fiber.alternate; - if (alternate !== null) { - alternate.effectTag &= ~PassiveUnmountPendingDev; - } - } - } - - if (typeof destroy === 'function') { - if (__DEV__) { - setCurrentDebugFiberInDEV(fiber); - if ( - enableProfilerTimer && - enableProfilerCommitHooks && - fiber.mode & ProfileMode - ) { - startPassiveEffectTimer(); - invokeGuardedCallback(null, destroy, null); - recordPassiveEffectDuration(fiber); - } else { - invokeGuardedCallback(null, destroy, null); - } - if (hasCaughtError()) { - invariant(fiber !== null, 'Should be working on an effect.'); - const error = clearCaughtError(); - captureCommitPhaseError(fiber, error); - } - resetCurrentDebugFiberInDEV(); - } else { - try { - if ( - enableProfilerTimer && - enableProfilerCommitHooks && - fiber.mode & ProfileMode - ) { - try { - startPassiveEffectTimer(); - destroy(); - } finally { - recordPassiveEffectDuration(fiber); - } - } else { - destroy(); - } - } catch (error) { - invariant(fiber !== null, 'Should be working on an effect.'); - captureCommitPhaseError(fiber, error); - } - } + if (__DEV__) { + fiber.effectTag &= ~PassiveUnmountPendingDev; + const alternate = fiber.alternate; + if (alternate !== null) { + alternate.effectTag &= ~PassiveUnmountPendingDev; } } - // Second pass: Create new passive effects. - const mountEffects = pendingPassiveHookEffectsMount; - pendingPassiveHookEffectsMount = []; - for (let i = 0; i < mountEffects.length; i += 2) { - const effect = ((mountEffects[i]: any): HookEffect); - const fiber = ((mountEffects[i + 1]: any): Fiber); + + if (typeof destroy === 'function') { if (__DEV__) { setCurrentDebugFiberInDEV(fiber); if ( @@ -2409,10 +2349,10 @@ function flushPassiveEffectsImpl() { fiber.mode & ProfileMode ) { startPassiveEffectTimer(); - invokeGuardedCallback(null, invokePassiveEffectCreate, null, effect); + invokeGuardedCallback(null, destroy, null); recordPassiveEffectDuration(fiber); } else { - invokeGuardedCallback(null, invokePassiveEffectCreate, null, effect); + invokeGuardedCallback(null, destroy, null); } if (hasCaughtError()) { invariant(fiber !== null, 'Should be working on an effect.'); @@ -2422,7 +2362,6 @@ function flushPassiveEffectsImpl() { resetCurrentDebugFiberInDEV(); } else { try { - const create = effect.create; if ( enableProfilerTimer && enableProfilerCommitHooks && @@ -2430,12 +2369,12 @@ function flushPassiveEffectsImpl() { ) { try { startPassiveEffectTimer(); - effect.destroy = create(); + destroy(); } finally { recordPassiveEffectDuration(fiber); } } else { - effect.destroy = create(); + destroy(); } } catch (error) { invariant(fiber !== null, 'Should be working on an effect.'); @@ -2444,33 +2383,60 @@ function flushPassiveEffectsImpl() { } } } + // Second pass: Create new passive effects. + const mountEffects = pendingPassiveHookEffectsMount; + pendingPassiveHookEffectsMount = []; + for (let i = 0; i < mountEffects.length; i += 2) { + const effect = ((mountEffects[i]: any): HookEffect); + const fiber = ((mountEffects[i + 1]: any): Fiber); + if (__DEV__) { + setCurrentDebugFiberInDEV(fiber); + if ( + enableProfilerTimer && + enableProfilerCommitHooks && + fiber.mode & ProfileMode + ) { + startPassiveEffectTimer(); + invokeGuardedCallback(null, invokePassiveEffectCreate, null, effect); + recordPassiveEffectDuration(fiber); + } else { + invokeGuardedCallback(null, invokePassiveEffectCreate, null, effect); + } + if (hasCaughtError()) { + invariant(fiber !== null, 'Should be working on an effect.'); + const error = clearCaughtError(); + captureCommitPhaseError(fiber, error); + } + resetCurrentDebugFiberInDEV(); + } else { + try { + const create = effect.create; + if ( + enableProfilerTimer && + enableProfilerCommitHooks && + fiber.mode & ProfileMode + ) { + try { + startPassiveEffectTimer(); + effect.destroy = create(); + } finally { + recordPassiveEffectDuration(fiber); + } + } else { + effect.destroy = create(); + } + } catch (error) { + invariant(fiber !== null, 'Should be working on an effect.'); + captureCommitPhaseError(fiber, error); + } + } + } + // Note: This currently assumes there are no passive effects on the root fiber // because the root is not part of its own effect list. // This could change in the future. let effect = root.current.firstEffect; while (effect !== null) { - // We do this work above if this flag is enabled, so we shouldn't be - // doing it here. - if (!runAllPassiveEffectDestroysBeforeCreates) { - if (__DEV__) { - setCurrentDebugFiberInDEV(effect); - invokeGuardedCallback(null, commitPassiveHookEffects, null, effect); - if (hasCaughtError()) { - invariant(effect !== null, 'Should be working on an effect.'); - const error = clearCaughtError(); - captureCommitPhaseError(effect, error); - } - resetCurrentDebugFiberInDEV(); - } else { - try { - commitPassiveHookEffects(effect); - } catch (error) { - invariant(effect !== null, 'Should be working on an effect.'); - captureCommitPhaseError(effect, error); - } - } - } - const nextNextEffect = effect.nextEffect; // Remove nextEffect pointer to assist GC effect.nextEffect = null; @@ -2868,15 +2834,10 @@ function warnAboutUpdateOnUnmountedFiberInDEV(fiber) { return; } - if ( - deferPassiveEffectCleanupDuringUnmount && - runAllPassiveEffectDestroysBeforeCreates - ) { - // If there are pending passive effects unmounts for this Fiber, - // we can assume that they would have prevented this update. - if ((fiber.effectTag & PassiveUnmountPendingDev) !== NoEffect) { - return; - } + // If there are pending passive effects unmounts for this Fiber, + // we can assume that they would have prevented this update. + if ((fiber.effectTag & PassiveUnmountPendingDev) !== NoEffect) { + return; } // We show the whole stack but dedupe on the top component's name because diff --git a/packages/react-reconciler/src/ReactFiberWorkLoop.old.js b/packages/react-reconciler/src/ReactFiberWorkLoop.old.js index 24bee14eb5..daaaced567 100644 --- a/packages/react-reconciler/src/ReactFiberWorkLoop.old.js +++ b/packages/react-reconciler/src/ReactFiberWorkLoop.old.js @@ -18,8 +18,6 @@ import type {Effect as HookEffect} from './ReactFiberHooks.old'; import { warnAboutDeprecatedLifecycles, - deferPassiveEffectCleanupDuringUnmount, - runAllPassiveEffectDestroysBeforeCreates, enableSuspenseServerRenderer, replayFailedUnitOfWorkWithInvokeGuardedCallback, enableProfilerTimer, @@ -151,7 +149,6 @@ import { import { commitBeforeMutationLifeCycles as commitBeforeMutationEffectOnFiber, commitLifeCycles as commitLayoutEffectOnFiber, - commitPassiveHookEffects, commitPlacement, commitWork, commitDeletion, @@ -2400,15 +2397,13 @@ export function enqueuePendingPassiveHookEffectMount( fiber: Fiber, effect: HookEffect, ): void { - if (runAllPassiveEffectDestroysBeforeCreates) { - pendingPassiveHookEffectsMount.push(effect, fiber); - if (!rootDoesHavePassiveEffects) { - rootDoesHavePassiveEffects = true; - scheduleCallback(NormalPriority, () => { - flushPassiveEffects(); - return null; - }); - } + pendingPassiveHookEffectsMount.push(effect, fiber); + if (!rootDoesHavePassiveEffects) { + rootDoesHavePassiveEffects = true; + scheduleCallback(NormalPriority, () => { + flushPassiveEffects(); + return null; + }); } } @@ -2416,25 +2411,21 @@ export function enqueuePendingPassiveHookEffectUnmount( fiber: Fiber, effect: HookEffect, ): void { - if (runAllPassiveEffectDestroysBeforeCreates) { - pendingPassiveHookEffectsUnmount.push(effect, fiber); - if (__DEV__) { - if (deferPassiveEffectCleanupDuringUnmount) { - fiber.effectTag |= PassiveUnmountPendingDev; - const alternate = fiber.alternate; - if (alternate !== null) { - alternate.effectTag |= PassiveUnmountPendingDev; - } - } - } - if (!rootDoesHavePassiveEffects) { - rootDoesHavePassiveEffects = true; - scheduleCallback(NormalPriority, () => { - flushPassiveEffects(); - return null; - }); + pendingPassiveHookEffectsUnmount.push(effect, fiber); + if (__DEV__) { + fiber.effectTag |= PassiveUnmountPendingDev; + const alternate = fiber.alternate; + if (alternate !== null) { + alternate.effectTag |= PassiveUnmountPendingDev; } } + if (!rootDoesHavePassiveEffects) { + rootDoesHavePassiveEffects = true; + scheduleCallback(NormalPriority, () => { + flushPassiveEffects(); + return null; + }); + } } function invokePassiveEffectCreate(effect: HookEffect): void { @@ -2473,82 +2464,31 @@ function flushPassiveEffectsImpl() { executionContext |= CommitContext; const prevInteractions = pushInteractions(root); - if (runAllPassiveEffectDestroysBeforeCreates) { - // It's important that ALL pending passive effect destroy functions are called - // before ANY passive effect create functions are called. - // Otherwise effects in sibling components might interfere with each other. - // e.g. a destroy function in one component may unintentionally override a ref - // value set by a create function in another component. - // Layout effects have the same constraint. + // It's important that ALL pending passive effect destroy functions are called + // before ANY passive effect create functions are called. + // Otherwise effects in sibling components might interfere with each other. + // e.g. a destroy function in one component may unintentionally override a ref + // value set by a create function in another component. + // Layout effects have the same constraint. - // First pass: Destroy stale passive effects. - const unmountEffects = pendingPassiveHookEffectsUnmount; - pendingPassiveHookEffectsUnmount = []; - for (let i = 0; i < unmountEffects.length; i += 2) { - const effect = ((unmountEffects[i]: any): HookEffect); - const fiber = ((unmountEffects[i + 1]: any): Fiber); - const destroy = effect.destroy; - effect.destroy = undefined; + // First pass: Destroy stale passive effects. + const unmountEffects = pendingPassiveHookEffectsUnmount; + pendingPassiveHookEffectsUnmount = []; + for (let i = 0; i < unmountEffects.length; i += 2) { + const effect = ((unmountEffects[i]: any): HookEffect); + const fiber = ((unmountEffects[i + 1]: any): Fiber); + const destroy = effect.destroy; + effect.destroy = undefined; - if (__DEV__) { - if (deferPassiveEffectCleanupDuringUnmount) { - fiber.effectTag &= ~PassiveUnmountPendingDev; - const alternate = fiber.alternate; - if (alternate !== null) { - alternate.effectTag &= ~PassiveUnmountPendingDev; - } - } - } - - if (typeof destroy === 'function') { - if (__DEV__) { - setCurrentDebugFiberInDEV(fiber); - if ( - enableProfilerTimer && - enableProfilerCommitHooks && - fiber.mode & ProfileMode - ) { - startPassiveEffectTimer(); - invokeGuardedCallback(null, destroy, null); - recordPassiveEffectDuration(fiber); - } else { - invokeGuardedCallback(null, destroy, null); - } - if (hasCaughtError()) { - invariant(fiber !== null, 'Should be working on an effect.'); - const error = clearCaughtError(); - captureCommitPhaseError(fiber, error); - } - resetCurrentDebugFiberInDEV(); - } else { - try { - if ( - enableProfilerTimer && - enableProfilerCommitHooks && - fiber.mode & ProfileMode - ) { - try { - startPassiveEffectTimer(); - destroy(); - } finally { - recordPassiveEffectDuration(fiber); - } - } else { - destroy(); - } - } catch (error) { - invariant(fiber !== null, 'Should be working on an effect.'); - captureCommitPhaseError(fiber, error); - } - } + if (__DEV__) { + fiber.effectTag &= ~PassiveUnmountPendingDev; + const alternate = fiber.alternate; + if (alternate !== null) { + alternate.effectTag &= ~PassiveUnmountPendingDev; } } - // Second pass: Create new passive effects. - const mountEffects = pendingPassiveHookEffectsMount; - pendingPassiveHookEffectsMount = []; - for (let i = 0; i < mountEffects.length; i += 2) { - const effect = ((mountEffects[i]: any): HookEffect); - const fiber = ((mountEffects[i + 1]: any): Fiber); + + if (typeof destroy === 'function') { if (__DEV__) { setCurrentDebugFiberInDEV(fiber); if ( @@ -2557,10 +2497,10 @@ function flushPassiveEffectsImpl() { fiber.mode & ProfileMode ) { startPassiveEffectTimer(); - invokeGuardedCallback(null, invokePassiveEffectCreate, null, effect); + invokeGuardedCallback(null, destroy, null); recordPassiveEffectDuration(fiber); } else { - invokeGuardedCallback(null, invokePassiveEffectCreate, null, effect); + invokeGuardedCallback(null, destroy, null); } if (hasCaughtError()) { invariant(fiber !== null, 'Should be working on an effect.'); @@ -2570,7 +2510,6 @@ function flushPassiveEffectsImpl() { resetCurrentDebugFiberInDEV(); } else { try { - const create = effect.create; if ( enableProfilerTimer && enableProfilerCommitHooks && @@ -2578,12 +2517,12 @@ function flushPassiveEffectsImpl() { ) { try { startPassiveEffectTimer(); - effect.destroy = create(); + destroy(); } finally { recordPassiveEffectDuration(fiber); } } else { - effect.destroy = create(); + destroy(); } } catch (error) { invariant(fiber !== null, 'Should be working on an effect.'); @@ -2592,33 +2531,60 @@ function flushPassiveEffectsImpl() { } } } + // Second pass: Create new passive effects. + const mountEffects = pendingPassiveHookEffectsMount; + pendingPassiveHookEffectsMount = []; + for (let i = 0; i < mountEffects.length; i += 2) { + const effect = ((mountEffects[i]: any): HookEffect); + const fiber = ((mountEffects[i + 1]: any): Fiber); + if (__DEV__) { + setCurrentDebugFiberInDEV(fiber); + if ( + enableProfilerTimer && + enableProfilerCommitHooks && + fiber.mode & ProfileMode + ) { + startPassiveEffectTimer(); + invokeGuardedCallback(null, invokePassiveEffectCreate, null, effect); + recordPassiveEffectDuration(fiber); + } else { + invokeGuardedCallback(null, invokePassiveEffectCreate, null, effect); + } + if (hasCaughtError()) { + invariant(fiber !== null, 'Should be working on an effect.'); + const error = clearCaughtError(); + captureCommitPhaseError(fiber, error); + } + resetCurrentDebugFiberInDEV(); + } else { + try { + const create = effect.create; + if ( + enableProfilerTimer && + enableProfilerCommitHooks && + fiber.mode & ProfileMode + ) { + try { + startPassiveEffectTimer(); + effect.destroy = create(); + } finally { + recordPassiveEffectDuration(fiber); + } + } else { + effect.destroy = create(); + } + } catch (error) { + invariant(fiber !== null, 'Should be working on an effect.'); + captureCommitPhaseError(fiber, error); + } + } + } + // Note: This currently assumes there are no passive effects on the root fiber // because the root is not part of its own effect list. // This could change in the future. let effect = root.current.firstEffect; while (effect !== null) { - // We do this work above if this flag is enabled, so we shouldn't be - // doing it here. - if (!runAllPassiveEffectDestroysBeforeCreates) { - if (__DEV__) { - setCurrentDebugFiberInDEV(effect); - invokeGuardedCallback(null, commitPassiveHookEffects, null, effect); - if (hasCaughtError()) { - invariant(effect !== null, 'Should be working on an effect.'); - const error = clearCaughtError(); - captureCommitPhaseError(effect, error); - } - resetCurrentDebugFiberInDEV(); - } else { - try { - commitPassiveHookEffects(effect); - } catch (error) { - invariant(effect !== null, 'Should be working on an effect.'); - captureCommitPhaseError(effect, error); - } - } - } - const nextNextEffect = effect.nextEffect; // Remove nextEffect pointer to assist GC effect.nextEffect = null; @@ -3037,15 +3003,10 @@ function warnAboutUpdateOnUnmountedFiberInDEV(fiber) { return; } - if ( - deferPassiveEffectCleanupDuringUnmount && - runAllPassiveEffectDestroysBeforeCreates - ) { - // If there are pending passive effects unmounts for this Fiber, - // we can assume that they would have prevented this update. - if ((fiber.effectTag & PassiveUnmountPendingDev) !== NoEffect) { - return; - } + // If there are pending passive effects unmounts for this Fiber, + // we can assume that they would have prevented this update. + if ((fiber.effectTag & PassiveUnmountPendingDev) !== NoEffect) { + return; } // We show the whole stack but dedupe on the top component's name because diff --git a/packages/react-reconciler/src/__tests__/ReactHooksWithNoopRenderer-test.js b/packages/react-reconciler/src/__tests__/ReactHooksWithNoopRenderer-test.js index 5e34e34646..8cf6e29a35 100644 --- a/packages/react-reconciler/src/__tests__/ReactHooksWithNoopRenderer-test.js +++ b/packages/react-reconciler/src/__tests__/ReactHooksWithNoopRenderer-test.js @@ -15,7 +15,6 @@ let React; let ReactCache; let TextResource; -let ReactFeatureFlags; let ReactNoop; let Scheduler; let SchedulerTracing; @@ -33,21 +32,12 @@ let useDeferredValue; let forwardRef; let memo; let act; -let deferPassiveEffectCleanupDuringUnmount; -let runAllPassiveEffectDestroysBeforeCreates; describe('ReactHooksWithNoopRenderer', () => { beforeEach(() => { jest.resetModules(); jest.useFakeTimers(); - ReactFeatureFlags = require('shared/ReactFeatureFlags'); - - deferPassiveEffectCleanupDuringUnmount = - ReactFeatureFlags.deferPassiveEffectCleanupDuringUnmount; - runAllPassiveEffectDestroysBeforeCreates = - ReactFeatureFlags.runAllPassiveEffectDestroysBeforeCreates; - React = require('react'); ReactNoop = require('react-noop-renderer'); Scheduler = require('scheduler'); @@ -1119,7 +1109,6 @@ describe('ReactHooksWithNoopRenderer', () => { }, ); - // @gate deferPassiveEffectCleanupDuringUnmount && runAllPassiveEffectDestroysBeforeCreates it('defers passive effect destroy functions during unmount', () => { function Child({bar, foo}) { React.useEffect(() => { @@ -1204,7 +1193,6 @@ describe('ReactHooksWithNoopRenderer', () => { }); }); - // @gate deferPassiveEffectCleanupDuringUnmount && runAllPassiveEffectDestroysBeforeCreates it('does not warn about state updates for unmounted components with pending passive unmounts', () => { let completePendingRequest = null; function Component() { @@ -1252,7 +1240,6 @@ describe('ReactHooksWithNoopRenderer', () => { }); }); - // @gate deferPassiveEffectCleanupDuringUnmount && runAllPassiveEffectDestroysBeforeCreates it('does not warn about state updates for unmounted components with pending passive unmounts for alternates', () => { let setParentState = null; const setChildStates = []; @@ -1378,7 +1365,6 @@ describe('ReactHooksWithNoopRenderer', () => { }); }); - // @gate deferPassiveEffectCleanupDuringUnmount && runAllPassiveEffectDestroysBeforeCreates it('still warns if there are pending passive unmount effects but not for the current fiber', () => { let completePendingRequest = null; function ComponentWithXHR() { @@ -2085,80 +2071,66 @@ describe('ReactHooksWithNoopRenderer', () => { ]); }); - if (runAllPassiveEffectDestroysBeforeCreates) { - it('unmounts all previous effects between siblings before creating any new ones', () => { - function Counter({count, label}) { - useEffect(() => { - Scheduler.unstable_yieldValue(`Mount ${label} [${count}]`); - return () => { - Scheduler.unstable_yieldValue(`Unmount ${label} [${count}]`); - }; - }); - return ; - } - act(() => { - ReactNoop.render( - <> - - - , - () => Scheduler.unstable_yieldValue('Sync effect'), - ); - expect(Scheduler).toFlushAndYieldThrough([ - 'A 0', - 'B 0', - 'Sync effect', - ]); - expect(ReactNoop.getChildren()).toEqual([span('A 0'), span('B 0')]); + it('unmounts all previous effects between siblings before creating any new ones', () => { + function Counter({count, label}) { + useEffect(() => { + Scheduler.unstable_yieldValue(`Mount ${label} [${count}]`); + return () => { + Scheduler.unstable_yieldValue(`Unmount ${label} [${count}]`); + }; }); - - expect(Scheduler).toHaveYielded(['Mount A [0]', 'Mount B [0]']); - - act(() => { - ReactNoop.render( - <> - - - , - () => Scheduler.unstable_yieldValue('Sync effect'), - ); - expect(Scheduler).toFlushAndYieldThrough([ - 'A 1', - 'B 1', - 'Sync effect', - ]); - expect(ReactNoop.getChildren()).toEqual([span('A 1'), span('B 1')]); - }); - expect(Scheduler).toHaveYielded([ - 'Unmount A [0]', - 'Unmount B [0]', - 'Mount A [1]', - 'Mount B [1]', - ]); - - act(() => { - ReactNoop.render( - <> - - - , - () => Scheduler.unstable_yieldValue('Sync effect'), - ); - expect(Scheduler).toFlushAndYieldThrough([ - 'B 2', - 'C 0', - 'Sync effect', - ]); - expect(ReactNoop.getChildren()).toEqual([span('B 2'), span('C 0')]); - }); - expect(Scheduler).toHaveYielded([ - 'Unmount A [1]', - 'Unmount B [1]', - 'Mount B [2]', - 'Mount C [0]', - ]); + return ; + } + act(() => { + ReactNoop.render( + <> + + + , + () => Scheduler.unstable_yieldValue('Sync effect'), + ); + expect(Scheduler).toFlushAndYieldThrough(['A 0', 'B 0', 'Sync effect']); + expect(ReactNoop.getChildren()).toEqual([span('A 0'), span('B 0')]); }); - } + + expect(Scheduler).toHaveYielded(['Mount A [0]', 'Mount B [0]']); + + act(() => { + ReactNoop.render( + <> + + + , + () => Scheduler.unstable_yieldValue('Sync effect'), + ); + expect(Scheduler).toFlushAndYieldThrough(['A 1', 'B 1', 'Sync effect']); + expect(ReactNoop.getChildren()).toEqual([span('A 1'), span('B 1')]); + }); + expect(Scheduler).toHaveYielded([ + 'Unmount A [0]', + 'Unmount B [0]', + 'Mount A [1]', + 'Mount B [1]', + ]); + + act(() => { + ReactNoop.render( + <> + + + , + () => Scheduler.unstable_yieldValue('Sync effect'), + ); + expect(Scheduler).toFlushAndYieldThrough(['B 2', 'C 0', 'Sync effect']); + expect(ReactNoop.getChildren()).toEqual([span('B 2'), span('C 0')]); + }); + expect(Scheduler).toHaveYielded([ + 'Unmount A [1]', + 'Unmount B [1]', + 'Mount B [2]', + 'Mount C [0]', + ]); + }); it('handles errors in create on mount', () => { function Counter(props) { @@ -2236,30 +2208,19 @@ describe('ReactHooksWithNoopRenderer', () => { expect(Scheduler).toFlushAndYieldThrough(['Count: 1', 'Sync effect']); expect(ReactNoop.getChildren()).toEqual([span('Count: 1')]); expect(() => ReactNoop.flushPassiveEffects()).toThrow('Oops'); - expect(Scheduler).toHaveYielded( - deferPassiveEffectCleanupDuringUnmount && - runAllPassiveEffectDestroysBeforeCreates - ? ['Unmount A [0]', 'Unmount B [0]', 'Mount A [1]', 'Oops!'] - : [ - 'Unmount A [0]', - 'Unmount B [0]', - 'Mount A [1]', - 'Oops!', - 'Unmount A [1]', - ], - ); + expect(Scheduler).toHaveYielded([ + 'Unmount A [0]', + 'Unmount B [0]', + 'Mount A [1]', + 'Oops!', + ]); expect(ReactNoop.getChildren()).toEqual([]); }); - if ( - deferPassiveEffectCleanupDuringUnmount && - runAllPassiveEffectDestroysBeforeCreates - ) { - expect(Scheduler).toHaveYielded([ - // Clean up effect A runs passively on unmount. - // There's no effect B to clean-up, because it never mounted. - 'Unmount A [1]', - ]); - } + expect(Scheduler).toHaveYielded([ + // Clean up effect A runs passively on unmount. + // There's no effect B to clean-up, because it never mounted. + 'Unmount A [1]', + ]); }); it('handles errors in destroy on update', () => { @@ -2292,49 +2253,32 @@ describe('ReactHooksWithNoopRenderer', () => { expect(Scheduler).toHaveYielded(['Mount A [0]', 'Mount B [0]']); }); - if ( - deferPassiveEffectCleanupDuringUnmount && - runAllPassiveEffectDestroysBeforeCreates - ) { - act(() => { - // This update will trigger an error during passive effect unmount - ReactNoop.render(, () => - Scheduler.unstable_yieldValue('Sync effect'), - ); - expect(Scheduler).toFlushAndYieldThrough(['Count: 1', 'Sync effect']); - expect(ReactNoop.getChildren()).toEqual([span('Count: 1')]); - expect(() => ReactNoop.flushPassiveEffects()).toThrow('Oops'); + act(() => { + // This update will trigger an error during passive effect unmount + ReactNoop.render(, () => + Scheduler.unstable_yieldValue('Sync effect'), + ); + expect(Scheduler).toFlushAndYieldThrough(['Count: 1', 'Sync effect']); + expect(ReactNoop.getChildren()).toEqual([span('Count: 1')]); + expect(() => ReactNoop.flushPassiveEffects()).toThrow('Oops'); - // This branch enables a feature flag that flushes all passive destroys in a - // separate pass before flushing any passive creates. - // A result of this two-pass flush is that an error thrown from unmount does - // not block the subsequent create functions from being run. - expect(Scheduler).toHaveYielded([ - 'Oops!', - 'Unmount B [0]', - 'Mount A [1]', - 'Mount B [1]', - ]); - }); + // This branch enables a feature flag that flushes all passive destroys in a + // separate pass before flushing any passive creates. + // A result of this two-pass flush is that an error thrown from unmount does + // not block the subsequent create functions from being run. + expect(Scheduler).toHaveYielded([ + 'Oops!', + 'Unmount B [0]', + 'Mount A [1]', + 'Mount B [1]', + ]); + }); - // gets unmounted because an error is thrown above. - // The remaining destroy functions are run later on unmount, since they're passive. - // In this case, one of them throws again (because of how the test is written). - expect(Scheduler).toHaveYielded(['Oops!', 'Unmount B [1]']); - expect(ReactNoop.getChildren()).toEqual([]); - } else { - act(() => { - // This update will trigger an error during passive effect unmount - ReactNoop.render(, () => - Scheduler.unstable_yieldValue('Sync effect'), - ); - expect(() => { - expect(Scheduler).toFlushAndYield(['Count: 1', 'Sync effect']); - }).toThrow('Oops!'); - expect(ReactNoop.getChildren()).toEqual([]); - ReactNoop.flushPassiveEffects(); - }); - } + // gets unmounted because an error is thrown above. + // The remaining destroy functions are run later on unmount, since they're passive. + // In this case, one of them throws again (because of how the test is written). + expect(Scheduler).toHaveYielded(['Oops!', 'Unmount B [1]']); + expect(ReactNoop.getChildren()).toEqual([]); }); it('works with memo', () => { diff --git a/packages/react-reconciler/src/__tests__/ReactSuspenseWithNoopRenderer-test.js b/packages/react-reconciler/src/__tests__/ReactSuspenseWithNoopRenderer-test.js index 2e8689027e..ea0df588a9 100644 --- a/packages/react-reconciler/src/__tests__/ReactSuspenseWithNoopRenderer-test.js +++ b/packages/react-reconciler/src/__tests__/ReactSuspenseWithNoopRenderer-test.js @@ -1,5 +1,4 @@ let React; -let ReactFeatureFlags; let Fragment; let ReactNoop; let Scheduler; @@ -14,7 +13,6 @@ describe('ReactSuspenseWithNoopRenderer', () => { beforeEach(() => { jest.resetModules(); - ReactFeatureFlags = require('shared/ReactFeatureFlags'); React = require('react'); Fragment = React.Fragment; ReactNoop = require('react-noop-renderer'); @@ -1737,26 +1735,13 @@ describe('ReactSuspenseWithNoopRenderer', () => { expect(Scheduler).toHaveYielded(['Promise resolved [B]']); - if ( - ReactFeatureFlags.deferPassiveEffectCleanupDuringUnmount && - ReactFeatureFlags.runAllPassiveEffectDestroysBeforeCreates - ) { - expect(Scheduler).toFlushAndYield([ - 'B', - 'Destroy Layout Effect [Loading...]', - 'Layout Effect [B]', - 'Destroy Effect [Loading...]', - 'Effect [B]', - ]); - } else { - expect(Scheduler).toFlushAndYield([ - 'B', - 'Destroy Layout Effect [Loading...]', - 'Destroy Effect [Loading...]', - 'Layout Effect [B]', - 'Effect [B]', - ]); - } + expect(Scheduler).toFlushAndYield([ + 'B', + 'Destroy Layout Effect [Loading...]', + 'Layout Effect [B]', + 'Destroy Effect [Loading...]', + 'Effect [B]', + ]); // Update ReactNoop.renderLegacySyncRoot(, () => @@ -1786,30 +1771,15 @@ describe('ReactSuspenseWithNoopRenderer', () => { expect(Scheduler).toHaveYielded(['Promise resolved [B2]']); - if ( - ReactFeatureFlags.deferPassiveEffectCleanupDuringUnmount && - ReactFeatureFlags.runAllPassiveEffectDestroysBeforeCreates - ) { - expect(Scheduler).toFlushAndYield([ - 'B2', - 'Destroy Layout Effect [Loading...]', - 'Destroy Layout Effect [B]', - 'Layout Effect [B2]', - 'Destroy Effect [Loading...]', - 'Destroy Effect [B]', - 'Effect [B2]', - ]); - } else { - expect(Scheduler).toFlushAndYield([ - 'B2', - 'Destroy Layout Effect [Loading...]', - 'Destroy Effect [Loading...]', - 'Destroy Layout Effect [B]', - 'Layout Effect [B2]', - 'Destroy Effect [B]', - 'Effect [B2]', - ]); - } + expect(Scheduler).toFlushAndYield([ + 'B2', + 'Destroy Layout Effect [Loading...]', + 'Destroy Layout Effect [B]', + 'Layout Effect [B2]', + 'Destroy Effect [Loading...]', + 'Destroy Effect [B]', + 'Effect [B2]', + ]); }); it('suspends for longer if something took a long (CPU bound) time to render', async () => { diff --git a/packages/react/src/__tests__/ReactProfiler-test.internal.js b/packages/react/src/__tests__/ReactProfiler-test.internal.js index 63f596f391..bccca51040 100644 --- a/packages/react/src/__tests__/ReactProfiler-test.internal.js +++ b/packages/react/src/__tests__/ReactProfiler-test.internal.js @@ -24,7 +24,6 @@ let TextResource; let resourcePromise; function loadModules({ - deferPassiveEffectCleanupDuringUnmount = false, enableProfilerTimer = true, enableProfilerCommitHooks = true, enableSchedulerTracing = true, @@ -33,8 +32,6 @@ function loadModules({ } = {}) { ReactFeatureFlags = require('shared/ReactFeatureFlags'); - ReactFeatureFlags.deferPassiveEffectCleanupDuringUnmount = deferPassiveEffectCleanupDuringUnmount; - ReactFeatureFlags.runAllPassiveEffectDestroysBeforeCreates = deferPassiveEffectCleanupDuringUnmount; ReactFeatureFlags.enableProfilerTimer = enableProfilerTimer; ReactFeatureFlags.enableProfilerCommitHooks = enableProfilerCommitHooks; ReactFeatureFlags.enableSchedulerTracing = enableSchedulerTracing; @@ -186,1872 +183,2173 @@ describe('Profiler', () => { }); }); - [true, false].forEach(deferPassiveEffectCleanupDuringUnmount => { - [true, false].forEach(enableSchedulerTracing => { - describe(`onRender enableSchedulerTracing:${ - enableSchedulerTracing ? 'enabled' : 'disabled' - } deferPassiveEffectCleanupDuringUnmount:${ - deferPassiveEffectCleanupDuringUnmount ? 'enabled' : 'disabled' - }`, () => { - beforeEach(() => { - jest.resetModules(); - - loadModules({ - deferPassiveEffectCleanupDuringUnmount, - enableSchedulerTracing, - }); - }); - - 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; - } - } - - // 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); - }); - - it('is not invoked until the commit phase', () => { - const callback = jest.fn(); - - const Yield = ({value}) => { - Scheduler.unstable_yieldValue(value); - return null; - }; - - ReactTestRenderer.create( - - - - , - { - unstable_isConcurrent: true, - }, - ); - - // Times are logged until a render is committed. - expect(Scheduler).toFlushAndYieldThrough(['first']); - expect(callback).toHaveBeenCalledTimes(0); - expect(Scheduler).toFlushAndYield(['last']); - expect(callback).toHaveBeenCalledTimes(1); - }); - - it('does not record times for components outside of Profiler tree', () => { - // Mock the Scheduler module so we can track how many times the current - // time is read - jest.mock('scheduler', obj => { - const ActualScheduler = require.requireActual( - 'scheduler/unstable_mock', - ); - return { - ...ActualScheduler, - unstable_now: function mockUnstableNow() { - ActualScheduler.unstable_yieldValue('read current time'); - return ActualScheduler.unstable_now(); - }, - }; - }); - - jest.resetModules(); - - loadModules({enableSchedulerTracing}); - - // Clear yields in case the current time is read during initialization. - Scheduler.unstable_clearYields(); - - ReactTestRenderer.create( -
- - - - - -
, - ); - - // Should be called two times: - // 1. To compute the update expiration time - // 2. To record the commit time - // No additional calls from ProfilerTimer are expected. - expect(Scheduler).toHaveYielded( - gate(flags => - flags.new - ? [ - // The new reconciler reads the current time in more places, - // to detect starvation. This is unrelated to the profiler, - // which happens to use the same Scheduler method that we - // mocked above. We should rewrite this test so that it's - // less fragile. - 'read current time', - 'read current time', - 'read current time', - 'read current time', - ] - : ['read current time', 'read current time'], - ), - ); - - // Restore original mock - jest.mock('scheduler', () => - require.requireActual('scheduler/unstable_mock'), - ); - }); - - it('does not report work done on a sibling', () => { - const callback = jest.fn(); - - const DoesNotUpdate = React.memo( - function DoesNotUpdateInner() { - Scheduler.unstable_advanceTime(10); - return null; - }, - () => true, - ); - - let updateProfilerSibling; - - function ProfilerSibling() { - const [count, setCount] = React.useState(0); - updateProfilerSibling = () => setCount(count + 1); - return null; - } - - function App() { - return ( - - - - - - - ); - } - - const renderer = ReactTestRenderer.create(); - - expect(callback).toHaveBeenCalledTimes(1); - - let call = callback.mock.calls[0]; - - 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(0); // start time - expect(call[5]).toBe(10); // commit time - expect(call[6]).toEqual( - enableSchedulerTracing ? new Set() : undefined, - ); // interaction events - - callback.mockReset(); - - Scheduler.unstable_advanceTime(20); // 10 -> 30 - - // Updating a parent should report a re-render, - // since React technically did a little bit of work between the Profiler and the bailed out subtree. - renderer.update(); - - expect(callback).toHaveBeenCalledTimes(1); - - call = callback.mock.calls[0]; - - expect(call).toHaveLength(enableSchedulerTracing ? 7 : 6); - expect(call[0]).toBe('test'); - expect(call[1]).toBe('update'); - expect(call[2]).toBe(0); // actual time - expect(call[3]).toBe(10); // base time - expect(call[4]).toBe(30); // start time - expect(call[5]).toBe(30); // commit time - expect(call[6]).toEqual( - enableSchedulerTracing ? new Set() : undefined, - ); // interaction events - - callback.mockReset(); - - Scheduler.unstable_advanceTime(20); // 30 -> 50 - - // Updating a sibling should not report a re-render. - ReactTestRenderer.act(updateProfilerSibling); - - expect(callback).not.toHaveBeenCalled(); - }); - - it('logs render times for both mount and update', () => { - const callback = jest.fn(); - - Scheduler.unstable_advanceTime(5); // 0 -> 5 - - const renderer = ReactTestRenderer.create( - - - , - ); - - expect(callback).toHaveBeenCalledTimes(1); - - 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 - - callback.mockReset(); - - Scheduler.unstable_advanceTime(20); // 15 -> 35 - - renderer.update( - - - , - ); - - expect(callback).toHaveBeenCalledTimes(1); - - [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 - - callback.mockReset(); - - Scheduler.unstable_advanceTime(20); // 45 -> 65 - - renderer.update( - - - , - ); - - expect(callback).toHaveBeenCalledTimes(1); - - [call] = callback.mock.calls; - - 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('includes render times of nested Profilers in their parent times', () => { - const callback = jest.fn(); - - Scheduler.unstable_advanceTime(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(); - - Scheduler.unstable_advanceTime(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(); - - Scheduler.unstable_advanceTime(5); // 0 -> 5 - - ReactTestRenderer.create( - - - - - - - , - ); - - 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 - }); - - it('is not called when blocked by sCU false', () => { - const callback = jest.fn(); - - let instance; - class Updater extends React.Component { - state = {}; - render() { - instance = this; - return this.props.children; - } - } - - const renderer = ReactTestRenderer.create( - - - -
- - - , - ); - - // All profile callbacks are called for initial render - expect(callback).toHaveBeenCalledTimes(2); - - callback.mockReset(); - - renderer.unstable_flushSync(() => { - instance.setState({ - count: 1, - }); - }); - - // Only call onRender for paths that have re-rendered. - // Since the Updater's props didn't change, - // React does not re-render its children. - expect(callback).toHaveBeenCalledTimes(1); - expect(callback.mock.calls[0][0]).toBe('outer'); - }); - - it('decreases actual time but not base time when sCU prevents an update', () => { - const callback = jest.fn(); - - Scheduler.unstable_advanceTime(5); // 0 -> 5 - - const renderer = ReactTestRenderer.create( - - - - - , - ); - - expect(callback).toHaveBeenCalledTimes(1); - - Scheduler.unstable_advanceTime(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() { - Scheduler.unstable_advanceTime(3); - return null; - } - shouldComponentUpdate() { - Scheduler.unstable_advanceTime(7); - return true; - } - render() { - Scheduler.unstable_advanceTime(5); - return null; - } - } - - const callback = jest.fn(); - - Scheduler.unstable_advanceTime(5); // 0 -> 5 - - const renderer = ReactTestRenderer.create( - - - , - ); - - Scheduler.unstable_advanceTime(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}) => { - Scheduler.unstable_advanceTime(renderTime); - Scheduler.unstable_yieldValue('Yield:' + renderTime); - return null; - }; - - Scheduler.unstable_advanceTime(5); // 0 -> 5 - - // Render partially, but run out of time before completing. - ReactTestRenderer.create( - - - - , - {unstable_isConcurrent: true}, - ); - expect(Scheduler).toFlushAndYieldThrough(['Yield:2']); - expect(callback).toHaveBeenCalledTimes(0); - - // Resume render for remaining children. - expect(Scheduler).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 - }); - - it('should not include time between frames', () => { - const callback = jest.fn(); - - const Yield = ({renderTime}) => { - Scheduler.unstable_advanceTime(renderTime); - Scheduler.unstable_yieldValue('Yield:' + renderTime); - return null; - }; - - Scheduler.unstable_advanceTime(5); // 0 -> 5 - - // Render partially, but don't finish. - // This partial render should take 5ms of simulated time. - ReactTestRenderer.create( - - - - - - - , - {unstable_isConcurrent: true}, - ); - expect(Scheduler).toFlushAndYieldThrough(['Yield:5']); - expect(callback).toHaveBeenCalledTimes(0); - - // Simulate time moving forward while frame is paused. - Scheduler.unstable_advanceTime(50); // 10 -> 60 - - // Flush the remaining work, - // Which should take an additional 10ms of simulated time. - expect(Scheduler).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 - }); - - it('should report the expected times when a high-pri update replaces a mount in-progress', () => { - const callback = jest.fn(); - - const Yield = ({renderTime}) => { - Scheduler.unstable_advanceTime(renderTime); - Scheduler.unstable_yieldValue('Yield:' + renderTime); - return null; - }; - - Scheduler.unstable_advanceTime(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(Scheduler).toFlushAndYieldThrough(['Yield:10']); - expect(callback).toHaveBeenCalledTimes(0); - - // Simulate time moving forward while frame is paused. - Scheduler.unstable_advanceTime(100); // 15 -> 115 - - // Interrupt with higher priority work. - // The interrupted work simulates an additional 5ms of time. - renderer.unstable_flushSync(() => { - renderer.update( - - - , - ); - }); - expect(Scheduler).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); - const 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(Scheduler).toFlushWithoutYielding(); - expect(callback).toHaveBeenCalledTimes(0); - }); - - it('should report the expected times when a high-priority update replaces a low-priority update', () => { - const callback = jest.fn(); - - const Yield = ({renderTime}) => { - Scheduler.unstable_advanceTime(renderTime); - Scheduler.unstable_yieldValue('Yield:' + renderTime); - return null; - }; - - Scheduler.unstable_advanceTime(5); // 0 -> 5 - - const renderer = ReactTestRenderer.create( - - - - , - {unstable_isConcurrent: true}, - ); - - // Render everything initially. - // This should take 21 seconds of actual and base time. - expect(Scheduler).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(); - - Scheduler.unstable_advanceTime(30); // 26 -> 56 - - // Render a partially update, but don't finish. - // This partial render should take 3ms of simulated time. - renderer.update( - - - - - , - ); - expect(Scheduler).toFlushAndYieldThrough(['Yield:3']); - expect(callback).toHaveBeenCalledTimes(0); - - // Simulate time moving forward while frame is paused. - Scheduler.unstable_advanceTime(100); // 59 -> 159 - - // Render another 5ms of simulated time. - expect(Scheduler).toFlushAndYieldThrough(['Yield:5']); - expect(callback).toHaveBeenCalledTimes(0); - - // Simulate time moving forward while frame is paused. - Scheduler.unstable_advanceTime(100); // 164 -> 264 - - // Interrupt with higher priority work. - // The interrupted work simulates an additional 11ms of time. - renderer.unstable_flushSync(() => { - renderer.update( - - - , - ); - }); - expect(Scheduler).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(Scheduler).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(); - - const Yield = ({renderTime}) => { - Scheduler.unstable_advanceTime(renderTime); - Scheduler.unstable_yieldValue('Yield:' + renderTime); - return null; - }; - - let first; - class FirstComponent extends React.Component { - state = {renderTime: 1}; - render() { - first = this; - Scheduler.unstable_advanceTime(this.state.renderTime); - Scheduler.unstable_yieldValue( - 'FirstComponent:' + this.state.renderTime, - ); - return ; - } - } - let second; - class SecondComponent extends React.Component { - state = {renderTime: 2}; - render() { - second = this; - Scheduler.unstable_advanceTime(this.state.renderTime); - Scheduler.unstable_yieldValue( - 'SecondComponent:' + this.state.renderTime, - ); - return ; - } - } - - Scheduler.unstable_advanceTime(5); // 0 -> 5 - - const renderer = ReactTestRenderer.create( - - - - , - {unstable_isConcurrent: true}, - ); - - // 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(Scheduler).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(); - - Scheduler.unstable_advanceTime(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(Scheduler).toFlushAndYieldThrough(['FirstComponent:10']); - expect(callback).toHaveBeenCalledTimes(0); - - // Simulate time moving forward while frame is paused. - Scheduler.unstable_advanceTime(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(Scheduler).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. - Scheduler.unstable_advanceTime(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(Scheduler).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 - }); - - [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 = ({unused}) => { - Scheduler.unstable_advanceTime(3); - throw Error('expected error'); - }; - - class ErrorBoundary extends React.Component { - state = {error: null}; - componentDidCatch(error) { - this.setState({error}); - } - render() { - Scheduler.unstable_advanceTime(2); - return this.state.error === null ? ( - this.props.children - ) : ( - - ); - } - } - - Scheduler.unstable_advanceTime(5); // 0 -> 5 - - ReactTestRenderer.create( - - - - - - , - ); - - expect(callback).toHaveBeenCalledTimes(2); - - // Callbacks bubble (reverse order). - const [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, - ); - }); - - it('should accumulate actual time after an error handled by getDerivedStateFromError()', () => { - const callback = jest.fn(); - - const ThrowsError = ({unused}) => { - Scheduler.unstable_advanceTime(10); - throw Error('expected error'); - }; - - class ErrorBoundary extends React.Component { - state = {error: null}; - static getDerivedStateFromError(error) { - return {error}; - } - render() { - Scheduler.unstable_advanceTime(2); - return this.state.error === null ? ( - this.props.children - ) : ( - - ); - } - } - - Scheduler.unstable_advanceTime(5); // 0 -> 5 - - ReactTestRenderer.create( - - - - - - , - ); - - expect(callback).toHaveBeenCalledTimes(1); - - // Callbacks bubble (reverse order). - const [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(Scheduler).toFlushAndThrow('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(Scheduler).toFlushAndThrow('Error in host config.'); - - // So long as the profiler timer's fiber stack is reset correctly, - // Subsequent renders should not error. - ReactNoop.render( - - hi - , - ); - expect(Scheduler).toFlushWithoutYielding(); - }); - }); - }, - ); - }); - - it('reflects the most recently rendered id value', () => { - const callback = jest.fn(); - - Scheduler.unstable_advanceTime(5); // 0 -> 5 - - const renderer = ReactTestRenderer.create( - - - , - ); - - expect(callback).toHaveBeenCalledTimes(1); - - Scheduler.unstable_advanceTime(20); // 7 -> 27 - - renderer.update( - - - , - ); - - expect(callback).toHaveBeenCalledTimes(2); - - 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(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); - }, - ); - - class ClassComponent extends React.Component { - componentDidMount() { - Scheduler.unstable_advanceTime(5); - classComponentMounted = true; - } - render() { - Scheduler.unstable_advanceTime(2); - return null; - } - } - - ReactTestRenderer.create( - - - , - ); - - expect(callback).toHaveBeenCalledTimes(1); + [true, false].forEach(enableSchedulerTracing => { + describe(`onRender enableSchedulerTracing:${ + enableSchedulerTracing ? 'enabled' : 'disabled' + }`, () => { + beforeEach(() => { + jest.resetModules(); + + loadModules({ + enableSchedulerTracing, }); }); - describe(`onCommit enableSchedulerTracing:${ - enableSchedulerTracing ? 'enabled' : 'disabled' - } deferPassiveEffectCleanupDuringUnmount:${ - deferPassiveEffectCleanupDuringUnmount ? 'enabled' : 'disabled' - }`, () => { - beforeEach(() => { - jest.resetModules(); + it('should handle errors thrown', () => { + const callback = jest.fn(id => { + if (id === 'throw') { + throw Error('expected'); + } + }); - loadModules({ - deferPassiveEffectCleanupDuringUnmount, - enableSchedulerTracing, + 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( + + + +
+ + + , + ), + ).toThrow('expected'); + expect(didMount).toBe(true); + expect(callback).toHaveBeenCalledTimes(2); + }); + + it('is not invoked until the commit phase', () => { + const callback = jest.fn(); + + const Yield = ({value}) => { + Scheduler.unstable_yieldValue(value); + return null; + }; + + ReactTestRenderer.create( + + + + , + { + unstable_isConcurrent: true, + }, + ); + + // Times are logged until a render is committed. + expect(Scheduler).toFlushAndYieldThrough(['first']); + expect(callback).toHaveBeenCalledTimes(0); + expect(Scheduler).toFlushAndYield(['last']); + expect(callback).toHaveBeenCalledTimes(1); + }); + + it('does not record times for components outside of Profiler tree', () => { + // Mock the Scheduler module so we can track how many times the current + // time is read + jest.mock('scheduler', obj => { + const ActualScheduler = require.requireActual( + 'scheduler/unstable_mock', + ); + return { + ...ActualScheduler, + unstable_now: function mockUnstableNow() { + ActualScheduler.unstable_yieldValue('read current time'); + return ActualScheduler.unstable_now(); + }, + }; + }); + + jest.resetModules(); + + loadModules({enableSchedulerTracing}); + + // Clear yields in case the current time is read during initialization. + Scheduler.unstable_clearYields(); + + ReactTestRenderer.create( +
+ + + + + +
, + ); + + // Should be called two times: + // 1. To compute the update expiration time + // 2. To record the commit time + // No additional calls from ProfilerTimer are expected. + expect(Scheduler).toHaveYielded( + gate(flags => + flags.new + ? [ + // The new reconciler reads the current time in more places, + // to detect starvation. This is unrelated to the profiler, + // which happens to use the same Scheduler method that we + // mocked above. We should rewrite this test so that it's + // less fragile. + 'read current time', + 'read current time', + 'read current time', + 'read current time', + ] + : ['read current time', 'read current time'], + ), + ); + + // Restore original mock + jest.mock('scheduler', () => + require.requireActual('scheduler/unstable_mock'), + ); + }); + + it('does not report work done on a sibling', () => { + const callback = jest.fn(); + + const DoesNotUpdate = React.memo( + function DoesNotUpdateInner() { + Scheduler.unstable_advanceTime(10); + return null; + }, + () => true, + ); + + let updateProfilerSibling; + + function ProfilerSibling() { + const [count, setCount] = React.useState(0); + updateProfilerSibling = () => setCount(count + 1); + return null; + } + + function App() { + return ( + + + + + + + ); + } + + const renderer = ReactTestRenderer.create(); + + expect(callback).toHaveBeenCalledTimes(1); + + let call = callback.mock.calls[0]; + + 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(0); // start time + expect(call[5]).toBe(10); // commit time + expect(call[6]).toEqual(enableSchedulerTracing ? new Set() : undefined); // interaction events + + callback.mockReset(); + + Scheduler.unstable_advanceTime(20); // 10 -> 30 + + // Updating a parent should report a re-render, + // since React technically did a little bit of work between the Profiler and the bailed out subtree. + renderer.update(); + + expect(callback).toHaveBeenCalledTimes(1); + + call = callback.mock.calls[0]; + + expect(call).toHaveLength(enableSchedulerTracing ? 7 : 6); + expect(call[0]).toBe('test'); + expect(call[1]).toBe('update'); + expect(call[2]).toBe(0); // actual time + expect(call[3]).toBe(10); // base time + expect(call[4]).toBe(30); // start time + expect(call[5]).toBe(30); // commit time + expect(call[6]).toEqual(enableSchedulerTracing ? new Set() : undefined); // interaction events + + callback.mockReset(); + + Scheduler.unstable_advanceTime(20); // 30 -> 50 + + // Updating a sibling should not report a re-render. + ReactTestRenderer.act(updateProfilerSibling); + + expect(callback).not.toHaveBeenCalled(); + }); + + it('logs render times for both mount and update', () => { + const callback = jest.fn(); + + Scheduler.unstable_advanceTime(5); // 0 -> 5 + + const renderer = ReactTestRenderer.create( + + + , + ); + + expect(callback).toHaveBeenCalledTimes(1); + + 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 + + callback.mockReset(); + + Scheduler.unstable_advanceTime(20); // 15 -> 35 + + renderer.update( + + + , + ); + + expect(callback).toHaveBeenCalledTimes(1); + + [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 + + callback.mockReset(); + + Scheduler.unstable_advanceTime(20); // 45 -> 65 + + renderer.update( + + + , + ); + + expect(callback).toHaveBeenCalledTimes(1); + + [call] = callback.mock.calls; + + 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('includes render times of nested Profilers in their parent times', () => { + const callback = jest.fn(); + + Scheduler.unstable_advanceTime(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(); + + Scheduler.unstable_advanceTime(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(); + + Scheduler.unstable_advanceTime(5); // 0 -> 5 + + ReactTestRenderer.create( + + + + + + + , + ); + + 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 + }); + + it('is not called when blocked by sCU false', () => { + const callback = jest.fn(); + + let instance; + class Updater extends React.Component { + state = {}; + render() { + instance = this; + return this.props.children; + } + } + + const renderer = ReactTestRenderer.create( + + + +
+ + + , + ); + + // All profile callbacks are called for initial render + expect(callback).toHaveBeenCalledTimes(2); + + callback.mockReset(); + + renderer.unstable_flushSync(() => { + instance.setState({ + count: 1, }); }); - it('should report time spent in layout effects and commit lifecycles', () => { + // Only call onRender for paths that have re-rendered. + // Since the Updater's props didn't change, + // React does not re-render its children. + expect(callback).toHaveBeenCalledTimes(1); + expect(callback.mock.calls[0][0]).toBe('outer'); + }); + + it('decreases actual time but not base time when sCU prevents an update', () => { + const callback = jest.fn(); + + Scheduler.unstable_advanceTime(5); // 0 -> 5 + + const renderer = ReactTestRenderer.create( + + + + + , + ); + + expect(callback).toHaveBeenCalledTimes(1); + + Scheduler.unstable_advanceTime(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() { + Scheduler.unstable_advanceTime(3); + return null; + } + shouldComponentUpdate() { + Scheduler.unstable_advanceTime(7); + return true; + } + render() { + Scheduler.unstable_advanceTime(5); + return null; + } + } + + const callback = jest.fn(); + + Scheduler.unstable_advanceTime(5); // 0 -> 5 + + const renderer = ReactTestRenderer.create( + + + , + ); + + Scheduler.unstable_advanceTime(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 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 () => { - Scheduler.unstable_advanceTime(7); - }; - }); + const Yield = ({renderTime}) => { + Scheduler.unstable_advanceTime(renderTime); + Scheduler.unstable_yieldValue('Yield:' + renderTime); return null; }; - class ComponentWithCommitHooks extends React.Component { - componentDidMount() { - Scheduler.unstable_advanceTime(100000); - } - componentDidUpdate() { - Scheduler.unstable_advanceTime(1000000); - } - render() { - return null; - } - } + Scheduler.unstable_advanceTime(5); // 0 -> 5 - Scheduler.unstable_advanceTime(1); - - const renderer = ReactTestRenderer.create( - - - + // Render partially, but run out of time before completing. + ReactTestRenderer.create( + + + , + {unstable_isConcurrent: true}, ); + expect(Scheduler).toFlushAndYieldThrough(['Yield:2']); + expect(callback).toHaveBeenCalledTimes(0); + // Resume render for remaining children. + expect(Scheduler).toFlushAndYield(['Yield:3']); + + // Verify that logged times include both durations above. 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(1112030); // commit start time (before mutations or effects) - expect(call[4]).toEqual( - enableSchedulerTracing ? new Set() : undefined, - ); // interaction events + 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 }); - it('should report time spent in layout effects and commit lifecycles with cascading renders', () => { + it('should not include time between frames', () => { 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]); + const Yield = ({renderTime}) => { + Scheduler.unstable_advanceTime(renderTime); + Scheduler.unstable_yieldValue('Yield:' + renderTime); 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(5); // 0 -> 5 - Scheduler.unstable_advanceTime(1); - - const renderer = ReactTestRenderer.create( - - - + // Render partially, but don't finish. + // This partial render should take 5ms of simulated time. + ReactTestRenderer.create( + + + + + + , + {unstable_isConcurrent: true}, ); + expect(Scheduler).toFlushAndYieldThrough(['Yield:5']); + expect(callback).toHaveBeenCalledTimes(0); + // Simulate time moving forward while frame is paused. + Scheduler.unstable_advanceTime(50); // 10 -> 60 + + // Flush the remaining work, + // Which should take an additional 10ms of simulated time. + expect(Scheduler).toFlushAndYield(['Yield:10', 'Yield:17']); expect(callback).toHaveBeenCalledTimes(2); - let call = callback.mock.calls[0]; + const [innerCall, outerCall] = callback.mock.calls; - 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 + // 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('should bubble time spent in layout effects to higher profilers', () => { + it('should report the expected times when a high-pri update replaces a mount in-progress', () => { 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); + const Yield = ({renderTime}) => { + Scheduler.unstable_advanceTime(renderTime); + Scheduler.unstable_yieldValue('Yield:' + renderTime); return null; }; - const setCountRef = React.createRef(null); + Scheduler.unstable_advanceTime(5); // 0 -> 5 - let renderer = null; - ReactTestRenderer.act(() => { - renderer = ReactTestRenderer.create( - - - - - - - - , - ); - }); + // 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(Scheduler).toFlushAndYieldThrough(['Yield:10']); + expect(callback).toHaveBeenCalledTimes(0); - expect(callback).toHaveBeenCalledTimes(1); + // Simulate time moving forward while frame is paused. + Scheduler.unstable_advanceTime(100); // 15 -> 115 - 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(() => { + // Interrupt with higher priority work. + // The interrupted work simulates an additional 5ms of time. + renderer.unstable_flushSync(() => { 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(Scheduler).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); + const 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(Scheduler).toFlushWithoutYielding(); + expect(callback).toHaveBeenCalledTimes(0); + }); + + it('should report the expected times when a high-priority update replaces a low-priority update', () => { + const callback = jest.fn(); + + const Yield = ({renderTime}) => { + Scheduler.unstable_advanceTime(renderTime); + Scheduler.unstable_yieldValue('Yield:' + renderTime); + return null; + }; + + Scheduler.unstable_advanceTime(5); // 0 -> 5 + + const renderer = ReactTestRenderer.create( + + + + , + {unstable_isConcurrent: true}, + ); + + // Render everything initially. + // This should take 21 seconds of actual and base time. + expect(Scheduler).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 - // 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.mockReset(); + + Scheduler.unstable_advanceTime(30); // 26 -> 56 + + // Render a partially update, but don't finish. + // This partial render should take 3ms of simulated time. + renderer.update( + + + + + , + ); + expect(Scheduler).toFlushAndYieldThrough(['Yield:3']); + expect(callback).toHaveBeenCalledTimes(0); + + // Simulate time moving forward while frame is paused. + Scheduler.unstable_advanceTime(100); // 59 -> 159 + + // Render another 5ms of simulated time. + expect(Scheduler).toFlushAndYieldThrough(['Yield:5']); + expect(callback).toHaveBeenCalledTimes(0); + + // Simulate time moving forward while frame is paused. + Scheduler.unstable_advanceTime(100); // 164 -> 264 + + // Interrupt with higher priority work. + // The interrupted work simulates an additional 11ms of time. + renderer.unstable_flushSync(() => { + renderer.update( + + + , + ); + }); + expect(Scheduler).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(Scheduler).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(); + + const Yield = ({renderTime}) => { + Scheduler.unstable_advanceTime(renderTime); + Scheduler.unstable_yieldValue('Yield:' + renderTime); + return null; + }; + + let first; + class FirstComponent extends React.Component { + state = {renderTime: 1}; + render() { + first = this; + Scheduler.unstable_advanceTime(this.state.renderTime); + Scheduler.unstable_yieldValue( + 'FirstComponent:' + this.state.renderTime, + ); + return ; + } + } + let second; + class SecondComponent extends React.Component { + state = {renderTime: 2}; + render() { + second = this; + Scheduler.unstable_advanceTime(this.state.renderTime); + Scheduler.unstable_yieldValue( + 'SecondComponent:' + this.state.renderTime, + ); + return ; + } + } + + Scheduler.unstable_advanceTime(5); // 0 -> 5 + + const renderer = ReactTestRenderer.create( + + + + , + {unstable_isConcurrent: true}, + ); + + // 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(Scheduler).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(); - // Test an error that happens during an cleanup function + Scheduler.unstable_advanceTime(100); // 19 -> 119 - ReactTestRenderer.act(() => { - renderer.update( - - - }> - - - - , - ); - }); + // Render a partially update, but don't finish. + // This partial render will take 10ms of actual render time. + first.setState({renderTime: 10}); + expect(Scheduler).toFlushAndYieldThrough(['FirstComponent:10']); + expect(callback).toHaveBeenCalledTimes(0); - expect(callback).toHaveBeenCalledTimes(2); + // Simulate time moving forward while frame is paused. + Scheduler.unstable_advanceTime(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(Scheduler).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 - // 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 + callback.mockClear(); - call = callback.mock.calls[1]; + // Simulate time moving forward while frame is paused. + Scheduler.unstable_advanceTime(100); // 266 -> 366 - // 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 + // 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(Scheduler).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 }); - if (enableSchedulerTracing) { - it('should report interactions that were active', () => { - const callback = jest.fn(); + [true, false].forEach( + replayFailedUnitOfWorkWithInvokeGuardedCallback => { + describe(`replayFailedUnitOfWorkWithInvokeGuardedCallback ${ + replayFailedUnitOfWorkWithInvokeGuardedCallback + ? 'enabled' + : 'disabled' + }`, () => { + beforeEach(() => { + jest.resetModules(); - 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); + loadModules({ + replayFailedUnitOfWorkWithInvokeGuardedCallback, + }); + }); + + it('should accumulate actual time after an error handled by componentDidCatch()', () => { + const callback = jest.fn(); + + const ThrowsError = ({unused}) => { + Scheduler.unstable_advanceTime(3); + throw Error('expected error'); }; - }, [didMount]); - Scheduler.unstable_advanceTime(10); - return null; - }; - const interaction = { - id: 0, - name: 'mount', - timestamp: Scheduler.unstable_now(), - }; + class ErrorBoundary extends React.Component { + state = {error: null}; + componentDidCatch(error) { + this.setState({error}); + } + render() { + Scheduler.unstable_advanceTime(2); + return this.state.error === null ? ( + this.props.children + ) : ( + + ); + } + } - Scheduler.unstable_advanceTime(1); + Scheduler.unstable_advanceTime(5); // 0 -> 5 - SchedulerTracing.unstable_trace( - interaction.name, - interaction.timestamp, - () => { ReactTestRenderer.create( - - + + + + + , ); - }, - ); - expect(callback).toHaveBeenCalledTimes(2); + expect(callback).toHaveBeenCalledTimes(2); - let call = callback.mock.calls[0]; + // Callbacks bubble (reverse order). + const [mountCall, updateCall] = callback.mock.calls; - expect(call).toHaveLength(enableSchedulerTracing ? 5 : 4); - expect(call[0]).toBe('root'); - expect(call[1]).toBe('mount'); - expect(call[4]).toMatchInteractions([interaction]); + // 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, + ); - call = callback.mock.calls[1]; + // 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(call).toHaveLength(enableSchedulerTracing ? 5 : 4); - expect(call[0]).toBe('root'); - expect(call[1]).toBe('update'); - expect(call[4]).toMatchInteractions([interaction]); - }); - } + it('should accumulate actual time after an error handled by getDerivedStateFromError()', () => { + const callback = jest.fn(); + + const ThrowsError = ({unused}) => { + Scheduler.unstable_advanceTime(10); + throw Error('expected error'); + }; + + class ErrorBoundary extends React.Component { + state = {error: null}; + static getDerivedStateFromError(error) { + return {error}; + } + render() { + Scheduler.unstable_advanceTime(2); + return this.state.error === null ? ( + this.props.children + ) : ( + + ); + } + } + + Scheduler.unstable_advanceTime(5); // 0 -> 5 + + ReactTestRenderer.create( + + + + + + , + ); + + expect(callback).toHaveBeenCalledTimes(1); + + // Callbacks bubble (reverse order). + const [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(Scheduler).toFlushAndThrow('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(Scheduler).toFlushAndThrow('Error in host config.'); + + // So long as the profiler timer's fiber stack is reset correctly, + // Subsequent renders should not error. + ReactNoop.render( + + hi + , + ); + expect(Scheduler).toFlushWithoutYielding(); + }); + }); + }, + ); }); - describe(`onPostCommit enableSchedulerTracing:${ - enableSchedulerTracing ? 'enabled' : 'disabled' - } deferPassiveEffectCleanupDuringUnmount:${ - deferPassiveEffectCleanupDuringUnmount ? 'enabled' : 'disabled' - }`, () => { - beforeEach(() => { - jest.resetModules(); + it('reflects the most recently rendered id value', () => { + const callback = jest.fn(); - loadModules({ - deferPassiveEffectCleanupDuringUnmount, - enableSchedulerTracing, + Scheduler.unstable_advanceTime(5); // 0 -> 5 + + const renderer = ReactTestRenderer.create( + + + , + ); + + expect(callback).toHaveBeenCalledTimes(1); + + Scheduler.unstable_advanceTime(20); // 7 -> 27 + + renderer.update( + + + , + ); + + expect(callback).toHaveBeenCalledTimes(2); + + 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(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); + }, + ); + + class ClassComponent extends React.Component { + componentDidMount() { + Scheduler.unstable_advanceTime(5); + classComponentMounted = true; + } + render() { + Scheduler.unstable_advanceTime(2); + return null; + } + } + + ReactTestRenderer.create( + + + , + ); + + expect(callback).toHaveBeenCalledTimes(1); + }); + }); + + describe(`onCommit enableSchedulerTracing:${ + enableSchedulerTracing ? 'enabled' : 'disabled' + }`, () => { + 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 () => { + Scheduler.unstable_advanceTime(7); + }; + }); + 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(1112030); // 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( + + + + + + + + , + ); }); - it('should report time spent in passive effects', () => { + 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(() => { - // This layout effect is here to verify that its time isn't reported. - Scheduler.unstable_advanceTime(5); - return () => { - Scheduler.unstable_advanceTime(7); - }; - }); - React.useEffect(() => { - Scheduler.unstable_advanceTime(10); - return () => { - Scheduler.unstable_advanceTime(100); - }; - }, []); - React.useEffect(() => { - Scheduler.unstable_advanceTime(1000); + 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); - let renderer; - ReactTestRenderer.act(() => { - renderer = ReactTestRenderer.create( - - - , - ); - }); - Scheduler.unstable_flushAll(); + SchedulerTracing.unstable_trace( + interaction.name, + interaction.timestamp, + () => { + ReactTestRenderer.create( + + + , + ); + }, + ); - expect(callback).toHaveBeenCalledTimes(1); + expect(callback).toHaveBeenCalledTimes(2); let call = callback.mock.calls[0]; expect(call).toHaveLength(enableSchedulerTracing ? 5 : 4); - expect(call[0]).toBe('mount-test'); + expect(call[0]).toBe('root'); 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( - - - , - ); - }); - Scheduler.unstable_flushAll(); - - expect(callback).toHaveBeenCalledTimes(2); + expect(call[4]).toMatchInteractions([interaction]); call = callback.mock.calls[1]; expect(call).toHaveLength(enableSchedulerTracing ? 5 : 4); - expect(call[0]).toBe('update-test'); + expect(call[0]).toBe('root'); 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 + expect(call[4]).toMatchInteractions([interaction]); + }); + } + }); - Scheduler.unstable_advanceTime(1); + describe(`onPostCommit enableSchedulerTracing:${ + enableSchedulerTracing ? 'enabled' : 'disabled' + }`, () => { + beforeEach(() => { + jest.resetModules(); - ReactTestRenderer.act(() => { - renderer.update( - , - ); + 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); + return () => { + Scheduler.unstable_advanceTime(7); + }; }); - Scheduler.unstable_flushAll(); + React.useEffect(() => { + Scheduler.unstable_advanceTime(10); + return () => { + Scheduler.unstable_advanceTime(100); + }; + }, []); + React.useEffect(() => { + Scheduler.unstable_advanceTime(1000); + return () => { + Scheduler.unstable_advanceTime(10000); + }; + }); + return null; + }; - expect(callback).toHaveBeenCalledTimes(3); + Scheduler.unstable_advanceTime(1); - call = callback.mock.calls[2]; + let renderer; + ReactTestRenderer.act(() => { + renderer = ReactTestRenderer.create( + + + , + ); + }); + Scheduler.unstable_flushAll(); - 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(12030); // commit start time (before mutations or effects) - expect(call[4]).toEqual( - enableSchedulerTracing ? new Set() : undefined, - ); // interaction events + 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( + + + , + ); + }); + Scheduler.unstable_flushAll(); + + 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( + , + ); + }); + Scheduler.unstable_flushAll(); + + 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(12030); // 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( + + + , + ); }); - it('should report time spent in passive effects with cascading renders', () => { + 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, + id, + }) => { + 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'); + // We continue flushing pending effects even if one throws. + 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(100000000); // durations + // The commit time varies because the above duration time varies + 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.useEffect(() => { + Scheduler.unstable_advanceTime(didMount ? 1000 : 100); if (!didMount) { setDidMount(true); } - Scheduler.unstable_advanceTime(didMount ? 30 : 10); return () => { - Scheduler.unstable_advanceTime(100); + 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(() => { - ReactTestRenderer.create( - - - , + SchedulerTracing.unstable_trace( + interaction.name, + interaction.timestamp, + () => { + ReactTestRenderer.create( + + + , + ); + }, ); }); @@ -2060,422 +2358,18 @@ describe('Profiler', () => { let call = callback.mock.calls[0]; expect(call).toHaveLength(enableSchedulerTracing ? 5 : 4); - expect(call[0]).toBe('mount-test'); + expect(call[0]).toBe('root'); 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 + expect(call[4]).toMatchInteractions([interaction]); call = callback.mock.calls[1]; expect(call).toHaveLength(enableSchedulerTracing ? 5 : 4); - expect(call[0]).toBe('mount-test'); + expect(call[0]).toBe('root'); 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 + expect(call[4]).toMatchInteractions([interaction]); }); - - 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, - id, - }) => { - 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'); - // The duration varies because the flushing behavior varies when this flag is on. - // We continue flushing pending effects even if one throws. - expect(call[2]).toBe( - deferPassiveEffectCleanupDuringUnmount ? 1101100 : 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 - // The commit time varies because the above duration time varies - expect(call[3]).toBe( - deferPassiveEffectCleanupDuringUnmount ? 11221221 : 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]); - }); - } - }); + } }); }); diff --git a/packages/shared/ReactFeatureFlags.js b/packages/shared/ReactFeatureFlags.js index 300bb5c859..466491b5b4 100644 --- a/packages/shared/ReactFeatureFlags.js +++ b/packages/shared/ReactFeatureFlags.js @@ -82,21 +82,6 @@ export const disableSchedulerTimeoutBasedOnReactExpirationTime = false; export const enableTrustedTypesIntegration = false; -// Controls sequence of passive effect destroy and create functions. -// If this flag is off, destroy and create functions may be interleaved. -// When the flag is on, all destroy functions will be run (for all fibers) -// before any create functions are run, similar to how layout effects work. -// This flag provides a killswitch if that proves to break existing code somehow. -export const runAllPassiveEffectDestroysBeforeCreates = false; - -// Controls behavior of deferred effect destroy functions during unmount. -// Previously these functions were run during commit (along with layout effects). -// Ideally we should delay these until after commit for performance reasons. -// This flag provides a killswitch if that proves to break existing code somehow. -// -// WARNING This flag only has an affect if used with runAllPassiveEffectDestroysBeforeCreates. -export const deferPassiveEffectCleanupDuringUnmount = false; - // Enables a warning when trying to spread a 'key' to an element; // a deprecated pattern we want to get rid of in the future export const warnAboutSpreadingKeyToJSX = false; diff --git a/packages/shared/forks/ReactFeatureFlags.native-fb.js b/packages/shared/forks/ReactFeatureFlags.native-fb.js index 375e579762..f4277d4c40 100644 --- a/packages/shared/forks/ReactFeatureFlags.native-fb.js +++ b/packages/shared/forks/ReactFeatureFlags.native-fb.js @@ -38,8 +38,6 @@ export const enableTrustedTypesIntegration = false; export const disableTextareaChildren = false; export const disableModulePatternComponents = false; export const warnUnstableRenderSubtreeIntoContainer = false; -export const deferPassiveEffectCleanupDuringUnmount = false; -export const runAllPassiveEffectDestroysBeforeCreates = false; export const enableModernEventSystem = false; export const warnAboutSpreadingKeyToJSX = false; export const enableComponentStackLocations = false; diff --git a/packages/shared/forks/ReactFeatureFlags.native-oss.js b/packages/shared/forks/ReactFeatureFlags.native-oss.js index c0e300fa17..f71b664b35 100644 --- a/packages/shared/forks/ReactFeatureFlags.native-oss.js +++ b/packages/shared/forks/ReactFeatureFlags.native-oss.js @@ -37,8 +37,6 @@ export const enableTrustedTypesIntegration = false; export const disableTextareaChildren = false; export const disableModulePatternComponents = false; export const warnUnstableRenderSubtreeIntoContainer = false; -export const deferPassiveEffectCleanupDuringUnmount = false; -export const runAllPassiveEffectDestroysBeforeCreates = false; export const enableModernEventSystem = false; export const warnAboutSpreadingKeyToJSX = false; export const enableComponentStackLocations = false; diff --git a/packages/shared/forks/ReactFeatureFlags.test-renderer.js b/packages/shared/forks/ReactFeatureFlags.test-renderer.js index 5dfa2ebf3e..8d16a4ba10 100644 --- a/packages/shared/forks/ReactFeatureFlags.test-renderer.js +++ b/packages/shared/forks/ReactFeatureFlags.test-renderer.js @@ -37,8 +37,6 @@ export const enableTrustedTypesIntegration = false; export const disableTextareaChildren = false; export const disableModulePatternComponents = false; export const warnUnstableRenderSubtreeIntoContainer = false; -export const deferPassiveEffectCleanupDuringUnmount = false; -export const runAllPassiveEffectDestroysBeforeCreates = false; export const enableModernEventSystem = false; export const warnAboutSpreadingKeyToJSX = false; export const enableComponentStackLocations = false; diff --git a/packages/shared/forks/ReactFeatureFlags.test-renderer.www.js b/packages/shared/forks/ReactFeatureFlags.test-renderer.www.js index 16a1e8fabe..b2b014065e 100644 --- a/packages/shared/forks/ReactFeatureFlags.test-renderer.www.js +++ b/packages/shared/forks/ReactFeatureFlags.test-renderer.www.js @@ -37,8 +37,6 @@ export const enableTrustedTypesIntegration = false; export const disableTextareaChildren = false; export const disableModulePatternComponents = true; export const warnUnstableRenderSubtreeIntoContainer = false; -export const deferPassiveEffectCleanupDuringUnmount = true; -export const runAllPassiveEffectDestroysBeforeCreates = true; export const enableModernEventSystem = false; export const warnAboutSpreadingKeyToJSX = false; export const enableComponentStackLocations = false; diff --git a/packages/shared/forks/ReactFeatureFlags.testing.js b/packages/shared/forks/ReactFeatureFlags.testing.js index b034a559fc..76667d09bb 100644 --- a/packages/shared/forks/ReactFeatureFlags.testing.js +++ b/packages/shared/forks/ReactFeatureFlags.testing.js @@ -37,8 +37,6 @@ export const enableTrustedTypesIntegration = false; export const disableTextareaChildren = false; export const disableModulePatternComponents = false; export const warnUnstableRenderSubtreeIntoContainer = false; -export const deferPassiveEffectCleanupDuringUnmount = false; -export const runAllPassiveEffectDestroysBeforeCreates = false; export const enableModernEventSystem = false; export const warnAboutSpreadingKeyToJSX = false; export const enableComponentStackLocations = false; diff --git a/packages/shared/forks/ReactFeatureFlags.testing.www.js b/packages/shared/forks/ReactFeatureFlags.testing.www.js index ae2ad698c3..5dd0f5b420 100644 --- a/packages/shared/forks/ReactFeatureFlags.testing.www.js +++ b/packages/shared/forks/ReactFeatureFlags.testing.www.js @@ -37,8 +37,6 @@ export const enableTrustedTypesIntegration = false; export const disableTextareaChildren = __EXPERIMENTAL__; export const disableModulePatternComponents = true; export const warnUnstableRenderSubtreeIntoContainer = false; -export const deferPassiveEffectCleanupDuringUnmount = true; -export const runAllPassiveEffectDestroysBeforeCreates = true; export const enableModernEventSystem = false; export const warnAboutSpreadingKeyToJSX = false; export const enableComponentStackLocations = false; diff --git a/packages/shared/forks/ReactFeatureFlags.www.js b/packages/shared/forks/ReactFeatureFlags.www.js index b9ee3f0408..82c05a8acc 100644 --- a/packages/shared/forks/ReactFeatureFlags.www.js +++ b/packages/shared/forks/ReactFeatureFlags.www.js @@ -76,9 +76,6 @@ export const warnUnstableRenderSubtreeIntoContainer = false; // to the correct value. export const enableNewReconciler = __VARIANT__; -export const deferPassiveEffectCleanupDuringUnmount = true; -export const runAllPassiveEffectDestroysBeforeCreates = true; - // Flow magic to verify the exports of this file match the original version. // eslint-disable-next-line no-unused-vars type Check<_X, Y: _X, X: Y = _X> = null;