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

[Flight][Fizz] schedule work async #29551

Merged
merged 1 commit into from
Jun 6, 2024
Merged

Conversation

gnoff
Copy link
Collaborator

@gnoff gnoff commented May 23, 2024

While most builds of Flight and Fizz schedule work in new tasks some do execute work synchronously. While this is necessary for legacy APIs like renderToString for modern APIs there really isn't a great reason to do this synchronously.

We could schedule works as microtasks but we actually want to yield so the runtime can run events and other things that will unblock additional work before starting the next work loop.

This change updates all non-legacy uses to be async using the best availalble macrotask scheduler.

Browser now uses postMessage
Bun uses setTimeout because while it also supports setImmediate the scheduling is not as eager as the same API in node
the FB build also uses setTimeout

This change required a number of changes to tests which were utilizing the sync nature of work in the Browser builds to avoid having to manage timers and tasks. I added a patch to install MessageChannel which is required by the browser builds and made this patched version integrate with the Scheduler mock. This way we can effectively use act to flush flight and fizz work similar to how we do this on the client.

Copy link

vercel bot commented May 23, 2024

The latest updates on your projects. Learn more about Vercel for Git ↗︎

Name Status Preview Comments Updated (UTC)
react-compiler-playground ✅ Ready (Inspect) Visit Preview 💬 Add feedback Jun 4, 2024 8:32pm

@@ -22,7 +22,7 @@ export opaque type Chunk = string;
export type BinaryChunk = $ArrayBufferView;

export function scheduleWork(callback: () => void) {
callback();
setImmediate(callback);
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

@Jarred-Sumner not sure why this was originally made to be sync but generally we'd want to schedule work after microtasks have had a chance to run. I believe Bun's support for setImmediate makes this the best option in that we don't want to throttle the task but lmk if there is a better option for Bun that I'm unaware of

Copy link
Contributor

Choose a reason for hiding this comment

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

We kind of should add another one here. Basically, you can use setImmediate, but it only runs after all the other tasks are complete - and that's tasks, not microtasks.

https://github.com/oven-sh/bun/blob/7f1880cafb21628fdfda5387ff897a9693845ead/src/bun.js/event_loop.zig#L1340-L1378

What if you did process.nextTick? Microtasks are drained after each one of those, and unlike setImmediate it doesn't have a dependency on the (async) event loop.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

interesting, my understanding is Node schedules setImmediate and setTimeout(f, 0) simiarly but we use setImmediate because it isn't throttled like setTimeout may be. In Bun it seems like setImmediate is actually lower priority than setTimeout(f, 0). We can use that for now however if it is also throttled that's not great either since it puts an upper bound on iterations and it unnecessarily delays work start.

We can't use microtasks here because we are going to add a new scheduling primitive for microtasks and we need to be able to ensure some work is scheduled in a macrotask to ensure the microtask work is flushed first.

Would postMessage be better? that's what we're going to use for the browser

Copy link
Contributor

Choose a reason for hiding this comment

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

Microtasks are drained after each process.nextTick runs in both node & bun.

The code is essentially (with some try catch and promise rejection handling and async loca storage)

while (tick = next()) {
  tick();
  drainMicrotasks()
}

I think it’s likely you want nextTick in bun and node, or to override nextTick to make yours have higher priority

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Well actually we want it to be a lower priority. If two fetches are resolving we'd prefer if both resolved before we restarted work otherwise we might stall on the second one. So we want a low priority task, we just don't want it to be artificially delayed. I maybe misinterpreted your earlier comment but my understanding is that in Node setImmediate will usually run before setTimeout(..., 0). In Bun is this true too? I wouldn't want it to run later than setTimeout(,0) but I also don't want it to run before IO tasks have had a chance to run which should be prioritized. The concern with setTimeout is that if there are many they may be throttled in some environments

@@ -43,7 +43,7 @@ export interface Destination {
}

export function scheduleWork(callback: () => void) {
callback();
setTimeout(callback, 0);
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

@alunyov I don't know enough about internal FB infra to know whether setImmediate is available all the time or ever. setTimeout is probably not the best choice here but I don't know if we should feature detect setImmediate, use postMessage, or just use setTimeout. Lmk what makes sense for Meta

Copy link
Collaborator

Choose a reason for hiding this comment

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

Currently there is none I believe. There’s no async I/O that can be scheduled from JS. Only called into.

So I believe resolving a Promise is the only available for now.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

updated

export const scheduleWork: (callback: () => void) => void =
LocalMessageChannel !== undefined
? (callback: () => void) => {
const channel = new LocalMessageChannel();
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

MessageChannel support goes back quite far. I wonder if we should just assume it

Copy link
Collaborator

Choose a reason for hiding this comment

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

Yea probably fine to assume and maybe encourages not abusing this build for other environments. Although we currently abuse it by using it in Deno. https://github.com/facebook/react/blob/main/packages/react-dom/package.json#L64

You don't need to create a new MessageChannel each time. Can use the same one and just post a new message to it like we do in the scheduler.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

the only safe way I think we can do that is to use a queue

@gnoff gnoff requested review from alunyov and sebmarkbage May 23, 2024 22:01
@react-sizebot
Copy link

react-sizebot commented May 23, 2024

Comparing: def67b9...1f062b4

Critical size changes

Includes critical production bundles, as well as any change greater than 2%:

Name +/- Base Current +/- gzip Base gzip Current gzip
oss-stable/react-dom/cjs/react-dom.production.js = 6.66 kB 6.66 kB +0.05% 1.82 kB 1.82 kB
oss-stable/react-dom/cjs/react-dom-client.production.js = 496.28 kB 496.28 kB = 88.92 kB 88.92 kB
oss-experimental/react-dom/cjs/react-dom.production.js = 6.67 kB 6.67 kB +0.05% 1.83 kB 1.83 kB
oss-experimental/react-dom/cjs/react-dom-client.production.js = 501.10 kB 501.10 kB = 89.61 kB 89.61 kB
facebook-www/ReactDOM-prod.classic.js = 593.77 kB 593.77 kB = 104.52 kB 104.52 kB
facebook-www/ReactDOM-prod.modern.js = 570.16 kB 570.16 kB = 100.92 kB 100.92 kB
test_utils/ReactAllWarnings.js Deleted 63.65 kB 0.00 kB Deleted 15.90 kB 0.00 kB

Significant size changes

Includes any change greater than 0.2%:

Expand to show
Name +/- Base Current +/- gzip Base gzip Current gzip
oss-stable-semver/react-server-dom-turbopack/cjs/react-server-dom-turbopack-server.browser.production.js +0.54% 86.12 kB 86.59 kB +0.67% 17.85 kB 17.97 kB
oss-stable/react-server-dom-turbopack/cjs/react-server-dom-turbopack-server.browser.production.js +0.54% 86.12 kB 86.59 kB +0.67% 17.85 kB 17.97 kB
oss-stable-semver/react-server-dom-webpack/cjs/react-server-dom-webpack-server.browser.production.js +0.54% 86.45 kB 86.91 kB +0.70% 17.93 kB 18.05 kB
oss-stable/react-server-dom-webpack/cjs/react-server-dom-webpack-server.browser.production.js +0.54% 86.45 kB 86.91 kB +0.70% 17.93 kB 18.05 kB
oss-experimental/react-server-dom-turbopack/cjs/react-server-dom-turbopack-server.browser.production.js +0.51% 90.83 kB 91.30 kB +0.69% 18.54 kB 18.66 kB
oss-experimental/react-server-dom-webpack/cjs/react-server-dom-webpack-server.browser.production.js +0.51% 91.16 kB 91.62 kB +0.69% 18.63 kB 18.75 kB
oss-stable-semver/react-dom/cjs/react-dom-server.browser.production.js +0.22% 211.61 kB 212.08 kB +0.38% 38.52 kB 38.67 kB
oss-stable/react-dom/cjs/react-dom-server.browser.production.js +0.22% 211.68 kB 212.15 kB +0.38% 38.54 kB 38.69 kB
test_utils/ReactAllWarnings.js Deleted 63.65 kB 0.00 kB Deleted 15.90 kB 0.00 kB

Generated by 🚫 dangerJS against 1f062b4

Comment on lines -24 to -36
global.requestIdleCallback = function (callback) {
return setTimeout(() => {
callback({
timeRemaining() {
return Infinity;
},
});
});
};

global.cancelIdleCallback = function (callbackID) {
clearTimeout(callbackID);
};
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

These haven't been used in a very long time

Comment on lines +3458 to +3461
scheduleWork(() => {
request.flushScheduled = false;
const destination = request.destination;
if (destination) {
flushCompletedChunks(request, destination);
}
});
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

This work uncovered a bug where we didn't properly check whether the destination still existed before attempting to flush.

While most builds of Flight and Fizz schedule work in new tasks some do execute work synchronously. While this is necessary for legacy APIs like renderToString for modern APIs there really isn't a great reason to do this synchronously.

This change updates all non-legacy uses to be async using the best availalble scheduler at runtime
@gnoff gnoff merged commit b526a0a into facebook:main Jun 6, 2024
40 checks passed
@gnoff gnoff deleted the schedule-work-async branch June 6, 2024 17:07
github-actions bot pushed a commit that referenced this pull request Jun 6, 2024
While most builds of Flight and Fizz schedule work in new tasks some do
execute work synchronously. While this is necessary for legacy APIs like
renderToString for modern APIs there really isn't a great reason to do
this synchronously.

We could schedule works as microtasks but we actually want to yield so
the runtime can run events and other things that will unblock additional
work before starting the next work loop.

This change updates all non-legacy uses to be async using the best
availalble macrotask scheduler.

Browser now uses postMessage
Bun uses setTimeout because while it also supports setImmediate the
scheduling is not as eager as the same API in node
the FB build also uses setTimeout

This change required a number of changes to tests which were utilizing
the sync nature of work in the Browser builds to avoid having to manage
timers and tasks. I added a patch to install MessageChannel which is
required by the browser builds and made this patched version integrate
with the Scheduler mock. This way we can effectively use `act` to flush
flight and fizz work similar to how we do this on the client.

DiffTrain build for commit b526a0a.
gnoff added a commit that referenced this pull request Jun 6, 2024
Stacked on #29551

Flight pings much more often than Fizz because async function components
will always take at least a microtask to resolve . Rather than
scheduling this work as a new macrotask Flight now schedules pings in a
microtask. This allows more microtasks to ping before actually doing a
work flush but doesn't force the vm to spin up a new task which is quite
common give n the nature of Server Components
github-actions bot pushed a commit that referenced this pull request Jun 6, 2024
Stacked on #29551

Flight pings much more often than Fizz because async function components
will always take at least a microtask to resolve . Rather than
scheduling this work as a new macrotask Flight now schedules pings in a
microtask. This allows more microtasks to ping before actually doing a
work flush but doesn't force the vm to spin up a new task which is quite
common give n the nature of Server Components

DiffTrain build for commit 1e1e5cd.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
CLA Signed React Core Team Opened by a member of the React Core Team
Projects
None yet
Development

Successfully merging this pull request may close these issues.

5 participants