[Scheduler] Delete old rAF implementation (#17252)

We've been using the message loop version for a while. Time to delete.
This commit is contained in:
Andrew Clark
2019-11-01 13:32:22 -07:00
committed by GitHub
parent 6dc2734b41
commit a2e05b6c14
6 changed files with 327 additions and 1495 deletions
@@ -8,5 +8,4 @@
export const enableSchedulerDebugging = false;
export const enableIsInputPending = false;
export const enableMessageLoopImplementation = true;
export const enableProfiling = __PROFILE__;
@@ -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']);
});
});
});
@@ -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']);
});
});
@@ -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'
});
@@ -13,4 +13,3 @@ export const {
} = require('SchedulerFeatureFlags');
export const enableProfiling = __PROFILE__ && enableProfilingFeatureFlag;
export const enableMessageLoopImplementation = true;
@@ -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);
}
};