[scheduler] Eagerly schedule rAF at beginning of frame (#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
Andrew Clark committed
Oct 8, 2018 at 17:28 UTC
b2cea9078d28015ce95a4da523a4172b171989ad
2 files changed
+61
-22
packages/scheduler/src/Scheduler.js
+38
-19
@@ -534,13 +534,13 @@ if (typeof window !== 'undefined' && window._schedMock) {
534
}
535
}
536
537
- var scheduledCallback = null;
538
- var isIdleScheduled = false;
537
+ var scheduledHostCallback = null;
538
+ var isMessageEventScheduled = false;
539
var timeoutTime = -1;
540
541
var isAnimationFrameScheduled = false;
542
543
- var isPerformingIdleWork = false;
543
+ var isFlushingHostCallback = false;
544
545
var frameDeadline = 0;
546
// We start out assuming that we run at 30fps but then the heuristic tracking
@@ -564,7 +564,12 @@ if (typeof window !== 'undefined' && window._schedMock) {
564
return;
565
}
566
567
- isIdleScheduled = false;
567
+ isMessageEventScheduled = false;
568
+
569
+ var prevScheduledCallback = scheduledHostCallback;
570
+ var prevTimeoutTime = timeoutTime;
571
+ scheduledHostCallback = null;
572
+ timeoutTime = -1;
573
574
var currentTime = getCurrentTime();
575
@@ -572,7 +577,7 @@ if (typeof window !== 'undefined' && window._schedMock) {
577
if (frameDeadline - currentTime <= 0) {
578
// There's no time left in this idle period. Check if the callback has
579
// a timeout and whether it's been exceeded.
575
- if (timeoutTime !== -1 && timeoutTime <= currentTime) {
580
+ if (prevTimeoutTime !== -1 && prevTimeoutTime <= currentTime) {
581
// Exceeded the timeout. Invoke the callback even though there's no
582
// time left.
583
didTimeout = true;
@@ -584,19 +589,18 @@ if (typeof window !== 'undefined' && window._schedMock) {
589
requestAnimationFrameWithTimeout(animationTick);
590
}
591
// Exit without invoking the callback.
592
+ scheduledHostCallback = prevScheduledCallback;
593
+ timeoutTime = prevTimeoutTime;
594
return;
595
}
596
}
597
591
- timeoutTime = -1;
592
- var callback = scheduledCallback;
593
- scheduledCallback = null;
594
- if (callback !== null) {
595
- isPerformingIdleWork = true;
598
+ if (prevScheduledCallback !== null) {
599
+ isFlushingHostCallback = true;
600
try {
597
- callback(didTimeout);
601
+ prevScheduledCallback(didTimeout);
602
} finally {
599
- isPerformingIdleWork = false;
603
+ isFlushingHostCallback = false;
604
}
605
}
606
};
@@ -605,7 +609,22 @@ if (typeof window !== 'undefined' && window._schedMock) {
609
window.addEventListener('message', idleTick, false);
610
611
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
+
628
var nextFrameTime = rafTime - frameDeadline + activeFrameTime;
629
if (
630
nextFrameTime < activeFrameTime &&
@@ -629,16 +648,16 @@ if (typeof window !== 'undefined' && window._schedMock) {
648
previousFrameTime = nextFrameTime;
649
}
650
frameDeadline = rafTime + activeFrameTime;
632
- if (!isIdleScheduled) {
633
- isIdleScheduled = true;
651
+ if (!isMessageEventScheduled) {
652
+ isMessageEventScheduled = true;
653
window.postMessage(messageKey, '*');
654
}
655
};
656
657
requestHostCallback = function(callback, absoluteTimeout) {
639
- scheduledCallback = callback;
658
+ scheduledHostCallback = callback;
659
timeoutTime = absoluteTimeout;
641
- if (isPerformingIdleWork || absoluteTimeout < 0) {
660
+ if (isFlushingHostCallback || absoluteTimeout < 0) {
661
// Don't wait for the next frame. Continue working ASAP, in a new event.
662
window.postMessage(messageKey, '*');
663
} else if (!isAnimationFrameScheduled) {
@@ -652,8 +671,8 @@ if (typeof window !== 'undefined' && window._schedMock) {
671
};
672
673
cancelHostCallback = function() {
655
- scheduledCallback = null;
656
- isIdleScheduled = false;
674
+ scheduledHostCallback = null;
675
+ isMessageEventScheduled = false;
676
timeoutTime = -1;
677
};
678
}
packages/scheduler/src/__tests__/SchedulerDOM-test.js
+23
-3
@@ -50,8 +50,9 @@ describe('SchedulerDOM', () => {
50
function runRAFCallbacks() {
51
startOfLatestFrame += frameSize;
52
currentTime = startOfLatestFrame;
53
- rAFCallbacks.forEach(cb => cb());
53
+ const cbs = rAFCallbacks;
54
rAFCallbacks = [];
55
+ cbs.forEach(cb => cb());
56
}
57
function advanceOneFrame(config: FrameTimeoutConfigType = {}) {
58
runRAFCallbacks();
@@ -59,8 +60,8 @@ describe('SchedulerDOM', () => {
60
}
61
62
let frameSize = 33;
62
- let startOfLatestFrame = Date.now();
63
- let currentTime = Date.now();
63
+ let startOfLatestFrame = 0;
64
+ let currentTime = 0;
65
66
beforeEach(() => {
67
// TODO pull this into helper method, reduce repetition.
@@ -109,6 +110,25 @@ describe('SchedulerDOM', () => {
110
expect(typeof cb.mock.calls[0][0].timeRemaining()).toBe('number');
111
});
112
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
+
132
describe('with multiple callbacks', () => {
133
it('accepts multiple callbacks and calls within frame when not blocked', () => {
134
const {unstable_scheduleCallback: scheduleCallback} = Scheduler;