Skip to content

Commit 35ef78d

Browse files
authored
[Scheduler] Integrated timers (#15911)
Adds a `delay` option to `scheduleCallback`. When specified, the task is not scheduled until after the delay has elapsed. Delayed tasks are scheduled on a timer queue maintained by Scheduler, instead of directly with the browser. The main benefit is to reduce the number of native browser timers that Scheduler's `message` event handler has to contend with; so, after yielding to the browser at the end of the frame, Scheduler will more quickly regain control of the main thread. Because we're able to flush the timer queue without yielding to browser timer events, there's also less task switching overhead (though in the absence of `isInputPending`, this is mostly a theoretical win since we yield every frame regardless). If the queue of non-delayed tasks is non-empty — that is, if there is pending CPU bound work — Scheduler is able to avoid a browser timer entirely by periodically checking its own timer queue while flushing tasks (inside the `message` event handler). Once the CPU-bound work is complete, if there are still pending delayed tasks, Scheduler will schedule a single browser timer that fires once the earliest delay has elapsed.
1 parent 3af91eb commit 35ef78d

4 files changed

Lines changed: 405 additions & 39 deletions

File tree

packages/scheduler/src/Scheduler.js

Lines changed: 167 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,8 @@
1111
import {enableSchedulerDebugging} from './SchedulerFeatureFlags';
1212
import {
1313
requestHostCallback,
14+
requestHostTimeout,
15+
cancelHostTimeout,
1416
shouldYieldToHost,
1517
getCurrentTime,
1618
forceFrameRate,
@@ -39,6 +41,7 @@ var IDLE_PRIORITY = maxSigned31BitInt;
3941

4042
// Tasks are stored as a circular, doubly linked list.
4143
var firstTask = null;
44+
var firstDelayedTask = null;
4245

4346
// Pausing the scheduler is useful for debugging.
4447
var isSchedulerPaused = false;
@@ -50,6 +53,7 @@ var currentPriorityLevel = NormalPriority;
5053
var isPerformingWork = false;
5154

5255
var isHostCallbackScheduled = false;
56+
var isHostTimeoutScheduled = false;
5357

5458
function flushTask(task, currentTime) {
5559
// Remove the task from the list before calling the callback. That way the
@@ -135,37 +139,85 @@ function flushTask(task, currentTime) {
135139
}
136140
}
137141

142+
function advanceTimers(currentTime) {
143+
// Check for tasks that are no longer delayed and add them to the queue.
144+
if (firstDelayedTask !== null && firstDelayedTask.startTime <= currentTime) {
145+
do {
146+
const task = firstDelayedTask;
147+
const next = task.next;
148+
if (task === next) {
149+
firstDelayedTask = null;
150+
} else {
151+
firstDelayedTask = next;
152+
const previous = task.previous;
153+
previous.next = next;
154+
next.previous = previous;
155+
}
156+
task.next = task.previous = null;
157+
insertScheduledTask(task, task.expirationTime);
158+
} while (
159+
firstDelayedTask !== null &&
160+
firstDelayedTask.startTime <= currentTime
161+
);
162+
}
163+
}
164+
165+
function handleTimeout(currentTime) {
166+
isHostTimeoutScheduled = false;
167+
advanceTimers(currentTime);
168+
169+
if (!isHostCallbackScheduled) {
170+
if (firstTask !== null) {
171+
isHostCallbackScheduled = true;
172+
requestHostCallback(flushWork);
173+
} else if (firstDelayedTask !== null) {
174+
requestHostTimeout(
175+
handleTimeout,
176+
firstDelayedTask.startTime - currentTime,
177+
);
178+
}
179+
}
180+
}
181+
138182
function flushWork(hasTimeRemaining, initialTime) {
139183
// Exit right away if we're currently paused
140184
if (enableSchedulerDebugging && isSchedulerPaused) {
141185
return;
142186
}
143187

144-
// We'll need a new host callback the next time work is scheduled.
188+
// We'll need a host callback the next time work is scheduled.
145189
isHostCallbackScheduled = false;
190+
if (isHostTimeoutScheduled) {
191+
// We scheduled a timeout but it's no longer needed. Cancel it.
192+
isHostTimeoutScheduled = false;
193+
cancelHostTimeout();
194+
}
195+
196+
let currentTime = initialTime;
197+
advanceTimers(currentTime);
146198

147199
isPerformingWork = true;
148200
try {
149201
if (!hasTimeRemaining) {
150202
// Flush all the expired callbacks without yielding.
151203
// TODO: Split flushWork into two separate functions instead of using
152204
// a boolean argument?
153-
let currentTime = initialTime;
154205
while (
155206
firstTask !== null &&
156207
firstTask.expirationTime <= currentTime &&
157208
!(enableSchedulerDebugging && isSchedulerPaused)
158209
) {
159210
flushTask(firstTask, currentTime);
160211
currentTime = getCurrentTime();
212+
advanceTimers(currentTime);
161213
}
162214
} else {
163215
// Keep flushing callbacks until we run out of time in the frame.
164-
let currentTime = initialTime;
165216
if (firstTask !== null) {
166217
do {
167218
flushTask(firstTask, currentTime);
168219
currentTime = getCurrentTime();
220+
advanceTimers(currentTime);
169221
} while (
170222
firstTask !== null &&
171223
!shouldYieldToHost() &&
@@ -174,7 +226,17 @@ function flushWork(hasTimeRemaining, initialTime) {
174226
}
175227
}
176228
// Return whether there's additional work
177-
return firstTask !== null;
229+
if (firstTask !== null) {
230+
return true;
231+
} else {
232+
if (firstDelayedTask !== null) {
233+
requestHostTimeout(
234+
handleTimeout,
235+
firstDelayedTask.startTime - currentTime,
236+
);
237+
}
238+
return false;
239+
}
178240
} finally {
179241
isPerformingWork = false;
180242
}
@@ -242,34 +304,41 @@ function unstable_wrapCallback(callback) {
242304
};
243305
}
244306

307+
function timeoutForPriorityLevel(priorityLevel) {
308+
switch (priorityLevel) {
309+
case ImmediatePriority:
310+
return IMMEDIATE_PRIORITY_TIMEOUT;
311+
case UserBlockingPriority:
312+
return USER_BLOCKING_PRIORITY;
313+
case IdlePriority:
314+
return IDLE_PRIORITY;
315+
case LowPriority:
316+
return LOW_PRIORITY_TIMEOUT;
317+
case NormalPriority:
318+
default:
319+
return NORMAL_PRIORITY_TIMEOUT;
320+
}
321+
}
322+
245323
function unstable_scheduleCallback(priorityLevel, callback, options) {
246-
var startTime = getCurrentTime();
324+
var currentTime = getCurrentTime();
247325

326+
var startTime;
248327
var timeout;
249-
if (
250-
typeof options === 'object' &&
251-
options !== null &&
252-
typeof options.timeout === 'number'
253-
) {
254-
timeout = options.timeout;
255-
} else {
256-
switch (priorityLevel) {
257-
case ImmediatePriority:
258-
timeout = IMMEDIATE_PRIORITY_TIMEOUT;
259-
break;
260-
case UserBlockingPriority:
261-
timeout = USER_BLOCKING_PRIORITY;
262-
break;
263-
case IdlePriority:
264-
timeout = IDLE_PRIORITY;
265-
break;
266-
case LowPriority:
267-
timeout = LOW_PRIORITY_TIMEOUT;
268-
break;
269-
case NormalPriority:
270-
default:
271-
timeout = NORMAL_PRIORITY_TIMEOUT;
328+
if (typeof options === 'object' && options !== null) {
329+
var delay = options.delay;
330+
if (typeof delay === 'number' && delay > 0) {
331+
startTime = currentTime + delay;
332+
} else {
333+
startTime = currentTime;
272334
}
335+
timeout =
336+
typeof options.timeout === 'number'
337+
? options.timeout
338+
: timeoutForPriorityLevel(priorityLevel);
339+
} else {
340+
timeout = timeoutForPriorityLevel(priorityLevel);
341+
startTime = currentTime;
273342
}
274343

275344
var expirationTime = startTime + timeout;
@@ -283,6 +352,34 @@ function unstable_scheduleCallback(priorityLevel, callback, options) {
283352
previous: null,
284353
};
285354

355+
if (startTime > currentTime) {
356+
// This is a delayed task.
357+
insertDelayedTask(newTask, startTime);
358+
if (firstTask === null && firstDelayedTask === newTask) {
359+
// All tasks are delayed, and this is the task with the earliest delay.
360+
if (isHostTimeoutScheduled) {
361+
// Cancel an existing timeout.
362+
cancelHostTimeout();
363+
} else {
364+
isHostTimeoutScheduled = true;
365+
}
366+
// Schedule a timeout.
367+
requestHostTimeout(handleTimeout, startTime - currentTime);
368+
}
369+
} else {
370+
insertScheduledTask(newTask, expirationTime);
371+
// Schedule a host callback, if needed. If we're already performing work,
372+
// wait until the next time we yield.
373+
if (!isHostCallbackScheduled && !isPerformingWork) {
374+
isHostCallbackScheduled = true;
375+
requestHostCallback(flushWork);
376+
}
377+
}
378+
379+
return newTask;
380+
}
381+
382+
function insertScheduledTask(newTask, expirationTime) {
286383
// Insert the new task into the list, ordered first by its timeout, then by
287384
// insertion. So the new task is inserted after any other task the
288385
// same timeout
@@ -315,15 +412,39 @@ function unstable_scheduleCallback(priorityLevel, callback, options) {
315412
newTask.next = next;
316413
newTask.previous = previous;
317414
}
415+
}
318416

319-
// Schedule a host callback, if needed. If we're already performing work, wait
320-
// until the next time we yield.
321-
if (!isHostCallbackScheduled && !isPerformingWork) {
322-
isHostCallbackScheduled = true;
323-
requestHostCallback(flushWork);
324-
}
417+
function insertDelayedTask(newTask, startTime) {
418+
// Insert the new task into the list, ordered by its start time.
419+
if (firstDelayedTask === null) {
420+
// This is the first task in the list.
421+
firstDelayedTask = newTask.next = newTask.previous = newTask;
422+
} else {
423+
var next = null;
424+
var task = firstDelayedTask;
425+
do {
426+
if (startTime < task.startTime) {
427+
// The new task times out before this one.
428+
next = task;
429+
break;
430+
}
431+
task = task.next;
432+
} while (task !== firstDelayedTask);
325433

326-
return newTask;
434+
if (next === null) {
435+
// No task with a later timeout was found, which means the new task has
436+
// the latest timeout in the list.
437+
next = firstDelayedTask;
438+
} else if (next === firstDelayedTask) {
439+
// The new task has the earliest expiration in the entire list.
440+
firstDelayedTask = newTask;
441+
}
442+
443+
var previous = next.previous;
444+
previous.next = next.previous = newTask;
445+
newTask.next = next;
446+
newTask.previous = previous;
447+
}
327448
}
328449

329450
function unstable_pauseExecution() {
@@ -349,13 +470,17 @@ function unstable_cancelCallback(task) {
349470
return;
350471
}
351472

352-
if (next === task) {
353-
// This is the only scheduled task. Clear the list.
354-
firstTask = null;
473+
if (task === next) {
474+
if (task === firstTask) {
475+
firstTask = null;
476+
} else if (task === firstDelayedTask) {
477+
firstDelayedTask = null;
478+
}
355479
} else {
356-
// Remove the task from its position in the list.
357480
if (task === firstTask) {
358481
firstTask = next;
482+
} else if (task === firstDelayedTask) {
483+
firstDelayedTask = next;
359484
}
360485
var previous = task.previous;
361486
previous.next = next;
@@ -370,9 +495,12 @@ function unstable_getCurrentPriorityLevel() {
370495
}
371496

372497
function unstable_shouldYield() {
498+
const currentTime = getCurrentTime();
499+
advanceTimers(currentTime);
373500
return (
374501
(currentTask !== null &&
375502
firstTask !== null &&
503+
firstTask.startTime <= currentTime &&
376504
firstTask.expirationTime < currentTask.expirationTime) ||
377505
shouldYieldToHost()
378506
);

0 commit comments

Comments
 (0)