Add version of scheduler that only swaps MessageChannel for postTask (#20206)
* Fork SchedulerDOM to SchedulerPostTaskOnly * Swap in postTask for MessageChannel * Add SchedulerPostTaskOnly-test.js * Update getCurrentTime * Gate tests to source * Prettier
Ricky committed
Nov 10, 2020 at 12:11 UTC
08986601546f69ba2c3107b472862cf2cb30d2ce
4 files changed
+892
packages/scheduler/src/__tests__/SchedulerPostTaskOnly-test.js
new
+275
@@ -0,0 +1,275 @@
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
+describe('SchedulerPostTaskOnly', () => {
23
+ beforeEach(() => {
24
+ if (!process.env.IS_BUILD) {
25
+ jest.resetModules();
26
+
27
+ // Un-mock scheduler
28
+ jest.mock('scheduler', () =>
29
+ require.requireActual('scheduler/unstable_post_task_only'),
30
+ );
31
+
32
+ runtime = installMockBrowserRuntime();
33
+ performance = window.performance;
34
+ Scheduler = require('scheduler');
35
+ cancelCallback = Scheduler.unstable_cancelCallback;
36
+ scheduleCallback = Scheduler.unstable_scheduleCallback;
37
+ NormalPriority = Scheduler.unstable_NormalPriority;
38
+ }
39
+ });
40
+
41
+ afterEach(() => {
42
+ if (!process.env.IS_BUILD) {
43
+ if (!runtime.isLogEmpty()) {
44
+ throw Error('Test exited without clearing log.');
45
+ }
46
+ }
47
+ });
48
+
49
+ function installMockBrowserRuntime() {
50
+ let hasPendingTask = false;
51
+ let timerIDCounter = 0;
52
+ let eventLog = [];
53
+
54
+ // Mock window functions
55
+ const window = {};
56
+ global.window = window;
57
+
58
+ // TODO: Scheduler no longer requires these methods to be polyfilled. But
59
+ // maybe we want to continue warning if they don't exist, to preserve the
60
+ // option to rely on it in the future?
61
+ window.requestAnimationFrame = window.cancelAnimationFrame = () => {};
62
+
63
+ let currentTime = 0;
64
+ window.performance = {
65
+ now() {
66
+ return currentTime;
67
+ },
68
+ };
69
+
70
+ window.setTimeout = (cb, delay) => {
71
+ const id = timerIDCounter++;
72
+ log(`Set Timer`);
73
+ // TODO
74
+ return id;
75
+ };
76
+ window.clearTimeout = id => {
77
+ // TODO
78
+ };
79
+
80
+ // Mock browser scheduler.
81
+ const scheduler = {};
82
+ global.scheduler = scheduler;
83
+
84
+ let nextTask;
85
+ scheduler.postTask = function(callback) {
86
+ if (hasPendingTask) {
87
+ throw Error('Task already scheduled');
88
+ }
89
+ log('Post Task');
90
+ hasPendingTask = true;
91
+ nextTask = callback;
92
+ };
93
+
94
+ function ensureLogIsEmpty() {
95
+ if (eventLog.length !== 0) {
96
+ throw Error('Log is not empty. Call assertLog before continuing.');
97
+ }
98
+ }
99
+
100
+ function advanceTime(ms) {
101
+ currentTime += ms;
102
+ }
103
+
104
+ function fireNextTask() {
105
+ ensureLogIsEmpty();
106
+ if (!hasPendingTask) {
107
+ throw Error('No task was scheduled');
108
+ }
109
+ hasPendingTask = false;
110
+
111
+ log('Task Event');
112
+
113
+ // If there's a continuation, it will call postTask again
114
+ // which will set nextTask. That means we need to clear
115
+ // nextTask before the invocation, otherwise we would
116
+ // delete the continuation task.
117
+ const task = nextTask;
118
+ nextTask = null;
119
+ task();
120
+ }
121
+
122
+ function log(val) {
123
+ eventLog.push(val);
124
+ }
125
+
126
+ function isLogEmpty() {
127
+ return eventLog.length === 0;
128
+ }
129
+
130
+ function assertLog(expected) {
131
+ const actual = eventLog;
132
+ eventLog = [];
133
+ expect(actual).toEqual(expected);
134
+ }
135
+
136
+ return {
137
+ advanceTime,
138
+ fireNextTask,
139
+ log,
140
+ isLogEmpty,
141
+ assertLog,
142
+ };
143
+ }
144
+
145
+ // @gate source
146
+ it('task that finishes before deadline', () => {
147
+ scheduleCallback(NormalPriority, () => {
148
+ runtime.log('Task');
149
+ });
150
+ runtime.assertLog(['Post Task']);
151
+ runtime.fireNextTask();
152
+ runtime.assertLog(['Task Event', 'Task']);
153
+ });
154
+
155
+ // @gate source
156
+ it('task with continuation', () => {
157
+ scheduleCallback(NormalPriority, () => {
158
+ runtime.log('Task');
159
+ while (!Scheduler.unstable_shouldYield()) {
160
+ runtime.advanceTime(1);
161
+ }
162
+ runtime.log(`Yield at ${performance.now()}ms`);
163
+ return () => {
164
+ runtime.log('Continuation');
165
+ };
166
+ });
167
+ runtime.assertLog(['Post Task']);
168
+
169
+ runtime.fireNextTask();
170
+ runtime.assertLog(['Task Event', 'Task', 'Yield at 5ms', 'Post Task']);
171
+
172
+ runtime.fireNextTask();
173
+ runtime.assertLog(['Task Event', 'Continuation']);
174
+ });
175
+
176
+ // @gate source
177
+ it('multiple tasks', () => {
178
+ scheduleCallback(NormalPriority, () => {
179
+ runtime.log('A');
180
+ });
181
+ scheduleCallback(NormalPriority, () => {
182
+ runtime.log('B');
183
+ });
184
+ runtime.assertLog(['Post Task']);
185
+ runtime.fireNextTask();
186
+ runtime.assertLog(['Task Event', 'A', 'B']);
187
+ });
188
+
189
+ // @gate source
190
+ it('multiple tasks with a yield in between', () => {
191
+ scheduleCallback(NormalPriority, () => {
192
+ runtime.log('A');
193
+ runtime.advanceTime(4999);
194
+ });
195
+ scheduleCallback(NormalPriority, () => {
196
+ runtime.log('B');
197
+ });
198
+ runtime.assertLog(['Post Task']);
199
+ runtime.fireNextTask();
200
+ runtime.assertLog([
201
+ 'Task Event',
202
+ 'A',
203
+ // Ran out of time. Post a continuation event.
204
+ 'Post Task',
205
+ ]);
206
+ runtime.fireNextTask();
207
+ runtime.assertLog(['Task Event', 'B']);
208
+ });
209
+
210
+ // @gate source
211
+ it('cancels tasks', () => {
212
+ const task = scheduleCallback(NormalPriority, () => {
213
+ runtime.log('Task');
214
+ });
215
+ runtime.assertLog(['Post Task']);
216
+ cancelCallback(task);
217
+ runtime.assertLog([]);
218
+ });
219
+
220
+ // @gate source
221
+ it('throws when a task errors then continues in a new event', () => {
222
+ scheduleCallback(NormalPriority, () => {
223
+ runtime.log('Oops!');
224
+ throw Error('Oops!');
225
+ });
226
+ scheduleCallback(NormalPriority, () => {
227
+ runtime.log('Yay');
228
+ });
229
+ runtime.assertLog(['Post Task']);
230
+
231
+ expect(() => runtime.fireNextTask()).toThrow('Oops!');
232
+ runtime.assertLog(['Task Event', 'Oops!', 'Post Task']);
233
+
234
+ runtime.fireNextTask();
235
+ runtime.assertLog(['Task Event', 'Yay']);
236
+ });
237
+
238
+ // @gate source
239
+ it('schedule new task after queue has emptied', () => {
240
+ scheduleCallback(NormalPriority, () => {
241
+ runtime.log('A');
242
+ });
243
+
244
+ runtime.assertLog(['Post Task']);
245
+ runtime.fireNextTask();
246
+ runtime.assertLog(['Task Event', 'A']);
247
+
248
+ scheduleCallback(NormalPriority, () => {
249
+ runtime.log('B');
250
+ });
251
+ runtime.assertLog(['Post Task']);
252
+ runtime.fireNextTask();
253
+ runtime.assertLog(['Task Event', 'B']);
254
+ });
255
+
256
+ // @gate source
257
+ it('schedule new task after a cancellation', () => {
258
+ const handle = scheduleCallback(NormalPriority, () => {
259
+ runtime.log('A');
260
+ });
261
+
262
+ runtime.assertLog(['Post Task']);
263
+ cancelCallback(handle);
264
+
265
+ runtime.fireNextTask();
266
+ runtime.assertLog(['Task Event']);
267
+
268
+ scheduleCallback(NormalPriority, () => {
269
+ runtime.log('B');
270
+ });
271
+ runtime.assertLog(['Post Task']);
272
+ runtime.fireNextTask();
273
+ runtime.assertLog(['Task Event', 'B']);
274
+ });
275
+});
packages/scheduler/src/forks/SchedulerPostTaskOnly.js
new
+592
@@ -0,0 +1,592 @@
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
+ */
8
+
9
+/* eslint-disable no-var */
10
+
11
+import {
12
+ enableSchedulerDebugging,
13
+ enableProfiling,
14
+} from '../SchedulerFeatureFlags';
15
+
16
+import {push, pop, peek} from '../SchedulerMinHeap';
17
+
18
+// TODO: Use symbols?
19
+import {
20
+ ImmediatePriority,
21
+ UserBlockingPriority,
22
+ NormalPriority,
23
+ LowPriority,
24
+ IdlePriority,
25
+} from '../SchedulerPriorities';
26
+import {
27
+ sharedProfilingBuffer,
28
+ markTaskRun,
29
+ markTaskYield,
30
+ markTaskCompleted,
31
+ markTaskCanceled,
32
+ markTaskErrored,
33
+ markSchedulerSuspended,
34
+ markSchedulerUnsuspended,
35
+ markTaskStart,
36
+ stopLoggingProfilingEvents,
37
+ startLoggingProfilingEvents,
38
+} from '../SchedulerProfiling';
39
+
40
+import {enableIsInputPending} from '../SchedulerFeatureFlags';
41
+
42
+const perf = window.performance;
43
+
44
+function getCurrentTime() {
45
+ return perf.now();
46
+}
47
+
48
+// Max 31 bit integer. The max integer size in V8 for 32-bit systems.
49
+// Math.pow(2, 30) - 1
50
+// 0b111111111111111111111111111111
51
+var maxSigned31BitInt = 1073741823;
52
+
53
+// Times out immediately
54
+var IMMEDIATE_PRIORITY_TIMEOUT = -1;
55
+// Eventually times out
56
+var USER_BLOCKING_PRIORITY_TIMEOUT = 250;
57
+var NORMAL_PRIORITY_TIMEOUT = 5000;
58
+var LOW_PRIORITY_TIMEOUT = 10000;
59
+// Never times out
60
+var IDLE_PRIORITY_TIMEOUT = maxSigned31BitInt;
61
+
62
+// Tasks are stored on a min heap
63
+var taskQueue = [];
64
+var timerQueue = [];
65
+
66
+// Incrementing id counter. Used to maintain insertion order.
67
+var taskIdCounter = 1;
68
+
69
+// Pausing the scheduler is useful for debugging.
70
+var isSchedulerPaused = false;
71
+
72
+var currentTask = null;
73
+var currentPriorityLevel = NormalPriority;
74
+
75
+// This is set while performing work, to prevent re-entrancy.
76
+var isPerformingWork = false;
77
+
78
+var isHostCallbackScheduled = false;
79
+var isHostTimeoutScheduled = false;
80
+
81
+// Capture local references to native APIs, in case a polyfill overrides them.
82
+const setTimeout = window.setTimeout;
83
+const clearTimeout = window.clearTimeout;
84
+
85
+if (typeof console !== 'undefined') {
86
+ // TODO: Scheduler no longer requires these methods to be polyfilled. But
87
+ // maybe we want to continue warning if they don't exist, to preserve the
88
+ // option to rely on it in the future?
89
+ const requestAnimationFrame = window.requestAnimationFrame;
90
+ const cancelAnimationFrame = window.cancelAnimationFrame;
91
+
92
+ if (typeof requestAnimationFrame !== 'function') {
93
+ // Using console['error'] to evade Babel and ESLint
94
+ console['error'](
95
+ "This browser doesn't support requestAnimationFrame. " +
96
+ 'Make sure that you load a ' +
97
+ 'polyfill in older browsers. https://reactjs.org/link/react-polyfills',
98
+ );
99
+ }
100
+ if (typeof cancelAnimationFrame !== 'function') {
101
+ // Using console['error'] to evade Babel and ESLint
102
+ console['error'](
103
+ "This browser doesn't support cancelAnimationFrame. " +
104
+ 'Make sure that you load a ' +
105
+ 'polyfill in older browsers. https://reactjs.org/link/react-polyfills',
106
+ );
107
+ }
108
+}
109
+
110
+function advanceTimers(currentTime) {
111
+ // Check for tasks that are no longer delayed and add them to the queue.
112
+ let timer = peek(timerQueue);
113
+ while (timer !== null) {
114
+ if (timer.callback === null) {
115
+ // Timer was cancelled.
116
+ pop(timerQueue);
117
+ } else if (timer.startTime <= currentTime) {
118
+ // Timer fired. Transfer to the task queue.
119
+ pop(timerQueue);
120
+ timer.sortIndex = timer.expirationTime;
121
+ push(taskQueue, timer);
122
+ if (enableProfiling) {
123
+ markTaskStart(timer, currentTime);
124
+ timer.isQueued = true;
125
+ }
126
+ } else {
127
+ // Remaining timers are pending.
128
+ return;
129
+ }
130
+ timer = peek(timerQueue);
131
+ }
132
+}
133
+
134
+function handleTimeout(currentTime) {
135
+ isHostTimeoutScheduled = false;
136
+ advanceTimers(currentTime);
137
+
138
+ if (!isHostCallbackScheduled) {
139
+ if (peek(taskQueue) !== null) {
140
+ isHostCallbackScheduled = true;
141
+ requestHostCallback(flushWork);
142
+ } else {
143
+ const firstTimer = peek(timerQueue);
144
+ if (firstTimer !== null) {
145
+ requestHostTimeout(handleTimeout, firstTimer.startTime - currentTime);
146
+ }
147
+ }
148
+ }
149
+}
150
+
151
+function flushWork(hasTimeRemaining, initialTime) {
152
+ if (enableProfiling) {
153
+ markSchedulerUnsuspended(initialTime);
154
+ }
155
+
156
+ // We'll need a host callback the next time work is scheduled.
157
+ isHostCallbackScheduled = false;
158
+ if (isHostTimeoutScheduled) {
159
+ // We scheduled a timeout but it's no longer needed. Cancel it.
160
+ isHostTimeoutScheduled = false;
161
+ cancelHostTimeout();
162
+ }
163
+
164
+ isPerformingWork = true;
165
+ const previousPriorityLevel = currentPriorityLevel;
166
+ try {
167
+ if (enableProfiling) {
168
+ try {
169
+ return workLoop(hasTimeRemaining, initialTime);
170
+ } catch (error) {
171
+ if (currentTask !== null) {
172
+ const currentTime = getCurrentTime();
173
+ markTaskErrored(currentTask, currentTime);
174
+ currentTask.isQueued = false;
175
+ }
176
+ throw error;
177
+ }
178
+ } else {
179
+ // No catch in prod code path.
180
+ return workLoop(hasTimeRemaining, initialTime);
181
+ }
182
+ } finally {
183
+ currentTask = null;
184
+ currentPriorityLevel = previousPriorityLevel;
185
+ isPerformingWork = false;
186
+ if (enableProfiling) {
187
+ const currentTime = getCurrentTime();
188
+ markSchedulerSuspended(currentTime);
189
+ }
190
+ }
191
+}
192
+
193
+function workLoop(hasTimeRemaining, initialTime) {
194
+ let currentTime = initialTime;
195
+ advanceTimers(currentTime);
196
+ currentTask = peek(taskQueue);
197
+ while (
198
+ currentTask !== null &&
199
+ !(enableSchedulerDebugging && isSchedulerPaused)
200
+ ) {
201
+ if (
202
+ currentTask.expirationTime > currentTime &&
203
+ (!hasTimeRemaining || shouldYieldToHost())
204
+ ) {
205
+ // This currentTask hasn't expired, and we've reached the deadline.
206
+ break;
207
+ }
208
+ const callback = currentTask.callback;
209
+ if (typeof callback === 'function') {
210
+ currentTask.callback = null;
211
+ currentPriorityLevel = currentTask.priorityLevel;
212
+ const didUserCallbackTimeout = currentTask.expirationTime <= currentTime;
213
+ markTaskRun(currentTask, currentTime);
214
+ const continuationCallback = callback(didUserCallbackTimeout);
215
+ currentTime = getCurrentTime();
216
+ if (typeof continuationCallback === 'function') {
217
+ currentTask.callback = continuationCallback;
218
+ markTaskYield(currentTask, currentTime);
219
+ } else {
220
+ if (enableProfiling) {
221
+ markTaskCompleted(currentTask, currentTime);
222
+ currentTask.isQueued = false;
223
+ }
224
+ if (currentTask === peek(taskQueue)) {
225
+ pop(taskQueue);
226
+ }
227
+ }
228
+ advanceTimers(currentTime);
229
+ } else {
230
+ pop(taskQueue);
231
+ }
232
+ currentTask = peek(taskQueue);
233
+ }
234
+ // Return whether there's additional work
235
+ if (currentTask !== null) {
236
+ return true;
237
+ } else {
238
+ const firstTimer = peek(timerQueue);
239
+ if (firstTimer !== null) {
240
+ requestHostTimeout(handleTimeout, firstTimer.startTime - currentTime);
241
+ }
242
+ return false;
243
+ }
244
+}
245
+
246
+function unstable_runWithPriority(priorityLevel, eventHandler) {
247
+ switch (priorityLevel) {
248
+ case ImmediatePriority:
249
+ case UserBlockingPriority:
250
+ case NormalPriority:
251
+ case LowPriority:
252
+ case IdlePriority:
253
+ break;
254
+ default:
255
+ priorityLevel = NormalPriority;
256
+ }
257
+
258
+ var previousPriorityLevel = currentPriorityLevel;
259
+ currentPriorityLevel = priorityLevel;
260
+
261
+ try {
262
+ return eventHandler();
263
+ } finally {
264
+ currentPriorityLevel = previousPriorityLevel;
265
+ }
266
+}
267
+
268
+function unstable_next(eventHandler) {
269
+ var priorityLevel;
270
+ switch (currentPriorityLevel) {
271
+ case ImmediatePriority:
272
+ case UserBlockingPriority:
273
+ case NormalPriority:
274
+ // Shift down to normal priority
275
+ priorityLevel = NormalPriority;
276
+ break;
277
+ default:
278
+ // Anything lower than normal priority should remain at the current level.
279
+ priorityLevel = currentPriorityLevel;
280
+ break;
281
+ }
282
+
283
+ var previousPriorityLevel = currentPriorityLevel;
284
+ currentPriorityLevel = priorityLevel;
285
+
286
+ try {
287
+ return eventHandler();
288
+ } finally {
289
+ currentPriorityLevel = previousPriorityLevel;
290
+ }
291
+}
292
+
293
+function unstable_wrapCallback(callback) {
294
+ var parentPriorityLevel = currentPriorityLevel;
295
+ return function() {
296
+ // This is a fork of runWithPriority, inlined for performance.
297
+ var previousPriorityLevel = currentPriorityLevel;
298
+ currentPriorityLevel = parentPriorityLevel;
299
+
300
+ try {
301
+ return callback.apply(this, arguments);
302
+ } finally {
303
+ currentPriorityLevel = previousPriorityLevel;
304
+ }
305
+ };
306
+}
307
+
308
+function unstable_scheduleCallback(priorityLevel, callback, options) {
309
+ var currentTime = getCurrentTime();
310
+
311
+ var startTime;
312
+ if (typeof options === 'object' && options !== null) {
313
+ var delay = options.delay;
314
+ if (typeof delay === 'number' && delay > 0) {
315
+ startTime = currentTime + delay;
316
+ } else {
317
+ startTime = currentTime;
318
+ }
319
+ } else {
320
+ startTime = currentTime;
321
+ }
322
+
323
+ var timeout;
324
+ switch (priorityLevel) {
325
+ case ImmediatePriority:
326
+ timeout = IMMEDIATE_PRIORITY_TIMEOUT;
327
+ break;
328
+ case UserBlockingPriority:
329
+ timeout = USER_BLOCKING_PRIORITY_TIMEOUT;
330
+ break;
331
+ case IdlePriority:
332
+ timeout = IDLE_PRIORITY_TIMEOUT;
333
+ break;
334
+ case LowPriority:
335
+ timeout = LOW_PRIORITY_TIMEOUT;
336
+ break;
337
+ case NormalPriority:
338
+ default:
339
+ timeout = NORMAL_PRIORITY_TIMEOUT;
340
+ break;
341
+ }
342
+
343
+ var expirationTime = startTime + timeout;
344
+
345
+ var newTask = {
346
+ id: taskIdCounter++,
347
+ callback,
348
+ priorityLevel,
349
+ startTime,
350
+ expirationTime,
351
+ sortIndex: -1,
352
+ };
353
+ if (enableProfiling) {
354
+ newTask.isQueued = false;
355
+ }
356
+
357
+ if (startTime > currentTime) {
358
+ // This is a delayed task.
359
+ newTask.sortIndex = startTime;
360
+ push(timerQueue, newTask);
361
+ if (peek(taskQueue) === null && newTask === peek(timerQueue)) {
362
+ // All tasks are delayed, and this is the task with the earliest delay.
363
+ if (isHostTimeoutScheduled) {
364
+ // Cancel an existing timeout.
365
+ cancelHostTimeout();
366
+ } else {
367
+ isHostTimeoutScheduled = true;
368
+ }
369
+ // Schedule a timeout.
370
+ requestHostTimeout(handleTimeout, startTime - currentTime);
371
+ }
372
+ } else {
373
+ newTask.sortIndex = expirationTime;
374
+ push(taskQueue, newTask);
375
+ if (enableProfiling) {
376
+ markTaskStart(newTask, currentTime);
377
+ newTask.isQueued = true;
378
+ }
379
+ // Schedule a host callback, if needed. If we're already performing work,
380
+ // wait until the next time we yield.
381
+ if (!isHostCallbackScheduled && !isPerformingWork) {
382
+ isHostCallbackScheduled = true;
383
+ requestHostCallback(flushWork);
384
+ }
385
+ }
386
+
387
+ return newTask;
388
+}
389
+
390
+function unstable_pauseExecution() {
391
+ isSchedulerPaused = true;
392
+}
393
+
394
+function unstable_continueExecution() {
395
+ isSchedulerPaused = false;
396
+ if (!isHostCallbackScheduled && !isPerformingWork) {
397
+ isHostCallbackScheduled = true;
398
+ requestHostCallback(flushWork);
399
+ }
400
+}
401
+
402
+function unstable_getFirstCallbackNode() {
403
+ return peek(taskQueue);
404
+}
405
+
406
+function unstable_cancelCallback(task) {
407
+ if (enableProfiling) {
408
+ if (task.isQueued) {
409
+ const currentTime = getCurrentTime();
410
+ markTaskCanceled(task, currentTime);
411
+ task.isQueued = false;
412
+ }
413
+ }
414
+
415
+ // Null out the callback to indicate the task has been canceled. (Can't
416
+ // remove from the queue because you can't remove arbitrary nodes from an
417
+ // array based heap, only the first one.)
418
+ task.callback = null;
419
+}
420
+
421
+function unstable_getCurrentPriorityLevel() {
422
+ return currentPriorityLevel;
423
+}
424
+
425
+let isTaskLoopRunning = false;
426
+let scheduledHostCallback = null;
427
+let taskTimeoutID = -1;
428
+
429
+// Scheduler periodically yields in case there is other work on the main
430
+// thread, like user events. By default, it yields multiple times per frame.
431
+// It does not attempt to align with frame boundaries, since most tasks don't
432
+// need to be frame aligned; for those that do, use requestAnimationFrame.
433
+let yieldInterval = 5;
434
+let deadline = 0;
435
+
436
+// TODO: Make this configurable
437
+// TODO: Adjust this based on priority?
438
+const maxYieldInterval = 300;
439
+let needsPaint = false;
440
+
441
+function shouldYieldToHost() {
442
+ if (
443
+ enableIsInputPending &&
444
+ navigator !== undefined &&
445
+ navigator.scheduling !== undefined &&
446
+ navigator.scheduling.isInputPending !== undefined
447
+ ) {
448
+ const scheduling = navigator.scheduling;
449
+ const currentTime = getCurrentTime();
450
+ if (currentTime >= deadline) {
451
+ // There's no time left. We may want to yield control of the main
452
+ // thread, so the browser can perform high priority tasks. The main ones
453
+ // are painting and user input. If there's a pending paint or a pending
454
+ // input, then we should yield. But if there's neither, then we can
455
+ // yield less often while remaining responsive. We'll eventually yield
456
+ // regardless, since there could be a pending paint that wasn't
457
+ // accompanied by a call to `requestPaint`, or other main thread tasks
458
+ // like network events.
459
+ if (needsPaint || scheduling.isInputPending()) {
460
+ // There is either a pending paint or a pending input.
461
+ return true;
462
+ }
463
+ // There's no pending input. Only yield if we've reached the max
464
+ // yield interval.
465
+ return currentTime >= maxYieldInterval;
466
+ } else {
467
+ // There's still time left in the frame.
468
+ return false;
469
+ }
470
+ } else {
471
+ // `isInputPending` is not available. Since we have no way of knowing if
472
+ // there's pending input, always yield at the end of the frame.
473
+ return getCurrentTime() >= deadline;
474
+ }
475
+}
476
+
477
+function requestPaint() {
478
+ if (
479
+ enableIsInputPending &&
480
+ navigator !== undefined &&
481
+ navigator.scheduling !== undefined &&
482
+ navigator.scheduling.isInputPending !== undefined
483
+ ) {
484
+ needsPaint = true;
485
+ }
486
+
487
+ // Since we yield every frame regardless, `requestPaint` has no effect.
488
+}
489
+
490
+function forceFrameRate(fps) {
491
+ if (fps < 0 || fps > 125) {
492
+ // Using console['error'] to evade Babel and ESLint
493
+ console['error'](
494
+ 'forceFrameRate takes a positive int between 0 and 125, ' +
495
+ 'forcing frame rates higher than 125 fps is not supported',
496
+ );
497
+ return;
498
+ }
499
+ if (fps > 0) {
500
+ yieldInterval = Math.floor(1000 / fps);
501
+ } else {
502
+ // reset the framerate
503
+ yieldInterval = 5;
504
+ }
505
+}
506
+
507
+const performWorkUntilDeadline = () => {
508
+ if (scheduledHostCallback !== null) {
509
+ const currentTime = getCurrentTime();
510
+ // Yield after `yieldInterval` ms, regardless of where we are in the vsync
511
+ // cycle. This means there's always time remaining at the beginning of
512
+ // the message event.
513
+ deadline = currentTime + yieldInterval;
514
+ const hasTimeRemaining = true;
515
+ try {
516
+ const hasMoreWork = scheduledHostCallback(hasTimeRemaining, currentTime);
517
+ if (!hasMoreWork) {
518
+ isTaskLoopRunning = false;
519
+ scheduledHostCallback = null;
520
+ } else {
521
+ // If there's more work, schedule the next message event at the end
522
+ // of the preceding one.
523
+ postTask(performWorkUntilDeadline);
524
+ }
525
+ } catch (error) {
526
+ // If a scheduler task throws, exit the current browser task so the
527
+ // error can be observed.
528
+ postTask(performWorkUntilDeadline);
529
+ throw error;
530
+ }
531
+ } else {
532
+ isTaskLoopRunning = false;
533
+ }
534
+ // Yielding to the browser will give it a chance to paint, so we can
535
+ // reset this.
536
+ needsPaint = false;
537
+};
538
+
539
+function postTask(callback) {
540
+ // Use experimental Chrome Scheduler postTask API.
541
+ global.scheduler.postTask(callback);
542
+}
543
+
544
+function requestHostCallback(callback) {
545
+ scheduledHostCallback = callback;
546
+ if (!isTaskLoopRunning) {
547
+ isTaskLoopRunning = true;
548
+ postTask(performWorkUntilDeadline);
549
+ }
550
+}
551
+
552
+function requestHostTimeout(callback, ms) {
553
+ taskTimeoutID = setTimeout(() => {
554
+ callback(getCurrentTime());
555
+ }, ms);
556
+}
557
+
558
+function cancelHostTimeout() {
559
+ clearTimeout(taskTimeoutID);
560
+ taskTimeoutID = -1;
561
+}
562
+
563
+const unstable_requestPaint = requestPaint;
564
+
565
+export {
566
+ ImmediatePriority as unstable_ImmediatePriority,
567
+ UserBlockingPriority as unstable_UserBlockingPriority,
568
+ NormalPriority as unstable_NormalPriority,
569
+ IdlePriority as unstable_IdlePriority,
570
+ LowPriority as unstable_LowPriority,
571
+ unstable_runWithPriority,
572
+ unstable_next,
573
+ unstable_scheduleCallback,
574
+ unstable_cancelCallback,
575
+ unstable_wrapCallback,
576
+ unstable_getCurrentPriorityLevel,
577
+ shouldYieldToHost as unstable_shouldYield,
578
+ unstable_requestPaint,
579
+ unstable_continueExecution,
580
+ unstable_pauseExecution,
581
+ unstable_getFirstCallbackNode,
582
+ getCurrentTime as unstable_now,
583
+ forceFrameRate as unstable_forceFrameRate,
584
+};
585
+
586
+export const unstable_Profiling = enableProfiling
587
+ ? {
588
+ startLoggingProfilingEvents,
589
+ stopLoggingProfilingEvents,
590
+ sharedProfilingBuffer,
591
+ }
592
+ : null;
packages/scheduler/unstable_post_task_only.js
new
+10
@@ -0,0 +1,10 @@
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
+
8
+'use strict';
9
+
10
+export * from './src/forks/SchedulerPostTaskOnly';
scripts/rollup/bundles.js
+15
@@ -659,6 +659,21 @@ const bundles = [
659
externals: [],
660
},
661
662
+ /******* React Scheduler Post Task Only (experimental) *******/
663
+ {
664
+ bundleTypes: [
665
+ NODE_DEV,
666
+ NODE_PROD,
667
+ FB_WWW_DEV,
668
+ FB_WWW_PROD,
669
+ FB_WWW_PROFILING,
670
+ ],
671
+ moduleType: ISOMORPHIC,
672
+ entry: 'scheduler/unstable_post_task_only',
673
+ global: 'SchedulerPostTaskOnly',
674
+ externals: [],
675
+ },
676
+
677
/******* React Scheduler No DOM (experimental) *******/
678
{
679
bundleTypes: [