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

Add scheduleMicrotaskCallback, use it to schedule flushSync for discrete update priority #20658

Closed

Conversation

rickhanlonii
Copy link
Member

Overview

We're researching a few updates to discrete events:

  • For concurrent mode, we want to revert back to flushing discrete events synchronously so that the upgrade path is easier.
  • To help catch bugs and make the behavior more consistent, we're going to schedule the sync flush in a microtask. This ensures that the update is "async", but is still flushed synchronously at the end of the current task, in order, before other work.
  • To schedule the microtask, this diff adds scheduleMicrotaskCallback to the scheduler along with a basic queueMicrotask pollyfill.

Note: there are a few tests that changes and others that are still disabled. Hope to work those use cases out in review.

@acdlite
Copy link
Collaborator

acdlite commented Jan 26, 2021

Do you mind splitting the queueMicrotask stuff into its own PR? Then stack the rest on top

@sizebot
Copy link

sizebot commented Jan 26, 2021

No significant bundle size changes to report.

Size changes (experimental)

Generated by 🚫 dangerJS against cf96b27

@sizebot
Copy link

sizebot commented Jan 26, 2021

Warnings
⚠️ Build job for base commit is still pending: a6b5256

Size changes (stable)

Generated by 🚫 dangerJS against cf96b27

@sebmarkbage
Copy link
Collaborator

I wasn't thinking that queueMicrotask would be integrated into the scheduler since it's not natively neither. They're separate things.

Is there a reason that this isn't literally export scheduleMicrotask = typeof queueMicrotask === 'function' ? queueMicrotask : ... when it's available? That's the minimal overhead.

@@ -27,6 +27,7 @@ import {
const {
unstable_runWithPriority: Scheduler_runWithPriority,
unstable_scheduleCallback: Scheduler_scheduleCallback,
unstable_scheduleMicrotaskCallback: Scheduler_scheduleMicrotaskCallback,
Copy link
Member Author

Choose a reason for hiding this comment

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

I don't have a strong opinion on the API.

An alternative may be:

scheduleCallback(
  MicrotaskPriority,
  performSyncWorkOnRoot.bind(null, root),
);

Copy link
Collaborator

@sebmarkbage sebmarkbage Jan 26, 2021

Choose a reason for hiding this comment

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

I think it should just be queueMicrotask. It's just a literal polyfill for that function which is already a standard. I'm a little doubtful that it'll be part of the Scheduler spec eventually since microtasks are layered differently spec wise, but even if it was that API has more overhead than a simple call into native.

@@ -232,7 +232,12 @@ describe('ReactIncrementalErrorHandling', () => {
expect(ReactNoop.getChildren()).toEqual([span('Caught an error: oops!')]);
});

it("retries at a lower priority if there's additional pending work", () => {
it("retries at a lower priority if there's additional pending work", async () => {
if (gate(flags => flags.enableDiscreteEventMicroTasks)) {
Copy link
Member Author

Choose a reason for hiding this comment

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

@acdlite there are 3 tests disabled in this file, which I'm not sure how to fix without an microtask integration with toFlushAndYieldThrough

});
// Low pri
root.render(<App text="A" shouldSuspend={true} />);
if (gate(flags => flags.enableDiscreteEventMicroTasks)) {
Copy link
Member Author

Choose a reason for hiding this comment

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

These tests are not as good as before since we can't flush only the microtasks without all the other work.

Promise.resolve(null)
.then(callback)
.catch(e => {
setTimeout(() => {
Copy link
Member Author

Choose a reason for hiding this comment

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

Important note: when we fallback to the promise implementation, we have to push errors into a new task or they'll be treated as unhandled promise rejections. I'm using the MDN polyfill version here, but we may want to schedule a MicrotaskPriority task instead, since these can be pushed behind other work.

Copy link
Collaborator

Choose a reason for hiding this comment

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

Not too concerned since it really only affects debugging and all modern browsers support queueMicrotask. (God I hate this problem so much, though.)

@rickhanlonii
Copy link
Member Author

@acdlite I can break this up, do you agree with @sebmarkbage that this shouldn't be in scheduler? If so, where should this go?

@sebmarkbage
Copy link
Collaborator

sebmarkbage commented Jan 26, 2021

I'm not sure where this should live but the part I don't get is why it's adding it to a scheduler queue? It seems like it should just be straight passthrough.

It could be part of scheduler just to have one convenient way to get all these polyfills but now I'm skeptical it belongs there because it's tempting to integrate it unnecessarily and you have to always pull in the whole scheduler even if you depend on only this function. Nobody else is going to do that so there's no code sharing benefit.

@acdlite
Copy link
Collaborator

acdlite commented Jan 26, 2021

I would do it as an export of Scheduler for now.

import queueMicrotask from 'scheduler/queueMicrotask';

@sebmarkbage
Copy link
Collaborator

Maybe it should just be a host config?

@acdlite
Copy link
Collaborator

acdlite commented Jan 26, 2021

Arguably all of the Scheduler methods should be host configs so we can possibly swap them out postTask one day without the Scheduler package indirection. (So, the same rationale for not wrapping queueMicrotask). But I don't want to bikeshed this right now so just put it somewhere and we can figure out the host config -> Scheduler boundary later.

@acdlite
Copy link
Collaborator

acdlite commented Jan 26, 2021

If we publish a timer polyfill we'll likely put that in the Scheduler package, too. We also may need to publish our own Promise polyfill for older browsers, so it integrates properly with our message event listener. I don't think we're going to solve all of that in this PR.

@sebmarkbage
Copy link
Collaborator

sebmarkbage commented Jan 26, 2021

I was thinking that for React Native it might be that discrete !== micro task. Since we can implement it differently there and some discrete might need to consider the thread they're on. Seems like Host Config might be the better injection point there but could be layered twice. Like the DOM host config defers to scheduler/queueMicrotask. Where as for scheduling work, it makes sense to have a shared one since it's public API and integrates with other scheduled things.

@acdlite
Copy link
Collaborator

acdlite commented Jan 26, 2021

We could call it like queueDiscrete or something. That's fine, though feels like too early of an abstraction right now and I wouldn't block the PR over it. I would put a TODO and then revisit when React Native is ready.

@rickhanlonii
Copy link
Member Author

@sebmarkbage one key benefit of using the scheduler for this right now is that we call cancelCallback when the priority changes. With this implementation, that just works. With a separate queueMicrotask impl, we'd have to sort out that cancelation. I know @acdlite had a suggestion for how to remove that dependency on cancelCallback, but I didn't entirely follow it.

@sebmarkbage
Copy link
Collaborator

We can't cancel discrete work though. So that won't make sense anyway.

@acdlite
Copy link
Collaborator

acdlite commented Jan 26, 2021

@rickhanlonii The suggestion is to not cancel the microtask once it's scheduled 😬

@rickhanlonii
Copy link
Member Author

We do need to cancel non-discrete tasks once the priority changes to discrete (like a child that uses startTransition and a parent that doesn't), so we'll need to only cancel the non-discrete tasks somehow, which seems non-trivial to unblock this.

@rickhanlonii
Copy link
Member Author

Maybe it is trivial, and we just don't store the microtask on the node.

@acdlite
Copy link
Collaborator

acdlite commented Jan 26, 2021

Let's continue in the separate PR? That way we can focus on that by itself

@codesandbox-ci
Copy link

codesandbox-ci bot commented Jan 26, 2021

This pull request is automatically built and testable in CodeSandbox.

To see build info of the built libraries, click here or the icon next to each commit SHA.

Latest deployment of this branch, based on commit cf96b27:

Sandbox Source
React Configuration

@@ -369,19 +373,24 @@ describe('SimpleEventPlugin', function() {
click();

// Flush the remaining work
Scheduler.unstable_flushAll();
await TestUtils.act(async () => {
Copy link
Collaborator

Choose a reason for hiding this comment

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

act should wrap click

@rickhanlonii
Copy link
Member Author

Thanks for the feedback.

I went with the HostConfig strategy and created a new stack of PRs with the head here.

@rickhanlonii rickhanlonii deleted the rh-scheduler-updates branch January 27, 2021 02:41
@rickhanlonii rickhanlonii added the React Core Team Opened by a member of the React Core Team label Feb 11, 2021
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