@samitouri / QOS-React-2 / commits / 0a360642dc

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 ```

Noah Lemen committed Jul 11, 2023 at 14:35 UTC 0a360642dcc08cab469da0a0e4f1050e56b0ef7e
2 files changed +100 -14
packages/scheduler/src/__tests__/SchedulerPostTask-test.js
+81 -2
@@ -94,6 +94,22 @@ describe('SchedulerPostTask', () => {
94 });
95 };
96
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 +
113 global.TaskController = class TaskController {
114 constructor() {
115 this.signal = {_controller: this};
@@ -178,7 +194,7 @@ describe('SchedulerPostTask', () => {
194 'Task 0 Fired',
195 'A',
196 'Yield at 5ms',
181 - 'Post Task 1 [user-visible]',
197 + 'Yield 1 [user-visible]',
198 ]);
199
200 runtime.flushTasks();
@@ -321,7 +337,7 @@ describe('SchedulerPostTask', () => {
337
338 // The continuation should be scheduled in a separate macrotask even
339 // though there's time remaining.
324 - 'Post Task 1 [user-visible]',
340 + 'Yield 1 [user-visible]',
341 ]);
342
343 // No time has elapsed
@@ -330,4 +346,67 @@ describe('SchedulerPostTask', () => {
346 runtime.flushTasks();
347 runtime.assertLog(['Task 1 Fired', 'Continuation Task']);
348 });
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 + });
412 });
packages/scheduler/src/forks/SchedulerPostTask.js
+19 -12
@@ -138,18 +138,25 @@ function runTask<T>(
138 // Update the original callback node's controller, since even though we're
139 // posting a new task, conceptually it's the same one.
140 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 + }
160 }
161 } catch (error) {
162 // We're inside a `postTask` promise. If we don't handle this error, then it