Skip to content

Commit a7b6422

Browse files
committed
[WIP][Scheduler] Call postTask directly
This updates the experimental Scheduler postTask build to call postTask directly, instead of managing our own custom queue and work loop. We still use a deadline 5ms mechanism to implement `shouldYield`. The main thing that postTask is currently missing is the continuation feature — when yielding to the main thread, the yielding task is sent to the back of the queue, instead of maintaining its position. While this would be nice to have, even without it, postTask may be good enough to replace our userspace implementation. We'll run some tests to see. TODO: Need to update the tests.
1 parent ce37bfa commit a7b6422

File tree

11 files changed

+262
-369
lines changed

11 files changed

+262
-369
lines changed

.eslintrc.js

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -166,6 +166,12 @@ module.exports = {
166166
__webpack_require__: true,
167167
},
168168
},
169+
{
170+
files: ['packages/scheduler/**/*.js'],
171+
globals: {
172+
TaskController: true,
173+
},
174+
},
169175
],
170176

171177
globals: {
Lines changed: 245 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,245 @@
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+
* @flow
8+
*/
9+
10+
import type {PriorityLevel} from './SchedulerPriorities';
11+
12+
declare class TaskController {
13+
constructor(priority?: string): TaskController;
14+
signal: mixed;
15+
abort(): void;
16+
}
17+
18+
type PostTaskPriorityLevel = 'user-blocking' | 'user-visible' | 'background';
19+
type CallbackStatus = 'ok' | 'errored' | 'canceled';
20+
21+
type CallbackNode = {
22+
_controller: TaskController,
23+
_status: CallbackStatus,
24+
};
25+
26+
import {
27+
ImmediatePriority,
28+
UserBlockingPriority,
29+
NormalPriority,
30+
LowPriority,
31+
IdlePriority,
32+
} from './SchedulerPriorities';
33+
34+
export {
35+
ImmediatePriority as unstable_ImmediatePriority,
36+
UserBlockingPriority as unstable_UserBlockingPriority,
37+
NormalPriority as unstable_NormalPriority,
38+
IdlePriority as unstable_IdlePriority,
39+
LowPriority as unstable_LowPriority,
40+
};
41+
42+
// Capture local references to native APIs, in case a polyfill overrides them.
43+
const perf = window.performance;
44+
45+
// Use experimental Chrome Scheduler postTask API.
46+
const scheduler = global.scheduler;
47+
48+
const getCurrentTime = perf.now.bind(perf);
49+
50+
export const unstable_now = getCurrentTime;
51+
52+
// Scheduler periodically yields in case there is other work on the main
53+
// thread, like user events. By default, it yields multiple times per frame.
54+
// It does not attempt to align with frame boundaries, since most tasks don't
55+
// need to be frame aligned; for those that do, use requestAnimationFrame.
56+
const yieldInterval = 5;
57+
let deadline = 0;
58+
59+
let currentPriorityLevel_DEPRECATED = NormalPriority;
60+
61+
// `isInputPending` is not available. Since we have no way of knowing if
62+
// there's pending input, always yield at the end of the frame.
63+
export function unstable_shouldYield() {
64+
return getCurrentTime() >= deadline;
65+
}
66+
67+
export function unstable_requestPaint() {
68+
// Since we yield every frame regardless, `requestPaint` has no effect.
69+
}
70+
71+
type SchedulerCallback<T> = (
72+
didTimeout_DEPRECATED: boolean,
73+
) =>
74+
| T
75+
// May return a continuation
76+
| SchedulerCallback<T>;
77+
78+
export function unstable_scheduleCallback<T>(
79+
priorityLevel: PriorityLevel,
80+
callback: SchedulerCallback<T>,
81+
options?: {delay?: number},
82+
): CallbackNode {
83+
let postTaskPriority;
84+
switch (priorityLevel) {
85+
case ImmediatePriority:
86+
case UserBlockingPriority:
87+
postTaskPriority = 'user-blocking';
88+
break;
89+
case LowPriority:
90+
case NormalPriority:
91+
postTaskPriority = 'user-visible';
92+
break;
93+
case IdlePriority:
94+
postTaskPriority = 'background';
95+
break;
96+
default:
97+
postTaskPriority = 'user-visible';
98+
break;
99+
}
100+
101+
const controller = new TaskController();
102+
const postTaskOptions = {
103+
priority: postTaskPriority,
104+
delay: typeof options === 'object' && options !== null ? options.delay : 0,
105+
signal: controller.signal,
106+
};
107+
108+
const node = {
109+
_controller: controller,
110+
_status: 'ok',
111+
};
112+
113+
scheduler
114+
.postTask(
115+
runTask.bind(null, priorityLevel, postTaskPriority, node, callback),
116+
postTaskOptions,
117+
)
118+
.catch(handlePostTaskError.bind(null, node));
119+
120+
return node;
121+
}
122+
123+
function runTask<T>(
124+
priorityLevel: PriorityLevel,
125+
postTaskPriority: PostTaskPriorityLevel,
126+
node: CallbackNode,
127+
callback: SchedulerCallback<T>,
128+
) {
129+
deadline = getCurrentTime() + yieldInterval;
130+
let result;
131+
try {
132+
currentPriorityLevel_DEPRECATED = priorityLevel;
133+
const didTimeout_DEPRECATED = false;
134+
result = callback(didTimeout_DEPRECATED);
135+
} catch (error) {
136+
node._status = 'errored';
137+
throw error;
138+
} finally {
139+
currentPriorityLevel_DEPRECATED = NormalPriority;
140+
}
141+
if (typeof result === 'function') {
142+
// Assume this is a continuation
143+
const continuation: SchedulerCallback<T> = (result: any);
144+
const continuationController = new TaskController();
145+
const continuationOptions = {
146+
priority: postTaskPriority,
147+
signal: continuationController.signal,
148+
};
149+
// Update the original callback node's controller, since even though we're
150+
// posting a new task, conceptually it's the same one.
151+
node._controller = continuationController;
152+
scheduler
153+
.postTask(
154+
runTask.bind(null, priorityLevel, postTaskPriority, node, continuation),
155+
continuationOptions,
156+
)
157+
.catch(handlePostTaskError.bind(null, node));
158+
}
159+
}
160+
161+
function handlePostTaskError(node, error) {
162+
if (node._status === 'canceled') {
163+
// This task was canceled by unstable_cancelCallback, and there were no
164+
// other user errors. So this must be an AbortError. Suppress it.
165+
} else {
166+
// Otherwise, surface to the user.
167+
throw error;
168+
}
169+
}
170+
171+
export function unstable_cancelCallback(node: CallbackNode) {
172+
if (node._status === 'ok') {
173+
// Unless the task already failed due to a user error, mark it as canceled.
174+
node._status = 'canceled';
175+
}
176+
const controller = node._controller;
177+
controller.abort();
178+
}
179+
180+
export function unstable_runWithPriority<T>(
181+
priorityLevel: PriorityLevel,
182+
callback: () => T,
183+
): T {
184+
const previousPriorityLevel = currentPriorityLevel_DEPRECATED;
185+
currentPriorityLevel_DEPRECATED = priorityLevel;
186+
try {
187+
return callback();
188+
} finally {
189+
currentPriorityLevel_DEPRECATED = previousPriorityLevel;
190+
}
191+
}
192+
193+
export function unstable_getCurrentPriorityLevel() {
194+
return currentPriorityLevel_DEPRECATED;
195+
}
196+
197+
export function unstable_next<T>(callback: () => T): T {
198+
let priorityLevel;
199+
switch (currentPriorityLevel_DEPRECATED) {
200+
case ImmediatePriority:
201+
case UserBlockingPriority:
202+
case NormalPriority:
203+
// Shift down to normal priority
204+
priorityLevel = NormalPriority;
205+
break;
206+
default:
207+
// Anything lower than normal priority should remain at the current level.
208+
priorityLevel = currentPriorityLevel_DEPRECATED;
209+
break;
210+
}
211+
212+
const previousPriorityLevel = currentPriorityLevel_DEPRECATED;
213+
currentPriorityLevel_DEPRECATED = priorityLevel;
214+
try {
215+
return callback();
216+
} finally {
217+
currentPriorityLevel_DEPRECATED = previousPriorityLevel;
218+
}
219+
}
220+
221+
export function unstable_wrapCallback<T>(callback: () => T): () => T {
222+
const parentPriorityLevel = currentPriorityLevel_DEPRECATED;
223+
return () => {
224+
const previousPriorityLevel = currentPriorityLevel_DEPRECATED;
225+
currentPriorityLevel_DEPRECATED = parentPriorityLevel;
226+
try {
227+
return callback();
228+
} finally {
229+
currentPriorityLevel_DEPRECATED = previousPriorityLevel;
230+
}
231+
};
232+
}
233+
234+
export function unstable_forceFrameRate() {}
235+
236+
export function unstable_pauseExecution() {}
237+
238+
export function unstable_continueExecution() {}
239+
240+
export function unstable_getFirstCallbackNode() {
241+
return null;
242+
}
243+
244+
// Currently no profiling build
245+
export const unstable_Profiling = null;

0 commit comments

Comments
 (0)