@samitouri / QOS-React / commits / 52c5146274

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 },