diff --git a/packages/scheduler/src/SchedulerFeatureFlags.js b/packages/scheduler/src/SchedulerFeatureFlags.js index c0efc0ab3625e..848c299dd6df4 100644 --- a/packages/scheduler/src/SchedulerFeatureFlags.js +++ b/packages/scheduler/src/SchedulerFeatureFlags.js @@ -8,5 +8,4 @@ export const enableSchedulerDebugging = false; export const enableIsInputPending = false; -export const enableMessageLoopImplementation = true; export const enableProfiling = __PROFILE__; diff --git a/packages/scheduler/src/__tests__/SchedulerBrowser-test.internal.js b/packages/scheduler/src/__tests__/SchedulerBrowser-test.internal.js deleted file mode 100644 index f7f56f31f7aaf..0000000000000 --- a/packages/scheduler/src/__tests__/SchedulerBrowser-test.internal.js +++ /dev/null @@ -1,493 +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. - * - * @emails react-core - * @jest-environment node - */ - -/* eslint-disable no-for-of-loops/no-for-of-loops */ - -'use strict'; - -let Scheduler; -let runtime; -let performance; -let cancelCallback; -let scheduleCallback; -let NormalPriority; - -// The Scheduler implementation uses browser APIs like `MessageChannel`, -// `requestAnimationFrame`, and `setTimeout` to schedule work on the main -// thread. Most of our tests treat these as implementation details; however, the -// sequence and timing of these APIs are not precisely specified, and can vary -// wildly across browsers. -// -// To prevent regressions, we need the ability to simulate specific edge cases -// that we may encounter in various browsers. -// -// This test suite mocks all browser methods used in our implementation. It -// assumes as little as possible about the order and timing of events. The only -// thing it assumes is that requestAnimationFrame is passed a frame time that is -// equal to or less than the time returned by performance.now. Everything else -// can be controlled at will. -// -// It also includes Scheduler-specific invariants, e.g. only one rAF callback -// can be scheduled at a time. -describe('SchedulerBrowser', () => { - function beforeAndAfterHooks(enableMessageLoopImplementation) { - beforeEach(() => { - jest.resetModules(); - - // 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 = window.performance; - require('scheduler/src/SchedulerFeatureFlags').enableMessageLoopImplementation = enableMessageLoopImplementation; - Scheduler = require('scheduler'); - cancelCallback = Scheduler.unstable_cancelCallback; - scheduleCallback = Scheduler.unstable_scheduleCallback; - NormalPriority = Scheduler.unstable_NormalPriority; - }); - - afterEach(() => { - if (!runtime.isLogEmpty()) { - throw Error('Test exited without clearing log.'); - } - }); - } - - function installMockBrowserRuntime() { - let VSYNC_INTERVAL = 33.33; - - let hasPendingMessageEvent = false; - - let rAFCallbackIDCounter = 0; - let rAFCallback = null; - let isRunningRAFCallback = false; - - let rICCallbackIDCounter = 0; - let rICCallback = null; - - let timerIDCounter = 0; - // let timerIDs = new Map(); - - let eventLog = []; - - const window = {}; - global.window = window; - - let currentTime = 0; - - window.performance = { - now() { - return currentTime; - }, - }; - window.requestAnimationFrame = cb => { - if (rAFCallback !== null) { - throw Error('rAF already scheduled'); - } - if (isRunningRAFCallback) { - log('Request Animation Frame [Reposted]'); - } else { - log('Request Animation Frame'); - } - rAFCallback = cb; - return rAFCallbackIDCounter++; - }; - window.cancelAnimationFrame = id => { - rAFCallback = null; - }; - - window.requestIdleCallback = cb => { - if (rICCallback !== null) { - throw Error('rAF already scheduled'); - } - log('Request Idle Callback'); - rICCallback = cb; - return rICCallbackIDCounter++; - }; - window.cancelIdleCallback = id => { - rICCallback = null; - }; - - window.setTimeout = (cb, delay) => { - const id = timerIDCounter++; - log(`Set Timer`); - // TODO - return id; - }; - window.clearTimeout = id => { - // TODO - }; - - const port1 = {}; - const port2 = { - postMessage() { - if (hasPendingMessageEvent) { - throw Error('Message event already scheduled'); - } - log('Post Message'); - hasPendingMessageEvent = true; - }, - }; - global.MessageChannel = function MessageChannel() { - this.port1 = port1; - this.port2 = port2; - }; - - function ensureLogIsEmpty() { - if (eventLog.length !== 0) { - throw Error('Log is not empty. Call assertLog before continuing.'); - } - } - function setHardwareFrameRate(fps) { - VSYNC_INTERVAL = 1000 / fps; - } - function advanceTime(ms) { - currentTime += ms; - } - function advanceTimeToNextFrame() { - const targetTime = - Math.ceil(currentTime / VSYNC_INTERVAL) * VSYNC_INTERVAL; - if (targetTime === currentTime) { - currentTime += VSYNC_INTERVAL; - } else { - currentTime = targetTime; - } - } - function getMostRecentFrameNumber() { - return Math.floor(currentTime / VSYNC_INTERVAL); - } - function fireMessageEvent() { - ensureLogIsEmpty(); - if (!hasPendingMessageEvent) { - throw Error('No message event was scheduled'); - } - hasPendingMessageEvent = false; - const onMessage = port1.onmessage; - log('Message Event'); - onMessage(); - } - function fireAnimationFrame() { - ensureLogIsEmpty(); - if (isRunningRAFCallback) { - throw Error('Cannot fire animation frame from inside another event.'); - } - if (rAFCallback === null) { - throw Error('No rAF scheduled.'); - } - const mostRecentFrameNumber = getMostRecentFrameNumber(); - const rAFTime = mostRecentFrameNumber * VSYNC_INTERVAL; - const cb = rAFCallback; - rAFCallback = null; - log(`Animation Frame [${mostRecentFrameNumber}]`); - isRunningRAFCallback = true; - try { - cb(rAFTime); - } finally { - isRunningRAFCallback = false; - } - } - function fireRIC() { - ensureLogIsEmpty(); - if (rICCallback === null) { - throw Error('No rIC scheduled.'); - } - const cb = rICCallback; - rICCallback = null; - log('Idle Callback'); - cb(); - } - function log(val) { - eventLog.push(val); - } - function isLogEmpty() { - return eventLog.length === 0; - } - function assertLog(expected) { - const actual = eventLog; - eventLog = []; - expect(actual).toEqual(expected); - } - return { - setHardwareFrameRate, - advanceTime, - advanceTimeToNextFrame, - getMostRecentFrameNumber, - fireMessageEvent, - fireAnimationFrame, - fireRIC, - log, - isLogEmpty, - assertLog, - }; - } - - describe('rAF aligned frame boundaries', () => { - const enableMessageLoopImplementation = false; - beforeAndAfterHooks(enableMessageLoopImplementation); - - it('callback with continuation', () => { - scheduleCallback(NormalPriority, () => { - runtime.log('Task'); - while (!Scheduler.unstable_shouldYield()) { - runtime.advanceTime(1); - } - runtime.log(`Yield at ${performance.now()}ms`); - return () => { - runtime.log('Continuation'); - }; - }); - runtime.assertLog(['Request Animation Frame']); - - runtime.fireAnimationFrame(); - runtime.assertLog([ - 'Animation Frame [0]', - 'Request Animation Frame [Reposted]', - 'Set Timer', - 'Post Message', - ]); - runtime.fireMessageEvent(); - runtime.assertLog(['Message Event', 'Task', 'Yield at 34ms']); - - runtime.fireAnimationFrame(); - runtime.assertLog([ - 'Animation Frame [1]', - 'Request Animation Frame [Reposted]', - 'Set Timer', - 'Post Message', - ]); - - runtime.fireMessageEvent(); - runtime.assertLog(['Message Event', 'Continuation']); - - runtime.advanceTimeToNextFrame(); - runtime.fireAnimationFrame(); - runtime.assertLog(['Animation Frame [2]']); - }); - - it('two rAF calls in the same frame', () => { - scheduleCallback(NormalPriority, () => runtime.log('A')); - runtime.assertLog(['Request Animation Frame']); - runtime.fireAnimationFrame(); - runtime.assertLog([ - 'Animation Frame [0]', - 'Request Animation Frame [Reposted]', - 'Set Timer', - 'Post Message', - ]); - runtime.fireMessageEvent(); - runtime.assertLog(['Message Event', 'A']); - - // The Scheduler queue is now empty. We're still in frame 0. - expect(runtime.getMostRecentFrameNumber()).toBe(0); - - // Post a task to Scheduler. - scheduleCallback(NormalPriority, () => runtime.log('B')); - - // Did not request another animation frame, since one was already scheduled - // during the previous rAF. - runtime.assertLog([]); - - // Fire the animation frame. - runtime.fireAnimationFrame(); - runtime.assertLog([ - 'Animation Frame [0]', - 'Request Animation Frame [Reposted]', - 'Set Timer', - 'Post Message', - ]); - - runtime.fireMessageEvent(); - runtime.assertLog(['Message Event', 'B']); - }); - - it('adjusts frame rate by measuring interval between rAF events', () => { - runtime.setHardwareFrameRate(60); - - scheduleCallback(NormalPriority, () => runtime.log('Tick')); - runtime.assertLog(['Request Animation Frame']); - - // Need to measure two consecutive intervals between frames. - for (let i = 0; i < 2; i++) { - runtime.fireAnimationFrame(); - runtime.assertLog([ - `Animation Frame [${runtime.getMostRecentFrameNumber()}]`, - 'Request Animation Frame [Reposted]', - 'Set Timer', - 'Post Message', - ]); - runtime.fireMessageEvent(); - runtime.assertLog(['Message Event', 'Tick']); - scheduleCallback(NormalPriority, () => runtime.log('Tick')); - runtime.advanceTimeToNextFrame(); - } - - // Scheduler should observe that it's receiving rAFs every 16.6 ms and - // adjust its frame rate accordingly. Test by blocking the thread until - // Scheduler tells us to yield. Then measure how much time has elapsed. - const start = performance.now(); - scheduleCallback(NormalPriority, () => { - while (!Scheduler.unstable_shouldYield()) { - runtime.advanceTime(1); - } - }); - runtime.fireAnimationFrame(); - runtime.assertLog([ - `Animation Frame [${runtime.getMostRecentFrameNumber()}]`, - 'Request Animation Frame [Reposted]', - 'Set Timer', - 'Post Message', - ]); - runtime.fireMessageEvent(); - runtime.assertLog(['Message Event', 'Tick']); - const end = performance.now(); - - // Check how much time elapsed in the frame. - expect(end - start).toEqual(17); - }); - }); - - describe('message event loop', () => { - const enableMessageLoopImplementation = true; - beforeAndAfterHooks(enableMessageLoopImplementation); - - it('task that finishes before deadline', () => { - scheduleCallback(NormalPriority, () => { - runtime.log('Task'); - }); - runtime.assertLog(['Post Message']); - runtime.fireMessageEvent(); - runtime.assertLog(['Message Event', 'Task']); - }); - - it('task with continuation', () => { - scheduleCallback(NormalPriority, () => { - runtime.log('Task'); - while (!Scheduler.unstable_shouldYield()) { - runtime.advanceTime(1); - } - runtime.log(`Yield at ${performance.now()}ms`); - return () => { - runtime.log('Continuation'); - }; - }); - runtime.assertLog(['Post Message']); - - runtime.fireMessageEvent(); - runtime.assertLog([ - 'Message Event', - 'Task', - 'Yield at 5ms', - 'Post Message', - ]); - - runtime.fireMessageEvent(); - runtime.assertLog(['Message Event', 'Continuation']); - }); - - it('multiple tasks', () => { - scheduleCallback(NormalPriority, () => { - runtime.log('A'); - }); - scheduleCallback(NormalPriority, () => { - runtime.log('B'); - }); - runtime.assertLog(['Post Message']); - runtime.fireMessageEvent(); - runtime.assertLog(['Message Event', 'A', 'B']); - }); - - it('multiple tasks with a yield in between', () => { - scheduleCallback(NormalPriority, () => { - runtime.log('A'); - runtime.advanceTime(4999); - }); - scheduleCallback(NormalPriority, () => { - runtime.log('B'); - }); - runtime.assertLog(['Post Message']); - runtime.fireMessageEvent(); - runtime.assertLog([ - 'Message Event', - 'A', - // Ran out of time. Post a continuation event. - 'Post Message', - ]); - runtime.fireMessageEvent(); - runtime.assertLog(['Message Event', 'B']); - }); - - it('cancels tasks', () => { - const task = scheduleCallback(NormalPriority, () => { - runtime.log('Task'); - }); - runtime.assertLog(['Post Message']); - cancelCallback(task); - runtime.assertLog([]); - }); - - it('throws when a task errors then continues in a new event', () => { - scheduleCallback(NormalPriority, () => { - runtime.log('Oops!'); - throw Error('Oops!'); - }); - scheduleCallback(NormalPriority, () => { - runtime.log('Yay'); - }); - runtime.assertLog(['Post Message']); - - expect(() => runtime.fireMessageEvent()).toThrow('Oops!'); - runtime.assertLog(['Message Event', 'Oops!', 'Post Message']); - - runtime.fireMessageEvent(); - runtime.assertLog(['Message Event', 'Yay']); - }); - - it('schedule new task after queue has emptied', () => { - scheduleCallback(NormalPriority, () => { - runtime.log('A'); - }); - - runtime.assertLog(['Post Message']); - runtime.fireMessageEvent(); - runtime.assertLog(['Message Event', 'A']); - - scheduleCallback(NormalPriority, () => { - runtime.log('B'); - }); - runtime.assertLog(['Post Message']); - runtime.fireMessageEvent(); - runtime.assertLog(['Message Event', 'B']); - }); - - it('schedule new task after a cancellation', () => { - let handle = scheduleCallback(NormalPriority, () => { - runtime.log('A'); - }); - - runtime.assertLog(['Post Message']); - cancelCallback(handle); - - runtime.fireMessageEvent(); - runtime.assertLog(['Message Event']); - - scheduleCallback(NormalPriority, () => { - runtime.log('B'); - }); - runtime.assertLog(['Post Message']); - runtime.fireMessageEvent(); - runtime.assertLog(['Message Event', 'B']); - }); - }); -}); diff --git a/packages/scheduler/src/__tests__/SchedulerBrowser-test.js b/packages/scheduler/src/__tests__/SchedulerBrowser-test.js new file mode 100644 index 0000000000000..a96b4be3f4e55 --- /dev/null +++ b/packages/scheduler/src/__tests__/SchedulerBrowser-test.js @@ -0,0 +1,272 @@ +/** + * 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. + * + * @emails react-core + * @jest-environment node + */ + +/* eslint-disable no-for-of-loops/no-for-of-loops */ + +'use strict'; + +let Scheduler; +let runtime; +let performance; +let cancelCallback; +let scheduleCallback; +let NormalPriority; + +// The Scheduler implementation uses browser APIs like `MessageChannel` and +// `setTimeout` to schedule work on the main thread. Most of our tests treat +// these as implementation details; however, the sequence and timing of these +// APIs are not precisely specified, and can vary across browsers. +// +// To prevent regressions, we need the ability to simulate specific edge cases +// that we may encounter in various browsers. +// +// This test suite mocks all browser methods used in our implementation. It +// assumes as little as possible about the order and timing of events. +describe('SchedulerBrowser', () => { + beforeEach(() => { + jest.resetModules(); + + // 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 = window.performance; + Scheduler = require('scheduler'); + cancelCallback = Scheduler.unstable_cancelCallback; + scheduleCallback = Scheduler.unstable_scheduleCallback; + NormalPriority = Scheduler.unstable_NormalPriority; + }); + + afterEach(() => { + if (!runtime.isLogEmpty()) { + throw Error('Test exited without clearing log.'); + } + }); + + function installMockBrowserRuntime() { + let hasPendingMessageEvent = false; + + let timerIDCounter = 0; + // let timerIDs = new Map(); + + let eventLog = []; + + const window = {}; + global.window = window; + + let currentTime = 0; + + window.performance = { + now() { + return currentTime; + }, + }; + + // 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? + window.requestAnimationFrame = window.cancelAnimationFrame = () => {}; + + window.setTimeout = (cb, delay) => { + const id = timerIDCounter++; + log(`Set Timer`); + // TODO + return id; + }; + window.clearTimeout = id => { + // TODO + }; + + const port1 = {}; + const port2 = { + postMessage() { + if (hasPendingMessageEvent) { + throw Error('Message event already scheduled'); + } + log('Post Message'); + hasPendingMessageEvent = true; + }, + }; + global.MessageChannel = function MessageChannel() { + this.port1 = port1; + this.port2 = port2; + }; + + function ensureLogIsEmpty() { + if (eventLog.length !== 0) { + throw Error('Log is not empty. Call assertLog before continuing.'); + } + } + function advanceTime(ms) { + currentTime += ms; + } + function fireMessageEvent() { + ensureLogIsEmpty(); + if (!hasPendingMessageEvent) { + throw Error('No message event was scheduled'); + } + hasPendingMessageEvent = false; + const onMessage = port1.onmessage; + log('Message Event'); + onMessage(); + } + function log(val) { + eventLog.push(val); + } + function isLogEmpty() { + return eventLog.length === 0; + } + function assertLog(expected) { + const actual = eventLog; + eventLog = []; + expect(actual).toEqual(expected); + } + return { + advanceTime, + fireMessageEvent, + log, + isLogEmpty, + assertLog, + }; + } + + it('task that finishes before deadline', () => { + scheduleCallback(NormalPriority, () => { + runtime.log('Task'); + }); + runtime.assertLog(['Post Message']); + runtime.fireMessageEvent(); + runtime.assertLog(['Message Event', 'Task']); + }); + + it('task with continuation', () => { + scheduleCallback(NormalPriority, () => { + runtime.log('Task'); + while (!Scheduler.unstable_shouldYield()) { + runtime.advanceTime(1); + } + runtime.log(`Yield at ${performance.now()}ms`); + return () => { + runtime.log('Continuation'); + }; + }); + runtime.assertLog(['Post Message']); + + runtime.fireMessageEvent(); + runtime.assertLog([ + 'Message Event', + 'Task', + 'Yield at 5ms', + 'Post Message', + ]); + + runtime.fireMessageEvent(); + runtime.assertLog(['Message Event', 'Continuation']); + }); + + it('multiple tasks', () => { + scheduleCallback(NormalPriority, () => { + runtime.log('A'); + }); + scheduleCallback(NormalPriority, () => { + runtime.log('B'); + }); + runtime.assertLog(['Post Message']); + runtime.fireMessageEvent(); + runtime.assertLog(['Message Event', 'A', 'B']); + }); + + it('multiple tasks with a yield in between', () => { + scheduleCallback(NormalPriority, () => { + runtime.log('A'); + runtime.advanceTime(4999); + }); + scheduleCallback(NormalPriority, () => { + runtime.log('B'); + }); + runtime.assertLog(['Post Message']); + runtime.fireMessageEvent(); + runtime.assertLog([ + 'Message Event', + 'A', + // Ran out of time. Post a continuation event. + 'Post Message', + ]); + runtime.fireMessageEvent(); + runtime.assertLog(['Message Event', 'B']); + }); + + it('cancels tasks', () => { + const task = scheduleCallback(NormalPriority, () => { + runtime.log('Task'); + }); + runtime.assertLog(['Post Message']); + cancelCallback(task); + runtime.assertLog([]); + }); + + it('throws when a task errors then continues in a new event', () => { + scheduleCallback(NormalPriority, () => { + runtime.log('Oops!'); + throw Error('Oops!'); + }); + scheduleCallback(NormalPriority, () => { + runtime.log('Yay'); + }); + runtime.assertLog(['Post Message']); + + expect(() => runtime.fireMessageEvent()).toThrow('Oops!'); + runtime.assertLog(['Message Event', 'Oops!', 'Post Message']); + + runtime.fireMessageEvent(); + runtime.assertLog(['Message Event', 'Yay']); + }); + + it('schedule new task after queue has emptied', () => { + scheduleCallback(NormalPriority, () => { + runtime.log('A'); + }); + + runtime.assertLog(['Post Message']); + runtime.fireMessageEvent(); + runtime.assertLog(['Message Event', 'A']); + + scheduleCallback(NormalPriority, () => { + runtime.log('B'); + }); + runtime.assertLog(['Post Message']); + runtime.fireMessageEvent(); + runtime.assertLog(['Message Event', 'B']); + }); + + it('schedule new task after a cancellation', () => { + let handle = scheduleCallback(NormalPriority, () => { + runtime.log('A'); + }); + + runtime.assertLog(['Post Message']); + cancelCallback(handle); + + runtime.fireMessageEvent(); + runtime.assertLog(['Message Event']); + + scheduleCallback(NormalPriority, () => { + runtime.log('B'); + }); + runtime.assertLog(['Post Message']); + runtime.fireMessageEvent(); + runtime.assertLog(['Message Event', 'B']); + }); +}); diff --git a/packages/scheduler/src/__tests__/SchedulerRAFOld-test.internal.js b/packages/scheduler/src/__tests__/SchedulerRAFOld-test.internal.js deleted file mode 100644 index 04e73a4e355fb..0000000000000 --- a/packages/scheduler/src/__tests__/SchedulerRAFOld-test.internal.js +++ /dev/null @@ -1,818 +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. - * - * @emails react-core - */ - -'use strict'; - -let Scheduler; -type FrameTimeoutConfigType = { - // should only specify one or the other - timeLeftInFrame: ?number, - timePastFrameDeadline: ?number, -}; - -// Note: this is testing the implementation that we turned off. -// enableMessageLoopImplementation is true on master. -describe('SchedulerRAFOld', () => { - let rAFCallbacks = []; - let postMessageCallback; - let postMessageEvents = []; - let postMessageErrors = []; - let catchPostMessageErrors = false; - - function runPostMessageCallbacks(config: FrameTimeoutConfigType) { - let timeLeftInFrame = 0; - if (typeof config.timeLeftInFrame === 'number') { - timeLeftInFrame = config.timeLeftInFrame; - } else if (typeof config.timePastFrameDeadline === 'number') { - timeLeftInFrame = -1 * config.timePastFrameDeadline; - } - currentTime = startOfLatestFrame + frameSize - timeLeftInFrame; - if (postMessageCallback) { - while (postMessageEvents.length) { - if (catchPostMessageErrors) { - // catch errors for testing error handling - try { - postMessageCallback(postMessageEvents.shift()); - } catch (e) { - postMessageErrors.push(e); - } - } else { - // we are not expecting errors - postMessageCallback(postMessageEvents.shift()); - } - } - } - } - function runRAFCallbacks() { - startOfLatestFrame += frameSize; - currentTime = startOfLatestFrame; - const cbs = rAFCallbacks; - rAFCallbacks = []; - cbs.forEach(cb => cb()); - } - function advanceOneFrame(config: FrameTimeoutConfigType = {}) { - runRAFCallbacks(); - runPostMessageCallbacks(config); - } - - let frameSize; - let startOfLatestFrame; - let currentTime; - - beforeEach(() => { - frameSize = 33; - startOfLatestFrame = 0; - currentTime = 0; - - delete global.performance; - global.requestAnimationFrame = function(cb) { - return rAFCallbacks.push(() => { - cb(startOfLatestFrame); - }); - }; - postMessageEvents = []; - postMessageErrors = []; - const port1 = {}; - const port2 = { - postMessage(messageKey) { - const postMessageEvent = {source: port2, data: messageKey}; - postMessageEvents.push(postMessageEvent); - }, - }; - global.MessageChannel = function MessageChannel() { - this.port1 = port1; - this.port2 = port2; - }; - postMessageCallback = () => port1.onmessage(); - global.Date.now = function() { - return currentTime; - }; - jest.resetModules(); - - // Un-mock scheduler - jest.mock('scheduler', () => require.requireActual('scheduler')); - jest.mock('scheduler/src/SchedulerHostConfig', () => - require.requireActual( - 'scheduler/src/forks/SchedulerHostConfig.default.js', - ), - ); - - require('scheduler/src/SchedulerFeatureFlags').enableMessageLoopImplementation = false; - Scheduler = require('scheduler'); - }); - - describe('scheduleCallback', () => { - it('calls the callback within the frame when not blocked', () => { - const { - unstable_scheduleCallback: scheduleCallback, - unstable_NormalPriority: NormalPriority, - } = Scheduler; - const cb = jest.fn(); - scheduleCallback(NormalPriority, cb); - advanceOneFrame({timeLeftInFrame: 15}); - expect(cb).toHaveBeenCalledTimes(1); - }); - - it('inserts its rAF callback as early into the queue as possible', () => { - const { - unstable_scheduleCallback: scheduleCallback, - unstable_NormalPriority: NormalPriority, - } = Scheduler; - const log = []; - const useRAFCallback = () => { - log.push('userRAFCallback'); - }; - scheduleCallback(NormalPriority, () => { - // Call rAF while idle work is being flushed. - requestAnimationFrame(useRAFCallback); - }); - advanceOneFrame({timeLeftInFrame: 1}); - // There should be two callbacks: the one scheduled by Scheduler at the - // beginning of the frame, and the one scheduled later during that frame. - expect(rAFCallbacks.length).toBe(2); - // The user callback should be the second callback. - rAFCallbacks[1](); - expect(log).toEqual(['userRAFCallback']); - }); - - describe('with multiple callbacks', () => { - it('accepts multiple callbacks and calls within frame when not blocked', () => { - const { - unstable_scheduleCallback: scheduleCallback, - unstable_NormalPriority: NormalPriority, - } = Scheduler; - const callbackLog = []; - const callbackA = jest.fn(() => callbackLog.push('A')); - const callbackB = jest.fn(() => callbackLog.push('B')); - scheduleCallback(NormalPriority, callbackA); - // initially waits to call the callback - expect(callbackLog).toEqual([]); - // waits while second callback is passed - scheduleCallback(NormalPriority, callbackB); - expect(callbackLog).toEqual([]); - // after a delay, calls as many callbacks as it has time for - advanceOneFrame({timeLeftInFrame: 15}); - expect(callbackLog).toEqual(['A', 'B']); - }); - - it("accepts callbacks between animationFrame and postMessage and doesn't stall", () => { - const { - unstable_scheduleCallback: scheduleCallback, - unstable_NormalPriority: NormalPriority, - } = Scheduler; - const callbackLog = []; - const callbackA = jest.fn(() => callbackLog.push('A')); - const callbackB = jest.fn(() => callbackLog.push('B')); - const callbackC = jest.fn(() => callbackLog.push('C')); - scheduleCallback(NormalPriority, callbackA); - // initially waits to call the callback - expect(callbackLog).toEqual([]); - runRAFCallbacks(); - // this should schedule work *after* the requestAnimationFrame but before the message handler - scheduleCallback(NormalPriority, callbackB); - expect(callbackLog).toEqual([]); - // now it should drain the message queue and do all scheduled work - runPostMessageCallbacks({timeLeftInFrame: 15}); - expect(callbackLog).toEqual(['A', 'B']); - - // advances timers, now with an empty queue of work (to ensure they don't deadlock) - advanceOneFrame({timeLeftInFrame: 15}); - - // see if more work can be done now. - scheduleCallback(NormalPriority, callbackC); - expect(callbackLog).toEqual(['A', 'B']); - advanceOneFrame({timeLeftInFrame: 15}); - expect(callbackLog).toEqual(['A', 'B', 'C']); - }); - - it( - 'schedules callbacks in correct order and' + - 'keeps calling them if there is time', - () => { - const { - unstable_scheduleCallback: scheduleCallback, - unstable_NormalPriority: NormalPriority, - } = Scheduler; - const callbackLog = []; - const callbackA = jest.fn(() => { - callbackLog.push('A'); - scheduleCallback(NormalPriority, callbackC); - }); - const callbackB = jest.fn(() => { - callbackLog.push('B'); - }); - const callbackC = jest.fn(() => { - callbackLog.push('C'); - }); - - scheduleCallback(NormalPriority, callbackA); - // initially waits to call the callback - expect(callbackLog).toEqual([]); - // continues waiting while B is scheduled - scheduleCallback(NormalPriority, callbackB); - expect(callbackLog).toEqual([]); - // after a delay, calls the scheduled callbacks, - // and also calls new callbacks scheduled by current callbacks - advanceOneFrame({timeLeftInFrame: 15}); - expect(callbackLog).toEqual(['A', 'B', 'C']); - }, - ); - - it('schedules callbacks in correct order when callbacks have many nested scheduleCallback calls', () => { - const { - unstable_scheduleCallback: scheduleCallback, - unstable_NormalPriority: NormalPriority, - } = Scheduler; - const callbackLog = []; - const callbackA = jest.fn(() => { - callbackLog.push('A'); - scheduleCallback(NormalPriority, callbackC); - scheduleCallback(NormalPriority, callbackD); - }); - const callbackB = jest.fn(() => { - callbackLog.push('B'); - scheduleCallback(NormalPriority, callbackE); - scheduleCallback(NormalPriority, callbackF); - }); - const callbackC = jest.fn(() => { - callbackLog.push('C'); - }); - const callbackD = jest.fn(() => { - callbackLog.push('D'); - }); - const callbackE = jest.fn(() => { - callbackLog.push('E'); - }); - const callbackF = jest.fn(() => { - callbackLog.push('F'); - }); - - scheduleCallback(NormalPriority, callbackA); - scheduleCallback(NormalPriority, callbackB); - // initially waits to call the callback - expect(callbackLog).toEqual([]); - // while flushing callbacks, calls as many as it has time for - advanceOneFrame({timeLeftInFrame: 15}); - expect(callbackLog).toEqual(['A', 'B', 'C', 'D', 'E', 'F']); - }); - - it('schedules callbacks in correct order when they use scheduleCallback to schedule themselves', () => { - const { - unstable_scheduleCallback: scheduleCallback, - unstable_NormalPriority: NormalPriority, - } = Scheduler; - const callbackLog = []; - let callbackAIterations = 0; - const callbackA = jest.fn(() => { - if (callbackAIterations < 1) { - scheduleCallback(NormalPriority, callbackA); - } - callbackLog.push('A' + callbackAIterations); - callbackAIterations++; - }); - const callbackB = jest.fn(() => callbackLog.push('B')); - - scheduleCallback(NormalPriority, callbackA); - // initially waits to call the callback - expect(callbackLog).toEqual([]); - scheduleCallback(NormalPriority, callbackB); - expect(callbackLog).toEqual([]); - // after a delay, calls the latest callback passed - advanceOneFrame({timeLeftInFrame: 15}); - expect(callbackLog).toEqual(['A0', 'B', 'A1']); - }); - }); - - describe('when callbacks time out: ', () => { - // USEFUL INFO: - // startOfLatestFrame is a global that goes up every time rAF runs - // currentTime defaults to startOfLatestFrame inside rAF callback - // and currentTime defaults to 15 before next frame inside idleTick - - describe('when there is no more time left in the frame', () => { - it('calls any callback which has timed out, waits for others', () => { - const { - unstable_scheduleCallback: scheduleCallback, - unstable_NormalPriority: NormalPriority, - } = Scheduler; - startOfLatestFrame = 1000000000000; - currentTime = startOfLatestFrame - 10; - const callbackLog = []; - // simple case of one callback which times out, another that won't. - const callbackA = jest.fn(() => callbackLog.push('A')); - const callbackB = jest.fn(() => callbackLog.push('B')); - const callbackC = jest.fn(() => callbackLog.push('C')); - - scheduleCallback(NormalPriority, callbackA); // won't time out - scheduleCallback(NormalPriority, callbackB, {timeout: 100}); // times out later - scheduleCallback(NormalPriority, callbackC, {timeout: 2}); // will time out fast - - // push time ahead a bit so that we have no idle time - advanceOneFrame({timePastFrameDeadline: 16}); - - // callbackC should have timed out - expect(callbackLog).toEqual(['C']); - - // push time ahead a bit so that we have no idle time - advanceOneFrame({timePastFrameDeadline: 16}); - - // callbackB should have timed out - expect(callbackLog).toEqual(['C', 'B']); - - // let's give ourselves some idle time now - advanceOneFrame({timeLeftInFrame: 16}); - - // we should have run callbackA in the idle time - expect(callbackLog).toEqual(['C', 'B', 'A']); - }); - }); - - describe('when there is some time left in the frame', () => { - it('calls timed out callbacks and then any more pending callbacks, defers others if time runs out', () => { - const { - unstable_scheduleCallback: scheduleCallback, - unstable_NormalPriority: NormalPriority, - } = Scheduler; - startOfLatestFrame = 1000000000000; - currentTime = startOfLatestFrame - 10; - const callbackLog = []; - // simple case of one callback which times out, others that won't. - const callbackA = jest.fn(() => { - callbackLog.push('A'); - // time passes, causing us to run out of idle time - currentTime += 25; - }); - const callbackB = jest.fn(() => callbackLog.push('B')); - const callbackC = jest.fn(() => callbackLog.push('C')); - const callbackD = jest.fn(() => callbackLog.push('D')); - - scheduleCallback(NormalPriority, callbackA, {timeout: 100}); // won't time out - scheduleCallback(NormalPriority, callbackB, {timeout: 100}); // times out later - scheduleCallback(NormalPriority, callbackC, {timeout: 2}); // will time out fast - scheduleCallback(NormalPriority, callbackD, {timeout: 200}); // won't time out - - advanceOneFrame({timeLeftInFrame: 15}); // runs rAF and postMessage callbacks - - // callbackC should have timed out - // we should have had time to call A also, then we run out of time - expect(callbackLog).toEqual(['C', 'A']); - - // push time ahead a bit so that we have no idle time - advanceOneFrame({timePastFrameDeadline: 16}); - - // callbackB should have timed out - // but we should not run callbackD because we have no idle time - expect(callbackLog).toEqual(['C', 'A', 'B']); - - advanceOneFrame({timeLeftInFrame: 15}); // runs rAF and postMessage callbacks - - // we should have run callbackD in the idle time - expect(callbackLog).toEqual(['C', 'A', 'B', 'D']); - - advanceOneFrame({timeLeftInFrame: 15}); // runs rAF and postMessage callbacks - - // we should not have run anything again, nothing is scheduled - expect(callbackLog).toEqual(['C', 'A', 'B', 'D']); - }); - }); - }); - }); - - describe('cancelCallback', () => { - it('cancels the scheduled callback', () => { - const { - unstable_scheduleCallback: scheduleCallback, - unstable_cancelCallback: cancelCallback, - unstable_NormalPriority: NormalPriority, - } = Scheduler; - const cb = jest.fn(); - const callbackId = scheduleCallback(NormalPriority, cb); - expect(cb).toHaveBeenCalledTimes(0); - cancelCallback(callbackId); - advanceOneFrame({timeLeftInFrame: 15}); - expect(cb).toHaveBeenCalledTimes(0); - }); - - describe('with multiple callbacks', () => { - it('when called more than once', () => { - const { - unstable_scheduleCallback: scheduleCallback, - unstable_cancelCallback: cancelCallback, - unstable_NormalPriority: NormalPriority, - } = Scheduler; - const callbackLog = []; - const callbackA = jest.fn(() => callbackLog.push('A')); - const callbackB = jest.fn(() => callbackLog.push('B')); - const callbackC = jest.fn(() => callbackLog.push('C')); - scheduleCallback(NormalPriority, callbackA); - const callbackId = scheduleCallback(NormalPriority, callbackB); - scheduleCallback(NormalPriority, callbackC); - cancelCallback(callbackId); - cancelCallback(callbackId); - cancelCallback(callbackId); - // Initially doesn't call anything - expect(callbackLog).toEqual([]); - advanceOneFrame({timeLeftInFrame: 15}); - - // Should still call A and C - expect(callbackLog).toEqual(['A', 'C']); - expect(callbackB).toHaveBeenCalledTimes(0); - }); - - it('when one callback cancels the next one', () => { - const { - unstable_scheduleCallback: scheduleCallback, - unstable_cancelCallback: cancelCallback, - unstable_NormalPriority: NormalPriority, - } = Scheduler; - const callbackLog = []; - let callbackBId; - const callbackA = jest.fn(() => { - callbackLog.push('A'); - cancelCallback(callbackBId); - }); - const callbackB = jest.fn(() => callbackLog.push('B')); - scheduleCallback(NormalPriority, callbackA); - callbackBId = scheduleCallback(NormalPriority, callbackB); - // Initially doesn't call anything - expect(callbackLog).toEqual([]); - advanceOneFrame({timeLeftInFrame: 15}); - // B should not get called because A cancelled B - expect(callbackLog).toEqual(['A']); - expect(callbackB).toHaveBeenCalledTimes(0); - }); - }); - }); - - describe('when callbacks throw errors', () => { - describe('when some callbacks throw', () => { - /** - * + + - * | rAF postMessage | - * | | - * | +---------------------+ | - * | | paint/layout | cbA() cbB() cbC() cbD() cbE() | - * | +---------------------+ ^ ^ | - * | | | | - * + | | + - * + + - * throw errors - * - * - */ - it('still calls all callbacks within same frame', () => { - const { - unstable_scheduleCallback: scheduleCallback, - unstable_NormalPriority: NormalPriority, - } = Scheduler; - const callbackLog = []; - const callbackA = jest.fn(() => callbackLog.push('A')); - const callbackB = jest.fn(() => { - callbackLog.push('B'); - throw new Error('B error'); - }); - const callbackC = jest.fn(() => callbackLog.push('C')); - const callbackD = jest.fn(() => { - callbackLog.push('D'); - throw new Error('D error'); - }); - const callbackE = jest.fn(() => callbackLog.push('E')); - scheduleCallback(NormalPriority, callbackA); - scheduleCallback(NormalPriority, callbackB); - scheduleCallback(NormalPriority, callbackC); - scheduleCallback(NormalPriority, callbackD); - scheduleCallback(NormalPriority, callbackE); - // Initially doesn't call anything - expect(callbackLog).toEqual([]); - catchPostMessageErrors = true; - advanceOneFrame({timeLeftInFrame: 15}); - // calls all callbacks - expect(callbackLog).toEqual(['A', 'B', 'C', 'D', 'E']); - // errors should still get thrown - const postMessageErrorMessages = postMessageErrors.map(e => e.message); - expect(postMessageErrorMessages).toEqual(['B error', 'D error']); - catchPostMessageErrors = false; - }); - - /** - * timed out - * + + +--+ - * + rAF postMessage | | | + - * | | | | | - * | +---------------------+ v v v | - * | | paint/layout | cbA() cbB() cbC() cbD() cbE() | - * | +---------------------+ ^ ^ | - * | | | | - * + | | + - * + + - * throw errors - * - * - */ - it('and with some timed out callbacks, still calls all callbacks within same frame', () => { - const { - unstable_scheduleCallback: scheduleCallback, - unstable_NormalPriority: NormalPriority, - } = Scheduler; - const callbackLog = []; - const callbackA = jest.fn(() => { - callbackLog.push('A'); - throw new Error('A error'); - }); - const callbackB = jest.fn(() => callbackLog.push('B')); - const callbackC = jest.fn(() => callbackLog.push('C')); - const callbackD = jest.fn(() => { - callbackLog.push('D'); - throw new Error('D error'); - }); - const callbackE = jest.fn(() => callbackLog.push('E')); - scheduleCallback(NormalPriority, callbackA); - scheduleCallback(NormalPriority, callbackB); - scheduleCallback(NormalPriority, callbackC, {timeout: 2}); // times out fast - scheduleCallback(NormalPriority, callbackD, {timeout: 2}); // times out fast - scheduleCallback(NormalPriority, callbackE, {timeout: 2}); // times out fast - // Initially doesn't call anything - expect(callbackLog).toEqual([]); - catchPostMessageErrors = true; - advanceOneFrame({timeLeftInFrame: 15}); - // calls all callbacks; calls timed out ones first - expect(callbackLog).toEqual(['C', 'D', 'E', 'A', 'B']); - // errors should still get thrown - const postMessageErrorMessages = postMessageErrors.map(e => e.message); - expect(postMessageErrorMessages).toEqual(['D error', 'A error']); - catchPostMessageErrors = false; - }); - }); - describe('when all scheduled callbacks throw', () => { - /** - * + + - * | rAF postMessage | - * | | - * | +---------------------+ | - * | | paint/layout | cbA() cbB() cbC() cbD() cbE() | - * | +---------------------+ ^ ^ ^ ^ ^ | - * | | | | | | | - * + | | | | | + - * | + + + + - * + all callbacks throw errors - * - * - */ - it('still calls all callbacks within same frame', () => { - const { - unstable_scheduleCallback: scheduleCallback, - unstable_NormalPriority: NormalPriority, - } = Scheduler; - const callbackLog = []; - const callbackA = jest.fn(() => { - callbackLog.push('A'); - throw new Error('A error'); - }); - const callbackB = jest.fn(() => { - callbackLog.push('B'); - throw new Error('B error'); - }); - const callbackC = jest.fn(() => { - callbackLog.push('C'); - throw new Error('C error'); - }); - const callbackD = jest.fn(() => { - callbackLog.push('D'); - throw new Error('D error'); - }); - const callbackE = jest.fn(() => { - callbackLog.push('E'); - throw new Error('E error'); - }); - scheduleCallback(NormalPriority, callbackA); - scheduleCallback(NormalPriority, callbackB); - scheduleCallback(NormalPriority, callbackC); - scheduleCallback(NormalPriority, callbackD); - scheduleCallback(NormalPriority, callbackE); - // Initially doesn't call anything - expect(callbackLog).toEqual([]); - catchPostMessageErrors = true; - advanceOneFrame({timeLeftInFrame: 15}); - // calls all callbacks - expect(callbackLog).toEqual(['A', 'B', 'C', 'D', 'E']); - // errors should still get thrown - const postMessageErrorMessages = postMessageErrors.map(e => e.message); - expect(postMessageErrorMessages).toEqual([ - 'A error', - 'B error', - 'C error', - 'D error', - 'E error', - ]); - catchPostMessageErrors = false; - }); - - /** - * postMessage - * + + - * | rAF all callbacks time out | - * | | - * | +---------------------+ | - * | | paint/layout | cbA() cbB() cbC() cbD() cbE() | - * | +---------------------+ ^ ^ ^ ^ ^ | - * | | | | | | | - * + | | | | | + - * | + + + + - * + all callbacks throw errors - * - * - */ - it('and with all timed out callbacks, still calls all callbacks within same frame', () => { - const { - unstable_scheduleCallback: scheduleCallback, - unstable_NormalPriority: NormalPriority, - } = Scheduler; - const callbackLog = []; - const callbackA = jest.fn(() => { - callbackLog.push('A'); - throw new Error('A error'); - }); - const callbackB = jest.fn(() => { - callbackLog.push('B'); - throw new Error('B error'); - }); - const callbackC = jest.fn(() => { - callbackLog.push('C'); - throw new Error('C error'); - }); - const callbackD = jest.fn(() => { - callbackLog.push('D'); - throw new Error('D error'); - }); - const callbackE = jest.fn(() => { - callbackLog.push('E'); - throw new Error('E error'); - }); - scheduleCallback(NormalPriority, callbackA, {timeout: 2}); // times out fast - scheduleCallback(NormalPriority, callbackB, {timeout: 2}); // times out fast - scheduleCallback(NormalPriority, callbackC, {timeout: 2}); // times out fast - scheduleCallback(NormalPriority, callbackD, {timeout: 2}); // times out fast - scheduleCallback(NormalPriority, callbackE, {timeout: 2}); // times out fast - // Initially doesn't call anything - expect(callbackLog).toEqual([]); - catchPostMessageErrors = true; - advanceOneFrame({timeLeftInFrame: 15}); - // calls all callbacks - expect(callbackLog).toEqual(['A', 'B', 'C', 'D', 'E']); - // errors should still get thrown - const postMessageErrorMessages = postMessageErrors.map(e => e.message); - expect(postMessageErrorMessages).toEqual([ - 'A error', - 'B error', - 'C error', - 'D error', - 'E error', - ]); - catchPostMessageErrors = false; - }); - }); - describe('when callbacks throw over multiple frames', () => { - /** - * - * **Detail View of Frame 1** - * - * + + - * | rAF postMessage | - * | | - * | +---------------------+ | - * | | paint/layout | cbA() cbB() | ... Frame 2 - * | +---------------------+ ^ ^ | - * | | | | - * + + | + - * errors | - * + - * takes long time - * and pushes rest of - * callbacks into - * next frame -> - * - * - * - * **Overview of frames 1-4** - * - * - * + + + + + - * | | | | | - * | +--+ | +--+ | +--+ | +--+ | - * | +--+ A,B+-> +--+ C,D+-> +--+ E,F+-> +--+ G | - * + ^ + ^ + ^ + + - * | | | - * error error error - * - * - */ - it('still calls all callbacks within same frame', () => { - const { - unstable_scheduleCallback: scheduleCallback, - unstable_NormalPriority: NormalPriority, - } = Scheduler; - startOfLatestFrame = 1000000000000; - currentTime = startOfLatestFrame - 10; - catchPostMessageErrors = true; - const callbackLog = []; - const callbackA = jest.fn(() => { - callbackLog.push('A'); - throw new Error('A error'); - }); - const callbackB = jest.fn(() => { - callbackLog.push('B'); - // time passes, causing us to run out of idle time - currentTime += 25; - }); - const callbackC = jest.fn(() => { - callbackLog.push('C'); - throw new Error('C error'); - }); - const callbackD = jest.fn(() => { - callbackLog.push('D'); - // time passes, causing us to run out of idle time - currentTime += 25; - }); - const callbackE = jest.fn(() => { - callbackLog.push('E'); - throw new Error('E error'); - }); - const callbackF = jest.fn(() => { - callbackLog.push('F'); - // time passes, causing us to run out of idle time - currentTime += 25; - }); - const callbackG = jest.fn(() => callbackLog.push('G')); - - scheduleCallback(NormalPriority, callbackA); - scheduleCallback(NormalPriority, callbackB); - scheduleCallback(NormalPriority, callbackC); - scheduleCallback(NormalPriority, callbackD); - scheduleCallback(NormalPriority, callbackE); - scheduleCallback(NormalPriority, callbackF); - scheduleCallback(NormalPriority, callbackG); - - // does nothing initially - expect(callbackLog).toEqual([]); - - // frame 1; - // callback A runs and throws, callback B takes up rest of frame - advanceOneFrame({timeLeftInFrame: 15}); // runs rAF and postMessage callbacks - - // calls A and B - expect(callbackLog).toEqual(['A', 'B']); - // error was thrown from A - let postMessageErrorMessages = postMessageErrors.map(e => e.message); - expect(postMessageErrorMessages).toEqual(['A error']); - - // frame 2; - // callback C runs and throws, callback D takes up rest of frame - advanceOneFrame({timeLeftInFrame: 15}); // runs rAF and postMessage callbacks - - // calls C and D - expect(callbackLog).toEqual(['A', 'B', 'C', 'D']); - // error was thrown from A - postMessageErrorMessages = postMessageErrors.map(e => e.message); - expect(postMessageErrorMessages).toEqual(['A error', 'C error']); - - // frame 3; - // callback E runs and throws, callback F takes up rest of frame - advanceOneFrame({timeLeftInFrame: 15}); // runs rAF and postMessage callbacks - - // calls E and F - expect(callbackLog).toEqual(['A', 'B', 'C', 'D', 'E', 'F']); - // error was thrown from A - postMessageErrorMessages = postMessageErrors.map(e => e.message); - expect(postMessageErrorMessages).toEqual([ - 'A error', - 'C error', - 'E error', - ]); - - // frame 4; - // callback G runs and it's the last one - advanceOneFrame({timeLeftInFrame: 15}); // runs rAF and postMessage callbacks - - // calls G - expect(callbackLog).toEqual(['A', 'B', 'C', 'D', 'E', 'F', 'G']); - // error was thrown from A - postMessageErrorMessages = postMessageErrors.map(e => e.message); - expect(postMessageErrorMessages).toEqual([ - 'A error', - 'C error', - 'E error', - ]); - - catchPostMessageErrors = true; - }); - }); - }); - - // TODO: test 'now' -}); diff --git a/packages/scheduler/src/forks/SchedulerFeatureFlags.www.js b/packages/scheduler/src/forks/SchedulerFeatureFlags.www.js index e5d4cd05f8d7f..9fd86c7f94b2e 100644 --- a/packages/scheduler/src/forks/SchedulerFeatureFlags.www.js +++ b/packages/scheduler/src/forks/SchedulerFeatureFlags.www.js @@ -13,4 +13,3 @@ export const { } = require('SchedulerFeatureFlags'); export const enableProfiling = __PROFILE__ && enableProfilingFeatureFlag; -export const enableMessageLoopImplementation = true; diff --git a/packages/scheduler/src/forks/SchedulerHostConfig.default.js b/packages/scheduler/src/forks/SchedulerHostConfig.default.js index 77e15c1000b52..c5b28c84bbf0f 100644 --- a/packages/scheduler/src/forks/SchedulerHostConfig.default.js +++ b/packages/scheduler/src/forks/SchedulerHostConfig.default.js @@ -5,18 +5,7 @@ * LICENSE file in the root directory of this source tree. */ -import { - enableIsInputPending, - enableMessageLoopImplementation, -} from '../SchedulerFeatureFlags'; - -// The DOM Scheduler implementation is similar to requestIdleCallback. It -// works by scheduling a requestAnimationFrame, storing the time for the start -// of the frame, then scheduling a postMessage which gets scheduled after paint. -// Within the postMessage handler do as much work as possible until time + frame -// rate. By separating the idle call into a separate event tick we ensure that -// layout, paint and other browser work is counted against the available time. -// The frame rate is dynamically adjusted. +import {enableIsInputPending} from '../SchedulerFeatureFlags'; export let requestHostCallback; export let cancelHostCallback; @@ -83,10 +72,13 @@ if ( const Date = window.Date; const setTimeout = window.setTimeout; const clearTimeout = window.clearTimeout; - const requestAnimationFrame = window.requestAnimationFrame; - const cancelAnimationFrame = window.cancelAnimationFrame; 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') { console.error( @@ -114,32 +106,20 @@ if ( getCurrentTime = () => Date.now() - initialTime; } - let isRAFLoopRunning = false; let isMessageLoopRunning = false; let scheduledHostCallback = null; - let rAFTimeoutID = -1; let taskTimeoutID = -1; - let frameLength = enableMessageLoopImplementation - ? // We won't attempt to align with the vsync. Instead we'll yield multiple - // times per frame, often enough to keep it responsive even at really - // high frame rates > 120. - 5 - : // Use a heuristic to measure the frame rate and yield at the end of the - // frame. We start out assuming that we run at 30fps but then the - // heuristic tracking will adjust this value to a faster fps if we get - // more frequent animation frames. - 33.33; - - let prevRAFTime = -1; - let prevRAFInterval = -1; - let frameDeadline = 0; - - let fpsLocked = false; + // 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? - let maxFrameLength = 300; + let maxYieldInterval = 300; let needsPaint = false; if ( @@ -151,13 +131,13 @@ if ( const scheduling = navigator.scheduling; shouldYieldToHost = function() { const currentTime = getCurrentTime(); - if (currentTime >= frameDeadline) { - // There's no time left in the frame. 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 + 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()) { @@ -165,8 +145,8 @@ if ( return true; } // There's no pending input. Only yield if we've reached the max - // frame length. - return currentTime >= frameDeadline + maxFrameLength; + // yield interval. + return currentTime >= maxYieldInterval; } else { // There's still time left in the frame. return false; @@ -180,7 +160,7 @@ if ( // `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() >= frameDeadline; + return getCurrentTime() >= deadline; }; // Since we yield every frame regardless, `requestPaint` has no effect. @@ -196,164 +176,57 @@ if ( return; } if (fps > 0) { - frameLength = Math.floor(1000 / fps); - fpsLocked = true; + yieldInterval = Math.floor(1000 / fps); } else { // reset the framerate - frameLength = 33.33; - fpsLocked = false; + yieldInterval = 5; } }; const performWorkUntilDeadline = () => { - if (enableMessageLoopImplementation) { - if (scheduledHostCallback !== null) { - const currentTime = getCurrentTime(); - // Yield after `frameLength` ms, regardless of where we are in the vsync - // cycle. This means there's always time remaining at the beginning of - // the message event. - frameDeadline = currentTime + frameLength; - 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. + 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); - throw error; } - } else { - isMessageLoopRunning = false; + } catch (error) { + // If a scheduler task throws, exit the current browser task so the + // error can be observed. + port.postMessage(null); + throw error; } - // Yielding to the browser will give it a chance to paint, so we can - // reset this. - needsPaint = false; } else { - if (scheduledHostCallback !== null) { - const currentTime = getCurrentTime(); - const hasTimeRemaining = frameDeadline - currentTime > 0; - try { - const hasMoreWork = scheduledHostCallback( - hasTimeRemaining, - currentTime, - ); - if (!hasMoreWork) { - scheduledHostCallback = null; - } - } catch (error) { - // If a scheduler task throws, exit the current browser task so the - // error can be observed, and post a new task as soon as possible - // so we can continue where we left off. - port.postMessage(null); - throw error; - } - } - // Yielding to the browser will give it a chance to paint, so we can - // reset this. - needsPaint = false; + 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; - const onAnimationFrame = rAFTime => { - if (scheduledHostCallback === null) { - // No scheduled work. Exit. - prevRAFTime = -1; - prevRAFInterval = -1; - isRAFLoopRunning = false; - return; - } - - // Eagerly schedule the next animation callback at the beginning of the - // frame. If the scheduler queue is not empty at the end of the frame, it - // will continue flushing inside that callback. If the queue *is* empty, - // then it will exit immediately. Posting the callback at the start of the - // frame ensures it's fired within the earliest possible frame. If we - // waited until the end of the frame to post the callback, we risk the - // browser skipping a frame and not firing the callback until the frame - // after that. - isRAFLoopRunning = true; - requestAnimationFrame(nextRAFTime => { - clearTimeout(rAFTimeoutID); - onAnimationFrame(nextRAFTime); - }); - - // requestAnimationFrame is throttled when the tab is backgrounded. We - // don't want to stop working entirely. So we'll fallback to a timeout loop. - // TODO: Need a better heuristic for backgrounded work. - const onTimeout = () => { - frameDeadline = getCurrentTime() + frameLength / 2; - performWorkUntilDeadline(); - rAFTimeoutID = setTimeout(onTimeout, frameLength * 3); - }; - rAFTimeoutID = setTimeout(onTimeout, frameLength * 3); - - if ( - prevRAFTime !== -1 && - // Make sure this rAF time is different from the previous one. This check - // could fail if two rAFs fire in the same frame. - rAFTime - prevRAFTime > 0.1 - ) { - const rAFInterval = rAFTime - prevRAFTime; - if (!fpsLocked && prevRAFInterval !== -1) { - // We've observed two consecutive frame intervals. We'll use this to - // dynamically adjust the frame rate. - // - // If one frame goes long, then the next one can be short to catch up. - // If two frames are short in a row, then that's an indication that we - // actually have a higher frame rate than what we're currently - // optimizing. For example, if we're running on 120hz display or 90hz VR - // display. Take the max of the two in case one of them was an anomaly - // due to missed frame deadlines. - if (rAFInterval < frameLength && prevRAFInterval < frameLength) { - frameLength = - rAFInterval < prevRAFInterval ? prevRAFInterval : rAFInterval; - if (frameLength < 8.33) { - // Defensive coding. We don't support higher frame rates than 120hz. - // If the calculated frame length gets lower than 8, it is probably - // a bug. - frameLength = 8.33; - } - } - } - prevRAFInterval = rAFInterval; - } - prevRAFTime = rAFTime; - frameDeadline = rAFTime + frameLength; - - // We use the postMessage trick to defer idle work until after the repaint. - port.postMessage(null); - }; - requestHostCallback = function(callback) { scheduledHostCallback = callback; - if (enableMessageLoopImplementation) { - if (!isMessageLoopRunning) { - isMessageLoopRunning = true; - port.postMessage(null); - } - } else { - if (!isRAFLoopRunning) { - // Start a rAF loop. - isRAFLoopRunning = true; - requestAnimationFrame(rAFTime => { - onAnimationFrame(rAFTime); - }); - } + if (!isMessageLoopRunning) { + isMessageLoopRunning = true; + port.postMessage(null); } };