Skip to content

Commit 067cc24

Browse files
authored
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
1 parent 1209ae5 commit 067cc24

File tree

7 files changed

+127
-283
lines changed

7 files changed

+127
-283
lines changed

packages/react-reconciler/src/ReactFiber.js

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -162,6 +162,7 @@ export type Fiber = {|
162162

163163
// Time spent rendering this Fiber and its descendants for the current update.
164164
// This tells us how well the tree makes use of sCU for memoization.
165+
// It is reset to 0 each time we render and only updated when we don't bailout.
165166
// This field is only set when the enableProfilerTimer flag is enabled.
166167
actualDuration?: number,
167168

@@ -238,7 +239,7 @@ function FiberNode(
238239

239240
if (enableProfilerTimer) {
240241
this.actualDuration = 0;
241-
this.actualStartTime = 0;
242+
this.actualStartTime = -1;
242243
this.selfBaseDuration = 0;
243244
this.treeBaseDuration = 0;
244245
}
@@ -330,7 +331,7 @@ export function createWorkInProgress(
330331
// This has the downside of resetting values for different priority renders,
331332
// But works for yielding (the common case) and should support resuming.
332333
workInProgress.actualDuration = 0;
333-
workInProgress.actualStartTime = 0;
334+
workInProgress.actualStartTime = -1;
334335
}
335336
}
336337

packages/react-reconciler/src/ReactFiberBeginWork.js

Lines changed: 4 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -62,7 +62,7 @@ import {
6262
} from './ReactChildFiber';
6363
import {processUpdateQueue} from './ReactUpdateQueue';
6464
import {NoWork, Never} from './ReactFiberExpirationTime';
65-
import {AsyncMode, ProfileMode, StrictMode} from './ReactTypeOfMode';
65+
import {AsyncMode, StrictMode} from './ReactTypeOfMode';
6666
import {
6767
shouldSetTextContent,
6868
shouldDeprioritizeSubtree,
@@ -75,10 +75,7 @@ import {
7575
prepareToReadContext,
7676
calculateChangedBits,
7777
} from './ReactFiberNewContext';
78-
import {
79-
markActualRenderTimeStarted,
80-
stopBaseRenderTimerIfRunning,
81-
} from './ReactProfilerTimer';
78+
import {stopProfilerTimerIfRunning} from './ReactProfilerTimer';
8279
import {
8380
getMaskedContext,
8481
getUnmaskedContext,
@@ -379,7 +376,7 @@ function finishClassComponent(
379376
nextChildren = null;
380377

381378
if (enableProfilerTimer) {
382-
stopBaseRenderTimerIfRunning();
379+
stopProfilerTimerIfRunning(workInProgress);
383380
}
384381
} else {
385382
if (__DEV__) {
@@ -954,7 +951,7 @@ function bailoutOnAlreadyFinishedWork(
954951

955952
if (enableProfilerTimer) {
956953
// Don't update "base" render times for bailouts.
957-
stopBaseRenderTimerIfRunning();
954+
stopProfilerTimerIfRunning(workInProgress);
958955
}
959956

960957
// Check if the children have any pending work.
@@ -991,12 +988,6 @@ function beginWork(
991988
workInProgress: Fiber,
992989
renderExpirationTime: ExpirationTime,
993990
): Fiber | null {
994-
if (enableProfilerTimer) {
995-
if (workInProgress.mode & ProfileMode) {
996-
markActualRenderTimeStarted(workInProgress);
997-
}
998-
}
999-
1000991
const updateExpirationTime = workInProgress.expirationTime;
1001992
if (
1002993
!hasLegacyContextChanged() &&

packages/react-reconciler/src/ReactFiberCompleteWork.js

Lines changed: 0 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,6 @@ import type {
2020
HostContext,
2121
} from './ReactFiberHostConfig';
2222

23-
import {enableProfilerTimer} from 'shared/ReactFeatureFlags';
2423
import {
2524
IndeterminateComponent,
2625
FunctionalComponent,
@@ -38,7 +37,6 @@ import {
3837
PlaceholderComponent,
3938
} from 'shared/ReactTypeOfWork';
4039
import {Placement, Ref, Update} from 'shared/ReactTypeOfSideEffect';
41-
import {ProfileMode} from './ReactTypeOfMode';
4240
import invariant from 'shared/invariant';
4341

4442
import {
@@ -60,7 +58,6 @@ import {
6058
getHostContext,
6159
popHostContainer,
6260
} from './ReactFiberHostContext';
63-
import {recordElapsedActualRenderTime} from './ReactProfilerTimer';
6461
import {
6562
popContextProvider as popLegacyContextProvider,
6663
popTopLevelContextObject as popTopLevelLegacyContextObject,
@@ -518,16 +515,6 @@ function completeWork(
518515
);
519516
}
520517

521-
if (enableProfilerTimer) {
522-
if (workInProgress.mode & ProfileMode) {
523-
// Don't record elapsed time unless the "complete" phase has succeeded.
524-
// Certain renderers may error during this phase (i.e. ReactNative View/Text nesting validation).
525-
// If an error occurs, we'll mark the time while unwinding.
526-
// This simplifies the unwinding logic and ensures consistency.
527-
recordElapsedActualRenderTime(workInProgress);
528-
}
529-
}
530-
531518
return null;
532519
}
533520

packages/react-reconciler/src/ReactFiberScheduler.js

Lines changed: 64 additions & 71 deletions
Original file line numberDiff line numberDiff line change
@@ -112,15 +112,9 @@ import {
112112
import {popProvider, resetContextDependences} from './ReactFiberNewContext';
113113
import {popHostContext, popHostContainer} from './ReactFiberHostContext';
114114
import {
115-
checkActualRenderTimeStackEmpty,
116-
pauseActualRenderTimerIfRunning,
117115
recordCommitTime,
118-
recordElapsedActualRenderTime,
119-
recordElapsedBaseRenderTimeIfRunning,
120-
resetActualRenderTimerStackAfterFatalErrorInDev,
121-
resumeActualRenderTimerIfPaused,
122-
startBaseRenderTimer,
123-
stopBaseRenderTimerIfRunning,
116+
startProfilerTimer,
117+
stopProfilerTimerIfRunningAndRecordDelta,
124118
} from './ReactProfilerTimer';
125119
import {
126120
checkThatStackIsEmpty,
@@ -312,15 +306,6 @@ if (__DEV__ && replayFailedUnitOfWorkWithInvokeGuardedCallback) {
312306
originalReplayError = null;
313307
if (hasCaughtError()) {
314308
clearCaughtError();
315-
316-
if (enableProfilerTimer) {
317-
if (failedUnitOfWork.mode & ProfileMode) {
318-
recordElapsedActualRenderTime(failedUnitOfWork);
319-
}
320-
321-
// Stop "base" render timer again (after the re-thrown error).
322-
stopBaseRenderTimerIfRunning();
323-
}
324309
} else {
325310
// If the begin phase did not fail the second time, set this pointer
326311
// back to the original value.
@@ -336,12 +321,6 @@ function resetStack() {
336321
if (nextUnitOfWork !== null) {
337322
let interruptedWork = nextUnitOfWork.return;
338323
while (interruptedWork !== null) {
339-
if (enableProfilerTimer) {
340-
if (interruptedWork.mode & ProfileMode) {
341-
// Resume in case we're picking up on work that was paused.
342-
resumeActualRenderTimerIfPaused(false);
343-
}
344-
}
345324
unwindInterruptedWork(interruptedWork);
346325
interruptedWork = interruptedWork.return;
347326
}
@@ -691,17 +670,6 @@ function commitRoot(root: FiberRoot, finishedWork: Fiber): void {
691670
}
692671
}
693672

694-
if (enableProfilerTimer) {
695-
if (__DEV__) {
696-
if (nextRoot === null) {
697-
// Only check this stack once we're done processing async work.
698-
// This prevents a false positive that occurs after a batched commit,
699-
// If there was in-progress async work before the commit.
700-
checkActualRenderTimeStackEmpty();
701-
}
702-
}
703-
}
704-
705673
isCommitting = false;
706674
isWorking = false;
707675
stopCommitLifeCyclesTimer();
@@ -741,9 +709,22 @@ function resetChildExpirationTime(
741709

742710
// Bubble up the earliest expiration time.
743711
if (enableProfilerTimer && workInProgress.mode & ProfileMode) {
744-
// We're in profiling mode. Let's use this same traversal to update the
745-
// "base" render times.
712+
// We're in profiling mode.
713+
// Let's use this same traversal to update the render durations.
714+
let actualDuration = workInProgress.actualDuration;
746715
let treeBaseDuration = workInProgress.selfBaseDuration;
716+
717+
// When a fiber is cloned, its actualDuration is reset to 0.
718+
// This value will only be updated if work is done on the fiber (i.e. it doesn't bailout).
719+
// When work is done, it should bubble to the parent's actualDuration.
720+
// If the fiber has not been cloned though, (meaning no work was done),
721+
// Then this value will reflect the amount of time spent working on a previous render.
722+
// In that case it should not bubble.
723+
// We determine whether it was cloned by comparing the child pointer.
724+
const shouldBubbleActualDurations =
725+
workInProgress.alternate === null ||
726+
workInProgress.child !== workInProgress.alternate.child;
727+
747728
let child = workInProgress.child;
748729
while (child !== null) {
749730
const childUpdateExpirationTime = child.expirationTime;
@@ -762,9 +743,13 @@ function resetChildExpirationTime(
762743
) {
763744
newChildExpirationTime = childChildExpirationTime;
764745
}
746+
if (shouldBubbleActualDurations) {
747+
actualDuration += child.actualDuration;
748+
}
765749
treeBaseDuration += child.treeBaseDuration;
766750
child = child.sibling;
767751
}
752+
workInProgress.actualDuration = actualDuration;
768753
workInProgress.treeBaseDuration = treeBaseDuration;
769754
} else {
770755
let child = workInProgress.child;
@@ -811,11 +796,28 @@ function completeUnitOfWork(workInProgress: Fiber): Fiber | null {
811796

812797
if ((workInProgress.effectTag & Incomplete) === NoEffect) {
813798
// This fiber completed.
814-
nextUnitOfWork = completeWork(
815-
current,
816-
workInProgress,
817-
nextRenderExpirationTime,
818-
);
799+
if (enableProfilerTimer) {
800+
if (workInProgress.mode & ProfileMode) {
801+
startProfilerTimer(workInProgress);
802+
}
803+
804+
nextUnitOfWork = completeWork(
805+
current,
806+
workInProgress,
807+
nextRenderExpirationTime,
808+
);
809+
810+
if (workInProgress.mode & ProfileMode) {
811+
// Update render duration assuming we didn't error.
812+
stopProfilerTimerIfRunningAndRecordDelta(workInProgress, false);
813+
}
814+
} else {
815+
nextUnitOfWork = completeWork(
816+
current,
817+
workInProgress,
818+
nextRenderExpirationTime,
819+
);
820+
}
819821
let next = nextUnitOfWork;
820822
stopWorkTimer(workInProgress);
821823
resetChildExpirationTime(workInProgress, nextRenderExpirationTime);
@@ -886,6 +888,11 @@ function completeUnitOfWork(workInProgress: Fiber): Fiber | null {
886888
return null;
887889
}
888890
} else {
891+
if (workInProgress.mode & ProfileMode) {
892+
// Record the render duration for the fiber that errored.
893+
stopProfilerTimerIfRunningAndRecordDelta(workInProgress, false);
894+
}
895+
889896
// This fiber did not complete because something threw. Pop values off
890897
// the stack without entering the complete phase. If this is a boundary,
891898
// capture values if possible.
@@ -908,6 +915,19 @@ function completeUnitOfWork(workInProgress: Fiber): Fiber | null {
908915
ReactFiberInstrumentation.debugTool.onCompleteWork(workInProgress);
909916
}
910917

918+
if (enableProfilerTimer) {
919+
// Include the time spent working on failed children before continuing.
920+
if (next.mode & ProfileMode) {
921+
let actualDuration = next.actualDuration;
922+
let child = next.child;
923+
while (child !== null) {
924+
actualDuration += child.actualDuration;
925+
child = child.sibling;
926+
}
927+
next.actualDuration = actualDuration;
928+
}
929+
}
930+
911931
// If completing this work spawned new work, do that next. We'll come
912932
// back here again.
913933
// Since we're restarting, remove anything that is not a host effect
@@ -968,15 +988,14 @@ function performUnitOfWork(workInProgress: Fiber): Fiber | null {
968988
let next;
969989
if (enableProfilerTimer) {
970990
if (workInProgress.mode & ProfileMode) {
971-
startBaseRenderTimer();
991+
startProfilerTimer(workInProgress);
972992
}
973993

974994
next = beginWork(current, workInProgress, nextRenderExpirationTime);
975995

976996
if (workInProgress.mode & ProfileMode) {
977-
// Update "base" time if the render wasn't bailed out on.
978-
recordElapsedBaseRenderTimeIfRunning(workInProgress);
979-
stopBaseRenderTimerIfRunning();
997+
// Record the render duration assuming we didn't bailout (or error).
998+
stopProfilerTimerIfRunningAndRecordDelta(workInProgress, true);
980999
}
9811000
} else {
9821001
next = beginWork(current, workInProgress, nextRenderExpirationTime);
@@ -1017,12 +1036,6 @@ function workLoop(isYieldy) {
10171036
while (nextUnitOfWork !== null && !shouldYield()) {
10181037
nextUnitOfWork = performUnitOfWork(nextUnitOfWork);
10191038
}
1020-
1021-
if (enableProfilerTimer) {
1022-
// If we didn't finish, pause the "actual" render timer.
1023-
// We'll restart it when we resume work.
1024-
pauseActualRenderTimerIfRunning();
1025-
}
10261039
}
10271040
}
10281041

@@ -1068,11 +1081,6 @@ function renderRoot(
10681081
try {
10691082
workLoop(isYieldy);
10701083
} catch (thrownValue) {
1071-
if (enableProfilerTimer) {
1072-
// Stop "base" render timer in the event of an error.
1073-
stopBaseRenderTimerIfRunning();
1074-
}
1075-
10761084
if (nextUnitOfWork === null) {
10771085
// This is a fatal error.
10781086
didFatal = true;
@@ -1139,10 +1147,6 @@ function renderRoot(
11391147
// There was a fatal error.
11401148
if (__DEV__) {
11411149
resetStackAfterFatalErrorInDev();
1142-
1143-
// Reset the DEV fiber stack in case we're profiling roots.
1144-
// (We do this for profiling bulds when DevTools is detected.)
1145-
resetActualRenderTimerStackAfterFatalErrorInDev();
11461150
}
11471151
// `nextRoot` points to the in-progress root. A non-null value indicates
11481152
// 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) {
18711875
// the deadline.
18721876
findHighestPriorityRoot();
18731877

1874-
if (enableProfilerTimer) {
1875-
resumeActualRenderTimerIfPaused(minExpirationTime === Sync);
1876-
}
1877-
18781878
if (deadline !== null) {
18791879
recomputeCurrentRendererTime();
18801880
currentSchedulerTime = currentRendererTime;
@@ -1947,7 +1947,6 @@ function flushRoot(root: FiberRoot, expirationTime: ExpirationTime) {
19471947
performWorkOnRoot(root, expirationTime, true);
19481948
// Flush any sync work that was scheduled by lifecycles
19491949
performSyncWork();
1950-
pauseActualRenderTimerIfRunning();
19511950
}
19521951

19531952
function finishRendering() {
@@ -2049,12 +2048,6 @@ function performWorkOnRoot(
20492048
// There's no time left. Mark this root as complete. We'll come
20502049
// back and commit it later.
20512050
root.finishedWork = finishedWork;
2052-
2053-
if (enableProfilerTimer) {
2054-
// If we didn't finish, pause the "actual" render timer.
2055-
// We'll restart it when we resume work.
2056-
pauseActualRenderTimerIfRunning();
2057-
}
20582051
}
20592052
}
20602053
}

0 commit comments

Comments
 (0)