Skip to content

Commit

Permalink
[scheduler] Yield many times per frame, no rAF
Browse files Browse the repository at this point in the history
Adds experimental flag to yield many times per frame using a message
event loop, instead of the current approach of guessing the next vsync
and yielding at the end of the frame.

This new approach forgoes a `requestAnimationFrame` entirely. It posts a
message event and performs a small amount of work (5ms) before yielding
to the browser, regardless of where it might be in the vsync cycle. At
the end of the event, if there's work left over, it posts another
message event.

This should keep the main thread responsive even for really high frame
rates. It also shouldn't matter if the hardware frame rate changes after
page load (our current heuristic only detects if the frame rate
increases, not decreases).

The main risk is that yielding more often will exacerbate main thread
contention with other browser tasks.

Let's try it and see.
  • Loading branch information
acdlite committed Jul 25, 2019
1 parent c0830a0 commit 7cc5b84
Show file tree
Hide file tree
Showing 4 changed files with 294 additions and 188 deletions.
1 change: 1 addition & 0 deletions packages/scheduler/src/SchedulerFeatureFlags.js
Expand Up @@ -10,3 +10,4 @@ export const enableSchedulerDebugging = false;
export const enableIsInputPending = false;
export const requestIdleCallbackBeforeFirstFrame = false;
export const requestTimerEventBeforeFirstFrame = false;
export const enableMessageLoopImplementation = false;
Expand Up @@ -36,29 +36,32 @@ let NormalPriority;
// It also includes Scheduler-specific invariants, e.g. only one rAF callback
// can be scheduled at a time.
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');
scheduleCallback = Scheduler.unstable_scheduleCallback;
NormalPriority = Scheduler.unstable_NormalPriority;
});
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');
scheduleCallback = Scheduler.unstable_scheduleCallback;
NormalPriority = Scheduler.unstable_NormalPriority;
});

afterEach(() => {
if (!runtime.isLogEmpty()) {
throw Error('Test exited without clearing log.');
}
});
afterEach(() => {
if (!runtime.isLogEmpty()) {
throw Error('Test exited without clearing log.');
}
});
}

function installMockBrowserRuntime() {
let VSYNC_INTERVAL = 33.33;
Expand Down Expand Up @@ -228,89 +231,115 @@ describe('SchedulerBrowser', () => {
};
}

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');
};
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]']);
});
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('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);

it('adjusts frame rate by measuring inteval between rAF events', () => {
runtime.setHardwareFrameRate(60);
// Post a task to Scheduler.
scheduleCallback(NormalPriority, () => runtime.log('B'));

scheduleCallback(NormalPriority, () => runtime.log('Tick'));
runtime.assertLog(['Request Animation Frame']);
// Did not request another animation frame, since one was already scheduled
// during the previous rAF.
runtime.assertLog([]);

// Need to measure two consecutive intervals between frames.
for (let i = 0; i < 2; i++) {
// 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 inteval 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()}]`,
Expand All @@ -320,31 +349,57 @@ describe('SchedulerBrowser', () => {
]);
runtime.fireMessageEvent();
runtime.assertLog(['Message Event', 'Tick']);
scheduleCallback(NormalPriority, () => runtime.log('Tick'));
runtime.advanceTimeToNextFrame();
}
const end = performance.now();

// 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);
}
// Check how much time elapsed in the frame.
expect(end - start).toEqual(17);
});
});

describe('message event loop', () => {
const enableMessageLoopImplementation = true;
beforeAndAfterHooks(enableMessageLoopImplementation);

it('callback with continutation', () => {
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('task that throws', () => {
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']);
});
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);
});
});
1 change: 1 addition & 0 deletions packages/scheduler/src/forks/SchedulerFeatureFlags.www.js
Expand Up @@ -11,4 +11,5 @@ export const {
enableSchedulerDebugging,
requestIdleCallbackBeforeFirstFrame,
requestTimerEventBeforeFirstFrame,
enableMessageLoopImplementation,
} = require('SchedulerFeatureFlags');

0 comments on commit 7cc5b84

Please sign in to comment.