5
5
* LICENSE file in the root directory of this source tree.
6
6
*/
7
7
8
- import { enableIsInputPending } from '../SchedulerFeatureFlags' ;
8
+ import {
9
+ enableIsInputPending ,
10
+ requestIdleCallbackBeforeFirstFrame as requestIdleCallbackBeforeFirstFrameFlag ,
11
+ requestTimerEventBeforeFirstFrame ,
12
+ } from '../SchedulerFeatureFlags' ;
9
13
10
14
// The DOM Scheduler implementation is similar to requestIdleCallback. It
11
15
// works by scheduling a requestAnimationFrame, storing the time for the start
@@ -24,65 +28,6 @@ export let requestPaint;
24
28
export let getCurrentTime ;
25
29
export let forceFrameRate ;
26
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
-
86
31
if (
87
32
// If Scheduler runs in a non-DOM environment, it falls back to a naive
88
33
// implementation using setTimeout.
107
52
}
108
53
}
109
54
} ;
55
+ getCurrentTime = function ( ) {
56
+ return Date . now ( ) ;
57
+ } ;
110
58
requestHostCallback = function ( cb ) {
111
59
if ( _callback !== null ) {
112
60
// Protect against re-entrancy.
@@ -130,16 +78,25 @@ if (
130
78
} ;
131
79
requestPaint = forceFrameRate = function ( ) { } ;
132
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
+
133
90
if ( typeof console !== 'undefined' ) {
134
91
// TODO: Remove fb.me link
135
- if ( typeof localRequestAnimationFrame !== 'function' ) {
92
+ if ( typeof requestAnimationFrame !== 'function' ) {
136
93
console . error (
137
94
"This browser doesn't support requestAnimationFrame. " +
138
95
'Make sure that you load a ' +
139
96
'polyfill in older browsers. https://fb.me/react-polyfills' ,
140
97
) ;
141
98
}
142
- if ( typeof localCancelAnimationFrame !== 'function' ) {
99
+ if ( typeof cancelAnimationFrame !== 'function' ) {
143
100
console . error (
144
101
"This browser doesn't support cancelAnimationFrame. " +
145
102
'Make sure that you load a ' +
@@ -148,19 +105,29 @@ if (
148
105
}
149
106
}
150
107
151
- let scheduledHostCallback = null ;
152
- let isMessageEventScheduled = false ;
108
+ const requestIdleCallbackBeforeFirstFrame =
109
+ requestIdleCallbackBeforeFirstFrameFlag &&
110
+ typeof requestIdleCallback === 'function' &&
111
+ typeof cancelIdleCallback === 'function' ;
153
112
154
- let isAnimationFrameScheduled = false ;
113
+ getCurrentTime =
114
+ typeof performance === 'object' && typeof performance . now === 'function'
115
+ ? ( ) => performance . now ( )
116
+ : ( ) => Date . now ( ) ;
155
117
156
- let timeoutID = - 1 ;
118
+ let isRAFLoopRunning = false ;
119
+ let scheduledHostCallback = null ;
120
+ let rAFTimeoutID = - 1 ;
121
+ let taskTimeoutID = - 1 ;
157
122
158
- let frameDeadline = 0 ;
159
123
// We start out assuming that we run at 30fps but then the heuristic tracking
160
124
// will adjust this value to a faster fps if we get more frequent animation
161
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
+
164
131
let fpsLocked = false ;
165
132
166
133
// TODO: Make this configurable
@@ -222,20 +189,16 @@ if (
222
189
return ;
223
190
}
224
191
if ( fps > 0 ) {
225
- activeFrameTime = Math . floor ( 1000 / fps ) ;
192
+ frameLength = Math . floor ( 1000 / fps ) ;
226
193
fpsLocked = true ;
227
194
} else {
228
195
// reset the framerate
229
- activeFrameTime = 33 ;
196
+ frameLength = 33. 33;
230
197
fpsLocked = false ;
231
198
}
232
199
} ;
233
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 = ( ) => {
239
202
if ( scheduledHostCallback !== null ) {
240
203
const currentTime = getCurrentTime ( ) ;
241
204
const hasTimeRemaining = frameDeadline - currentTime > 0 ;
@@ -244,103 +207,162 @@ if (
244
207
hasTimeRemaining ,
245
208
currentTime ,
246
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 ) {
254
211
scheduledHostCallback = null ;
255
212
}
256
213
} catch ( error ) {
257
214
// If a scheduler task throws, exit the current browser task so the
258
215
// error can be observed, and post a new task as soon as possible
259
216
// so we can continue where we left off.
260
- isMessageEventScheduled = true ;
261
- port . postMessage ( undefined ) ;
217
+ port . postMessage ( null ) ;
262
218
throw error ;
263
219
}
264
- // Yielding to the browser will give it a chance to paint, so we can
265
- // reset this.
266
- needsPaint = false ;
267
220
}
221
+ // Yielding to the browser will give it a chance to paint, so we can
222
+ // reset this.
223
+ needsPaint = false ;
268
224
} ;
269
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).
284
247
return ;
285
248
}
286
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
+ }
297
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 ;
314
296
}
297
+ prevRAFTime = rAFTime ;
298
+ frameDeadline = rAFTime + frameLength ;
299
+
300
+ port . postMessage ( null ) ;
315
301
} ;
316
302
317
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 ) ;
327
350
}
328
351
}
329
352
} ;
330
353
331
354
cancelHostCallback = function ( ) {
332
355
scheduledHostCallback = null ;
333
- isMessageEventScheduled = false ;
334
356
} ;
335
357
336
358
requestHostTimeout = function ( callback , ms ) {
337
- timeoutID = localSetTimeout ( ( ) => {
359
+ taskTimeoutID = setTimeout ( ( ) => {
338
360
callback ( getCurrentTime ( ) ) ;
339
361
} , ms ) ;
340
362
} ;
341
363
342
364
cancelHostTimeout = function ( ) {
343
- localClearTimeout ( timeoutID ) ;
344
- timeoutID = - 1 ;
365
+ clearTimeout ( taskTimeoutID ) ;
366
+ taskTimeoutID = - 1 ;
345
367
} ;
346
368
}
0 commit comments