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
Sebastian Markbåge committed
Aug 20, 2020 at 17:39 UTC
d6e433899f387be42a3cec2115b4607f32910a3b
11 files changed
+265
-50
packages/react-reconciler/src/ReactFiberBeginWork.new.js
-2
@@ -2644,7 +2644,6 @@ function initSuspenseListRenderState(
2644
renderingStartTime: 0,
2645
last: lastContentRow,
2646
tail: tail,
2647
- tailExpiration: 0,
2647
tailMode: tailMode,
2648
lastEffect: lastEffectBeforeRendering,
2649
}: SuspenseListRenderState);
@@ -2655,7 +2654,6 @@ function initSuspenseListRenderState(
2654
renderState.renderingStartTime = 0;
2655
renderState.last = lastContentRow;
2656
renderState.tail = tail;
2658
- renderState.tailExpiration = 0;
2657
renderState.tailMode = tailMode;
2658
renderState.lastEffect = lastEffectBeforeRendering;
2659
}
packages/react-reconciler/src/ReactFiberBeginWork.old.js
-2
@@ -2635,7 +2635,6 @@ function initSuspenseListRenderState(
2635
renderingStartTime: 0,
2636
last: lastContentRow,
2637
tail: tail,
2638
- tailExpiration: 0,
2638
tailMode: tailMode,
2639
lastEffect: lastEffectBeforeRendering,
2640
}: SuspenseListRenderState);
@@ -2646,7 +2645,6 @@ function initSuspenseListRenderState(
2645
renderState.renderingStartTime = 0;
2646
renderState.last = lastContentRow;
2647
renderState.tail = tail;
2649
- renderState.tailExpiration = 0;
2648
renderState.tailMode = tailMode;
2649
renderState.lastEffect = lastEffectBeforeRendering;
2650
}
packages/react-reconciler/src/ReactFiberCompleteWork.new.js
+31
-18
@@ -137,9 +137,10 @@ import {
137
renderDidSuspendDelayIfPossible,
138
renderHasNotSuspendedYet,
139
popRenderLanes,
140
+ getRenderTargetTime,
141
} from './ReactFiberWorkLoop.new';
142
import {createFundamentalStateInstance} from './ReactFiberFundamental.new';
142
-import {OffscreenLane} from './ReactFiberLane';
143
+import {OffscreenLane, SomeRetryLane} from './ReactFiberLane';
144
import {resetChildFibers} from './ReactChildFiber.new';
145
import {createScopeInstance} from './ReactFiberScope.new';
146
import {transferActualDuration} from './ReactProfilerTimer.new';
@@ -1076,6 +1077,29 @@ function completeWork(
1077
row = row.sibling;
1078
}
1079
}
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
+ }
1103
} else {
1104
cutOffTailIfNeeded(renderState, false);
1105
}
@@ -1117,10 +1141,11 @@ function completeWork(
1141
return null;
1142
}
1143
} 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.
1147
now() * 2 - renderState.renderingStartTime >
1123
- renderState.tailExpiration &&
1148
+ getRenderTargetTime() &&
1149
renderLanes !== OffscreenLane
1150
) {
1151
// We have now passed our CPU deadline and we'll just give up further
@@ -1136,9 +1161,9 @@ function completeWork(
1161
// them, then they really have the same priority as this render.
1162
// So we'll pick it back up the very next render pass once we've had
1163
// an opportunity to yield for paint.
1139
- workInProgress.lanes = renderLanes;
1164
+ workInProgress.lanes = SomeRetryLane;
1165
if (enableSchedulerTracing) {
1141
- markSpawnedWork(renderLanes);
1166
+ markSpawnedWork(SomeRetryLane);
1167
}
1168
}
1169
}
@@ -1163,18 +1188,6 @@ function completeWork(
1188
1189
if (renderState.tail !== null) {
1190
// 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
- }
1191
// Pop a row.
1192
const next = renderState.tail;
1193
renderState.rendering = next;
packages/react-reconciler/src/ReactFiberCompleteWork.old.js
+38
-22
@@ -135,9 +135,10 @@ import {
135
renderDidSuspendDelayIfPossible,
136
renderHasNotSuspendedYet,
137
popRenderLanes,
138
+ getRenderTargetTime,
139
} from './ReactFiberWorkLoop.old';
140
import {createFundamentalStateInstance} from './ReactFiberFundamental.old';
140
-import {OffscreenLane} from './ReactFiberLane';
141
+import {OffscreenLane, SomeRetryLane} from './ReactFiberLane';
142
import {resetChildFibers} from './ReactChildFiber.old';
143
import {createScopeInstance} from './ReactFiberScope.old';
144
import {transferActualDuration} from './ReactProfilerTimer.old';
@@ -1049,6 +1050,29 @@ function completeWork(
1050
row = row.sibling;
1051
}
1052
}
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
+ }
1076
} else {
1077
cutOffTailIfNeeded(renderState, false);
1078
}
@@ -1090,10 +1114,11 @@ function completeWork(
1114
return null;
1115
}
1116
} 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.
1120
now() * 2 - renderState.renderingStartTime >
1096
- renderState.tailExpiration &&
1121
+ getRenderTargetTime() &&
1122
renderLanes !== OffscreenLane
1123
) {
1124
// We have now passed our CPU deadline and we'll just give up further
@@ -1105,13 +1130,16 @@ function completeWork(
1130
cutOffTailIfNeeded(renderState, false);
1131
1132
// 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;
1141
if (enableSchedulerTracing) {
1114
- markSpawnedWork(renderLanes);
1142
+ markSpawnedWork(SomeRetryLane);
1143
}
1144
}
1145
}
@@ -1136,18 +1164,6 @@ function completeWork(
1164
1165
if (renderState.tail !== null) {
1166
// 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
- }
1167
// Pop a row.
1168
const next = renderState.tail;
1169
renderState.rendering = next;
packages/react-reconciler/src/ReactFiberLane.js
+2
@@ -97,6 +97,8 @@ const TransitionLongLanes: Lanes = /* */ 0b0000000001111000000
97
98
const RetryLanes: Lanes = /* */ 0b0000011110000000000000000000000;
99
100
+export const SomeRetryLane: Lanes = /* */ 0b0000010000000000000000000000000;
101
+
102
export const SelectiveHydrationLane: Lane = /* */ 0b0000100000000000000000000000000;
103
104
const NonIdleLanes = /* */ 0b0000111111111111111111111111111;
packages/react-reconciler/src/ReactFiberSuspenseComponent.new.js
+1
-3
@@ -41,14 +41,12 @@ export type SuspenseListRenderState = {|
41
isBackwards: boolean,
42
// The currently rendering tail row.
43
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.
45
renderingStartTime: number,
46
// The last of the already rendered children.
47
last: null | Fiber,
48
// Remaining rows on the tail of the list.
49
tail: null | Fiber,
50
- // The absolute time in ms that we'll expire the tail rendering.
51
- tailExpiration: number,
50
// Tail insertions setting.
51
tailMode: SuspenseListTailMode,
52
// Last Effect before we rendered the "rendering" item.
packages/react-reconciler/src/ReactFiberSuspenseComponent.old.js
+1
-3
@@ -41,14 +41,12 @@ export type SuspenseListRenderState = {|
41
isBackwards: boolean,
42
// The currently rendering tail row.
43
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.
45
renderingStartTime: number,
46
// The last of the already rendered children.
47
last: null | Fiber,
48
// Remaining rows on the tail of the list.
49
tail: null | Fiber,
50
- // The absolute time in ms that we'll expire the tail rendering.
51
- tailExpiration: number,
50
// Tail insertions setting.
51
tailMode: SuspenseListTailMode,
52
// Last Effect before we rendered the "rendering" item.
packages/react-reconciler/src/ReactFiberWorkLoop.new.js
+25
@@ -328,6 +328,21 @@ let globalMostRecentFallbackTime: number = 0;
328
const FALLBACK_THROTTLE_MS: number = 500;
329
const DEFAULT_TIMEOUT_MS: number = 5000;
330
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
+
346
let hasUncaughtError = false;
347
let firstUncaughtError = null;
348
let legacyErrorBoundariesThatAlreadyFailed: Set<mixed> | null = null;
@@ -603,6 +618,7 @@ export function scheduleUpdateOnFiber(
618
// scheduleCallbackForFiber to preserve the ability to schedule a callback
619
// without immediately flushing it. We only do this for user-initiated
620
// updates, to preserve historical behavior of legacy mode.
621
+ resetRenderTimer();
622
flushSyncCallbackQueue();
623
}
624
}
@@ -1111,6 +1127,7 @@ export function flushRoot(root: FiberRoot, lanes: Lanes) {
1127
markRootExpired(root, lanes);
1128
ensureRootIsScheduled(root, now());
1129
if ((executionContext & (RenderContext | CommitContext)) === NoContext) {
1130
+ resetRenderTimer();
1131
flushSyncCallbackQueue();
1132
}
1133
}
@@ -1185,6 +1202,7 @@ export function batchedUpdates<A, R>(fn: A => R, a: A): R {
1202
executionContext = prevExecutionContext;
1203
if (executionContext === NoContext) {
1204
// Flush the immediate callbacks that were scheduled during this batch
1205
+ resetRenderTimer();
1206
flushSyncCallbackQueue();
1207
}
1208
}
@@ -1199,6 +1217,7 @@ export function batchedEventUpdates<A, R>(fn: A => R, a: A): R {
1217
executionContext = prevExecutionContext;
1218
if (executionContext === NoContext) {
1219
// Flush the immediate callbacks that were scheduled during this batch
1220
+ resetRenderTimer();
1221
flushSyncCallbackQueue();
1222
}
1223
}
@@ -1227,6 +1246,7 @@ export function discreteUpdates<A, B, C, D, R>(
1246
executionContext = prevExecutionContext;
1247
if (executionContext === NoContext) {
1248
// Flush the immediate callbacks that were scheduled during this batch
1249
+ resetRenderTimer();
1250
flushSyncCallbackQueue();
1251
}
1252
}
@@ -1240,6 +1260,7 @@ export function discreteUpdates<A, B, C, D, R>(
1260
executionContext = prevExecutionContext;
1261
if (executionContext === NoContext) {
1262
// Flush the immediate callbacks that were scheduled during this batch
1263
+ resetRenderTimer();
1264
flushSyncCallbackQueue();
1265
}
1266
}
@@ -1256,6 +1277,7 @@ export function unbatchedUpdates<A, R>(fn: (a: A) => R, a: A): R {
1277
executionContext = prevExecutionContext;
1278
if (executionContext === NoContext) {
1279
// Flush the immediate callbacks that were scheduled during this batch
1280
+ resetRenderTimer();
1281
flushSyncCallbackQueue();
1282
}
1283
}
@@ -1323,6 +1345,7 @@ export function flushControlled(fn: () => mixed): void {
1345
executionContext = prevExecutionContext;
1346
if (executionContext === NoContext) {
1347
// Flush the immediate callbacks that were scheduled during this batch
1348
+ resetRenderTimer();
1349
flushSyncCallbackQueue();
1350
}
1351
}
@@ -1333,6 +1356,7 @@ export function flushControlled(fn: () => mixed): void {
1356
executionContext = prevExecutionContext;
1357
if (executionContext === NoContext) {
1358
// Flush the immediate callbacks that were scheduled during this batch
1359
+ resetRenderTimer();
1360
flushSyncCallbackQueue();
1361
}
1362
}
@@ -1651,6 +1675,7 @@ function renderRootConcurrent(root: FiberRoot, lanes: Lanes) {
1675
// If the root or lanes have changed, throw out the existing stack
1676
// and prepare a fresh one. Otherwise we'll continue where we left off.
1677
if (workInProgressRoot !== root || workInProgressRootRenderLanes !== lanes) {
1678
+ resetRenderTimer();
1679
prepareFreshStack(root, lanes);
1680
startWorkOnPendingInteractions(root, lanes);
1681
}
packages/react-reconciler/src/ReactFiberWorkLoop.old.js
+25
@@ -312,6 +312,21 @@ let globalMostRecentFallbackTime: number = 0;
312
const FALLBACK_THROTTLE_MS: number = 500;
313
const DEFAULT_TIMEOUT_MS: number = 5000;
314
315
+// The absolute time for when we should start giving up on rendering
316
+// more and prefer CPU suspense heuristics instead.
317
+let workInProgressRootRenderTargetTime: number = Infinity;
318
+// How long a render is supposed to take before we start following CPU
319
+// suspense heuristics and opt out of rendering more content.
320
+const RENDER_TIMEOUT_MS = 500;
321
+
322
+function resetRenderTimer() {
323
+ workInProgressRootRenderTargetTime = now() + RENDER_TIMEOUT_MS;
324
+}
325
+
326
+export function getRenderTargetTime(): number {
327
+ return workInProgressRootRenderTargetTime;
328
+}
329
+
330
let nextEffect: Fiber | null = null;
331
let hasUncaughtError = false;
332
let firstUncaughtError = null;
@@ -590,6 +605,7 @@ export function scheduleUpdateOnFiber(
605
// scheduleCallbackForFiber to preserve the ability to schedule a callback
606
// without immediately flushing it. We only do this for user-initiated
607
// updates, to preserve historical behavior of legacy mode.
608
+ resetRenderTimer();
609
flushSyncCallbackQueue();
610
}
611
}
@@ -1098,6 +1114,7 @@ export function flushRoot(root: FiberRoot, lanes: Lanes) {
1114
markRootExpired(root, lanes);
1115
ensureRootIsScheduled(root, now());
1116
if ((executionContext & (RenderContext | CommitContext)) === NoContext) {
1117
+ resetRenderTimer();
1118
flushSyncCallbackQueue();
1119
}
1120
}
@@ -1172,6 +1189,7 @@ export function batchedUpdates<A, R>(fn: A => R, a: A): R {
1189
executionContext = prevExecutionContext;
1190
if (executionContext === NoContext) {
1191
// Flush the immediate callbacks that were scheduled during this batch
1192
+ resetRenderTimer();
1193
flushSyncCallbackQueue();
1194
}
1195
}
@@ -1186,6 +1204,7 @@ export function batchedEventUpdates<A, R>(fn: A => R, a: A): R {
1204
executionContext = prevExecutionContext;
1205
if (executionContext === NoContext) {
1206
// Flush the immediate callbacks that were scheduled during this batch
1207
+ resetRenderTimer();
1208
flushSyncCallbackQueue();
1209
}
1210
}
@@ -1214,6 +1233,7 @@ export function discreteUpdates<A, B, C, D, R>(
1233
executionContext = prevExecutionContext;
1234
if (executionContext === NoContext) {
1235
// Flush the immediate callbacks that were scheduled during this batch
1236
+ resetRenderTimer();
1237
flushSyncCallbackQueue();
1238
}
1239
}
@@ -1227,6 +1247,7 @@ export function discreteUpdates<A, B, C, D, R>(
1247
executionContext = prevExecutionContext;
1248
if (executionContext === NoContext) {
1249
// Flush the immediate callbacks that were scheduled during this batch
1250
+ resetRenderTimer();
1251
flushSyncCallbackQueue();
1252
}
1253
}
@@ -1243,6 +1264,7 @@ export function unbatchedUpdates<A, R>(fn: (a: A) => R, a: A): R {
1264
executionContext = prevExecutionContext;
1265
if (executionContext === NoContext) {
1266
// Flush the immediate callbacks that were scheduled during this batch
1267
+ resetRenderTimer();
1268
flushSyncCallbackQueue();
1269
}
1270
}
@@ -1310,6 +1332,7 @@ export function flushControlled(fn: () => mixed): void {
1332
executionContext = prevExecutionContext;
1333
if (executionContext === NoContext) {
1334
// Flush the immediate callbacks that were scheduled during this batch
1335
+ resetRenderTimer();
1336
flushSyncCallbackQueue();
1337
}
1338
}
@@ -1320,6 +1343,7 @@ export function flushControlled(fn: () => mixed): void {
1343
executionContext = prevExecutionContext;
1344
if (executionContext === NoContext) {
1345
// Flush the immediate callbacks that were scheduled during this batch
1346
+ resetRenderTimer();
1347
flushSyncCallbackQueue();
1348
}
1349
}
@@ -1638,6 +1662,7 @@ function renderRootConcurrent(root: FiberRoot, lanes: Lanes) {
1662
// If the root or lanes have changed, throw out the existing stack
1663
// and prepare a fresh one. Otherwise we'll continue where we left off.
1664
if (workInProgressRoot !== root || workInProgressRootRenderLanes !== lanes) {
1665
+ resetRenderTimer();
1666
prepareFreshStack(root, lanes);
1667
startWorkOnPendingInteractions(root, lanes);
1668
}
packages/react-reconciler/src/__tests__/ReactSuspenseList-test.js
+141
@@ -2556,6 +2556,147 @@ describe('ReactSuspenseList', () => {
2556
);
2557
});
2558
2559
+ // @gate experimental
2560
+ it('should be able to progressively show CPU expensive rows with two pass rendering', async () => {
2561
+ function TwoPass({text}) {
2562
+ const [pass, setPass] = React.useState(0);
2563
+ React.useLayoutEffect(() => {
2564
+ Scheduler.unstable_yieldValue('Mount ' + text);
2565
+ setPass(1);
2566
+ }, []);
2567
+ return <Text text={pass === 0 ? 'First Pass ' + text : text} />;
2568
+ }
2569
+
2570
+ function Sleep({time, children}) {
2571
+ Scheduler.unstable_advanceTime(time);
2572
+ return children;
2573
+ }
2574
+
2575
+ function App() {
2576
+ Scheduler.unstable_yieldValue('App');
2577
+ return (
2578
+ <SuspenseList revealOrder="forwards" tail="hidden">
2579
+ <Suspense fallback={<Text text="Loading A" />}>
2580
+ <Sleep time={600}>
2581
+ <TwoPass text="A" />
2582
+ </Sleep>
2583
+ </Suspense>
2584
+ <Suspense fallback={<Text text="Loading B" />}>
2585
+ <Sleep time={600}>
2586
+ <TwoPass text="B" />
2587
+ </Sleep>
2588
+ </Suspense>
2589
+ <Sleep time={600}>
2590
+ <Text text="C" />
2591
+ </Sleep>
2592
+ </SuspenseList>
2593
+ );
2594
+ }
2595
+
2596
+ ReactNoop.render(<App />);
2597
+
2598
+ expect(Scheduler).toFlushAndYieldThrough([
2599
+ 'App',
2600
+ 'First Pass A',
2601
+ 'Mount A',
2602
+ 'A',
2603
+ ]);
2604
+ expect(ReactNoop).toMatchRenderedOutput(<span>A</span>);
2605
+
2606
+ expect(Scheduler).toFlushAndYieldThrough(['First Pass B', 'Mount B', 'B']);
2607
+ expect(ReactNoop).toMatchRenderedOutput(
2608
+ <>
2609
+ <span>A</span>
2610
+ <span>B</span>
2611
+ </>,
2612
+ );
2613
+
2614
+ expect(Scheduler).toFlushAndYield(['C']);
2615
+ expect(ReactNoop).toMatchRenderedOutput(
2616
+ <>
2617
+ <span>A</span>
2618
+ <span>B</span>
2619
+ <span>C</span>
2620
+ </>,
2621
+ );
2622
+ });
2623
+
2624
+ // @gate experimental
2625
+ it('should be able to progressively show rows with two pass rendering and visible', async () => {
2626
+ function TwoPass({text}) {
2627
+ const [pass, setPass] = React.useState(0);
2628
+ React.useLayoutEffect(() => {
2629
+ Scheduler.unstable_yieldValue('Mount ' + text);
2630
+ setPass(1);
2631
+ }, []);
2632
+ return <Text text={pass === 0 ? 'First Pass ' + text : text} />;
2633
+ }
2634
+
2635
+ function Sleep({time, children}) {
2636
+ Scheduler.unstable_advanceTime(time);
2637
+ return children;
2638
+ }
2639
+
2640
+ function App() {
2641
+ Scheduler.unstable_yieldValue('App');
2642
+ return (
2643
+ <SuspenseList revealOrder="forwards">
2644
+ <Suspense fallback={<Text text="Loading A" />}>
2645
+ <Sleep time={600}>
2646
+ <TwoPass text="A" />
2647
+ </Sleep>
2648
+ </Suspense>
2649
+ <Suspense fallback={<Text text="Loading B" />}>
2650
+ <Sleep time={600}>
2651
+ <TwoPass text="B" />
2652
+ </Sleep>
2653
+ </Suspense>
2654
+ <Suspense fallback={<Text text="Loading C" />}>
2655
+ <Sleep time={600}>
2656
+ <Text text="C" />
2657
+ </Sleep>
2658
+ </Suspense>
2659
+ </SuspenseList>
2660
+ );
2661
+ }
2662
+
2663
+ ReactNoop.render(<App />);
2664
+
2665
+ expect(Scheduler).toFlushAndYieldThrough([
2666
+ 'App',
2667
+ 'First Pass A',
2668
+ 'Loading B',
2669
+ 'Loading C',
2670
+ 'Mount A',
2671
+ 'A',
2672
+ ]);
2673
+ expect(ReactNoop).toMatchRenderedOutput(
2674
+ <>
2675
+ <span>A</span>
2676
+ <span>Loading B</span>
2677
+ <span>Loading C</span>
2678
+ </>,
2679
+ );
2680
+
2681
+ expect(Scheduler).toFlushAndYieldThrough(['First Pass B', 'Mount B', 'B']);
2682
+ expect(ReactNoop).toMatchRenderedOutput(
2683
+ <>
2684
+ <span>A</span>
2685
+ <span>B</span>
2686
+ <span>Loading C</span>
2687
+ </>,
2688
+ );
2689
+
2690
+ expect(Scheduler).toFlushAndYield(['C']);
2691
+ expect(ReactNoop).toMatchRenderedOutput(
2692
+ <>
2693
+ <span>A</span>
2694
+ <span>B</span>
2695
+ <span>C</span>
2696
+ </>,
2697
+ );
2698
+ });
2699
+
2700
// @gate experimental && enableProfilerTimer
2701
it('counts the actual duration when profiling a SuspenseList', async () => {
2702
// Order of parameters: id, phase, actualDuration, treeBaseDuration
packages/react/src/__tests__/ReactProfiler-test.internal.js
+1
@@ -294,6 +294,7 @@ describe('Profiler', () => {
294
'read current time',
295
'read current time',
296
'read current time',
297
+ 'read current time',
298
]);
299
300
// Restore original mock