Add SchedulerHostConfig fork for post task (#19470)
Ricky committed
Jul 28, 2020 at 11:11 UTC
52c51462744ac6a9437d64ae84fcd94eacbb8aa8
4 files changed
+270
packages/scheduler/src/forks/SchedulerHostConfig.post-task.js
new
+250
@@ -0,0 +1,250 @@
1
+/**
2
+ * Copyright (c) Facebook, Inc. and its affiliates.
3
+ *
4
+ * This source code is licensed under the MIT license found in the
5
+ * LICENSE file in the root directory of this source tree.
6
+ */
7
+
8
+import {enableIsInputPending} from '../SchedulerFeatureFlags';
9
+
10
+export let requestHostCallback;
11
+export let cancelHostCallback;
12
+export let requestHostTimeout;
13
+export let cancelHostTimeout;
14
+export let shouldYieldToHost;
15
+export let requestPaint;
16
+export let getCurrentTime;
17
+export let forceFrameRate;
18
+
19
+if (
20
+ // If Scheduler runs in a non-DOM environment, it falls back to a naive
21
+ // implementation using setTimeout.
22
+ typeof window === 'undefined' ||
23
+ // Check if MessageChannel is supported, too.
24
+ typeof MessageChannel !== 'function'
25
+) {
26
+ // If this accidentally gets imported in a non-browser environment, e.g. JavaScriptCore,
27
+ // fallback to a naive implementation.
28
+ let _callback = null;
29
+ let _timeoutID = null;
30
+ const _flushCallback = function() {
31
+ if (_callback !== null) {
32
+ try {
33
+ const currentTime = getCurrentTime();
34
+ const hasRemainingTime = true;
35
+ _callback(hasRemainingTime, currentTime);
36
+ _callback = null;
37
+ } catch (e) {
38
+ setTimeout(_flushCallback, 0);
39
+ throw e;
40
+ }
41
+ }
42
+ };
43
+ const initialTime = Date.now();
44
+ getCurrentTime = function() {
45
+ return Date.now() - initialTime;
46
+ };
47
+ requestHostCallback = function(cb) {
48
+ if (_callback !== null) {
49
+ // Protect against re-entrancy.
50
+ setTimeout(requestHostCallback, 0, cb);
51
+ } else {
52
+ _callback = cb;
53
+ setTimeout(_flushCallback, 0);
54
+ }
55
+ };
56
+ cancelHostCallback = function() {
57
+ _callback = null;
58
+ };
59
+ requestHostTimeout = function(cb, ms) {
60
+ _timeoutID = setTimeout(cb, ms);
61
+ };
62
+ cancelHostTimeout = function() {
63
+ clearTimeout(_timeoutID);
64
+ };
65
+ shouldYieldToHost = function() {
66
+ return false;
67
+ };
68
+ requestPaint = forceFrameRate = function() {};
69
+} else {
70
+ // Capture local references to native APIs, in case a polyfill overrides them.
71
+ const performance = window.performance;
72
+ const Date = window.Date;
73
+ const setTimeout = window.setTimeout;
74
+ const clearTimeout = window.clearTimeout;
75
+
76
+ if (typeof console !== 'undefined') {
77
+ // TODO: Scheduler no longer requires these methods to be polyfilled. But
78
+ // maybe we want to continue warning if they don't exist, to preserve the
79
+ // option to rely on it in the future?
80
+ const requestAnimationFrame = window.requestAnimationFrame;
81
+ const cancelAnimationFrame = window.cancelAnimationFrame;
82
+ // TODO: Remove fb.me link
83
+ if (typeof requestAnimationFrame !== 'function') {
84
+ // Using console['error'] to evade Babel and ESLint
85
+ console['error'](
86
+ "This browser doesn't support requestAnimationFrame. " +
87
+ 'Make sure that you load a ' +
88
+ 'polyfill in older browsers. https://fb.me/react-polyfills',
89
+ );
90
+ }
91
+ if (typeof cancelAnimationFrame !== 'function') {
92
+ // Using console['error'] to evade Babel and ESLint
93
+ console['error'](
94
+ "This browser doesn't support cancelAnimationFrame. " +
95
+ 'Make sure that you load a ' +
96
+ 'polyfill in older browsers. https://fb.me/react-polyfills',
97
+ );
98
+ }
99
+ }
100
+
101
+ if (
102
+ typeof performance === 'object' &&
103
+ typeof performance.now === 'function'
104
+ ) {
105
+ getCurrentTime = () => performance.now();
106
+ } else {
107
+ const initialTime = Date.now();
108
+ getCurrentTime = () => Date.now() - initialTime;
109
+ }
110
+
111
+ let isMessageLoopRunning = false;
112
+ let scheduledHostCallback = null;
113
+ let taskTimeoutID = -1;
114
+
115
+ // Scheduler periodically yields in case there is other work on the main
116
+ // thread, like user events. By default, it yields multiple times per frame.
117
+ // It does not attempt to align with frame boundaries, since most tasks don't
118
+ // need to be frame aligned; for those that do, use requestAnimationFrame.
119
+ let yieldInterval = 5;
120
+ let deadline = 0;
121
+
122
+ // TODO: Make this configurable
123
+ // TODO: Adjust this based on priority?
124
+ const maxYieldInterval = 300;
125
+ let needsPaint = false;
126
+
127
+ if (
128
+ enableIsInputPending &&
129
+ navigator !== undefined &&
130
+ navigator.scheduling !== undefined &&
131
+ navigator.scheduling.isInputPending !== undefined
132
+ ) {
133
+ const scheduling = navigator.scheduling;
134
+ shouldYieldToHost = function() {
135
+ const currentTime = getCurrentTime();
136
+ if (currentTime >= deadline) {
137
+ // There's no time left. We may want to yield control of the main
138
+ // thread, so the browser can perform high priority tasks. The main ones
139
+ // are painting and user input. If there's a pending paint or a pending
140
+ // input, then we should yield. But if there's neither, then we can
141
+ // yield less often while remaining responsive. We'll eventually yield
142
+ // regardless, since there could be a pending paint that wasn't
143
+ // accompanied by a call to `requestPaint`, or other main thread tasks
144
+ // like network events.
145
+ if (needsPaint || scheduling.isInputPending()) {
146
+ // There is either a pending paint or a pending input.
147
+ return true;
148
+ }
149
+ // There's no pending input. Only yield if we've reached the max
150
+ // yield interval.
151
+ return currentTime >= maxYieldInterval;
152
+ } else {
153
+ // There's still time left in the frame.
154
+ return false;
155
+ }
156
+ };
157
+
158
+ requestPaint = function() {
159
+ needsPaint = true;
160
+ };
161
+ } else {
162
+ // `isInputPending` is not available. Since we have no way of knowing if
163
+ // there's pending input, always yield at the end of the frame.
164
+ shouldYieldToHost = function() {
165
+ return getCurrentTime() >= deadline;
166
+ };
167
+
168
+ // Since we yield every frame regardless, `requestPaint` has no effect.
169
+ requestPaint = function() {};
170
+ }
171
+
172
+ forceFrameRate = function(fps) {
173
+ if (fps < 0 || fps > 125) {
174
+ // Using console['error'] to evade Babel and ESLint
175
+ console['error'](
176
+ 'forceFrameRate takes a positive int between 0 and 125, ' +
177
+ 'forcing frame rates higher than 125 fps is not unsupported',
178
+ );
179
+ return;
180
+ }
181
+ if (fps > 0) {
182
+ yieldInterval = Math.floor(1000 / fps);
183
+ } else {
184
+ // reset the framerate
185
+ yieldInterval = 5;
186
+ }
187
+ };
188
+
189
+ const performWorkUntilDeadline = () => {
190
+ if (scheduledHostCallback !== null) {
191
+ const currentTime = getCurrentTime();
192
+ // Yield after `yieldInterval` ms, regardless of where we are in the vsync
193
+ // cycle. This means there's always time remaining at the beginning of
194
+ // the message event.
195
+ deadline = currentTime + yieldInterval;
196
+ const hasTimeRemaining = true;
197
+ try {
198
+ const hasMoreWork = scheduledHostCallback(
199
+ hasTimeRemaining,
200
+ currentTime,
201
+ );
202
+ if (!hasMoreWork) {
203
+ isMessageLoopRunning = false;
204
+ scheduledHostCallback = null;
205
+ } else {
206
+ // If there's more work, schedule the next message event at the end
207
+ // of the preceding one.
208
+ port.postMessage(null);
209
+ }
210
+ } catch (error) {
211
+ // If a scheduler task throws, exit the current browser task so the
212
+ // error can be observed.
213
+ port.postMessage(null);
214
+ throw error;
215
+ }
216
+ } else {
217
+ isMessageLoopRunning = false;
218
+ }
219
+ // Yielding to the browser will give it a chance to paint, so we can
220
+ // reset this.
221
+ needsPaint = false;
222
+ };
223
+
224
+ const channel = new MessageChannel();
225
+ const port = channel.port2;
226
+ channel.port1.onmessage = performWorkUntilDeadline;
227
+
228
+ requestHostCallback = function(callback) {
229
+ scheduledHostCallback = callback;
230
+ if (!isMessageLoopRunning) {
231
+ isMessageLoopRunning = true;
232
+ port.postMessage(null);
233
+ }
234
+ };
235
+
236
+ cancelHostCallback = function() {
237
+ scheduledHostCallback = null;
238
+ };
239
+
240
+ requestHostTimeout = function(callback, ms) {
241
+ taskTimeoutID = setTimeout(() => {
242
+ callback(getCurrentTime());
243
+ }, ms);
244
+ };
245
+
246
+ cancelHostTimeout = function() {
247
+ clearTimeout(taskTimeoutID);
248
+ taskTimeoutID = -1;
249
+ };
250
+}
packages/scheduler/unstable_post_task.js
new
+10
@@ -0,0 +1,10 @@
1
+/**
2
+ * Copyright (c) Facebook, Inc. and its affiliates.
3
+ *
4
+ * This source code is licensed under the MIT license found in the
5
+ * LICENSE file in the root directory of this source tree.
6
+ */
7
+
8
+'use strict';
9
+
10
+export * from './src/Scheduler';
scripts/rollup/bundles.js
+8
@@ -634,6 +634,14 @@ const bundles = [
634
externals: [],
635
},
636
637
+ {
638
+ bundleTypes: [FB_WWW_DEV, FB_WWW_PROD, FB_WWW_PROFILING],
639
+ moduleType: ISOMORPHIC,
640
+ entry: 'scheduler/unstable_post_task',
641
+ global: 'SchedulerPostTask',
642
+ externals: [],
643
+ },
644
+
645
/******* Jest React (experimental) *******/
646
{
647
bundleTypes: [NODE_DEV, NODE_PROD],
scripts/rollup/forks.js
+2
@@ -198,6 +198,8 @@ const forks = Object.freeze({
198
entry === 'react-test-renderer'
199
) {
200
return 'scheduler/src/forks/SchedulerHostConfig.mock';
201
+ } else if (entry === 'scheduler/unstable_post_task') {
202
+ return 'scheduler/src/forks/SchedulerHostConfig.post-task';
203
}
204
return 'scheduler/src/forks/SchedulerHostConfig.default';
205
},