@samitouri / QOS-React-2 / commits / 454c2211c0

Refactor SchedulerHostConfigs (#20025)

* Remove SchedulerHostConfigs * Fix builds * Fix forks * Move SchedulerNoDom check to npm/index.js * Fix tests * Add @gate source * Gate build-only test to build test runs

Ricky committed Nov 2, 2020 at 12:46 UTC 454c2211c09bfa2fd5475c25665f3bbeb6882841
28 files changed +1365 -548
packages/react-dom/src/__tests__/ReactTestUtilsActUnmockedScheduler-test.js
+3 -1
@@ -33,7 +33,9 @@ function unmount(dom) {
33
34 beforeEach(() => {
35 jest.resetModules();
36 - jest.unmock('scheduler');
36 + jest.mock('scheduler', () =>
37 + require.requireActual('scheduler/unstable_no_dom'),
38 + );
39 yields = [];
40 React = require('react');
41 ReactDOM = require('react-dom');
packages/react-dom/src/__tests__/ReactUnmockedSchedulerWarning-test.internal.js
+1 -1
@@ -17,7 +17,7 @@ function App() {
17
18 beforeEach(() => {
19 jest.resetModules();
20 - jest.unmock('scheduler');
20 + jest.mock('scheduler', () => require('scheduler/unstable_no_dom'));
21 React = require('react');
22 ReactDOM = require('react-dom');
23 ReactFeatureFlags = require('shared/ReactFeatureFlags');
packages/react-dom/src/__tests__/ReactUnmockedSchedulerWarning-test.js
+3 -1
@@ -16,7 +16,9 @@ function App() {
16
17 beforeEach(() => {
18 jest.resetModules();
19 - jest.unmock('scheduler');
19 + jest.mock('scheduler', () =>
20 + require.requireActual('scheduler/unstable_no_dom'),
21 + );
22 React = require('react');
23 ReactDOM = require('react-dom');
24 });
packages/scheduler/index.js
+1 -1
@@ -7,4 +7,4 @@
7
8 'use strict';
9
10 -export * from './src/Scheduler';
10 +export * from './src/forks/SchedulerDOM';
packages/scheduler/npm/index.js
+3 -1
@@ -1,6 +1,8 @@
1 'use strict';
2
3 -if (process.env.NODE_ENV === 'production') {
3 +if (typeof window === 'undefined' || typeof MessageChannel !== 'function') {
4 + module.exports = require('./unstable_no_dom');
5 +} else if (process.env.NODE_ENV === 'production') {
6 module.exports = require('./cjs/scheduler.production.min.js');
7 } else {
8 module.exports = require('./cjs/scheduler.development.js');
packages/scheduler/npm/unstable_no_dom.js new
+7
@@ -0,0 +1,7 @@
1 +'use strict';
2 +
3 +if (process.env.NODE_ENV === 'production') {
4 + module.exports = require('./cjs/scheduler-unstable_no_dom.production.min.js');
5 +} else {
6 + module.exports = require('./cjs/scheduler-unstable_no_dom.development.js');
7 +}
packages/scheduler/package.json
+1
@@ -28,6 +28,7 @@
28 "tracing.js",
29 "tracing-profiling.js",
30 "unstable_mock.js",
31 + "unstable_no_dom.js",
32 "unstable_post_task.js",
33 "cjs/",
34 "umd/"
packages/scheduler/src/__tests__/SchedulerDOM-test.js renamed
-5
@@ -35,11 +35,6 @@ describe('SchedulerBrowser', () => {
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 - );
38
39 runtime = installMockBrowserRuntime();
40 performance = global.performance;
packages/scheduler/src/__tests__/SchedulerMock-test.js renamed
packages/scheduler/src/__tests__/SchedulerNoDOM-test.js
+2 -5
@@ -24,11 +24,8 @@ describe('SchedulerNoDOM', () => {
24 jest.useFakeTimers();
25
26 // Un-mock scheduler
27 - jest.mock('scheduler', () => require.requireActual('scheduler'));
28 - jest.mock('scheduler/src/SchedulerHostConfig', () =>
29 - require.requireActual(
30 - 'scheduler/src/forks/SchedulerHostConfig.default.js',
31 - ),
27 + jest.mock('scheduler', () =>
28 + require.requireActual('scheduler/unstable_no_dom'),
29 );
30
31 Scheduler = require('scheduler');
packages/scheduler/src/__tests__/SchedulerUMDBundle-test.internal.js
+19 -5
@@ -8,6 +8,13 @@
8 */
9 'use strict';
10
11 +class MockMessageChannel {
12 + constructor() {
13 + this.port1 = jest.fn();
14 + this.port2 = jest.fn();
15 + }
16 +}
17 +
18 describe('Scheduling UMD bundle', () => {
19 beforeEach(() => {
20 // Fool SECRET_INTERNALS object into including UMD forwarding methods.
@@ -16,11 +23,18 @@ describe('Scheduling UMD bundle', () => {
23 jest.resetModules();
24
25 jest.mock('scheduler', () => require.requireActual('scheduler'));
19 - jest.mock('scheduler/src/SchedulerHostConfig', () =>
20 - require.requireActual(
21 - 'scheduler/src/forks/SchedulerHostConfig.default.js',
22 - ),
23 - );
26 +
27 + // Mock a browser environment since we're testing UMD modules.
28 + global.window = {
29 + requestAnimationFrame: jest.fn(),
30 + cancelAnimationFrame: jest.fn(),
31 + };
32 + global.MessageChannel = MockMessageChannel;
33 + });
34 +
35 + afterEach(() => {
36 + global.window = undefined;
37 + global.MessageChannel = undefined;
38 });
39
40 function filterPrivateKeys(name) {
packages/scheduler/src/forks/SchedulerDOM.js new
+598
@@ -0,0 +1,598 @@
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 +let getCurrentTime;
43 +const hasPerformanceNow =
44 + typeof performance === 'object' && typeof performance.now === 'function';
45 +
46 +if (hasPerformanceNow) {
47 + const localPerformance = performance;
48 + getCurrentTime = () => localPerformance.now();
49 +} else {
50 + const localDate = Date;
51 + const initialTime = localDate.now();
52 + getCurrentTime = () => localDate.now() - initialTime;
53 +}
54 +
55 +// Max 31 bit integer. The max integer size in V8 for 32-bit systems.
56 +// Math.pow(2, 30) - 1
57 +// 0b111111111111111111111111111111
58 +var maxSigned31BitInt = 1073741823;
59 +
60 +// Times out immediately
61 +var IMMEDIATE_PRIORITY_TIMEOUT = -1;
62 +// Eventually times out
63 +var USER_BLOCKING_PRIORITY_TIMEOUT = 250;
64 +var NORMAL_PRIORITY_TIMEOUT = 5000;
65 +var LOW_PRIORITY_TIMEOUT = 10000;
66 +// Never times out
67 +var IDLE_PRIORITY_TIMEOUT = maxSigned31BitInt;
68 +
69 +// Tasks are stored on a min heap
70 +var taskQueue = [];
71 +var timerQueue = [];
72 +
73 +// Incrementing id counter. Used to maintain insertion order.
74 +var taskIdCounter = 1;
75 +
76 +// Pausing the scheduler is useful for debugging.
77 +var isSchedulerPaused = false;
78 +
79 +var currentTask = null;
80 +var currentPriorityLevel = NormalPriority;
81 +
82 +// This is set while performing work, to prevent re-entrancy.
83 +var isPerformingWork = false;
84 +
85 +var isHostCallbackScheduled = false;
86 +var isHostTimeoutScheduled = false;
87 +
88 +// Capture local references to native APIs, in case a polyfill overrides them.
89 +const setTimeout = window.setTimeout;
90 +const clearTimeout = window.clearTimeout;
91 +
92 +if (typeof console !== 'undefined') {
93 + // TODO: Scheduler no longer requires these methods to be polyfilled. But
94 + // maybe we want to continue warning if they don't exist, to preserve the
95 + // option to rely on it in the future?
96 + const requestAnimationFrame = window.requestAnimationFrame;
97 + const cancelAnimationFrame = window.cancelAnimationFrame;
98 +
99 + if (typeof requestAnimationFrame !== 'function') {
100 + // Using console['error'] to evade Babel and ESLint
101 + console['error'](
102 + "This browser doesn't support requestAnimationFrame. " +
103 + 'Make sure that you load a ' +
104 + 'polyfill in older browsers. https://reactjs.org/link/react-polyfills',
105 + );
106 + }
107 + if (typeof cancelAnimationFrame !== 'function') {
108 + // Using console['error'] to evade Babel and ESLint
109 + console['error'](
110 + "This browser doesn't support cancelAnimationFrame. " +
111 + 'Make sure that you load a ' +
112 + 'polyfill in older browsers. https://reactjs.org/link/react-polyfills',
113 + );
114 + }
115 +}
116 +
117 +function advanceTimers(currentTime) {
118 + // Check for tasks that are no longer delayed and add them to the queue.
119 + let timer = peek(timerQueue);
120 + while (timer !== null) {
121 + if (timer.callback === null) {
122 + // Timer was cancelled.
123 + pop(timerQueue);
124 + } else if (timer.startTime <= currentTime) {
125 + // Timer fired. Transfer to the task queue.
126 + pop(timerQueue);
127 + timer.sortIndex = timer.expirationTime;
128 + push(taskQueue, timer);
129 + if (enableProfiling) {
130 + markTaskStart(timer, currentTime);
131 + timer.isQueued = true;
132 + }
133 + } else {
134 + // Remaining timers are pending.
135 + return;
136 + }
137 + timer = peek(timerQueue);
138 + }
139 +}
140 +
141 +function handleTimeout(currentTime) {
142 + isHostTimeoutScheduled = false;
143 + advanceTimers(currentTime);
144 +
145 + if (!isHostCallbackScheduled) {
146 + if (peek(taskQueue) !== null) {
147 + isHostCallbackScheduled = true;
148 + requestHostCallback(flushWork);
149 + } else {
150 + const firstTimer = peek(timerQueue);
151 + if (firstTimer !== null) {
152 + requestHostTimeout(handleTimeout, firstTimer.startTime - currentTime);
153 + }
154 + }
155 + }
156 +}
157 +
158 +function flushWork(hasTimeRemaining, initialTime) {
159 + if (enableProfiling) {
160 + markSchedulerUnsuspended(initialTime);
161 + }
162 +
163 + // We'll need a host callback the next time work is scheduled.
164 + isHostCallbackScheduled = false;
165 + if (isHostTimeoutScheduled) {
166 + // We scheduled a timeout but it's no longer needed. Cancel it.
167 + isHostTimeoutScheduled = false;
168 + cancelHostTimeout();
169 + }
170 +
171 + isPerformingWork = true;
172 + const previousPriorityLevel = currentPriorityLevel;
173 + try {
174 + if (enableProfiling) {
175 + try {
176 + return workLoop(hasTimeRemaining, initialTime);
177 + } catch (error) {
178 + if (currentTask !== null) {
179 + const currentTime = getCurrentTime();
180 + markTaskErrored(currentTask, currentTime);
181 + currentTask.isQueued = false;
182 + }
183 + throw error;
184 + }
185 + } else {
186 + // No catch in prod code path.
187 + return workLoop(hasTimeRemaining, initialTime);
188 + }
189 + } finally {
190 + currentTask = null;
191 + currentPriorityLevel = previousPriorityLevel;
192 + isPerformingWork = false;
193 + if (enableProfiling) {
194 + const currentTime = getCurrentTime();
195 + markSchedulerSuspended(currentTime);
196 + }
197 + }
198 +}
199 +
200 +function workLoop(hasTimeRemaining, initialTime) {
201 + let currentTime = initialTime;
202 + advanceTimers(currentTime);
203 + currentTask = peek(taskQueue);
204 + while (
205 + currentTask !== null &&
206 + !(enableSchedulerDebugging && isSchedulerPaused)
207 + ) {
208 + if (
209 + currentTask.expirationTime > currentTime &&
210 + (!hasTimeRemaining || shouldYieldToHost())
211 + ) {
212 + // This currentTask hasn't expired, and we've reached the deadline.
213 + break;
214 + }
215 + const callback = currentTask.callback;
216 + if (typeof callback === 'function') {
217 + currentTask.callback = null;
218 + currentPriorityLevel = currentTask.priorityLevel;
219 + const didUserCallbackTimeout = currentTask.expirationTime <= currentTime;
220 + markTaskRun(currentTask, currentTime);
221 + const continuationCallback = callback(didUserCallbackTimeout);
222 + currentTime = getCurrentTime();
223 + if (typeof continuationCallback === 'function') {
224 + currentTask.callback = continuationCallback;
225 + markTaskYield(currentTask, currentTime);
226 + } else {
227 + if (enableProfiling) {
228 + markTaskCompleted(currentTask, currentTime);
229 + currentTask.isQueued = false;
230 + }
231 + if (currentTask === peek(taskQueue)) {
232 + pop(taskQueue);
233 + }
234 + }
235 + advanceTimers(currentTime);
236 + } else {
237 + pop(taskQueue);
238 + }
239 + currentTask = peek(taskQueue);
240 + }
241 + // Return whether there's additional work
242 + if (currentTask !== null) {
243 + return true;
244 + } else {
245 + const firstTimer = peek(timerQueue);
246 + if (firstTimer !== null) {
247 + requestHostTimeout(handleTimeout, firstTimer.startTime - currentTime);
248 + }
249 + return false;
250 + }
251 +}
252 +
253 +function unstable_runWithPriority(priorityLevel, eventHandler) {
254 + switch (priorityLevel) {
255 + case ImmediatePriority:
256 + case UserBlockingPriority:
257 + case NormalPriority:
258 + case LowPriority:
259 + case IdlePriority:
260 + break;
261 + default:
262 + priorityLevel = NormalPriority;
263 + }
264 +
265 + var previousPriorityLevel = currentPriorityLevel;
266 + currentPriorityLevel = priorityLevel;
267 +
268 + try {
269 + return eventHandler();
270 + } finally {
271 + currentPriorityLevel = previousPriorityLevel;
272 + }
273 +}
274 +
275 +function unstable_next(eventHandler) {
276 + var priorityLevel;
277 + switch (currentPriorityLevel) {
278 + case ImmediatePriority:
279 + case UserBlockingPriority:
280 + case NormalPriority:
281 + // Shift down to normal priority
282 + priorityLevel = NormalPriority;
283 + break;
284 + default:
285 + // Anything lower than normal priority should remain at the current level.
286 + priorityLevel = currentPriorityLevel;
287 + break;
288 + }
289 +
290 + var previousPriorityLevel = currentPriorityLevel;
291 + currentPriorityLevel = priorityLevel;
292 +
293 + try {
294 + return eventHandler();
295 + } finally {
296 + currentPriorityLevel = previousPriorityLevel;
297 + }
298 +}
299 +
300 +function unstable_wrapCallback(callback) {
301 + var parentPriorityLevel = currentPriorityLevel;
302 + return function() {
303 + // This is a fork of runWithPriority, inlined for performance.
304 + var previousPriorityLevel = currentPriorityLevel;
305 + currentPriorityLevel = parentPriorityLevel;
306 +
307 + try {
308 + return callback.apply(this, arguments);
309 + } finally {
310 + currentPriorityLevel = previousPriorityLevel;
311 + }
312 + };
313 +}
314 +
315 +function unstable_scheduleCallback(priorityLevel, callback, options) {
316 + var currentTime = getCurrentTime();
317 +
318 + var startTime;
319 + if (typeof options === 'object' && options !== null) {
320 + var delay = options.delay;
321 + if (typeof delay === 'number' && delay > 0) {
322 + startTime = currentTime + delay;
323 + } else {
324 + startTime = currentTime;
325 + }
326 + } else {
327 + startTime = currentTime;
328 + }
329 +
330 + var timeout;
331 + switch (priorityLevel) {
332 + case ImmediatePriority:
333 + timeout = IMMEDIATE_PRIORITY_TIMEOUT;
334 + break;
335 + case UserBlockingPriority:
336 + timeout = USER_BLOCKING_PRIORITY_TIMEOUT;
337 + break;
338 + case IdlePriority:
339 + timeout = IDLE_PRIORITY_TIMEOUT;
340 + break;
341 + case LowPriority:
342 + timeout = LOW_PRIORITY_TIMEOUT;
343 + break;
344 + case NormalPriority:
345 + default:
346 + timeout = NORMAL_PRIORITY_TIMEOUT;
347 + break;
348 + }
349 +
350 + var expirationTime = startTime + timeout;
351 +
352 + var newTask = {
353 + id: taskIdCounter++,
354 + callback,
355 + priorityLevel,
356 + startTime,
357 + expirationTime,
358 + sortIndex: -1,
359 + };
360 + if (enableProfiling) {
361 + newTask.isQueued = false;
362 + }
363 +
364 + if (startTime > currentTime) {
365 + // This is a delayed task.
366 + newTask.sortIndex = startTime;
367 + push(timerQueue, newTask);
368 + if (peek(taskQueue) === null && newTask === peek(timerQueue)) {
369 + // All tasks are delayed, and this is the task with the earliest delay.
370 + if (isHostTimeoutScheduled) {
371 + // Cancel an existing timeout.
372 + cancelHostTimeout();
373 + } else {
374 + isHostTimeoutScheduled = true;
375 + }
376 + // Schedule a timeout.
377 + requestHostTimeout(handleTimeout, startTime - currentTime);
378 + }
379 + } else {
380 + newTask.sortIndex = expirationTime;
381 + push(taskQueue, newTask);
382 + if (enableProfiling) {
383 + markTaskStart(newTask, currentTime);
384 + newTask.isQueued = true;
385 + }
386 + // Schedule a host callback, if needed. If we're already performing work,
387 + // wait until the next time we yield.
388 + if (!isHostCallbackScheduled && !isPerformingWork) {
389 + isHostCallbackScheduled = true;
390 + requestHostCallback(flushWork);
391 + }
392 + }
393 +
394 + return newTask;
395 +}
396 +
397 +function unstable_pauseExecution() {
398 + isSchedulerPaused = true;
399 +}
400 +
401 +function unstable_continueExecution() {
402 + isSchedulerPaused = false;
403 + if (!isHostCallbackScheduled && !isPerformingWork) {
404 + isHostCallbackScheduled = true;
405 + requestHostCallback(flushWork);
406 + }
407 +}
408 +
409 +function unstable_getFirstCallbackNode() {
410 + return peek(taskQueue);
411 +}
412 +
413 +function unstable_cancelCallback(task) {
414 + if (enableProfiling) {
415 + if (task.isQueued) {
416 + const currentTime = getCurrentTime();
417 + markTaskCanceled(task, currentTime);
418 + task.isQueued = false;
419 + }
420 + }
421 +
422 + // Null out the callback to indicate the task has been canceled. (Can't
423 + // remove from the queue because you can't remove arbitrary nodes from an
424 + // array based heap, only the first one.)
425 + task.callback = null;
426 +}
427 +
428 +function unstable_getCurrentPriorityLevel() {
429 + return currentPriorityLevel;
430 +}
431 +
432 +let isMessageLoopRunning = false;
433 +let scheduledHostCallback = null;
434 +let taskTimeoutID = -1;
435 +
436 +// Scheduler periodically yields in case there is other work on the main
437 +// thread, like user events. By default, it yields multiple times per frame.
438 +// It does not attempt to align with frame boundaries, since most tasks don't
439 +// need to be frame aligned; for those that do, use requestAnimationFrame.
440 +let yieldInterval = 5;
441 +let deadline = 0;
442 +
443 +// TODO: Make this configurable
444 +// TODO: Adjust this based on priority?
445 +const maxYieldInterval = 300;
446 +let needsPaint = false;
447 +
448 +function shouldYieldToHost() {
449 + if (
450 + enableIsInputPending &&
451 + navigator !== undefined &&
452 + navigator.scheduling !== undefined &&
453 + navigator.scheduling.isInputPending !== undefined
454 + ) {
455 + const scheduling = navigator.scheduling;
456 + const currentTime = getCurrentTime();
457 + if (currentTime >= deadline) {
458 + // There's no time left. We may want to yield control of the main
459 + // thread, so the browser can perform high priority tasks. The main ones
460 + // are painting and user input. If there's a pending paint or a pending
461 + // input, then we should yield. But if there's neither, then we can
462 + // yield less often while remaining responsive. We'll eventually yield
463 + // regardless, since there could be a pending paint that wasn't
464 + // accompanied by a call to `requestPaint`, or other main thread tasks
465 + // like network events.
466 + if (needsPaint || scheduling.isInputPending()) {
467 + // There is either a pending paint or a pending input.
468 + return true;
469 + }
470 + // There's no pending input. Only yield if we've reached the max
471 + // yield interval.
472 + return currentTime >= maxYieldInterval;
473 + } else {
474 + // There's still time left in the frame.
475 + return false;
476 + }
477 + } else {
478 + // `isInputPending` is not available. Since we have no way of knowing if
479 + // there's pending input, always yield at the end of the frame.
480 + return getCurrentTime() >= deadline;
481 + }
482 +}
483 +
484 +function requestPaint() {
485 + if (
486 + enableIsInputPending &&
487 + navigator !== undefined &&
488 + navigator.scheduling !== undefined &&
489 + navigator.scheduling.isInputPending !== undefined
490 + ) {
491 + needsPaint = true;
492 + }
493 +
494 + // Since we yield every frame regardless, `requestPaint` has no effect.
495 +}
496 +
497 +function forceFrameRate(fps) {
498 + if (fps < 0 || fps > 125) {
499 + // Using console['error'] to evade Babel and ESLint
500 + console['error'](
501 + 'forceFrameRate takes a positive int between 0 and 125, ' +
502 + 'forcing frame rates higher than 125 fps is not supported',
503 + );
504 + return;
505 + }
506 + if (fps > 0) {
507 + yieldInterval = Math.floor(1000 / fps);
508 + } else {
509 + // reset the framerate
510 + yieldInterval = 5;
511 + }
512 +}
513 +
514 +const performWorkUntilDeadline = () => {
515 + if (scheduledHostCallback !== null) {
516 + const currentTime = getCurrentTime();
517 + // Yield after `yieldInterval` ms, regardless of where we are in the vsync
518 + // cycle. This means there's always time remaining at the beginning of
519 + // the message event.
520 + deadline = currentTime + yieldInterval;
521 + const hasTimeRemaining = true;
522 + try {
523 + const hasMoreWork = scheduledHostCallback(hasTimeRemaining, currentTime);
524 + if (!hasMoreWork) {
525 + isMessageLoopRunning = false;
526 + scheduledHostCallback = null;
527 + } else {
528 + // If there's more work, schedule the next message event at the end
529 + // of the preceding one.
530 + port.postMessage(null);
531 + }
532 + } catch (error) {
533 + // If a scheduler task throws, exit the current browser task so the
534 + // error can be observed.
535 + port.postMessage(null);
536 + throw error;
537 + }
538 + } else {
539 + isMessageLoopRunning = false;
540 + }
541 + // Yielding to the browser will give it a chance to paint, so we can
542 + // reset this.
543 + needsPaint = false;
544 +};
545 +
546 +const channel = new MessageChannel();
547 +const port = channel.port2;
548 +channel.port1.onmessage = performWorkUntilDeadline;
549 +
550 +function requestHostCallback(callback) {
551 + scheduledHostCallback = callback;
552 + if (!isMessageLoopRunning) {
553 + isMessageLoopRunning = true;
554 + port.postMessage(null);
555 + }
556 +}
557 +
558 +function requestHostTimeout(callback, ms) {
559 + taskTimeoutID = setTimeout(() => {
560 + callback(getCurrentTime());
561 + }, ms);
562 +}
563 +
564 +function cancelHostTimeout() {
565 + clearTimeout(taskTimeoutID);
566 + taskTimeoutID = -1;
567 +}
568 +
569 +const unstable_requestPaint = requestPaint;
570 +
571 +export {
572 + ImmediatePriority as unstable_ImmediatePriority,
573 + UserBlockingPriority as unstable_UserBlockingPriority,
574 + NormalPriority as unstable_NormalPriority,
575 + IdlePriority as unstable_IdlePriority,
576 + LowPriority as unstable_LowPriority,
577 + unstable_runWithPriority,
578 + unstable_next,
579 + unstable_scheduleCallback,
580 + unstable_cancelCallback,
581 + unstable_wrapCallback,
582 + unstable_getCurrentPriorityLevel,
583 + shouldYieldToHost as unstable_shouldYield,
584 + unstable_requestPaint,
585 + unstable_continueExecution,
586 + unstable_pauseExecution,
587 + unstable_getFirstCallbackNode,
588 + getCurrentTime as unstable_now,
589 + forceFrameRate as unstable_forceFrameRate,
590 +};
591 +
592 +export const unstable_Profiling = enableProfiling
593 + ? {
594 + startLoggingProfilingEvents,
595 + stopLoggingProfilingEvents,
596 + sharedProfilingBuffer,
597 + }
598 + : null;
packages/scheduler/src/forks/SchedulerFeatureFlags.www.js
+1 -1
@@ -10,6 +10,6 @@ export const {
10 enableIsInputPending,
11 enableSchedulerDebugging,
12 enableProfiling: enableProfilingFeatureFlag,
13 -} = require('SchedulerFeatureFlags');
13 +} = require('packages/scheduler/src/SchedulerFeatureFlags');
14
15 export const enableProfiling = __PROFILE__ && enableProfilingFeatureFlag;
packages/scheduler/src/forks/SchedulerHostConfig.default.js deleted
-246
@@ -1,246 +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 -
8 -import {enableIsInputPending} from '../SchedulerFeatureFlags';
9 -
10 -export let requestHostCallback;
11 -export let cancelHostCallback;
12 -export let requestHostTimeout;
13 -export let cancelHostTimeout;
14 -export let shouldYieldToHost;
15 -export let requestPaint;
16 -export let getCurrentTime;
17 -export let forceFrameRate;
18 -
19 -const hasPerformanceNow =
20 - typeof performance === 'object' && typeof performance.now === 'function';
21 -
22 -if (hasPerformanceNow) {
23 - const localPerformance = performance;
24 - getCurrentTime = () => localPerformance.now();
25 -} else {
26 - const localDate = Date;
27 - const initialTime = localDate.now();
28 - getCurrentTime = () => localDate.now() - initialTime;
29 -}
30 -
31 -if (
32 - // If Scheduler runs in a non-DOM environment, it falls back to a naive
33 - // implementation using setTimeout.
34 - typeof window === 'undefined' ||
35 - // Check if MessageChannel is supported, too.
36 - typeof MessageChannel !== 'function'
37 -) {
38 - // If this accidentally gets imported in a non-browser environment, e.g. JavaScriptCore,
39 - // fallback to a naive implementation.
40 - let _callback = null;
41 - let _timeoutID = null;
42 - const _flushCallback = function() {
43 - if (_callback !== null) {
44 - try {
45 - const currentTime = getCurrentTime();
46 - const hasRemainingTime = true;
47 - _callback(hasRemainingTime, currentTime);
48 - _callback = null;
49 - } catch (e) {
50 - setTimeout(_flushCallback, 0);
51 - throw e;
52 - }
53 - }
54 - };
55 - requestHostCallback = function(cb) {
56 - if (_callback !== null) {
57 - // Protect against re-entrancy.
58 - setTimeout(requestHostCallback, 0, cb);
59 - } else {
60 - _callback = cb;
61 - setTimeout(_flushCallback, 0);
62 - }
63 - };
64 - cancelHostCallback = function() {
65 - _callback = null;
66 - };
67 - requestHostTimeout = function(cb, ms) {
68 - _timeoutID = setTimeout(cb, ms);
69 - };
70 - cancelHostTimeout = function() {
71 - clearTimeout(_timeoutID);
72 - };
73 - shouldYieldToHost = function() {
74 - return false;
75 - };
76 - requestPaint = forceFrameRate = function() {};
77 -} else {
78 - // Capture local references to native APIs, in case a polyfill overrides them.
79 - const setTimeout = window.setTimeout;
80 - const clearTimeout = window.clearTimeout;
81 -
82 - if (typeof console !== 'undefined') {
83 - // TODO: Scheduler no longer requires these methods to be polyfilled. But
84 - // maybe we want to continue warning if they don't exist, to preserve the
85 - // option to rely on it in the future?
86 - const requestAnimationFrame = window.requestAnimationFrame;
87 - const cancelAnimationFrame = window.cancelAnimationFrame;
88 -
89 - if (typeof requestAnimationFrame !== 'function') {
90 - // Using console['error'] to evade Babel and ESLint
91 - console['error'](
92 - "This browser doesn't support requestAnimationFrame. " +
93 - 'Make sure that you load a ' +
94 - 'polyfill in older browsers. https://reactjs.org/link/react-polyfills',
95 - );
96 - }
97 - if (typeof cancelAnimationFrame !== 'function') {
98 - // Using console['error'] to evade Babel and ESLint
99 - console['error'](
100 - "This browser doesn't support cancelAnimationFrame. " +
101 - 'Make sure that you load a ' +
102 - 'polyfill in older browsers. https://reactjs.org/link/react-polyfills',
103 - );
104 - }
105 - }
106 -
107 - let isMessageLoopRunning = false;
108 - let scheduledHostCallback = null;
109 - let taskTimeoutID = -1;
110 -
111 - // Scheduler periodically yields in case there is other work on the main
112 - // thread, like user events. By default, it yields multiple times per frame.
113 - // It does not attempt to align with frame boundaries, since most tasks don't
114 - // need to be frame aligned; for those that do, use requestAnimationFrame.
115 - let yieldInterval = 5;
116 - let deadline = 0;
117 -
118 - // TODO: Make this configurable
119 - // TODO: Adjust this based on priority?
120 - const maxYieldInterval = 300;
121 - let needsPaint = false;
122 -
123 - if (
124 - enableIsInputPending &&
125 - navigator !== undefined &&
126 - navigator.scheduling !== undefined &&
127 - navigator.scheduling.isInputPending !== undefined
128 - ) {
129 - const scheduling = navigator.scheduling;
130 - shouldYieldToHost = function() {
131 - const currentTime = getCurrentTime();
132 - if (currentTime >= deadline) {
133 - // There's no time left. We may want to yield control of the main
134 - // thread, so the browser can perform high priority tasks. The main ones
135 - // are painting and user input. If there's a pending paint or a pending
136 - // input, then we should yield. But if there's neither, then we can
137 - // yield less often while remaining responsive. We'll eventually yield
138 - // regardless, since there could be a pending paint that wasn't
139 - // accompanied by a call to `requestPaint`, or other main thread tasks
140 - // like network events.
141 - if (needsPaint || scheduling.isInputPending()) {
142 - // There is either a pending paint or a pending input.
143 - return true;
144 - }
145 - // There's no pending input. Only yield if we've reached the max
146 - // yield interval.
147 - return currentTime >= maxYieldInterval;
148 - } else {
149 - // There's still time left in the frame.
150 - return false;
151 - }
152 - };
153 -
154 - requestPaint = function() {
155 - needsPaint = true;
156 - };
157 - } else {
158 - // `isInputPending` is not available. Since we have no way of knowing if
159 - // there's pending input, always yield at the end of the frame.
160 - shouldYieldToHost = function() {
161 - return getCurrentTime() >= deadline;
162 - };
163 -
164 - // Since we yield every frame regardless, `requestPaint` has no effect.
165 - requestPaint = function() {};
166 - }
167 -
168 - forceFrameRate = function(fps) {
169 - if (fps < 0 || fps > 125) {
170 - // Using console['error'] to evade Babel and ESLint
171 - console['error'](
172 - 'forceFrameRate takes a positive int between 0 and 125, ' +
173 - 'forcing frame rates higher than 125 fps is not supported',
174 - );
175 - return;
176 - }
177 - if (fps > 0) {
178 - yieldInterval = Math.floor(1000 / fps);
179 - } else {
180 - // reset the framerate
181 - yieldInterval = 5;
182 - }
183 - };
184 -
185 - const performWorkUntilDeadline = () => {
186 - if (scheduledHostCallback !== null) {
187 - const currentTime = getCurrentTime();
188 - // Yield after `yieldInterval` ms, regardless of where we are in the vsync
189 - // cycle. This means there's always time remaining at the beginning of
190 - // the message event.
191 - deadline = currentTime + yieldInterval;
192 - const hasTimeRemaining = true;
193 - try {
194 - const hasMoreWork = scheduledHostCallback(
195 - hasTimeRemaining,
196 - currentTime,
197 - );
198 - if (!hasMoreWork) {
199 - isMessageLoopRunning = false;
200 - scheduledHostCallback = null;
201 - } else {
202 - // If there's more work, schedule the next message event at the end
203 - // of the preceding one.
204 - port.postMessage(null);
205 - }
206 - } catch (error) {
207 - // If a scheduler task throws, exit the current browser task so the
208 - // error can be observed.
209 - port.postMessage(null);
210 - throw error;
211 - }
212 - } else {
213 - isMessageLoopRunning = false;
214 - }
215 - // Yielding to the browser will give it a chance to paint, so we can
216 - // reset this.
217 - needsPaint = false;
218 - };
219 -
220 - const channel = new MessageChannel();
221 - const port = channel.port2;
222 - channel.port1.onmessage = performWorkUntilDeadline;
223 -
224 - requestHostCallback = function(callback) {
225 - scheduledHostCallback = callback;
226 - if (!isMessageLoopRunning) {
227 - isMessageLoopRunning = true;
228 - port.postMessage(null);
229 - }
230 - };
231 -
232 - cancelHostCallback = function() {
233 - scheduledHostCallback = null;
234 - };
235 -
236 - requestHostTimeout = function(callback, ms) {
237 - taskTimeoutID = setTimeout(() => {
238 - callback(getCurrentTime());
239 - }, ms);
240 - };
241 -
242 - cancelHostTimeout = function() {
243 - clearTimeout(taskTimeoutID);
244 - taskTimeoutID = -1;
245 - };
246 -}
packages/scheduler/src/forks/SchedulerHostConfig.mock.js deleted
-225
@@ -1,225 +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 - * @flow
8 - */
9 -
10 -let currentTime: number = 0;
11 -let scheduledCallback: ((boolean, number) => void) | null = null;
12 -let scheduledTimeout: (number => void) | null = null;
13 -let timeoutTime: number = -1;
14 -let yieldedValues: Array<mixed> | null = null;
15 -let expectedNumberOfYields: number = -1;
16 -let didStop: boolean = false;
17 -let isFlushing: boolean = false;
18 -let needsPaint: boolean = false;
19 -let shouldYieldForPaint: boolean = false;
20 -
21 -export function requestHostCallback(callback: boolean => void) {
22 - scheduledCallback = callback;
23 -}
24 -
25 -export function cancelHostCallback(): void {
26 - scheduledCallback = null;
27 -}
28 -
29 -export function requestHostTimeout(callback: number => void, ms: number) {
30 - scheduledTimeout = callback;
31 - timeoutTime = currentTime + ms;
32 -}
33 -
34 -export function cancelHostTimeout(): void {
35 - scheduledTimeout = null;
36 - timeoutTime = -1;
37 -}
38 -
39 -export function shouldYieldToHost(): boolean {
40 - if (
41 - (expectedNumberOfYields !== -1 &&
42 - yieldedValues !== null &&
43 - yieldedValues.length >= expectedNumberOfYields) ||
44 - (shouldYieldForPaint && needsPaint)
45 - ) {
46 - // We yielded at least as many values as expected. Stop flushing.
47 - didStop = true;
48 - return true;
49 - }
50 - return false;
51 -}
52 -
53 -export function getCurrentTime(): number {
54 - return currentTime;
55 -}
56 -
57 -export function forceFrameRate() {
58 - // No-op
59 -}
60 -
61 -export function reset() {
62 - if (isFlushing) {
63 - throw new Error('Cannot reset while already flushing work.');
64 - }
65 - currentTime = 0;
66 - scheduledCallback = null;
67 - scheduledTimeout = null;
68 - timeoutTime = -1;
69 - yieldedValues = null;
70 - expectedNumberOfYields = -1;
71 - didStop = false;
72 - isFlushing = false;
73 - needsPaint = false;
74 -}
75 -
76 -// Should only be used via an assertion helper that inspects the yielded values.
77 -export function unstable_flushNumberOfYields(count: number): void {
78 - if (isFlushing) {
79 - throw new Error('Already flushing work.');
80 - }
81 - if (scheduledCallback !== null) {
82 - const cb = scheduledCallback;
83 - expectedNumberOfYields = count;
84 - isFlushing = true;
85 - try {
86 - let hasMoreWork = true;
87 - do {
88 - hasMoreWork = cb(true, currentTime);
89 - } while (hasMoreWork && !didStop);
90 - if (!hasMoreWork) {
91 - scheduledCallback = null;
92 - }
93 - } finally {
94 - expectedNumberOfYields = -1;
95 - didStop = false;
96 - isFlushing = false;
97 - }
98 - }
99 -}
100 -
101 -export function unstable_flushUntilNextPaint(): void {
102 - if (isFlushing) {
103 - throw new Error('Already flushing work.');
104 - }
105 - if (scheduledCallback !== null) {
106 - const cb = scheduledCallback;
107 - shouldYieldForPaint = true;
108 - needsPaint = false;
109 - isFlushing = true;
110 - try {
111 - let hasMoreWork = true;
112 - do {
113 - hasMoreWork = cb(true, currentTime);
114 - } while (hasMoreWork && !didStop);
115 - if (!hasMoreWork) {
116 - scheduledCallback = null;
117 - }
118 - } finally {
119 - shouldYieldForPaint = false;
120 - didStop = false;
121 - isFlushing = false;
122 - }
123 - }
124 -}
125 -
126 -export function unstable_flushExpired() {
127 - if (isFlushing) {
128 - throw new Error('Already flushing work.');
129 - }
130 - if (scheduledCallback !== null) {
131 - isFlushing = true;
132 - try {
133 - const hasMoreWork = scheduledCallback(false, currentTime);
134 - if (!hasMoreWork) {
135 - scheduledCallback = null;
136 - }
137 - } finally {
138 - isFlushing = false;
139 - }
140 - }
141 -}
142 -
143 -export function unstable_flushAllWithoutAsserting(): boolean {
144 - // Returns false if no work was flushed.
145 - if (isFlushing) {
146 - throw new Error('Already flushing work.');
147 - }
148 - if (scheduledCallback !== null) {
149 - const cb = scheduledCallback;
150 - isFlushing = true;
151 - try {
152 - let hasMoreWork = true;
153 - do {
154 - hasMoreWork = cb(true, currentTime);
155 - } while (hasMoreWork);
156 - if (!hasMoreWork) {
157 - scheduledCallback = null;
158 - }
159 - return true;
160 - } finally {
161 - isFlushing = false;
162 - }
163 - } else {
164 - return false;
165 - }
166 -}
167 -
168 -export function unstable_clearYields(): Array<mixed> {
169 - if (yieldedValues === null) {
170 - return [];
171 - }
172 - const values = yieldedValues;
173 - yieldedValues = null;
174 - return values;
175 -}
176 -
177 -export function unstable_flushAll(): void {
178 - if (yieldedValues !== null) {
179 - throw new Error(
180 - 'Log is not empty. Assert on the log of yielded values before ' +
181 - 'flushing additional work.',
182 - );
183 - }
184 - unstable_flushAllWithoutAsserting();
185 - if (yieldedValues !== null) {
186 - throw new Error(
187 - 'While flushing work, something yielded a value. Use an ' +
188 - 'assertion helper to assert on the log of yielded values, e.g. ' +
189 - 'expect(Scheduler).toFlushAndYield([...])',
190 - );
191 - }
192 -}
193 -
194 -export function unstable_yieldValue(value: mixed): void {
195 - // eslint-disable-next-line react-internal/no-production-logging
196 - if (console.log.name === 'disabledLog') {
197 - // If console.log has been patched, we assume we're in render
198 - // replaying and we ignore any values yielding in the second pass.
199 - return;
200 - }
201 - if (yieldedValues === null) {
202 - yieldedValues = [value];
203 - } else {
204 - yieldedValues.push(value);
205 - }
206 -}
207 -
208 -export function unstable_advanceTime(ms: number) {
209 - // eslint-disable-next-line react-internal/no-production-logging
210 - if (console.log.name === 'disabledLog') {
211 - // If console.log has been patched, we assume we're in render
212 - // replaying and we ignore any time advancing in the second pass.
213 - return;
214 - }
215 - currentTime += ms;
216 - if (scheduledTimeout !== null && timeoutTime <= currentTime) {
217 - scheduledTimeout(currentTime);
218 - timeoutTime = -1;
219 - scheduledTimeout = null;
220 - }
221 -}
222 -
223 -export function requestPaint() {
224 - needsPaint = true;
225 -}
packages/scheduler/src/forks/SchedulerMock.js new
+636
@@ -0,0 +1,636 @@
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 +import {push, pop, peek} from '../SchedulerMinHeap';
16 +
17 +// TODO: Use symbols?
18 +import {
19 + ImmediatePriority,
20 + UserBlockingPriority,
21 + NormalPriority,
22 + LowPriority,
23 + IdlePriority,
24 +} from '../SchedulerPriorities';
25 +import {
26 + sharedProfilingBuffer,
27 + markTaskRun,
28 + markTaskYield,
29 + markTaskCompleted,
30 + markTaskCanceled,
31 + markTaskErrored,
32 + markSchedulerSuspended,
33 + markSchedulerUnsuspended,
34 + markTaskStart,
35 + stopLoggingProfilingEvents,
36 + startLoggingProfilingEvents,
37 +} from '../SchedulerProfiling';
38 +
39 +// Max 31 bit integer. The max integer size in V8 for 32-bit systems.
40 +// Math.pow(2, 30) - 1
41 +// 0b111111111111111111111111111111
42 +var maxSigned31BitInt = 1073741823;
43 +
44 +// Times out immediately
45 +var IMMEDIATE_PRIORITY_TIMEOUT = -1;
46 +// Eventually times out
47 +var USER_BLOCKING_PRIORITY_TIMEOUT = 250;
48 +var NORMAL_PRIORITY_TIMEOUT = 5000;
49 +var LOW_PRIORITY_TIMEOUT = 10000;
50 +// Never times out
51 +var IDLE_PRIORITY_TIMEOUT = maxSigned31BitInt;
52 +
53 +// Tasks are stored on a min heap
54 +var taskQueue = [];
55 +var timerQueue = [];
56 +
57 +// Incrementing id counter. Used to maintain insertion order.
58 +var taskIdCounter = 1;
59 +
60 +// Pausing the scheduler is useful for debugging.
61 +var isSchedulerPaused = false;
62 +
63 +var currentTask = null;
64 +var currentPriorityLevel = NormalPriority;
65 +
66 +// This is set while performing work, to prevent re-entrancy.
67 +var isPerformingWork = false;
68 +
69 +var isHostCallbackScheduled = false;
70 +var isHostTimeoutScheduled = false;
71 +
72 +let currentMockTime: number = 0;
73 +let scheduledCallback: ((boolean, number) => void) | null = null;
74 +let scheduledTimeout: (number => void) | null = null;
75 +let timeoutTime: number = -1;
76 +let yieldedValues: Array<mixed> | null = null;
77 +let expectedNumberOfYields: number = -1;
78 +let didStop: boolean = false;
79 +let isFlushing: boolean = false;
80 +let needsPaint: boolean = false;
81 +let shouldYieldForPaint: boolean = false;
82 +
83 +function advanceTimers(currentTime) {
84 + // Check for tasks that are no longer delayed and add them to the queue.
85 + let timer = peek(timerQueue);
86 + while (timer !== null) {
87 + if (timer.callback === null) {
88 + // Timer was cancelled.
89 + pop(timerQueue);
90 + } else if (timer.startTime <= currentTime) {
91 + // Timer fired. Transfer to the task queue.
92 + pop(timerQueue);
93 + timer.sortIndex = timer.expirationTime;
94 + push(taskQueue, timer);
95 + if (enableProfiling) {
96 + markTaskStart(timer, currentTime);
97 + timer.isQueued = true;
98 + }
99 + } else {
100 + // Remaining timers are pending.
101 + return;
102 + }
103 + timer = peek(timerQueue);
104 + }
105 +}
106 +
107 +function handleTimeout(currentTime) {
108 + isHostTimeoutScheduled = false;
109 + advanceTimers(currentTime);
110 +
111 + if (!isHostCallbackScheduled) {
112 + if (peek(taskQueue) !== null) {
113 + isHostCallbackScheduled = true;
114 + requestHostCallback(flushWork);
115 + } else {
116 + const firstTimer = peek(timerQueue);
117 + if (firstTimer !== null) {
118 + requestHostTimeout(handleTimeout, firstTimer.startTime - currentTime);
119 + }
120 + }
121 + }
122 +}
123 +
124 +function flushWork(hasTimeRemaining, initialTime) {
125 + if (enableProfiling) {
126 + markSchedulerUnsuspended(initialTime);
127 + }
128 +
129 + // We'll need a host callback the next time work is scheduled.
130 + isHostCallbackScheduled = false;
131 + if (isHostTimeoutScheduled) {
132 + // We scheduled a timeout but it's no longer needed. Cancel it.
133 + isHostTimeoutScheduled = false;
134 + cancelHostTimeout();
135 + }
136 +
137 + isPerformingWork = true;
138 + const previousPriorityLevel = currentPriorityLevel;
139 + try {
140 + if (enableProfiling) {
141 + try {
142 + return workLoop(hasTimeRemaining, initialTime);
143 + } catch (error) {
144 + if (currentTask !== null) {
145 + const currentTime = getCurrentTime();
146 + markTaskErrored(currentTask, currentTime);
147 + currentTask.isQueued = false;
148 + }
149 + throw error;
150 + }
151 + } else {
152 + // No catch in prod code path.
153 + return workLoop(hasTimeRemaining, initialTime);
154 + }
155 + } finally {
156 + currentTask = null;
157 + currentPriorityLevel = previousPriorityLevel;
158 + isPerformingWork = false;
159 + if (enableProfiling) {
160 + const currentTime = getCurrentTime();
161 + markSchedulerSuspended(currentTime);
162 + }
163 + }
164 +}
165 +
166 +function workLoop(hasTimeRemaining, initialTime) {
167 + let currentTime = initialTime;
168 + advanceTimers(currentTime);
169 + currentTask = peek(taskQueue);
170 + while (
171 + currentTask !== null &&
172 + !(enableSchedulerDebugging && isSchedulerPaused)
173 + ) {
174 + if (
175 + currentTask.expirationTime > currentTime &&
176 + (!hasTimeRemaining || shouldYieldToHost())
177 + ) {
178 + // This currentTask hasn't expired, and we've reached the deadline.
179 + break;
180 + }
181 + const callback = currentTask.callback;
182 + if (typeof callback === 'function') {
183 + currentTask.callback = null;
184 + currentPriorityLevel = currentTask.priorityLevel;
185 + const didUserCallbackTimeout = currentTask.expirationTime <= currentTime;
186 + markTaskRun(currentTask, currentTime);
187 + const continuationCallback = callback(didUserCallbackTimeout);
188 + currentTime = getCurrentTime();
189 + if (typeof continuationCallback === 'function') {
190 + currentTask.callback = continuationCallback;
191 + markTaskYield(currentTask, currentTime);
192 + } else {
193 + if (enableProfiling) {
194 + markTaskCompleted(currentTask, currentTime);
195 + currentTask.isQueued = false;
196 + }
197 + if (currentTask === peek(taskQueue)) {
198 + pop(taskQueue);
199 + }
200 + }
201 + advanceTimers(currentTime);
202 + } else {
203 + pop(taskQueue);
204 + }
205 + currentTask = peek(taskQueue);
206 + }
207 + // Return whether there's additional work
208 + if (currentTask !== null) {
209 + return true;
210 + } else {
211 + const firstTimer = peek(timerQueue);
212 + if (firstTimer !== null) {
213 + requestHostTimeout(handleTimeout, firstTimer.startTime - currentTime);
214 + }
215 + return false;
216 + }
217 +}
218 +
219 +function unstable_runWithPriority(priorityLevel, eventHandler) {
220 + switch (priorityLevel) {
221 + case ImmediatePriority:
222 + case UserBlockingPriority:
223 + case NormalPriority:
224 + case LowPriority:
225 + case IdlePriority:
226 + break;
227 + default:
228 + priorityLevel = NormalPriority;
229 + }
230 +
231 + var previousPriorityLevel = currentPriorityLevel;
232 + currentPriorityLevel = priorityLevel;
233 +
234 + try {
235 + return eventHandler();
236 + } finally {
237 + currentPriorityLevel = previousPriorityLevel;
238 + }
239 +}
240 +
241 +function unstable_next(eventHandler) {
242 + var priorityLevel;
243 + switch (currentPriorityLevel) {
244 + case ImmediatePriority:
245 + case UserBlockingPriority:
246 + case NormalPriority:
247 + // Shift down to normal priority
248 + priorityLevel = NormalPriority;
249 + break;
250 + default:
251 + // Anything lower than normal priority should remain at the current level.
252 + priorityLevel = currentPriorityLevel;
253 + break;
254 + }
255 +
256 + var previousPriorityLevel = currentPriorityLevel;
257 + currentPriorityLevel = priorityLevel;
258 +
259 + try {
260 + return eventHandler();
261 + } finally {
262 + currentPriorityLevel = previousPriorityLevel;
263 + }
264 +}
265 +
266 +function unstable_wrapCallback(callback) {
267 + var parentPriorityLevel = currentPriorityLevel;
268 + return function() {
269 + // This is a fork of runWithPriority, inlined for performance.
270 + var previousPriorityLevel = currentPriorityLevel;
271 + currentPriorityLevel = parentPriorityLevel;
272 +
273 + try {
274 + return callback.apply(this, arguments);
275 + } finally {
276 + currentPriorityLevel = previousPriorityLevel;
277 + }
278 + };
279 +}
280 +
281 +function unstable_scheduleCallback(priorityLevel, callback, options) {
282 + var currentTime = getCurrentTime();
283 +
284 + var startTime;
285 + if (typeof options === 'object' && options !== null) {
286 + var delay = options.delay;
287 + if (typeof delay === 'number' && delay > 0) {
288 + startTime = currentTime + delay;
289 + } else {
290 + startTime = currentTime;
291 + }
292 + } else {
293 + startTime = currentTime;
294 + }
295 +
296 + var timeout;
297 + switch (priorityLevel) {
298 + case ImmediatePriority:
299 + timeout = IMMEDIATE_PRIORITY_TIMEOUT;
300 + break;
301 + case UserBlockingPriority:
302 + timeout = USER_BLOCKING_PRIORITY_TIMEOUT;
303 + break;
304 + case IdlePriority:
305 + timeout = IDLE_PRIORITY_TIMEOUT;
306 + break;
307 + case LowPriority:
308 + timeout = LOW_PRIORITY_TIMEOUT;
309 + break;
310 + case NormalPriority:
311 + default:
312 + timeout = NORMAL_PRIORITY_TIMEOUT;
313 + break;
314 + }
315 +
316 + var expirationTime = startTime + timeout;
317 +
318 + var newTask = {
319 + id: taskIdCounter++,
320 + callback,
321 + priorityLevel,
322 + startTime,
323 + expirationTime,
324 + sortIndex: -1,
325 + };
326 + if (enableProfiling) {
327 + newTask.isQueued = false;
328 + }
329 +
330 + if (startTime > currentTime) {
331 + // This is a delayed task.
332 + newTask.sortIndex = startTime;
333 + push(timerQueue, newTask);
334 + if (peek(taskQueue) === null && newTask === peek(timerQueue)) {
335 + // All tasks are delayed, and this is the task with the earliest delay.
336 + if (isHostTimeoutScheduled) {
337 + // Cancel an existing timeout.
338 + cancelHostTimeout();
339 + } else {
340 + isHostTimeoutScheduled = true;
341 + }
342 + // Schedule a timeout.
343 + requestHostTimeout(handleTimeout, startTime - currentTime);
344 + }
345 + } else {
346 + newTask.sortIndex = expirationTime;
347 + push(taskQueue, newTask);
348 + if (enableProfiling) {
349 + markTaskStart(newTask, currentTime);
350 + newTask.isQueued = true;
351 + }
352 + // Schedule a host callback, if needed. If we're already performing work,
353 + // wait until the next time we yield.
354 + if (!isHostCallbackScheduled && !isPerformingWork) {
355 + isHostCallbackScheduled = true;
356 + requestHostCallback(flushWork);
357 + }
358 + }
359 +
360 + return newTask;
361 +}
362 +
363 +function unstable_pauseExecution() {
364 + isSchedulerPaused = true;
365 +}
366 +
367 +function unstable_continueExecution() {
368 + isSchedulerPaused = false;
369 + if (!isHostCallbackScheduled && !isPerformingWork) {
370 + isHostCallbackScheduled = true;
371 + requestHostCallback(flushWork);
372 + }
373 +}
374 +
375 +function unstable_getFirstCallbackNode() {
376 + return peek(taskQueue);
377 +}
378 +
379 +function unstable_cancelCallback(task) {
380 + if (enableProfiling) {
381 + if (task.isQueued) {
382 + const currentTime = getCurrentTime();
383 + markTaskCanceled(task, currentTime);
384 + task.isQueued = false;
385 + }
386 + }
387 +
388 + // Null out the callback to indicate the task has been canceled. (Can't
389 + // remove from the queue because you can't remove arbitrary nodes from an
390 + // array based heap, only the first one.)
391 + task.callback = null;
392 +}
393 +
394 +function unstable_getCurrentPriorityLevel() {
395 + return currentPriorityLevel;
396 +}
397 +
398 +function requestHostCallback(callback: boolean => void) {
399 + scheduledCallback = callback;
400 +}
401 +
402 +function requestHostTimeout(callback: number => void, ms: number) {
403 + scheduledTimeout = callback;
404 + timeoutTime = currentMockTime + ms;
405 +}
406 +
407 +function cancelHostTimeout(): void {
408 + scheduledTimeout = null;
409 + timeoutTime = -1;
410 +}
411 +
412 +function shouldYieldToHost(): boolean {
413 + if (
414 + (expectedNumberOfYields !== -1 &&
415 + yieldedValues !== null &&
416 + yieldedValues.length >= expectedNumberOfYields) ||
417 + (shouldYieldForPaint && needsPaint)
418 + ) {
419 + // We yielded at least as many values as expected. Stop flushing.
420 + didStop = true;
421 + return true;
422 + }
423 + return false;
424 +}
425 +
426 +function getCurrentTime(): number {
427 + return currentMockTime;
428 +}
429 +
430 +function forceFrameRate() {
431 + // No-op
432 +}
433 +
434 +function reset() {
435 + if (isFlushing) {
436 + throw new Error('Cannot reset while already flushing work.');
437 + }
438 + currentMockTime = 0;
439 + scheduledCallback = null;
440 + scheduledTimeout = null;
441 + timeoutTime = -1;
442 + yieldedValues = null;
443 + expectedNumberOfYields = -1;
444 + didStop = false;
445 + isFlushing = false;
446 + needsPaint = false;
447 +}
448 +
449 +// Should only be used via an assertion helper that inspects the yielded values.
450 +function unstable_flushNumberOfYields(count: number): void {
451 + if (isFlushing) {
452 + throw new Error('Already flushing work.');
453 + }
454 + if (scheduledCallback !== null) {
455 + const cb = scheduledCallback;
456 + expectedNumberOfYields = count;
457 + isFlushing = true;
458 + try {
459 + let hasMoreWork = true;
460 + do {
461 + hasMoreWork = cb(true, currentMockTime);
462 + } while (hasMoreWork && !didStop);
463 + if (!hasMoreWork) {
464 + scheduledCallback = null;
465 + }
466 + } finally {
467 + expectedNumberOfYields = -1;
468 + didStop = false;
469 + isFlushing = false;
470 + }
471 + }
472 +}
473 +
474 +function unstable_flushUntilNextPaint(): void {
475 + if (isFlushing) {
476 + throw new Error('Already flushing work.');
477 + }
478 + if (scheduledCallback !== null) {
479 + const cb = scheduledCallback;
480 + shouldYieldForPaint = true;
481 + needsPaint = false;
482 + isFlushing = true;
483 + try {
484 + let hasMoreWork = true;
485 + do {
486 + hasMoreWork = cb(true, currentMockTime);
487 + } while (hasMoreWork && !didStop);
488 + if (!hasMoreWork) {
489 + scheduledCallback = null;
490 + }
491 + } finally {
492 + shouldYieldForPaint = false;
493 + didStop = false;
494 + isFlushing = false;
495 + }
496 + }
497 +}
498 +
499 +function unstable_flushExpired() {
500 + if (isFlushing) {
501 + throw new Error('Already flushing work.');
502 + }
503 + if (scheduledCallback !== null) {
504 + isFlushing = true;
505 + try {
506 + const hasMoreWork = scheduledCallback(false, currentMockTime);
507 + if (!hasMoreWork) {
508 + scheduledCallback = null;
509 + }
510 + } finally {
511 + isFlushing = false;
512 + }
513 + }
514 +}
515 +
516 +function unstable_flushAllWithoutAsserting(): boolean {
517 + // Returns false if no work was flushed.
518 + if (isFlushing) {
519 + throw new Error('Already flushing work.');
520 + }
521 + if (scheduledCallback !== null) {
522 + const cb = scheduledCallback;
523 + isFlushing = true;
524 + try {
525 + let hasMoreWork = true;
526 + do {
527 + hasMoreWork = cb(true, currentMockTime);
528 + } while (hasMoreWork);
529 + if (!hasMoreWork) {
530 + scheduledCallback = null;
531 + }
532 + return true;
533 + } finally {
534 + isFlushing = false;
535 + }
536 + } else {
537 + return false;
538 + }
539 +}
540 +
541 +function unstable_clearYields(): Array<mixed> {
542 + if (yieldedValues === null) {
543 + return [];
544 + }
545 + const values = yieldedValues;
546 + yieldedValues = null;
547 + return values;
548 +}
549 +
550 +function unstable_flushAll(): void {
551 + if (yieldedValues !== null) {
552 + throw new Error(
553 + 'Log is not empty. Assert on the log of yielded values before ' +
554 + 'flushing additional work.',
555 + );
556 + }
557 + unstable_flushAllWithoutAsserting();
558 + if (yieldedValues !== null) {
559 + throw new Error(
560 + 'While flushing work, something yielded a value. Use an ' +
561 + 'assertion helper to assert on the log of yielded values, e.g. ' +
562 + 'expect(Scheduler).toFlushAndYield([...])',
563 + );
564 + }
565 +}
566 +
567 +function unstable_yieldValue(value: mixed): void {
568 + // eslint-disable-next-line react-internal/no-production-logging
569 + if (console.log.name === 'disabledLog') {
570 + // If console.log has been patched, we assume we're in render
571 + // replaying and we ignore any values yielding in the second pass.
572 + return;
573 + }
574 + if (yieldedValues === null) {
575 + yieldedValues = [value];
576 + } else {
577 + yieldedValues.push(value);
578 + }
579 +}
580 +
581 +function unstable_advanceTime(ms: number) {
582 + // eslint-disable-next-line react-internal/no-production-logging
583 + if (console.log.name === 'disabledLog') {
584 + // If console.log has been patched, we assume we're in render
585 + // replaying and we ignore any time advancing in the second pass.
586 + return;
587 + }
588 + currentMockTime += ms;
589 + if (scheduledTimeout !== null && timeoutTime <= currentMockTime) {
590 + scheduledTimeout(currentMockTime);
591 + timeoutTime = -1;
592 + scheduledTimeout = null;
593 + }
594 +}
595 +
596 +function requestPaint() {
597 + needsPaint = true;
598 +}
599 +
600 +export {
601 + ImmediatePriority as unstable_ImmediatePriority,
602 + UserBlockingPriority as unstable_UserBlockingPriority,
603 + NormalPriority as unstable_NormalPriority,
604 + IdlePriority as unstable_IdlePriority,
605 + LowPriority as unstable_LowPriority,
606 + unstable_runWithPriority,
607 + unstable_next,
608 + unstable_scheduleCallback,
609 + unstable_cancelCallback,
610 + unstable_wrapCallback,
611 + unstable_getCurrentPriorityLevel,
612 + shouldYieldToHost as unstable_shouldYield,
613 + requestPaint as unstable_requestPaint,
614 + unstable_continueExecution,
615 + unstable_pauseExecution,
616 + unstable_getFirstCallbackNode,
617 + getCurrentTime as unstable_now,
618 + forceFrameRate as unstable_forceFrameRate,
619 + unstable_flushAllWithoutAsserting,
620 + unstable_flushNumberOfYields,
621 + unstable_flushExpired,
622 + unstable_clearYields,
623 + unstable_flushUntilNextPaint,
624 + unstable_flushAll,
625 + unstable_yieldValue,
626 + unstable_advanceTime,
627 + reset,
628 +};
629 +
630 +export const unstable_Profiling = enableProfiling
631 + ? {
632 + startLoggingProfilingEvents,
633 + stopLoggingProfilingEvents,
634 + sharedProfilingBuffer,
635 + }
636 + : null;
packages/scheduler/src/forks/SchedulerNoDOM.js renamed
+61 -15
@@ -11,17 +11,8 @@
11 import {
12 enableSchedulerDebugging,
13 enableProfiling,
14 -} from './SchedulerFeatureFlags';
15 -import {
16 - requestHostCallback,
17 - requestHostTimeout,
18 - cancelHostTimeout,
19 - shouldYieldToHost,
20 - getCurrentTime,
21 - forceFrameRate,
22 - requestPaint,
23 -} from './SchedulerHostConfig';
24 -import {push, pop, peek} from './SchedulerMinHeap';
14 +} from '../SchedulerFeatureFlags';
15 +import {push, pop, peek} from '../SchedulerMinHeap';
16
17 // TODO: Use symbols?
18 import {
@@ -30,7 +21,7 @@ import {
21 NormalPriority,
22 LowPriority,
23 IdlePriority,
33 -} from './SchedulerPriorities';
24 +} from '../SchedulerPriorities';
25 import {
26 sharedProfilingBuffer,
27 markTaskRun,
@@ -43,7 +34,7 @@ import {
34 markTaskStart,
35 stopLoggingProfilingEvents,
36 startLoggingProfilingEvents,
46 -} from './SchedulerProfiling';
37 +} from '../SchedulerProfiling';
38
39 // Max 31 bit integer. The max integer size in V8 for 32-bit systems.
40 // Math.pow(2, 30) - 1
@@ -78,6 +69,19 @@ var isPerformingWork = false;
69 var isHostCallbackScheduled = false;
70 var isHostTimeoutScheduled = false;
71
72 +let getCurrentTime;
73 +const hasPerformanceNow =
74 + typeof performance === 'object' && typeof performance.now === 'function';
75 +
76 +if (hasPerformanceNow) {
77 + const localPerformance = performance;
78 + getCurrentTime = () => localPerformance.now();
79 +} else {
80 + const localDate = Date;
81 + const initialTime = localDate.now();
82 + getCurrentTime = () => localDate.now() - initialTime;
83 +}
84 +
85 function advanceTimers(currentTime) {
86 // Check for tasks that are no longer delayed and add them to the queue.
87 let timer = peek(timerQueue);
@@ -393,7 +397,49 @@ function unstable_getCurrentPriorityLevel() {
397 return currentPriorityLevel;
398 }
399
396 -const unstable_requestPaint = requestPaint;
400 +// If this accidentally gets imported in a non-browser environment, e.g. JavaScriptCore,
401 +// fallback to a naive implementation.
402 +let _callback = null;
403 +let _timeoutID = null;
404 +const _flushCallback = function() {
405 + if (_callback !== null) {
406 + try {
407 + const currentTime = getCurrentTime();
408 + const hasRemainingTime = true;
409 + _callback(hasRemainingTime, currentTime);
410 + _callback = null;
411 + } catch (e) {
412 + setTimeout(_flushCallback, 0);
413 + throw e;
414 + }
415 + }
416 +};
417 +
418 +function requestHostCallback(cb) {
419 + if (_callback !== null) {
420 + // Protect against re-entrancy.
421 + setTimeout(requestHostCallback, 0, cb);
422 + } else {
423 + _callback = cb;
424 + setTimeout(_flushCallback, 0);
425 + }
426 +}
427 +
428 +function requestHostTimeout(cb, ms) {
429 + _timeoutID = setTimeout(cb, ms);
430 +}
431 +
432 +function cancelHostTimeout() {
433 + clearTimeout(_timeoutID);
434 +}
435 +
436 +function shouldYieldToHost() {
437 + return false;
438 +}
439 +
440 +function forceFrameRate() {}
441 +
442 +function requestPaint() {}
443
444 export {
445 ImmediatePriority as unstable_ImmediatePriority,
@@ -408,7 +454,7 @@ export {
454 unstable_wrapCallback,
455 unstable_getCurrentPriorityLevel,
456 shouldYieldToHost as unstable_shouldYield,
411 - unstable_requestPaint,
457 + requestPaint as unstable_requestPaint,
458 unstable_continueExecution,
459 unstable_pauseExecution,
460 unstable_getFirstCallbackNode,
packages/scheduler/src/forks/SchedulerPostTask.js renamed
+2 -2
@@ -7,7 +7,7 @@
7 * @flow
8 */
9
10 -import type {PriorityLevel} from './SchedulerPriorities';
10 +import type {PriorityLevel} from '../SchedulerPriorities';
11
12 declare class TaskController {
13 constructor(priority?: string): TaskController;
@@ -27,7 +27,7 @@ import {
27 NormalPriority,
28 LowPriority,
29 IdlePriority,
30 -} from './SchedulerPriorities';
30 +} from '../SchedulerPriorities';
31
32 export {
33 ImmediatePriority as unstable_ImmediatePriority,
packages/scheduler/unstable_mock.js
+1 -12
@@ -7,15 +7,4 @@
7
8 'use strict';
9
10 -export * from './src/Scheduler';
11 -
12 -export {
13 - unstable_flushAllWithoutAsserting,
14 - unstable_flushNumberOfYields,
15 - unstable_flushExpired,
16 - unstable_clearYields,
17 - unstable_flushUntilNextPaint,
18 - unstable_flushAll,
19 - unstable_yieldValue,
20 - unstable_advanceTime,
21 -} from './src/SchedulerHostConfig.js';
10 +export * from './src/forks/SchedulerMock';
packages/scheduler/unstable_no_dom.js renamed
+3 -3
@@ -3,8 +3,8 @@
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 - * @flow
6 */
7
10 -throw new Error('This module must be shimmed by a specific build.');
8 +'use strict';
9 +
10 +export * from './src/forks/SchedulerNoDOM';
packages/scheduler/unstable_post_task.js
+1 -1
@@ -7,4 +7,4 @@
7
8 'use strict';
9
10 -export * from './src/SchedulerPostTask';
10 +export * from './src/forks/SchedulerPostTask';
packages/shared/__tests__/ReactDOMFrameScheduling-test.js
+1 -5
@@ -15,11 +15,6 @@ describe('ReactDOMFrameScheduling', () => {
15
16 // Un-mock scheduler
17 jest.mock('scheduler', () => require.requireActual('scheduler'));
18 - jest.mock('scheduler/src/SchedulerHostConfig', () =>
19 - require.requireActual(
20 - 'scheduler/src/forks/SchedulerHostConfig.default.js',
21 - ),
22 - );
18 });
19
20 it('warns when requestAnimationFrame is not polyfilled in the browser', () => {
@@ -47,6 +42,7 @@ describe('ReactDOMFrameScheduling', () => {
42
43 // We're just testing importing, not using it.
44 // It is important because even isomorphic components may import it.
45 + // @gate !source
46 it('can import findDOMNode in Node environment', () => {
47 const previousRAF = global.requestAnimationFrame;
48 const previousRIC = global.requestIdleCallback;
scripts/jest/TestFlags.js
+1
@@ -74,6 +74,7 @@ function getTestFlags() {
74 channel: releaseChannel,
75 modern: releaseChannel === 'modern',
76 classic: releaseChannel === 'classic',
77 + source: !process.env.IS_BUILD,
78 www,
79
80 ...featureFlags,
scripts/jest/config.build.js
+2
@@ -4,6 +4,8 @@ const {readdirSync, statSync} = require('fs');
4 const {join} = require('path');
5 const baseConfig = require('./config.base');
6
7 +process.env.IS_BUILD = true;
8 +
9 // Find all folders in packages/* with package.json
10 const packagesRoot = join(__dirname, '..', '..', 'packages');
11 const packages = readdirSync(packagesRoot).filter(dir => {
scripts/jest/setupHostConfigs.js
-3
@@ -96,6 +96,3 @@ jest.mock('shared/ReactSharedInternals', () =>
96 );
97
98 jest.mock('scheduler', () => require.requireActual('scheduler/unstable_mock'));
99 -jest.mock('scheduler/src/SchedulerHostConfig', () =>
100 - require.requireActual('scheduler/src/forks/SchedulerHostConfig.mock.js')
101 -);
scripts/jest/setupTests.build.js
-3
@@ -1,6 +1,3 @@
1 'use strict';
2
3 jest.mock('scheduler', () => require.requireActual('scheduler/unstable_mock'));
4 -jest.mock('scheduler/src/SchedulerHostConfig', () =>
5 - require.requireActual('scheduler/src/forks/SchedulerHostConfig.mock.js')
6 -);
scripts/rollup/bundles.js
+18
@@ -633,6 +633,24 @@ const bundles = [
633 externals: [],
634 },
635
636 + /******* React Scheduler No DOM (experimental) *******/
637 + {
638 + bundleTypes: [
639 + NODE_DEV,
640 + NODE_PROD,
641 + FB_WWW_DEV,
642 + FB_WWW_PROD,
643 + FB_WWW_PROFILING,
644 + RN_FB_DEV,
645 + RN_FB_PROD,
646 + RN_FB_PROFILING,
647 + ],
648 + moduleType: ISOMORPHIC,
649 + entry: 'scheduler/unstable_no_dom',
650 + global: 'SchedulerNoDOM',
651 + externals: [],
652 + },
653 +
654 /******* Jest React (experimental) *******/
655 {
656 bundleTypes: [NODE_DEV, NODE_PROD],
scripts/rollup/forks.js
-12
@@ -201,18 +201,6 @@ const forks = Object.freeze({
201 return 'scheduler/src/SchedulerFeatureFlags';
202 },
203
204 - 'scheduler/src/SchedulerHostConfig': (bundleType, entry, dependencies) => {
205 - if (
206 - entry === 'scheduler/unstable_mock' ||
207 - entry === 'react-noop-renderer' ||
208 - entry === 'react-noop-renderer/persistent' ||
209 - entry === 'react-test-renderer'
210 - ) {
211 - return 'scheduler/src/forks/SchedulerHostConfig.mock';
212 - }
213 - return 'scheduler/src/forks/SchedulerHostConfig.default';
214 - },
215 -
204 'shared/consoleWithStackDev': (bundleType, entry) => {
205 switch (bundleType) {
206 case FB_WWW_DEV: