Skip to content

Commit 0a36064

Browse files
authored
integrate scheduler.yield in SchedulerPostTask (#27069)
## Summary `scheduler.yield` is entering [Origin Trial soon in Chrome 115](https://chromestatus.com/feature/6266249336586240). This diff adds it to `SchedulerPostTask` when scheduling continuations to allow Origin Trial participation for early feedback on the new API. It seems the difference here versus the current use of `postTask` will be minor – the intent behind `scheduler.yield` seems to mostly be better ergonomics for scheduling continuations, but it may be interesting to see if the follow aspect of it results in any tangible difference in scheduling (from [here](https://github.com/WICG/scheduling-apis/blob/main/explainers/yield-and-continuation.md#introduction)): > To mitigate yielding performance penalty concerns, UAs prioritize scheduler.yield() continuations over tasks of the same priority or similar task sources. ## How did you test this change? ``` yarn test SchedulerPostTask ```
1 parent fdc8c81 commit 0a36064

File tree

2 files changed

+100
-14
lines changed

2 files changed

+100
-14
lines changed

packages/scheduler/src/__tests__/SchedulerPostTask-test.js

Lines changed: 81 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -94,6 +94,22 @@ describe('SchedulerPostTask', () => {
9494
});
9595
};
9696

97+
scheduler.yield = function ({priority, signal}) {
98+
const id = idCounter++;
99+
log(`Yield ${id} [${priority === undefined ? '<default>' : priority}]`);
100+
const controller = signal._controller;
101+
let callback;
102+
103+
return {
104+
then(cb) {
105+
callback = cb;
106+
return new Promise((resolve, reject) => {
107+
taskQueue.set(controller, {id, callback, resolve, reject});
108+
});
109+
},
110+
};
111+
};
112+
97113
global.TaskController = class TaskController {
98114
constructor() {
99115
this.signal = {_controller: this};
@@ -178,7 +194,7 @@ describe('SchedulerPostTask', () => {
178194
'Task 0 Fired',
179195
'A',
180196
'Yield at 5ms',
181-
'Post Task 1 [user-visible]',
197+
'Yield 1 [user-visible]',
182198
]);
183199

184200
runtime.flushTasks();
@@ -321,7 +337,7 @@ describe('SchedulerPostTask', () => {
321337

322338
// The continuation should be scheduled in a separate macrotask even
323339
// though there's time remaining.
324-
'Post Task 1 [user-visible]',
340+
'Yield 1 [user-visible]',
325341
]);
326342

327343
// No time has elapsed
@@ -330,4 +346,67 @@ describe('SchedulerPostTask', () => {
330346
runtime.flushTasks();
331347
runtime.assertLog(['Task 1 Fired', 'Continuation Task']);
332348
});
349+
350+
describe('falls back to postTask for scheduling continuations when scheduler.yield is not available', () => {
351+
beforeEach(() => {
352+
delete global.scheduler.yield;
353+
});
354+
355+
it('task with continuation', () => {
356+
scheduleCallback(NormalPriority, () => {
357+
runtime.log('A');
358+
while (!Scheduler.unstable_shouldYield()) {
359+
runtime.advanceTime(1);
360+
}
361+
runtime.log(`Yield at ${performance.now()}ms`);
362+
return () => {
363+
runtime.log('Continuation');
364+
};
365+
});
366+
runtime.assertLog(['Post Task 0 [user-visible]']);
367+
368+
runtime.flushTasks();
369+
runtime.assertLog([
370+
'Task 0 Fired',
371+
'A',
372+
'Yield at 5ms',
373+
'Post Task 1 [user-visible]',
374+
]);
375+
376+
runtime.flushTasks();
377+
runtime.assertLog(['Task 1 Fired', 'Continuation']);
378+
});
379+
380+
it('yielding continues in a new task regardless of how much time is remaining', () => {
381+
scheduleCallback(NormalPriority, () => {
382+
runtime.log('Original Task');
383+
runtime.log('shouldYield: ' + shouldYield());
384+
runtime.log('Return a continuation');
385+
return () => {
386+
runtime.log('Continuation Task');
387+
};
388+
});
389+
runtime.assertLog(['Post Task 0 [user-visible]']);
390+
391+
runtime.flushTasks();
392+
runtime.assertLog([
393+
'Task 0 Fired',
394+
'Original Task',
395+
// Immediately before returning a continuation, `shouldYield` returns
396+
// false, which means there must be time remaining in the frame.
397+
'shouldYield: false',
398+
'Return a continuation',
399+
400+
// The continuation should be scheduled in a separate macrotask even
401+
// though there's time remaining.
402+
'Post Task 1 [user-visible]',
403+
]);
404+
405+
// No time has elapsed
406+
expect(performance.now()).toBe(0);
407+
408+
runtime.flushTasks();
409+
runtime.assertLog(['Task 1 Fired', 'Continuation Task']);
410+
});
411+
});
333412
});

packages/scheduler/src/forks/SchedulerPostTask.js

Lines changed: 19 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -138,18 +138,25 @@ function runTask<T>(
138138
// Update the original callback node's controller, since even though we're
139139
// posting a new task, conceptually it's the same one.
140140
node._controller = continuationController;
141-
scheduler
142-
.postTask(
143-
runTask.bind(
144-
null,
145-
priorityLevel,
146-
postTaskPriority,
147-
node,
148-
continuation,
149-
),
150-
continuationOptions,
151-
)
152-
.catch(handleAbortError);
141+
142+
const nextTask = runTask.bind(
143+
null,
144+
priorityLevel,
145+
postTaskPriority,
146+
node,
147+
continuation,
148+
);
149+
150+
if (scheduler.yield !== undefined) {
151+
scheduler
152+
.yield(continuationOptions)
153+
.then(nextTask)
154+
.catch(handleAbortError);
155+
} else {
156+
scheduler
157+
.postTask(nextTask, continuationOptions)
158+
.catch(handleAbortError);
159+
}
153160
}
154161
} catch (error) {
155162
// We're inside a `postTask` promise. If we don't handle this error, then it

0 commit comments

Comments
 (0)