Skip to content
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 0 additions & 2 deletions packages/react-reconciler/src/ReactFiberBeginWork.new.js
Original file line number Diff line number Diff line change
@@ -2644,7 +2644,6 @@ function initSuspenseListRenderState(
renderingStartTime: 0,
last: lastContentRow,
tail: tail,
tailExpiration: 0,
tailMode: tailMode,
lastEffect: lastEffectBeforeRendering,
}: SuspenseListRenderState);
@@ -2655,7 +2654,6 @@ function initSuspenseListRenderState(
renderState.renderingStartTime = 0;
renderState.last = lastContentRow;
renderState.tail = tail;
renderState.tailExpiration = 0;
renderState.tailMode = tailMode;
renderState.lastEffect = lastEffectBeforeRendering;
}
2 changes: 0 additions & 2 deletions packages/react-reconciler/src/ReactFiberBeginWork.old.js
Original file line number Diff line number Diff line change
@@ -2635,7 +2635,6 @@ function initSuspenseListRenderState(
renderingStartTime: 0,
last: lastContentRow,
tail: tail,
tailExpiration: 0,
tailMode: tailMode,
lastEffect: lastEffectBeforeRendering,
}: SuspenseListRenderState);
@@ -2646,7 +2645,6 @@ function initSuspenseListRenderState(
renderState.renderingStartTime = 0;
renderState.last = lastContentRow;
renderState.tail = tail;
renderState.tailExpiration = 0;
renderState.tailMode = tailMode;
renderState.lastEffect = lastEffectBeforeRendering;
}
49 changes: 31 additions & 18 deletions packages/react-reconciler/src/ReactFiberCompleteWork.new.js
Original file line number Diff line number Diff line change
@@ -137,9 +137,10 @@ import {
renderDidSuspendDelayIfPossible,
renderHasNotSuspendedYet,
popRenderLanes,
getRenderTargetTime,
} from './ReactFiberWorkLoop.new';
import {createFundamentalStateInstance} from './ReactFiberFundamental.new';
import {OffscreenLane} from './ReactFiberLane';
import {OffscreenLane, SomeRetryLane} from './ReactFiberLane';
import {resetChildFibers} from './ReactChildFiber.new';
import {createScopeInstance} from './ReactFiberScope.new';
import {transferActualDuration} from './ReactProfilerTimer.new';
@@ -1076,6 +1077,29 @@ function completeWork(
row = row.sibling;
}
}

if (renderState.tail !== null && now() > getRenderTargetTime()) {
// We have already passed our CPU deadline but we still have rows
// left in the tail. We'll just give up further attempts to render
// the main content and only render fallbacks.
workInProgress.effectTag |= DidCapture;
didSuspendAlready = true;

cutOffTailIfNeeded(renderState, false);

// Since nothing actually suspended, there will nothing to ping this
// to get it started back up to attempt the next item. While in terms
// of priority this work has the same priority as this current render,
// it's not part of the same transition once the transition has
// committed. If it's sync, we still want to yield so that it can be
// painted. Conceptually, this is really the same as pinging.
// We can use any RetryLane even if it's the one currently rendering
// since we're leaving it behind on this node.
workInProgress.lanes = SomeRetryLane;
if (enableSchedulerTracing) {
markSpawnedWork(SomeRetryLane);
}
}
} else {
cutOffTailIfNeeded(renderState, false);
}
@@ -1117,10 +1141,11 @@ function completeWork(
return null;
}
} else if (
// The time it took to render last row is greater than time until
// the expiration.
// The time it took to render last row is greater than the remaining
// time we have to render. So rendering one more row would likely
// exceed it.
now() * 2 - renderState.renderingStartTime >
renderState.tailExpiration &&
getRenderTargetTime() &&
renderLanes !== OffscreenLane
) {
// We have now passed our CPU deadline and we'll just give up further
@@ -1136,9 +1161,9 @@ function completeWork(
// them, then they really have the same priority as this render.
// So we'll pick it back up the very next render pass once we've had
// an opportunity to yield for paint.
workInProgress.lanes = renderLanes;
workInProgress.lanes = SomeRetryLane;
if (enableSchedulerTracing) {
markSpawnedWork(renderLanes);
markSpawnedWork(SomeRetryLane);
}
}
}
@@ -1163,18 +1188,6 @@ function completeWork(

if (renderState.tail !== null) {
// We still have tail rows to render.
if (renderState.tailExpiration === 0) {
// Heuristic for how long we're willing to spend rendering rows
// until we just give up and show what we have so far.
const TAIL_EXPIRATION_TIMEOUT_MS = 500;
renderState.tailExpiration = now() + TAIL_EXPIRATION_TIMEOUT_MS;
// TODO: This is meant to mimic the train model or JND but this
// is a per component value. It should really be since the start
// of the total render or last commit. Consider using something like
// globalMostRecentFallbackTime. That doesn't account for being
// suspended for part of the time or when it's a new render.
// It should probably use a global start time value instead.
}
// Pop a row.
const next = renderState.tail;
renderState.rendering = next;
60 changes: 38 additions & 22 deletions packages/react-reconciler/src/ReactFiberCompleteWork.old.js
Original file line number Diff line number Diff line change
@@ -135,9 +135,10 @@ import {
renderDidSuspendDelayIfPossible,
renderHasNotSuspendedYet,
popRenderLanes,
getRenderTargetTime,
} from './ReactFiberWorkLoop.old';
import {createFundamentalStateInstance} from './ReactFiberFundamental.old';
import {OffscreenLane} from './ReactFiberLane';
import {OffscreenLane, SomeRetryLane} from './ReactFiberLane';
import {resetChildFibers} from './ReactChildFiber.old';
import {createScopeInstance} from './ReactFiberScope.old';
import {transferActualDuration} from './ReactProfilerTimer.old';
@@ -1049,6 +1050,29 @@ function completeWork(
row = row.sibling;
}
}

if (renderState.tail !== null && now() > getRenderTargetTime()) {
// We have already passed our CPU deadline but we still have rows
// left in the tail. We'll just give up further attempts to render
// the main content and only render fallbacks.
workInProgress.effectTag |= DidCapture;
didSuspendAlready = true;

cutOffTailIfNeeded(renderState, false);

// Since nothing actually suspended, there will nothing to ping this
// to get it started back up to attempt the next item. While in terms
// of priority this work has the same priority as this current render,
// it's not part of the same transition once the transition has
// committed. If it's sync, we still want to yield so that it can be
// painted. Conceptually, this is really the same as pinging.
// We can use any RetryLane even if it's the one currently rendering
// since we're leaving it behind on this node.
workInProgress.lanes = SomeRetryLane;
if (enableSchedulerTracing) {
markSpawnedWork(SomeRetryLane);
}
}
} else {
cutOffTailIfNeeded(renderState, false);
}
@@ -1090,10 +1114,11 @@ function completeWork(
return null;
}
} else if (
// The time it took to render last row is greater than time until
// the expiration.
// The time it took to render last row is greater than the remaining
// time we have to render. So rendering one more row would likely
// exceed it.
now() * 2 - renderState.renderingStartTime >
renderState.tailExpiration &&
getRenderTargetTime() &&
renderLanes !== OffscreenLane
) {
// We have now passed our CPU deadline and we'll just give up further
@@ -1105,13 +1130,16 @@ function completeWork(
cutOffTailIfNeeded(renderState, false);

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

if (renderState.tail !== null) {
// We still have tail rows to render.
if (renderState.tailExpiration === 0) {
// Heuristic for how long we're willing to spend rendering rows
// until we just give up and show what we have so far.
const TAIL_EXPIRATION_TIMEOUT_MS = 500;
renderState.tailExpiration = now() + TAIL_EXPIRATION_TIMEOUT_MS;
// TODO: This is meant to mimic the train model or JND but this
// is a per component value. It should really be since the start
// of the total render or last commit. Consider using something like
// globalMostRecentFallbackTime. That doesn't account for being
// suspended for part of the time or when it's a new render.
// It should probably use a global start time value instead.
}
// Pop a row.
const next = renderState.tail;
renderState.rendering = next;
2 changes: 2 additions & 0 deletions packages/react-reconciler/src/ReactFiberLane.js
Original file line number Diff line number Diff line change
@@ -97,6 +97,8 @@ const TransitionLongLanes: Lanes = /* */ 0b0000000001111000000

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

export const SomeRetryLane: Lanes = /* */ 0b0000010000000000000000000000000;

export const SelectiveHydrationLane: Lane = /* */ 0b0000100000000000000000000000000;

const NonIdleLanes = /* */ 0b0000111111111111111111111111111;
Original file line number Diff line number Diff line change
@@ -41,14 +41,12 @@ export type SuspenseListRenderState = {|
isBackwards: boolean,
// The currently rendering tail row.
rendering: null | Fiber,
// The absolute time when we started rendering the tail row.
// The absolute time when we started rendering the most recent tail row.
renderingStartTime: number,
// The last of the already rendered children.
last: null | Fiber,
// Remaining rows on the tail of the list.
tail: null | Fiber,
// The absolute time in ms that we'll expire the tail rendering.
tailExpiration: number,
// Tail insertions setting.
tailMode: SuspenseListTailMode,
// Last Effect before we rendered the "rendering" item.
Original file line number Diff line number Diff line change
@@ -41,14 +41,12 @@ export type SuspenseListRenderState = {|
isBackwards: boolean,
// The currently rendering tail row.
rendering: null | Fiber,
// The absolute time when we started rendering the tail row.
// The absolute time when we started rendering the most recent tail row.
renderingStartTime: number,
// The last of the already rendered children.
last: null | Fiber,
// Remaining rows on the tail of the list.
tail: null | Fiber,
// The absolute time in ms that we'll expire the tail rendering.
tailExpiration: number,
// Tail insertions setting.
tailMode: SuspenseListTailMode,
// Last Effect before we rendered the "rendering" item.
25 changes: 25 additions & 0 deletions packages/react-reconciler/src/ReactFiberWorkLoop.new.js
Original file line number Diff line number Diff line change
@@ -328,6 +328,21 @@ let globalMostRecentFallbackTime: number = 0;
const FALLBACK_THROTTLE_MS: number = 500;
const DEFAULT_TIMEOUT_MS: number = 5000;

// The absolute time for when we should start giving up on rendering
// more and prefer CPU suspense heuristics instead.
let workInProgressRootRenderTargetTime: number = Infinity;
// How long a render is supposed to take before we start following CPU
// suspense heuristics and opt out of rendering more content.
const RENDER_TIMEOUT_MS = 500;

function resetRenderTimer() {
workInProgressRootRenderTargetTime = now() + RENDER_TIMEOUT_MS;
}

export function getRenderTargetTime(): number {
return workInProgressRootRenderTargetTime;
}

let hasUncaughtError = false;
let firstUncaughtError = null;
let legacyErrorBoundariesThatAlreadyFailed: Set<mixed> | null = null;
@@ -603,6 +618,7 @@ export function scheduleUpdateOnFiber(
// scheduleCallbackForFiber to preserve the ability to schedule a callback
// without immediately flushing it. We only do this for user-initiated
// updates, to preserve historical behavior of legacy mode.
resetRenderTimer();
flushSyncCallbackQueue();
}
}
@@ -1111,6 +1127,7 @@ export function flushRoot(root: FiberRoot, lanes: Lanes) {
markRootExpired(root, lanes);
ensureRootIsScheduled(root, now());
if ((executionContext & (RenderContext | CommitContext)) === NoContext) {
resetRenderTimer();
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe we could put this call inside flushSyncCallbackQueue? Since that's the entry point for synchronous work.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not all of them check for NoContext, so I was hesitant to do that without digging into why that is first.

flushSyncCallbackQueue();
}
}
@@ -1185,6 +1202,7 @@ export function batchedUpdates<A, R>(fn: A => R, a: A): R {
executionContext = prevExecutionContext;
if (executionContext === NoContext) {
// Flush the immediate callbacks that were scheduled during this batch
resetRenderTimer();
flushSyncCallbackQueue();
}
}
@@ -1199,6 +1217,7 @@ export function batchedEventUpdates<A, R>(fn: A => R, a: A): R {
executionContext = prevExecutionContext;
if (executionContext === NoContext) {
// Flush the immediate callbacks that were scheduled during this batch
resetRenderTimer();
flushSyncCallbackQueue();
}
}
@@ -1227,6 +1246,7 @@ export function discreteUpdates<A, B, C, D, R>(
executionContext = prevExecutionContext;
if (executionContext === NoContext) {
// Flush the immediate callbacks that were scheduled during this batch
resetRenderTimer();
flushSyncCallbackQueue();
}
}
@@ -1240,6 +1260,7 @@ export function discreteUpdates<A, B, C, D, R>(
executionContext = prevExecutionContext;
if (executionContext === NoContext) {
// Flush the immediate callbacks that were scheduled during this batch
resetRenderTimer();
flushSyncCallbackQueue();
}
}
@@ -1256,6 +1277,7 @@ export function unbatchedUpdates<A, R>(fn: (a: A) => R, a: A): R {
executionContext = prevExecutionContext;
if (executionContext === NoContext) {
// Flush the immediate callbacks that were scheduled during this batch
resetRenderTimer();
flushSyncCallbackQueue();
}
}
@@ -1323,6 +1345,7 @@ export function flushControlled(fn: () => mixed): void {
executionContext = prevExecutionContext;
if (executionContext === NoContext) {
// Flush the immediate callbacks that were scheduled during this batch
resetRenderTimer();
flushSyncCallbackQueue();
}
}
@@ -1333,6 +1356,7 @@ export function flushControlled(fn: () => mixed): void {
executionContext = prevExecutionContext;
if (executionContext === NoContext) {
// Flush the immediate callbacks that were scheduled during this batch
resetRenderTimer();
flushSyncCallbackQueue();
}
}
@@ -1651,6 +1675,7 @@ function renderRootConcurrent(root: FiberRoot, lanes: Lanes) {
// If the root or lanes have changed, throw out the existing stack
// and prepare a fresh one. Otherwise we'll continue where we left off.
if (workInProgressRoot !== root || workInProgressRootRenderLanes !== lanes) {
resetRenderTimer();
prepareFreshStack(root, lanes);
startWorkOnPendingInteractions(root, lanes);
}
25 changes: 25 additions & 0 deletions packages/react-reconciler/src/ReactFiberWorkLoop.old.js
Original file line number Diff line number Diff line change
@@ -312,6 +312,21 @@ let globalMostRecentFallbackTime: number = 0;
const FALLBACK_THROTTLE_MS: number = 500;
const DEFAULT_TIMEOUT_MS: number = 5000;

// The absolute time for when we should start giving up on rendering
// more and prefer CPU suspense heuristics instead.
let workInProgressRootRenderTargetTime: number = Infinity;
// How long a render is supposed to take before we start following CPU
// suspense heuristics and opt out of rendering more content.
const RENDER_TIMEOUT_MS = 500;

function resetRenderTimer() {
workInProgressRootRenderTargetTime = now() + RENDER_TIMEOUT_MS;
}

export function getRenderTargetTime(): number {
return workInProgressRootRenderTargetTime;
}

let nextEffect: Fiber | null = null;
let hasUncaughtError = false;
let firstUncaughtError = null;
@@ -590,6 +605,7 @@ export function scheduleUpdateOnFiber(
// scheduleCallbackForFiber to preserve the ability to schedule a callback
// without immediately flushing it. We only do this for user-initiated
// updates, to preserve historical behavior of legacy mode.
resetRenderTimer();
flushSyncCallbackQueue();
}
}
@@ -1098,6 +1114,7 @@ export function flushRoot(root: FiberRoot, lanes: Lanes) {
markRootExpired(root, lanes);
ensureRootIsScheduled(root, now());
if ((executionContext & (RenderContext | CommitContext)) === NoContext) {
resetRenderTimer();
flushSyncCallbackQueue();
}
}
@@ -1172,6 +1189,7 @@ export function batchedUpdates<A, R>(fn: A => R, a: A): R {
executionContext = prevExecutionContext;
if (executionContext === NoContext) {
// Flush the immediate callbacks that were scheduled during this batch
resetRenderTimer();
flushSyncCallbackQueue();
}
}
@@ -1186,6 +1204,7 @@ export function batchedEventUpdates<A, R>(fn: A => R, a: A): R {
executionContext = prevExecutionContext;
if (executionContext === NoContext) {
// Flush the immediate callbacks that were scheduled during this batch
resetRenderTimer();
flushSyncCallbackQueue();
}
}
@@ -1214,6 +1233,7 @@ export function discreteUpdates<A, B, C, D, R>(
executionContext = prevExecutionContext;
if (executionContext === NoContext) {
// Flush the immediate callbacks that were scheduled during this batch
resetRenderTimer();
flushSyncCallbackQueue();
}
}
@@ -1227,6 +1247,7 @@ export function discreteUpdates<A, B, C, D, R>(
executionContext = prevExecutionContext;
if (executionContext === NoContext) {
// Flush the immediate callbacks that were scheduled during this batch
resetRenderTimer();
flushSyncCallbackQueue();
}
}
@@ -1243,6 +1264,7 @@ export function unbatchedUpdates<A, R>(fn: (a: A) => R, a: A): R {
executionContext = prevExecutionContext;
if (executionContext === NoContext) {
// Flush the immediate callbacks that were scheduled during this batch
resetRenderTimer();
flushSyncCallbackQueue();
}
}
@@ -1310,6 +1332,7 @@ export function flushControlled(fn: () => mixed): void {
executionContext = prevExecutionContext;
if (executionContext === NoContext) {
// Flush the immediate callbacks that were scheduled during this batch
resetRenderTimer();
flushSyncCallbackQueue();
}
}
@@ -1320,6 +1343,7 @@ export function flushControlled(fn: () => mixed): void {
executionContext = prevExecutionContext;
if (executionContext === NoContext) {
// Flush the immediate callbacks that were scheduled during this batch
resetRenderTimer();
flushSyncCallbackQueue();
}
}
@@ -1638,6 +1662,7 @@ function renderRootConcurrent(root: FiberRoot, lanes: Lanes) {
// If the root or lanes have changed, throw out the existing stack
// and prepare a fresh one. Otherwise we'll continue where we left off.
if (workInProgressRoot !== root || workInProgressRootRenderLanes !== lanes) {
resetRenderTimer();
prepareFreshStack(root, lanes);
startWorkOnPendingInteractions(root, lanes);
}
141 changes: 141 additions & 0 deletions packages/react-reconciler/src/__tests__/ReactSuspenseList-test.js
Original file line number Diff line number Diff line change
@@ -2556,6 +2556,147 @@ describe('ReactSuspenseList', () => {
);
});

// @gate experimental
it('should be able to progressively show CPU expensive rows with two pass rendering', async () => {
function TwoPass({text}) {
const [pass, setPass] = React.useState(0);
React.useLayoutEffect(() => {
Scheduler.unstable_yieldValue('Mount ' + text);
setPass(1);
}, []);
return <Text text={pass === 0 ? 'First Pass ' + text : text} />;
}

function Sleep({time, children}) {
Scheduler.unstable_advanceTime(time);
return children;
}

function App() {
Scheduler.unstable_yieldValue('App');
return (
<SuspenseList revealOrder="forwards" tail="hidden">
<Suspense fallback={<Text text="Loading A" />}>
<Sleep time={600}>
<TwoPass text="A" />
</Sleep>
</Suspense>
<Suspense fallback={<Text text="Loading B" />}>
<Sleep time={600}>
<TwoPass text="B" />
</Sleep>
</Suspense>
<Sleep time={600}>
<Text text="C" />
</Sleep>
</SuspenseList>
);
}

ReactNoop.render(<App />);

expect(Scheduler).toFlushAndYieldThrough([
'App',
'First Pass A',
'Mount A',
'A',
]);
expect(ReactNoop).toMatchRenderedOutput(<span>A</span>);

expect(Scheduler).toFlushAndYieldThrough(['First Pass B', 'Mount B', 'B']);
expect(ReactNoop).toMatchRenderedOutput(
<>
<span>A</span>
<span>B</span>
</>,
);

expect(Scheduler).toFlushAndYield(['C']);
expect(ReactNoop).toMatchRenderedOutput(
<>
<span>A</span>
<span>B</span>
<span>C</span>
</>,
);
});

// @gate experimental
it('should be able to progressively show rows with two pass rendering and visible', async () => {
function TwoPass({text}) {
const [pass, setPass] = React.useState(0);
React.useLayoutEffect(() => {
Scheduler.unstable_yieldValue('Mount ' + text);
setPass(1);
}, []);
return <Text text={pass === 0 ? 'First Pass ' + text : text} />;
}

function Sleep({time, children}) {
Scheduler.unstable_advanceTime(time);
return children;
}

function App() {
Scheduler.unstable_yieldValue('App');
return (
<SuspenseList revealOrder="forwards">
<Suspense fallback={<Text text="Loading A" />}>
<Sleep time={600}>
<TwoPass text="A" />
</Sleep>
</Suspense>
<Suspense fallback={<Text text="Loading B" />}>
<Sleep time={600}>
<TwoPass text="B" />
</Sleep>
</Suspense>
<Suspense fallback={<Text text="Loading C" />}>
<Sleep time={600}>
<Text text="C" />
</Sleep>
</Suspense>
</SuspenseList>
);
}

ReactNoop.render(<App />);

expect(Scheduler).toFlushAndYieldThrough([
'App',
'First Pass A',
'Loading B',
'Loading C',
'Mount A',
'A',
]);
expect(ReactNoop).toMatchRenderedOutput(
<>
<span>A</span>
<span>Loading B</span>
<span>Loading C</span>
</>,
);

expect(Scheduler).toFlushAndYieldThrough(['First Pass B', 'Mount B', 'B']);
expect(ReactNoop).toMatchRenderedOutput(
<>
<span>A</span>
<span>B</span>
<span>Loading C</span>
</>,
);

expect(Scheduler).toFlushAndYield(['C']);
expect(ReactNoop).toMatchRenderedOutput(
<>
<span>A</span>
<span>B</span>
<span>C</span>
</>,
);
});

// @gate experimental && enableProfilerTimer
it('counts the actual duration when profiling a SuspenseList', async () => {
// Order of parameters: id, phase, actualDuration, treeBaseDuration
Original file line number Diff line number Diff line change
@@ -294,6 +294,7 @@ describe('Profiler', () => {
'read current time',
'read current time',
'read current time',
'read current time',
]);

// Restore original mock