@samitouri / QOS-React-2 / commits / d6e433899f

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