diff --git a/packages/scheduler/src/forks/SchedulerHostConfig.post-task.js b/packages/scheduler/src/forks/SchedulerHostConfig.post-task.js new file mode 100644 index 0000000000..b7e958911e --- /dev/null +++ b/packages/scheduler/src/forks/SchedulerHostConfig.post-task.js @@ -0,0 +1,250 @@ +/** + * 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; + +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; + } + } + }; + const initialTime = Date.now(); + getCurrentTime = function() { + return Date.now() - initialTime; + }; + 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 performance = window.performance; + const Date = window.Date; + 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; + // TODO: Remove fb.me link + 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://fb.me/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://fb.me/react-polyfills', + ); + } + } + + if ( + typeof performance === 'object' && + typeof performance.now === 'function' + ) { + getCurrentTime = () => performance.now(); + } else { + const initialTime = Date.now(); + getCurrentTime = () => Date.now() - initialTime; + } + + 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 unsupported', + ); + 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/unstable_post_task.js b/packages/scheduler/unstable_post_task.js new file mode 100644 index 0000000000..aa14495a61 --- /dev/null +++ b/packages/scheduler/unstable_post_task.js @@ -0,0 +1,10 @@ +/** + * 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. + */ + +'use strict'; + +export * from './src/Scheduler'; diff --git a/scripts/rollup/bundles.js b/scripts/rollup/bundles.js index b255a1039b..4323001b61 100644 --- a/scripts/rollup/bundles.js +++ b/scripts/rollup/bundles.js @@ -634,6 +634,14 @@ const bundles = [ externals: [], }, + { + bundleTypes: [FB_WWW_DEV, FB_WWW_PROD, FB_WWW_PROFILING], + moduleType: ISOMORPHIC, + entry: 'scheduler/unstable_post_task', + global: 'SchedulerPostTask', + externals: [], + }, + /******* Jest React (experimental) *******/ { bundleTypes: [NODE_DEV, NODE_PROD], diff --git a/scripts/rollup/forks.js b/scripts/rollup/forks.js index 80f1b724aa..5f2744e65b 100644 --- a/scripts/rollup/forks.js +++ b/scripts/rollup/forks.js @@ -198,6 +198,8 @@ const forks = Object.freeze({ entry === 'react-test-renderer' ) { return 'scheduler/src/forks/SchedulerHostConfig.mock'; + } else if (entry === 'scheduler/unstable_post_task') { + return 'scheduler/src/forks/SchedulerHostConfig.post-task'; } return 'scheduler/src/forks/SchedulerHostConfig.default'; },