Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Schedule prerender after something suspends #30800

Merged
merged 1 commit into from
Sep 4, 2024
Merged
Show file tree
Hide file tree
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
25 changes: 19 additions & 6 deletions packages/react-cache/src/__tests__/ReactCacheOld-test.internal.js
Original file line number Diff line number Diff line change
Expand Up @@ -148,7 +148,15 @@ describe('ReactCache', () => {
error = e;
}
expect(error.message).toMatch('Failed to load: Hi');
assertLog(['Promise rejected [Hi]', 'Error! [Hi]', 'Error! [Hi]']);
assertLog([
'Promise rejected [Hi]',
'Error! [Hi]',
'Error! [Hi]',

...(gate('enableSiblingPrerendering')
? ['Error! [Hi]', 'Error! [Hi]']
: []),
]);

// Should throw again on a subsequent read
root.render(<App />);
Expand Down Expand Up @@ -191,6 +199,7 @@ describe('ReactCache', () => {
}
});

// @gate enableSiblingPrerendering
Copy link
Collaborator

Choose a reason for hiding this comment

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

Shouldn't this be inlined into a different set of assertions?

it('evicts least recently used values', async () => {
ReactCache.unstable_setGlobalCacheLimit(3);

Expand All @@ -206,15 +215,13 @@ describe('ReactCache', () => {
await waitForAll(['Suspend! [1]', 'Loading...']);
jest.advanceTimersByTime(100);
assertLog(['Promise resolved [1]']);
await waitForAll([1, 'Suspend! [2]']);
await waitForAll([1, 'Suspend! [2]', 1, 'Suspend! [2]', 'Suspend! [3]']);

jest.advanceTimersByTime(100);
assertLog(['Promise resolved [2]']);
await waitForAll([1, 2, 'Suspend! [3]']);
assertLog(['Promise resolved [2]', 'Promise resolved [3]']);
await waitForAll([1, 2, 3]);

await act(() => jest.advanceTimersByTime(100));
assertLog(['Promise resolved [3]', 1, 2, 3]);

expect(root).toMatchRenderedOutput('123');

// Render 1, 4, 5
Expand All @@ -234,6 +241,9 @@ describe('ReactCache', () => {
1,
4,
'Suspend! [5]',
1,
4,
'Suspend! [5]',
'Promise resolved [5]',
1,
4,
Expand Down Expand Up @@ -267,6 +277,9 @@ describe('ReactCache', () => {
1,
2,
'Suspend! [3]',
1,
2,
'Suspend! [3]',
'Promise resolved [3]',
1,
2,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -744,7 +744,7 @@ describe('ReactDOMFiberAsync', () => {
// Because it suspended, it remains on the current path
expect(div.textContent).toBe('/path/a');
});
assertLog([]);
assertLog(gate('enableSiblingPrerendering') ? ['Suspend! [/path/b]'] : []);

await act(async () => {
resolvePromise();
Expand Down
61 changes: 54 additions & 7 deletions packages/react-dom/src/__tests__/ReactDOMForm-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -699,7 +699,15 @@ describe('ReactDOMForm', () => {
// This should suspend because form actions are implicitly wrapped
// in startTransition.
await submit(formRef.current);
assertLog(['Pending...', 'Suspend! [Updated]', 'Loading...']);
assertLog([
'Pending...',
'Suspend! [Updated]',
'Loading...',

...(gate('enableSiblingPrerendering')
? ['Suspend! [Updated]', 'Loading...']
: []),
]);
expect(container.textContent).toBe('Pending...Initial');

await act(() => resolveText('Updated'));
Expand Down Expand Up @@ -736,7 +744,15 @@ describe('ReactDOMForm', () => {

// Update
await submit(formRef.current);
assertLog(['Pending...', 'Suspend! [Count: 1]', 'Loading...']);
assertLog([
'Pending...',
'Suspend! [Count: 1]',
'Loading...',

...(gate('enableSiblingPrerendering')
? ['Suspend! [Count: 1]', 'Loading...']
: []),
]);
expect(container.textContent).toBe('Pending...Count: 0');

await act(() => resolveText('Count: 1'));
Expand All @@ -745,7 +761,15 @@ describe('ReactDOMForm', () => {

// Update again
await submit(formRef.current);
assertLog(['Pending...', 'Suspend! [Count: 2]', 'Loading...']);
assertLog([
'Pending...',
'Suspend! [Count: 2]',
'Loading...',

...(gate('enableSiblingPrerendering')
? ['Suspend! [Count: 2]', 'Loading...']
: []),
]);
expect(container.textContent).toBe('Pending...Count: 1');

await act(() => resolveText('Count: 2'));
Expand Down Expand Up @@ -789,7 +813,14 @@ describe('ReactDOMForm', () => {
assertLog(['Async action started', 'Pending...']);

await act(() => resolveText('Wait'));
assertLog(['Suspend! [Updated]', 'Loading...']);
assertLog([
'Suspend! [Updated]',
'Loading...',

...(gate('enableSiblingPrerendering')
? ['Suspend! [Updated]', 'Loading...']
: []),
]);
expect(container.textContent).toBe('Pending...Initial');

await act(() => resolveText('Updated'));
Expand Down Expand Up @@ -1475,7 +1506,15 @@ describe('ReactDOMForm', () => {
// Now dispatch inside of a transition. This one does not trigger a
// loading state.
await act(() => startTransition(() => dispatch()));
assertLog(['Count: 1', 'Suspend! [Count: 2]', 'Loading...']);
assertLog([
'Count: 1',
'Suspend! [Count: 2]',
'Loading...',

...(gate('enableSiblingPrerendering')
? ['Suspend! [Count: 2]', 'Loading...']
: []),
]);
expect(container.textContent).toBe('Count: 1');

await act(() => resolveText('Count: 2'));
Expand All @@ -1495,7 +1534,11 @@ describe('ReactDOMForm', () => {

const root = ReactDOMClient.createRoot(container);
await act(() => root.render(<App />));
assertLog(['Suspend! [Count: 0]']);
assertLog([
'Suspend! [Count: 0]',

...(gate('enableSiblingPrerendering') ? ['Suspend! [Count: 0]'] : []),
]);
await act(() => resolveText('Count: 0'));
assertLog(['Count: 0']);

Expand All @@ -1508,7 +1551,11 @@ describe('ReactDOMForm', () => {
{withoutStack: true},
],
]);
assertLog(['Suspend! [Count: 1]']);
assertLog([
'Suspend! [Count: 1]',

...(gate('enableSiblingPrerendering') ? ['Suspend! [Count: 1]'] : []),
]);
expect(container.textContent).toBe('Count: 0');
});

Expand Down
50 changes: 50 additions & 0 deletions packages/react-reconciler/src/ReactFiberLane.js
Original file line number Diff line number Diff line change
Expand Up @@ -229,28 +229,49 @@ export function getNextLanes(root: FiberRoot, wipLanes: Lanes): Lanes {

const suspendedLanes = root.suspendedLanes;
const pingedLanes = root.pingedLanes;
const warmLanes = root.warmLanes;

// Do not work on any idle work until all the non-idle work has finished,
// even if the work is suspended.
const nonIdlePendingLanes = pendingLanes & NonIdleLanes;
if (nonIdlePendingLanes !== NoLanes) {
// First check for fresh updates.
const nonIdleUnblockedLanes = nonIdlePendingLanes & ~suspendedLanes;
if (nonIdleUnblockedLanes !== NoLanes) {
nextLanes = getHighestPriorityLanes(nonIdleUnblockedLanes);
} else {
// No fresh updates. Check if suspended work has been pinged.
const nonIdlePingedLanes = nonIdlePendingLanes & pingedLanes;
if (nonIdlePingedLanes !== NoLanes) {
nextLanes = getHighestPriorityLanes(nonIdlePingedLanes);
} else {
// Nothing has been pinged. Check for lanes that need to be prewarmed.
const lanesToPrewarm = nonIdlePendingLanes & ~warmLanes;
if (lanesToPrewarm !== NoLanes) {
nextLanes = getHighestPriorityLanes(lanesToPrewarm);
}
}
}
} else {
// The only remaining work is Idle.
// TODO: Idle isn't really used anywhere, and the thinking around
// speculative rendering has evolved since this was implemented. Consider
// removing until we've thought about this again.

// First check for fresh updates.
const unblockedLanes = pendingLanes & ~suspendedLanes;
if (unblockedLanes !== NoLanes) {
nextLanes = getHighestPriorityLanes(unblockedLanes);
} else {
// No fresh updates. Check if suspended work has been pinged.
if (pingedLanes !== NoLanes) {
nextLanes = getHighestPriorityLanes(pingedLanes);
} else {
// Nothing has been pinged. Check for lanes that need to be prewarmed.
const lanesToPrewarm = pendingLanes & ~warmLanes;
eps1lon marked this conversation as resolved.
Show resolved Hide resolved
if (lanesToPrewarm !== NoLanes) {
nextLanes = getHighestPriorityLanes(lanesToPrewarm);
}
}
}
}
Expand Down Expand Up @@ -335,6 +356,21 @@ export function getNextLanesToFlushSync(
return NoLanes;
}

export function checkIfRootIsPrerendering(
root: FiberRoot,
renderLanes: Lanes,
): boolean {
const pendingLanes = root.pendingLanes;
const suspendedLanes = root.suspendedLanes;
const pingedLanes = root.pingedLanes;
// Remove lanes that are suspended (but not pinged)
const unblockedLanes = pendingLanes & ~(suspendedLanes & ~pingedLanes);

// If there are no unsuspended or pinged lanes, that implies that we're
// performing a prerender.
return (unblockedLanes & renderLanes) === 0;
}

export function getEntangledLanes(root: FiberRoot, renderLanes: Lanes): Lanes {
let entangledLanes = renderLanes;

Expand Down Expand Up @@ -670,17 +706,27 @@ export function markRootUpdated(root: FiberRoot, updateLane: Lane) {
if (updateLane !== IdleLane) {
root.suspendedLanes = NoLanes;
root.pingedLanes = NoLanes;
root.warmLanes = NoLanes;
}
}

export function markRootSuspended(
root: FiberRoot,
suspendedLanes: Lanes,
spawnedLane: Lane,
didSkipSuspendedSiblings: boolean,
) {
root.suspendedLanes |= suspendedLanes;
root.pingedLanes &= ~suspendedLanes;

if (!didSkipSuspendedSiblings) {
// Mark these lanes as warm so we know there's nothing else to work on.
root.warmLanes |= suspendedLanes;
} else {
// Render unwound without attempting all the siblings. Do no mark the lanes
// as warm. This will cause a prewarm render to be scheduled.
}

// The suspended lanes are no longer CPU-bound. Clear their expiration times.
const expirationTimes = root.expirationTimes;
let lanes = suspendedLanes;
Expand All @@ -700,6 +746,9 @@ export function markRootSuspended(

export function markRootPinged(root: FiberRoot, pingedLanes: Lanes) {
root.pingedLanes |= root.suspendedLanes & pingedLanes;
// The data that just resolved could have unblocked additional children, which
// will also need to be prewarmed if something suspends again.
root.warmLanes &= ~pingedLanes;
}

export function markRootFinished(
Expand All @@ -714,6 +763,7 @@ export function markRootFinished(
// Let's try everything again
root.suspendedLanes = NoLanes;
root.pingedLanes = NoLanes;
root.warmLanes = NoLanes;

root.expiredLanes &= remainingLanes;

Expand Down
1 change: 1 addition & 0 deletions packages/react-reconciler/src/ReactFiberRoot.js
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,7 @@ function FiberRootNode(
this.pendingLanes = NoLanes;
this.suspendedLanes = NoLanes;
this.pingedLanes = NoLanes;
this.warmLanes = NoLanes;
this.expiredLanes = NoLanes;
this.finishedLanes = NoLanes;
this.errorRecoveryDisabledLanes = NoLanes;
Expand Down
Loading
Loading