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

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: [