main
js 718 lines 21.5 KB
Raw
1 /**
2 * Copyright (c) Meta Platforms, Inc. and affiliates.
3 *
4 * This source code is licensed under the MIT license found in the
5 * LICENSE file in the root directory of this source tree.
6 *
7 * @emails react-core
8 */
9
10 'use strict';
11
12 let Scheduler;
13 let runWithPriority;
14 let ImmediatePriority;
15 let UserBlockingPriority;
16 let NormalPriority;
17 let LowPriority;
18 let IdlePriority;
19 let scheduleCallback;
20 let cancelCallback;
21 let wrapCallback;
22 let getCurrentPriorityLevel;
23 let shouldYield;
24 let waitForAll;
25 let assertLog;
26 let waitFor;
27 let waitForPaint;
28
29 describe('Scheduler', () => {
30 beforeEach(() => {
31 jest.resetModules();
32 jest.mock('scheduler', () => require('scheduler/unstable_mock'));
33
34 Scheduler = require('scheduler');
35
36 runWithPriority = Scheduler.unstable_runWithPriority;
37 ImmediatePriority = Scheduler.unstable_ImmediatePriority;
38 UserBlockingPriority = Scheduler.unstable_UserBlockingPriority;
39 NormalPriority = Scheduler.unstable_NormalPriority;
40 LowPriority = Scheduler.unstable_LowPriority;
41 IdlePriority = Scheduler.unstable_IdlePriority;
42 scheduleCallback = Scheduler.unstable_scheduleCallback;
43 cancelCallback = Scheduler.unstable_cancelCallback;
44 wrapCallback = Scheduler.unstable_wrapCallback;
45 getCurrentPriorityLevel = Scheduler.unstable_getCurrentPriorityLevel;
46 shouldYield = Scheduler.unstable_shouldYield;
47
48 const InternalTestUtils = require('internal-test-utils');
49 waitForAll = InternalTestUtils.waitForAll;
50 assertLog = InternalTestUtils.assertLog;
51 waitFor = InternalTestUtils.waitFor;
52 waitForPaint = InternalTestUtils.waitForPaint;
53 });
54
55 it('flushes work incrementally', async () => {
56 scheduleCallback(NormalPriority, () => Scheduler.log('A'));
57 scheduleCallback(NormalPriority, () => Scheduler.log('B'));
58 scheduleCallback(NormalPriority, () => Scheduler.log('C'));
59 scheduleCallback(NormalPriority, () => Scheduler.log('D'));
60
61 await waitFor(['A', 'B']);
62 await waitFor(['C']);
63 await waitForAll(['D']);
64 });
65
66 it('cancels work', async () => {
67 scheduleCallback(NormalPriority, () => Scheduler.log('A'));
68 const callbackHandleB = scheduleCallback(NormalPriority, () =>
69 Scheduler.log('B'),
70 );
71 scheduleCallback(NormalPriority, () => Scheduler.log('C'));
72
73 cancelCallback(callbackHandleB);
74
75 await waitForAll([
76 'A',
77 // B should have been cancelled
78 'C',
79 ]);
80 });
81
82 it('executes the highest priority callbacks first', async () => {
83 scheduleCallback(NormalPriority, () => Scheduler.log('A'));
84 scheduleCallback(NormalPriority, () => Scheduler.log('B'));
85
86 // Yield before B is flushed
87 await waitFor(['A']);
88
89 scheduleCallback(UserBlockingPriority, () => Scheduler.log('C'));
90 scheduleCallback(UserBlockingPriority, () => Scheduler.log('D'));
91
92 // C and D should come first, because they are higher priority
93 await waitForAll(['C', 'D', 'B']);
94 });
95
96 it('expires work', async () => {
97 scheduleCallback(NormalPriority, didTimeout => {
98 Scheduler.unstable_advanceTime(100);
99 Scheduler.log(`A (did timeout: ${didTimeout})`);
100 });
101 scheduleCallback(UserBlockingPriority, didTimeout => {
102 Scheduler.unstable_advanceTime(100);
103 Scheduler.log(`B (did timeout: ${didTimeout})`);
104 });
105 scheduleCallback(UserBlockingPriority, didTimeout => {
106 Scheduler.unstable_advanceTime(100);
107 Scheduler.log(`C (did timeout: ${didTimeout})`);
108 });
109
110 // Advance time, but not by enough to expire any work
111 Scheduler.unstable_advanceTime(249);
112 assertLog([]);
113
114 // Schedule a few more callbacks
115 scheduleCallback(NormalPriority, didTimeout => {
116 Scheduler.unstable_advanceTime(100);
117 Scheduler.log(`D (did timeout: ${didTimeout})`);
118 });
119 scheduleCallback(NormalPriority, didTimeout => {
120 Scheduler.unstable_advanceTime(100);
121 Scheduler.log(`E (did timeout: ${didTimeout})`);
122 });
123
124 // Advance by just a bit more to expire the user blocking callbacks
125 Scheduler.unstable_advanceTime(1);
126 await waitFor(['B (did timeout: true)', 'C (did timeout: true)']);
127
128 // Expire A
129 Scheduler.unstable_advanceTime(4600);
130 await waitFor(['A (did timeout: true)']);
131
132 // Flush the rest without expiring
133 await waitForAll(['D (did timeout: false)', 'E (did timeout: true)']);
134 });
135
136 it('has a default expiration of ~5 seconds', () => {
137 scheduleCallback(NormalPriority, () => Scheduler.log('A'));
138
139 Scheduler.unstable_advanceTime(4999);
140 assertLog([]);
141
142 Scheduler.unstable_advanceTime(1);
143 Scheduler.unstable_flushExpired();
144 assertLog(['A']);
145 });
146
147 it('continues working on same task after yielding', async () => {
148 scheduleCallback(NormalPriority, () => {
149 Scheduler.unstable_advanceTime(100);
150 Scheduler.log('A');
151 });
152 scheduleCallback(NormalPriority, () => {
153 Scheduler.unstable_advanceTime(100);
154 Scheduler.log('B');
155 });
156
157 let didYield = false;
158 const tasks = [
159 ['C1', 100],
160 ['C2', 100],
161 ['C3', 100],
162 ];
163 const C = () => {
164 while (tasks.length > 0) {
165 const [label, ms] = tasks.shift();
166 Scheduler.unstable_advanceTime(ms);
167 Scheduler.log(label);
168 if (shouldYield()) {
169 didYield = true;
170 return C;
171 }
172 }
173 };
174
175 scheduleCallback(NormalPriority, C);
176
177 scheduleCallback(NormalPriority, () => {
178 Scheduler.unstable_advanceTime(100);
179 Scheduler.log('D');
180 });
181 scheduleCallback(NormalPriority, () => {
182 Scheduler.unstable_advanceTime(100);
183 Scheduler.log('E');
184 });
185
186 // Flush, then yield while in the middle of C.
187 expect(didYield).toBe(false);
188 await waitFor(['A', 'B', 'C1']);
189 expect(didYield).toBe(true);
190
191 // When we resume, we should continue working on C.
192 await waitForAll(['C2', 'C3', 'D', 'E']);
193 });
194
195 it('continuation callbacks inherit the expiration of the previous callback', async () => {
196 const tasks = [
197 ['A', 125],
198 ['B', 124],
199 ['C', 100],
200 ['D', 100],
201 ];
202 const work = () => {
203 while (tasks.length > 0) {
204 const [label, ms] = tasks.shift();
205 Scheduler.unstable_advanceTime(ms);
206 Scheduler.log(label);
207 if (shouldYield()) {
208 return work;
209 }
210 }
211 };
212
213 // Schedule a high priority callback
214 scheduleCallback(UserBlockingPriority, work);
215
216 // Flush until just before the expiration time
217 await waitFor(['A', 'B']);
218
219 // Advance time by just a bit more. This should expire all the remaining work.
220 Scheduler.unstable_advanceTime(1);
221 Scheduler.unstable_flushExpired();
222 assertLog(['C', 'D']);
223 });
224
225 it('continuations are interrupted by higher priority work', async () => {
226 const tasks = [
227 ['A', 100],
228 ['B', 100],
229 ['C', 100],
230 ['D', 100],
231 ];
232 const work = () => {
233 while (tasks.length > 0) {
234 const [label, ms] = tasks.shift();
235 Scheduler.unstable_advanceTime(ms);
236 Scheduler.log(label);
237 if (tasks.length > 0 && shouldYield()) {
238 return work;
239 }
240 }
241 };
242 scheduleCallback(NormalPriority, work);
243 await waitFor(['A']);
244
245 scheduleCallback(UserBlockingPriority, () => {
246 Scheduler.unstable_advanceTime(100);
247 Scheduler.log('High pri');
248 });
249
250 await waitForAll(['High pri', 'B', 'C', 'D']);
251 });
252
253 it(
254 'continuations do not block higher priority work scheduled ' +
255 'inside an executing callback',
256 async () => {
257 const tasks = [
258 ['A', 100],
259 ['B', 100],
260 ['C', 100],
261 ['D', 100],
262 ];
263 const work = () => {
264 while (tasks.length > 0) {
265 const task = tasks.shift();
266 const [label, ms] = task;
267 Scheduler.unstable_advanceTime(ms);
268 Scheduler.log(label);
269 if (label === 'B') {
270 // Schedule high pri work from inside another callback
271 Scheduler.log('Schedule high pri');
272 scheduleCallback(UserBlockingPriority, () => {
273 Scheduler.unstable_advanceTime(100);
274 Scheduler.log('High pri');
275 });
276 }
277 if (tasks.length > 0) {
278 // Return a continuation
279 return work;
280 }
281 }
282 };
283 scheduleCallback(NormalPriority, work);
284 await waitForAll([
285 'A',
286 'B',
287 'Schedule high pri',
288 // The high pri callback should fire before the continuation of the
289 // lower pri work
290 'High pri',
291 // Continue low pri work
292 'C',
293 'D',
294 ]);
295 },
296 );
297
298 it('cancelling a continuation', async () => {
299 const task = scheduleCallback(NormalPriority, () => {
300 Scheduler.log('Yield');
301 return () => {
302 Scheduler.log('Continuation');
303 };
304 });
305
306 await waitFor(['Yield']);
307 cancelCallback(task);
308 await waitForAll([]);
309 });
310
311 it('top-level immediate callbacks fire in a subsequent task', () => {
312 scheduleCallback(ImmediatePriority, () => Scheduler.log('A'));
313 scheduleCallback(ImmediatePriority, () => Scheduler.log('B'));
314 scheduleCallback(ImmediatePriority, () => Scheduler.log('C'));
315 scheduleCallback(ImmediatePriority, () => Scheduler.log('D'));
316 // Immediate callback hasn't fired, yet.
317 assertLog([]);
318 // They all flush immediately within the subsequent task.
319 Scheduler.unstable_flushExpired();
320 assertLog(['A', 'B', 'C', 'D']);
321 });
322
323 it('nested immediate callbacks are added to the queue of immediate callbacks', () => {
324 scheduleCallback(ImmediatePriority, () => Scheduler.log('A'));
325 scheduleCallback(ImmediatePriority, () => {
326 Scheduler.log('B');
327 // This callback should go to the end of the queue
328 scheduleCallback(ImmediatePriority, () => Scheduler.log('C'));
329 });
330 scheduleCallback(ImmediatePriority, () => Scheduler.log('D'));
331 assertLog([]);
332 // C should flush at the end
333 Scheduler.unstable_flushExpired();
334 assertLog(['A', 'B', 'D', 'C']);
335 });
336
337 it('wrapped callbacks have same signature as original callback', () => {
338 const wrappedCallback = wrapCallback((...args) => ({args}));
339 expect(wrappedCallback('a', 'b')).toEqual({args: ['a', 'b']});
340 });
341
342 it('wrapped callbacks inherit the current priority', () => {
343 const wrappedCallback = runWithPriority(NormalPriority, () =>
344 wrapCallback(() => {
345 Scheduler.log(getCurrentPriorityLevel());
346 }),
347 );
348
349 const wrappedUserBlockingCallback = runWithPriority(
350 UserBlockingPriority,
351 () =>
352 wrapCallback(() => {
353 Scheduler.log(getCurrentPriorityLevel());
354 }),
355 );
356
357 wrappedCallback();
358 assertLog([NormalPriority]);
359
360 wrappedUserBlockingCallback();
361 assertLog([UserBlockingPriority]);
362 });
363
364 it('wrapped callbacks inherit the current priority even when nested', () => {
365 let wrappedCallback;
366 let wrappedUserBlockingCallback;
367
368 runWithPriority(NormalPriority, () => {
369 wrappedCallback = wrapCallback(() => {
370 Scheduler.log(getCurrentPriorityLevel());
371 });
372 wrappedUserBlockingCallback = runWithPriority(UserBlockingPriority, () =>
373 wrapCallback(() => {
374 Scheduler.log(getCurrentPriorityLevel());
375 }),
376 );
377 });
378
379 wrappedCallback();
380 assertLog([NormalPriority]);
381
382 wrappedUserBlockingCallback();
383 assertLog([UserBlockingPriority]);
384 });
385
386 it("immediate callbacks fire even if there's an error", () => {
387 scheduleCallback(ImmediatePriority, () => {
388 Scheduler.log('A');
389 throw new Error('Oops A');
390 });
391 scheduleCallback(ImmediatePriority, () => {
392 Scheduler.log('B');
393 });
394 scheduleCallback(ImmediatePriority, () => {
395 Scheduler.log('C');
396 throw new Error('Oops C');
397 });
398
399 expect(() => Scheduler.unstable_flushExpired()).toThrow('Oops A');
400 assertLog(['A']);
401
402 // B and C flush in a subsequent event. That way, the second error is not
403 // swallowed.
404 expect(() => Scheduler.unstable_flushExpired()).toThrow('Oops C');
405 assertLog(['B', 'C']);
406 });
407
408 it('multiple immediate callbacks can throw and there will be an error for each one', () => {
409 scheduleCallback(ImmediatePriority, () => {
410 throw new Error('First error');
411 });
412 scheduleCallback(ImmediatePriority, () => {
413 throw new Error('Second error');
414 });
415 expect(() => Scheduler.unstable_flushAll()).toThrow('First error');
416 // The next error is thrown in the subsequent event
417 expect(() => Scheduler.unstable_flushAll()).toThrow('Second error');
418 });
419
420 it('exposes the current priority level', () => {
421 Scheduler.log(getCurrentPriorityLevel());
422 runWithPriority(ImmediatePriority, () => {
423 Scheduler.log(getCurrentPriorityLevel());
424 runWithPriority(NormalPriority, () => {
425 Scheduler.log(getCurrentPriorityLevel());
426 runWithPriority(UserBlockingPriority, () => {
427 Scheduler.log(getCurrentPriorityLevel());
428 });
429 });
430 Scheduler.log(getCurrentPriorityLevel());
431 });
432
433 assertLog([
434 NormalPriority,
435 ImmediatePriority,
436 NormalPriority,
437 UserBlockingPriority,
438 ImmediatePriority,
439 ]);
440 });
441
442 if (__DEV__) {
443 // Function names are minified in prod, though you could still infer the
444 // priority if you have sourcemaps.
445 // TODO: Feature temporarily disabled while we investigate a bug in one of
446 // our minifiers.
447 // eslint-disable-next-line jest/no-disabled-tests
448 it.skip('adds extra function to the JS stack whose name includes the priority level', async () => {
449 function inferPriorityFromCallstack() {
450 try {
451 throw Error();
452 } catch (e) {
453 const stack = e.stack;
454 const lines = stack.split('\n');
455 for (let i = lines.length - 1; i >= 0; i--) {
456 const line = lines[i];
457 const found = line.match(
458 /scheduler_flushTaskAtPriority_([A-Za-z]+)/,
459 );
460 if (found !== null) {
461 const priorityStr = found[1];
462 switch (priorityStr) {
463 case 'Immediate':
464 return ImmediatePriority;
465 case 'UserBlocking':
466 return UserBlockingPriority;
467 case 'Normal':
468 return NormalPriority;
469 case 'Low':
470 return LowPriority;
471 case 'Idle':
472 return IdlePriority;
473 }
474 }
475 }
476 return null;
477 }
478 }
479
480 scheduleCallback(ImmediatePriority, () =>
481 Scheduler.log('Immediate: ' + inferPriorityFromCallstack()),
482 );
483 scheduleCallback(UserBlockingPriority, () =>
484 Scheduler.log('UserBlocking: ' + inferPriorityFromCallstack()),
485 );
486 scheduleCallback(NormalPriority, () =>
487 Scheduler.log('Normal: ' + inferPriorityFromCallstack()),
488 );
489 scheduleCallback(LowPriority, () =>
490 Scheduler.log('Low: ' + inferPriorityFromCallstack()),
491 );
492 scheduleCallback(IdlePriority, () =>
493 Scheduler.log('Idle: ' + inferPriorityFromCallstack()),
494 );
495
496 await waitForAll([
497 'Immediate: ' + ImmediatePriority,
498 'UserBlocking: ' + UserBlockingPriority,
499 'Normal: ' + NormalPriority,
500 'Low: ' + LowPriority,
501 'Idle: ' + IdlePriority,
502 ]);
503 });
504 }
505
506 describe('delayed tasks', () => {
507 it('schedules a delayed task', async () => {
508 scheduleCallback(NormalPriority, () => Scheduler.log('A'), {
509 delay: 1000,
510 });
511
512 // Should flush nothing, because delay hasn't elapsed
513 await waitForAll([]);
514
515 // Advance time until right before the threshold
516 Scheduler.unstable_advanceTime(999);
517 // Still nothing
518 await waitForAll([]);
519
520 // Advance time past the threshold
521 Scheduler.unstable_advanceTime(1);
522
523 // Now it should flush like normal
524 await waitForAll(['A']);
525 });
526
527 it('schedules multiple delayed tasks', async () => {
528 scheduleCallback(NormalPriority, () => Scheduler.log('C'), {
529 delay: 300,
530 });
531
532 scheduleCallback(NormalPriority, () => Scheduler.log('B'), {
533 delay: 200,
534 });
535
536 scheduleCallback(NormalPriority, () => Scheduler.log('D'), {
537 delay: 400,
538 });
539
540 scheduleCallback(NormalPriority, () => Scheduler.log('A'), {
541 delay: 100,
542 });
543
544 // Should flush nothing, because delay hasn't elapsed
545 await waitForAll([]);
546
547 // Advance some time.
548 Scheduler.unstable_advanceTime(200);
549 // Both A and B are no longer delayed. They can now flush incrementally.
550 await waitFor(['A']);
551 await waitForAll(['B']);
552
553 // Advance the rest
554 Scheduler.unstable_advanceTime(200);
555 await waitForAll(['C', 'D']);
556 });
557
558 it('interleaves normal tasks and delayed tasks', async () => {
559 // Schedule some high priority callbacks with a delay. When their delay
560 // elapses, they will be the most important callback in the queue.
561 scheduleCallback(UserBlockingPriority, () => Scheduler.log('Timer 2'), {
562 delay: 300,
563 });
564 scheduleCallback(UserBlockingPriority, () => Scheduler.log('Timer 1'), {
565 delay: 100,
566 });
567
568 // Schedule some tasks at default priority.
569 scheduleCallback(NormalPriority, () => {
570 Scheduler.log('A');
571 Scheduler.unstable_advanceTime(100);
572 });
573 scheduleCallback(NormalPriority, () => {
574 Scheduler.log('B');
575 Scheduler.unstable_advanceTime(100);
576 });
577 scheduleCallback(NormalPriority, () => {
578 Scheduler.log('C');
579 Scheduler.unstable_advanceTime(100);
580 });
581 scheduleCallback(NormalPriority, () => {
582 Scheduler.log('D');
583 Scheduler.unstable_advanceTime(100);
584 });
585
586 // Flush all the work. The timers should be interleaved with the
587 // other tasks.
588 await waitForAll(['A', 'Timer 1', 'B', 'C', 'Timer 2', 'D']);
589 });
590
591 it('interleaves delayed tasks with time-sliced tasks', async () => {
592 // Schedule some high priority callbacks with a delay. When their delay
593 // elapses, they will be the most important callback in the queue.
594 scheduleCallback(UserBlockingPriority, () => Scheduler.log('Timer 2'), {
595 delay: 300,
596 });
597 scheduleCallback(UserBlockingPriority, () => Scheduler.log('Timer 1'), {
598 delay: 100,
599 });
600
601 // Schedule a time-sliced task at default priority.
602 const tasks = [
603 ['A', 100],
604 ['B', 100],
605 ['C', 100],
606 ['D', 100],
607 ];
608 const work = () => {
609 while (tasks.length > 0) {
610 const task = tasks.shift();
611 const [label, ms] = task;
612 Scheduler.unstable_advanceTime(ms);
613 Scheduler.log(label);
614 if (tasks.length > 0) {
615 return work;
616 }
617 }
618 };
619 scheduleCallback(NormalPriority, work);
620
621 // Flush all the work. The timers should be interleaved with the
622 // other tasks.
623 await waitForAll(['A', 'Timer 1', 'B', 'C', 'Timer 2', 'D']);
624 });
625
626 it('cancels a delayed task', async () => {
627 // Schedule several tasks with the same delay
628 const options = {delay: 100};
629
630 scheduleCallback(NormalPriority, () => Scheduler.log('A'), options);
631 const taskB = scheduleCallback(
632 NormalPriority,
633 () => Scheduler.log('B'),
634 options,
635 );
636 const taskC = scheduleCallback(
637 NormalPriority,
638 () => Scheduler.log('C'),
639 options,
640 );
641
642 // Cancel B before its delay has elapsed
643 await waitForAll([]);
644 cancelCallback(taskB);
645
646 // Cancel C after its delay has elapsed
647 Scheduler.unstable_advanceTime(500);
648 cancelCallback(taskC);
649
650 // Only A should flush
651 await waitForAll(['A']);
652 });
653
654 it('gracefully handles scheduled tasks that are not a function', async () => {
655 scheduleCallback(ImmediatePriority, null);
656 await waitForAll([]);
657
658 scheduleCallback(ImmediatePriority, undefined);
659 await waitForAll([]);
660
661 scheduleCallback(ImmediatePriority, {});
662 await waitForAll([]);
663
664 scheduleCallback(ImmediatePriority, 42);
665 await waitForAll([]);
666 });
667
668 it('toFlushUntilNextPaint stops if a continuation is returned', async () => {
669 scheduleCallback(NormalPriority, () => {
670 Scheduler.log('Original Task');
671 Scheduler.log('shouldYield: ' + shouldYield());
672 Scheduler.log('Return a continuation');
673 return () => {
674 Scheduler.log('Continuation Task');
675 };
676 });
677
678 await waitForPaint([
679 'Original Task',
680 // Immediately before returning a continuation, `shouldYield` returns
681 // false, which means there must be time remaining in the frame.
682 'shouldYield: false',
683 'Return a continuation',
684
685 // The continuation should not flush yet.
686 ]);
687
688 // No time has elapsed
689 expect(Scheduler.unstable_now()).toBe(0);
690
691 // Continue the task
692 await waitForAll(['Continuation Task']);
693 });
694
695 it("toFlushAndYield keeps flushing even if there's a continuation", async () => {
696 scheduleCallback(NormalPriority, () => {
697 Scheduler.log('Original Task');
698 Scheduler.log('shouldYield: ' + shouldYield());
699 Scheduler.log('Return a continuation');
700 return () => {
701 Scheduler.log('Continuation Task');
702 };
703 });
704
705 await waitForAll([
706 'Original Task',
707 // Immediately before returning a continuation, `shouldYield` returns
708 // false, which means there must be time remaining in the frame.
709 'shouldYield: false',
710 'Return a continuation',
711
712 // The continuation should flush immediately, even though the task
713 // yielded a continuation.
714 'Continuation Task',
715 ]);
716 });
717 });
718 });