Skip to content

Commit d6e4338

Browse files
authored
Use Global Render Timeout for CPU Suspense (#19643)
* Use Retry lane for resuming CPU suspended work * Use a global render timeout for CPU suspense heuristics * Fix profiler test since we're now reading time more often * Sync to new reconciler * Test synchronously rerendering should not render more rows
1 parent f912186 commit d6e4338

11 files changed

+265
-50
lines changed

Diff for: packages/react-reconciler/src/ReactFiberBeginWork.new.js

-2
Original file line numberDiff line numberDiff line change
@@ -2644,7 +2644,6 @@ function initSuspenseListRenderState(
26442644
renderingStartTime: 0,
26452645
last: lastContentRow,
26462646
tail: tail,
2647-
tailExpiration: 0,
26482647
tailMode: tailMode,
26492648
lastEffect: lastEffectBeforeRendering,
26502649
}: SuspenseListRenderState);
@@ -2655,7 +2654,6 @@ function initSuspenseListRenderState(
26552654
renderState.renderingStartTime = 0;
26562655
renderState.last = lastContentRow;
26572656
renderState.tail = tail;
2658-
renderState.tailExpiration = 0;
26592657
renderState.tailMode = tailMode;
26602658
renderState.lastEffect = lastEffectBeforeRendering;
26612659
}

Diff for: packages/react-reconciler/src/ReactFiberBeginWork.old.js

-2
Original file line numberDiff line numberDiff line change
@@ -2635,7 +2635,6 @@ function initSuspenseListRenderState(
26352635
renderingStartTime: 0,
26362636
last: lastContentRow,
26372637
tail: tail,
2638-
tailExpiration: 0,
26392638
tailMode: tailMode,
26402639
lastEffect: lastEffectBeforeRendering,
26412640
}: SuspenseListRenderState);
@@ -2646,7 +2645,6 @@ function initSuspenseListRenderState(
26462645
renderState.renderingStartTime = 0;
26472646
renderState.last = lastContentRow;
26482647
renderState.tail = tail;
2649-
renderState.tailExpiration = 0;
26502648
renderState.tailMode = tailMode;
26512649
renderState.lastEffect = lastEffectBeforeRendering;
26522650
}

Diff for: packages/react-reconciler/src/ReactFiberCompleteWork.new.js

+31-18
Original file line numberDiff line numberDiff line change
@@ -137,9 +137,10 @@ import {
137137
renderDidSuspendDelayIfPossible,
138138
renderHasNotSuspendedYet,
139139
popRenderLanes,
140+
getRenderTargetTime,
140141
} from './ReactFiberWorkLoop.new';
141142
import {createFundamentalStateInstance} from './ReactFiberFundamental.new';
142-
import {OffscreenLane} from './ReactFiberLane';
143+
import {OffscreenLane, SomeRetryLane} from './ReactFiberLane';
143144
import {resetChildFibers} from './ReactChildFiber.new';
144145
import {createScopeInstance} from './ReactFiberScope.new';
145146
import {transferActualDuration} from './ReactProfilerTimer.new';
@@ -1076,6 +1077,29 @@ function completeWork(
10761077
row = row.sibling;
10771078
}
10781079
}
1080+
1081+
if (renderState.tail !== null && now() > getRenderTargetTime()) {
1082+
// We have already passed our CPU deadline but we still have rows
1083+
// left in the tail. We'll just give up further attempts to render
1084+
// the main content and only render fallbacks.
1085+
workInProgress.effectTag |= DidCapture;
1086+
didSuspendAlready = true;
1087+
1088+
cutOffTailIfNeeded(renderState, false);
1089+
1090+
// Since nothing actually suspended, there will nothing to ping this
1091+
// to get it started back up to attempt the next item. While in terms
1092+
// of priority this work has the same priority as this current render,
1093+
// it's not part of the same transition once the transition has
1094+
// committed. If it's sync, we still want to yield so that it can be
1095+
// painted. Conceptually, this is really the same as pinging.
1096+
// We can use any RetryLane even if it's the one currently rendering
1097+
// since we're leaving it behind on this node.
1098+
workInProgress.lanes = SomeRetryLane;
1099+
if (enableSchedulerTracing) {
1100+
markSpawnedWork(SomeRetryLane);
1101+
}
1102+
}
10791103
} else {
10801104
cutOffTailIfNeeded(renderState, false);
10811105
}
@@ -1117,10 +1141,11 @@ function completeWork(
11171141
return null;
11181142
}
11191143
} else if (
1120-
// The time it took to render last row is greater than time until
1121-
// the expiration.
1144+
// The time it took to render last row is greater than the remaining
1145+
// time we have to render. So rendering one more row would likely
1146+
// exceed it.
11221147
now() * 2 - renderState.renderingStartTime >
1123-
renderState.tailExpiration &&
1148+
getRenderTargetTime() &&
11241149
renderLanes !== OffscreenLane
11251150
) {
11261151
// We have now passed our CPU deadline and we'll just give up further
@@ -1136,9 +1161,9 @@ function completeWork(
11361161
// them, then they really have the same priority as this render.
11371162
// So we'll pick it back up the very next render pass once we've had
11381163
// an opportunity to yield for paint.
1139-
workInProgress.lanes = renderLanes;
1164+
workInProgress.lanes = SomeRetryLane;
11401165
if (enableSchedulerTracing) {
1141-
markSpawnedWork(renderLanes);
1166+
markSpawnedWork(SomeRetryLane);
11421167
}
11431168
}
11441169
}
@@ -1163,18 +1188,6 @@ function completeWork(
11631188

11641189
if (renderState.tail !== null) {
11651190
// We still have tail rows to render.
1166-
if (renderState.tailExpiration === 0) {
1167-
// Heuristic for how long we're willing to spend rendering rows
1168-
// until we just give up and show what we have so far.
1169-
const TAIL_EXPIRATION_TIMEOUT_MS = 500;
1170-
renderState.tailExpiration = now() + TAIL_EXPIRATION_TIMEOUT_MS;
1171-
// TODO: This is meant to mimic the train model or JND but this
1172-
// is a per component value. It should really be since the start
1173-
// of the total render or last commit. Consider using something like
1174-
// globalMostRecentFallbackTime. That doesn't account for being
1175-
// suspended for part of the time or when it's a new render.
1176-
// It should probably use a global start time value instead.
1177-
}
11781191
// Pop a row.
11791192
const next = renderState.tail;
11801193
renderState.rendering = next;

Diff for: packages/react-reconciler/src/ReactFiberCompleteWork.old.js

+38-22
Original file line numberDiff line numberDiff line change
@@ -135,9 +135,10 @@ import {
135135
renderDidSuspendDelayIfPossible,
136136
renderHasNotSuspendedYet,
137137
popRenderLanes,
138+
getRenderTargetTime,
138139
} from './ReactFiberWorkLoop.old';
139140
import {createFundamentalStateInstance} from './ReactFiberFundamental.old';
140-
import {OffscreenLane} from './ReactFiberLane';
141+
import {OffscreenLane, SomeRetryLane} from './ReactFiberLane';
141142
import {resetChildFibers} from './ReactChildFiber.old';
142143
import {createScopeInstance} from './ReactFiberScope.old';
143144
import {transferActualDuration} from './ReactProfilerTimer.old';
@@ -1049,6 +1050,29 @@ function completeWork(
10491050
row = row.sibling;
10501051
}
10511052
}
1053+
1054+
if (renderState.tail !== null && now() > getRenderTargetTime()) {
1055+
// We have already passed our CPU deadline but we still have rows
1056+
// left in the tail. We'll just give up further attempts to render
1057+
// the main content and only render fallbacks.
1058+
workInProgress.effectTag |= DidCapture;
1059+
didSuspendAlready = true;
1060+
1061+
cutOffTailIfNeeded(renderState, false);
1062+
1063+
// Since nothing actually suspended, there will nothing to ping this
1064+
// to get it started back up to attempt the next item. While in terms
1065+
// of priority this work has the same priority as this current render,
1066+
// it's not part of the same transition once the transition has
1067+
// committed. If it's sync, we still want to yield so that it can be
1068+
// painted. Conceptually, this is really the same as pinging.
1069+
// We can use any RetryLane even if it's the one currently rendering
1070+
// since we're leaving it behind on this node.
1071+
workInProgress.lanes = SomeRetryLane;
1072+
if (enableSchedulerTracing) {
1073+
markSpawnedWork(SomeRetryLane);
1074+
}
1075+
}
10521076
} else {
10531077
cutOffTailIfNeeded(renderState, false);
10541078
}
@@ -1090,10 +1114,11 @@ function completeWork(
10901114
return null;
10911115
}
10921116
} else if (
1093-
// The time it took to render last row is greater than time until
1094-
// the expiration.
1117+
// The time it took to render last row is greater than the remaining
1118+
// time we have to render. So rendering one more row would likely
1119+
// exceed it.
10951120
now() * 2 - renderState.renderingStartTime >
1096-
renderState.tailExpiration &&
1121+
getRenderTargetTime() &&
10971122
renderLanes !== OffscreenLane
10981123
) {
10991124
// We have now passed our CPU deadline and we'll just give up further
@@ -1105,13 +1130,16 @@ function completeWork(
11051130
cutOffTailIfNeeded(renderState, false);
11061131

11071132
// Since nothing actually suspended, there will nothing to ping this
1108-
// to get it started back up to attempt the next item. If we can show
1109-
// them, then they really have the same priority as this render.
1110-
// So we'll pick it back up the very next render pass once we've had
1111-
// an opportunity to yield for paint.
1112-
workInProgress.lanes = renderLanes;
1133+
// to get it started back up to attempt the next item. While in terms
1134+
// of priority this work has the same priority as this current render,
1135+
// it's not part of the same transition once the transition has
1136+
// committed. If it's sync, we still want to yield so that it can be
1137+
// painted. Conceptually, this is really the same as pinging.
1138+
// We can use any RetryLane even if it's the one currently rendering
1139+
// since we're leaving it behind on this node.
1140+
workInProgress.lanes = SomeRetryLane;
11131141
if (enableSchedulerTracing) {
1114-
markSpawnedWork(renderLanes);
1142+
markSpawnedWork(SomeRetryLane);
11151143
}
11161144
}
11171145
}
@@ -1136,18 +1164,6 @@ function completeWork(
11361164

11371165
if (renderState.tail !== null) {
11381166
// We still have tail rows to render.
1139-
if (renderState.tailExpiration === 0) {
1140-
// Heuristic for how long we're willing to spend rendering rows
1141-
// until we just give up and show what we have so far.
1142-
const TAIL_EXPIRATION_TIMEOUT_MS = 500;
1143-
renderState.tailExpiration = now() + TAIL_EXPIRATION_TIMEOUT_MS;
1144-
// TODO: This is meant to mimic the train model or JND but this
1145-
// is a per component value. It should really be since the start
1146-
// of the total render or last commit. Consider using something like
1147-
// globalMostRecentFallbackTime. That doesn't account for being
1148-
// suspended for part of the time or when it's a new render.
1149-
// It should probably use a global start time value instead.
1150-
}
11511167
// Pop a row.
11521168
const next = renderState.tail;
11531169
renderState.rendering = next;

Diff for: packages/react-reconciler/src/ReactFiberLane.js

+2
Original file line numberDiff line numberDiff line change
@@ -97,6 +97,8 @@ const TransitionLongLanes: Lanes = /* */ 0b0000000001111000000
9797

9898
const RetryLanes: Lanes = /* */ 0b0000011110000000000000000000000;
9999

100+
export const SomeRetryLane: Lanes = /* */ 0b0000010000000000000000000000000;
101+
100102
export const SelectiveHydrationLane: Lane = /* */ 0b0000100000000000000000000000000;
101103

102104
const NonIdleLanes = /* */ 0b0000111111111111111111111111111;

Diff for: packages/react-reconciler/src/ReactFiberSuspenseComponent.new.js

+1-3
Original file line numberDiff line numberDiff line change
@@ -41,14 +41,12 @@ export type SuspenseListRenderState = {|
4141
isBackwards: boolean,
4242
// The currently rendering tail row.
4343
rendering: null | Fiber,
44-
// The absolute time when we started rendering the tail row.
44+
// The absolute time when we started rendering the most recent tail row.
4545
renderingStartTime: number,
4646
// The last of the already rendered children.
4747
last: null | Fiber,
4848
// Remaining rows on the tail of the list.
4949
tail: null | Fiber,
50-
// The absolute time in ms that we'll expire the tail rendering.
51-
tailExpiration: number,
5250
// Tail insertions setting.
5351
tailMode: SuspenseListTailMode,
5452
// Last Effect before we rendered the "rendering" item.

Diff for: packages/react-reconciler/src/ReactFiberSuspenseComponent.old.js

+1-3
Original file line numberDiff line numberDiff line change
@@ -41,14 +41,12 @@ export type SuspenseListRenderState = {|
4141
isBackwards: boolean,
4242
// The currently rendering tail row.
4343
rendering: null | Fiber,
44-
// The absolute time when we started rendering the tail row.
44+
// The absolute time when we started rendering the most recent tail row.
4545
renderingStartTime: number,
4646
// The last of the already rendered children.
4747
last: null | Fiber,
4848
// Remaining rows on the tail of the list.
4949
tail: null | Fiber,
50-
// The absolute time in ms that we'll expire the tail rendering.
51-
tailExpiration: number,
5250
// Tail insertions setting.
5351
tailMode: SuspenseListTailMode,
5452
// Last Effect before we rendered the "rendering" item.

Diff for: packages/react-reconciler/src/ReactFiberWorkLoop.new.js

+25
Original file line numberDiff line numberDiff line change
@@ -328,6 +328,21 @@ let globalMostRecentFallbackTime: number = 0;
328328
const FALLBACK_THROTTLE_MS: number = 500;
329329
const DEFAULT_TIMEOUT_MS: number = 5000;
330330

331+
// The absolute time for when we should start giving up on rendering
332+
// more and prefer CPU suspense heuristics instead.
333+
let workInProgressRootRenderTargetTime: number = Infinity;
334+
// How long a render is supposed to take before we start following CPU
335+
// suspense heuristics and opt out of rendering more content.
336+
const RENDER_TIMEOUT_MS = 500;
337+
338+
function resetRenderTimer() {
339+
workInProgressRootRenderTargetTime = now() + RENDER_TIMEOUT_MS;
340+
}
341+
342+
export function getRenderTargetTime(): number {
343+
return workInProgressRootRenderTargetTime;
344+
}
345+
331346
let hasUncaughtError = false;
332347
let firstUncaughtError = null;
333348
let legacyErrorBoundariesThatAlreadyFailed: Set<mixed> | null = null;
@@ -603,6 +618,7 @@ export function scheduleUpdateOnFiber(
603618
// scheduleCallbackForFiber to preserve the ability to schedule a callback
604619
// without immediately flushing it. We only do this for user-initiated
605620
// updates, to preserve historical behavior of legacy mode.
621+
resetRenderTimer();
606622
flushSyncCallbackQueue();
607623
}
608624
}
@@ -1111,6 +1127,7 @@ export function flushRoot(root: FiberRoot, lanes: Lanes) {
11111127
markRootExpired(root, lanes);
11121128
ensureRootIsScheduled(root, now());
11131129
if ((executionContext & (RenderContext | CommitContext)) === NoContext) {
1130+
resetRenderTimer();
11141131
flushSyncCallbackQueue();
11151132
}
11161133
}
@@ -1185,6 +1202,7 @@ export function batchedUpdates<A, R>(fn: A => R, a: A): R {
11851202
executionContext = prevExecutionContext;
11861203
if (executionContext === NoContext) {
11871204
// Flush the immediate callbacks that were scheduled during this batch
1205+
resetRenderTimer();
11881206
flushSyncCallbackQueue();
11891207
}
11901208
}
@@ -1199,6 +1217,7 @@ export function batchedEventUpdates<A, R>(fn: A => R, a: A): R {
11991217
executionContext = prevExecutionContext;
12001218
if (executionContext === NoContext) {
12011219
// Flush the immediate callbacks that were scheduled during this batch
1220+
resetRenderTimer();
12021221
flushSyncCallbackQueue();
12031222
}
12041223
}
@@ -1227,6 +1246,7 @@ export function discreteUpdates<A, B, C, D, R>(
12271246
executionContext = prevExecutionContext;
12281247
if (executionContext === NoContext) {
12291248
// Flush the immediate callbacks that were scheduled during this batch
1249+
resetRenderTimer();
12301250
flushSyncCallbackQueue();
12311251
}
12321252
}
@@ -1240,6 +1260,7 @@ export function discreteUpdates<A, B, C, D, R>(
12401260
executionContext = prevExecutionContext;
12411261
if (executionContext === NoContext) {
12421262
// Flush the immediate callbacks that were scheduled during this batch
1263+
resetRenderTimer();
12431264
flushSyncCallbackQueue();
12441265
}
12451266
}
@@ -1256,6 +1277,7 @@ export function unbatchedUpdates<A, R>(fn: (a: A) => R, a: A): R {
12561277
executionContext = prevExecutionContext;
12571278
if (executionContext === NoContext) {
12581279
// Flush the immediate callbacks that were scheduled during this batch
1280+
resetRenderTimer();
12591281
flushSyncCallbackQueue();
12601282
}
12611283
}
@@ -1323,6 +1345,7 @@ export function flushControlled(fn: () => mixed): void {
13231345
executionContext = prevExecutionContext;
13241346
if (executionContext === NoContext) {
13251347
// Flush the immediate callbacks that were scheduled during this batch
1348+
resetRenderTimer();
13261349
flushSyncCallbackQueue();
13271350
}
13281351
}
@@ -1333,6 +1356,7 @@ export function flushControlled(fn: () => mixed): void {
13331356
executionContext = prevExecutionContext;
13341357
if (executionContext === NoContext) {
13351358
// Flush the immediate callbacks that were scheduled during this batch
1359+
resetRenderTimer();
13361360
flushSyncCallbackQueue();
13371361
}
13381362
}
@@ -1651,6 +1675,7 @@ function renderRootConcurrent(root: FiberRoot, lanes: Lanes) {
16511675
// If the root or lanes have changed, throw out the existing stack
16521676
// and prepare a fresh one. Otherwise we'll continue where we left off.
16531677
if (workInProgressRoot !== root || workInProgressRootRenderLanes !== lanes) {
1678+
resetRenderTimer();
16541679
prepareFreshStack(root, lanes);
16551680
startWorkOnPendingInteractions(root, lanes);
16561681
}

0 commit comments

Comments
 (0)