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

Clean up Scheduler forks #20915

Merged
merged 9 commits into from
May 17, 2021
Merged

Conversation

rickhanlonii
Copy link
Member

Overview

This PR cleans up the scheduler files to reduce bundle size, support more environments correctly, and allow for better layering in the future.

Details

Previously, we had a scheduler for DOM environments (SchedulerDOM) and a scheduler for non-DOM environments (SchedulerNoDom). In this model, users would select the Scheduler they needed based on the environment they used.

There were a few problems with this approach. SchedulerDOM included the MessageChannel implementation which should be used outside of DOM environments (e.g. Worker environments). Second, in practice we were still shipping feature detection to select between different environments, which added bundle size without achieving the desired layering. Finally, we've since refactored these two Schedulers such that the only difference between them is how they schedule work in the event loop. This means the files are identical except for a few lines, so the added bundle size is unnecessary.

The updated structure includes the following forks:

  • Scheduler: The current scheduler implementation which will use setImmediate, MessageChannel, or setTimeout, in that order, based on what is globally available.
  • SchedulerPostTask: An experimental, internal-only scheduler which is a different implementation based on the new Chrome postTask API. This does not add bundle size because it is not exposed.
  • SchedulerMock: An internal-only scheduler for testing. Also not exposed.

@facebook-github-bot facebook-github-bot added the React Core Team Opened by a member of the React Core Team label Mar 2, 2021
@rickhanlonii
Copy link
Member Author

Tests are failing because GCC does not like using the setTimeout global

Screen Shot 2021-03-02 at 1 04 36 PM

@acdlite
Copy link
Collaborator

acdlite commented Mar 2, 2021

It's because you have a local variable that's called the same thing. Rename to something else (localSetTimeout) and should work.

@sizebot
Copy link

sizebot commented Mar 2, 2021

Comparing: 1a2d792...0bbcfc2

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.min.js = 126.00 kB 126.00 kB = 40.41 kB 40.41 kB
oss-experimental/react-dom/cjs/react-dom.production.min.js = 128.82 kB 128.82 kB = 41.35 kB 41.35 kB
facebook-www/ReactDOM-prod.classic.js = 406.09 kB 406.09 kB = 75.11 kB 75.11 kB
facebook-www/ReactDOM-prod.modern.js = 394.46 kB 394.46 kB = 73.29 kB 73.29 kB
facebook-www/ReactDOMForked-prod.classic.js = 406.09 kB 406.09 kB = 75.11 kB 75.11 kB
facebook-www/Scheduler-dev.classic.js = 23.63 kB 23.11 kB = 6.34 kB 6.24 kB
facebook-www/Scheduler-dev.modern.js = 23.63 kB 23.11 kB = 6.34 kB 6.24 kB
facebook-www/Scheduler-prod.classic.js = 11.27 kB 11.01 kB = 2.86 kB 2.75 kB
facebook-www/Scheduler-prod.modern.js = 11.27 kB 11.01 kB = 2.86 kB 2.75 kB
facebook-react-native/scheduler/cjs/Scheduler-prod.js = 10.33 kB 10.07 kB = 2.65 kB 2.54 kB
facebook-react-native/scheduler/cjs/Scheduler-profiling.js = 10.33 kB 10.07 kB = 2.65 kB 2.54 kB
oss-experimental/react/umd/react.production.min.js = 11.63 kB 11.32 kB = 4.62 kB 4.53 kB
oss-experimental/react/umd/react.profiling.min.js = 11.63 kB 11.32 kB = 4.62 kB 4.53 kB
oss-stable/react/umd/react.production.min.js = 11.24 kB 10.93 kB = 4.50 kB 4.41 kB
oss-stable/react/umd/react.profiling.min.js = 11.24 kB 10.93 kB = 4.50 kB 4.41 kB
oss-experimental/scheduler/cjs/scheduler.development.js = 17.10 kB 16.63 kB = 4.88 kB 4.77 kB
oss-stable/scheduler/cjs/scheduler.development.js = 17.10 kB 16.63 kB = 4.88 kB 4.77 kB
facebook-react-native/scheduler/cjs/Scheduler-dev.js = 17.15 kB 16.62 kB = 4.88 kB 4.77 kB
oss-experimental/scheduler/cjs/scheduler.production.min.js = 4.37 kB 4.07 kB = 1.84 kB 1.73 kB
oss-stable/scheduler/cjs/scheduler.production.min.js = 4.37 kB 4.07 kB = 1.84 kB 1.73 kB
oss-experimental/scheduler/index.js = 0.33 kB 0.20 kB = 0.21 kB 0.15 kB
oss-stable/scheduler/index.js = 0.33 kB 0.20 kB = 0.21 kB 0.15 kB
facebook-react-native/scheduler/cjs/SchedulerNoDOM-dev.js Deleted 12.97 kB 0.00 kB Deleted 3.43 kB 0.00 kB
facebook-react-native/scheduler/cjs/SchedulerNoDOM-prod.js Deleted 8.52 kB 0.00 kB Deleted 2.16 kB 0.00 kB
facebook-react-native/scheduler/cjs/SchedulerNoDOM-profiling.js Deleted 8.52 kB 0.00 kB Deleted 2.16 kB 0.00 kB
facebook-www/SchedulerNoDOM-dev.classic.js Deleted 17.83 kB 0.00 kB Deleted 4.54 kB 0.00 kB
facebook-www/SchedulerNoDOM-dev.modern.js Deleted 17.83 kB 0.00 kB Deleted 4.54 kB 0.00 kB
facebook-www/SchedulerNoDOM-prod.classic.js Deleted 8.77 kB 0.00 kB Deleted 2.24 kB 0.00 kB
facebook-www/SchedulerNoDOM-prod.modern.js Deleted 8.77 kB 0.00 kB Deleted 2.24 kB 0.00 kB
facebook-www/SchedulerNoDOM-profiling.classic.js Deleted 12.48 kB 0.00 kB Deleted 2.95 kB 0.00 kB
facebook-www/SchedulerNoDOM-profiling.modern.js Deleted 12.48 kB 0.00 kB Deleted 2.95 kB 0.00 kB
facebook-www/SchedulerPostTaskOnly-dev.classic.js Deleted 22.38 kB 0.00 kB Deleted 5.95 kB 0.00 kB
facebook-www/SchedulerPostTaskOnly-dev.modern.js Deleted 22.38 kB 0.00 kB Deleted 5.95 kB 0.00 kB
facebook-www/SchedulerPostTaskOnly-prod.classic.js Deleted 10.60 kB 0.00 kB Deleted 2.73 kB 0.00 kB
facebook-www/SchedulerPostTaskOnly-prod.modern.js Deleted 10.60 kB 0.00 kB Deleted 2.73 kB 0.00 kB
facebook-www/SchedulerPostTaskOnly-profiling.classic.js Deleted 14.29 kB 0.00 kB Deleted 3.42 kB 0.00 kB
facebook-www/SchedulerPostTaskOnly-profiling.modern.js Deleted 14.29 kB 0.00 kB Deleted 3.42 kB 0.00 kB
oss-experimental/scheduler/cjs/scheduler-unstable_no_dom.development.js Deleted 13.02 kB 0.00 kB Deleted 3.46 kB 0.00 kB
oss-experimental/scheduler/cjs/scheduler-unstable_no_dom.production.min.js Deleted 3.51 kB 0.00 kB Deleted 1.48 kB 0.00 kB
oss-experimental/scheduler/cjs/scheduler-unstable_post_task_only.development.js Deleted 15.89 kB 0.00 kB Deleted 4.51 kB 0.00 kB
oss-experimental/scheduler/cjs/scheduler-unstable_post_task_only.production.min.js Deleted 4.05 kB 0.00 kB Deleted 1.76 kB 0.00 kB
oss-experimental/scheduler/unstable_no_dom.js Deleted 0.23 kB 0.00 kB Deleted 0.16 kB 0.00 kB
oss-stable/scheduler/cjs/scheduler-unstable_no_dom.development.js Deleted 13.02 kB 0.00 kB Deleted 3.46 kB 0.00 kB
oss-stable/scheduler/cjs/scheduler-unstable_no_dom.production.min.js Deleted 3.51 kB 0.00 kB Deleted 1.48 kB 0.00 kB
oss-stable/scheduler/cjs/scheduler-unstable_post_task_only.development.js Deleted 15.89 kB 0.00 kB Deleted 4.51 kB 0.00 kB
oss-stable/scheduler/cjs/scheduler-unstable_post_task_only.production.min.js Deleted 4.05 kB 0.00 kB Deleted 1.76 kB 0.00 kB
oss-stable/scheduler/unstable_no_dom.js Deleted 0.23 kB 0.00 kB Deleted 0.16 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-experimental/react/umd/react.development.js = 98.18 kB 97.69 kB = 25.14 kB 25.07 kB
oss-stable/react/umd/react.development.js = 97.71 kB 97.22 kB = 25.08 kB 25.01 kB
facebook-www/Scheduler-profiling.classic.js = 14.97 kB 14.71 kB = 3.56 kB 3.45 kB
facebook-www/Scheduler-profiling.modern.js = 14.97 kB 14.71 kB = 3.56 kB 3.45 kB
facebook-www/Scheduler-dev.classic.js = 23.63 kB 23.11 kB = 6.34 kB 6.24 kB
facebook-www/Scheduler-dev.modern.js = 23.63 kB 23.11 kB = 6.34 kB 6.24 kB
facebook-www/Scheduler-prod.classic.js = 11.27 kB 11.01 kB = 2.86 kB 2.75 kB
facebook-www/Scheduler-prod.modern.js = 11.27 kB 11.01 kB = 2.86 kB 2.75 kB
facebook-react-native/scheduler/cjs/Scheduler-prod.js = 10.33 kB 10.07 kB = 2.65 kB 2.54 kB
facebook-react-native/scheduler/cjs/Scheduler-profiling.js = 10.33 kB 10.07 kB = 2.65 kB 2.54 kB
oss-experimental/react/umd/react.production.min.js = 11.63 kB 11.32 kB = 4.62 kB 4.53 kB
oss-experimental/react/umd/react.profiling.min.js = 11.63 kB 11.32 kB = 4.62 kB 4.53 kB
oss-stable/react/umd/react.production.min.js = 11.24 kB 10.93 kB = 4.50 kB 4.41 kB
oss-stable/react/umd/react.profiling.min.js = 11.24 kB 10.93 kB = 4.50 kB 4.41 kB
oss-experimental/scheduler/cjs/scheduler.development.js = 17.10 kB 16.63 kB = 4.88 kB 4.77 kB
oss-stable/scheduler/cjs/scheduler.development.js = 17.10 kB 16.63 kB = 4.88 kB 4.77 kB
facebook-react-native/scheduler/cjs/Scheduler-dev.js = 17.15 kB 16.62 kB = 4.88 kB 4.77 kB
oss-experimental/scheduler/cjs/scheduler.production.min.js = 4.37 kB 4.07 kB = 1.84 kB 1.73 kB
oss-stable/scheduler/cjs/scheduler.production.min.js = 4.37 kB 4.07 kB = 1.84 kB 1.73 kB
oss-experimental/scheduler/index.js = 0.33 kB 0.20 kB = 0.21 kB 0.15 kB
oss-stable/scheduler/index.js = 0.33 kB 0.20 kB = 0.21 kB 0.15 kB
facebook-react-native/scheduler/cjs/SchedulerNoDOM-dev.js Deleted 12.97 kB 0.00 kB Deleted 3.43 kB 0.00 kB
facebook-react-native/scheduler/cjs/SchedulerNoDOM-prod.js Deleted 8.52 kB 0.00 kB Deleted 2.16 kB 0.00 kB
facebook-react-native/scheduler/cjs/SchedulerNoDOM-profiling.js Deleted 8.52 kB 0.00 kB Deleted 2.16 kB 0.00 kB
facebook-www/SchedulerNoDOM-dev.classic.js Deleted 17.83 kB 0.00 kB Deleted 4.54 kB 0.00 kB
facebook-www/SchedulerNoDOM-dev.modern.js Deleted 17.83 kB 0.00 kB Deleted 4.54 kB 0.00 kB
facebook-www/SchedulerNoDOM-prod.classic.js Deleted 8.77 kB 0.00 kB Deleted 2.24 kB 0.00 kB
facebook-www/SchedulerNoDOM-prod.modern.js Deleted 8.77 kB 0.00 kB Deleted 2.24 kB 0.00 kB
facebook-www/SchedulerNoDOM-profiling.classic.js Deleted 12.48 kB 0.00 kB Deleted 2.95 kB 0.00 kB
facebook-www/SchedulerNoDOM-profiling.modern.js Deleted 12.48 kB 0.00 kB Deleted 2.95 kB 0.00 kB
facebook-www/SchedulerPostTaskOnly-dev.classic.js Deleted 22.38 kB 0.00 kB Deleted 5.95 kB 0.00 kB
facebook-www/SchedulerPostTaskOnly-dev.modern.js Deleted 22.38 kB 0.00 kB Deleted 5.95 kB 0.00 kB
facebook-www/SchedulerPostTaskOnly-prod.classic.js Deleted 10.60 kB 0.00 kB Deleted 2.73 kB 0.00 kB
facebook-www/SchedulerPostTaskOnly-prod.modern.js Deleted 10.60 kB 0.00 kB Deleted 2.73 kB 0.00 kB
facebook-www/SchedulerPostTaskOnly-profiling.classic.js Deleted 14.29 kB 0.00 kB Deleted 3.42 kB 0.00 kB
facebook-www/SchedulerPostTaskOnly-profiling.modern.js Deleted 14.29 kB 0.00 kB Deleted 3.42 kB 0.00 kB
oss-experimental/scheduler/cjs/scheduler-unstable_no_dom.development.js Deleted 13.02 kB 0.00 kB Deleted 3.46 kB 0.00 kB
oss-experimental/scheduler/cjs/scheduler-unstable_no_dom.production.min.js Deleted 3.51 kB 0.00 kB Deleted 1.48 kB 0.00 kB
oss-experimental/scheduler/cjs/scheduler-unstable_post_task_only.development.js Deleted 15.89 kB 0.00 kB Deleted 4.51 kB 0.00 kB
oss-experimental/scheduler/cjs/scheduler-unstable_post_task_only.production.min.js Deleted 4.05 kB 0.00 kB Deleted 1.76 kB 0.00 kB
oss-experimental/scheduler/unstable_no_dom.js Deleted 0.23 kB 0.00 kB Deleted 0.16 kB 0.00 kB
oss-stable/scheduler/cjs/scheduler-unstable_no_dom.development.js Deleted 13.02 kB 0.00 kB Deleted 3.46 kB 0.00 kB
oss-stable/scheduler/cjs/scheduler-unstable_no_dom.production.min.js Deleted 3.51 kB 0.00 kB Deleted 1.48 kB 0.00 kB
oss-stable/scheduler/cjs/scheduler-unstable_post_task_only.development.js Deleted 15.89 kB 0.00 kB Deleted 4.51 kB 0.00 kB
oss-stable/scheduler/cjs/scheduler-unstable_post_task_only.production.min.js Deleted 4.05 kB 0.00 kB Deleted 1.76 kB 0.00 kB
oss-stable/scheduler/unstable_no_dom.js Deleted 0.23 kB 0.00 kB Deleted 0.16 kB 0.00 kB

Generated by 🚫 dangerJS against 0bbcfc2

const localClearTimeout =
typeof window !== 'undefined' ? window.clearTimeout : clearTimeout;
const localSetImmediate =
typeof window !== 'undefined' ? window.setImmediate : setImmediate; // IE and Node.js + jsdom
Copy link
Collaborator

Choose a reason for hiding this comment

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

This will insta-crash non-browser environments that don't have setImmediate global.

I suggest this pattern:

let localSetTimeout = typeof setTimeout === 'function' ? setTimeout : undefined;
let localClearTimeout = typeof clearTimeout === 'function' ? clearTimeout : undefined;
let localSetImmediate = typeof setImmediate === 'function' ? setImmediate : undefined;

Copy link
Collaborator

@sebmarkbage sebmarkbage Mar 2, 2021

Choose a reason for hiding this comment

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

I'd recommend not hoisting these up like this and instead move them into where they're used so that the variable always has a consistent type. No need for the "undefined" intermediate step.

For example:

if (typeof setImmediate === 'function') {
  const localSetImmediate = setImmediate
  schedulePerformWorkUntilDeadline = () => {
    localSetImmediate(performWorkUntilDeadline);
  };
} else {

or even

if (typeof setImmediate === 'function') {
  schedulePerformWorkUntilDeadline = () => {
    setImmediate(performWorkUntilDeadline);
  };
} else {

Copy link
Member Author

Choose a reason for hiding this comment

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

setTimeout and clearTimeout are not only used at the top level, so how would you handle that without adding a runtime check cost?

Copy link
Collaborator

@gaearon gaearon left a comment

Choose a reason for hiding this comment

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

See my comment about insta-crash.

@gaearon
Copy link
Collaborator

gaearon commented Mar 3, 2021

Do we know why CI fails?

@rickhanlonii
Copy link
Member Author

@gaearon yes, it's failing because the test is now using setImmediate instead of setTimeout, and for some reason that semantic change causes this test:

it('resets correctly across renderers', async () => {
  function Effecty() {
    React.useEffect(() => {}, []);
    return null;
  }
  TestUtils.act(() => {
    TestRenderer.act(() => {});
    expect(() => {
      TestRenderer.create(<Effecty />);
    }).toWarnDev(["It looks like you're using the wrong act()"], {
      withoutStack: true,
    });
  });
});

to throw this error:

The document global was defined when React was initialized, but is not defined anymore. This can happen in a test environment if a component schedules an update from an asynchronous callback, but the test has already finished running. To solve this, you can either unmount the component at the end of your test (and ensure that any asynchronous operations get canceled in componentWillUnmount), or you can change the test itself to be asynchronous.

Note that changing the test to use async act fixes the failure, but I'm concerned about just doing that without understanding the cause.

@gaearon
Copy link
Collaborator

gaearon commented Mar 4, 2021

I have a vague memory of (maybe?) writing that error message a few years ago but I have no recollection of what it was about. I’ll need to take a closer look.

@rickhanlonii
Copy link
Member Author

So I confirmed that this failure is due to differences between setTimeout and setImmediate in Node and Jest.

For example, consider this test (repl):

it("crashes", () => {
  let document = {};

  setTimeout(() => {
    // This is never hit
    document.foo;
  });
  setImmediate(() => {
    // This is hit and will crash the test
    document.foo
  });

  document = null;
});

Here, the setTimeout will not crash the test, but setImmediate will.

I believe this is happening because of how the Node event loop processes setImmediate vs setTimeout. Specifically, setTimeout is processed after the close callbacks and after Jest cleans up the test environment. But setImmediate is called before close callbacks, and after Jest cleans up the test. The result is that work scheduled in setTimeout is not processed, but work in setImmediate it. Since Jest has already cleaned up the environment, the document is gone, and we error.

The fix here is to use async act so that Jest doesn't clean up the environment before all of the work is flushed. That turns the above into essentially this, which passes:

it("should pass", async () => {
  let document = {};

  setTimeout(() => {
    // This is never hit
    document.foo;
  });
  
  await new Promise((resolve) => {
    setImmediate(() => {
      // This is hit but the document is available
      document.foo;
      resolve();
    });
  });

  document = null;
});

That fixes the issue here, but makes the setImmediate change more of a breaking change than expected.

@rickhanlonii
Copy link
Member Author

I've reduced this down to this test, which shows it's happens when:

  • Rendering a component with an effect
  • With either ReactDOM or TestRenderer
  • Using real timers
jest.useRealTimers();

it('fails', () => {
  function Effecty() {
    React.useEffect(() => {}, []);
    return null;
  }

  TestRenderer.create(<Effecty />);
  
  // Or:
  // ReactDOM.render(<Effecty />, document.createElement('div'));
});

This will fail both internally and in the fixture (in different ways where document is not defined).

@rickhanlonii rickhanlonii merged commit e0f89aa into facebook:master May 17, 2021
@rickhanlonii rickhanlonii deleted the rh-refac-scheduler branch May 17, 2021 20:54
facebook-github-bot pushed a commit to facebook/react-native that referenced this pull request May 25, 2021
Summary:
This sync includes the following changes:
- **[316943091](facebook/react@316943091 )**: Make StrictMode double rendering flag static for FB/www ([#21517](facebook/react#21517)) //<Brian Vaughn>//
- **[e0f89aa05](facebook/react@e0f89aa05 )**: Clean up Scheduler forks ([#20915](facebook/react#20915)) //<Ricky>//
- **[5890e0e69](facebook/react@5890e0e69 )**: Remove data-reactroot from server rendering and hydration heuristic ([#20996](facebook/react#20996)) //<Sebastian Markbåge>//
- **[46491dce9](facebook/react@46491dce9 )**: [Bugfix] Prevent already-committed setState callback from firing again during a rebase ([#21498](facebook/react#21498)) //<Andrew Clark>//
- **[b770f7500](facebook/react@b770f7500 )**: lint-build: Infer format from artifact filename ([#21489](facebook/react#21489)) //<Andrew Clark>//
- **[2bf4805e4](facebook/react@2bf4805e4 )**: Update entry point exports ([#21488](facebook/react#21488)) //<Brian Vaughn>//

Changelog:
[General][Changed] - React Native sync for revisions b8fda6c...ebcec3c

jest_e2e[run_all_tests]

Reviewed By: JoshuaGross

Differential Revision: D28572047

fbshipit-source-id: eb09c0358edb7fbf241333ea9c08724748906fea
koto pushed a commit to koto/react that referenced this pull request Jun 15, 2021
* Clean up Scheduler forks

* Un-shadow variables

* Use timer globals directly, add a test for overrides

* Remove more window references

* Don't crash for undefined globals + tests

* Update lint config globals

* Fix test by using async act

* Add test fixture

* Delete test fixture
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.

6 participants