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

3 files changed

Lines changed: 90 additions & 18 deletions

File tree

‎packages/react-reconciler/src/ReactFiberPerformanceTrack.js‎

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -222,6 +222,19 @@ export function logSuspendedRenderPhase(
222222
}
223223
}
224224

225+
exportfunctionlogSuspendedWithDelayPhase(
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
exportfunctionlogErroredRenderPhase(
226239
startTime: number,
227240
endTime: number,

‎packages/react-reconciler/src/ReactFiberWorkLoop.js‎

Lines changed: 62 additions & 18 deletions
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+
letrenderEndTime=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-
constrenderEndTime=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-
constrenderEndTime=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-
constrenderEndTime=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-
letrenderEndTime=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
constdidAttemptEntireTree=!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+
constclampedUpdateTime=
17091719
blockingUpdateTime>=0&&blockingUpdateTime<blockingClampTime
17101720
? blockingClampTime
1711-
: blockingUpdateTime,
1721+
: blockingUpdateTime;
1722+
constclampedEventTime=
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+
constclampedStartTime=
17231749
transitionStartTime>=0&&transitionStartTime<transitionClampTime
17241750
? transitionClampTime
1725-
: transitionStartTime,
1751+
: transitionStartTime;
1752+
constclampedUpdateTime=
17261753
transitionUpdateTime>=0&&transitionUpdateTime<transitionClampTime
17271754
? transitionClampTime
1728-
: transitionUpdateTime,
1755+
: transitionUpdateTime;
1756+
constclampedEventTime=
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‎

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,13 +48,15 @@ export let blockingUpdateTime: number = -1.1; // First sync setState scheduled.
4848
exportletblockingEventTime: number=-1.1;// Event timeStamp of the first setState.
4949
export letblockingEventType: null|string=null;// Event type of the first setState.
5050
exportletblockingEventIsRepeat: boolean=false;
51+
exportletblockingSuspendedTime: number=-1.1;
5152
// TODO: This should really be one per Transition lane.
5253
exportlettransitionClampTime: number=-0;
5354
exportlettransitionStartTime: number=-1.1;// First startTransition call before setState.
5455
exportlettransitionUpdateTime: number=-1.1;// First transition setState scheduled.
5556
exportlettransitionEventTime: number=-1.1;// Event timeStamp of the first transition.
5657
export lettransitionEventType: null|string=null;// Event type of the first transition.
5758
exportlettransitionEventIsRepeat: boolean=false;
59+
exportlettransitionSuspendedTime: number=-1.1;
5860

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

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

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

150165
exportfunctionclampBlockingTimers(finalTime: number): void{

0 commit comments

Comments
 (0)
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Add copy buttons to all
 blocks\n(function() {\n function addCopyButtons() {\n document.querySelectorAll('pre code').forEach(function(codeBlock) {\n if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;\n codeBlock.parentElement.setAttribute('data-copy-added', 'true');\n \n var btn = document.createElement('button');\n btn.textContent = 'Copy';\n btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';\n btn.onmouseover = function() { this.style.opacity = '1'; };\n btn.onmouseout = function() { this.style.opacity = '0.7'; };\n btn.onclick = function() {\n navigator.clipboard.writeText(codeBlock.textContent).then(function() {\n btn.textContent = 'Copied!';\n setTimeout(function() { btn.textContent = 'Copy'; }, 1500);\n });\n };\n codeBlock.parentElement.style.position = 'relative';\n codeBlock.parentElement.appendChild(btn);\n });\n }\n \n addCopyButtons();\n \n // Re-run on dynamic content\n var observer = new MutationObserver(addCopyButtons);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Add Copy Buttons to Code Blocks");
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
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

3 files changed

Lines changed: 90 additions & 18 deletions

File tree

‎packages/react-reconciler/src/ReactFiberPerformanceTrack.js‎

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -222,6 +222,19 @@ export function logSuspendedRenderPhase(
222222
}
223223
}
224224

225+
exportfunctionlogSuspendedWithDelayPhase(
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
exportfunctionlogErroredRenderPhase(
226239
startTime: number,
227240
endTime: number,

‎packages/react-reconciler/src/ReactFiberWorkLoop.js‎

Lines changed: 62 additions & 18 deletions
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+
letrenderEndTime=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-
constrenderEndTime=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-
constrenderEndTime=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-
constrenderEndTime=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-
letrenderEndTime=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
constdidAttemptEntireTree=!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+
constclampedUpdateTime=
17091719
blockingUpdateTime>=0&&blockingUpdateTime<blockingClampTime
17101720
? blockingClampTime
1711-
: blockingUpdateTime,
1721+
: blockingUpdateTime;
1722+
constclampedEventTime=
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+
constclampedStartTime=
17231749
transitionStartTime>=0&&transitionStartTime<transitionClampTime
17241750
? transitionClampTime
1725-
: transitionStartTime,
1751+
: transitionStartTime;
1752+
constclampedUpdateTime=
17261753
transitionUpdateTime>=0&&transitionUpdateTime<transitionClampTime
17271754
? transitionClampTime
1728-
: transitionUpdateTime,
1755+
: transitionUpdateTime;
1756+
constclampedEventTime=
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‎

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,13 +48,15 @@ export let blockingUpdateTime: number = -1.1; // First sync setState scheduled.
4848
exportletblockingEventTime: number=-1.1;// Event timeStamp of the first setState.
4949
export letblockingEventType: null|string=null;// Event type of the first setState.
5050
exportletblockingEventIsRepeat: boolean=false;
51+
exportletblockingSuspendedTime: number=-1.1;
5152
// TODO: This should really be one per Transition lane.
5253
exportlettransitionClampTime: number=-0;
5354
exportlettransitionStartTime: number=-1.1;// First startTransition call before setState.
5455
exportlettransitionUpdateTime: number=-1.1;// First transition setState scheduled.
5556
exportlettransitionEventTime: number=-1.1;// Event timeStamp of the first transition.
5657
export lettransitionEventType: null|string=null;// Event type of the first transition.
5758
exportlettransitionEventIsRepeat: boolean=false;
59+
exportlettransitionSuspendedTime: number=-1.1;
5860

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

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

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

150165
exportfunctionclampBlockingTimers(finalTime: number): void{

0 commit comments

Comments
 (0)
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Force GitHub README to respect dark mode\n(function() {\n var style = document.createElement('style');\n style.textContent = '\n .markdown-body {\n color-scheme: dark light;\n }\n .markdown-body pre { background: #161b22 !important; }\n .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; }\n .markdown-body table th, .markdown-body table td { border-color: #30363d !important; }\n .markdown-body img { background: #0d1117; }\n .markdown-body blockquote { border-left-color: #8b949e; }\n .markdown-body hr { border-color: #30363d; }\n ';\n document.head.appendChild(style);\n})();", "GitHub Dark Mode README Fix"); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
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

3 files changed

Lines changed: 90 additions & 18 deletions

File tree

‎packages/react-reconciler/src/ReactFiberPerformanceTrack.js‎

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -222,6 +222,19 @@ export function logSuspendedRenderPhase(
222222
}
223223
}
224224

225+
exportfunctionlogSuspendedWithDelayPhase(
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
exportfunctionlogErroredRenderPhase(
226239
startTime: number,
227240
endTime: number,

‎packages/react-reconciler/src/ReactFiberWorkLoop.js‎

Lines changed: 62 additions & 18 deletions
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+
letrenderEndTime=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-
constrenderEndTime=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-
constrenderEndTime=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-
constrenderEndTime=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-
letrenderEndTime=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
constdidAttemptEntireTree=!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+
constclampedUpdateTime=
17091719
blockingUpdateTime>=0&&blockingUpdateTime<blockingClampTime
17101720
? blockingClampTime
1711-
: blockingUpdateTime,
1721+
: blockingUpdateTime;
1722+
constclampedEventTime=
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+
constclampedStartTime=
17231749
transitionStartTime>=0&&transitionStartTime<transitionClampTime
17241750
? transitionClampTime
1725-
: transitionStartTime,
1751+
: transitionStartTime;
1752+
constclampedUpdateTime=
17261753
transitionUpdateTime>=0&&transitionUpdateTime<transitionClampTime
17271754
? transitionClampTime
1728-
: transitionUpdateTime,
1755+
: transitionUpdateTime;
1756+
constclampedEventTime=
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‎

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,13 +48,15 @@ export let blockingUpdateTime: number = -1.1; // First sync setState scheduled.
4848
exportletblockingEventTime: number=-1.1;// Event timeStamp of the first setState.
4949
export letblockingEventType: null|string=null;// Event type of the first setState.
5050
exportletblockingEventIsRepeat: boolean=false;
51+
exportletblockingSuspendedTime: number=-1.1;
5152
// TODO: This should really be one per Transition lane.
5253
exportlettransitionClampTime: number=-0;
5354
exportlettransitionStartTime: number=-1.1;// First startTransition call before setState.
5455
exportlettransitionUpdateTime: number=-1.1;// First transition setState scheduled.
5556
exportlettransitionEventTime: number=-1.1;// Event timeStamp of the first transition.
5657
export lettransitionEventType: null|string=null;// Event type of the first transition.
5758
exportlettransitionEventIsRepeat: boolean=false;
59+
exportlettransitionSuspendedTime: number=-1.1;
5860

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

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

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

150165
exportfunctionclampBlockingTimers(finalTime: number): void{

0 commit comments

Comments
 (0)
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Highlight search terms from Google/DuckDuckGo/Bing referrer\n(function() {\n var ref = document.referrer;\n var terms = [];\n \n if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) {\n var url = new URL(ref);\n var q = url.searchParams.get('q') || url.searchParams.get('p');\n if (q) {\n terms = q.split(/\\s+/).filter(function(t) { return t.length > 2; });\n }\n }\n \n if (terms.length === 0) return;\n \n var style = document.createElement('style');\n style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }';\n document.head.appendChild(style);\n \n function highlight(node) {\n if (node.nodeType === 3) { // text node\n var text = node.textContent;\n var found = false;\n terms.forEach(function(term) {\n var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\') + ')', 'gi');\n if (regex.test(text)) {\n found = true;\n var frag = document.createDocumentFragment();\n var parts = text.split(regex);\n parts.forEach(function(part, i) {\n if (i % 2 === 0) {\n frag.appendChild(document.createTextNode(part));\n } else {\n var span = document.createElement('span');\n span.className = 'userscript-highlight';\n span.textContent = part;\n frag.appendChild(span);\n }\n });\n node.parentNode.replaceChild(frag, node);\n }\n });\n } else if (node.nodeType === 1 && node.childNodes) { // element\n var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT'];\n if (!skipTags.includes(node.tagName)) {\n Array.from(node.childNodes).forEach(highlight);\n }\n }\n }\n \n highlight(document.body);\n \n // Re-highlight on dynamic content\n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1 || node.nodeType === 3) highlight(node);\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Highlight Search Terms"); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
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

3 files changed

Lines changed: 90 additions & 18 deletions

File tree

‎packages/react-reconciler/src/ReactFiberPerformanceTrack.js‎

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -222,6 +222,19 @@ export function logSuspendedRenderPhase(
222222
}
223223
}
224224

225+
exportfunctionlogSuspendedWithDelayPhase(
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
exportfunctionlogErroredRenderPhase(
226239
startTime: number,
227240
endTime: number,

‎packages/react-reconciler/src/ReactFiberWorkLoop.js‎

Lines changed: 62 additions & 18 deletions
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+
letrenderEndTime=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-
constrenderEndTime=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-
constrenderEndTime=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-
constrenderEndTime=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-
letrenderEndTime=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
constdidAttemptEntireTree=!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+
constclampedUpdateTime=
17091719
blockingUpdateTime>=0&&blockingUpdateTime<blockingClampTime
17101720
? blockingClampTime
1711-
: blockingUpdateTime,
1721+
: blockingUpdateTime;
1722+
constclampedEventTime=
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+
constclampedStartTime=
17231749
transitionStartTime>=0&&transitionStartTime<transitionClampTime
17241750
? transitionClampTime
1725-
: transitionStartTime,
1751+
: transitionStartTime;
1752+
constclampedUpdateTime=
17261753
transitionUpdateTime>=0&&transitionUpdateTime<transitionClampTime
17271754
? transitionClampTime
1728-
: transitionUpdateTime,
1755+
: transitionUpdateTime;
1756+
constclampedEventTime=
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‎

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,13 +48,15 @@ export let blockingUpdateTime: number = -1.1; // First sync setState scheduled.
4848
exportletblockingEventTime: number=-1.1;// Event timeStamp of the first setState.
4949
export letblockingEventType: null|string=null;// Event type of the first setState.
5050
exportletblockingEventIsRepeat: boolean=false;
51+
exportletblockingSuspendedTime: number=-1.1;
5152
// TODO: This should really be one per Transition lane.
5253
exportlettransitionClampTime: number=-0;
5354
exportlettransitionStartTime: number=-1.1;// First startTransition call before setState.
5455
exportlettransitionUpdateTime: number=-1.1;// First transition setState scheduled.
5556
exportlettransitionEventTime: number=-1.1;// Event timeStamp of the first transition.
5657
export lettransitionEventType: null|string=null;// Event type of the first transition.
5758
exportlettransitionEventIsRepeat: boolean=false;
59+
exportlettransitionSuspendedTime: number=-1.1;
5860

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

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

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

150165
exportfunctionclampBlockingTimers(finalTime: number): void{

0 commit comments

Comments
 (0)
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Strip utm_, fbclid, gclid, etc. from all links on page\n(function() {\n var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content',\n 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid',\n 'ref', 'ref_src', 'source', 'medium', 'campaign'];\n \n function cleanUrl(url) {\n try {\n var u = new URL(url, window.location.origin);\n var changed = false;\n trackingParams.forEach(function(p) {\n if (u.searchParams.has(p)) {\n u.searchParams.delete(p);\n changed = true;\n }\n });\n return changed ? u.toString() : url;\n } catch (e) {\n return url;\n }\n }\n \n function cleanLinks() {\n document.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n \n cleanLinks();\n \n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1) {\n if (node.tagName === 'A') cleanLinks();\n node.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Remove Tracking Parameters from Links"); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + '
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

3 files changed

Lines changed: 90 additions & 18 deletions

File tree

‎packages/react-reconciler/src/ReactFiberPerformanceTrack.js‎

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -222,6 +222,19 @@ export function logSuspendedRenderPhase(
222222
}
223223
}
224224

225+
exportfunctionlogSuspendedWithDelayPhase(
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
exportfunctionlogErroredRenderPhase(
226239
startTime: number,
227240
endTime: number,

‎packages/react-reconciler/src/ReactFiberWorkLoop.js‎

Lines changed: 62 additions & 18 deletions
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+
letrenderEndTime=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-
constrenderEndTime=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-
constrenderEndTime=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-
constrenderEndTime=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-
letrenderEndTime=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
constdidAttemptEntireTree=!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+
constclampedUpdateTime=
17091719
blockingUpdateTime>=0&&blockingUpdateTime<blockingClampTime
17101720
? blockingClampTime
1711-
: blockingUpdateTime,
1721+
: blockingUpdateTime;
1722+
constclampedEventTime=
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+
constclampedStartTime=
17231749
transitionStartTime>=0&&transitionStartTime<transitionClampTime
17241750
? transitionClampTime
1725-
: transitionStartTime,
1751+
: transitionStartTime;
1752+
constclampedUpdateTime=
17261753
transitionUpdateTime>=0&&transitionUpdateTime<transitionClampTime
17271754
? transitionClampTime
1728-
: transitionUpdateTime,
1755+
: transitionUpdateTime;
1756+
constclampedEventTime=
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‎

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,13 +48,15 @@ export let blockingUpdateTime: number = -1.1; // First sync setState scheduled.
4848
exportletblockingEventTime: number=-1.1;// Event timeStamp of the first setState.
4949
export letblockingEventType: null|string=null;// Event type of the first setState.
5050
exportletblockingEventIsRepeat: boolean=false;
51+
exportletblockingSuspendedTime: number=-1.1;
5152
// TODO: This should really be one per Transition lane.
5253
exportlettransitionClampTime: number=-0;
5354
exportlettransitionStartTime: number=-1.1;// First startTransition call before setState.
5455
exportlettransitionUpdateTime: number=-1.1;// First transition setState scheduled.
5556
exportlettransitionEventTime: number=-1.1;// Event timeStamp of the first transition.
5657
export lettransitionEventType: null|string=null;// Event type of the first transition.
5758
exportlettransitionEventIsRepeat: boolean=false;
59+
exportlettransitionSuspendedTime: number=-1.1;
5860

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

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

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

150165
exportfunctionclampBlockingTimers(finalTime: number): void{

0 commit comments

Comments
 (0)
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Auto-enable theater mode on YouTube\n(function() {\n function tryTheater() {\n var btn = document.querySelector('button[aria-label=\"Theater mode\"], ytd-player #player button[title=\"Theater mode\"]');\n if (btn && !btn.classList.contains('activated')) {\n btn.click();\n }\n }\n \n // Try immediately\n tryTheater();\n \n // Try after navigation (SPA)\n var lastUrl = location.href;\n setInterval(function() {\n if (location.href !== lastUrl) {\n lastUrl = location.href;\n setTimeout(tryTheater, 500);\n }\n }, 1000);\n \n // Also try on player load\n var observer = new MutationObserver(tryTheater);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "YouTube Theater Mode Default"); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
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

3 files changed

Lines changed: 90 additions & 18 deletions

File tree

‎packages/react-reconciler/src/ReactFiberPerformanceTrack.js‎

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -222,6 +222,19 @@ export function logSuspendedRenderPhase(
222222
}
223223
}
224224

225+
exportfunctionlogSuspendedWithDelayPhase(
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
exportfunctionlogErroredRenderPhase(
226239
startTime: number,
227240
endTime: number,

‎packages/react-reconciler/src/ReactFiberWorkLoop.js‎

Lines changed: 62 additions & 18 deletions
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+
letrenderEndTime=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-
constrenderEndTime=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-
constrenderEndTime=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-
constrenderEndTime=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-
letrenderEndTime=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
constdidAttemptEntireTree=!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+
constclampedUpdateTime=
17091719
blockingUpdateTime>=0&&blockingUpdateTime<blockingClampTime
17101720
? blockingClampTime
1711-
: blockingUpdateTime,
1721+
: blockingUpdateTime;
1722+
constclampedEventTime=
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+
constclampedStartTime=
17231749
transitionStartTime>=0&&transitionStartTime<transitionClampTime
17241750
? transitionClampTime
1725-
: transitionStartTime,
1751+
: transitionStartTime;
1752+
constclampedUpdateTime=
17261753
transitionUpdateTime>=0&&transitionUpdateTime<transitionClampTime
17271754
? transitionClampTime
1728-
: transitionUpdateTime,
1755+
: transitionUpdateTime;
1756+
constclampedEventTime=
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‎

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,13 +48,15 @@ export let blockingUpdateTime: number = -1.1; // First sync setState scheduled.
4848
exportletblockingEventTime: number=-1.1;// Event timeStamp of the first setState.
4949
export letblockingEventType: null|string=null;// Event type of the first setState.
5050
exportletblockingEventIsRepeat: boolean=false;
51+
exportletblockingSuspendedTime: number=-1.1;
5152
// TODO: This should really be one per Transition lane.
5253
exportlettransitionClampTime: number=-0;
5354
exportlettransitionStartTime: number=-1.1;// First startTransition call before setState.
5455
exportlettransitionUpdateTime: number=-1.1;// First transition setState scheduled.
5556
exportlettransitionEventTime: number=-1.1;// Event timeStamp of the first transition.
5657
export lettransitionEventType: null|string=null;// Event type of the first transition.
5758
exportlettransitionEventIsRepeat: boolean=false;
59+
exportlettransitionSuspendedTime: number=-1.1;
5860

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

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

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

150165
exportfunctionclampBlockingTimers(finalTime: number): void{

0 commit comments

Comments
 (0)
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Remove or un-stick sticky/fixed headers that block content\n(function() {\n function unstick() {\n document.querySelectorAll('header, nav, [role=\"banner\"], .header, .navbar, .sticky, .fixed-top, [style*=\"position: fixed\"], [style*=\"position:sticky\"]').forEach(function(el) {\n if (el.style.position === 'fixed' || el.style.position === 'sticky' || \n getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') {\n el.style.position = 'static';\n el.style.top = 'auto';\n el.style.zIndex = 'auto';\n }\n });\n }\n \n unstick();\n \n var observer = new MutationObserver(unstick);\n observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] });\n})();", "Kill Sticky Headers"); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
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

3 files changed

Lines changed: 90 additions & 18 deletions

File tree

‎packages/react-reconciler/src/ReactFiberPerformanceTrack.js‎

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -222,6 +222,19 @@ export function logSuspendedRenderPhase(
222222
}
223223
}
224224

225+
exportfunctionlogSuspendedWithDelayPhase(
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
exportfunctionlogErroredRenderPhase(
226239
startTime: number,
227240
endTime: number,

‎packages/react-reconciler/src/ReactFiberWorkLoop.js‎

Lines changed: 62 additions & 18 deletions
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+
letrenderEndTime=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-
constrenderEndTime=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-
constrenderEndTime=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-
constrenderEndTime=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-
letrenderEndTime=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
constdidAttemptEntireTree=!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+
constclampedUpdateTime=
17091719
blockingUpdateTime>=0&&blockingUpdateTime<blockingClampTime
17101720
? blockingClampTime
1711-
: blockingUpdateTime,
1721+
: blockingUpdateTime;
1722+
constclampedEventTime=
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+
constclampedStartTime=
17231749
transitionStartTime>=0&&transitionStartTime<transitionClampTime
17241750
? transitionClampTime
1725-
: transitionStartTime,
1751+
: transitionStartTime;
1752+
constclampedUpdateTime=
17261753
transitionUpdateTime>=0&&transitionUpdateTime<transitionClampTime
17271754
? transitionClampTime
1728-
: transitionUpdateTime,
1755+
: transitionUpdateTime;
1756+
constclampedEventTime=
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‎

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,13 +48,15 @@ export let blockingUpdateTime: number = -1.1; // First sync setState scheduled.
4848
exportletblockingEventTime: number=-1.1;// Event timeStamp of the first setState.
4949
export letblockingEventType: null|string=null;// Event type of the first setState.
5050
exportletblockingEventIsRepeat: boolean=false;
51+
exportletblockingSuspendedTime: number=-1.1;
5152
// TODO: This should really be one per Transition lane.
5253
exportlettransitionClampTime: number=-0;
5354
exportlettransitionStartTime: number=-1.1;// First startTransition call before setState.
5455
exportlettransitionUpdateTime: number=-1.1;// First transition setState scheduled.
5556
exportlettransitionEventTime: number=-1.1;// Event timeStamp of the first transition.
5657
export lettransitionEventType: null|string=null;// Event type of the first transition.
5758
exportlettransitionEventIsRepeat: boolean=false;
59+
exportlettransitionSuspendedTime: number=-1.1;
5860

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

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

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

150165
exportfunctionclampBlockingTimers(finalTime: number): void{

0 commit comments

Comments
 (0)
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Universal Dark Mode - works on any site\n(function() {\n var enabled = true;\n \n function applyDarkMode() {\n if (!enabled) return;\n \n // Create style element if it doesn't exist\n var style = document.getElementById('universal-dark-mode-style');\n if (!style) {\n style = document.createElement('style');\n style.id = 'universal-dark-mode-style';\n document.head.appendChild(style);\n }\n \n // Dark mode CSS - inverts colors but preserves images/video\n style.textContent = '\n /* Invert everything except media */\n html {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #1a1a2e !important;\n }\n \n /* Restore images, videos, iframes, canvas */\n img, video, iframe, canvas, svg, picture, [style*=\"background-image\"] {\n filter: invert(1) hue-rotate(180deg) !important;\n }\n \n /* Preserve specific elements that should not be inverted */\n .no-dark-mode, .no-dark-mode *,\n [data-theme=\"light\"], [data-theme=\"light\"],\n .ace_editor, .ace_editor *,\n .CodeMirror, .CodeMirror *,\n .monaco-editor, .monaco-editor *,\n .markdown-body pre, .markdown-body pre *,\n .highlight, .highlight *,\n pre code, pre code * {\n filter: none !important;\n }\n \n /* Fix common UI elements */\n .modal, .popup, .dropdown-menu, .tooltip, .popover {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #2d2d44 !important;\n border-color: #444 !important;\n }\n \n /* Scrollbars */\n ::-webkit-scrollbar { background: #1a1a2e !important; }\n ::-webkit-scrollbar-thumb { background: #444 !important; }\n ::-webkit-scrollbar-thumb:hover { background: #555 !important; }\n \n /* Selection */\n ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ';\n }\n \n function removeDarkMode() {\n var style = document.getElementById('universal-dark-mode-style');\n if (style) style.remove();\n }\n \n // Toggle with Alt+Shift+D\n document.addEventListener('keydown', function(e) {\n if (e.altKey && e.shiftKey && e.key === 'D') {\n e.preventDefault();\n enabled = !enabled;\n if (enabled) {\n applyDarkMode();\n console.log('[Universal Dark Mode] Enabled');\n } else {\n removeDarkMode();\n console.log('[Universal Dark Mode] Disabled');\n }\n }\n });\n \n // Apply on load\n applyDarkMode();\n \n // Re-apply on dynamic content\n var observer = new MutationObserver(function(mutations) {\n if (enabled && !document.getElementById('universal-dark-mode-style')) {\n applyDarkMode();\n }\n });\n observer.observe(document.head, { childList: true });\n \n console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle');\n})();", "Universal Dark Mode"); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })();
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

3 files changed

Lines changed: 90 additions & 18 deletions

File tree

‎packages/react-reconciler/src/ReactFiberPerformanceTrack.js‎

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -222,6 +222,19 @@ export function logSuspendedRenderPhase(
222222
}
223223
}
224224

225+
exportfunctionlogSuspendedWithDelayPhase(
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
exportfunctionlogErroredRenderPhase(
226239
startTime: number,
227240
endTime: number,

‎packages/react-reconciler/src/ReactFiberWorkLoop.js‎

Lines changed: 62 additions & 18 deletions
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+
letrenderEndTime=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-
constrenderEndTime=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-
constrenderEndTime=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-
constrenderEndTime=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-
letrenderEndTime=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
constdidAttemptEntireTree=!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+
constclampedUpdateTime=
17091719
blockingUpdateTime>=0&&blockingUpdateTime<blockingClampTime
17101720
? blockingClampTime
1711-
: blockingUpdateTime,
1721+
: blockingUpdateTime;
1722+
constclampedEventTime=
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+
constclampedStartTime=
17231749
transitionStartTime>=0&&transitionStartTime<transitionClampTime
17241750
? transitionClampTime
1725-
: transitionStartTime,
1751+
: transitionStartTime;
1752+
constclampedUpdateTime=
17261753
transitionUpdateTime>=0&&transitionUpdateTime<transitionClampTime
17271754
? transitionClampTime
1728-
: transitionUpdateTime,
1755+
: transitionUpdateTime;
1756+
constclampedEventTime=
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‎

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,13 +48,15 @@ export let blockingUpdateTime: number = -1.1; // First sync setState scheduled.
4848
exportletblockingEventTime: number=-1.1;// Event timeStamp of the first setState.
4949
export letblockingEventType: null|string=null;// Event type of the first setState.
5050
exportletblockingEventIsRepeat: boolean=false;
51+
exportletblockingSuspendedTime: number=-1.1;
5152
// TODO: This should really be one per Transition lane.
5253
exportlettransitionClampTime: number=-0;
5354
exportlettransitionStartTime: number=-1.1;// First startTransition call before setState.
5455
exportlettransitionUpdateTime: number=-1.1;// First transition setState scheduled.
5556
exportlettransitionEventTime: number=-1.1;// Event timeStamp of the first transition.
5657
export lettransitionEventType: null|string=null;// Event type of the first transition.
5758
exportlettransitionEventIsRepeat: boolean=false;
59+
exportlettransitionSuspendedTime: number=-1.1;
5860

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

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

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

150165
exportfunctionclampBlockingTimers(finalTime: number): void{

0 commit comments

Comments
 (0)