Skip to content

Commit 05ca66a

Browse files
authored
[Fizz] model fb bundle's external work scheduling explicitly (react#36576)
There are parts of Fizz that need to schedule work regardless of whether the primary rendering pathway is drive externally through performWork. Historically scheduleWork and later s cheduleMicrotask were noops in this bundle but it makes it hard to reason about the code because you cannot be assured that calling scheduleWork will actually result in the function ever executing. Now we model this explicitly through config. For builds that drive work through external calls to performaWork we simply omit any work scheduling in startWork or pi ngTask. Now that this is modeled explicitly we can implement scheduleMicrotask and scheduleWork to actually provide a guarantee that the callbacks will get invoked. For now I've implemented these two for the fb build as synchronous however it is likely that queueMicrotask and setTimeout or similar are preferred.
1 parent f39ed9f commit 05ca66a

11 files changed

Lines changed: 91 additions & 46 deletions

packages/react-server/src/ReactFizzServer.js

Lines changed: 49 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -88,6 +88,7 @@ import {
8888
hoistHoistables,
8989
createHoistableState,
9090
createPreambleState,
91+
isWorkLoopExternallyDriven,
9192
supportsRequestStorage,
9293
requestStorage,
9394
pushFormStateMarkerIsMatching,
@@ -527,7 +528,7 @@ function RequestInstance(
527528
progressiveChunkSize === undefined
528529
? DEFAULT_PROGRESSIVE_CHUNK_SIZE
529530
: progressiveChunkSize;
530-
this.status = OPENING;
531+
this.status = isWorkLoopExternallyDriven ? OPEN : OPENING;
531532
this.fatalError = null;
532533
this.nextSegmentId = 0;
533534
this.allPendingTasks = 0;
@@ -790,12 +791,16 @@ export function resolveRequest(): null | Request {
790791
function pingTask(request: Request, task: Task): void {
791792
const pingedTasks = request.pingedTasks;
792793
pingedTasks.push(task);
793-
if (request.pingedTasks.length === 1) {
794-
request.flushScheduled = request.destination !== null;
795-
if (request.trackedPostpones !== null || request.status === OPENING) {
796-
scheduleMicrotask(() => performWork(request));
797-
} else {
798-
scheduleWork(() => performWork(request));
794+
if (isWorkLoopExternallyDriven) {
795+
return;
796+
} else {
797+
if (request.pingedTasks.length === 1) {
798+
request.flushScheduled = request.destination !== null;
799+
if (request.trackedPostpones !== null || request.status === OPENING) {
800+
scheduleMicrotask(() => performWork(request));
801+
} else {
802+
scheduleWork(() => performWork(request));
803+
}
799804
}
800805
}
801806
}
@@ -6030,39 +6035,45 @@ function flushCompletedQueues(
60306035
}
60316036

60326037
export function startWork(request: Request): void {
6033-
request.flushScheduled = request.destination !== null;
6034-
// When prerendering we use microtasks for pinging work
6035-
if (supportsRequestStorage) {
6036-
scheduleMicrotask(() => requestStorage.run(request, performWork, request));
6038+
if (isWorkLoopExternallyDriven) {
6039+
return;
60376040
} else {
6038-
scheduleMicrotask(() => performWork(request));
6039-
}
6040-
scheduleWork(() => {
6041-
if (request.status === OPENING) {
6042-
request.status = OPEN;
6043-
}
6044-
6045-
if (request.trackedPostpones === null) {
6046-
// this is either a regular render or a resume. For regular render we want
6047-
// to call emitEarlyPreloads after the first performWork because we want
6048-
// are responding to a live request and need to balance sending something early
6049-
// (i.e. don't want for the shell to finish) but we need something to send.
6050-
// The only implementation of this is for DOM at the moment and during resumes nothing
6051-
// actually emits but the code paths here are the same.
6052-
// During a prerender we don't want to be too aggressive in emitting early preloads
6053-
// because we aren't responding to a live request and we can wait for the prerender to
6054-
// postpone before we emit anything.
6055-
if (supportsRequestStorage) {
6056-
requestStorage.run(
6057-
request,
6058-
enqueueEarlyPreloadsAfterInitialWork,
6059-
request,
6060-
);
6061-
} else {
6062-
enqueueEarlyPreloadsAfterInitialWork(request);
6063-
}
6041+
request.flushScheduled = request.destination !== null;
6042+
// When prerendering we use microtasks for pinging work
6043+
if (supportsRequestStorage) {
6044+
scheduleMicrotask(() =>
6045+
requestStorage.run(request, performWork, request),
6046+
);
6047+
} else {
6048+
scheduleMicrotask(() => performWork(request));
60646049
}
6065-
});
6050+
scheduleWork(() => {
6051+
if (request.status === OPENING) {
6052+
request.status = OPEN;
6053+
}
6054+
6055+
if (request.trackedPostpones === null) {
6056+
// this is either a regular render or a resume. For regular render we want
6057+
// to call emitEarlyPreloads after the first performWork because we want
6058+
// are responding to a live request and need to balance sending something early
6059+
// (i.e. don't want for the shell to finish) but we need something to send.
6060+
// The only implementation of this is for DOM at the moment and during resumes nothing
6061+
// actually emits but the code paths here are the same.
6062+
// During a prerender we don't want to be too aggressive in emitting early preloads
6063+
// because we aren't responding to a live request and we can wait for the prerender to
6064+
// postpone before we emit anything.
6065+
if (supportsRequestStorage) {
6066+
requestStorage.run(
6067+
request,
6068+
enqueueEarlyPreloadsAfterInitialWork,
6069+
request,
6070+
);
6071+
} else {
6072+
enqueueEarlyPreloadsAfterInitialWork(request);
6073+
}
6074+
}
6075+
});
6076+
}
60666077
}
60676078

60686079
function enqueueEarlyPreloadsAfterInitialWork(request: Request) {

packages/react-server/src/ReactServerStreamConfigFB.js

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,18 @@ export opaque type PrecomputedChunk = string;
1818
export opaque type Chunk = string;
1919
export opaque type BinaryChunk = string;
2020

21+
export function scheduleMicrotask(callback: () => void) {
22+
// TODO: Consider unifying this with the FB Flight stream config and
23+
// adopting its microtask scheduling implementation.
24+
callback();
25+
}
26+
27+
export function scheduleWork(callback: () => void) {
28+
// TODO: Consider unifying this with the FB Flight stream config and
29+
// adopting its work scheduling implementation.
30+
callback();
31+
}
32+
2133
export function flushBuffered(destination: Destination) {}
2234

2335
export const supportsRequestStorage = false;

packages/react-server/src/forks/ReactFizzConfig.custom.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,8 @@ export const isPrimaryRenderer = false;
4040

4141
export const supportsClientAPIs = true;
4242

43+
export const isWorkLoopExternallyDriven =
44+
$$$config.isWorkLoopExternallyDriven === true;
4345
export const supportsRequestStorage = false;
4446
export const requestStorage: AsyncLocalStorage<Request | void> = (null: any);
4547

packages/react-server/src/forks/ReactFizzConfig.dom-edge.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,8 @@ export * from 'react-dom-bindings/src/server/ReactFizzConfigDOM';
1212

1313
export * from 'react-client/src/ReactClientConsoleConfigServer';
1414

15+
export const isWorkLoopExternallyDriven = false;
16+
1517
// For now, we get this from the global scope, but this will likely move to a module.
1618
export const supportsRequestStorage = typeof AsyncLocalStorage === 'function';
1719
export const requestStorage: AsyncLocalStorage<Request | void> =
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
/**
2+
* Copyright (c) Meta Platforms, Inc. and affiliates.
3+
*
4+
* This source code is licensed under the MIT license found in the
5+
* LICENSE file in the root directory of this source tree.
6+
*
7+
* @flow
8+
*/
9+
import type {Request} from 'react-server/src/ReactFizzServer';
10+
11+
export * from 'react-dom-bindings/src/server/ReactFizzConfigDOM';
12+
13+
export * from 'react-client/src/ReactClientConsoleConfigBrowser';
14+
15+
// This renderer is pulled from the outside through renderNextChunk. Promises
16+
// can ping tasks, but the outer caller decides when to process them.
17+
export const isWorkLoopExternallyDriven = true;
18+
19+
export const supportsRequestStorage = false;
20+
export const requestStorage: AsyncLocalStorage<Request | void> = (null: any);

packages/react-server/src/forks/ReactFizzConfig.dom-legacy.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,5 +12,6 @@ export * from 'react-dom-bindings/src/server/ReactFizzConfigDOMLegacy';
1212

1313
export * from 'react-client/src/ReactClientConsoleConfigPlain';
1414

15+
export const isWorkLoopExternallyDriven = false;
1516
export const supportsRequestStorage = false;
1617
export const requestStorage: AsyncLocalStorage<Request | void> = (null: any);

packages/react-server/src/forks/ReactFizzConfig.dom-node.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ export * from 'react-dom-bindings/src/server/ReactFizzConfigDOM';
1515

1616
export * from 'react-client/src/ReactClientConsoleConfigServer';
1717

18+
export const isWorkLoopExternallyDriven = false;
1819
export const supportsRequestStorage = true;
1920
export const requestStorage: AsyncLocalStorage<Request | void> =
2021
new AsyncLocalStorage();

packages/react-server/src/forks/ReactFizzConfig.dom.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,5 +12,6 @@ export * from 'react-dom-bindings/src/server/ReactFizzConfigDOM';
1212

1313
export * from 'react-client/src/ReactClientConsoleConfigBrowser';
1414

15+
export const isWorkLoopExternallyDriven = false;
1516
export const supportsRequestStorage = false;
1617
export const requestStorage: AsyncLocalStorage<Request | void> = (null: any);

packages/react-server/src/forks/ReactFizzConfig.markup.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,5 +12,6 @@ export * from 'react-markup/src/ReactFizzConfigMarkup.js';
1212

1313
export * from 'react-client/src/ReactClientConsoleConfigPlain';
1414

15+
export const isWorkLoopExternallyDriven = false;
1516
export const supportsRequestStorage = false;
1617
export const requestStorage: AsyncLocalStorage<Request | void> = (null: any);

packages/react-server/src/forks/ReactFizzConfig.noop.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,8 @@ export const isPrimaryRenderer = false;
4040

4141
export const supportsClientAPIs = true;
4242

43+
export const isWorkLoopExternallyDriven =
44+
$$$config.isWorkLoopExternallyDriven === true;
4345
export const supportsRequestStorage = false;
4446
export const requestStorage: AsyncLocalStorage<Request | void> = (null: any);
4547

0 commit comments

Comments
 (0)