Skip to content

Commit 6177b18

Browse files
authored
Track suspended time when the render doesn't commit because it suspended (#31552)
When we suspend the render with delay, we won't do any more work until we get some kind of another update/ping. It's because conceptually something is suspended and then will update later. We need to highlight this period to show why it's not doing any work. We fill the empty space with "Suspended". This stops whenever the same lane group starts rendering again. Clamped by the preceeding start time/event time/update time. <img width="902" alt="Screenshot 2024-11-15 at 1 01 29 PM" src="https://github.com/user-attachments/assets/acf9dc9a-8fc3-4367-a8b0-d19f9c9eac73"> Ideally we would instead start the next render and suspend the work loop at all places we suspend. In that mode this will instead show up as a very long "Render" with a "Suspended" period instead highlighted in the Components track as one component is suspended. We'll soon have that for `use()` but not all updates so this covers the rest. One issue with `useActionState` is that it is implemented as suspending at the point of the `useActionState` which means that the period of the Action shows up as a suspended render instead of as an Action which happens for raw actions. This is not really how you conceptually think about it so we need some special case for `useActionState`. In the screenshot above, the first "Suspended" is actually awaiting an Action and the second "Suspended" is awaiting the data from it.
1 parent 6f0dc29 commit 6177b18

File tree

3 files changed

+90
-18
lines changed

3 files changed

+90
-18
lines changed

packages/react-reconciler/src/ReactFiberPerformanceTrack.js

+13
Original file line numberDiff line numberDiff line change
@@ -222,6 +222,19 @@ export function logSuspendedRenderPhase(
222222
}
223223
}
224224

225+
export function logSuspendedWithDelayPhase(
226+
startTime: number,
227+
endTime: number,
228+
): void {
229+
// This means the render was suspended and cannot commit until it gets unblocked.
230+
if (supportsUserTiming) {
231+
reusableLaneDevToolDetails.color = 'primary-dark';
232+
reusableLaneOptions.start = startTime;
233+
reusableLaneOptions.end = endTime;
234+
performance.measure('Suspended', reusableLaneOptions);
235+
}
236+
}
237+
225238
export function logErroredRenderPhase(
226239
startTime: number,
227240
endTime: number,

packages/react-reconciler/src/ReactFiberWorkLoop.js

+62-18
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,7 @@ import {
7373
logSuspendedRenderPhase,
7474
logErroredRenderPhase,
7575
logInconsistentRender,
76+
logSuspendedWithDelayPhase,
7677
logSuspenseThrottlePhase,
7778
logSuspendedCommitPhase,
7879
logCommitPhase,
@@ -239,12 +240,14 @@ import {
239240
blockingEventTime,
240241
blockingEventType,
241242
blockingEventIsRepeat,
243+
blockingSuspendedTime,
242244
transitionClampTime,
243245
transitionStartTime,
244246
transitionUpdateTime,
245247
transitionEventTime,
246248
transitionEventType,
247249
transitionEventIsRepeat,
250+
transitionSuspendedTime,
248251
clearBlockingTimers,
249252
clearTransitionTimers,
250253
clampBlockingTimers,
@@ -260,6 +263,7 @@ import {
260263
stopProfilerTimerIfRunningAndRecordDuration,
261264
stopProfilerTimerIfRunningAndRecordIncompleteDuration,
262265
markUpdateAsRepeat,
266+
trackSuspendedTime,
263267
} from './ReactProfilerTimer';
264268
import {setCurrentTrackFromLanes} from './ReactFiberPerformanceTrack';
265269

@@ -954,6 +958,11 @@ export function performWorkOnRoot(
954958
}
955959
break;
956960
} else {
961+
let renderEndTime = 0;
962+
if (enableProfilerTimer && enableComponentPerformanceTrack) {
963+
renderEndTime = now();
964+
}
965+
957966
// The render completed.
958967

959968
// Check if this render may have yielded to a concurrent event, and if so,
@@ -968,7 +977,6 @@ export function performWorkOnRoot(
968977
) {
969978
if (enableProfilerTimer && enableComponentPerformanceTrack) {
970979
setCurrentTrackFromLanes(lanes);
971-
const renderEndTime = now();
972980
logInconsistentRender(renderStartTime, renderEndTime);
973981
finalizeRender(lanes, renderEndTime);
974982
markUpdateAsRepeat(lanes);
@@ -996,7 +1004,6 @@ export function performWorkOnRoot(
9961004
if (errorRetryLanes !== NoLanes) {
9971005
if (enableProfilerTimer && enableComponentPerformanceTrack) {
9981006
setCurrentTrackFromLanes(lanes);
999-
const renderEndTime = now();
10001007
logErroredRenderPhase(renderStartTime, renderEndTime);
10011008
finalizeRender(lanes, renderEndTime);
10021009
markUpdateAsRepeat(lanes);
@@ -1020,13 +1027,15 @@ export function performWorkOnRoot(
10201027
continue;
10211028
} else {
10221029
// The root errored yet again. Proceed to commit the tree.
1030+
if (enableProfilerTimer && enableComponentPerformanceTrack) {
1031+
renderEndTime = now();
1032+
}
10231033
}
10241034
}
10251035
}
10261036
if (exitStatus === RootFatalErrored) {
10271037
if (enableProfilerTimer && enableComponentPerformanceTrack) {
10281038
setCurrentTrackFromLanes(lanes);
1029-
const renderEndTime = now();
10301039
logErroredRenderPhase(renderStartTime, renderEndTime);
10311040
finalizeRender(lanes, renderEndTime);
10321041
}
@@ -1040,7 +1049,13 @@ export function performWorkOnRoot(
10401049

10411050
// We now have a consistent tree. The next step is either to commit it,
10421051
// or, if something suspended, wait to commit it after a timeout.
1043-
finishConcurrentRender(root, exitStatus, finishedWork, lanes);
1052+
finishConcurrentRender(
1053+
root,
1054+
exitStatus,
1055+
finishedWork,
1056+
lanes,
1057+
renderEndTime,
1058+
);
10441059
}
10451060
break;
10461061
} while (true);
@@ -1139,14 +1154,8 @@ function finishConcurrentRender(
11391154
exitStatus: RootExitStatus,
11401155
finishedWork: Fiber,
11411156
lanes: Lanes,
1157+
renderEndTime: number, // Profiling-only
11421158
) {
1143-
let renderEndTime = 0;
1144-
if (enableProfilerTimer && enableComponentPerformanceTrack) {
1145-
// Track when we finished the last unit of work, before we actually commit it.
1146-
// The commit can be suspended/blocked until we commit it.
1147-
renderEndTime = now();
1148-
}
1149-
11501159
// TODO: The fact that most of these branches are identical suggests that some
11511160
// of the exit statuses are not best modeled as exit statuses and should be
11521161
// tracked orthogonally.
@@ -1170,6 +1179,7 @@ function finishConcurrentRender(
11701179
setCurrentTrackFromLanes(lanes);
11711180
logSuspendedRenderPhase(renderStartTime, renderEndTime);
11721181
finalizeRender(lanes, renderEndTime);
1182+
trackSuspendedTime(lanes, renderEndTime);
11731183
}
11741184
const didAttemptEntireTree = !workInProgressRootDidSkipSuspendedSiblings;
11751185
markRootSuspended(
@@ -1705,30 +1715,64 @@ function prepareFreshStack(root: FiberRoot, lanes: Lanes): Fiber {
17051715
}
17061716

17071717
if (includesSyncLane(lanes) || includesBlockingLane(lanes)) {
1708-
logBlockingStart(
1718+
const clampedUpdateTime =
17091719
blockingUpdateTime >= 0 && blockingUpdateTime < blockingClampTime
17101720
? blockingClampTime
1711-
: blockingUpdateTime,
1721+
: blockingUpdateTime;
1722+
const clampedEventTime =
17121723
blockingEventTime >= 0 && blockingEventTime < blockingClampTime
17131724
? blockingClampTime
1714-
: blockingEventTime,
1725+
: blockingEventTime;
1726+
if (blockingSuspendedTime >= 0) {
1727+
setCurrentTrackFromLanes(lanes);
1728+
logSuspendedWithDelayPhase(
1729+
blockingSuspendedTime,
1730+
// Clamp the suspended time to the first event/update.
1731+
clampedEventTime >= 0
1732+
? clampedEventTime
1733+
: clampedUpdateTime >= 0
1734+
? clampedUpdateTime
1735+
: renderStartTime,
1736+
);
1737+
}
1738+
logBlockingStart(
1739+
clampedUpdateTime,
1740+
clampedEventTime,
17151741
blockingEventType,
17161742
blockingEventIsRepeat,
17171743
renderStartTime,
17181744
);
17191745
clearBlockingTimers();
17201746
}
17211747
if (includesTransitionLane(lanes)) {
1722-
logTransitionStart(
1748+
const clampedStartTime =
17231749
transitionStartTime >= 0 && transitionStartTime < transitionClampTime
17241750
? transitionClampTime
1725-
: transitionStartTime,
1751+
: transitionStartTime;
1752+
const clampedUpdateTime =
17261753
transitionUpdateTime >= 0 && transitionUpdateTime < transitionClampTime
17271754
? transitionClampTime
1728-
: transitionUpdateTime,
1755+
: transitionUpdateTime;
1756+
const clampedEventTime =
17291757
transitionEventTime >= 0 && transitionEventTime < transitionClampTime
17301758
? transitionClampTime
1731-
: transitionEventTime,
1759+
: transitionEventTime;
1760+
if (transitionSuspendedTime >= 0) {
1761+
setCurrentTrackFromLanes(lanes);
1762+
logSuspendedWithDelayPhase(
1763+
transitionSuspendedTime,
1764+
// Clamp the suspended time to the first event/update.
1765+
clampedEventTime >= 0
1766+
? clampedEventTime
1767+
: clampedUpdateTime >= 0
1768+
? clampedUpdateTime
1769+
: renderStartTime,
1770+
);
1771+
}
1772+
logTransitionStart(
1773+
clampedStartTime,
1774+
clampedUpdateTime,
1775+
clampedEventTime,
17321776
transitionEventType,
17331777
transitionEventIsRepeat,
17341778
renderStartTime,

packages/react-reconciler/src/ReactProfilerTimer.js

+15
Original file line numberDiff line numberDiff line change
@@ -48,13 +48,15 @@ export let blockingUpdateTime: number = -1.1; // First sync setState scheduled.
4848
export let blockingEventTime: number = -1.1; // Event timeStamp of the first setState.
4949
export let blockingEventType: null | string = null; // Event type of the first setState.
5050
export let blockingEventIsRepeat: boolean = false;
51+
export let blockingSuspendedTime: number = -1.1;
5152
// TODO: This should really be one per Transition lane.
5253
export let transitionClampTime: number = -0;
5354
export let transitionStartTime: number = -1.1; // First startTransition call before setState.
5455
export let transitionUpdateTime: number = -1.1; // First transition setState scheduled.
5556
export let transitionEventTime: number = -1.1; // Event timeStamp of the first transition.
5657
export let transitionEventType: null | string = null; // Event type of the first transition.
5758
export let transitionEventIsRepeat: boolean = false;
59+
export let transitionSuspendedTime: number = -1.1;
5860

5961
export function startUpdateTimerByLane(lane: Lane): void {
6062
if (!enableProfilerTimer || !enableComponentPerformanceTrack) {
@@ -100,8 +102,20 @@ export function markUpdateAsRepeat(lanes: Lanes): void {
100102
}
101103
}
102104

105+
export function trackSuspendedTime(lanes: Lanes, renderEndTime: number) {
106+
if (!enableProfilerTimer || !enableComponentPerformanceTrack) {
107+
return;
108+
}
109+
if (includesSyncLane(lanes) || includesBlockingLane(lanes)) {
110+
blockingSuspendedTime = renderEndTime;
111+
} else if (includesTransitionLane(lanes)) {
112+
transitionSuspendedTime = renderEndTime;
113+
}
114+
}
115+
103116
export function clearBlockingTimers(): void {
104117
blockingUpdateTime = -1.1;
118+
blockingSuspendedTime = -1.1;
105119
}
106120

107121
export function startAsyncTransitionTimer(): void {
@@ -145,6 +159,7 @@ export function clearAsyncTransitionTimer(): void {
145159
export function clearTransitionTimers(): void {
146160
transitionStartTime = -1.1;
147161
transitionUpdateTime = -1.1;
162+
transitionSuspendedTime = -1.1;
148163
}
149164

150165
export function clampBlockingTimers(finalTime: number): void {

0 commit comments

Comments
 (0)