diff --git a/packages/react-dom/src/__tests__/ReactTestUtilsActUnmockedScheduler-test.js b/packages/react-dom/src/__tests__/ReactTestUtilsActUnmockedScheduler-test.js index f7145f657c72d..6f1587e7042e1 100644 --- a/packages/react-dom/src/__tests__/ReactTestUtilsActUnmockedScheduler-test.js +++ b/packages/react-dom/src/__tests__/ReactTestUtilsActUnmockedScheduler-test.js @@ -33,7 +33,9 @@ function unmount(dom) { beforeEach(() => { jest.resetModules(); - jest.unmock('scheduler'); + jest.mock('scheduler', () => + require.requireActual('scheduler/unstable_no_dom'), + ); yields = []; React = require('react'); ReactDOM = require('react-dom'); diff --git a/packages/react-dom/src/__tests__/ReactUnmockedSchedulerWarning-test.internal.js b/packages/react-dom/src/__tests__/ReactUnmockedSchedulerWarning-test.internal.js index 501b01336b1b3..6c443a832aa1f 100644 --- a/packages/react-dom/src/__tests__/ReactUnmockedSchedulerWarning-test.internal.js +++ b/packages/react-dom/src/__tests__/ReactUnmockedSchedulerWarning-test.internal.js @@ -17,7 +17,7 @@ function App() { beforeEach(() => { jest.resetModules(); - jest.unmock('scheduler'); + jest.mock('scheduler', () => require('scheduler/unstable_no_dom')); React = require('react'); ReactDOM = require('react-dom'); ReactFeatureFlags = require('shared/ReactFeatureFlags'); diff --git a/packages/react-dom/src/__tests__/ReactUnmockedSchedulerWarning-test.js b/packages/react-dom/src/__tests__/ReactUnmockedSchedulerWarning-test.js index 57fc7fdcdc403..435b4989c1157 100644 --- a/packages/react-dom/src/__tests__/ReactUnmockedSchedulerWarning-test.js +++ b/packages/react-dom/src/__tests__/ReactUnmockedSchedulerWarning-test.js @@ -16,7 +16,9 @@ function App() { beforeEach(() => { jest.resetModules(); - jest.unmock('scheduler'); + jest.mock('scheduler', () => + require.requireActual('scheduler/unstable_no_dom'), + ); React = require('react'); ReactDOM = require('react-dom'); }); diff --git a/packages/scheduler/index.js b/packages/scheduler/index.js index aa14495a61abf..a5e913d1dd26b 100644 --- a/packages/scheduler/index.js +++ b/packages/scheduler/index.js @@ -7,4 +7,4 @@ 'use strict'; -export * from './src/Scheduler'; +export * from './src/forks/SchedulerDOM'; diff --git a/packages/scheduler/npm/index.js b/packages/scheduler/npm/index.js index 77770b0c219e2..68a443aa1de8b 100644 --- a/packages/scheduler/npm/index.js +++ b/packages/scheduler/npm/index.js @@ -1,6 +1,8 @@ 'use strict'; -if (process.env.NODE_ENV === 'production') { +if (typeof window === 'undefined' || typeof MessageChannel !== 'function') { + module.exports = require('./unstable_no_dom'); +} else if (process.env.NODE_ENV === 'production') { module.exports = require('./cjs/scheduler.production.min.js'); } else { module.exports = require('./cjs/scheduler.development.js'); diff --git a/packages/scheduler/npm/unstable_no_dom.js b/packages/scheduler/npm/unstable_no_dom.js new file mode 100644 index 0000000000000..2ba763765a0c3 --- /dev/null +++ b/packages/scheduler/npm/unstable_no_dom.js @@ -0,0 +1,7 @@ +'use strict'; + +if (process.env.NODE_ENV === 'production') { + module.exports = require('./cjs/scheduler-unstable_no_dom.production.min.js'); +} else { + module.exports = require('./cjs/scheduler-unstable_no_dom.development.js'); +} diff --git a/packages/scheduler/package.json b/packages/scheduler/package.json index cea8e16c2e87e..04daf3e6a3133 100644 --- a/packages/scheduler/package.json +++ b/packages/scheduler/package.json @@ -28,6 +28,7 @@ "tracing.js", "tracing-profiling.js", "unstable_mock.js", + "unstable_no_dom.js", "unstable_post_task.js", "cjs/", "umd/" diff --git a/packages/scheduler/src/__tests__/SchedulerBrowser-test.js b/packages/scheduler/src/__tests__/SchedulerDOM-test.js similarity index 97% rename from packages/scheduler/src/__tests__/SchedulerBrowser-test.js rename to packages/scheduler/src/__tests__/SchedulerDOM-test.js index 50a9927ae3c6b..f6dd4be5db18d 100644 --- a/packages/scheduler/src/__tests__/SchedulerBrowser-test.js +++ b/packages/scheduler/src/__tests__/SchedulerDOM-test.js @@ -35,11 +35,6 @@ describe('SchedulerBrowser', () => { // Un-mock scheduler jest.mock('scheduler', () => require.requireActual('scheduler')); - jest.mock('scheduler/src/SchedulerHostConfig', () => - require.requireActual( - 'scheduler/src/forks/SchedulerHostConfig.default.js', - ), - ); runtime = installMockBrowserRuntime(); performance = global.performance; diff --git a/packages/scheduler/src/__tests__/Scheduler-test.js b/packages/scheduler/src/__tests__/SchedulerMock-test.js similarity index 100% rename from packages/scheduler/src/__tests__/Scheduler-test.js rename to packages/scheduler/src/__tests__/SchedulerMock-test.js diff --git a/packages/scheduler/src/__tests__/SchedulerNoDOM-test.js b/packages/scheduler/src/__tests__/SchedulerNoDOM-test.js index 2cabdfa298593..8d3920fd920f9 100644 --- a/packages/scheduler/src/__tests__/SchedulerNoDOM-test.js +++ b/packages/scheduler/src/__tests__/SchedulerNoDOM-test.js @@ -24,11 +24,8 @@ describe('SchedulerNoDOM', () => { jest.useFakeTimers(); // Un-mock scheduler - jest.mock('scheduler', () => require.requireActual('scheduler')); - jest.mock('scheduler/src/SchedulerHostConfig', () => - require.requireActual( - 'scheduler/src/forks/SchedulerHostConfig.default.js', - ), + jest.mock('scheduler', () => + require.requireActual('scheduler/unstable_no_dom'), ); Scheduler = require('scheduler'); diff --git a/packages/scheduler/src/__tests__/SchedulerUMDBundle-test.internal.js b/packages/scheduler/src/__tests__/SchedulerUMDBundle-test.internal.js index 219bbc69bec88..d6dfeb2081e4a 100644 --- a/packages/scheduler/src/__tests__/SchedulerUMDBundle-test.internal.js +++ b/packages/scheduler/src/__tests__/SchedulerUMDBundle-test.internal.js @@ -8,6 +8,13 @@ */ 'use strict'; +class MockMessageChannel { + constructor() { + this.port1 = jest.fn(); + this.port2 = jest.fn(); + } +} + describe('Scheduling UMD bundle', () => { beforeEach(() => { // Fool SECRET_INTERNALS object into including UMD forwarding methods. @@ -16,11 +23,18 @@ describe('Scheduling UMD bundle', () => { jest.resetModules(); jest.mock('scheduler', () => require.requireActual('scheduler')); - jest.mock('scheduler/src/SchedulerHostConfig', () => - require.requireActual( - 'scheduler/src/forks/SchedulerHostConfig.default.js', - ), - ); + + // Mock a browser environment since we're testing UMD modules. + global.window = { + requestAnimationFrame: jest.fn(), + cancelAnimationFrame: jest.fn(), + }; + global.MessageChannel = MockMessageChannel; + }); + + afterEach(() => { + global.window = undefined; + global.MessageChannel = undefined; }); function filterPrivateKeys(name) { diff --git a/packages/scheduler/src/forks/SchedulerDOM.js b/packages/scheduler/src/forks/SchedulerDOM.js new file mode 100644 index 0000000000000..53101a6e6e580 --- /dev/null +++ b/packages/scheduler/src/forks/SchedulerDOM.js @@ -0,0 +1,598 @@ +/** + * Copyright (c) Facebook, Inc. and its affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + */ + +/* eslint-disable no-var */ + +import { + enableSchedulerDebugging, + enableProfiling, +} from '../SchedulerFeatureFlags'; + +import {push, pop, peek} from '../SchedulerMinHeap'; + +// TODO: Use symbols? +import { + ImmediatePriority, + UserBlockingPriority, + NormalPriority, + LowPriority, + IdlePriority, +} from '../SchedulerPriorities'; +import { + sharedProfilingBuffer, + markTaskRun, + markTaskYield, + markTaskCompleted, + markTaskCanceled, + markTaskErrored, + markSchedulerSuspended, + markSchedulerUnsuspended, + markTaskStart, + stopLoggingProfilingEvents, + startLoggingProfilingEvents, +} from '../SchedulerProfiling'; + +import {enableIsInputPending} from '../SchedulerFeatureFlags'; + +let getCurrentTime; +const hasPerformanceNow = + typeof performance === 'object' && typeof performance.now === 'function'; + +if (hasPerformanceNow) { + const localPerformance = performance; + getCurrentTime = () => localPerformance.now(); +} else { + const localDate = Date; + const initialTime = localDate.now(); + getCurrentTime = () => localDate.now() - initialTime; +} + +// Max 31 bit integer. The max integer size in V8 for 32-bit systems. +// Math.pow(2, 30) - 1 +// 0b111111111111111111111111111111 +var maxSigned31BitInt = 1073741823; + +// Times out immediately +var IMMEDIATE_PRIORITY_TIMEOUT = -1; +// Eventually times out +var USER_BLOCKING_PRIORITY_TIMEOUT = 250; +var NORMAL_PRIORITY_TIMEOUT = 5000; +var LOW_PRIORITY_TIMEOUT = 10000; +// Never times out +var IDLE_PRIORITY_TIMEOUT = maxSigned31BitInt; + +// Tasks are stored on a min heap +var taskQueue = []; +var timerQueue = []; + +// Incrementing id counter. Used to maintain insertion order. +var taskIdCounter = 1; + +// Pausing the scheduler is useful for debugging. +var isSchedulerPaused = false; + +var currentTask = null; +var currentPriorityLevel = NormalPriority; + +// This is set while performing work, to prevent re-entrancy. +var isPerformingWork = false; + +var isHostCallbackScheduled = false; +var isHostTimeoutScheduled = false; + +// Capture local references to native APIs, in case a polyfill overrides them. +const setTimeout = window.setTimeout; +const clearTimeout = window.clearTimeout; + +if (typeof console !== 'undefined') { + // TODO: Scheduler no longer requires these methods to be polyfilled. But + // maybe we want to continue warning if they don't exist, to preserve the + // option to rely on it in the future? + const requestAnimationFrame = window.requestAnimationFrame; + const cancelAnimationFrame = window.cancelAnimationFrame; + + if (typeof requestAnimationFrame !== 'function') { + // Using console['error'] to evade Babel and ESLint + console['error']( + "This browser doesn't support requestAnimationFrame. " + + 'Make sure that you load a ' + + 'polyfill in older browsers. https://reactjs.org/link/react-polyfills', + ); + } + if (typeof cancelAnimationFrame !== 'function') { + // Using console['error'] to evade Babel and ESLint + console['error']( + "This browser doesn't support cancelAnimationFrame. " + + 'Make sure that you load a ' + + 'polyfill in older browsers. https://reactjs.org/link/react-polyfills', + ); + } +} + +function advanceTimers(currentTime) { + // Check for tasks that are no longer delayed and add them to the queue. + let timer = peek(timerQueue); + while (timer !== null) { + if (timer.callback === null) { + // Timer was cancelled. + pop(timerQueue); + } else if (timer.startTime <= currentTime) { + // Timer fired. Transfer to the task queue. + pop(timerQueue); + timer.sortIndex = timer.expirationTime; + push(taskQueue, timer); + if (enableProfiling) { + markTaskStart(timer, currentTime); + timer.isQueued = true; + } + } else { + // Remaining timers are pending. + return; + } + timer = peek(timerQueue); + } +} + +function handleTimeout(currentTime) { + isHostTimeoutScheduled = false; + advanceTimers(currentTime); + + if (!isHostCallbackScheduled) { + if (peek(taskQueue) !== null) { + isHostCallbackScheduled = true; + requestHostCallback(flushWork); + } else { + const firstTimer = peek(timerQueue); + if (firstTimer !== null) { + requestHostTimeout(handleTimeout, firstTimer.startTime - currentTime); + } + } + } +} + +function flushWork(hasTimeRemaining, initialTime) { + if (enableProfiling) { + markSchedulerUnsuspended(initialTime); + } + + // We'll need a host callback the next time work is scheduled. + isHostCallbackScheduled = false; + if (isHostTimeoutScheduled) { + // We scheduled a timeout but it's no longer needed. Cancel it. + isHostTimeoutScheduled = false; + cancelHostTimeout(); + } + + isPerformingWork = true; + const previousPriorityLevel = currentPriorityLevel; + try { + if (enableProfiling) { + try { + return workLoop(hasTimeRemaining, initialTime); + } catch (error) { + if (currentTask !== null) { + const currentTime = getCurrentTime(); + markTaskErrored(currentTask, currentTime); + currentTask.isQueued = false; + } + throw error; + } + } else { + // No catch in prod code path. + return workLoop(hasTimeRemaining, initialTime); + } + } finally { + currentTask = null; + currentPriorityLevel = previousPriorityLevel; + isPerformingWork = false; + if (enableProfiling) { + const currentTime = getCurrentTime(); + markSchedulerSuspended(currentTime); + } + } +} + +function workLoop(hasTimeRemaining, initialTime) { + let currentTime = initialTime; + advanceTimers(currentTime); + currentTask = peek(taskQueue); + while ( + currentTask !== null && + !(enableSchedulerDebugging && isSchedulerPaused) + ) { + if ( + currentTask.expirationTime > currentTime && + (!hasTimeRemaining || shouldYieldToHost()) + ) { + // This currentTask hasn't expired, and we've reached the deadline. + break; + } + const callback = currentTask.callback; + if (typeof callback === 'function') { + currentTask.callback = null; + currentPriorityLevel = currentTask.priorityLevel; + const didUserCallbackTimeout = currentTask.expirationTime <= currentTime; + markTaskRun(currentTask, currentTime); + const continuationCallback = callback(didUserCallbackTimeout); + currentTime = getCurrentTime(); + if (typeof continuationCallback === 'function') { + currentTask.callback = continuationCallback; + markTaskYield(currentTask, currentTime); + } else { + if (enableProfiling) { + markTaskCompleted(currentTask, currentTime); + currentTask.isQueued = false; + } + if (currentTask === peek(taskQueue)) { + pop(taskQueue); + } + } + advanceTimers(currentTime); + } else { + pop(taskQueue); + } + currentTask = peek(taskQueue); + } + // Return whether there's additional work + if (currentTask !== null) { + return true; + } else { + const firstTimer = peek(timerQueue); + if (firstTimer !== null) { + requestHostTimeout(handleTimeout, firstTimer.startTime - currentTime); + } + return false; + } +} + +function unstable_runWithPriority(priorityLevel, eventHandler) { + switch (priorityLevel) { + case ImmediatePriority: + case UserBlockingPriority: + case NormalPriority: + case LowPriority: + case IdlePriority: + break; + default: + priorityLevel = NormalPriority; + } + + var previousPriorityLevel = currentPriorityLevel; + currentPriorityLevel = priorityLevel; + + try { + return eventHandler(); + } finally { + currentPriorityLevel = previousPriorityLevel; + } +} + +function unstable_next(eventHandler) { + var priorityLevel; + switch (currentPriorityLevel) { + case ImmediatePriority: + case UserBlockingPriority: + case NormalPriority: + // Shift down to normal priority + priorityLevel = NormalPriority; + break; + default: + // Anything lower than normal priority should remain at the current level. + priorityLevel = currentPriorityLevel; + break; + } + + var previousPriorityLevel = currentPriorityLevel; + currentPriorityLevel = priorityLevel; + + try { + return eventHandler(); + } finally { + currentPriorityLevel = previousPriorityLevel; + } +} + +function unstable_wrapCallback(callback) { + var parentPriorityLevel = currentPriorityLevel; + return function() { + // This is a fork of runWithPriority, inlined for performance. + var previousPriorityLevel = currentPriorityLevel; + currentPriorityLevel = parentPriorityLevel; + + try { + return callback.apply(this, arguments); + } finally { + currentPriorityLevel = previousPriorityLevel; + } + }; +} + +function unstable_scheduleCallback(priorityLevel, callback, options) { + var currentTime = getCurrentTime(); + + var startTime; + if (typeof options === 'object' && options !== null) { + var delay = options.delay; + if (typeof delay === 'number' && delay > 0) { + startTime = currentTime + delay; + } else { + startTime = currentTime; + } + } else { + startTime = currentTime; + } + + var timeout; + switch (priorityLevel) { + case ImmediatePriority: + timeout = IMMEDIATE_PRIORITY_TIMEOUT; + break; + case UserBlockingPriority: + timeout = USER_BLOCKING_PRIORITY_TIMEOUT; + break; + case IdlePriority: + timeout = IDLE_PRIORITY_TIMEOUT; + break; + case LowPriority: + timeout = LOW_PRIORITY_TIMEOUT; + break; + case NormalPriority: + default: + timeout = NORMAL_PRIORITY_TIMEOUT; + break; + } + + var expirationTime = startTime + timeout; + + var newTask = { + id: taskIdCounter++, + callback, + priorityLevel, + startTime, + expirationTime, + sortIndex: -1, + }; + if (enableProfiling) { + newTask.isQueued = false; + } + + if (startTime > currentTime) { + // This is a delayed task. + newTask.sortIndex = startTime; + push(timerQueue, newTask); + if (peek(taskQueue) === null && newTask === peek(timerQueue)) { + // All tasks are delayed, and this is the task with the earliest delay. + if (isHostTimeoutScheduled) { + // Cancel an existing timeout. + cancelHostTimeout(); + } else { + isHostTimeoutScheduled = true; + } + // Schedule a timeout. + requestHostTimeout(handleTimeout, startTime - currentTime); + } + } else { + newTask.sortIndex = expirationTime; + push(taskQueue, newTask); + if (enableProfiling) { + markTaskStart(newTask, currentTime); + newTask.isQueued = true; + } + // Schedule a host callback, if needed. If we're already performing work, + // wait until the next time we yield. + if (!isHostCallbackScheduled && !isPerformingWork) { + isHostCallbackScheduled = true; + requestHostCallback(flushWork); + } + } + + return newTask; +} + +function unstable_pauseExecution() { + isSchedulerPaused = true; +} + +function unstable_continueExecution() { + isSchedulerPaused = false; + if (!isHostCallbackScheduled && !isPerformingWork) { + isHostCallbackScheduled = true; + requestHostCallback(flushWork); + } +} + +function unstable_getFirstCallbackNode() { + return peek(taskQueue); +} + +function unstable_cancelCallback(task) { + if (enableProfiling) { + if (task.isQueued) { + const currentTime = getCurrentTime(); + markTaskCanceled(task, currentTime); + task.isQueued = false; + } + } + + // Null out the callback to indicate the task has been canceled. (Can't + // remove from the queue because you can't remove arbitrary nodes from an + // array based heap, only the first one.) + task.callback = null; +} + +function unstable_getCurrentPriorityLevel() { + return currentPriorityLevel; +} + +let isMessageLoopRunning = false; +let scheduledHostCallback = null; +let taskTimeoutID = -1; + +// Scheduler periodically yields in case there is other work on the main +// thread, like user events. By default, it yields multiple times per frame. +// It does not attempt to align with frame boundaries, since most tasks don't +// need to be frame aligned; for those that do, use requestAnimationFrame. +let yieldInterval = 5; +let deadline = 0; + +// TODO: Make this configurable +// TODO: Adjust this based on priority? +const maxYieldInterval = 300; +let needsPaint = false; + +function shouldYieldToHost() { + if ( + enableIsInputPending && + navigator !== undefined && + navigator.scheduling !== undefined && + navigator.scheduling.isInputPending !== undefined + ) { + const scheduling = navigator.scheduling; + const currentTime = getCurrentTime(); + if (currentTime >= deadline) { + // There's no time left. We may want to yield control of the main + // thread, so the browser can perform high priority tasks. The main ones + // are painting and user input. If there's a pending paint or a pending + // input, then we should yield. But if there's neither, then we can + // yield less often while remaining responsive. We'll eventually yield + // regardless, since there could be a pending paint that wasn't + // accompanied by a call to `requestPaint`, or other main thread tasks + // like network events. + if (needsPaint || scheduling.isInputPending()) { + // There is either a pending paint or a pending input. + return true; + } + // There's no pending input. Only yield if we've reached the max + // yield interval. + return currentTime >= maxYieldInterval; + } else { + // There's still time left in the frame. + return false; + } + } else { + // `isInputPending` is not available. Since we have no way of knowing if + // there's pending input, always yield at the end of the frame. + return getCurrentTime() >= deadline; + } +} + +function requestPaint() { + if ( + enableIsInputPending && + navigator !== undefined && + navigator.scheduling !== undefined && + navigator.scheduling.isInputPending !== undefined + ) { + needsPaint = true; + } + + // Since we yield every frame regardless, `requestPaint` has no effect. +} + +function forceFrameRate(fps) { + if (fps < 0 || fps > 125) { + // Using console['error'] to evade Babel and ESLint + console['error']( + 'forceFrameRate takes a positive int between 0 and 125, ' + + 'forcing frame rates higher than 125 fps is not supported', + ); + return; + } + if (fps > 0) { + yieldInterval = Math.floor(1000 / fps); + } else { + // reset the framerate + yieldInterval = 5; + } +} + +const performWorkUntilDeadline = () => { + if (scheduledHostCallback !== null) { + const currentTime = getCurrentTime(); + // Yield after `yieldInterval` ms, regardless of where we are in the vsync + // cycle. This means there's always time remaining at the beginning of + // the message event. + deadline = currentTime + yieldInterval; + const hasTimeRemaining = true; + try { + const hasMoreWork = scheduledHostCallback(hasTimeRemaining, currentTime); + if (!hasMoreWork) { + isMessageLoopRunning = false; + scheduledHostCallback = null; + } else { + // If there's more work, schedule the next message event at the end + // of the preceding one. + port.postMessage(null); + } + } catch (error) { + // If a scheduler task throws, exit the current browser task so the + // error can be observed. + port.postMessage(null); + throw error; + } + } else { + isMessageLoopRunning = false; + } + // Yielding to the browser will give it a chance to paint, so we can + // reset this. + needsPaint = false; +}; + +const channel = new MessageChannel(); +const port = channel.port2; +channel.port1.onmessage = performWorkUntilDeadline; + +function requestHostCallback(callback) { + scheduledHostCallback = callback; + if (!isMessageLoopRunning) { + isMessageLoopRunning = true; + port.postMessage(null); + } +} + +function requestHostTimeout(callback, ms) { + taskTimeoutID = setTimeout(() => { + callback(getCurrentTime()); + }, ms); +} + +function cancelHostTimeout() { + clearTimeout(taskTimeoutID); + taskTimeoutID = -1; +} + +const unstable_requestPaint = requestPaint; + +export { + ImmediatePriority as unstable_ImmediatePriority, + UserBlockingPriority as unstable_UserBlockingPriority, + NormalPriority as unstable_NormalPriority, + IdlePriority as unstable_IdlePriority, + LowPriority as unstable_LowPriority, + unstable_runWithPriority, + unstable_next, + unstable_scheduleCallback, + unstable_cancelCallback, + unstable_wrapCallback, + unstable_getCurrentPriorityLevel, + shouldYieldToHost as unstable_shouldYield, + unstable_requestPaint, + unstable_continueExecution, + unstable_pauseExecution, + unstable_getFirstCallbackNode, + getCurrentTime as unstable_now, + forceFrameRate as unstable_forceFrameRate, +}; + +export const unstable_Profiling = enableProfiling + ? { + startLoggingProfilingEvents, + stopLoggingProfilingEvents, + sharedProfilingBuffer, + } + : null; diff --git a/packages/scheduler/src/forks/SchedulerFeatureFlags.www.js b/packages/scheduler/src/forks/SchedulerFeatureFlags.www.js index 9fd86c7f94b2e..ae94a0fa8c34d 100644 --- a/packages/scheduler/src/forks/SchedulerFeatureFlags.www.js +++ b/packages/scheduler/src/forks/SchedulerFeatureFlags.www.js @@ -10,6 +10,6 @@ export const { enableIsInputPending, enableSchedulerDebugging, enableProfiling: enableProfilingFeatureFlag, -} = require('SchedulerFeatureFlags'); +} = require('packages/scheduler/src/SchedulerFeatureFlags'); export const enableProfiling = __PROFILE__ && enableProfilingFeatureFlag; diff --git a/packages/scheduler/src/forks/SchedulerHostConfig.default.js b/packages/scheduler/src/forks/SchedulerHostConfig.default.js deleted file mode 100644 index 825cf3d22834c..0000000000000 --- a/packages/scheduler/src/forks/SchedulerHostConfig.default.js +++ /dev/null @@ -1,246 +0,0 @@ -/** - * Copyright (c) Facebook, Inc. and its affiliates. - * - * This source code is licensed under the MIT license found in the - * LICENSE file in the root directory of this source tree. - */ - -import {enableIsInputPending} from '../SchedulerFeatureFlags'; - -export let requestHostCallback; -export let cancelHostCallback; -export let requestHostTimeout; -export let cancelHostTimeout; -export let shouldYieldToHost; -export let requestPaint; -export let getCurrentTime; -export let forceFrameRate; - -const hasPerformanceNow = - typeof performance === 'object' && typeof performance.now === 'function'; - -if (hasPerformanceNow) { - const localPerformance = performance; - getCurrentTime = () => localPerformance.now(); -} else { - const localDate = Date; - const initialTime = localDate.now(); - getCurrentTime = () => localDate.now() - initialTime; -} - -if ( - // If Scheduler runs in a non-DOM environment, it falls back to a naive - // implementation using setTimeout. - typeof window === 'undefined' || - // Check if MessageChannel is supported, too. - typeof MessageChannel !== 'function' -) { - // If this accidentally gets imported in a non-browser environment, e.g. JavaScriptCore, - // fallback to a naive implementation. - let _callback = null; - let _timeoutID = null; - const _flushCallback = function() { - if (_callback !== null) { - try { - const currentTime = getCurrentTime(); - const hasRemainingTime = true; - _callback(hasRemainingTime, currentTime); - _callback = null; - } catch (e) { - setTimeout(_flushCallback, 0); - throw e; - } - } - }; - requestHostCallback = function(cb) { - if (_callback !== null) { - // Protect against re-entrancy. - setTimeout(requestHostCallback, 0, cb); - } else { - _callback = cb; - setTimeout(_flushCallback, 0); - } - }; - cancelHostCallback = function() { - _callback = null; - }; - requestHostTimeout = function(cb, ms) { - _timeoutID = setTimeout(cb, ms); - }; - cancelHostTimeout = function() { - clearTimeout(_timeoutID); - }; - shouldYieldToHost = function() { - return false; - }; - requestPaint = forceFrameRate = function() {}; -} else { - // Capture local references to native APIs, in case a polyfill overrides them. - const setTimeout = window.setTimeout; - const clearTimeout = window.clearTimeout; - - if (typeof console !== 'undefined') { - // TODO: Scheduler no longer requires these methods to be polyfilled. But - // maybe we want to continue warning if they don't exist, to preserve the - // option to rely on it in the future? - const requestAnimationFrame = window.requestAnimationFrame; - const cancelAnimationFrame = window.cancelAnimationFrame; - - if (typeof requestAnimationFrame !== 'function') { - // Using console['error'] to evade Babel and ESLint - console['error']( - "This browser doesn't support requestAnimationFrame. " + - 'Make sure that you load a ' + - 'polyfill in older browsers. https://reactjs.org/link/react-polyfills', - ); - } - if (typeof cancelAnimationFrame !== 'function') { - // Using console['error'] to evade Babel and ESLint - console['error']( - "This browser doesn't support cancelAnimationFrame. " + - 'Make sure that you load a ' + - 'polyfill in older browsers. https://reactjs.org/link/react-polyfills', - ); - } - } - - let isMessageLoopRunning = false; - let scheduledHostCallback = null; - let taskTimeoutID = -1; - - // Scheduler periodically yields in case there is other work on the main - // thread, like user events. By default, it yields multiple times per frame. - // It does not attempt to align with frame boundaries, since most tasks don't - // need to be frame aligned; for those that do, use requestAnimationFrame. - let yieldInterval = 5; - let deadline = 0; - - // TODO: Make this configurable - // TODO: Adjust this based on priority? - const maxYieldInterval = 300; - let needsPaint = false; - - if ( - enableIsInputPending && - navigator !== undefined && - navigator.scheduling !== undefined && - navigator.scheduling.isInputPending !== undefined - ) { - const scheduling = navigator.scheduling; - shouldYieldToHost = function() { - const currentTime = getCurrentTime(); - if (currentTime >= deadline) { - // There's no time left. We may want to yield control of the main - // thread, so the browser can perform high priority tasks. The main ones - // are painting and user input. If there's a pending paint or a pending - // input, then we should yield. But if there's neither, then we can - // yield less often while remaining responsive. We'll eventually yield - // regardless, since there could be a pending paint that wasn't - // accompanied by a call to `requestPaint`, or other main thread tasks - // like network events. - if (needsPaint || scheduling.isInputPending()) { - // There is either a pending paint or a pending input. - return true; - } - // There's no pending input. Only yield if we've reached the max - // yield interval. - return currentTime >= maxYieldInterval; - } else { - // There's still time left in the frame. - return false; - } - }; - - requestPaint = function() { - needsPaint = true; - }; - } else { - // `isInputPending` is not available. Since we have no way of knowing if - // there's pending input, always yield at the end of the frame. - shouldYieldToHost = function() { - return getCurrentTime() >= deadline; - }; - - // Since we yield every frame regardless, `requestPaint` has no effect. - requestPaint = function() {}; - } - - forceFrameRate = function(fps) { - if (fps < 0 || fps > 125) { - // Using console['error'] to evade Babel and ESLint - console['error']( - 'forceFrameRate takes a positive int between 0 and 125, ' + - 'forcing frame rates higher than 125 fps is not supported', - ); - return; - } - if (fps > 0) { - yieldInterval = Math.floor(1000 / fps); - } else { - // reset the framerate - yieldInterval = 5; - } - }; - - const performWorkUntilDeadline = () => { - if (scheduledHostCallback !== null) { - const currentTime = getCurrentTime(); - // Yield after `yieldInterval` ms, regardless of where we are in the vsync - // cycle. This means there's always time remaining at the beginning of - // the message event. - deadline = currentTime + yieldInterval; - const hasTimeRemaining = true; - try { - const hasMoreWork = scheduledHostCallback( - hasTimeRemaining, - currentTime, - ); - if (!hasMoreWork) { - isMessageLoopRunning = false; - scheduledHostCallback = null; - } else { - // If there's more work, schedule the next message event at the end - // of the preceding one. - port.postMessage(null); - } - } catch (error) { - // If a scheduler task throws, exit the current browser task so the - // error can be observed. - port.postMessage(null); - throw error; - } - } else { - isMessageLoopRunning = false; - } - // Yielding to the browser will give it a chance to paint, so we can - // reset this. - needsPaint = false; - }; - - const channel = new MessageChannel(); - const port = channel.port2; - channel.port1.onmessage = performWorkUntilDeadline; - - requestHostCallback = function(callback) { - scheduledHostCallback = callback; - if (!isMessageLoopRunning) { - isMessageLoopRunning = true; - port.postMessage(null); - } - }; - - cancelHostCallback = function() { - scheduledHostCallback = null; - }; - - requestHostTimeout = function(callback, ms) { - taskTimeoutID = setTimeout(() => { - callback(getCurrentTime()); - }, ms); - }; - - cancelHostTimeout = function() { - clearTimeout(taskTimeoutID); - taskTimeoutID = -1; - }; -} diff --git a/packages/scheduler/src/forks/SchedulerHostConfig.mock.js b/packages/scheduler/src/forks/SchedulerHostConfig.mock.js deleted file mode 100644 index 070b31043a42c..0000000000000 --- a/packages/scheduler/src/forks/SchedulerHostConfig.mock.js +++ /dev/null @@ -1,225 +0,0 @@ -/** - * Copyright (c) Facebook, Inc. and its affiliates. - * - * This source code is licensed under the MIT license found in the - * LICENSE file in the root directory of this source tree. - * - * @flow - */ - -let currentTime: number = 0; -let scheduledCallback: ((boolean, number) => void) | null = null; -let scheduledTimeout: (number => void) | null = null; -let timeoutTime: number = -1; -let yieldedValues: Array | null = null; -let expectedNumberOfYields: number = -1; -let didStop: boolean = false; -let isFlushing: boolean = false; -let needsPaint: boolean = false; -let shouldYieldForPaint: boolean = false; - -export function requestHostCallback(callback: boolean => void) { - scheduledCallback = callback; -} - -export function cancelHostCallback(): void { - scheduledCallback = null; -} - -export function requestHostTimeout(callback: number => void, ms: number) { - scheduledTimeout = callback; - timeoutTime = currentTime + ms; -} - -export function cancelHostTimeout(): void { - scheduledTimeout = null; - timeoutTime = -1; -} - -export function shouldYieldToHost(): boolean { - if ( - (expectedNumberOfYields !== -1 && - yieldedValues !== null && - yieldedValues.length >= expectedNumberOfYields) || - (shouldYieldForPaint && needsPaint) - ) { - // We yielded at least as many values as expected. Stop flushing. - didStop = true; - return true; - } - return false; -} - -export function getCurrentTime(): number { - return currentTime; -} - -export function forceFrameRate() { - // No-op -} - -export function reset() { - if (isFlushing) { - throw new Error('Cannot reset while already flushing work.'); - } - currentTime = 0; - scheduledCallback = null; - scheduledTimeout = null; - timeoutTime = -1; - yieldedValues = null; - expectedNumberOfYields = -1; - didStop = false; - isFlushing = false; - needsPaint = false; -} - -// Should only be used via an assertion helper that inspects the yielded values. -export function unstable_flushNumberOfYields(count: number): void { - if (isFlushing) { - throw new Error('Already flushing work.'); - } - if (scheduledCallback !== null) { - const cb = scheduledCallback; - expectedNumberOfYields = count; - isFlushing = true; - try { - let hasMoreWork = true; - do { - hasMoreWork = cb(true, currentTime); - } while (hasMoreWork && !didStop); - if (!hasMoreWork) { - scheduledCallback = null; - } - } finally { - expectedNumberOfYields = -1; - didStop = false; - isFlushing = false; - } - } -} - -export function unstable_flushUntilNextPaint(): void { - if (isFlushing) { - throw new Error('Already flushing work.'); - } - if (scheduledCallback !== null) { - const cb = scheduledCallback; - shouldYieldForPaint = true; - needsPaint = false; - isFlushing = true; - try { - let hasMoreWork = true; - do { - hasMoreWork = cb(true, currentTime); - } while (hasMoreWork && !didStop); - if (!hasMoreWork) { - scheduledCallback = null; - } - } finally { - shouldYieldForPaint = false; - didStop = false; - isFlushing = false; - } - } -} - -export function unstable_flushExpired() { - if (isFlushing) { - throw new Error('Already flushing work.'); - } - if (scheduledCallback !== null) { - isFlushing = true; - try { - const hasMoreWork = scheduledCallback(false, currentTime); - if (!hasMoreWork) { - scheduledCallback = null; - } - } finally { - isFlushing = false; - } - } -} - -export function unstable_flushAllWithoutAsserting(): boolean { - // Returns false if no work was flushed. - if (isFlushing) { - throw new Error('Already flushing work.'); - } - if (scheduledCallback !== null) { - const cb = scheduledCallback; - isFlushing = true; - try { - let hasMoreWork = true; - do { - hasMoreWork = cb(true, currentTime); - } while (hasMoreWork); - if (!hasMoreWork) { - scheduledCallback = null; - } - return true; - } finally { - isFlushing = false; - } - } else { - return false; - } -} - -export function unstable_clearYields(): Array { - if (yieldedValues === null) { - return []; - } - const values = yieldedValues; - yieldedValues = null; - return values; -} - -export function unstable_flushAll(): void { - if (yieldedValues !== null) { - throw new Error( - 'Log is not empty. Assert on the log of yielded values before ' + - 'flushing additional work.', - ); - } - unstable_flushAllWithoutAsserting(); - if (yieldedValues !== null) { - throw new Error( - 'While flushing work, something yielded a value. Use an ' + - 'assertion helper to assert on the log of yielded values, e.g. ' + - 'expect(Scheduler).toFlushAndYield([...])', - ); - } -} - -export function unstable_yieldValue(value: mixed): void { - // eslint-disable-next-line react-internal/no-production-logging - if (console.log.name === 'disabledLog') { - // If console.log has been patched, we assume we're in render - // replaying and we ignore any values yielding in the second pass. - return; - } - if (yieldedValues === null) { - yieldedValues = [value]; - } else { - yieldedValues.push(value); - } -} - -export function unstable_advanceTime(ms: number) { - // eslint-disable-next-line react-internal/no-production-logging - if (console.log.name === 'disabledLog') { - // If console.log has been patched, we assume we're in render - // replaying and we ignore any time advancing in the second pass. - return; - } - currentTime += ms; - if (scheduledTimeout !== null && timeoutTime <= currentTime) { - scheduledTimeout(currentTime); - timeoutTime = -1; - scheduledTimeout = null; - } -} - -export function requestPaint() { - needsPaint = true; -} diff --git a/packages/scheduler/src/forks/SchedulerMock.js b/packages/scheduler/src/forks/SchedulerMock.js new file mode 100644 index 0000000000000..9ec5504b243f4 --- /dev/null +++ b/packages/scheduler/src/forks/SchedulerMock.js @@ -0,0 +1,636 @@ +/** + * Copyright (c) Facebook, Inc. and its affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + */ + +/* eslint-disable no-var */ + +import { + enableSchedulerDebugging, + enableProfiling, +} from '../SchedulerFeatureFlags'; +import {push, pop, peek} from '../SchedulerMinHeap'; + +// TODO: Use symbols? +import { + ImmediatePriority, + UserBlockingPriority, + NormalPriority, + LowPriority, + IdlePriority, +} from '../SchedulerPriorities'; +import { + sharedProfilingBuffer, + markTaskRun, + markTaskYield, + markTaskCompleted, + markTaskCanceled, + markTaskErrored, + markSchedulerSuspended, + markSchedulerUnsuspended, + markTaskStart, + stopLoggingProfilingEvents, + startLoggingProfilingEvents, +} from '../SchedulerProfiling'; + +// Max 31 bit integer. The max integer size in V8 for 32-bit systems. +// Math.pow(2, 30) - 1 +// 0b111111111111111111111111111111 +var maxSigned31BitInt = 1073741823; + +// Times out immediately +var IMMEDIATE_PRIORITY_TIMEOUT = -1; +// Eventually times out +var USER_BLOCKING_PRIORITY_TIMEOUT = 250; +var NORMAL_PRIORITY_TIMEOUT = 5000; +var LOW_PRIORITY_TIMEOUT = 10000; +// Never times out +var IDLE_PRIORITY_TIMEOUT = maxSigned31BitInt; + +// Tasks are stored on a min heap +var taskQueue = []; +var timerQueue = []; + +// Incrementing id counter. Used to maintain insertion order. +var taskIdCounter = 1; + +// Pausing the scheduler is useful for debugging. +var isSchedulerPaused = false; + +var currentTask = null; +var currentPriorityLevel = NormalPriority; + +// This is set while performing work, to prevent re-entrancy. +var isPerformingWork = false; + +var isHostCallbackScheduled = false; +var isHostTimeoutScheduled = false; + +let currentMockTime: number = 0; +let scheduledCallback: ((boolean, number) => void) | null = null; +let scheduledTimeout: (number => void) | null = null; +let timeoutTime: number = -1; +let yieldedValues: Array | null = null; +let expectedNumberOfYields: number = -1; +let didStop: boolean = false; +let isFlushing: boolean = false; +let needsPaint: boolean = false; +let shouldYieldForPaint: boolean = false; + +function advanceTimers(currentTime) { + // Check for tasks that are no longer delayed and add them to the queue. + let timer = peek(timerQueue); + while (timer !== null) { + if (timer.callback === null) { + // Timer was cancelled. + pop(timerQueue); + } else if (timer.startTime <= currentTime) { + // Timer fired. Transfer to the task queue. + pop(timerQueue); + timer.sortIndex = timer.expirationTime; + push(taskQueue, timer); + if (enableProfiling) { + markTaskStart(timer, currentTime); + timer.isQueued = true; + } + } else { + // Remaining timers are pending. + return; + } + timer = peek(timerQueue); + } +} + +function handleTimeout(currentTime) { + isHostTimeoutScheduled = false; + advanceTimers(currentTime); + + if (!isHostCallbackScheduled) { + if (peek(taskQueue) !== null) { + isHostCallbackScheduled = true; + requestHostCallback(flushWork); + } else { + const firstTimer = peek(timerQueue); + if (firstTimer !== null) { + requestHostTimeout(handleTimeout, firstTimer.startTime - currentTime); + } + } + } +} + +function flushWork(hasTimeRemaining, initialTime) { + if (enableProfiling) { + markSchedulerUnsuspended(initialTime); + } + + // We'll need a host callback the next time work is scheduled. + isHostCallbackScheduled = false; + if (isHostTimeoutScheduled) { + // We scheduled a timeout but it's no longer needed. Cancel it. + isHostTimeoutScheduled = false; + cancelHostTimeout(); + } + + isPerformingWork = true; + const previousPriorityLevel = currentPriorityLevel; + try { + if (enableProfiling) { + try { + return workLoop(hasTimeRemaining, initialTime); + } catch (error) { + if (currentTask !== null) { + const currentTime = getCurrentTime(); + markTaskErrored(currentTask, currentTime); + currentTask.isQueued = false; + } + throw error; + } + } else { + // No catch in prod code path. + return workLoop(hasTimeRemaining, initialTime); + } + } finally { + currentTask = null; + currentPriorityLevel = previousPriorityLevel; + isPerformingWork = false; + if (enableProfiling) { + const currentTime = getCurrentTime(); + markSchedulerSuspended(currentTime); + } + } +} + +function workLoop(hasTimeRemaining, initialTime) { + let currentTime = initialTime; + advanceTimers(currentTime); + currentTask = peek(taskQueue); + while ( + currentTask !== null && + !(enableSchedulerDebugging && isSchedulerPaused) + ) { + if ( + currentTask.expirationTime > currentTime && + (!hasTimeRemaining || shouldYieldToHost()) + ) { + // This currentTask hasn't expired, and we've reached the deadline. + break; + } + const callback = currentTask.callback; + if (typeof callback === 'function') { + currentTask.callback = null; + currentPriorityLevel = currentTask.priorityLevel; + const didUserCallbackTimeout = currentTask.expirationTime <= currentTime; + markTaskRun(currentTask, currentTime); + const continuationCallback = callback(didUserCallbackTimeout); + currentTime = getCurrentTime(); + if (typeof continuationCallback === 'function') { + currentTask.callback = continuationCallback; + markTaskYield(currentTask, currentTime); + } else { + if (enableProfiling) { + markTaskCompleted(currentTask, currentTime); + currentTask.isQueued = false; + } + if (currentTask === peek(taskQueue)) { + pop(taskQueue); + } + } + advanceTimers(currentTime); + } else { + pop(taskQueue); + } + currentTask = peek(taskQueue); + } + // Return whether there's additional work + if (currentTask !== null) { + return true; + } else { + const firstTimer = peek(timerQueue); + if (firstTimer !== null) { + requestHostTimeout(handleTimeout, firstTimer.startTime - currentTime); + } + return false; + } +} + +function unstable_runWithPriority(priorityLevel, eventHandler) { + switch (priorityLevel) { + case ImmediatePriority: + case UserBlockingPriority: + case NormalPriority: + case LowPriority: + case IdlePriority: + break; + default: + priorityLevel = NormalPriority; + } + + var previousPriorityLevel = currentPriorityLevel; + currentPriorityLevel = priorityLevel; + + try { + return eventHandler(); + } finally { + currentPriorityLevel = previousPriorityLevel; + } +} + +function unstable_next(eventHandler) { + var priorityLevel; + switch (currentPriorityLevel) { + case ImmediatePriority: + case UserBlockingPriority: + case NormalPriority: + // Shift down to normal priority + priorityLevel = NormalPriority; + break; + default: + // Anything lower than normal priority should remain at the current level. + priorityLevel = currentPriorityLevel; + break; + } + + var previousPriorityLevel = currentPriorityLevel; + currentPriorityLevel = priorityLevel; + + try { + return eventHandler(); + } finally { + currentPriorityLevel = previousPriorityLevel; + } +} + +function unstable_wrapCallback(callback) { + var parentPriorityLevel = currentPriorityLevel; + return function() { + // This is a fork of runWithPriority, inlined for performance. + var previousPriorityLevel = currentPriorityLevel; + currentPriorityLevel = parentPriorityLevel; + + try { + return callback.apply(this, arguments); + } finally { + currentPriorityLevel = previousPriorityLevel; + } + }; +} + +function unstable_scheduleCallback(priorityLevel, callback, options) { + var currentTime = getCurrentTime(); + + var startTime; + if (typeof options === 'object' && options !== null) { + var delay = options.delay; + if (typeof delay === 'number' && delay > 0) { + startTime = currentTime + delay; + } else { + startTime = currentTime; + } + } else { + startTime = currentTime; + } + + var timeout; + switch (priorityLevel) { + case ImmediatePriority: + timeout = IMMEDIATE_PRIORITY_TIMEOUT; + break; + case UserBlockingPriority: + timeout = USER_BLOCKING_PRIORITY_TIMEOUT; + break; + case IdlePriority: + timeout = IDLE_PRIORITY_TIMEOUT; + break; + case LowPriority: + timeout = LOW_PRIORITY_TIMEOUT; + break; + case NormalPriority: + default: + timeout = NORMAL_PRIORITY_TIMEOUT; + break; + } + + var expirationTime = startTime + timeout; + + var newTask = { + id: taskIdCounter++, + callback, + priorityLevel, + startTime, + expirationTime, + sortIndex: -1, + }; + if (enableProfiling) { + newTask.isQueued = false; + } + + if (startTime > currentTime) { + // This is a delayed task. + newTask.sortIndex = startTime; + push(timerQueue, newTask); + if (peek(taskQueue) === null && newTask === peek(timerQueue)) { + // All tasks are delayed, and this is the task with the earliest delay. + if (isHostTimeoutScheduled) { + // Cancel an existing timeout. + cancelHostTimeout(); + } else { + isHostTimeoutScheduled = true; + } + // Schedule a timeout. + requestHostTimeout(handleTimeout, startTime - currentTime); + } + } else { + newTask.sortIndex = expirationTime; + push(taskQueue, newTask); + if (enableProfiling) { + markTaskStart(newTask, currentTime); + newTask.isQueued = true; + } + // Schedule a host callback, if needed. If we're already performing work, + // wait until the next time we yield. + if (!isHostCallbackScheduled && !isPerformingWork) { + isHostCallbackScheduled = true; + requestHostCallback(flushWork); + } + } + + return newTask; +} + +function unstable_pauseExecution() { + isSchedulerPaused = true; +} + +function unstable_continueExecution() { + isSchedulerPaused = false; + if (!isHostCallbackScheduled && !isPerformingWork) { + isHostCallbackScheduled = true; + requestHostCallback(flushWork); + } +} + +function unstable_getFirstCallbackNode() { + return peek(taskQueue); +} + +function unstable_cancelCallback(task) { + if (enableProfiling) { + if (task.isQueued) { + const currentTime = getCurrentTime(); + markTaskCanceled(task, currentTime); + task.isQueued = false; + } + } + + // Null out the callback to indicate the task has been canceled. (Can't + // remove from the queue because you can't remove arbitrary nodes from an + // array based heap, only the first one.) + task.callback = null; +} + +function unstable_getCurrentPriorityLevel() { + return currentPriorityLevel; +} + +function requestHostCallback(callback: boolean => void) { + scheduledCallback = callback; +} + +function requestHostTimeout(callback: number => void, ms: number) { + scheduledTimeout = callback; + timeoutTime = currentMockTime + ms; +} + +function cancelHostTimeout(): void { + scheduledTimeout = null; + timeoutTime = -1; +} + +function shouldYieldToHost(): boolean { + if ( + (expectedNumberOfYields !== -1 && + yieldedValues !== null && + yieldedValues.length >= expectedNumberOfYields) || + (shouldYieldForPaint && needsPaint) + ) { + // We yielded at least as many values as expected. Stop flushing. + didStop = true; + return true; + } + return false; +} + +function getCurrentTime(): number { + return currentMockTime; +} + +function forceFrameRate() { + // No-op +} + +function reset() { + if (isFlushing) { + throw new Error('Cannot reset while already flushing work.'); + } + currentMockTime = 0; + scheduledCallback = null; + scheduledTimeout = null; + timeoutTime = -1; + yieldedValues = null; + expectedNumberOfYields = -1; + didStop = false; + isFlushing = false; + needsPaint = false; +} + +// Should only be used via an assertion helper that inspects the yielded values. +function unstable_flushNumberOfYields(count: number): void { + if (isFlushing) { + throw new Error('Already flushing work.'); + } + if (scheduledCallback !== null) { + const cb = scheduledCallback; + expectedNumberOfYields = count; + isFlushing = true; + try { + let hasMoreWork = true; + do { + hasMoreWork = cb(true, currentMockTime); + } while (hasMoreWork && !didStop); + if (!hasMoreWork) { + scheduledCallback = null; + } + } finally { + expectedNumberOfYields = -1; + didStop = false; + isFlushing = false; + } + } +} + +function unstable_flushUntilNextPaint(): void { + if (isFlushing) { + throw new Error('Already flushing work.'); + } + if (scheduledCallback !== null) { + const cb = scheduledCallback; + shouldYieldForPaint = true; + needsPaint = false; + isFlushing = true; + try { + let hasMoreWork = true; + do { + hasMoreWork = cb(true, currentMockTime); + } while (hasMoreWork && !didStop); + if (!hasMoreWork) { + scheduledCallback = null; + } + } finally { + shouldYieldForPaint = false; + didStop = false; + isFlushing = false; + } + } +} + +function unstable_flushExpired() { + if (isFlushing) { + throw new Error('Already flushing work.'); + } + if (scheduledCallback !== null) { + isFlushing = true; + try { + const hasMoreWork = scheduledCallback(false, currentMockTime); + if (!hasMoreWork) { + scheduledCallback = null; + } + } finally { + isFlushing = false; + } + } +} + +function unstable_flushAllWithoutAsserting(): boolean { + // Returns false if no work was flushed. + if (isFlushing) { + throw new Error('Already flushing work.'); + } + if (scheduledCallback !== null) { + const cb = scheduledCallback; + isFlushing = true; + try { + let hasMoreWork = true; + do { + hasMoreWork = cb(true, currentMockTime); + } while (hasMoreWork); + if (!hasMoreWork) { + scheduledCallback = null; + } + return true; + } finally { + isFlushing = false; + } + } else { + return false; + } +} + +function unstable_clearYields(): Array { + if (yieldedValues === null) { + return []; + } + const values = yieldedValues; + yieldedValues = null; + return values; +} + +function unstable_flushAll(): void { + if (yieldedValues !== null) { + throw new Error( + 'Log is not empty. Assert on the log of yielded values before ' + + 'flushing additional work.', + ); + } + unstable_flushAllWithoutAsserting(); + if (yieldedValues !== null) { + throw new Error( + 'While flushing work, something yielded a value. Use an ' + + 'assertion helper to assert on the log of yielded values, e.g. ' + + 'expect(Scheduler).toFlushAndYield([...])', + ); + } +} + +function unstable_yieldValue(value: mixed): void { + // eslint-disable-next-line react-internal/no-production-logging + if (console.log.name === 'disabledLog') { + // If console.log has been patched, we assume we're in render + // replaying and we ignore any values yielding in the second pass. + return; + } + if (yieldedValues === null) { + yieldedValues = [value]; + } else { + yieldedValues.push(value); + } +} + +function unstable_advanceTime(ms: number) { + // eslint-disable-next-line react-internal/no-production-logging + if (console.log.name === 'disabledLog') { + // If console.log has been patched, we assume we're in render + // replaying and we ignore any time advancing in the second pass. + return; + } + currentMockTime += ms; + if (scheduledTimeout !== null && timeoutTime <= currentMockTime) { + scheduledTimeout(currentMockTime); + timeoutTime = -1; + scheduledTimeout = null; + } +} + +function requestPaint() { + needsPaint = true; +} + +export { + ImmediatePriority as unstable_ImmediatePriority, + UserBlockingPriority as unstable_UserBlockingPriority, + NormalPriority as unstable_NormalPriority, + IdlePriority as unstable_IdlePriority, + LowPriority as unstable_LowPriority, + unstable_runWithPriority, + unstable_next, + unstable_scheduleCallback, + unstable_cancelCallback, + unstable_wrapCallback, + unstable_getCurrentPriorityLevel, + shouldYieldToHost as unstable_shouldYield, + requestPaint as unstable_requestPaint, + unstable_continueExecution, + unstable_pauseExecution, + unstable_getFirstCallbackNode, + getCurrentTime as unstable_now, + forceFrameRate as unstable_forceFrameRate, + unstable_flushAllWithoutAsserting, + unstable_flushNumberOfYields, + unstable_flushExpired, + unstable_clearYields, + unstable_flushUntilNextPaint, + unstable_flushAll, + unstable_yieldValue, + unstable_advanceTime, + reset, +}; + +export const unstable_Profiling = enableProfiling + ? { + startLoggingProfilingEvents, + stopLoggingProfilingEvents, + sharedProfilingBuffer, + } + : null; diff --git a/packages/scheduler/src/Scheduler.js b/packages/scheduler/src/forks/SchedulerNoDOM.js similarity index 87% rename from packages/scheduler/src/Scheduler.js rename to packages/scheduler/src/forks/SchedulerNoDOM.js index 9162da0a0875c..e1a4a5e946d35 100644 --- a/packages/scheduler/src/Scheduler.js +++ b/packages/scheduler/src/forks/SchedulerNoDOM.js @@ -11,17 +11,8 @@ import { enableSchedulerDebugging, enableProfiling, -} from './SchedulerFeatureFlags'; -import { - requestHostCallback, - requestHostTimeout, - cancelHostTimeout, - shouldYieldToHost, - getCurrentTime, - forceFrameRate, - requestPaint, -} from './SchedulerHostConfig'; -import {push, pop, peek} from './SchedulerMinHeap'; +} from '../SchedulerFeatureFlags'; +import {push, pop, peek} from '../SchedulerMinHeap'; // TODO: Use symbols? import { @@ -30,7 +21,7 @@ import { NormalPriority, LowPriority, IdlePriority, -} from './SchedulerPriorities'; +} from '../SchedulerPriorities'; import { sharedProfilingBuffer, markTaskRun, @@ -43,7 +34,7 @@ import { markTaskStart, stopLoggingProfilingEvents, startLoggingProfilingEvents, -} from './SchedulerProfiling'; +} from '../SchedulerProfiling'; // Max 31 bit integer. The max integer size in V8 for 32-bit systems. // Math.pow(2, 30) - 1 @@ -78,6 +69,19 @@ var isPerformingWork = false; var isHostCallbackScheduled = false; var isHostTimeoutScheduled = false; +let getCurrentTime; +const hasPerformanceNow = + typeof performance === 'object' && typeof performance.now === 'function'; + +if (hasPerformanceNow) { + const localPerformance = performance; + getCurrentTime = () => localPerformance.now(); +} else { + const localDate = Date; + const initialTime = localDate.now(); + getCurrentTime = () => localDate.now() - initialTime; +} + function advanceTimers(currentTime) { // Check for tasks that are no longer delayed and add them to the queue. let timer = peek(timerQueue); @@ -393,7 +397,49 @@ function unstable_getCurrentPriorityLevel() { return currentPriorityLevel; } -const unstable_requestPaint = requestPaint; +// If this accidentally gets imported in a non-browser environment, e.g. JavaScriptCore, +// fallback to a naive implementation. +let _callback = null; +let _timeoutID = null; +const _flushCallback = function() { + if (_callback !== null) { + try { + const currentTime = getCurrentTime(); + const hasRemainingTime = true; + _callback(hasRemainingTime, currentTime); + _callback = null; + } catch (e) { + setTimeout(_flushCallback, 0); + throw e; + } + } +}; + +function requestHostCallback(cb) { + if (_callback !== null) { + // Protect against re-entrancy. + setTimeout(requestHostCallback, 0, cb); + } else { + _callback = cb; + setTimeout(_flushCallback, 0); + } +} + +function requestHostTimeout(cb, ms) { + _timeoutID = setTimeout(cb, ms); +} + +function cancelHostTimeout() { + clearTimeout(_timeoutID); +} + +function shouldYieldToHost() { + return false; +} + +function forceFrameRate() {} + +function requestPaint() {} export { ImmediatePriority as unstable_ImmediatePriority, @@ -408,7 +454,7 @@ export { unstable_wrapCallback, unstable_getCurrentPriorityLevel, shouldYieldToHost as unstable_shouldYield, - unstable_requestPaint, + requestPaint as unstable_requestPaint, unstable_continueExecution, unstable_pauseExecution, unstable_getFirstCallbackNode, diff --git a/packages/scheduler/src/SchedulerPostTask.js b/packages/scheduler/src/forks/SchedulerPostTask.js similarity index 98% rename from packages/scheduler/src/SchedulerPostTask.js rename to packages/scheduler/src/forks/SchedulerPostTask.js index 5d986d5127cb0..c07f7f03819c3 100644 --- a/packages/scheduler/src/SchedulerPostTask.js +++ b/packages/scheduler/src/forks/SchedulerPostTask.js @@ -7,7 +7,7 @@ * @flow */ -import type {PriorityLevel} from './SchedulerPriorities'; +import type {PriorityLevel} from '../SchedulerPriorities'; declare class TaskController { constructor(priority?: string): TaskController; @@ -27,7 +27,7 @@ import { NormalPriority, LowPriority, IdlePriority, -} from './SchedulerPriorities'; +} from '../SchedulerPriorities'; export { ImmediatePriority as unstable_ImmediatePriority, diff --git a/packages/scheduler/unstable_mock.js b/packages/scheduler/unstable_mock.js index 11afa5abd11ce..474fe7e4f1da2 100644 --- a/packages/scheduler/unstable_mock.js +++ b/packages/scheduler/unstable_mock.js @@ -7,15 +7,4 @@ 'use strict'; -export * from './src/Scheduler'; - -export { - unstable_flushAllWithoutAsserting, - unstable_flushNumberOfYields, - unstable_flushExpired, - unstable_clearYields, - unstable_flushUntilNextPaint, - unstable_flushAll, - unstable_yieldValue, - unstable_advanceTime, -} from './src/SchedulerHostConfig.js'; +export * from './src/forks/SchedulerMock'; diff --git a/packages/scheduler/src/SchedulerHostConfig.js b/packages/scheduler/unstable_no_dom.js similarity index 70% rename from packages/scheduler/src/SchedulerHostConfig.js rename to packages/scheduler/unstable_no_dom.js index a4af848702581..bf235f9b15146 100644 --- a/packages/scheduler/src/SchedulerHostConfig.js +++ b/packages/scheduler/unstable_no_dom.js @@ -3,8 +3,8 @@ * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. - * - * @flow */ -throw new Error('This module must be shimmed by a specific build.'); +'use strict'; + +export * from './src/forks/SchedulerNoDOM'; diff --git a/packages/scheduler/unstable_post_task.js b/packages/scheduler/unstable_post_task.js index 666eff8a85898..9954099f4b69c 100644 --- a/packages/scheduler/unstable_post_task.js +++ b/packages/scheduler/unstable_post_task.js @@ -7,4 +7,4 @@ 'use strict'; -export * from './src/SchedulerPostTask'; +export * from './src/forks/SchedulerPostTask'; diff --git a/packages/shared/__tests__/ReactDOMFrameScheduling-test.js b/packages/shared/__tests__/ReactDOMFrameScheduling-test.js index fa3d145e6cb2f..3d6407ebe3846 100644 --- a/packages/shared/__tests__/ReactDOMFrameScheduling-test.js +++ b/packages/shared/__tests__/ReactDOMFrameScheduling-test.js @@ -15,11 +15,6 @@ describe('ReactDOMFrameScheduling', () => { // Un-mock scheduler jest.mock('scheduler', () => require.requireActual('scheduler')); - jest.mock('scheduler/src/SchedulerHostConfig', () => - require.requireActual( - 'scheduler/src/forks/SchedulerHostConfig.default.js', - ), - ); }); it('warns when requestAnimationFrame is not polyfilled in the browser', () => { @@ -47,6 +42,7 @@ describe('ReactDOMFrameScheduling', () => { // We're just testing importing, not using it. // It is important because even isomorphic components may import it. + // @gate !source it('can import findDOMNode in Node environment', () => { const previousRAF = global.requestAnimationFrame; const previousRIC = global.requestIdleCallback; diff --git a/scripts/jest/TestFlags.js b/scripts/jest/TestFlags.js index 790d5fbcc1aea..6a2be742e2960 100644 --- a/scripts/jest/TestFlags.js +++ b/scripts/jest/TestFlags.js @@ -74,6 +74,7 @@ function getTestFlags() { channel: releaseChannel, modern: releaseChannel === 'modern', classic: releaseChannel === 'classic', + source: !process.env.IS_BUILD, www, ...featureFlags, diff --git a/scripts/jest/config.build.js b/scripts/jest/config.build.js index 765909b41b164..fe83740fa4fe7 100644 --- a/scripts/jest/config.build.js +++ b/scripts/jest/config.build.js @@ -4,6 +4,8 @@ const {readdirSync, statSync} = require('fs'); const {join} = require('path'); const baseConfig = require('./config.base'); +process.env.IS_BUILD = true; + // Find all folders in packages/* with package.json const packagesRoot = join(__dirname, '..', '..', 'packages'); const packages = readdirSync(packagesRoot).filter(dir => { diff --git a/scripts/jest/setupHostConfigs.js b/scripts/jest/setupHostConfigs.js index 78b4de4164170..4f9cbf5d479d8 100644 --- a/scripts/jest/setupHostConfigs.js +++ b/scripts/jest/setupHostConfigs.js @@ -96,6 +96,3 @@ jest.mock('shared/ReactSharedInternals', () => ); jest.mock('scheduler', () => require.requireActual('scheduler/unstable_mock')); -jest.mock('scheduler/src/SchedulerHostConfig', () => - require.requireActual('scheduler/src/forks/SchedulerHostConfig.mock.js') -); diff --git a/scripts/jest/setupTests.build.js b/scripts/jest/setupTests.build.js index 62db0fc007f95..f3d4be6c01eec 100644 --- a/scripts/jest/setupTests.build.js +++ b/scripts/jest/setupTests.build.js @@ -1,6 +1,3 @@ 'use strict'; jest.mock('scheduler', () => require.requireActual('scheduler/unstable_mock')); -jest.mock('scheduler/src/SchedulerHostConfig', () => - require.requireActual('scheduler/src/forks/SchedulerHostConfig.mock.js') -); diff --git a/scripts/rollup/bundles.js b/scripts/rollup/bundles.js index 45fe488b7ced6..56d59127a35c2 100644 --- a/scripts/rollup/bundles.js +++ b/scripts/rollup/bundles.js @@ -633,6 +633,24 @@ const bundles = [ externals: [], }, + /******* React Scheduler No DOM (experimental) *******/ + { + bundleTypes: [ + NODE_DEV, + NODE_PROD, + FB_WWW_DEV, + FB_WWW_PROD, + FB_WWW_PROFILING, + RN_FB_DEV, + RN_FB_PROD, + RN_FB_PROFILING, + ], + moduleType: ISOMORPHIC, + entry: 'scheduler/unstable_no_dom', + global: 'SchedulerNoDOM', + externals: [], + }, + /******* Jest React (experimental) *******/ { bundleTypes: [NODE_DEV, NODE_PROD], diff --git a/scripts/rollup/forks.js b/scripts/rollup/forks.js index e34daab32c6d3..03437e1f31034 100644 --- a/scripts/rollup/forks.js +++ b/scripts/rollup/forks.js @@ -201,18 +201,6 @@ const forks = Object.freeze({ return 'scheduler/src/SchedulerFeatureFlags'; }, - 'scheduler/src/SchedulerHostConfig': (bundleType, entry, dependencies) => { - if ( - entry === 'scheduler/unstable_mock' || - entry === 'react-noop-renderer' || - entry === 'react-noop-renderer/persistent' || - entry === 'react-test-renderer' - ) { - return 'scheduler/src/forks/SchedulerHostConfig.mock'; - } - return 'scheduler/src/forks/SchedulerHostConfig.default'; - }, - 'shared/consoleWithStackDev': (bundleType, entry) => { switch (bundleType) { case FB_WWW_DEV: