@samitouri / QOS-React / commits / b2cea9078d

[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;