mirror of
https://github.com/facebook/react.git
synced 2025-11-01 09:12:30 +00:00
[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:
@@ -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);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
Reference in New Issue
Block a user