@samitouri / QOS-React / commits / 15767a8f8f

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

* Initial failing unit test for error handling in schedule **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! * Add fixture for testing error handling in scheduler **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. * Handle errors in scheduler **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. * Add more tests for error handling in scheduler **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. * callSafely -> callUnsafely * Fix bugs with error handling in schedule **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. * Unit tests for error handling with timed out callbacks **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. * Adds fixture to test timed out callbacks with scheduler **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. * fix rebase problems * make fixture compensate for chrome JS speed * ran prettier * Remove 'cancelled' flag on callbackConfig in scheduler, add test **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. * fix typo

Flarnie Marchan committed May 30, 2018 at 15:38 UTC 15767a8f8f798f0d80df4ddbc7cfaebdc29996a8
3 files changed +624 -30
fixtures/schedule/index.html
+177 -1
@@ -69,6 +69,16 @@
69 <div><b>Actual:</b></div>
70 <div id="test-4"></div>
71 </li>
72 + <li>
73 + <p>When some callbacks throw errors, still calls them all within the same frame</p>
74 + <p><b>IMPORTANT:</b> Open the console when you run this! Inspect the logs there!</p>
75 + <button onClick="runTestFive()">Run Test 5</button>
76 + </li>
77 + <li>
78 + <p>When some callbacks throw errors <b> and some also time out</b>, still calls them all within the same frame</p>
79 + <p><b>IMPORTANT:</b> Open the console when you run this! Inspect the logs there!</p>
80 + <button onClick="runTestSix()">Run Test 6</button>
81 + </li>
82 </ol>
83 <script src="../../build/dist/react-scheduler.development.js"></script>
84 <script src="https://unpkg.com/babel-standalone@6/babel.js"></script>
@@ -134,6 +144,9 @@ const latestResults = [
144 // test 4
145 [
146 ],
147 + // test 5
148 + [
149 + ],
150 ];
151
152 const expectedResults = [
@@ -182,6 +195,10 @@ const expectedResults = [
195 'cbD called with argument of {"didTimeout":false}',
196 'frame 3 started... we stop counting now.',
197 ],
198 + // test 5
199 + [
200 + // ... TODO
201 + ],
202 ];
203 function runTestOne() {
204 // Test 1
@@ -276,7 +293,7 @@ function runTestFour() {
293 updateTestResult(4, 'scheduled cbA');
294 scheduleWork(cbB, {timeout: 100}); // times out later
295 updateTestResult(4, 'scheduled cbB');
279 - scheduleWork(cbC, {timeout: 2}); // will time out fast
296 + scheduleWork(cbC, {timeout: 1}); // will time out fast
297 updateTestResult(4, 'scheduled cbC');
298 scheduleWork(cbD); // won't time out
299 updateTestResult(4, 'scheduled cbD');
@@ -287,6 +304,165 @@ function runTestFour() {
304 displayTestResult(4);
305 checkTestResult(4);
306 });
307 +
308 +}
309 +
310 +// Error handling
311 +
312 +function runTestFive() {
313 + // Test 5
314 + // When some callbacks throw errors, still calls them all within the same frame
315 + const cbA = (x) => {
316 + console.log('cbA called with argument of ' + JSON.stringify(x));
317 + }
318 + const cbB = (x) => {
319 + console.log('cbB called with argument of ' + JSON.stringify(x));
320 + console.log('cbB is about to throw an error!');
321 + throw new Error('error B');
322 + }
323 + const cbC = (x) => {
324 + console.log('cbC called with argument of ' + JSON.stringify(x));
325 + }
326 + const cbD = (x) => {
327 + console.log('cbD called with argument of ' + JSON.stringify(x));
328 + console.log('cbD is about to throw an error!');
329 + throw new Error('error D');
330 + }
331 + const cbE = (x) => {
332 + console.log('cbE called with argument of ' + JSON.stringify(x));
333 + console.log('This was the last callback! ------------------');
334 + }
335 +
336 + console.log('We are aiming to roughly emulate the way ' +
337 + '`requestAnimationFrame` handles errors from callbacks.');
338 +
339 + console.log('about to run the simulation of what it should look like...:');
340 +
341 + requestAnimationFrame(() => {
342 + console.log('frame 1 started');
343 + requestAnimationFrame(() => {
344 + console.log('frame 2 started');
345 + requestAnimationFrame(() => {
346 + console.log('frame 3 started... we stop counting now.');
347 + console.log('about to wait a moment and start this again but ' +
348 + 'with the scheduler instead of requestAnimationFrame');
349 + setTimeout(runSchedulerCode, 1000);
350 + });
351 + });
352 + });
353 + requestAnimationFrame(cbA);
354 + console.log('scheduled cbA');
355 + requestAnimationFrame(cbB); // will throw error
356 + console.log('scheduled cbB');
357 + requestAnimationFrame(cbC);
358 + console.log('scheduled cbC');
359 + requestAnimationFrame(cbD); // will throw error
360 + console.log('scheduled cbD');
361 + requestAnimationFrame(cbE);
362 + console.log('scheduled cbE');
363 +
364 +
365 + function runSchedulerCode() {
366 + console.log('-------------------------------------------------------------');
367 + console.log('now lets see what it looks like using the scheduler...:');
368 + requestAnimationFrame(() => {
369 + console.log('frame 1 started');
370 + requestAnimationFrame(() => {
371 + console.log('frame 2 started');
372 + requestAnimationFrame(() => {
373 + console.log('frame 3 started... we stop counting now.');
374 + });
375 + });
376 + });
377 + scheduleWork(cbA);
378 + console.log('scheduled cbA');
379 + scheduleWork(cbB); // will throw error
380 + console.log('scheduled cbB');
381 + scheduleWork(cbC);
382 + console.log('scheduled cbC');
383 + scheduleWork(cbD); // will throw error
384 + console.log('scheduled cbD');
385 + scheduleWork(cbE);
386 + console.log('scheduled cbE');
387 + };
388 +}
389 +
390 +function runTestSix() {
391 + // Test 6
392 + // When some callbacks throw errors, still calls them all within the same frame
393 + const cbA = (x) => {
394 + console.log('cbA called with argument of ' + JSON.stringify(x));
395 + console.log('cbA is about to throw an error!');
396 + throw new Error('error A');
397 + }
398 + const cbB = (x) => {
399 + console.log('cbB called with argument of ' + JSON.stringify(x));
400 + }
401 + const cbC = (x) => {
402 + console.log('cbC called with argument of ' + JSON.stringify(x));
403 + }
404 + const cbD = (x) => {
405 + console.log('cbD called with argument of ' + JSON.stringify(x));
406 + console.log('cbD is about to throw an error!');
407 + throw new Error('error D');
408 + }
409 + const cbE = (x) => {
410 + console.log('cbE called with argument of ' + JSON.stringify(x));
411 + console.log('This was the last callback! ------------------');
412 + }
413 +
414 + console.log('We are aiming to roughly emulate the way ' +
415 + '`requestAnimationFrame` handles errors from callbacks.');
416 +
417 + console.log('about to run the simulation of what it should look like...:');
418 +
419 + requestAnimationFrame(() => {
420 + console.log('frame 1 started');
421 + requestAnimationFrame(() => {
422 + console.log('frame 2 started');
423 + requestAnimationFrame(() => {
424 + console.log('frame 3 started... we stop counting now.');
425 + console.log('about to wait a moment and start this again but ' +
426 + 'with the scheduler instead of requestAnimationFrame');
427 + setTimeout(runSchedulerCode, 1000);
428 + });
429 + });
430 + });
431 + requestAnimationFrame(cbC);
432 + console.log('scheduled cbC first; simulating timing out');
433 + requestAnimationFrame(cbD); // will throw error
434 + console.log('scheduled cbD first; simulating timing out');
435 + requestAnimationFrame(cbE);
436 + console.log('scheduled cbE first; simulating timing out');
437 + requestAnimationFrame(cbA);
438 + console.log('scheduled cbA'); // will throw error
439 + requestAnimationFrame(cbB);
440 + console.log('scheduled cbB');
441 +
442 +
443 + function runSchedulerCode() {
444 + console.log('-------------------------------------------------------------');
445 + console.log('now lets see what it looks like using the scheduler...:');
446 + requestAnimationFrame(() => {
447 + console.log('frame 1 started');
448 + requestAnimationFrame(() => {
449 + console.log('frame 2 started');
450 + requestAnimationFrame(() => {
451 + console.log('frame 3 started... we stop counting now.');
452 + });
453 + });
454 + });
455 + scheduleWork(cbA);
456 + console.log('scheduled cbA');
457 + scheduleWork(cbB); // will throw error
458 + console.log('scheduled cbB');
459 + scheduleWork(cbC, {timeout: 1});
460 + console.log('scheduled cbC');
461 + scheduleWork(cbD, {timeout: 1}); // will throw error
462 + console.log('scheduled cbD');
463 + scheduleWork(cbE, {timeout: 1});
464 + console.log('scheduled cbE');
465 + };
466 }
467 </script type="text/babel">
468 </body>
packages/react-scheduler/src/ReactScheduler.js
+63 -26
@@ -129,6 +129,33 @@ if (!ExecutionEnvironment.canUseDOM) {
129 },
130 };
131
132 + /**
133 + * Handles the case where a callback errors:
134 + * - don't catch the error, because this changes debugging behavior
135 + * - do start a new postMessage callback, to call any remaining callbacks,
136 + * - but only if there is an error, so there is not extra overhead.
137 + */
138 + const callUnsafely = function(
139 + callbackConfig: CallbackConfigType,
140 + arg: Deadline,
141 + ) {
142 + const callback = callbackConfig.scheduledCallback;
143 + let finishedCalling = false;
144 + try {
145 + callback(arg);
146 + finishedCalling = true;
147 + } finally {
148 + // always remove it from linked list
149 + cancelScheduledWork(callbackConfig);
150 +
151 + if (!finishedCalling) {
152 + // an error must have been thrown
153 + isIdleScheduled = true;
154 + window.postMessage(messageKey, '*');
155 + }
156 + }
157 + };
158 +
159 /**
160 * Checks for timed out callbacks, runs them, and then checks again to see if
161 * any more have timed out.
@@ -152,32 +179,42 @@ if (!ExecutionEnvironment.canUseDOM) {
179 // We know that none of them have timed out yet.
180 return;
181 }
155 - nextSoonestTimeoutTime = -1; // we will reset it below
156 -
157 - // keep checking until we don't find any more timed out callbacks
158 - frameDeadlineObject.didTimeout = true;
182 + // NOTE: we intentionally wait to update the nextSoonestTimeoutTime until
183 + // after successfully calling any timed out callbacks.
184 + // If a timed out callback throws an error, we could get stuck in a state
185 + // where the nextSoonestTimeoutTime was set wrong.
186 + let updatedNextSoonestTimeoutTime = -1; // we will update nextSoonestTimeoutTime below
187 + const timedOutCallbacks = [];
188 +
189 + // iterate once to find timed out callbacks and find nextSoonestTimeoutTime
190 let currentCallbackConfig = headOfPendingCallbacksLinkedList;
191 while (currentCallbackConfig !== null) {
192 const timeoutTime = currentCallbackConfig.timeoutTime;
193 if (timeoutTime !== -1 && timeoutTime <= currentTime) {
194 // it has timed out!
164 - // call it
165 - const callback = currentCallbackConfig.scheduledCallback;
166 - // TODO: error handling
167 - callback(frameDeadlineObject);
168 - // remove it from linked list
169 - cancelScheduledWork(currentCallbackConfig);
195 + timedOutCallbacks.push(currentCallbackConfig);
196 } else {
197 if (
198 timeoutTime !== -1 &&
173 - (nextSoonestTimeoutTime === -1 ||
174 - timeoutTime < nextSoonestTimeoutTime)
199 + (updatedNextSoonestTimeoutTime === -1 ||
200 + timeoutTime < updatedNextSoonestTimeoutTime)
201 ) {
176 - nextSoonestTimeoutTime = timeoutTime;
202 + updatedNextSoonestTimeoutTime = timeoutTime;
203 }
204 }
205 currentCallbackConfig = currentCallbackConfig.next;
206 }
207 +
208 + if (timedOutCallbacks.length > 0) {
209 + frameDeadlineObject.didTimeout = true;
210 + for (let i = 0, len = timedOutCallbacks.length; i < len; i++) {
211 + callUnsafely(timedOutCallbacks[i], frameDeadlineObject);
212 + }
213 + }
214 +
215 + // NOTE: we intentionally wait to update the nextSoonestTimeoutTime until
216 + // after successfully calling any timed out callbacks.
217 + nextSoonestTimeoutTime = updatedNextSoonestTimeoutTime;
218 };
219
220 // We use the postMessage trick to defer idle work until after the repaint.
@@ -206,20 +243,9 @@ if (!ExecutionEnvironment.canUseDOM) {
243 headOfPendingCallbacksLinkedList !== null
244 ) {
245 const latestCallbackConfig = headOfPendingCallbacksLinkedList;
209 - // move head of list to next callback
210 - headOfPendingCallbacksLinkedList = latestCallbackConfig.next;
211 - if (headOfPendingCallbacksLinkedList !== null) {
212 - headOfPendingCallbacksLinkedList.prev = null;
213 - } else {
214 - // if headOfPendingCallbacksLinkedList is null,
215 - // then the list must be empty.
216 - // make sure we set the tail to null as well.
217 - tailOfPendingCallbacksLinkedList = null;
218 - }
246 frameDeadlineObject.didTimeout = false;
220 - const latestCallback = latestCallbackConfig.scheduledCallback;
221 - // TODO: before using this outside of React we need to add error handling
222 - latestCallback(frameDeadlineObject);
247 + // callUnsafely will remove it from the head of the linked list
248 + callUnsafely(latestCallbackConfig, frameDeadlineObject);
249 currentTime = now();
250 }
251 if (headOfPendingCallbacksLinkedList !== null) {
@@ -315,6 +341,15 @@ if (!ExecutionEnvironment.canUseDOM) {
341 cancelScheduledWork = function(
342 callbackConfig: CallbackIdType /* CallbackConfigType */,
343 ) {
344 + if (
345 + callbackConfig.prev === null &&
346 + headOfPendingCallbacksLinkedList !== callbackConfig
347 + ) {
348 + // this callbackConfig has already been cancelled.
349 + // cancelScheduledWork should be idempotent, a no-op after first call.
350 + return;
351 + }
352 +
353 /**
354 * There are four possible cases:
355 * - Head/nodeToRemove/Tail -> null
@@ -331,6 +366,8 @@ if (!ExecutionEnvironment.canUseDOM) {
366 */
367 const next = callbackConfig.next;
368 const prev = callbackConfig.prev;
369 + callbackConfig.next = null;
370 + callbackConfig.prev = null;
371 if (next !== null) {
372 // we have a next
373
packages/react-scheduler/src/__tests__/ReactScheduler-test.js
+384 -3
@@ -20,6 +20,8 @@ describe('ReactScheduler', () => {
20 let rAFCallbacks = [];
21 let postMessageCallback;
22 let postMessageEvents = [];
23 + let postMessageErrors = [];
24 + let catchPostMessageErrors = false;
25
26 function runPostMessageCallbacks(config: FrameTimeoutConfigType) {
27 let timeLeftInFrame = 0;
@@ -31,7 +33,17 @@ describe('ReactScheduler', () => {
33 currentTime = startOfLatestFrame + frameSize - timeLeftInFrame;
34 if (postMessageCallback) {
35 while (postMessageEvents.length) {
34 - postMessageCallback(postMessageEvents.shift());
36 + if (catchPostMessageErrors) {
37 + // catch errors for testing error handling
38 + try {
39 + postMessageCallback(postMessageEvents.shift());
40 + } catch (e) {
41 + postMessageErrors.push(e);
42 + }
43 + } else {
44 + // we are not expecting errors
45 + postMessageCallback(postMessageEvents.shift());
46 + }
47 }
48 }
49 }
@@ -64,6 +76,7 @@ describe('ReactScheduler', () => {
76 const originalAddEventListener = global.addEventListener;
77 postMessageCallback = null;
78 postMessageEvents = [];
79 + postMessageErrors = [];
80 global.addEventListener = function(eventName, callback, useCapture) {
81 if (eventName === 'message') {
82 postMessageCallback = callback;
@@ -280,8 +293,6 @@ describe('ReactScheduler', () => {
293
294 describe('when there is some time left in the frame', () => {
295 it('calls timed out callbacks and then any more pending callbacks, defers others if time runs out', () => {
283 - // TODO first call timed out callbacks
284 - // then any non-timed out callbacks if there is time
296 const {scheduleWork} = ReactScheduler;
297 startOfLatestFrame = 1000000000000;
298 currentTime = startOfLatestFrame - 10;
@@ -340,6 +351,27 @@ describe('ReactScheduler', () => {
351 });
352
353 describe('with multiple callbacks', () => {
354 + it('when called more than once', () => {
355 + const {scheduleWork, cancelScheduledWork} = ReactScheduler;
356 + const callbackLog = [];
357 + const callbackA = jest.fn(() => callbackLog.push('A'));
358 + const callbackB = jest.fn(() => callbackLog.push('B'));
359 + const callbackC = jest.fn(() => callbackLog.push('C'));
360 + scheduleWork(callbackA);
361 + const callbackId = scheduleWork(callbackB);
362 + scheduleWork(callbackC);
363 + cancelScheduledWork(callbackId);
364 + cancelScheduledWork(callbackId);
365 + cancelScheduledWork(callbackId);
366 + // Initially doesn't call anything
367 + expect(callbackLog).toEqual([]);
368 + advanceOneFrame({timeLeftInFrame: 15});
369 +
370 + // Should still call A and C
371 + expect(callbackLog).toEqual(['A', 'C']);
372 + expect(callbackB).toHaveBeenCalledTimes(0);
373 + });
374 +
375 it('when one callback cancels the next one', () => {
376 const {scheduleWork, cancelScheduledWork} = ReactScheduler;
377 const callbackLog = [];
@@ -361,5 +393,354 @@ describe('ReactScheduler', () => {
393 });
394 });
395
396 + describe('when callbacks throw errors', () => {
397 + describe('when some callbacks throw', () => {
398 + /**
399 + * + +
400 + * | rAF postMessage |
401 + * | |
402 + * | +---------------------+ |
403 + * | | paint/layout | cbA() cbB() cbC() cbD() cbE() |
404 + * | +---------------------+ ^ ^ |
405 + * | | | |
406 + * + | | +
407 + * + +
408 + * throw errors
409 + *
410 + *
411 + */
412 + it('still calls all callbacks within same frame', () => {
413 + const {scheduleWork} = ReactScheduler;
414 + const callbackLog = [];
415 + const callbackA = jest.fn(() => callbackLog.push('A'));
416 + const callbackB = jest.fn(() => {
417 + callbackLog.push('B');
418 + throw new Error('B error');
419 + });
420 + const callbackC = jest.fn(() => callbackLog.push('C'));
421 + const callbackD = jest.fn(() => {
422 + callbackLog.push('D');
423 + throw new Error('D error');
424 + });
425 + const callbackE = jest.fn(() => callbackLog.push('E'));
426 + scheduleWork(callbackA);
427 + scheduleWork(callbackB);
428 + scheduleWork(callbackC);
429 + scheduleWork(callbackD);
430 + scheduleWork(callbackE);
431 + // Initially doesn't call anything
432 + expect(callbackLog).toEqual([]);
433 + catchPostMessageErrors = true;
434 + advanceOneFrame({timeLeftInFrame: 15});
435 + // calls all callbacks
436 + expect(callbackLog).toEqual(['A', 'B', 'C', 'D', 'E']);
437 + // errors should still get thrown
438 + const postMessageErrorMessages = postMessageErrors.map(e => e.message);
439 + expect(postMessageErrorMessages).toEqual(['B error', 'D error']);
440 + catchPostMessageErrors = false;
441 + });
442 +
443 + /**
444 + * timed out
445 + * + + +--+
446 + * + rAF postMessage | | | +
447 + * | | | | |
448 + * | +---------------------+ v v v |
449 + * | | paint/layout | cbA() cbB() cbC() cbD() cbE() |
450 + * | +---------------------+ ^ ^ |
451 + * | | | |
452 + * + | | +
453 + * + +
454 + * throw errors
455 + *
456 + *
457 + */
458 + it('and with some timed out callbacks, still calls all callbacks within same frame', () => {
459 + const {scheduleWork} = ReactScheduler;
460 + const callbackLog = [];
461 + const callbackA = jest.fn(() => {
462 + callbackLog.push('A');
463 + throw new Error('A error');
464 + });
465 + const callbackB = jest.fn(() => callbackLog.push('B'));
466 + const callbackC = jest.fn(() => callbackLog.push('C'));
467 + const callbackD = jest.fn(() => {
468 + callbackLog.push('D');
469 + throw new Error('D error');
470 + });
471 + const callbackE = jest.fn(() => callbackLog.push('E'));
472 + scheduleWork(callbackA);
473 + scheduleWork(callbackB);
474 + scheduleWork(callbackC, {timeout: 2}); // times out fast
475 + scheduleWork(callbackD, {timeout: 2}); // times out fast
476 + scheduleWork(callbackE, {timeout: 2}); // times out fast
477 + // Initially doesn't call anything
478 + expect(callbackLog).toEqual([]);
479 + catchPostMessageErrors = true;
480 + advanceOneFrame({timeLeftInFrame: 15});
481 + // calls all callbacks; calls timed out ones first
482 + expect(callbackLog).toEqual(['C', 'D', 'E', 'A', 'B']);
483 + // errors should still get thrown
484 + const postMessageErrorMessages = postMessageErrors.map(e => e.message);
485 + expect(postMessageErrorMessages).toEqual(['D error', 'A error']);
486 + catchPostMessageErrors = false;
487 + });
488 + });
489 + describe('when all scheduled callbacks throw', () => {
490 + /**
491 + * + +
492 + * | rAF postMessage |
493 + * | |
494 + * | +---------------------+ |
495 + * | | paint/layout | cbA() cbB() cbC() cbD() cbE() |
496 + * | +---------------------+ ^ ^ ^ ^ ^ |
497 + * | | | | | | |
498 + * + | | | | | +
499 + * | + + + +
500 + * + all callbacks throw errors
501 + *
502 + *
503 + */
504 + it('still calls all callbacks within same frame', () => {
505 + const {scheduleWork} = ReactScheduler;
506 + const callbackLog = [];
507 + const callbackA = jest.fn(() => {
508 + callbackLog.push('A');
509 + throw new Error('A error');
510 + });
511 + const callbackB = jest.fn(() => {
512 + callbackLog.push('B');
513 + throw new Error('B error');
514 + });
515 + const callbackC = jest.fn(() => {
516 + callbackLog.push('C');
517 + throw new Error('C error');
518 + });
519 + const callbackD = jest.fn(() => {
520 + callbackLog.push('D');
521 + throw new Error('D error');
522 + });
523 + const callbackE = jest.fn(() => {
524 + callbackLog.push('E');
525 + throw new Error('E error');
526 + });
527 + scheduleWork(callbackA);
528 + scheduleWork(callbackB);
529 + scheduleWork(callbackC);
530 + scheduleWork(callbackD);
531 + scheduleWork(callbackE);
532 + // Initially doesn't call anything
533 + expect(callbackLog).toEqual([]);
534 + catchPostMessageErrors = true;
535 + advanceOneFrame({timeLeftInFrame: 15});
536 + // calls all callbacks
537 + expect(callbackLog).toEqual(['A', 'B', 'C', 'D', 'E']);
538 + // errors should still get thrown
539 + const postMessageErrorMessages = postMessageErrors.map(e => e.message);
540 + expect(postMessageErrorMessages).toEqual([
541 + 'A error',
542 + 'B error',
543 + 'C error',
544 + 'D error',
545 + 'E error',
546 + ]);
547 + catchPostMessageErrors = false;
548 + });
549 +
550 + /**
551 + * postMessage
552 + * + +
553 + * | rAF all callbacks time out |
554 + * | |
555 + * | +---------------------+ |
556 + * | | paint/layout | cbA() cbB() cbC() cbD() cbE() |
557 + * | +---------------------+ ^ ^ ^ ^ ^ |
558 + * | | | | | | |
559 + * + | | | | | +
560 + * | + + + +
561 + * + all callbacks throw errors
562 + *
563 + *
564 + */
565 + it('and with all timed out callbacks, still calls all callbacks within same frame', () => {
566 + const {scheduleWork} = ReactScheduler;
567 + const callbackLog = [];
568 + const callbackA = jest.fn(() => {
569 + callbackLog.push('A');
570 + throw new Error('A error');
571 + });
572 + const callbackB = jest.fn(() => {
573 + callbackLog.push('B');
574 + throw new Error('B error');
575 + });
576 + const callbackC = jest.fn(() => {
577 + callbackLog.push('C');
578 + throw new Error('C error');
579 + });
580 + const callbackD = jest.fn(() => {
581 + callbackLog.push('D');
582 + throw new Error('D error');
583 + });
584 + const callbackE = jest.fn(() => {
585 + callbackLog.push('E');
586 + throw new Error('E error');
587 + });
588 + scheduleWork(callbackA, {timeout: 2}); // times out fast
589 + scheduleWork(callbackB, {timeout: 2}); // times out fast
590 + scheduleWork(callbackC, {timeout: 2}); // times out fast
591 + scheduleWork(callbackD, {timeout: 2}); // times out fast
592 + scheduleWork(callbackE, {timeout: 2}); // times out fast
593 + // Initially doesn't call anything
594 + expect(callbackLog).toEqual([]);
595 + catchPostMessageErrors = true;
596 + advanceOneFrame({timeLeftInFrame: 15});
597 + // calls all callbacks
598 + expect(callbackLog).toEqual(['A', 'B', 'C', 'D', 'E']);
599 + // errors should still get thrown
600 + const postMessageErrorMessages = postMessageErrors.map(e => e.message);
601 + expect(postMessageErrorMessages).toEqual([
602 + 'A error',
603 + 'B error',
604 + 'C error',
605 + 'D error',
606 + 'E error',
607 + ]);
608 + catchPostMessageErrors = false;
609 + });
610 + });
611 + describe('when callbacks throw over multiple frames', () => {
612 + /**
613 + *
614 + * **Detail View of Frame 1**
615 + *
616 + * + +
617 + * | rAF postMessage |
618 + * | |
619 + * | +---------------------+ |
620 + * | | paint/layout | cbA() cbB() | ... Frame 2
621 + * | +---------------------+ ^ ^ |
622 + * | | | |
623 + * + + | +
624 + * errors |
625 + * +
626 + * takes long time
627 + * and pushes rest of
628 + * callbacks into
629 + * next frame ->
630 + *
631 + *
632 + *
633 + * **Overview of frames 1-4**
634 + *
635 + *
636 + * + + + + +
637 + * | | | | |
638 + * | +--+ | +--+ | +--+ | +--+ |
639 + * | +--+ A,B+-> +--+ C,D+-> +--+ E,F+-> +--+ G |
640 + * + ^ + ^ + ^ + +
641 + * | | |
642 + * error error error
643 + *
644 + *
645 + */
646 + it('still calls all callbacks within same frame', () => {
647 + const {scheduleWork} = ReactScheduler;
648 + startOfLatestFrame = 1000000000000;
649 + currentTime = startOfLatestFrame - 10;
650 + catchPostMessageErrors = true;
651 + const callbackLog = [];
652 + const callbackA = jest.fn(() => {
653 + callbackLog.push('A');
654 + throw new Error('A error');
655 + });
656 + const callbackB = jest.fn(() => {
657 + callbackLog.push('B');
658 + // time passes, causing us to run out of idle time
659 + currentTime += 25;
660 + });
661 + const callbackC = jest.fn(() => {
662 + callbackLog.push('C');
663 + throw new Error('C error');
664 + });
665 + const callbackD = jest.fn(() => {
666 + callbackLog.push('D');
667 + // time passes, causing us to run out of idle time
668 + currentTime += 25;
669 + });
670 + const callbackE = jest.fn(() => {
671 + callbackLog.push('E');
672 + throw new Error('E error');
673 + });
674 + const callbackF = jest.fn(() => {
675 + callbackLog.push('F');
676 + // time passes, causing us to run out of idle time
677 + currentTime += 25;
678 + });
679 + const callbackG = jest.fn(() => callbackLog.push('G'));
680 +
681 + scheduleWork(callbackA);
682 + scheduleWork(callbackB);
683 + scheduleWork(callbackC);
684 + scheduleWork(callbackD);
685 + scheduleWork(callbackE);
686 + scheduleWork(callbackF);
687 + scheduleWork(callbackG);
688 +
689 + // does nothing initially
690 + expect(callbackLog).toEqual([]);
691 +
692 + // frame 1;
693 + // callback A runs and throws, callback B takes up rest of frame
694 + advanceOneFrame({timeLeftInFrame: 15}); // runs rAF and postMessage callbacks
695 +
696 + // calls A and B
697 + expect(callbackLog).toEqual(['A', 'B']);
698 + // error was thrown from A
699 + let postMessageErrorMessages = postMessageErrors.map(e => e.message);
700 + expect(postMessageErrorMessages).toEqual(['A error']);
701 +
702 + // frame 2;
703 + // callback C runs and throws, callback D takes up rest of frame
704 + advanceOneFrame({timeLeftInFrame: 15}); // runs rAF and postMessage callbacks
705 +
706 + // calls C and D
707 + expect(callbackLog).toEqual(['A', 'B', 'C', 'D']);
708 + // error was thrown from A
709 + postMessageErrorMessages = postMessageErrors.map(e => e.message);
710 + expect(postMessageErrorMessages).toEqual(['A error', 'C error']);
711 +
712 + // frame 3;
713 + // callback E runs and throws, callback F takes up rest of frame
714 + advanceOneFrame({timeLeftInFrame: 15}); // runs rAF and postMessage callbacks
715 +
716 + // calls E and F
717 + expect(callbackLog).toEqual(['A', 'B', 'C', 'D', 'E', 'F']);
718 + // error was thrown from A
719 + postMessageErrorMessages = postMessageErrors.map(e => e.message);
720 + expect(postMessageErrorMessages).toEqual([
721 + 'A error',
722 + 'C error',
723 + 'E error',
724 + ]);
725 +
726 + // frame 4;
727 + // callback G runs and it's the last one
728 + advanceOneFrame({timeLeftInFrame: 15}); // runs rAF and postMessage callbacks
729 +
730 + // calls G
731 + expect(callbackLog).toEqual(['A', 'B', 'C', 'D', 'E', 'F', 'G']);
732 + // error was thrown from A
733 + postMessageErrorMessages = postMessageErrors.map(e => e.message);
734 + expect(postMessageErrorMessages).toEqual([
735 + 'A error',
736 + 'C error',
737 + 'E error',
738 + ]);
739 +
740 + catchPostMessageErrors = true;
741 + });
742 + });
743 + });
744 +
745 // TODO: test 'now'
746 });