Skip to content

Commit 52c5146

Browse files
authored
Add SchedulerHostConfig fork for post task (facebook#19470)
1 parent 722bc04 commit 52c5146

File tree

4 files changed

+270
-0
lines changed

4 files changed

+270
-0
lines changed
Lines changed: 250 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -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+
}
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -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

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -634,6 +634,14 @@ const bundles = [
634634
externals: [],
635635
},
636636

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+
637645
/******* Jest React (experimental) *******/
638646
{
639647
bundleTypes: [NODE_DEV, NODE_PROD],

scripts/rollup/forks.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -198,6 +198,8 @@ const forks = Object.freeze({
198198
entry === 'react-test-renderer'
199199
) {
200200
return 'scheduler/src/forks/SchedulerHostConfig.mock';
201+
} else if (entry === 'scheduler/unstable_post_task') {
202+
return 'scheduler/src/forks/SchedulerHostConfig.post-task';
201203
}
202204
return 'scheduler/src/forks/SchedulerHostConfig.default';
203205
},

0 commit comments

Comments
 (0)