@samitouri / QOS-React-2 / commits / a2e05b6c14

[Scheduler] Delete old rAF implementation (#17252)

We've been using the message loop version for a while. Time to delete.

Andrew Clark committed Nov 1, 2019 at 13:32 UTC a2e05b6c148b25590884e8911d4d4acfcb76a487
6 files changed +327 -1495
packages/scheduler/src/SchedulerFeatureFlags.js
-1
@@ -8,5 +8,4 @@
8
9 export const enableSchedulerDebugging = false;
10 export const enableIsInputPending = false;
11 -export const enableMessageLoopImplementation = true;
11 export const enableProfiling = __PROFILE__;
packages/scheduler/src/__tests__/SchedulerBrowser-test.internal.js deleted
-493
@@ -1,493 +0,0 @@
1 -/**
2 - * Copyright (c) Facebook, Inc. and its 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 - * @jest-environment node
9 - */
10 -
11 -/* eslint-disable no-for-of-loops/no-for-of-loops */
12 -
13 -'use strict';
14 -
15 -let Scheduler;
16 -let runtime;
17 -let performance;
18 -let cancelCallback;
19 -let scheduleCallback;
20 -let NormalPriority;
21 -
22 -// The Scheduler implementation uses browser APIs like `MessageChannel`,
23 -// `requestAnimationFrame`, and `setTimeout` to schedule work on the main
24 -// thread. Most of our tests treat these as implementation details; however, the
25 -// sequence and timing of these APIs are not precisely specified, and can vary
26 -// wildly across browsers.
27 -//
28 -// To prevent regressions, we need the ability to simulate specific edge cases
29 -// that we may encounter in various browsers.
30 -//
31 -// This test suite mocks all browser methods used in our implementation. It
32 -// assumes as little as possible about the order and timing of events. The only
33 -// thing it assumes is that requestAnimationFrame is passed a frame time that is
34 -// equal to or less than the time returned by performance.now. Everything else
35 -// can be controlled at will.
36 -//
37 -// It also includes Scheduler-specific invariants, e.g. only one rAF callback
38 -// can be scheduled at a time.
39 -describe('SchedulerBrowser', () => {
40 - function beforeAndAfterHooks(enableMessageLoopImplementation) {
41 - beforeEach(() => {
42 - jest.resetModules();
43 -
44 - // Un-mock scheduler
45 - jest.mock('scheduler', () => require.requireActual('scheduler'));
46 - jest.mock('scheduler/src/SchedulerHostConfig', () =>
47 - require.requireActual(
48 - 'scheduler/src/forks/SchedulerHostConfig.default.js',
49 - ),
50 - );
51 -
52 - runtime = installMockBrowserRuntime();
53 - performance = window.performance;
54 - require('scheduler/src/SchedulerFeatureFlags').enableMessageLoopImplementation = enableMessageLoopImplementation;
55 - Scheduler = require('scheduler');
56 - cancelCallback = Scheduler.unstable_cancelCallback;
57 - scheduleCallback = Scheduler.unstable_scheduleCallback;
58 - NormalPriority = Scheduler.unstable_NormalPriority;
59 - });
60 -
61 - afterEach(() => {
62 - if (!runtime.isLogEmpty()) {
63 - throw Error('Test exited without clearing log.');
64 - }
65 - });
66 - }
67 -
68 - function installMockBrowserRuntime() {
69 - let VSYNC_INTERVAL = 33.33;
70 -
71 - let hasPendingMessageEvent = false;
72 -
73 - let rAFCallbackIDCounter = 0;
74 - let rAFCallback = null;
75 - let isRunningRAFCallback = false;
76 -
77 - let rICCallbackIDCounter = 0;
78 - let rICCallback = null;
79 -
80 - let timerIDCounter = 0;
81 - // let timerIDs = new Map();
82 -
83 - let eventLog = [];
84 -
85 - const window = {};
86 - global.window = window;
87 -
88 - let currentTime = 0;
89 -
90 - window.performance = {
91 - now() {
92 - return currentTime;
93 - },
94 - };
95 - window.requestAnimationFrame = cb => {
96 - if (rAFCallback !== null) {
97 - throw Error('rAF already scheduled');
98 - }
99 - if (isRunningRAFCallback) {
100 - log('Request Animation Frame [Reposted]');
101 - } else {
102 - log('Request Animation Frame');
103 - }
104 - rAFCallback = cb;
105 - return rAFCallbackIDCounter++;
106 - };
107 - window.cancelAnimationFrame = id => {
108 - rAFCallback = null;
109 - };
110 -
111 - window.requestIdleCallback = cb => {
112 - if (rICCallback !== null) {
113 - throw Error('rAF already scheduled');
114 - }
115 - log('Request Idle Callback');
116 - rICCallback = cb;
117 - return rICCallbackIDCounter++;
118 - };
119 - window.cancelIdleCallback = id => {
120 - rICCallback = null;
121 - };
122 -
123 - window.setTimeout = (cb, delay) => {
124 - const id = timerIDCounter++;
125 - log(`Set Timer`);
126 - // TODO
127 - return id;
128 - };
129 - window.clearTimeout = id => {
130 - // TODO
131 - };
132 -
133 - const port1 = {};
134 - const port2 = {
135 - postMessage() {
136 - if (hasPendingMessageEvent) {
137 - throw Error('Message event already scheduled');
138 - }
139 - log('Post Message');
140 - hasPendingMessageEvent = true;
141 - },
142 - };
143 - global.MessageChannel = function MessageChannel() {
144 - this.port1 = port1;
145 - this.port2 = port2;
146 - };
147 -
148 - function ensureLogIsEmpty() {
149 - if (eventLog.length !== 0) {
150 - throw Error('Log is not empty. Call assertLog before continuing.');
151 - }
152 - }
153 - function setHardwareFrameRate(fps) {
154 - VSYNC_INTERVAL = 1000 / fps;
155 - }
156 - function advanceTime(ms) {
157 - currentTime += ms;
158 - }
159 - function advanceTimeToNextFrame() {
160 - const targetTime =
161 - Math.ceil(currentTime / VSYNC_INTERVAL) * VSYNC_INTERVAL;
162 - if (targetTime === currentTime) {
163 - currentTime += VSYNC_INTERVAL;
164 - } else {
165 - currentTime = targetTime;
166 - }
167 - }
168 - function getMostRecentFrameNumber() {
169 - return Math.floor(currentTime / VSYNC_INTERVAL);
170 - }
171 - function fireMessageEvent() {
172 - ensureLogIsEmpty();
173 - if (!hasPendingMessageEvent) {
174 - throw Error('No message event was scheduled');
175 - }
176 - hasPendingMessageEvent = false;
177 - const onMessage = port1.onmessage;
178 - log('Message Event');
179 - onMessage();
180 - }
181 - function fireAnimationFrame() {
182 - ensureLogIsEmpty();
183 - if (isRunningRAFCallback) {
184 - throw Error('Cannot fire animation frame from inside another event.');
185 - }
186 - if (rAFCallback === null) {
187 - throw Error('No rAF scheduled.');
188 - }
189 - const mostRecentFrameNumber = getMostRecentFrameNumber();
190 - const rAFTime = mostRecentFrameNumber * VSYNC_INTERVAL;
191 - const cb = rAFCallback;
192 - rAFCallback = null;
193 - log(`Animation Frame [${mostRecentFrameNumber}]`);
194 - isRunningRAFCallback = true;
195 - try {
196 - cb(rAFTime);
197 - } finally {
198 - isRunningRAFCallback = false;
199 - }
200 - }
201 - function fireRIC() {
202 - ensureLogIsEmpty();
203 - if (rICCallback === null) {
204 - throw Error('No rIC scheduled.');
205 - }
206 - const cb = rICCallback;
207 - rICCallback = null;
208 - log('Idle Callback');
209 - cb();
210 - }
211 - function log(val) {
212 - eventLog.push(val);
213 - }
214 - function isLogEmpty() {
215 - return eventLog.length === 0;
216 - }
217 - function assertLog(expected) {
218 - const actual = eventLog;
219 - eventLog = [];
220 - expect(actual).toEqual(expected);
221 - }
222 - return {
223 - setHardwareFrameRate,
224 - advanceTime,
225 - advanceTimeToNextFrame,
226 - getMostRecentFrameNumber,
227 - fireMessageEvent,
228 - fireAnimationFrame,
229 - fireRIC,
230 - log,
231 - isLogEmpty,
232 - assertLog,
233 - };
234 - }
235 -
236 - describe('rAF aligned frame boundaries', () => {
237 - const enableMessageLoopImplementation = false;
238 - beforeAndAfterHooks(enableMessageLoopImplementation);
239 -
240 - it('callback with continuation', () => {
241 - scheduleCallback(NormalPriority, () => {
242 - runtime.log('Task');
243 - while (!Scheduler.unstable_shouldYield()) {
244 - runtime.advanceTime(1);
245 - }
246 - runtime.log(`Yield at ${performance.now()}ms`);
247 - return () => {
248 - runtime.log('Continuation');
249 - };
250 - });
251 - runtime.assertLog(['Request Animation Frame']);
252 -
253 - runtime.fireAnimationFrame();
254 - runtime.assertLog([
255 - 'Animation Frame [0]',
256 - 'Request Animation Frame [Reposted]',
257 - 'Set Timer',
258 - 'Post Message',
259 - ]);
260 - runtime.fireMessageEvent();
261 - runtime.assertLog(['Message Event', 'Task', 'Yield at 34ms']);
262 -
263 - runtime.fireAnimationFrame();
264 - runtime.assertLog([
265 - 'Animation Frame [1]',
266 - 'Request Animation Frame [Reposted]',
267 - 'Set Timer',
268 - 'Post Message',
269 - ]);
270 -
271 - runtime.fireMessageEvent();
272 - runtime.assertLog(['Message Event', 'Continuation']);
273 -
274 - runtime.advanceTimeToNextFrame();
275 - runtime.fireAnimationFrame();
276 - runtime.assertLog(['Animation Frame [2]']);
277 - });
278 -
279 - it('two rAF calls in the same frame', () => {
280 - scheduleCallback(NormalPriority, () => runtime.log('A'));
281 - runtime.assertLog(['Request Animation Frame']);
282 - runtime.fireAnimationFrame();
283 - runtime.assertLog([
284 - 'Animation Frame [0]',
285 - 'Request Animation Frame [Reposted]',
286 - 'Set Timer',
287 - 'Post Message',
288 - ]);
289 - runtime.fireMessageEvent();
290 - runtime.assertLog(['Message Event', 'A']);
291 -
292 - // The Scheduler queue is now empty. We're still in frame 0.
293 - expect(runtime.getMostRecentFrameNumber()).toBe(0);
294 -
295 - // Post a task to Scheduler.
296 - scheduleCallback(NormalPriority, () => runtime.log('B'));
297 -
298 - // Did not request another animation frame, since one was already scheduled
299 - // during the previous rAF.
300 - runtime.assertLog([]);
301 -
302 - // Fire the animation frame.
303 - runtime.fireAnimationFrame();
304 - runtime.assertLog([
305 - 'Animation Frame [0]',
306 - 'Request Animation Frame [Reposted]',
307 - 'Set Timer',
308 - 'Post Message',
309 - ]);
310 -
311 - runtime.fireMessageEvent();
312 - runtime.assertLog(['Message Event', 'B']);
313 - });
314 -
315 - it('adjusts frame rate by measuring interval between rAF events', () => {
316 - runtime.setHardwareFrameRate(60);
317 -
318 - scheduleCallback(NormalPriority, () => runtime.log('Tick'));
319 - runtime.assertLog(['Request Animation Frame']);
320 -
321 - // Need to measure two consecutive intervals between frames.
322 - for (let i = 0; i < 2; i++) {
323 - runtime.fireAnimationFrame();
324 - runtime.assertLog([
325 - `Animation Frame [${runtime.getMostRecentFrameNumber()}]`,
326 - 'Request Animation Frame [Reposted]',
327 - 'Set Timer',
328 - 'Post Message',
329 - ]);
330 - runtime.fireMessageEvent();
331 - runtime.assertLog(['Message Event', 'Tick']);
332 - scheduleCallback(NormalPriority, () => runtime.log('Tick'));
333 - runtime.advanceTimeToNextFrame();
334 - }
335 -
336 - // Scheduler should observe that it's receiving rAFs every 16.6 ms and
337 - // adjust its frame rate accordingly. Test by blocking the thread until
338 - // Scheduler tells us to yield. Then measure how much time has elapsed.
339 - const start = performance.now();
340 - scheduleCallback(NormalPriority, () => {
341 - while (!Scheduler.unstable_shouldYield()) {
342 - runtime.advanceTime(1);
343 - }
344 - });
345 - runtime.fireAnimationFrame();
346 - runtime.assertLog([
347 - `Animation Frame [${runtime.getMostRecentFrameNumber()}]`,
348 - 'Request Animation Frame [Reposted]',
349 - 'Set Timer',
350 - 'Post Message',
351 - ]);
352 - runtime.fireMessageEvent();
353 - runtime.assertLog(['Message Event', 'Tick']);
354 - const end = performance.now();
355 -
356 - // Check how much time elapsed in the frame.
357 - expect(end - start).toEqual(17);
358 - });
359 - });
360 -
361 - describe('message event loop', () => {
362 - const enableMessageLoopImplementation = true;
363 - beforeAndAfterHooks(enableMessageLoopImplementation);
364 -
365 - it('task that finishes before deadline', () => {
366 - scheduleCallback(NormalPriority, () => {
367 - runtime.log('Task');
368 - });
369 - runtime.assertLog(['Post Message']);
370 - runtime.fireMessageEvent();
371 - runtime.assertLog(['Message Event', 'Task']);
372 - });
373 -
374 - it('task with continuation', () => {
375 - scheduleCallback(NormalPriority, () => {
376 - runtime.log('Task');
377 - while (!Scheduler.unstable_shouldYield()) {
378 - runtime.advanceTime(1);
379 - }
380 - runtime.log(`Yield at ${performance.now()}ms`);
381 - return () => {
382 - runtime.log('Continuation');
383 - };
384 - });
385 - runtime.assertLog(['Post Message']);
386 -
387 - runtime.fireMessageEvent();
388 - runtime.assertLog([
389 - 'Message Event',
390 - 'Task',
391 - 'Yield at 5ms',
392 - 'Post Message',
393 - ]);
394 -
395 - runtime.fireMessageEvent();
396 - runtime.assertLog(['Message Event', 'Continuation']);
397 - });
398 -
399 - it('multiple tasks', () => {
400 - scheduleCallback(NormalPriority, () => {
401 - runtime.log('A');
402 - });
403 - scheduleCallback(NormalPriority, () => {
404 - runtime.log('B');
405 - });
406 - runtime.assertLog(['Post Message']);
407 - runtime.fireMessageEvent();
408 - runtime.assertLog(['Message Event', 'A', 'B']);
409 - });
410 -
411 - it('multiple tasks with a yield in between', () => {
412 - scheduleCallback(NormalPriority, () => {
413 - runtime.log('A');
414 - runtime.advanceTime(4999);
415 - });
416 - scheduleCallback(NormalPriority, () => {
417 - runtime.log('B');
418 - });
419 - runtime.assertLog(['Post Message']);
420 - runtime.fireMessageEvent();
421 - runtime.assertLog([
422 - 'Message Event',
423 - 'A',
424 - // Ran out of time. Post a continuation event.
425 - 'Post Message',
426 - ]);
427 - runtime.fireMessageEvent();
428 - runtime.assertLog(['Message Event', 'B']);
429 - });
430 -
431 - it('cancels tasks', () => {
432 - const task = scheduleCallback(NormalPriority, () => {
433 - runtime.log('Task');
434 - });
435 - runtime.assertLog(['Post Message']);
436 - cancelCallback(task);
437 - runtime.assertLog([]);
438 - });
439 -
440 - it('throws when a task errors then continues in a new event', () => {
441 - scheduleCallback(NormalPriority, () => {
442 - runtime.log('Oops!');
443 - throw Error('Oops!');
444 - });
445 - scheduleCallback(NormalPriority, () => {
446 - runtime.log('Yay');
447 - });
448 - runtime.assertLog(['Post Message']);
449 -
450 - expect(() => runtime.fireMessageEvent()).toThrow('Oops!');
451 - runtime.assertLog(['Message Event', 'Oops!', 'Post Message']);
452 -
453 - runtime.fireMessageEvent();
454 - runtime.assertLog(['Message Event', 'Yay']);
455 - });
456 -
457 - it('schedule new task after queue has emptied', () => {
458 - scheduleCallback(NormalPriority, () => {
459 - runtime.log('A');
460 - });
461 -
462 - runtime.assertLog(['Post Message']);
463 - runtime.fireMessageEvent();
464 - runtime.assertLog(['Message Event', 'A']);
465 -
466 - scheduleCallback(NormalPriority, () => {
467 - runtime.log('B');
468 - });
469 - runtime.assertLog(['Post Message']);
470 - runtime.fireMessageEvent();
471 - runtime.assertLog(['Message Event', 'B']);
472 - });
473 -
474 - it('schedule new task after a cancellation', () => {
475 - let handle = scheduleCallback(NormalPriority, () => {
476 - runtime.log('A');
477 - });
478 -
479 - runtime.assertLog(['Post Message']);
480 - cancelCallback(handle);
481 -
482 - runtime.fireMessageEvent();
483 - runtime.assertLog(['Message Event']);
484 -
485 - scheduleCallback(NormalPriority, () => {
486 - runtime.log('B');
487 - });
488 - runtime.assertLog(['Post Message']);
489 - runtime.fireMessageEvent();
490 - runtime.assertLog(['Message Event', 'B']);
491 - });
492 - });
493 -});
packages/scheduler/src/__tests__/SchedulerBrowser-test.js new
+272
@@ -0,0 +1,272 @@
1 +/**
2 + * Copyright (c) Facebook, Inc. and its 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 + * @jest-environment node
9 + */
10 +
11 +/* eslint-disable no-for-of-loops/no-for-of-loops */
12 +
13 +'use strict';
14 +
15 +let Scheduler;
16 +let runtime;
17 +let performance;
18 +let cancelCallback;
19 +let scheduleCallback;
20 +let NormalPriority;
21 +
22 +// The Scheduler implementation uses browser APIs like `MessageChannel` and
23 +// `setTimeout` to schedule work on the main thread. Most of our tests treat
24 +// these as implementation details; however, the sequence and timing of these
25 +// APIs are not precisely specified, and can vary across browsers.
26 +//
27 +// To prevent regressions, we need the ability to simulate specific edge cases
28 +// that we may encounter in various browsers.
29 +//
30 +// This test suite mocks all browser methods used in our implementation. It
31 +// assumes as little as possible about the order and timing of events.
32 +describe('SchedulerBrowser', () => {
33 + beforeEach(() => {
34 + jest.resetModules();
35 +
36 + // Un-mock scheduler
37 + jest.mock('scheduler', () => require.requireActual('scheduler'));
38 + jest.mock('scheduler/src/SchedulerHostConfig', () =>
39 + require.requireActual(
40 + 'scheduler/src/forks/SchedulerHostConfig.default.js',
41 + ),
42 + );
43 +
44 + runtime = installMockBrowserRuntime();
45 + performance = window.performance;
46 + Scheduler = require('scheduler');
47 + cancelCallback = Scheduler.unstable_cancelCallback;
48 + scheduleCallback = Scheduler.unstable_scheduleCallback;
49 + NormalPriority = Scheduler.unstable_NormalPriority;
50 + });
51 +
52 + afterEach(() => {
53 + if (!runtime.isLogEmpty()) {
54 + throw Error('Test exited without clearing log.');
55 + }
56 + });
57 +
58 + function installMockBrowserRuntime() {
59 + let hasPendingMessageEvent = false;
60 +
61 + let timerIDCounter = 0;
62 + // let timerIDs = new Map();
63 +
64 + let eventLog = [];
65 +
66 + const window = {};
67 + global.window = window;
68 +
69 + let currentTime = 0;
70 +
71 + window.performance = {
72 + now() {
73 + return currentTime;
74 + },
75 + };
76 +
77 + // TODO: Scheduler no longer requires these methods to be polyfilled. But
78 + // maybe we want to continue warning if they don't exist, to preserve the
79 + // option to rely on it in the future?
80 + window.requestAnimationFrame = window.cancelAnimationFrame = () => {};
81 +
82 + window.setTimeout = (cb, delay) => {
83 + const id = timerIDCounter++;
84 + log(`Set Timer`);
85 + // TODO
86 + return id;
87 + };
88 + window.clearTimeout = id => {
89 + // TODO
90 + };
91 +
92 + const port1 = {};
93 + const port2 = {
94 + postMessage() {
95 + if (hasPendingMessageEvent) {
96 + throw Error('Message event already scheduled');
97 + }
98 + log('Post Message');
99 + hasPendingMessageEvent = true;
100 + },
101 + };
102 + global.MessageChannel = function MessageChannel() {
103 + this.port1 = port1;
104 + this.port2 = port2;
105 + };
106 +
107 + function ensureLogIsEmpty() {
108 + if (eventLog.length !== 0) {
109 + throw Error('Log is not empty. Call assertLog before continuing.');
110 + }
111 + }
112 + function advanceTime(ms) {
113 + currentTime += ms;
114 + }
115 + function fireMessageEvent() {
116 + ensureLogIsEmpty();
117 + if (!hasPendingMessageEvent) {
118 + throw Error('No message event was scheduled');
119 + }
120 + hasPendingMessageEvent = false;
121 + const onMessage = port1.onmessage;
122 + log('Message Event');
123 + onMessage();
124 + }
125 + function log(val) {
126 + eventLog.push(val);
127 + }
128 + function isLogEmpty() {
129 + return eventLog.length === 0;
130 + }
131 + function assertLog(expected) {
132 + const actual = eventLog;
133 + eventLog = [];
134 + expect(actual).toEqual(expected);
135 + }
136 + return {
137 + advanceTime,
138 + fireMessageEvent,
139 + log,
140 + isLogEmpty,
141 + assertLog,
142 + };
143 + }
144 +
145 + it('task that finishes before deadline', () => {
146 + scheduleCallback(NormalPriority, () => {
147 + runtime.log('Task');
148 + });
149 + runtime.assertLog(['Post Message']);
150 + runtime.fireMessageEvent();
151 + runtime.assertLog(['Message Event', 'Task']);
152 + });
153 +
154 + it('task with continuation', () => {
155 + scheduleCallback(NormalPriority, () => {
156 + runtime.log('Task');
157 + while (!Scheduler.unstable_shouldYield()) {
158 + runtime.advanceTime(1);
159 + }
160 + runtime.log(`Yield at ${performance.now()}ms`);
161 + return () => {
162 + runtime.log('Continuation');
163 + };
164 + });
165 + runtime.assertLog(['Post Message']);
166 +
167 + runtime.fireMessageEvent();
168 + runtime.assertLog([
169 + 'Message Event',
170 + 'Task',
171 + 'Yield at 5ms',
172 + 'Post Message',
173 + ]);
174 +
175 + runtime.fireMessageEvent();
176 + runtime.assertLog(['Message Event', 'Continuation']);
177 + });
178 +
179 + it('multiple tasks', () => {
180 + scheduleCallback(NormalPriority, () => {
181 + runtime.log('A');
182 + });
183 + scheduleCallback(NormalPriority, () => {
184 + runtime.log('B');
185 + });
186 + runtime.assertLog(['Post Message']);
187 + runtime.fireMessageEvent();
188 + runtime.assertLog(['Message Event', 'A', 'B']);
189 + });
190 +
191 + it('multiple tasks with a yield in between', () => {
192 + scheduleCallback(NormalPriority, () => {
193 + runtime.log('A');
194 + runtime.advanceTime(4999);
195 + });
196 + scheduleCallback(NormalPriority, () => {
197 + runtime.log('B');
198 + });
199 + runtime.assertLog(['Post Message']);
200 + runtime.fireMessageEvent();
201 + runtime.assertLog([
202 + 'Message Event',
203 + 'A',
204 + // Ran out of time. Post a continuation event.
205 + 'Post Message',
206 + ]);
207 + runtime.fireMessageEvent();
208 + runtime.assertLog(['Message Event', 'B']);
209 + });
210 +
211 + it('cancels tasks', () => {
212 + const task = scheduleCallback(NormalPriority, () => {
213 + runtime.log('Task');
214 + });
215 + runtime.assertLog(['Post Message']);
216 + cancelCallback(task);
217 + runtime.assertLog([]);
218 + });
219 +
220 + it('throws when a task errors then continues in a new event', () => {
221 + scheduleCallback(NormalPriority, () => {
222 + runtime.log('Oops!');
223 + throw Error('Oops!');
224 + });
225 + scheduleCallback(NormalPriority, () => {
226 + runtime.log('Yay');
227 + });
228 + runtime.assertLog(['Post Message']);
229 +
230 + expect(() => runtime.fireMessageEvent()).toThrow('Oops!');
231 + runtime.assertLog(['Message Event', 'Oops!', 'Post Message']);
232 +
233 + runtime.fireMessageEvent();
234 + runtime.assertLog(['Message Event', 'Yay']);
235 + });
236 +
237 + it('schedule new task after queue has emptied', () => {
238 + scheduleCallback(NormalPriority, () => {
239 + runtime.log('A');
240 + });
241 +
242 + runtime.assertLog(['Post Message']);
243 + runtime.fireMessageEvent();
244 + runtime.assertLog(['Message Event', 'A']);
245 +
246 + scheduleCallback(NormalPriority, () => {
247 + runtime.log('B');
248 + });
249 + runtime.assertLog(['Post Message']);
250 + runtime.fireMessageEvent();
251 + runtime.assertLog(['Message Event', 'B']);
252 + });
253 +
254 + it('schedule new task after a cancellation', () => {
255 + let handle = scheduleCallback(NormalPriority, () => {
256 + runtime.log('A');
257 + });
258 +
259 + runtime.assertLog(['Post Message']);
260 + cancelCallback(handle);
261 +
262 + runtime.fireMessageEvent();
263 + runtime.assertLog(['Message Event']);
264 +
265 + scheduleCallback(NormalPriority, () => {
266 + runtime.log('B');
267 + });
268 + runtime.assertLog(['Post Message']);
269 + runtime.fireMessageEvent();
270 + runtime.assertLog(['Message Event', 'B']);
271 + });
272 +});
packages/scheduler/src/__tests__/SchedulerRAFOld-test.internal.js deleted
-818
@@ -1,818 +0,0 @@
1 -/**
2 - * Copyright (c) Facebook, Inc. and its 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 -type FrameTimeoutConfigType = {
14 - // should only specify one or the other
15 - timeLeftInFrame: ?number,
16 - timePastFrameDeadline: ?number,
17 -};
18 -
19 -// Note: this is testing the implementation that we turned off.
20 -// enableMessageLoopImplementation is true on master.
21 -describe('SchedulerRAFOld', () => {
22 - let rAFCallbacks = [];
23 - let postMessageCallback;
24 - let postMessageEvents = [];
25 - let postMessageErrors = [];
26 - let catchPostMessageErrors = false;
27 -
28 - function runPostMessageCallbacks(config: FrameTimeoutConfigType) {
29 - let timeLeftInFrame = 0;
30 - if (typeof config.timeLeftInFrame === 'number') {
31 - timeLeftInFrame = config.timeLeftInFrame;
32 - } else if (typeof config.timePastFrameDeadline === 'number') {
33 - timeLeftInFrame = -1 * config.timePastFrameDeadline;
34 - }
35 - currentTime = startOfLatestFrame + frameSize - timeLeftInFrame;
36 - if (postMessageCallback) {
37 - while (postMessageEvents.length) {
38 - if (catchPostMessageErrors) {
39 - // catch errors for testing error handling
40 - try {
41 - postMessageCallback(postMessageEvents.shift());
42 - } catch (e) {
43 - postMessageErrors.push(e);
44 - }
45 - } else {
46 - // we are not expecting errors
47 - postMessageCallback(postMessageEvents.shift());
48 - }
49 - }
50 - }
51 - }
52 - function runRAFCallbacks() {
53 - startOfLatestFrame += frameSize;
54 - currentTime = startOfLatestFrame;
55 - const cbs = rAFCallbacks;
56 - rAFCallbacks = [];
57 - cbs.forEach(cb => cb());
58 - }
59 - function advanceOneFrame(config: FrameTimeoutConfigType = {}) {
60 - runRAFCallbacks();
61 - runPostMessageCallbacks(config);
62 - }
63 -
64 - let frameSize;
65 - let startOfLatestFrame;
66 - let currentTime;
67 -
68 - beforeEach(() => {
69 - frameSize = 33;
70 - startOfLatestFrame = 0;
71 - currentTime = 0;
72 -
73 - delete global.performance;
74 - global.requestAnimationFrame = function(cb) {
75 - return rAFCallbacks.push(() => {
76 - cb(startOfLatestFrame);
77 - });
78 - };
79 - postMessageEvents = [];
80 - postMessageErrors = [];
81 - const port1 = {};
82 - const port2 = {
83 - postMessage(messageKey) {
84 - const postMessageEvent = {source: port2, data: messageKey};
85 - postMessageEvents.push(postMessageEvent);
86 - },
87 - };
88 - global.MessageChannel = function MessageChannel() {
89 - this.port1 = port1;
90 - this.port2 = port2;
91 - };
92 - postMessageCallback = () => port1.onmessage();
93 - global.Date.now = function() {
94 - return currentTime;
95 - };
96 - jest.resetModules();
97 -
98 - // Un-mock scheduler
99 - jest.mock('scheduler', () => require.requireActual('scheduler'));
100 - jest.mock('scheduler/src/SchedulerHostConfig', () =>
101 - require.requireActual(
102 - 'scheduler/src/forks/SchedulerHostConfig.default.js',
103 - ),
104 - );
105 -
106 - require('scheduler/src/SchedulerFeatureFlags').enableMessageLoopImplementation = false;
107 - Scheduler = require('scheduler');
108 - });
109 -
110 - describe('scheduleCallback', () => {
111 - it('calls the callback within the frame when not blocked', () => {
112 - const {
113 - unstable_scheduleCallback: scheduleCallback,
114 - unstable_NormalPriority: NormalPriority,
115 - } = Scheduler;
116 - const cb = jest.fn();
117 - scheduleCallback(NormalPriority, cb);
118 - advanceOneFrame({timeLeftInFrame: 15});
119 - expect(cb).toHaveBeenCalledTimes(1);
120 - });
121 -
122 - it('inserts its rAF callback as early into the queue as possible', () => {
123 - const {
124 - unstable_scheduleCallback: scheduleCallback,
125 - unstable_NormalPriority: NormalPriority,
126 - } = Scheduler;
127 - const log = [];
128 - const useRAFCallback = () => {
129 - log.push('userRAFCallback');
130 - };
131 - scheduleCallback(NormalPriority, () => {
132 - // Call rAF while idle work is being flushed.
133 - requestAnimationFrame(useRAFCallback);
134 - });
135 - advanceOneFrame({timeLeftInFrame: 1});
136 - // There should be two callbacks: the one scheduled by Scheduler at the
137 - // beginning of the frame, and the one scheduled later during that frame.
138 - expect(rAFCallbacks.length).toBe(2);
139 - // The user callback should be the second callback.
140 - rAFCallbacks[1]();
141 - expect(log).toEqual(['userRAFCallback']);
142 - });
143 -
144 - describe('with multiple callbacks', () => {
145 - it('accepts multiple callbacks and calls within frame when not blocked', () => {
146 - const {
147 - unstable_scheduleCallback: scheduleCallback,
148 - unstable_NormalPriority: NormalPriority,
149 - } = Scheduler;
150 - const callbackLog = [];
151 - const callbackA = jest.fn(() => callbackLog.push('A'));
152 - const callbackB = jest.fn(() => callbackLog.push('B'));
153 - scheduleCallback(NormalPriority, callbackA);
154 - // initially waits to call the callback
155 - expect(callbackLog).toEqual([]);
156 - // waits while second callback is passed
157 - scheduleCallback(NormalPriority, callbackB);
158 - expect(callbackLog).toEqual([]);
159 - // after a delay, calls as many callbacks as it has time for
160 - advanceOneFrame({timeLeftInFrame: 15});
161 - expect(callbackLog).toEqual(['A', 'B']);
162 - });
163 -
164 - it("accepts callbacks between animationFrame and postMessage and doesn't stall", () => {
165 - const {
166 - unstable_scheduleCallback: scheduleCallback,
167 - unstable_NormalPriority: NormalPriority,
168 - } = Scheduler;
169 - const callbackLog = [];
170 - const callbackA = jest.fn(() => callbackLog.push('A'));
171 - const callbackB = jest.fn(() => callbackLog.push('B'));
172 - const callbackC = jest.fn(() => callbackLog.push('C'));
173 - scheduleCallback(NormalPriority, callbackA);
174 - // initially waits to call the callback
175 - expect(callbackLog).toEqual([]);
176 - runRAFCallbacks();
177 - // this should schedule work *after* the requestAnimationFrame but before the message handler
178 - scheduleCallback(NormalPriority, callbackB);
179 - expect(callbackLog).toEqual([]);
180 - // now it should drain the message queue and do all scheduled work
181 - runPostMessageCallbacks({timeLeftInFrame: 15});
182 - expect(callbackLog).toEqual(['A', 'B']);
183 -
184 - // advances timers, now with an empty queue of work (to ensure they don't deadlock)
185 - advanceOneFrame({timeLeftInFrame: 15});
186 -
187 - // see if more work can be done now.
188 - scheduleCallback(NormalPriority, callbackC);
189 - expect(callbackLog).toEqual(['A', 'B']);
190 - advanceOneFrame({timeLeftInFrame: 15});
191 - expect(callbackLog).toEqual(['A', 'B', 'C']);
192 - });
193 -
194 - it(
195 - 'schedules callbacks in correct order and' +
196 - 'keeps calling them if there is time',
197 - () => {
198 - const {
199 - unstable_scheduleCallback: scheduleCallback,
200 - unstable_NormalPriority: NormalPriority,
201 - } = Scheduler;
202 - const callbackLog = [];
203 - const callbackA = jest.fn(() => {
204 - callbackLog.push('A');
205 - scheduleCallback(NormalPriority, callbackC);
206 - });
207 - const callbackB = jest.fn(() => {
208 - callbackLog.push('B');
209 - });
210 - const callbackC = jest.fn(() => {
211 - callbackLog.push('C');
212 - });
213 -
214 - scheduleCallback(NormalPriority, callbackA);
215 - // initially waits to call the callback
216 - expect(callbackLog).toEqual([]);
217 - // continues waiting while B is scheduled
218 - scheduleCallback(NormalPriority, callbackB);
219 - expect(callbackLog).toEqual([]);
220 - // after a delay, calls the scheduled callbacks,
221 - // and also calls new callbacks scheduled by current callbacks
222 - advanceOneFrame({timeLeftInFrame: 15});
223 - expect(callbackLog).toEqual(['A', 'B', 'C']);
224 - },
225 - );
226 -
227 - it('schedules callbacks in correct order when callbacks have many nested scheduleCallback calls', () => {
228 - const {
229 - unstable_scheduleCallback: scheduleCallback,
230 - unstable_NormalPriority: NormalPriority,
231 - } = Scheduler;
232 - const callbackLog = [];
233 - const callbackA = jest.fn(() => {
234 - callbackLog.push('A');
235 - scheduleCallback(NormalPriority, callbackC);
236 - scheduleCallback(NormalPriority, callbackD);
237 - });
238 - const callbackB = jest.fn(() => {
239 - callbackLog.push('B');
240 - scheduleCallback(NormalPriority, callbackE);
241 - scheduleCallback(NormalPriority, callbackF);
242 - });
243 - const callbackC = jest.fn(() => {
244 - callbackLog.push('C');
245 - });
246 - const callbackD = jest.fn(() => {
247 - callbackLog.push('D');
248 - });
249 - const callbackE = jest.fn(() => {
250 - callbackLog.push('E');
251 - });
252 - const callbackF = jest.fn(() => {
253 - callbackLog.push('F');
254 - });
255 -
256 - scheduleCallback(NormalPriority, callbackA);
257 - scheduleCallback(NormalPriority, callbackB);
258 - // initially waits to call the callback
259 - expect(callbackLog).toEqual([]);
260 - // while flushing callbacks, calls as many as it has time for
261 - advanceOneFrame({timeLeftInFrame: 15});
262 - expect(callbackLog).toEqual(['A', 'B', 'C', 'D', 'E', 'F']);
263 - });
264 -
265 - it('schedules callbacks in correct order when they use scheduleCallback to schedule themselves', () => {
266 - const {
267 - unstable_scheduleCallback: scheduleCallback,
268 - unstable_NormalPriority: NormalPriority,
269 - } = Scheduler;
270 - const callbackLog = [];
271 - let callbackAIterations = 0;
272 - const callbackA = jest.fn(() => {
273 - if (callbackAIterations < 1) {
274 - scheduleCallback(NormalPriority, callbackA);
275 - }
276 - callbackLog.push('A' + callbackAIterations);
277 - callbackAIterations++;
278 - });
279 - const callbackB = jest.fn(() => callbackLog.push('B'));
280 -
281 - scheduleCallback(NormalPriority, callbackA);
282 - // initially waits to call the callback
283 - expect(callbackLog).toEqual([]);
284 - scheduleCallback(NormalPriority, callbackB);
285 - expect(callbackLog).toEqual([]);
286 - // after a delay, calls the latest callback passed
287 - advanceOneFrame({timeLeftInFrame: 15});
288 - expect(callbackLog).toEqual(['A0', 'B', 'A1']);
289 - });
290 - });
291 -
292 - describe('when callbacks time out: ', () => {
293 - // USEFUL INFO:
294 - // startOfLatestFrame is a global that goes up every time rAF runs
295 - // currentTime defaults to startOfLatestFrame inside rAF callback
296 - // and currentTime defaults to 15 before next frame inside idleTick
297 -
298 - describe('when there is no more time left in the frame', () => {
299 - it('calls any callback which has timed out, waits for others', () => {
300 - const {
301 - unstable_scheduleCallback: scheduleCallback,
302 - unstable_NormalPriority: NormalPriority,
303 - } = Scheduler;
304 - startOfLatestFrame = 1000000000000;
305 - currentTime = startOfLatestFrame - 10;
306 - const callbackLog = [];
307 - // simple case of one callback which times out, another that won't.
308 - const callbackA = jest.fn(() => callbackLog.push('A'));
309 - const callbackB = jest.fn(() => callbackLog.push('B'));
310 - const callbackC = jest.fn(() => callbackLog.push('C'));
311 -
312 - scheduleCallback(NormalPriority, callbackA); // won't time out
313 - scheduleCallback(NormalPriority, callbackB, {timeout: 100}); // times out later
314 - scheduleCallback(NormalPriority, callbackC, {timeout: 2}); // will time out fast
315 -
316 - // push time ahead a bit so that we have no idle time
317 - advanceOneFrame({timePastFrameDeadline: 16});
318 -
319 - // callbackC should have timed out
320 - expect(callbackLog).toEqual(['C']);
321 -
322 - // push time ahead a bit so that we have no idle time
323 - advanceOneFrame({timePastFrameDeadline: 16});
324 -
325 - // callbackB should have timed out
326 - expect(callbackLog).toEqual(['C', 'B']);
327 -
328 - // let's give ourselves some idle time now
329 - advanceOneFrame({timeLeftInFrame: 16});
330 -
331 - // we should have run callbackA in the idle time
332 - expect(callbackLog).toEqual(['C', 'B', 'A']);
333 - });
334 - });
335 -
336 - describe('when there is some time left in the frame', () => {
337 - it('calls timed out callbacks and then any more pending callbacks, defers others if time runs out', () => {
338 - const {
339 - unstable_scheduleCallback: scheduleCallback,
340 - unstable_NormalPriority: NormalPriority,
341 - } = Scheduler;
342 - startOfLatestFrame = 1000000000000;
343 - currentTime = startOfLatestFrame - 10;
344 - const callbackLog = [];
345 - // simple case of one callback which times out, others that won't.
346 - const callbackA = jest.fn(() => {
347 - callbackLog.push('A');
348 - // time passes, causing us to run out of idle time
349 - currentTime += 25;
350 - });
351 - const callbackB = jest.fn(() => callbackLog.push('B'));
352 - const callbackC = jest.fn(() => callbackLog.push('C'));
353 - const callbackD = jest.fn(() => callbackLog.push('D'));
354 -
355 - scheduleCallback(NormalPriority, callbackA, {timeout: 100}); // won't time out
356 - scheduleCallback(NormalPriority, callbackB, {timeout: 100}); // times out later
357 - scheduleCallback(NormalPriority, callbackC, {timeout: 2}); // will time out fast
358 - scheduleCallback(NormalPriority, callbackD, {timeout: 200}); // won't time out
359 -
360 - advanceOneFrame({timeLeftInFrame: 15}); // runs rAF and postMessage callbacks
361 -
362 - // callbackC should have timed out
363 - // we should have had time to call A also, then we run out of time
364 - expect(callbackLog).toEqual(['C', 'A']);
365 -
366 - // push time ahead a bit so that we have no idle time
367 - advanceOneFrame({timePastFrameDeadline: 16});
368 -
369 - // callbackB should have timed out
370 - // but we should not run callbackD because we have no idle time
371 - expect(callbackLog).toEqual(['C', 'A', 'B']);
372 -
373 - advanceOneFrame({timeLeftInFrame: 15}); // runs rAF and postMessage callbacks
374 -
375 - // we should have run callbackD in the idle time
376 - expect(callbackLog).toEqual(['C', 'A', 'B', 'D']);
377 -
378 - advanceOneFrame({timeLeftInFrame: 15}); // runs rAF and postMessage callbacks
379 -
380 - // we should not have run anything again, nothing is scheduled
381 - expect(callbackLog).toEqual(['C', 'A', 'B', 'D']);
382 - });
383 - });
384 - });
385 - });
386 -
387 - describe('cancelCallback', () => {
388 - it('cancels the scheduled callback', () => {
389 - const {
390 - unstable_scheduleCallback: scheduleCallback,
391 - unstable_cancelCallback: cancelCallback,
392 - unstable_NormalPriority: NormalPriority,
393 - } = Scheduler;
394 - const cb = jest.fn();
395 - const callbackId = scheduleCallback(NormalPriority, cb);
396 - expect(cb).toHaveBeenCalledTimes(0);
397 - cancelCallback(callbackId);
398 - advanceOneFrame({timeLeftInFrame: 15});
399 - expect(cb).toHaveBeenCalledTimes(0);
400 - });
401 -
402 - describe('with multiple callbacks', () => {
403 - it('when called more than once', () => {
404 - const {
405 - unstable_scheduleCallback: scheduleCallback,
406 - unstable_cancelCallback: cancelCallback,
407 - unstable_NormalPriority: NormalPriority,
408 - } = Scheduler;
409 - const callbackLog = [];
410 - const callbackA = jest.fn(() => callbackLog.push('A'));
411 - const callbackB = jest.fn(() => callbackLog.push('B'));
412 - const callbackC = jest.fn(() => callbackLog.push('C'));
413 - scheduleCallback(NormalPriority, callbackA);
414 - const callbackId = scheduleCallback(NormalPriority, callbackB);
415 - scheduleCallback(NormalPriority, callbackC);
416 - cancelCallback(callbackId);
417 - cancelCallback(callbackId);
418 - cancelCallback(callbackId);
419 - // Initially doesn't call anything
420 - expect(callbackLog).toEqual([]);
421 - advanceOneFrame({timeLeftInFrame: 15});
422 -
423 - // Should still call A and C
424 - expect(callbackLog).toEqual(['A', 'C']);
425 - expect(callbackB).toHaveBeenCalledTimes(0);
426 - });
427 -
428 - it('when one callback cancels the next one', () => {
429 - const {
430 - unstable_scheduleCallback: scheduleCallback,
431 - unstable_cancelCallback: cancelCallback,
432 - unstable_NormalPriority: NormalPriority,
433 - } = Scheduler;
434 - const callbackLog = [];
435 - let callbackBId;
436 - const callbackA = jest.fn(() => {
437 - callbackLog.push('A');
438 - cancelCallback(callbackBId);
439 - });
440 - const callbackB = jest.fn(() => callbackLog.push('B'));
441 - scheduleCallback(NormalPriority, callbackA);
442 - callbackBId = scheduleCallback(NormalPriority, callbackB);
443 - // Initially doesn't call anything
444 - expect(callbackLog).toEqual([]);
445 - advanceOneFrame({timeLeftInFrame: 15});
446 - // B should not get called because A cancelled B
447 - expect(callbackLog).toEqual(['A']);
448 - expect(callbackB).toHaveBeenCalledTimes(0);
449 - });
450 - });
451 - });
452 -
453 - describe('when callbacks throw errors', () => {
454 - describe('when some callbacks throw', () => {
455 - /**
456 - * + +
457 - * | rAF postMessage |
458 - * | |
459 - * | +---------------------+ |
460 - * | | paint/layout | cbA() cbB() cbC() cbD() cbE() |
461 - * | +---------------------+ ^ ^ |
462 - * | | | |
463 - * + | | +
464 - * + +
465 - * throw errors
466 - *
467 - *
468 - */
469 - it('still calls all callbacks within same frame', () => {
470 - const {
471 - unstable_scheduleCallback: scheduleCallback,
472 - unstable_NormalPriority: NormalPriority,
473 - } = Scheduler;
474 - const callbackLog = [];
475 - const callbackA = jest.fn(() => callbackLog.push('A'));
476 - const callbackB = jest.fn(() => {
477 - callbackLog.push('B');
478 - throw new Error('B error');
479 - });
480 - const callbackC = jest.fn(() => callbackLog.push('C'));
481 - const callbackD = jest.fn(() => {
482 - callbackLog.push('D');
483 - throw new Error('D error');
484 - });
485 - const callbackE = jest.fn(() => callbackLog.push('E'));
486 - scheduleCallback(NormalPriority, callbackA);
487 - scheduleCallback(NormalPriority, callbackB);
488 - scheduleCallback(NormalPriority, callbackC);
489 - scheduleCallback(NormalPriority, callbackD);
490 - scheduleCallback(NormalPriority, callbackE);
491 - // Initially doesn't call anything
492 - expect(callbackLog).toEqual([]);
493 - catchPostMessageErrors = true;
494 - advanceOneFrame({timeLeftInFrame: 15});
495 - // calls all callbacks
496 - expect(callbackLog).toEqual(['A', 'B', 'C', 'D', 'E']);
497 - // errors should still get thrown
498 - const postMessageErrorMessages = postMessageErrors.map(e => e.message);
499 - expect(postMessageErrorMessages).toEqual(['B error', 'D error']);
500 - catchPostMessageErrors = false;
501 - });
502 -
503 - /**
504 - * timed out
505 - * + + +--+
506 - * + rAF postMessage | | | +
507 - * | | | | |
508 - * | +---------------------+ v v v |
509 - * | | paint/layout | cbA() cbB() cbC() cbD() cbE() |
510 - * | +---------------------+ ^ ^ |
511 - * | | | |
512 - * + | | +
513 - * + +
514 - * throw errors
515 - *
516 - *
517 - */
518 - it('and with some timed out callbacks, still calls all callbacks within same frame', () => {
519 - const {
520 - unstable_scheduleCallback: scheduleCallback,
521 - unstable_NormalPriority: NormalPriority,
522 - } = Scheduler;
523 - const callbackLog = [];
524 - const callbackA = jest.fn(() => {
525 - callbackLog.push('A');
526 - throw new Error('A error');
527 - });
528 - const callbackB = jest.fn(() => callbackLog.push('B'));
529 - const callbackC = jest.fn(() => callbackLog.push('C'));
530 - const callbackD = jest.fn(() => {
531 - callbackLog.push('D');
532 - throw new Error('D error');
533 - });
534 - const callbackE = jest.fn(() => callbackLog.push('E'));
535 - scheduleCallback(NormalPriority, callbackA);
536 - scheduleCallback(NormalPriority, callbackB);
537 - scheduleCallback(NormalPriority, callbackC, {timeout: 2}); // times out fast
538 - scheduleCallback(NormalPriority, callbackD, {timeout: 2}); // times out fast
539 - scheduleCallback(NormalPriority, callbackE, {timeout: 2}); // times out fast
540 - // Initially doesn't call anything
541 - expect(callbackLog).toEqual([]);
542 - catchPostMessageErrors = true;
543 - advanceOneFrame({timeLeftInFrame: 15});
544 - // calls all callbacks; calls timed out ones first
545 - expect(callbackLog).toEqual(['C', 'D', 'E', 'A', 'B']);
546 - // errors should still get thrown
547 - const postMessageErrorMessages = postMessageErrors.map(e => e.message);
548 - expect(postMessageErrorMessages).toEqual(['D error', 'A error']);
549 - catchPostMessageErrors = false;
550 - });
551 - });
552 - describe('when all scheduled callbacks throw', () => {
553 - /**
554 - * + +
555 - * | rAF postMessage |
556 - * | |
557 - * | +---------------------+ |
558 - * | | paint/layout | cbA() cbB() cbC() cbD() cbE() |
559 - * | +---------------------+ ^ ^ ^ ^ ^ |
560 - * | | | | | | |
561 - * + | | | | | +
562 - * | + + + +
563 - * + all callbacks throw errors
564 - *
565 - *
566 - */
567 - it('still calls all callbacks within same frame', () => {
568 - const {
569 - unstable_scheduleCallback: scheduleCallback,
570 - unstable_NormalPriority: NormalPriority,
571 - } = Scheduler;
572 - const callbackLog = [];
573 - const callbackA = jest.fn(() => {
574 - callbackLog.push('A');
575 - throw new Error('A error');
576 - });
577 - const callbackB = jest.fn(() => {
578 - callbackLog.push('B');
579 - throw new Error('B error');
580 - });
581 - const callbackC = jest.fn(() => {
582 - callbackLog.push('C');
583 - throw new Error('C error');
584 - });
585 - const callbackD = jest.fn(() => {
586 - callbackLog.push('D');
587 - throw new Error('D error');
588 - });
589 - const callbackE = jest.fn(() => {
590 - callbackLog.push('E');
591 - throw new Error('E error');
592 - });
593 - scheduleCallback(NormalPriority, callbackA);
594 - scheduleCallback(NormalPriority, callbackB);
595 - scheduleCallback(NormalPriority, callbackC);
596 - scheduleCallback(NormalPriority, callbackD);
597 - scheduleCallback(NormalPriority, callbackE);
598 - // Initially doesn't call anything
599 - expect(callbackLog).toEqual([]);
600 - catchPostMessageErrors = true;
601 - advanceOneFrame({timeLeftInFrame: 15});
602 - // calls all callbacks
603 - expect(callbackLog).toEqual(['A', 'B', 'C', 'D', 'E']);
604 - // errors should still get thrown
605 - const postMessageErrorMessages = postMessageErrors.map(e => e.message);
606 - expect(postMessageErrorMessages).toEqual([
607 - 'A error',
608 - 'B error',
609 - 'C error',
610 - 'D error',
611 - 'E error',
612 - ]);
613 - catchPostMessageErrors = false;
614 - });
615 -
616 - /**
617 - * postMessage
618 - * + +
619 - * | rAF all callbacks time out |
620 - * | |
621 - * | +---------------------+ |
622 - * | | paint/layout | cbA() cbB() cbC() cbD() cbE() |
623 - * | +---------------------+ ^ ^ ^ ^ ^ |
624 - * | | | | | | |
625 - * + | | | | | +
626 - * | + + + +
627 - * + all callbacks throw errors
628 - *
629 - *
630 - */
631 - it('and with all timed out callbacks, still calls all callbacks within same frame', () => {
632 - const {
633 - unstable_scheduleCallback: scheduleCallback,
634 - unstable_NormalPriority: NormalPriority,
635 - } = Scheduler;
636 - const callbackLog = [];
637 - const callbackA = jest.fn(() => {
638 - callbackLog.push('A');
639 - throw new Error('A error');
640 - });
641 - const callbackB = jest.fn(() => {
642 - callbackLog.push('B');
643 - throw new Error('B error');
644 - });
645 - const callbackC = jest.fn(() => {
646 - callbackLog.push('C');
647 - throw new Error('C error');
648 - });
649 - const callbackD = jest.fn(() => {
650 - callbackLog.push('D');
651 - throw new Error('D error');
652 - });
653 - const callbackE = jest.fn(() => {
654 - callbackLog.push('E');
655 - throw new Error('E error');
656 - });
657 - scheduleCallback(NormalPriority, callbackA, {timeout: 2}); // times out fast
658 - scheduleCallback(NormalPriority, callbackB, {timeout: 2}); // times out fast
659 - scheduleCallback(NormalPriority, callbackC, {timeout: 2}); // times out fast
660 - scheduleCallback(NormalPriority, callbackD, {timeout: 2}); // times out fast
661 - scheduleCallback(NormalPriority, callbackE, {timeout: 2}); // times out fast
662 - // Initially doesn't call anything
663 - expect(callbackLog).toEqual([]);
664 - catchPostMessageErrors = true;
665 - advanceOneFrame({timeLeftInFrame: 15});
666 - // calls all callbacks
667 - expect(callbackLog).toEqual(['A', 'B', 'C', 'D', 'E']);
668 - // errors should still get thrown
669 - const postMessageErrorMessages = postMessageErrors.map(e => e.message);
670 - expect(postMessageErrorMessages).toEqual([
671 - 'A error',
672 - 'B error',
673 - 'C error',
674 - 'D error',
675 - 'E error',
676 - ]);
677 - catchPostMessageErrors = false;
678 - });
679 - });
680 - describe('when callbacks throw over multiple frames', () => {
681 - /**
682 - *
683 - * **Detail View of Frame 1**
684 - *
685 - * + +
686 - * | rAF postMessage |
687 - * | |
688 - * | +---------------------+ |
689 - * | | paint/layout | cbA() cbB() | ... Frame 2
690 - * | +---------------------+ ^ ^ |
691 - * | | | |
692 - * + + | +
693 - * errors |
694 - * +
695 - * takes long time
696 - * and pushes rest of
697 - * callbacks into
698 - * next frame ->
699 - *
700 - *
701 - *
702 - * **Overview of frames 1-4**
703 - *
704 - *
705 - * + + + + +
706 - * | | | | |
707 - * | +--+ | +--+ | +--+ | +--+ |
708 - * | +--+ A,B+-> +--+ C,D+-> +--+ E,F+-> +--+ G |
709 - * + ^ + ^ + ^ + +
710 - * | | |
711 - * error error error
712 - *
713 - *
714 - */
715 - it('still calls all callbacks within same frame', () => {
716 - const {
717 - unstable_scheduleCallback: scheduleCallback,
718 - unstable_NormalPriority: NormalPriority,
719 - } = Scheduler;
720 - startOfLatestFrame = 1000000000000;
721 - currentTime = startOfLatestFrame - 10;
722 - catchPostMessageErrors = true;
723 - const callbackLog = [];
724 - const callbackA = jest.fn(() => {
725 - callbackLog.push('A');
726 - throw new Error('A error');
727 - });
728 - const callbackB = jest.fn(() => {
729 - callbackLog.push('B');
730 - // time passes, causing us to run out of idle time
731 - currentTime += 25;
732 - });
733 - const callbackC = jest.fn(() => {
734 - callbackLog.push('C');
735 - throw new Error('C error');
736 - });
737 - const callbackD = jest.fn(() => {
738 - callbackLog.push('D');
739 - // time passes, causing us to run out of idle time
740 - currentTime += 25;
741 - });
742 - const callbackE = jest.fn(() => {
743 - callbackLog.push('E');
744 - throw new Error('E error');
745 - });
746 - const callbackF = jest.fn(() => {
747 - callbackLog.push('F');
748 - // time passes, causing us to run out of idle time
749 - currentTime += 25;
750 - });
751 - const callbackG = jest.fn(() => callbackLog.push('G'));
752 -
753 - scheduleCallback(NormalPriority, callbackA);
754 - scheduleCallback(NormalPriority, callbackB);
755 - scheduleCallback(NormalPriority, callbackC);
756 - scheduleCallback(NormalPriority, callbackD);
757 - scheduleCallback(NormalPriority, callbackE);
758 - scheduleCallback(NormalPriority, callbackF);
759 - scheduleCallback(NormalPriority, callbackG);
760 -
761 - // does nothing initially
762 - expect(callbackLog).toEqual([]);
763 -
764 - // frame 1;
765 - // callback A runs and throws, callback B takes up rest of frame
766 - advanceOneFrame({timeLeftInFrame: 15}); // runs rAF and postMessage callbacks
767 -
768 - // calls A and B
769 - expect(callbackLog).toEqual(['A', 'B']);
770 - // error was thrown from A
771 - let postMessageErrorMessages = postMessageErrors.map(e => e.message);
772 - expect(postMessageErrorMessages).toEqual(['A error']);
773 -
774 - // frame 2;
775 - // callback C runs and throws, callback D takes up rest of frame
776 - advanceOneFrame({timeLeftInFrame: 15}); // runs rAF and postMessage callbacks
777 -
778 - // calls C and D
779 - expect(callbackLog).toEqual(['A', 'B', 'C', 'D']);
780 - // error was thrown from A
781 - postMessageErrorMessages = postMessageErrors.map(e => e.message);
782 - expect(postMessageErrorMessages).toEqual(['A error', 'C error']);
783 -
784 - // frame 3;
785 - // callback E runs and throws, callback F takes up rest of frame
786 - advanceOneFrame({timeLeftInFrame: 15}); // runs rAF and postMessage callbacks
787 -
788 - // calls E and F
789 - expect(callbackLog).toEqual(['A', 'B', 'C', 'D', 'E', 'F']);
790 - // error was thrown from A
791 - postMessageErrorMessages = postMessageErrors.map(e => e.message);
792 - expect(postMessageErrorMessages).toEqual([
793 - 'A error',
794 - 'C error',
795 - 'E error',
796 - ]);
797 -
798 - // frame 4;
799 - // callback G runs and it's the last one
800 - advanceOneFrame({timeLeftInFrame: 15}); // runs rAF and postMessage callbacks
801 -
802 - // calls G
803 - expect(callbackLog).toEqual(['A', 'B', 'C', 'D', 'E', 'F', 'G']);
804 - // error was thrown from A
805 - postMessageErrorMessages = postMessageErrors.map(e => e.message);
806 - expect(postMessageErrorMessages).toEqual([
807 - 'A error',
808 - 'C error',
809 - 'E error',
810 - ]);
811 -
812 - catchPostMessageErrors = true;
813 - });
814 - });
815 - });
816 -
817 - // TODO: test 'now'
818 -});
packages/scheduler/src/forks/SchedulerFeatureFlags.www.js
-1
@@ -13,4 +13,3 @@ export const {
13 } = require('SchedulerFeatureFlags');
14
15 export const enableProfiling = __PROFILE__ && enableProfilingFeatureFlag;
16 -export const enableMessageLoopImplementation = true;
packages/scheduler/src/forks/SchedulerHostConfig.default.js
+55 -182
@@ -5,18 +5,7 @@
5 * LICENSE file in the root directory of this source tree.
6 */
7
8 -import {
9 - enableIsInputPending,
10 - enableMessageLoopImplementation,
11 -} from '../SchedulerFeatureFlags';
12 -
13 -// The DOM Scheduler implementation is similar to requestIdleCallback. It
14 -// works by scheduling a requestAnimationFrame, storing the time for the start
15 -// of the frame, then scheduling a postMessage which gets scheduled after paint.
16 -// Within the postMessage handler do as much work as possible until time + frame
17 -// rate. By separating the idle call into a separate event tick we ensure that
18 -// layout, paint and other browser work is counted against the available time.
19 -// The frame rate is dynamically adjusted.
8 +import {enableIsInputPending} from '../SchedulerFeatureFlags';
9
10 export let requestHostCallback;
11 export let cancelHostCallback;
@@ -83,10 +72,13 @@ if (
72 const Date = window.Date;
73 const setTimeout = window.setTimeout;
74 const clearTimeout = window.clearTimeout;
86 - const requestAnimationFrame = window.requestAnimationFrame;
87 - const cancelAnimationFrame = window.cancelAnimationFrame;
75
76 if (typeof console !== 'undefined') {
77 + // TODO: Scheduler no longer requires these methods to be polyfilled. But
78 + // maybe we want to continue warning if they don't exist, to preserve the
79 + // option to rely on it in the future?
80 + const requestAnimationFrame = window.requestAnimationFrame;
81 + const cancelAnimationFrame = window.cancelAnimationFrame;
82 // TODO: Remove fb.me link
83 if (typeof requestAnimationFrame !== 'function') {
84 console.error(
@@ -114,32 +106,20 @@ if (
106 getCurrentTime = () => Date.now() - initialTime;
107 }
108
117 - let isRAFLoopRunning = false;
109 let isMessageLoopRunning = false;
110 let scheduledHostCallback = null;
120 - let rAFTimeoutID = -1;
111 let taskTimeoutID = -1;
112
123 - let frameLength = enableMessageLoopImplementation
124 - ? // We won't attempt to align with the vsync. Instead we'll yield multiple
125 - // times per frame, often enough to keep it responsive even at really
126 - // high frame rates > 120.
127 - 5
128 - : // Use a heuristic to measure the frame rate and yield at the end of the
129 - // frame. We start out assuming that we run at 30fps but then the
130 - // heuristic tracking will adjust this value to a faster fps if we get
131 - // more frequent animation frames.
132 - 33.33;
133 -
134 - let prevRAFTime = -1;
135 - let prevRAFInterval = -1;
136 - let frameDeadline = 0;
137 -
138 - let fpsLocked = false;
113 + // Scheduler periodically yields in case there is other work on the main
114 + // thread, like user events. By default, it yields multiple times per frame.
115 + // It does not attempt to align with frame boundaries, since most tasks don't
116 + // need to be frame aligned; for those that do, use requestAnimationFrame.
117 + let yieldInterval = 5;
118 + let deadline = 0;
119
120 // TODO: Make this configurable
121 // TODO: Adjust this based on priority?
142 - let maxFrameLength = 300;
122 + let maxYieldInterval = 300;
123 let needsPaint = false;
124
125 if (
@@ -151,13 +131,13 @@ if (
131 const scheduling = navigator.scheduling;
132 shouldYieldToHost = function() {
133 const currentTime = getCurrentTime();
154 - if (currentTime >= frameDeadline) {
155 - // There's no time left in the frame. We may want to yield control of
156 - // the main thread, so the browser can perform high priority tasks. The
157 - // main ones are painting and user input. If there's a pending paint or
158 - // a pending input, then we should yield. But if there's neither, then
159 - // we can yield less often while remaining responsive. We'll eventually
160 - // yield regardless, since there could be a pending paint that wasn't
134 + if (currentTime >= deadline) {
135 + // There's no time left. We may want to yield control of the main
136 + // thread, so the browser can perform high priority tasks. The main ones
137 + // are painting and user input. If there's a pending paint or a pending
138 + // input, then we should yield. But if there's neither, then we can
139 + // yield less often while remaining responsive. We'll eventually yield
140 + // regardless, since there could be a pending paint that wasn't
141 // accompanied by a call to `requestPaint`, or other main thread tasks
142 // like network events.
143 if (needsPaint || scheduling.isInputPending()) {
@@ -165,8 +145,8 @@ if (
145 return true;
146 }
147 // There's no pending input. Only yield if we've reached the max
168 - // frame length.
169 - return currentTime >= frameDeadline + maxFrameLength;
148 + // yield interval.
149 + return currentTime >= maxYieldInterval;
150 } else {
151 // There's still time left in the frame.
152 return false;
@@ -180,7 +160,7 @@ if (
160 // `isInputPending` is not available. Since we have no way of knowing if
161 // there's pending input, always yield at the end of the frame.
162 shouldYieldToHost = function() {
183 - return getCurrentTime() >= frameDeadline;
163 + return getCurrentTime() >= deadline;
164 };
165
166 // Since we yield every frame regardless, `requestPaint` has no effect.
@@ -196,164 +176,57 @@ if (
176 return;
177 }
178 if (fps > 0) {
199 - frameLength = Math.floor(1000 / fps);
200 - fpsLocked = true;
179 + yieldInterval = Math.floor(1000 / fps);
180 } else {
181 // reset the framerate
203 - frameLength = 33.33;
204 - fpsLocked = false;
182 + yieldInterval = 5;
183 }
184 };
185
186 const performWorkUntilDeadline = () => {
209 - if (enableMessageLoopImplementation) {
210 - if (scheduledHostCallback !== null) {
211 - const currentTime = getCurrentTime();
212 - // Yield after `frameLength` ms, regardless of where we are in the vsync
213 - // cycle. This means there's always time remaining at the beginning of
214 - // the message event.
215 - frameDeadline = currentTime + frameLength;
216 - const hasTimeRemaining = true;
217 - try {
218 - const hasMoreWork = scheduledHostCallback(
219 - hasTimeRemaining,
220 - currentTime,
221 - );
222 - if (!hasMoreWork) {
223 - isMessageLoopRunning = false;
224 - scheduledHostCallback = null;
225 - } else {
226 - // If there's more work, schedule the next message event at the end
227 - // of the preceding one.
228 - port.postMessage(null);
229 - }
230 - } catch (error) {
231 - // If a scheduler task throws, exit the current browser task so the
232 - // error can be observed.
187 + if (scheduledHostCallback !== null) {
188 + const currentTime = getCurrentTime();
189 + // Yield after `yieldInterval` ms, regardless of where we are in the vsync
190 + // cycle. This means there's always time remaining at the beginning of
191 + // the message event.
192 + deadline = currentTime + yieldInterval;
193 + const hasTimeRemaining = true;
194 + try {
195 + const hasMoreWork = scheduledHostCallback(
196 + hasTimeRemaining,
197 + currentTime,
198 + );
199 + if (!hasMoreWork) {
200 + isMessageLoopRunning = false;
201 + scheduledHostCallback = null;
202 + } else {
203 + // If there's more work, schedule the next message event at the end
204 + // of the preceding one.
205 port.postMessage(null);
234 - throw error;
206 }
236 - } else {
237 - isMessageLoopRunning = false;
207 + } catch (error) {
208 + // If a scheduler task throws, exit the current browser task so the
209 + // error can be observed.
210 + port.postMessage(null);
211 + throw error;
212 }
239 - // Yielding to the browser will give it a chance to paint, so we can
240 - // reset this.
241 - needsPaint = false;
213 } else {
243 - if (scheduledHostCallback !== null) {
244 - const currentTime = getCurrentTime();
245 - const hasTimeRemaining = frameDeadline - currentTime > 0;
246 - try {
247 - const hasMoreWork = scheduledHostCallback(
248 - hasTimeRemaining,
249 - currentTime,
250 - );
251 - if (!hasMoreWork) {
252 - scheduledHostCallback = null;
253 - }
254 - } catch (error) {
255 - // If a scheduler task throws, exit the current browser task so the
256 - // error can be observed, and post a new task as soon as possible
257 - // so we can continue where we left off.
258 - port.postMessage(null);
259 - throw error;
260 - }
261 - }
262 - // Yielding to the browser will give it a chance to paint, so we can
263 - // reset this.
264 - needsPaint = false;
214 + isMessageLoopRunning = false;
215 }
216 + // Yielding to the browser will give it a chance to paint, so we can
217 + // reset this.
218 + needsPaint = false;
219 };
220
221 const channel = new MessageChannel();
222 const port = channel.port2;
223 channel.port1.onmessage = performWorkUntilDeadline;
224
272 - const onAnimationFrame = rAFTime => {
273 - if (scheduledHostCallback === null) {
274 - // No scheduled work. Exit.
275 - prevRAFTime = -1;
276 - prevRAFInterval = -1;
277 - isRAFLoopRunning = false;
278 - return;
279 - }
280 -
281 - // Eagerly schedule the next animation callback at the beginning of the
282 - // frame. If the scheduler queue is not empty at the end of the frame, it
283 - // will continue flushing inside that callback. If the queue *is* empty,
284 - // then it will exit immediately. Posting the callback at the start of the
285 - // frame ensures it's fired within the earliest possible frame. If we
286 - // waited until the end of the frame to post the callback, we risk the
287 - // browser skipping a frame and not firing the callback until the frame
288 - // after that.
289 - isRAFLoopRunning = true;
290 - requestAnimationFrame(nextRAFTime => {
291 - clearTimeout(rAFTimeoutID);
292 - onAnimationFrame(nextRAFTime);
293 - });
294 -
295 - // requestAnimationFrame is throttled when the tab is backgrounded. We
296 - // don't want to stop working entirely. So we'll fallback to a timeout loop.
297 - // TODO: Need a better heuristic for backgrounded work.
298 - const onTimeout = () => {
299 - frameDeadline = getCurrentTime() + frameLength / 2;
300 - performWorkUntilDeadline();
301 - rAFTimeoutID = setTimeout(onTimeout, frameLength * 3);
302 - };
303 - rAFTimeoutID = setTimeout(onTimeout, frameLength * 3);
304 -
305 - if (
306 - prevRAFTime !== -1 &&
307 - // Make sure this rAF time is different from the previous one. This check
308 - // could fail if two rAFs fire in the same frame.
309 - rAFTime - prevRAFTime > 0.1
310 - ) {
311 - const rAFInterval = rAFTime - prevRAFTime;
312 - if (!fpsLocked && prevRAFInterval !== -1) {
313 - // We've observed two consecutive frame intervals. We'll use this to
314 - // dynamically adjust the frame rate.
315 - //
316 - // If one frame goes long, then the next one can be short to catch up.
317 - // If two frames are short in a row, then that's an indication that we
318 - // actually have a higher frame rate than what we're currently
319 - // optimizing. For example, if we're running on 120hz display or 90hz VR
320 - // display. Take the max of the two in case one of them was an anomaly
321 - // due to missed frame deadlines.
322 - if (rAFInterval < frameLength && prevRAFInterval < frameLength) {
323 - frameLength =
324 - rAFInterval < prevRAFInterval ? prevRAFInterval : rAFInterval;
325 - if (frameLength < 8.33) {
326 - // Defensive coding. We don't support higher frame rates than 120hz.
327 - // If the calculated frame length gets lower than 8, it is probably
328 - // a bug.
329 - frameLength = 8.33;
330 - }
331 - }
332 - }
333 - prevRAFInterval = rAFInterval;
334 - }
335 - prevRAFTime = rAFTime;
336 - frameDeadline = rAFTime + frameLength;
337 -
338 - // We use the postMessage trick to defer idle work until after the repaint.
339 - port.postMessage(null);
340 - };
341 -
225 requestHostCallback = function(callback) {
226 scheduledHostCallback = callback;
344 - if (enableMessageLoopImplementation) {
345 - if (!isMessageLoopRunning) {
346 - isMessageLoopRunning = true;
347 - port.postMessage(null);
348 - }
349 - } else {
350 - if (!isRAFLoopRunning) {
351 - // Start a rAF loop.
352 - isRAFLoopRunning = true;
353 - requestAnimationFrame(rAFTime => {
354 - onAnimationFrame(rAFTime);
355 - });
356 - }
227 + if (!isMessageLoopRunning) {
228 + isMessageLoopRunning = true;
229 + port.postMessage(null);
230 }
231 };
232