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

[scheduler] 5/n Error handling in scheduler #12920

Merged
merged 13 commits into from
May 30, 2018

Conversation

flarnie
Copy link
Contributor

@flarnie flarnie commented May 28, 2018

what is the change?:
We set a flag before calling any callback, and then use a 'try/finally'
block to wrap it. Note that we do not catch the error, if one is
thrown. But, we only unset the flag after the callback successfully
finishes.

If we reach the 'finally' block and the flag was not unset, then it
means an error was thrown.

In that case we start a new postMessage callback, to finish calling any
other pending callbacks if there is time.

why make this change?:
We need to make sure that an error thrown from one callback doesn't stop
other callbacks from firing, but we also don't want to catch or swallow
the error because we want engineers to still be able to log and debug
errors. And we don't want to push errors into the wrong frame or change their timing - this simulates almost exactly how it would work with 'requestAnimationFrame' if the callback throws an error.

test plan:
New tests added are passing, and we verified that they fail without this
change.

In particular, the fixture shows how errors and callbacks act in a real browser and compares the 'schedule.scheduleWork' method to 'requestAnimationFrame'. In Chrome the results are the same:

screen shot 2018-05-27 at 7 14 55 am

Edit: In Firefox they are also the same;
screen shot 2018-05-29 at 6 11 32 pm
screen shot 2018-05-29 at 6 11 53 pm

Haven't tested IE/Edge yet but I will, not expecting problems there.

@pull-bot
Copy link

pull-bot commented May 28, 2018

ReactDOM: size: 🔺+0.2%, gzip: 🔺+0.2%

Details of bundled changes.

Comparing: 79a740c...a7b13aa

react-dom

File Filesize Diff Gzip Diff Prev Size Current Size Prev Gzip Current Gzip ENV
react-dom.development.js +0.2% +0.2% 624.83 KB 625.93 KB 145.39 KB 145.69 KB UMD_DEV
react-dom.production.min.js 🔺+0.2% 🔺+0.2% 93.9 KB 94.04 KB 30.42 KB 30.48 KB UMD_PROD
react-dom.development.js +0.2% +0.2% 609.19 KB 610.29 KB 141.39 KB 141.69 KB NODE_DEV
react-dom.production.min.js 🔺+0.2% 🔺+0.2% 92.4 KB 92.54 KB 29.41 KB 29.46 KB NODE_PROD

react-art

File Filesize Diff Gzip Diff Prev Size Current Size Prev Gzip Current Gzip ENV
react-art.development.js +0.3% +0.3% 406.8 KB 407.9 KB 90.75 KB 91.04 KB UMD_DEV
react-art.production.min.js 🔺+0.2% 🔺+0.2% 80.91 KB 81.05 KB 24.93 KB 25 KB UMD_PROD
react-art.development.js +0.3% +0.5% 332.65 KB 333.75 KB 71.81 KB 72.13 KB NODE_DEV
react-art.production.min.js 🔺+0.3% 🔺+0.7% 45.26 KB 45.41 KB 14.04 KB 14.14 KB NODE_PROD

react-scheduler

File Filesize Diff Gzip Diff Prev Size Current Size Prev Gzip Current Gzip ENV
react-scheduler.development.js +6.8% +6.2% 16.21 KB 17.31 KB 4.96 KB 5.27 KB UMD_DEV
react-scheduler.production.min.js 🔺+6.2% 🔺+7.2% 2.29 KB 2.43 KB 1.1 KB 1.18 KB UMD_PROD
react-scheduler.development.js +6.9% +6.2% 16.02 KB 17.12 KB 4.91 KB 5.22 KB NODE_DEV
react-scheduler.production.min.js 🔺+6.0% 🔺+7.1% 2.38 KB 2.53 KB 1.12 KB 1.2 KB NODE_PROD
ReactScheduler-dev.js +8.6% +8.1% 13.05 KB 14.16 KB 4.01 KB 4.33 KB FB_WWW_DEV
ReactScheduler-prod.js 🔺+9.2% 🔺+10.7% 6.6 KB 7.21 KB 1.7 KB 1.88 KB FB_WWW_PROD

Generated by 🚫 dangerJS

* - do start a new postMessage callback, to call any remaining callbacks,
* - but only if there is an error, so there is not extra overhead.
*/
const callSafely = function(callback, arg) {
Copy link
Collaborator

Choose a reason for hiding this comment

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

callSafely implies that the function won't throw. Can we rename this to callUnsafely or similar?

@@ -155,8 +175,7 @@ if (!ExecutionEnvironment.canUseDOM) {
// it has timed out!
// call it
const callback = currentCallbackConfig.scheduledCallback;
// TODO: error handling
callback(frameDeadlineObject);
callSafely(callback, frameDeadlineObject);
Copy link
Collaborator

Choose a reason for hiding this comment

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

A timed out callback that throws will infinite loop, because it never gets cancelled by the line below. Need unit test.

@flarnie
Copy link
Contributor Author

flarnie commented May 29, 2018

I have nearly finished fixing the issues @acdlite caught in code review, and some other things. I noticed the previous PR broke the fixture, going to fix it.

#12930

flarnie added 10 commits May 29, 2018 18:09
**what is the change?:**
see title

**why make this change?:**
Adding tests for the error handling behavior we are about to add. This
test is failing, which gives us the chance to make it pass.

Wrote skeletons of some other tests to add.

Unit testing this way is really hacky, and I'm also adding to the
fixture to test this in the real browser environment.

**test plan:**
Ran new test, saw it fail!
**what is the change?:**
Added a fixture which does the following -
logs in the console to show what happens when you use
`requestAnimationFrame` to schedule a series of callbacks and some of
them throw errors.

Then does the same actions with the `scheduler` and verifies that it
behaves in a similar way.

Hard to really verify the errors get thrown at the proper time without
looking at the console.

**why make this change?:**
We want the most authentic, accurate test of how errors are handled in
the scheduler. That's what this fixture should be.

**test plan:**
Manually verified that this test does what I expect - right now it's
failing but follow up commits will fix that.
**what is the change?:**
We set a flag before calling any callback, and then use a 'try/finally'
block to wrap it. Note that we *do not* catch the error, if one is
thrown. But, we only unset the flag after the callback successfully
finishes.

If we reach the 'finally' block and the flag was not unset, then it
means an error was thrown.

In that case we start a new postMessage callback, to finish calling any
other pending callbacks if there is time.

**why make this change?:**
We need to make sure that an error thrown from one callback doesn't stop
other callbacks from firing, but we also don't want to catch or swallow
the error because we want engineers to still be able to log and debug
errors.

**test plan:**
New tests added are passing, and we verified that they fail without this
change.
**what is the change?:**
Added tests for more situations where error handling may come up.

**why make this change?:**
To get additional protection against this being broken in the future.

**test plan:**
Ran new tests and verified that they fail when error handling fails.
**what is the change?:**
- ensure that we properly remove the callback from the linked list, even
if it throws an error and is timed out.
- ensure that you can call 'cancelScheduledWork' more than once and it
is idempotent.

**why make this change?:**
To fix bugs :)

**test plan:**
Existing tests pass, and we'll add more tests in a follow up commit.
**what is the change?:**
More unit tests, to cover behavior which we missed; error handling of
timed out callbacks.

**why make this change?:**
To protect the future!~

**test plan:**
Run the new tests.
**what is the change?:**
See title

In the other error handling fixture we compare 'scheduleWork' error
handling to 'requestAnimationFrame' and try to get as close as possible.
There is no 'timing out' feature with 'requestAnimationFrame' but
effectively the 'timing out' feature changes the order in which things
are called. So we just changed the order in the 'requestAnimationFrame'
version and that works well for illustrating the behavior we expect in
the 'scheduleWork' test.

**why make this change?:**
We need more test coverage of timed out callbacks.

**test plan:**
Executed the fixture manually in Firefox and Chrome. Results looked
good.
@flarnie flarnie force-pushed the errorHandlingInScheduler1 branch from e60db4f to 8759d56 Compare May 30, 2018 01:09
@flarnie
Copy link
Contributor Author

flarnie commented May 30, 2018

Minor TODOs for tomorrow morning -
[ ] Test the fixture in IE/Edge
[ ] Add another test which covers the case where we might end up messing up the 'nextLowestTimeoutTime' if we didn't handle it carefully when callbacks error in callTimedOutCallbacks.

@flarnie
Copy link
Contributor Author

flarnie commented May 30, 2018

:( forgot prettier

Please still review 😇

}

// cancelScheduledWork should be idempotent, a no-op after first call.
callbackConfig.cancelled = true;
Copy link
Collaborator

@acdlite acdlite May 30, 2018

Choose a reason for hiding this comment

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

This doesn't seem necessary. Could you replace with something like

const alreadyCancelled = callbackConfig.next === null && callbackConfig !== tail;

?

Copy link
Collaborator

@acdlite acdlite left a comment

Choose a reason for hiding this comment

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

Yay

**what is the change?:**
- Instead of using a 'cancelled' flag on the callbackConfig, it's easier
to just check the state of the callbackConfig inside
'cancelScheduledWork' to determine if it's already been cancelled. That
way we don't have to remember to set the 'cancelled' flag every time we
call a callback or cancel it. One less thing to remember.
- We added a test for calling 'cancelScheduledWork' more than once,
which would have failed before.

Thanks @acdlite for suggesting this in code review. :)

**why make this change?:**
To increase stability of the schedule module, increase test coverage.

**test plan:**
Existing tests pass and we added a new test to cover this behavior.
@flarnie flarnie merged commit 15767a8 into facebook:master May 30, 2018
* throw errors
*
*
*/
Copy link
Contributor

Choose a reason for hiding this comment

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

I'm a little confused about the graph, if you call poseMessage, the onmessage handler is fired after microtask and before rAF, but this graph shows it will be fired after rAF, why?

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.

None yet

6 participants