Profiler actualDuration bugfix (#13313)

* Simplified profiler actualDuration timing

While testing the new DevTools profiler, I noticed that sometimes– in larger, more complicated applications– the actualDuration value was incorrect (either too large, or sometimes negative). I was not able to reproduce this in a smaller application or test (which sucks) but I assume it has something to do with the way I was tracking render times across priorities/roots. So this PR replaces the previous approach with a simpler one.

* Changed bubbling logic after chatting out of band with Andrew

* Replaced __PROFILE__ with feature-flag conditionals in test

* Updated test comment
This commit is contained in:
Brian Vaughn
2018-08-08 09:42:53 -07:00
committed by GitHub
parent 1209ae50f3
commit 067cc24f55
7 changed files with 129 additions and 285 deletions
+3 -2
View File
@@ -162,6 +162,7 @@ export type Fiber = {|
// Time spent rendering this Fiber and its descendants for the current update.
// This tells us how well the tree makes use of sCU for memoization.
// It is reset to 0 each time we render and only updated when we don't bailout.
// This field is only set when the enableProfilerTimer flag is enabled.
actualDuration?: number,
@@ -238,7 +239,7 @@ function FiberNode(
if (enableProfilerTimer) {
this.actualDuration = 0;
this.actualStartTime = 0;
this.actualStartTime = -1;
this.selfBaseDuration = 0;
this.treeBaseDuration = 0;
}
@@ -330,7 +331,7 @@ export function createWorkInProgress(
// This has the downside of resetting values for different priority renders,
// But works for yielding (the common case) and should support resuming.
workInProgress.actualDuration = 0;
workInProgress.actualStartTime = 0;
workInProgress.actualStartTime = -1;
}
}
+4 -13
View File
@@ -62,7 +62,7 @@ import {
} from './ReactChildFiber';
import {processUpdateQueue} from './ReactUpdateQueue';
import {NoWork, Never} from './ReactFiberExpirationTime';
import {AsyncMode, ProfileMode, StrictMode} from './ReactTypeOfMode';
import {AsyncMode, StrictMode} from './ReactTypeOfMode';
import {
shouldSetTextContent,
shouldDeprioritizeSubtree,
@@ -75,10 +75,7 @@ import {
prepareToReadContext,
calculateChangedBits,
} from './ReactFiberNewContext';
import {
markActualRenderTimeStarted,
stopBaseRenderTimerIfRunning,
} from './ReactProfilerTimer';
import {stopProfilerTimerIfRunning} from './ReactProfilerTimer';
import {
getMaskedContext,
getUnmaskedContext,
@@ -379,7 +376,7 @@ function finishClassComponent(
nextChildren = null;
if (enableProfilerTimer) {
stopBaseRenderTimerIfRunning();
stopProfilerTimerIfRunning(workInProgress);
}
} else {
if (__DEV__) {
@@ -954,7 +951,7 @@ function bailoutOnAlreadyFinishedWork(
if (enableProfilerTimer) {
// Don't update "base" render times for bailouts.
stopBaseRenderTimerIfRunning();
stopProfilerTimerIfRunning(workInProgress);
}
// Check if the children have any pending work.
@@ -991,12 +988,6 @@ function beginWork(
workInProgress: Fiber,
renderExpirationTime: ExpirationTime,
): Fiber | null {
if (enableProfilerTimer) {
if (workInProgress.mode & ProfileMode) {
markActualRenderTimeStarted(workInProgress);
}
}
const updateExpirationTime = workInProgress.expirationTime;
if (
!hasLegacyContextChanged() &&
-13
View File
@@ -20,7 +20,6 @@ import type {
HostContext,
} from './ReactFiberHostConfig';
import {enableProfilerTimer} from 'shared/ReactFeatureFlags';
import {
IndeterminateComponent,
FunctionalComponent,
@@ -38,7 +37,6 @@ import {
PlaceholderComponent,
} from 'shared/ReactTypeOfWork';
import {Placement, Ref, Update} from 'shared/ReactTypeOfSideEffect';
import {ProfileMode} from './ReactTypeOfMode';
import invariant from 'shared/invariant';
import {
@@ -60,7 +58,6 @@ import {
getHostContext,
popHostContainer,
} from './ReactFiberHostContext';
import {recordElapsedActualRenderTime} from './ReactProfilerTimer';
import {
popContextProvider as popLegacyContextProvider,
popTopLevelContextObject as popTopLevelLegacyContextObject,
@@ -518,16 +515,6 @@ function completeWork(
);
}
if (enableProfilerTimer) {
if (workInProgress.mode & ProfileMode) {
// Don't record elapsed time unless the "complete" phase has succeeded.
// Certain renderers may error during this phase (i.e. ReactNative View/Text nesting validation).
// If an error occurs, we'll mark the time while unwinding.
// This simplifies the unwinding logic and ensures consistency.
recordElapsedActualRenderTime(workInProgress);
}
}
return null;
}
+64 -71
View File
@@ -112,15 +112,9 @@ import {
import {popProvider, resetContextDependences} from './ReactFiberNewContext';
import {popHostContext, popHostContainer} from './ReactFiberHostContext';
import {
checkActualRenderTimeStackEmpty,
pauseActualRenderTimerIfRunning,
recordCommitTime,
recordElapsedActualRenderTime,
recordElapsedBaseRenderTimeIfRunning,
resetActualRenderTimerStackAfterFatalErrorInDev,
resumeActualRenderTimerIfPaused,
startBaseRenderTimer,
stopBaseRenderTimerIfRunning,
startProfilerTimer,
stopProfilerTimerIfRunningAndRecordDelta,
} from './ReactProfilerTimer';
import {
checkThatStackIsEmpty,
@@ -312,15 +306,6 @@ if (__DEV__ && replayFailedUnitOfWorkWithInvokeGuardedCallback) {
originalReplayError = null;
if (hasCaughtError()) {
clearCaughtError();
if (enableProfilerTimer) {
if (failedUnitOfWork.mode & ProfileMode) {
recordElapsedActualRenderTime(failedUnitOfWork);
}
// Stop "base" render timer again (after the re-thrown error).
stopBaseRenderTimerIfRunning();
}
} else {
// If the begin phase did not fail the second time, set this pointer
// back to the original value.
@@ -336,12 +321,6 @@ function resetStack() {
if (nextUnitOfWork !== null) {
let interruptedWork = nextUnitOfWork.return;
while (interruptedWork !== null) {
if (enableProfilerTimer) {
if (interruptedWork.mode & ProfileMode) {
// Resume in case we're picking up on work that was paused.
resumeActualRenderTimerIfPaused(false);
}
}
unwindInterruptedWork(interruptedWork);
interruptedWork = interruptedWork.return;
}
@@ -691,17 +670,6 @@ function commitRoot(root: FiberRoot, finishedWork: Fiber): void {
}
}
if (enableProfilerTimer) {
if (__DEV__) {
if (nextRoot === null) {
// Only check this stack once we're done processing async work.
// This prevents a false positive that occurs after a batched commit,
// If there was in-progress async work before the commit.
checkActualRenderTimeStackEmpty();
}
}
}
isCommitting = false;
isWorking = false;
stopCommitLifeCyclesTimer();
@@ -741,9 +709,22 @@ function resetChildExpirationTime(
// Bubble up the earliest expiration time.
if (enableProfilerTimer && workInProgress.mode & ProfileMode) {
// We're in profiling mode. Let's use this same traversal to update the
// "base" render times.
// We're in profiling mode.
// Let's use this same traversal to update the render durations.
let actualDuration = workInProgress.actualDuration;
let treeBaseDuration = workInProgress.selfBaseDuration;
// When a fiber is cloned, its actualDuration is reset to 0.
// This value will only be updated if work is done on the fiber (i.e. it doesn't bailout).
// When work is done, it should bubble to the parent's actualDuration.
// If the fiber has not been cloned though, (meaning no work was done),
// Then this value will reflect the amount of time spent working on a previous render.
// In that case it should not bubble.
// We determine whether it was cloned by comparing the child pointer.
const shouldBubbleActualDurations =
workInProgress.alternate === null ||
workInProgress.child !== workInProgress.alternate.child;
let child = workInProgress.child;
while (child !== null) {
const childUpdateExpirationTime = child.expirationTime;
@@ -762,9 +743,13 @@ function resetChildExpirationTime(
) {
newChildExpirationTime = childChildExpirationTime;
}
if (shouldBubbleActualDurations) {
actualDuration += child.actualDuration;
}
treeBaseDuration += child.treeBaseDuration;
child = child.sibling;
}
workInProgress.actualDuration = actualDuration;
workInProgress.treeBaseDuration = treeBaseDuration;
} else {
let child = workInProgress.child;
@@ -811,11 +796,28 @@ function completeUnitOfWork(workInProgress: Fiber): Fiber | null {
if ((workInProgress.effectTag & Incomplete) === NoEffect) {
// This fiber completed.
nextUnitOfWork = completeWork(
current,
workInProgress,
nextRenderExpirationTime,
);
if (enableProfilerTimer) {
if (workInProgress.mode & ProfileMode) {
startProfilerTimer(workInProgress);
}
nextUnitOfWork = completeWork(
current,
workInProgress,
nextRenderExpirationTime,
);
if (workInProgress.mode & ProfileMode) {
// Update render duration assuming we didn't error.
stopProfilerTimerIfRunningAndRecordDelta(workInProgress, false);
}
} else {
nextUnitOfWork = completeWork(
current,
workInProgress,
nextRenderExpirationTime,
);
}
let next = nextUnitOfWork;
stopWorkTimer(workInProgress);
resetChildExpirationTime(workInProgress, nextRenderExpirationTime);
@@ -886,6 +888,11 @@ function completeUnitOfWork(workInProgress: Fiber): Fiber | null {
return null;
}
} else {
if (workInProgress.mode & ProfileMode) {
// Record the render duration for the fiber that errored.
stopProfilerTimerIfRunningAndRecordDelta(workInProgress, false);
}
// This fiber did not complete because something threw. Pop values off
// the stack without entering the complete phase. If this is a boundary,
// capture values if possible.
@@ -908,6 +915,19 @@ function completeUnitOfWork(workInProgress: Fiber): Fiber | null {
ReactFiberInstrumentation.debugTool.onCompleteWork(workInProgress);
}
if (enableProfilerTimer) {
// Include the time spent working on failed children before continuing.
if (next.mode & ProfileMode) {
let actualDuration = next.actualDuration;
let child = next.child;
while (child !== null) {
actualDuration += child.actualDuration;
child = child.sibling;
}
next.actualDuration = actualDuration;
}
}
// If completing this work spawned new work, do that next. We'll come
// back here again.
// Since we're restarting, remove anything that is not a host effect
@@ -968,15 +988,14 @@ function performUnitOfWork(workInProgress: Fiber): Fiber | null {
let next;
if (enableProfilerTimer) {
if (workInProgress.mode & ProfileMode) {
startBaseRenderTimer();
startProfilerTimer(workInProgress);
}
next = beginWork(current, workInProgress, nextRenderExpirationTime);
if (workInProgress.mode & ProfileMode) {
// Update "base" time if the render wasn't bailed out on.
recordElapsedBaseRenderTimeIfRunning(workInProgress);
stopBaseRenderTimerIfRunning();
// Record the render duration assuming we didn't bailout (or error).
stopProfilerTimerIfRunningAndRecordDelta(workInProgress, true);
}
} else {
next = beginWork(current, workInProgress, nextRenderExpirationTime);
@@ -1017,12 +1036,6 @@ function workLoop(isYieldy) {
while (nextUnitOfWork !== null && !shouldYield()) {
nextUnitOfWork = performUnitOfWork(nextUnitOfWork);
}
if (enableProfilerTimer) {
// If we didn't finish, pause the "actual" render timer.
// We'll restart it when we resume work.
pauseActualRenderTimerIfRunning();
}
}
}
@@ -1068,11 +1081,6 @@ function renderRoot(
try {
workLoop(isYieldy);
} catch (thrownValue) {
if (enableProfilerTimer) {
// Stop "base" render timer in the event of an error.
stopBaseRenderTimerIfRunning();
}
if (nextUnitOfWork === null) {
// This is a fatal error.
didFatal = true;
@@ -1139,10 +1147,6 @@ function renderRoot(
// There was a fatal error.
if (__DEV__) {
resetStackAfterFatalErrorInDev();
// Reset the DEV fiber stack in case we're profiling roots.
// (We do this for profiling bulds when DevTools is detected.)
resetActualRenderTimerStackAfterFatalErrorInDev();
}
// `nextRoot` points to the in-progress root. A non-null value indicates
// that we're in the middle of an async render. Set it to null to indicate
@@ -1871,10 +1875,6 @@ function performWork(minExpirationTime: ExpirationTime, dl: Deadline | null) {
// the deadline.
findHighestPriorityRoot();
if (enableProfilerTimer) {
resumeActualRenderTimerIfPaused(minExpirationTime === Sync);
}
if (deadline !== null) {
recomputeCurrentRendererTime();
currentSchedulerTime = currentRendererTime;
@@ -1947,7 +1947,6 @@ function flushRoot(root: FiberRoot, expirationTime: ExpirationTime) {
performWorkOnRoot(root, expirationTime, true);
// Flush any sync work that was scheduled by lifecycles
performSyncWork();
pauseActualRenderTimerIfRunning();
}
function finishRendering() {
@@ -2049,12 +2048,6 @@ function performWorkOnRoot(
// There's no time left. Mark this root as complete. We'll come
// back and commit it later.
root.finishedWork = finishedWork;
if (enableProfilerTimer) {
// If we didn't finish, pause the "actual" render timer.
// We'll restart it when we resume work.
pauseActualRenderTimerIfRunning();
}
}
}
}
+1 -15
View File
@@ -34,10 +34,9 @@ import {
} from 'shared/ReactTypeOfSideEffect';
import {
enableGetDerivedStateFromCatch,
enableProfilerTimer,
enableSuspense,
} from 'shared/ReactFeatureFlags';
import {ProfileMode, StrictMode, AsyncMode} from './ReactTypeOfMode';
import {StrictMode, AsyncMode} from './ReactTypeOfMode';
import {createCapturedValue} from './ReactCapturedValue';
import {
@@ -52,7 +51,6 @@ import {
popTopLevelContextObject as popTopLevelLegacyContextObject,
} from './ReactFiberContext';
import {popProvider} from './ReactFiberNewContext';
import {recordElapsedActualRenderTime} from './ReactProfilerTimer';
import {
renderDidSuspend,
renderDidError,
@@ -380,12 +378,6 @@ function unwindWork(
workInProgress: Fiber,
renderExpirationTime: ExpirationTime,
) {
if (enableProfilerTimer) {
if (workInProgress.mode & ProfileMode) {
recordElapsedActualRenderTime(workInProgress);
}
}
switch (workInProgress.tag) {
case ClassComponent: {
popLegacyContextProvider(workInProgress);
@@ -432,12 +424,6 @@ function unwindWork(
}
function unwindInterruptedWork(interruptedWork: Fiber) {
if (enableProfilerTimer) {
if (interruptedWork.mode & ProfileMode) {
recordElapsedActualRenderTime(interruptedWork);
}
}
switch (interruptedWork.tag) {
case ClassComponent: {
popLegacyContextProvider(interruptedWork);
+25 -140
View File
@@ -9,27 +9,20 @@
import type {Fiber} from './ReactFiber';
import getComponentName from 'shared/getComponentName';
import {enableProfilerTimer} from 'shared/ReactFeatureFlags';
import warningWithoutStack from 'shared/warningWithoutStack';
import {now} from './ReactFiberHostConfig';
export type ProfilerTimer = {
checkActualRenderTimeStackEmpty(): void,
getCommitTime(): number,
markActualRenderTimeStarted(fiber: Fiber): void,
pauseActualRenderTimerIfRunning(): void,
recordElapsedActualRenderTime(fiber: Fiber): void,
resumeActualRenderTimerIfPaused(isProcessingBatchCommit: boolean): void,
recordCommitTime(): void,
recordElapsedBaseRenderTimeIfRunning(fiber: Fiber): void,
resetActualRenderTimerStackAfterFatalErrorInDev(): void,
startBaseRenderTimer(): void,
stopBaseRenderTimerIfRunning(): void,
startProfilerTimer(fiber: Fiber): void,
stopProfilerTimerIfRunning(fiber: Fiber): void,
stopProfilerTimerIfRunningAndRecordDelta(fiber: Fiber): void,
};
let commitTime: number = 0;
let profilerStartTime: number = -1;
function getCommitTime(): number {
return commitTime;
@@ -42,155 +35,47 @@ function recordCommitTime(): void {
commitTime = now();
}
/**
* The "actual" render time is total time required to render the descendants of a Profiler component.
* This time is stored as a stack, since Profilers can be nested.
* This time is started during the "begin" phase and stopped during the "complete" phase.
* It is paused (and accumulated) in the event of an interruption or an aborted render.
*/
let fiberStack: Array<Fiber | null>;
if (__DEV__) {
fiberStack = [];
}
let syncCommitStartTime: number = 0;
let timerPausedAt: number = 0;
let totalElapsedPauseTime: number = 0;
function checkActualRenderTimeStackEmpty(): void {
function startProfilerTimer(fiber: Fiber): void {
if (!enableProfilerTimer) {
return;
}
if (__DEV__) {
warningWithoutStack(
fiberStack.length === 0,
'Expected an empty stack. Something was not reset properly.',
);
profilerStartTime = now();
if (((fiber.actualStartTime: any): number) < 0) {
fiber.actualStartTime = now();
}
}
function markActualRenderTimeStarted(fiber: Fiber): void {
function stopProfilerTimerIfRunning(fiber: Fiber): void {
if (!enableProfilerTimer) {
return;
}
if (__DEV__) {
fiberStack.push(fiber);
}
fiber.actualDuration =
now() - ((fiber.actualDuration: any): number) - totalElapsedPauseTime;
fiber.actualStartTime = now();
profilerStartTime = -1;
}
function pauseActualRenderTimerIfRunning(): void {
function stopProfilerTimerIfRunningAndRecordDelta(
fiber: Fiber,
overrideBaseTime: boolean,
): void {
if (!enableProfilerTimer) {
return;
}
if (timerPausedAt === 0) {
timerPausedAt = now();
}
if (syncCommitStartTime > 0) {
// Don't count the time spent processing sycn work,
// Against yielded async work that will be resumed later.
totalElapsedPauseTime += now() - syncCommitStartTime;
syncCommitStartTime = 0;
} else {
totalElapsedPauseTime = 0;
}
}
function recordElapsedActualRenderTime(fiber: Fiber): void {
if (!enableProfilerTimer) {
return;
}
if (__DEV__) {
warningWithoutStack(
fiber === fiberStack.pop(),
'Unexpected Fiber (%s) popped.',
getComponentName(fiber.type),
);
}
fiber.actualDuration =
now() - totalElapsedPauseTime - ((fiber.actualDuration: any): number);
}
function resumeActualRenderTimerIfPaused(isProcessingSyncWork: boolean): void {
if (!enableProfilerTimer) {
return;
}
if (isProcessingSyncWork && syncCommitStartTime === 0) {
syncCommitStartTime = now();
}
if (timerPausedAt > 0) {
totalElapsedPauseTime += now() - timerPausedAt;
timerPausedAt = 0;
}
}
function resetActualRenderTimerStackAfterFatalErrorInDev(): void {
if (!enableProfilerTimer) {
return;
}
if (__DEV__) {
fiberStack.length = 0;
}
}
/**
* The "base" render time is the duration of the “begin” phase of work for a particular fiber.
* This time is measured and stored on each fiber.
* The time for all sibling fibers are accumulated and stored on their parent during the "complete" phase.
* If a fiber bails out (sCU false) then its "base" timer is cancelled and the fiber is not updated.
*/
let baseStartTime: number = -1;
function recordElapsedBaseRenderTimeIfRunning(fiber: Fiber): void {
if (!enableProfilerTimer) {
return;
}
if (baseStartTime !== -1) {
fiber.selfBaseDuration = now() - baseStartTime;
}
}
function startBaseRenderTimer(): void {
if (!enableProfilerTimer) {
return;
}
if (__DEV__) {
if (baseStartTime !== -1) {
warningWithoutStack(
false,
'Cannot start base timer that is already running. ' +
'This error is likely caused by a bug in React. ' +
'Please file an issue.',
);
if (profilerStartTime >= 0) {
const elapsedTime = now() - profilerStartTime;
fiber.actualDuration += elapsedTime;
if (overrideBaseTime) {
fiber.selfBaseDuration = elapsedTime;
}
profilerStartTime = -1;
}
baseStartTime = now();
}
function stopBaseRenderTimerIfRunning(): void {
if (!enableProfilerTimer) {
return;
}
baseStartTime = -1;
}
export {
checkActualRenderTimeStackEmpty,
getCommitTime,
markActualRenderTimeStarted,
pauseActualRenderTimerIfRunning,
recordCommitTime,
recordElapsedActualRenderTime,
resetActualRenderTimerStackAfterFatalErrorInDev,
resumeActualRenderTimerIfPaused,
recordElapsedBaseRenderTimeIfRunning,
startBaseRenderTimer,
stopBaseRenderTimerIfRunning,
startProfilerTimer,
stopProfilerTimerIfRunning,
stopProfilerTimerIfRunningAndRecordDelta,
};
@@ -57,7 +57,7 @@ describe('Profiler', () => {
// This will throw in production too,
// But the test is only interested in verifying the DEV error message.
if (__DEV__) {
if (__DEV__ && flagEnabled) {
it('should warn if required params are missing', () => {
expect(() => {
ReactTestRenderer.create(<React.unstable_Profiler />);
@@ -196,11 +196,10 @@ describe('Profiler', () => {
);
// Should be called two times:
// 1. To mark the start (resuming) of work
// 2. To compute the update expiration time,
// 3. To record the commit time.
// 2. To compute the update expiration time
// 3. To record the commit time
// No additional calls from ProfilerTimer are expected.
expect(mockNow).toHaveBeenCalledTimes(3);
expect(mockNow).toHaveBeenCalledTimes(2);
});
it('logs render times for both mount and update', () => {
@@ -422,19 +421,19 @@ describe('Profiler', () => {
const renderer = ReactTestRenderer.create(
<React.unstable_Profiler id="test" onRender={callback}>
<AdvanceTime byAmount={10}>
<AdvanceTime byAmount={10} shouldComponentUpdate={false} />
<AdvanceTime byAmount={13} shouldComponentUpdate={false} />
</AdvanceTime>
</React.unstable_Profiler>,
);
expect(callback).toHaveBeenCalledTimes(1);
advanceTimeBy(30); // 25 -> 55
advanceTimeBy(30); // 28 -> 58
renderer.update(
<React.unstable_Profiler id="test" onRender={callback}>
<AdvanceTime byAmount={10}>
<AdvanceTime byAmount={10} shouldComponentUpdate={false} />
<AdvanceTime byAmount={4}>
<AdvanceTime byAmount={7} shouldComponentUpdate={false} />
</AdvanceTime>
</React.unstable_Profiler>,
);
@@ -444,16 +443,16 @@ describe('Profiler', () => {
const [mountCall, updateCall] = callback.mock.calls;
expect(mountCall[1]).toBe('mount');
expect(mountCall[2]).toBe(20); // actual time
expect(mountCall[3]).toBe(20); // base time
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(25); // commit time
expect(mountCall[5]).toBe(28); // commit time
expect(updateCall[1]).toBe('update');
expect(updateCall[2]).toBe(10); // actual time
expect(updateCall[3]).toBe(20); // base time
expect(updateCall[4]).toBe(55); // start time
expect(updateCall[5]).toBe(65); // commit time
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', () => {
@@ -809,7 +808,7 @@ describe('Profiler', () => {
]);
// The actual time should include only the most recent render (37ms),
// Because this lets us avoid a lot of commit phase reset complexity.
// 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);
@@ -856,7 +855,7 @@ describe('Profiler', () => {
const callback = jest.fn();
const ThrowsError = () => {
advanceTimeBy(10);
advanceTimeBy(3);
throw Error('expected error');
};
@@ -880,7 +879,7 @@ describe('Profiler', () => {
ReactTestRenderer.create(
<React.unstable_Profiler id="test" onRender={callback}>
<ErrorBoundary>
<AdvanceTime byAmount={5} />
<AdvanceTime byAmount={9} />
<ThrowsError />
</ErrorBoundary>
</React.unstable_Profiler>,
@@ -891,18 +890,19 @@ describe('Profiler', () => {
// Callbacks bubble (reverse order).
let [mountCall, updateCall] = callback.mock.calls;
// The initial mount only includes the ErrorBoundary (which takes 2ms)
// 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) + 5 (AdvanceTime) + 10 (ThrowsError)
// If replayFailedUnitOfWorkWithInvokeGuardedCallback is enbaled, ThrowsError is replayed.
expect(mountCall[2]).toBe(flagEnabled && __DEV__ ? 27 : 17);
// 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)
expect(mountCall[3]).toBe(2);
// start time
expect(mountCall[4]).toBe(5);
// commit time
expect(mountCall[5]).toBe(flagEnabled && __DEV__ ? 32 : 22);
// commit time: 5 initially + 14 of work
// Add an additional 3 (ThrowsError) if we replaced the failed work
expect(mountCall[5]).toBe(__DEV__ && flagEnabled ? 22 : 19);
// The update includes the ErrorBoundary and its fallback child
expect(updateCall[1]).toBe('update');
@@ -911,9 +911,10 @@ describe('Profiler', () => {
// base time includes: 2 (ErrorBoundary) + 20 (AdvanceTime)
expect(updateCall[3]).toBe(22);
// start time
expect(updateCall[4]).toBe(flagEnabled && __DEV__ ? 32 : 22);
// commit time
expect(updateCall[5]).toBe(flagEnabled && __DEV__ ? 54 : 44);
expect(updateCall[4]).toBe(__DEV__ && flagEnabled ? 22 : 19);
// commit time: 19 (startTime) + 2 (ErrorBoundary) + 20 (AdvanceTime)
// Add an additional 3 (ThrowsError) if we replaced the failed work
expect(updateCall[5]).toBe(__DEV__ && flagEnabled ? 44 : 41);
});
it('should accumulate actual time after an error handled by getDerivedStateFromCatch()', () => {
@@ -960,14 +961,14 @@ describe('Profiler', () => {
expect(mountCall[1]).toBe('mount');
// actual time includes: 2 (ErrorBoundary) + 5 (AdvanceTime) + 10 (ThrowsError)
// Then the re-render: 2 (ErrorBoundary) + 20 (AdvanceTime)
// If replayFailedUnitOfWorkWithInvokeGuardedCallback is enbaled, ThrowsError is replayed.
expect(mountCall[2]).toBe(flagEnabled && __DEV__ ? 49 : 39);
// 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(flagEnabled && __DEV__ ? 54 : 44);
expect(mountCall[5]).toBe(__DEV__ && flagEnabled ? 54 : 44);
});
it('should reset the fiber stack correct after a "complete" phase error', () => {