Skip to content

Commit a44ab2a

Browse files
Andrew Clarkjetoneza
Andrew Clark
authored andcommitted
[scheduler] Eagerly schedule rAF at beginning of frame (facebook#13785)
* [scheduler] Eagerly schedule rAF at beginning of frame 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. * Re-name scheduledCallback -> scheduledHostCallback
1 parent 067c3af commit a44ab2a

File tree

2 files changed

+61
-22
lines changed

2 files changed

+61
-22
lines changed

packages/scheduler/src/Scheduler.js

Lines changed: 38 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -534,13 +534,13 @@ if (typeof window !== 'undefined' && window._schedMock) {
534534
}
535535
}
536536

537-
var scheduledCallback = null;
538-
var isIdleScheduled = false;
537+
var scheduledHostCallback = null;
538+
var isMessageEventScheduled = false;
539539
var timeoutTime = -1;
540540

541541
var isAnimationFrameScheduled = false;
542542

543-
var isPerformingIdleWork = false;
543+
var isFlushingHostCallback = false;
544544

545545
var frameDeadline = 0;
546546
// We start out assuming that we run at 30fps but then the heuristic tracking
@@ -564,15 +564,20 @@ if (typeof window !== 'undefined' && window._schedMock) {
564564
return;
565565
}
566566

567-
isIdleScheduled = false;
567+
isMessageEventScheduled = false;
568+
569+
var prevScheduledCallback = scheduledHostCallback;
570+
var prevTimeoutTime = timeoutTime;
571+
scheduledHostCallback = null;
572+
timeoutTime = -1;
568573

569574
var currentTime = getCurrentTime();
570575

571576
var didTimeout = false;
572577
if (frameDeadline - currentTime <= 0) {
573578
// There's no time left in this idle period. Check if the callback has
574579
// a timeout and whether it's been exceeded.
575-
if (timeoutTime !== -1 && timeoutTime <= currentTime) {
580+
if (prevTimeoutTime !== -1 && prevTimeoutTime <= currentTime) {
576581
// Exceeded the timeout. Invoke the callback even though there's no
577582
// time left.
578583
didTimeout = true;
@@ -584,19 +589,18 @@ if (typeof window !== 'undefined' && window._schedMock) {
584589
requestAnimationFrameWithTimeout(animationTick);
585590
}
586591
// Exit without invoking the callback.
592+
scheduledHostCallback = prevScheduledCallback;
593+
timeoutTime = prevTimeoutTime;
587594
return;
588595
}
589596
}
590597

591-
timeoutTime = -1;
592-
var callback = scheduledCallback;
593-
scheduledCallback = null;
594-
if (callback !== null) {
595-
isPerformingIdleWork = true;
598+
if (prevScheduledCallback !== null) {
599+
isFlushingHostCallback = true;
596600
try {
597-
callback(didTimeout);
601+
prevScheduledCallback(didTimeout);
598602
} finally {
599-
isPerformingIdleWork = false;
603+
isFlushingHostCallback = false;
600604
}
601605
}
602606
};
@@ -605,7 +609,22 @@ if (typeof window !== 'undefined' && window._schedMock) {
605609
window.addEventListener('message', idleTick, false);
606610

607611
var animationTick = function(rafTime) {
608-
isAnimationFrameScheduled = false;
612+
if (scheduledHostCallback !== null) {
613+
// Eagerly schedule the next animation callback at the beginning of the
614+
// frame. If the scheduler queue is not empty at the end of the frame, it
615+
// will continue flushing inside that callback. If the queue *is* empty,
616+
// then it will exit immediately. Posting the callback at the start of the
617+
// frame ensures it's fired within the earliest possible frame. If we
618+
// waited until the end of the frame to post the callback, we risk the
619+
// browser skipping a frame and not firing the callback until the frame
620+
// after that.
621+
requestAnimationFrameWithTimeout(animationTick);
622+
} else {
623+
// No pending work. Exit.
624+
isAnimationFrameScheduled = false;
625+
return;
626+
}
627+
609628
var nextFrameTime = rafTime - frameDeadline + activeFrameTime;
610629
if (
611630
nextFrameTime < activeFrameTime &&
@@ -629,16 +648,16 @@ if (typeof window !== 'undefined' && window._schedMock) {
629648
previousFrameTime = nextFrameTime;
630649
}
631650
frameDeadline = rafTime + activeFrameTime;
632-
if (!isIdleScheduled) {
633-
isIdleScheduled = true;
651+
if (!isMessageEventScheduled) {
652+
isMessageEventScheduled = true;
634653
window.postMessage(messageKey, '*');
635654
}
636655
};
637656

638657
requestHostCallback = function(callback, absoluteTimeout) {
639-
scheduledCallback = callback;
658+
scheduledHostCallback = callback;
640659
timeoutTime = absoluteTimeout;
641-
if (isPerformingIdleWork || absoluteTimeout < 0) {
660+
if (isFlushingHostCallback || absoluteTimeout < 0) {
642661
// Don't wait for the next frame. Continue working ASAP, in a new event.
643662
window.postMessage(messageKey, '*');
644663
} else if (!isAnimationFrameScheduled) {
@@ -652,8 +671,8 @@ if (typeof window !== 'undefined' && window._schedMock) {
652671
};
653672

654673
cancelHostCallback = function() {
655-
scheduledCallback = null;
656-
isIdleScheduled = false;
674+
scheduledHostCallback = null;
675+
isMessageEventScheduled = false;
657676
timeoutTime = -1;
658677
};
659678
}

packages/scheduler/src/__tests__/SchedulerDOM-test.js

Lines changed: 23 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -50,17 +50,18 @@ describe('SchedulerDOM', () => {
5050
function runRAFCallbacks() {
5151
startOfLatestFrame += frameSize;
5252
currentTime = startOfLatestFrame;
53-
rAFCallbacks.forEach(cb => cb());
53+
const cbs = rAFCallbacks;
5454
rAFCallbacks = [];
55+
cbs.forEach(cb => cb());
5556
}
5657
function advanceOneFrame(config: FrameTimeoutConfigType = {}) {
5758
runRAFCallbacks();
5859
runPostMessageCallbacks(config);
5960
}
6061

6162
let frameSize = 33;
62-
let startOfLatestFrame = Date.now();
63-
let currentTime = Date.now();
63+
let startOfLatestFrame = 0;
64+
let currentTime = 0;
6465

6566
beforeEach(() => {
6667
// TODO pull this into helper method, reduce repetition.
@@ -109,6 +110,25 @@ describe('SchedulerDOM', () => {
109110
expect(typeof cb.mock.calls[0][0].timeRemaining()).toBe('number');
110111
});
111112

113+
it('inserts its rAF callback as early into the queue as possible', () => {
114+
const {unstable_scheduleCallback: scheduleCallback} = Scheduler;
115+
const log = [];
116+
const useRAFCallback = () => {
117+
log.push('userRAFCallback');
118+
};
119+
scheduleCallback(() => {
120+
// Call rAF while idle work is being flushed.
121+
requestAnimationFrame(useRAFCallback);
122+
});
123+
advanceOneFrame({timeLeftInFrame: 1});
124+
// There should be two callbacks: the one scheduled by Scheduler at the
125+
// beginning of the frame, and the one scheduled later during that frame.
126+
expect(rAFCallbacks.length).toBe(2);
127+
// The user callback should be the second callback.
128+
rAFCallbacks[1]();
129+
expect(log).toEqual(['userRAFCallback']);
130+
});
131+
112132
describe('with multiple callbacks', () => {
113133
it('accepts multiple callbacks and calls within frame when not blocked', () => {
114134
const {unstable_scheduleCallback: scheduleCallback} = Scheduler;

0 commit comments

Comments
 (0)