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 scheduleCallback;
19
+let NormalPriority;
20
+
21
+// The Scheduler implementation uses browser APIs like `MessageChannel`,
22
+// `requestAnimationFrame`, and `setTimeout` to schedule work on the main
23
+// thread. Most of our tests treat these as implementation details; however, the
24
+// sequence and timing of these APIs are not precisely specified, and can vary
25
+// wildly 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. The only
32
+// thing it assumes is that requestAnimationFrame is passed a frame time that is
33
+// equal to or less than the time returned by performance.now. Everything else
34
+// can be controlled at will.
35
+//
36
+// It also includes Scheduler-specific invariants, e.g. only one rAF callback
37
+// can be scheduled at a time.
38
+describe('SchedulerBrowser', () => {
39
+ function beforeAndAfterHooks(enableMessageLoopImplementation) {
40
+ beforeEach(() => {
41
+ jest.resetModules();
42
+
43
+ // Un-mock scheduler
44
+ jest.mock('scheduler', () => require.requireActual('scheduler'));
45
+ jest.mock('scheduler/src/SchedulerHostConfig', () =>
46
+ require.requireActual(
47
+ 'scheduler/src/forks/SchedulerHostConfig.default.js',
48
+ ),
49
+ );
50
+
51
+ runtime = installMockBrowserRuntime();
52
+ performance = window.performance;
53
+ require('scheduler/src/SchedulerFeatureFlags').enableMessageLoopImplementation = enableMessageLoopImplementation;
54
+ Scheduler = require('scheduler');
55
+ scheduleCallback = Scheduler.unstable_scheduleCallback;
56
+ NormalPriority = Scheduler.unstable_NormalPriority;
57
+ });
58
+
59
+ afterEach(() => {
60
+ if (!runtime.isLogEmpty()) {
61
+ throw Error('Test exited without clearing log.');
62
+ }
63
+ });
64
+ }
65
+
66
+ function installMockBrowserRuntime() {
67
+ let VSYNC_INTERVAL = 33.33;
68
+
69
+ let hasPendingMessageEvent = false;
70
+
71
+ let rAFCallbackIDCounter = 0;
72
+ let rAFCallback = null;
73
+ let isRunningRAFCallback = false;
74
+
75
+ let rICCallbackIDCounter = 0;
76
+ let rICCallback = null;
77
+
78
+ let timerIDCounter = 0;
79
+ // let timerIDs = new Map();
80
+
81
+ let eventLog = [];
82
+
83
+ const window = {};
84
+ global.window = window;
85
+
86
+ let currentTime = 0;
87
+
88
+ window.performance = {
89
+ now() {
90
+ return currentTime;
91
+ },
92
+ };
93
+ window.requestAnimationFrame = cb => {
94
+ if (rAFCallback !== null) {
95
+ throw Error('rAF already scheduled');
96
+ }
97
+ if (isRunningRAFCallback) {
98
+ log('Request Animation Frame [Reposted]');
99
+ } else {
100
+ log('Request Animation Frame');
101
+ }
102
+ rAFCallback = cb;
103
+ return rAFCallbackIDCounter++;
104
+ };
105
+ window.cancelAnimationFrame = id => {
106
+ rAFCallback = null;
107
+ };
108
+
109
+ window.requestIdleCallback = cb => {
110
+ if (rICCallback !== null) {
111
+ throw Error('rAF already scheduled');
112
+ }
113
+ log('Request Idle Callback');
114
+ rICCallback = cb;
115
+ return rICCallbackIDCounter++;
116
+ };
117
+ window.cancelIdleCallback = id => {
118
+ rICCallback = null;
119
+ };
120
+
121
+ window.setTimeout = (cb, delay) => {
122
+ const id = timerIDCounter++;
123
+ log(`Set Timer`);
124
+ // TODO
125
+ return id;
126
+ };
127
+ window.clearTimeout = id => {
128
+ // TODO
129
+ };
130
+
131
+ const port1 = {};
132
+ const port2 = {
133
+ postMessage() {
134
+ if (hasPendingMessageEvent) {
135
+ throw Error('Message event already scheduled');
136
+ }
137
+ log('Post Message');
138
+ hasPendingMessageEvent = true;
139
+ },
140
+ };
141
+ global.MessageChannel = function MessageChannel() {
142
+ this.port1 = port1;
143
+ this.port2 = port2;
144
+ };
145
+
146
+ function ensureLogIsEmpty() {
147
+ if (eventLog.length !== 0) {
148
+ throw Error('Log is not empty. Call assertLog before continuing.');
149
+ }
150
+ }
151
+ function setHardwareFrameRate(fps) {
152
+ VSYNC_INTERVAL = 1000 / fps;
153
+ }
154
+ function advanceTime(ms) {
155
+ currentTime += ms;
156
+ }
157
+ function advanceTimeToNextFrame() {
158
+ const targetTime =
159
+ Math.ceil(currentTime / VSYNC_INTERVAL) * VSYNC_INTERVAL;
160
+ if (targetTime === currentTime) {
161
+ currentTime += VSYNC_INTERVAL;
162
+ } else {
163
+ currentTime = targetTime;
164
+ }
165
+ }
166
+ function getMostRecentFrameNumber() {
167
+ return Math.floor(currentTime / VSYNC_INTERVAL);
168
+ }
169
+ function fireMessageEvent() {
170
+ ensureLogIsEmpty();
171
+ if (!hasPendingMessageEvent) {
172
+ throw Error('No message event was scheduled');
173
+ }
174
+ hasPendingMessageEvent = false;
175
+ const onMessage = port1.onmessage;
176
+ log('Message Event');
177
+ onMessage();
178
+ }
179
+ function fireAnimationFrame() {
180
+ ensureLogIsEmpty();
181
+ if (isRunningRAFCallback) {
182
+ throw Error('Cannot fire animation frame from inside another event.');
183
+ }
184
+ if (rAFCallback === null) {
185
+ throw Error('No rAF scheduled.');
186
+ }
187
+ const mostRecentFrameNumber = getMostRecentFrameNumber();
188
+ const rAFTime = mostRecentFrameNumber * VSYNC_INTERVAL;
189
+ const cb = rAFCallback;
190
+ rAFCallback = null;
191
+ log(`Animation Frame [${mostRecentFrameNumber}]`);
192
+ isRunningRAFCallback = true;
193
+ try {
194
+ cb(rAFTime);
195
+ } finally {
196
+ isRunningRAFCallback = false;
197
+ }
198
+ }
199
+ function fireRIC() {
200
+ ensureLogIsEmpty();
201
+ if (rICCallback === null) {
202
+ throw Error('No rIC scheduled.');
203
+ }
204
+ const cb = rICCallback;
205
+ rICCallback = null;
206
+ log('Idle Callback');
207
+ cb();
208
+ }
209
+ function log(val) {
210
+ eventLog.push(val);
211
+ }
212
+ function isLogEmpty() {
213
+ return eventLog.length === 0;
214
+ }
215
+ function assertLog(expected) {
216
+ const actual = eventLog;
217
+ eventLog = [];
218
+ expect(actual).toEqual(expected);
219
+ }
220
+ return {
221
+ setHardwareFrameRate,
222
+ advanceTime,
223
+ advanceTimeToNextFrame,
224
+ getMostRecentFrameNumber,
225
+ fireMessageEvent,
226
+ fireAnimationFrame,
227
+ fireRIC,
228
+ log,
229
+ isLogEmpty,
230
+ assertLog,
231
+ };
232
+ }
233
+
234
+ describe('rAF aligned frame boundaries', () => {
235
+ const enableMessageLoopImplementation = false;
236
+ beforeAndAfterHooks(enableMessageLoopImplementation);
237
+
238
+ it('callback with continuation', () => {
239
+ scheduleCallback(NormalPriority, () => {
240
+ runtime.log('Task');
241
+ while (!Scheduler.unstable_shouldYield()) {
242
+ runtime.advanceTime(1);
243
+ }
244
+ runtime.log(`Yield at ${performance.now()}ms`);
245
+ return () => {
246
+ runtime.log('Continuation');
247
+ };
248
+ });
249
+ runtime.assertLog(['Request Animation Frame']);
250
+
251
+ runtime.fireAnimationFrame();
252
+ runtime.assertLog([
253
+ 'Animation Frame [0]',
254
+ 'Request Animation Frame [Reposted]',
255
+ 'Set Timer',
256
+ 'Post Message',
257
+ ]);
258
+ runtime.fireMessageEvent();
259
+ runtime.assertLog(['Message Event', 'Task', 'Yield at 34ms']);
260
+
261
+ runtime.fireAnimationFrame();
262
+ runtime.assertLog([
263
+ 'Animation Frame [1]',
264
+ 'Request Animation Frame [Reposted]',
265
+ 'Set Timer',
266
+ 'Post Message',
267
+ ]);
268
+
269
+ runtime.fireMessageEvent();
270
+ runtime.assertLog(['Message Event', 'Continuation']);
271
+
272
+ runtime.advanceTimeToNextFrame();
273
+ runtime.fireAnimationFrame();
274
+ runtime.assertLog(['Animation Frame [2]']);
275
+ });
276
+
277
+ it('two rAF calls in the same frame', () => {
278
+ scheduleCallback(NormalPriority, () => runtime.log('A'));
279
+ runtime.assertLog(['Request Animation Frame']);
280
+ runtime.fireAnimationFrame();
281
+ runtime.assertLog([
282
+ 'Animation Frame [0]',
283
+ 'Request Animation Frame [Reposted]',
284
+ 'Set Timer',
285
+ 'Post Message',
286
+ ]);
287
+ runtime.fireMessageEvent();
288
+ runtime.assertLog(['Message Event', 'A']);
289
+
290
+ // The Scheduler queue is now empty. We're still in frame 0.
291
+ expect(runtime.getMostRecentFrameNumber()).toBe(0);
292
+
293
+ // Post a task to Scheduler.
294
+ scheduleCallback(NormalPriority, () => runtime.log('B'));
295
+
296
+ // Did not request another animation frame, since one was already scheduled
297
+ // during the previous rAF.
298
+ runtime.assertLog([]);
299
+
300
+ // Fire the animation frame.
301
+ runtime.fireAnimationFrame();
302
+ runtime.assertLog([
303
+ 'Animation Frame [0]',
304
+ 'Request Animation Frame [Reposted]',
305
+ 'Set Timer',
306
+ 'Post Message',
307
+ ]);
308
+
309
+ runtime.fireMessageEvent();
310
+ runtime.assertLog(['Message Event', 'B']);
311
+ });
312
+
313
+ it('adjusts frame rate by measuring inteval between rAF events', () => {
314
+ runtime.setHardwareFrameRate(60);
315
+
316
+ scheduleCallback(NormalPriority, () => runtime.log('Tick'));
317
+ runtime.assertLog(['Request Animation Frame']);
318
+
319
+ // Need to measure two consecutive intervals between frames.
320
+ for (let i = 0; i < 2; i++) {
321
+ runtime.fireAnimationFrame();
322
+ runtime.assertLog([
323
+ `Animation Frame [${runtime.getMostRecentFrameNumber()}]`,
324
+ 'Request Animation Frame [Reposted]',
325
+ 'Set Timer',
326
+ 'Post Message',
327
+ ]);
328
+ runtime.fireMessageEvent();
329
+ runtime.assertLog(['Message Event', 'Tick']);
330
+ scheduleCallback(NormalPriority, () => runtime.log('Tick'));
331
+ runtime.advanceTimeToNextFrame();
332
+ }
333
+
334
+ // Scheduler should observe that it's receiving rAFs every 16.6 ms and
335
+ // adjust its frame rate accordingly. Test by blocking the thread until
336
+ // Scheduler tells us to yield. Then measure how much time has elapsed.
337
+ const start = performance.now();
338
+ scheduleCallback(NormalPriority, () => {
339
+ while (!Scheduler.unstable_shouldYield()) {
340
+ runtime.advanceTime(1);
341
+ }
342
+ });
343
+ runtime.fireAnimationFrame();
344
+ runtime.assertLog([
345
+ `Animation Frame [${runtime.getMostRecentFrameNumber()}]`,
346
+ 'Request Animation Frame [Reposted]',
347
+ 'Set Timer',
348
+ 'Post Message',
349
+ ]);
350
+ runtime.fireMessageEvent();
351
+ runtime.assertLog(['Message Event', 'Tick']);
352
+ const end = performance.now();
353
+
354
+ // Check how much time elapsed in the frame.
355
+ expect(end - start).toEqual(17);
356
+ });
357
+ });
358
+
359
+ describe('message event loop', () => {
360
+ const enableMessageLoopImplementation = true;
361
+ beforeAndAfterHooks(enableMessageLoopImplementation);
362
+
363
+ it('task with continutation', () => {
364
+ scheduleCallback(NormalPriority, () => {
365
+ runtime.log('Task');
366
+ while (!Scheduler.unstable_shouldYield()) {
367
+ runtime.advanceTime(1);
368
+ }
369
+ runtime.log(`Yield at ${performance.now()}ms`);
370
+ return () => {
371
+ runtime.log('Continuation');
372
+ };
373
+ });
374
+ runtime.assertLog(['Post Message']);
375
+
376
+ runtime.fireMessageEvent();
377
+ runtime.assertLog([
378
+ 'Message Event',
379
+ 'Task',
380
+ 'Yield at 5ms',
381
+ 'Post Message',
382
+ ]);
383
+
384
+ runtime.fireMessageEvent();
385
+ runtime.assertLog(['Message Event', 'Continuation']);
386
+ });
387
+
388
+ it('task that throws', () => {
389
+ scheduleCallback(NormalPriority, () => {
390
+ runtime.log('Oops!');
391
+ throw Error('Oops!');
392
+ });
393
+ scheduleCallback(NormalPriority, () => {
394
+ runtime.log('Yay');
395
+ });
396
+ runtime.assertLog(['Post Message']);
397
+
398
+ expect(() => runtime.fireMessageEvent()).toThrow('Oops!');
399
+ runtime.assertLog(['Message Event', 'Oops!', 'Post Message']);
400
+
401
+ runtime.fireMessageEvent();
402
+ runtime.assertLog(['Message Event', 'Yay']);
403
+ });
404
+
405
+ it('schedule new task after queue has emptied', () => {
406
+ scheduleCallback(NormalPriority, () => {
407
+ runtime.log('A');
408
+ });
409
+
410
+ runtime.assertLog(['Post Message']);
411
+ runtime.fireMessageEvent();
412
+ runtime.assertLog(['Message Event', 'A']);
413
+
414
+ scheduleCallback(NormalPriority, () => {
415
+ runtime.log('B');
416
+ });
417
+ runtime.assertLog(['Post Message']);
418
+ runtime.fireMessageEvent();
419
+ runtime.assertLog(['Message Event', 'B']);
420
+ });
421
+ });
422
+});