5
* LICENSE file in the root directory of this source tree.
6
*/
7
8
-import {enableIsInputPending} from '../SchedulerFeatureFlags';
8
+import {
9
+ enableIsInputPending,
10
+ requestIdleCallbackBeforeFirstFrame as requestIdleCallbackBeforeFirstFrameFlag,
11
+ requestTimerEventBeforeFirstFrame,
12
+} from '../SchedulerFeatureFlags';
13
14
// The DOM Scheduler implementation is similar to requestIdleCallback. It
15
// works by scheduling a requestAnimationFrame, storing the time for the start
28
export let getCurrentTime;
29
export let forceFrameRate;
30
27
-const hasNativePerformanceNow =
28
- typeof performance === 'object' && typeof performance.now === 'function';
29
-
30
-// We capture a local reference to any global, in case it gets polyfilled after
31
-// this module is initially evaluated. We want to be using a
32
-// consistent implementation.
33
-const localDate = Date;
34
-
35
-// This initialization code may run even on server environments if a component
36
-// just imports ReactDOM (e.g. for findDOMNode). Some environments might not
37
-// have setTimeout or clearTimeout. However, we always expect them to be defined
38
-// on the client. https://github.com/facebook/react/pull/13088
39
-const localSetTimeout =
40
- typeof setTimeout === 'function' ? setTimeout : undefined;
41
-const localClearTimeout =
42
- typeof clearTimeout === 'function' ? clearTimeout : undefined;
43
-
44
-// We don't expect either of these to necessarily be defined, but we will error
45
-// later if they are missing on the client.
46
-const localRequestAnimationFrame =
47
- typeof requestAnimationFrame === 'function'
48
- ? requestAnimationFrame
49
- : undefined;
50
-const localCancelAnimationFrame =
51
- typeof cancelAnimationFrame === 'function' ? cancelAnimationFrame : undefined;
52
-
53
-// requestAnimationFrame does not run when the tab is in the background. If
54
-// we're backgrounded we prefer for that work to happen so that the page
55
-// continues to load in the background. So we also schedule a 'setTimeout' as
56
-// a fallback.
57
-// TODO: Need a better heuristic for backgrounded work.
58
-const ANIMATION_FRAME_TIMEOUT = 100;
59
-let rAFID;
60
-let rAFTimeoutID;
61
-const requestAnimationFrameWithTimeout = function(callback) {
62
- // schedule rAF and also a setTimeout
63
- rAFID = localRequestAnimationFrame(function(timestamp) {
64
- // cancel the setTimeout
65
- localClearTimeout(rAFTimeoutID);
66
- callback(timestamp);
67
- });
68
- rAFTimeoutID = localSetTimeout(function() {
69
- // cancel the requestAnimationFrame
70
- localCancelAnimationFrame(rAFID);
71
- callback(getCurrentTime());
72
- }, ANIMATION_FRAME_TIMEOUT);
73
-};
74
-
75
-if (hasNativePerformanceNow) {
76
- const Performance = performance;
77
- getCurrentTime = function() {
78
- return Performance.now();
79
- };
80
-} else {
81
- getCurrentTime = function() {
82
- return localDate.now();
83
- };
84
-}
85
-
31
if (
32
// If Scheduler runs in a non-DOM environment, it falls back to a naive
33
// implementation using setTimeout.
52
}
53
}
54
};
55
+ getCurrentTime = function() {
56
+ return Date.now();
57
+ };
58
requestHostCallback = function(cb) {
59
if (_callback !== null) {
60
// Protect against re-entrancy.
78
};
79
requestPaint = forceFrameRate = function() {};
80
} else {
81
+ // Capture local references to native APIs, in case a polyfill overrides them.
82
+ const performance = window.performance;
83
+ const Date = window.Date;
84
+ const setTimeout = window.setTimeout;
85
+ const clearTimeout = window.clearTimeout;
86
+ const requestAnimationFrame = window.requestAnimationFrame;
87
+ const cancelAnimationFrame = window.cancelAnimationFrame;
88
+ const requestIdleCallback = window.requestIdleCallback;
89
+
90
if (typeof console !== 'undefined') {
91
// TODO: Remove fb.me link
135
- if (typeof localRequestAnimationFrame !== 'function') {
92
+ if (typeof requestAnimationFrame !== 'function') {
93
console.error(
94
"This browser doesn't support requestAnimationFrame. " +
95
'Make sure that you load a ' +
96
'polyfill in older browsers. https://fb.me/react-polyfills',
97
);
98
}
142
- if (typeof localCancelAnimationFrame !== 'function') {
99
+ if (typeof cancelAnimationFrame !== 'function') {
100
console.error(
101
"This browser doesn't support cancelAnimationFrame. " +
102
'Make sure that you load a ' +
105
}
106
}
107
151
- let scheduledHostCallback = null;
152
- let isMessageEventScheduled = false;
108
+ const requestIdleCallbackBeforeFirstFrame =
109
+ requestIdleCallbackBeforeFirstFrameFlag &&
110
+ typeof requestIdleCallback === 'function' &&
111
+ typeof cancelIdleCallback === 'function';
112
154
- let isAnimationFrameScheduled = false;
113
+ getCurrentTime =
114
+ typeof performance === 'object' && typeof performance.now === 'function'
115
+ ? () => performance.now()
116
+ : () => Date.now();
117
156
- let timeoutID = -1;
118
+ let isRAFLoopRunning = false;
119
+ let scheduledHostCallback = null;
120
+ let rAFTimeoutID = -1;
121
+ let taskTimeoutID = -1;
122
158
- let frameDeadline = 0;
123
// We start out assuming that we run at 30fps but then the heuristic tracking
124
// will adjust this value to a faster fps if we get more frequent animation
125
// frames.
162
- let previousFrameTime = 33;
163
- let activeFrameTime = 33;
126
+ let frameLength = 33.33;
127
+ let prevRAFTime = -1;
128
+ let prevRAFInterval = -1;
129
+ let frameDeadline = 0;
130
+
131
let fpsLocked = false;
132
133
// TODO: Make this configurable
189
return;
190
}
191
if (fps > 0) {
225
- activeFrameTime = Math.floor(1000 / fps);
192
+ frameLength = Math.floor(1000 / fps);
193
fpsLocked = true;
194
} else {
195
// reset the framerate
229
- activeFrameTime = 33;
196
+ frameLength = 33.33;
197
fpsLocked = false;
198
}
199
};
200
234
- // We use the postMessage trick to defer idle work until after the repaint.
235
- const channel = new MessageChannel();
236
- const port = channel.port2;
237
- channel.port1.onmessage = function(event) {
238
- isMessageEventScheduled = false;
201
+ const performWorkUntilDeadline = () => {
202
if (scheduledHostCallback !== null) {
203
const currentTime = getCurrentTime();
204
const hasTimeRemaining = frameDeadline - currentTime > 0;
207
hasTimeRemaining,
208
currentTime,
209
);
247
- if (hasMoreWork) {
248
- // Ensure the next frame is scheduled.
249
- if (!isAnimationFrameScheduled) {
250
- isAnimationFrameScheduled = true;
251
- requestAnimationFrameWithTimeout(animationTick);
252
- }
253
- } else {
210
+ if (!hasMoreWork) {
211
scheduledHostCallback = null;
212
}
213
} catch (error) {
214
// If a scheduler task throws, exit the current browser task so the
215
// error can be observed, and post a new task as soon as possible
216
// so we can continue where we left off.
260
- isMessageEventScheduled = true;
261
- port.postMessage(undefined);
217
+ port.postMessage(null);
218
throw error;
219
}
264
- // Yielding to the browser will give it a chance to paint, so we can
265
- // reset this.
266
- needsPaint = false;
220
}
221
+ // Yielding to the browser will give it a chance to paint, so we can
222
+ // reset this.
223
+ needsPaint = false;
224
};
225
270
- const animationTick = function(rafTime) {
271
- if (scheduledHostCallback !== null) {
272
- // Eagerly schedule the next animation callback at the beginning of the
273
- // frame. If the scheduler queue is not empty at the end of the frame, it
274
- // will continue flushing inside that callback. If the queue *is* empty,
275
- // then it will exit immediately. Posting the callback at the start of the
276
- // frame ensures it's fired within the earliest possible frame. If we
277
- // waited until the end of the frame to post the callback, we risk the
278
- // browser skipping a frame and not firing the callback until the frame
279
- // after that.
280
- requestAnimationFrameWithTimeout(animationTick);
281
- } else {
282
- // No pending work. Exit.
283
- isAnimationFrameScheduled = false;
226
+ // We use the postMessage trick to defer idle work until after the repaint.
227
+ const channel = new MessageChannel();
228
+ const port = channel.port2;
229
+ channel.port1.onmessage = performWorkUntilDeadline;
230
+
231
+ const onAnimationFrame = rAFTime => {
232
+ if (scheduledHostCallback === null) {
233
+ // No scheduled work. Exit.
234
+ isRAFLoopRunning = false;
235
+ prevRAFTime = -1;
236
+ prevRAFInterval = -1;
237
+ return;
238
+ }
239
+ if (rAFTime - prevRAFTime < 0.1) {
240
+ // Defensive coding. Received two rAFs in the same frame. Exit and wait
241
+ // for the next frame.
242
+ // TODO: This could be an indication that the frame rate is too high. We
243
+ // don't currently handle the case where the browser dynamically lowers
244
+ // the framerate, e.g. in low power situation (other than the rAF timeout,
245
+ // but that's designed for when the tab is backgrounded and doesn't
246
+ // optimize for maxiumum CPU utilization).
247
return;
248
}
249
287
- let nextFrameTime = rafTime - frameDeadline + activeFrameTime;
288
- if (
289
- nextFrameTime < activeFrameTime &&
290
- previousFrameTime < activeFrameTime &&
291
- !fpsLocked
292
- ) {
293
- if (nextFrameTime < 8) {
294
- // Defensive coding. We don't support higher frame rates than 120hz.
295
- // If the calculated frame time gets lower than 8, it is probably a bug.
296
- nextFrameTime = 8;
250
+ // Eagerly schedule the next animation callback at the beginning of the
251
+ // frame. If the scheduler queue is not empty at the end of the frame, it
252
+ // will continue flushing inside that callback. If the queue *is* empty,
253
+ // then it will exit immediately. Posting the callback at the start of the
254
+ // frame ensures it's fired within the earliest possible frame. If we
255
+ // waited until the end of the frame to post the callback, we risk the
256
+ // browser skipping a frame and not firing the callback until the frame
257
+ // after that.
258
+ requestAnimationFrame(nextRAFTime => {
259
+ clearTimeout(rAFTimeoutID);
260
+ onAnimationFrame(nextRAFTime);
261
+ });
262
+ // requestAnimationFrame is throttled when the tab is backgrounded. We
263
+ // don't want to stop working entirely. So we'll fallback to a timeout loop.
264
+ // TODO: Need a better heuristic for backgrounded work.
265
+ const onTimeout = () => {
266
+ frameDeadline = getCurrentTime() + frameLength / 2;
267
+ performWorkUntilDeadline();
268
+ rAFTimeoutID = setTimeout(onTimeout, frameLength * 3);
269
+ };
270
+ rAFTimeoutID = setTimeout(onTimeout, frameLength * 3);
271
+
272
+ if (prevRAFTime !== -1) {
273
+ const rAFInterval = rAFTime - prevRAFTime;
274
+ if (!fpsLocked && prevRAFInterval !== -1) {
275
+ // We've observed two consecutive frame intervals. We'll use this to
276
+ // dynamically adjust the frame rate.
277
+ //
278
+ // If one frame goes long, then the next one can be short to catch up.
279
+ // If two frames are short in a row, then that's an indication that we
280
+ // actually have a higher frame rate than what we're currently
281
+ // optimizing. For example, if we're running on 120hz display or 90hz VR
282
+ // display. Take the max of the two in case one of them was an anomaly
283
+ // due to missed frame deadlines.
284
+ if (rAFInterval < frameLength && prevRAFInterval < frameLength) {
285
+ frameLength =
286
+ rAFInterval < prevRAFInterval ? prevRAFInterval : rAFInterval;
287
+ if (frameLength < 8.33) {
288
+ // Defensive coding. We don't support higher frame rates than 120hz.
289
+ // If the calculated frame length gets lower than 8, it is probably
290
+ // a bug.
291
+ frameLength = 8.33;
292
+ }
293
+ }
294
}
298
- // If one frame goes long, then the next one can be short to catch up.
299
- // If two frames are short in a row, then that's an indication that we
300
- // actually have a higher frame rate than what we're currently optimizing.
301
- // We adjust our heuristic dynamically accordingly. For example, if we're
302
- // running on 120hz display or 90hz VR display.
303
- // Take the max of the two in case one of them was an anomaly due to
304
- // missed frame deadlines.
305
- activeFrameTime =
306
- nextFrameTime < previousFrameTime ? previousFrameTime : nextFrameTime;
307
- } else {
308
- previousFrameTime = nextFrameTime;
309
- }
310
- frameDeadline = rafTime + activeFrameTime;
311
- if (!isMessageEventScheduled) {
312
- isMessageEventScheduled = true;
313
- port.postMessage(undefined);
295
+ prevRAFInterval = rAFInterval;
296
}
297
+ prevRAFTime = rAFTime;
298
+ frameDeadline = rAFTime + frameLength;
299
+
300
+ port.postMessage(null);
301
};
302
303
requestHostCallback = function(callback) {
318
- if (scheduledHostCallback === null) {
319
- scheduledHostCallback = callback;
320
- if (!isAnimationFrameScheduled) {
321
- // If rAF didn't already schedule one, we need to schedule a frame.
322
- // TODO: If this rAF doesn't materialize because the browser throttles,
323
- // we might want to still have setTimeout trigger rIC as a backup to
324
- // ensure that we keep performing work.
325
- isAnimationFrameScheduled = true;
326
- requestAnimationFrameWithTimeout(animationTick);
304
+ scheduledHostCallback = callback;
305
+ if (!isRAFLoopRunning) {
306
+ isRAFLoopRunning = true;
307
+
308
+ // Start a rAF loop.
309
+ requestAnimationFrame(rAFTime => {
310
+ if (requestIdleCallbackBeforeFirstFrame) {
311
+ cancelIdleCallback(idleCallbackID);
312
+ }
313
+ if (requestTimerEventBeforeFirstFrame) {
314
+ clearTimeout(idleTimeoutID);
315
+ }
316
+ onAnimationFrame(rAFTime);
317
+ });
318
+
319
+ // If we just missed the last vsync, the next rAF might not happen for
320
+ // another frame. To claim as much idle time as possible, post a callback
321
+ // with `requestIdleCallback`, which should fire if there's idle time left
322
+ // in the frame.
323
+ //
324
+ // This should only be an issue for the first rAF in the loop; subsequent
325
+ // rAFs are scheduled at the beginning of the preceding frame.
326
+ let idleCallbackID;
327
+ if (requestIdleCallbackBeforeFirstFrame) {
328
+ idleCallbackID = requestIdleCallback(() => {
329
+ if (requestTimerEventBeforeFirstFrame) {
330
+ clearTimeout(idleTimeoutID);
331
+ }
332
+ frameDeadline = getCurrentTime() + frameLength;
333
+ performWorkUntilDeadline();
334
+ });
335
+ }
336
+ // Alternate strategy to address the same problem. Scheduler a timer with
337
+ // no delay. If this fires before the rAF, that likely indicates that
338
+ // there's idle time before the next vsync. This isn't always the case,
339
+ // but we'll be aggressive and assume it is, as a trade off to prevent
340
+ // idle periods.
341
+ let idleTimeoutID;
342
+ if (requestTimerEventBeforeFirstFrame) {
343
+ idleTimeoutID = setTimeout(() => {
344
+ if (requestIdleCallbackBeforeFirstFrame) {
345
+ cancelIdleCallback(idleCallbackID);
346
+ }
347
+ frameDeadline = getCurrentTime() + frameLength;
348
+ performWorkUntilDeadline();
349
+ }, 0);
350
}
351
}
352
};
353
354
cancelHostCallback = function() {
355
scheduledHostCallback = null;
333
- isMessageEventScheduled = false;
356
};
357
358
requestHostTimeout = function(callback, ms) {
337
- timeoutID = localSetTimeout(() => {
359
+ taskTimeoutID = setTimeout(() => {
360
callback(getCurrentTime());
361
}, ms);
362
};
363
364
cancelHostTimeout = function() {
343
- localClearTimeout(timeoutID);
344
- timeoutID = -1;
365
+ clearTimeout(taskTimeoutID);
366
+ taskTimeoutID = -1;
367
};
368
}