[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