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

[RFC] Schedule supports two priorities #12707

Closed

Conversation

flarnie
Copy link
Contributor

@flarnie flarnie commented Apr 27, 2018

This depends on #12682 which still needs review, but I wanted to keep pushing forward so submitting this anyway.

Would love early feedback on this, and for now I'll shift to working on test coverage, which is the last significant piece.

To write the unit and fixture test coverage for this new complex behavior, we need to allow configuring 'schedule' with dependency injection. Something like this - pseudocode - :

scheduleFactory(config: {requestAnimationFrame: Function, ...other dependencies}) {
  // ... schedule logic defining requestSerialCallback and requestDeferredCallback
  return {
    requestSerialCallback,
    requestDeferredCallback,
  };
};

// in schedule.js
// check to ensure global.requestAnimationFrame is defined
export scheduleFactory({requestAnimationFrame: global.requestAnimationFrame});

// in noopSchedule.js
// ... define mockRequestAnimationFrame and a 'flushFrame' method you could use to flush callbacks.
export scheduleFactory({requestAnimationFrame: mockRequestAnimationFrame});

// in our fixture
const wrappedRequestAnimationFrame = function(callback) {
  // record something to a log which can be displayed in the fixture later
  const wrappedCallback = function() {
    // record something to a log which can be displayed in the fixture later
    callback(...arguments);
  };
  requestAnimationFrame(wrappedCallback);
};
export scheduleFactory({requestAnimationFrame: wrappedRequestAnimationFrame});

**what is the change?:**
We want to support calling ReactScheduler multiple times with different
callbacks, even if the initial callback hasn't been called yet.

There are two possible ways ReactScheduler can handle multiple
callbacks, and in one case we know that callbackA depends on callbackB
having been called already. For example;
callbackA -> updates SelectionState in a textArea
callbackB -> processes the deletion of the text currently selected.

We want to ensure that callbackA happens before callbackB. For now we
will flush callbackB as soon as callbackA is added.

In the next commit we'll split this into two methods, which support two
different behaviors here. We will support the usual behavior, which
would defer both callbackA and callbackB.

One issue with this is that we now create a new object to pass to the
callback for every use of the scheduler, while before we reused the same
object and mutated the 'didExpire' before passing it to each new
callback. With multiple callbacks, I think this leads to a risk of
mutating the object which is being used by multiple callbacks.

**why make this change?:**
We want to use this scheduling helper to coordinate between React and
non-React scripts.

**test plan:**
Added and ran a unit test.
**what is the change?:**
We added support for serial scheduled callbacks to schedule more
callbacks, and maintained the order of first-in first-called.

**why make this change?:**
This is sort of a corner case, but it's totally possible and we do
something similar in React.

We wouldn't do this with serial callbacks, but with deferred callbacks
we do a pattern like this:
```
   +                                             +
   |   +--------------------+ +----------------+ | +--------------------------------+ +-------------------------+
   |   |                    | |                | | |                                | |                         |
   |   | main thread blocked| |callback A runs | | | main thread blocked again      | |callback A runs again, finishes
   |   +--------------------+ +-----+----------+ | +--------------------------------+ ++------------------------+
   v                                ^            v                                     ^
schedule +------------------------->+            no time left!+----------------------->+
callbackA                                        reschedule   |
                                                 callbackA    +
                                                 to do more
                                                 work later.

```

**test plan:**
Wrote some fun new tests and ran them~
Also ran existing React unit tests. As of this PR they are still
directly using this module.

Rename ReactScheduler.rIC -> scheduleSerialCallback

**what is the change?:**
We renamed this method.

**why make this change?:**
1) This is no longer just a polyfill for requestIdleCallback. It's
   becoming something of it's own.
2) We are about to introduce a second variant of behavior for handling
   scheduling of callbacks, so want to have names which make sense for
   differentiating the two priorities.

**test plan:**
Ran tests
**what is the change?:**
Rename a method and add tests

**why make this change?:**
This will set things up for adding a new method which cancels callbacks
which have 'deferred' priority.

**test plan:**
Run the updated tests.
**what is the change?:**
see title

**why make this change?:**
For more readable and organized test and test output.
I put this in it's own commit because the diff looks gnarly, but we're
just moving things around. No actual changes.

**test plan:**
Run the tests
**what is the change?:**
Add support for multiple priorities.

- We are storing pending deferred callbacks in an array for simplicity,
which means that we have to do a linear search of the array repeatedly
to find callbacks which are past their timeout.
It would be more efficient to use a min heap to store them and then
quickly find the expired ones, but I'm not sure it's worth the extra
complexity unless we know that we'll be dealing with a high number of
pending deferred callbacks at once.

If there are a high number of pending deferred callbacks, we have other
problems to consider.

Also the implementation of cancel is missing and I haven't added support
for returning ids which can be used to cancel. Not sure we need this
yet, but will have to add it eventually.

**why make this change?:**
To get feedback on this approach as early as possible.

**test plan:**
Tests are a challenge.

To write a unit test I'll need to add support for injecting some of the
browser APIs as dependencies, and basically allow us to create a
noopScheduler.

We also should have a real browser fixture to test this. Going to work
on that in another branch.

**issue:**
Internal task T28128480
@@ -41,16 +41,210 @@ describe('ReactScheduler', () => {
ReactScheduler = require('react-scheduler');
});

it('rIC calls the callback within the frame when not blocked', () => {
const {rIC} = ReactScheduler;
Copy link
Contributor Author

Choose a reason for hiding this comment

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

It looks like lots was rewritten but really I just wrapped the existing tests in a new 'describe' block. Would separate this into a different PR ideally.

The only real change is that new tests were added at the end.

@pull-bot
Copy link

ReactDOM: size: 🔺+0.6%, gzip: 🔺+0.8%

Details of bundled changes.

Comparing: 9c77ffb...a13a126

react-dom

File Filesize Diff Gzip Diff Prev Size Current Size Prev Gzip Current Gzip ENV
react-dom.development.js +0.8% +0.7% 611.19 KB 616.13 KB 141.28 KB 142.25 KB UMD_DEV
react-dom.production.min.js 🔺+0.6% 🔺+0.8% 100.3 KB 100.92 KB 31.85 KB 32.1 KB UMD_PROD
react-dom.development.js +0.8% +0.7% 595.56 KB 600.51 KB 137.14 KB 138.15 KB NODE_DEV
react-dom.production.min.js 🔺+0.6% 🔺+0.7% 98.74 KB 99.36 KB 31.05 KB 31.28 KB NODE_PROD
ReactDOM-dev.js +0.8% +0.7% 619.99 KB 625.04 KB 139.85 KB 140.89 KB FB_WWW_DEV
ReactDOM-prod.js 🔺+1.3% 🔺+1.0% 283.99 KB 287.69 KB 51.93 KB 52.47 KB FB_WWW_PROD

react-art

File Filesize Diff Gzip Diff Prev Size Current Size Prev Gzip Current Gzip ENV
react-art.development.js +1.2% +1.1% 414.1 KB 418.98 KB 90.15 KB 91.13 KB UMD_DEV
react-art.production.min.js 🔺+0.7% 🔺+0.7% 90.62 KB 91.23 KB 27.51 KB 27.69 KB UMD_PROD
react-art.development.js +1.4% +1.4% 339.94 KB 344.82 KB 71.51 KB 72.5 KB NODE_DEV
react-art.production.min.js 🔺+1.1% 🔺+1.4% 55.14 KB 55.76 KB 16.76 KB 17 KB NODE_PROD
ReactART-dev.js +1.4% +1.4% 348.2 KB 353.18 KB 71.06 KB 72.07 KB FB_WWW_DEV
ReactART-prod.js 🔺+2.2% 🔺+1.7% 166.73 KB 170.36 KB 27.41 KB 27.89 KB FB_WWW_PROD

react-scheduler

File Filesize Diff Gzip Diff Prev Size Current Size Prev Gzip Current Gzip ENV
react-scheduler.development.js +57.4% +35.6% 10.2 KB 16.06 KB 3.6 KB 4.88 KB UMD_DEV
react-scheduler.production.min.js 🔺+61.4% 🔺+38.5% 1.58 KB 2.55 KB 860 B 1.16 KB UMD_PROD
react-scheduler.development.js +58.5% +36.2% 10.01 KB 15.87 KB 3.55 KB 4.83 KB NODE_DEV
react-scheduler.production.min.js 🔺+61.1% 🔺+38.3% 1.66 KB 2.67 KB 871 B 1.18 KB NODE_PROD

Generated by 🚫 dangerJS

@flarnie
Copy link
Contributor Author

flarnie commented May 3, 2018

We re-architected the commits this depends on, will open a new PR once I get this far again.

See #12682

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants