Skip to content

Commit ca990e9

Browse files
authored
Add API to force Scheduler to yield for macrotask (facebook#25044)
We need a way to yield control of the main thread and schedule a continuation in a separate macrotask. (This is related to some Suspense optimizations we have planned.) Our solution needs account for how Scheduler is implemented. Scheduler tasks are not 1:1 with real browser macrotasks — many Scheduler "tasks" can be executed within a single browser task. If a Scheduler task yields control and posts a continuation, but there's still time left in the frame, Scheduler will execute the continuation immediately (synchronously) without yielding control back to the main thread. That's not what we want — we want to schedule a new macrotask regardless of where we are in the browser's render cycle. There are several ways we could approach this. What I ended up doing was adding a new Scheduler method `unstable_requestYield`. (It's similar to the existing `unstable_requestPaint` that we use to yield at the end of the frame.) It works by setting the internal start time of the current work loop to a large negative number, so that when the `shouldYield` call computes how much time has elapsed, it's guaranteed to exceed the deadline. The advantage of doing it this way is that there are no additional checks in the normal hot path of the work loop. The existing layering between Scheduler and React DOM is not ideal. None of the APIs are public, so despite the fact that Scheduler is a separate package, I consider that a private implementation detail, and think of them as part of the same unit. So for now, though, I think it makes sense to implement this macrotask logic directly inside of Scheduler instead of layering it on top. The rough eventual plan for Scheduler is turn it into a `postTask` prollyfill. Because `postTask` does not yet have an equivalent for `shouldYield`, we would split that out into its own layer, perhaps directly inside the reconciler. In that world, the macrotask logic I've added in this commit would likely live in that same layer. When the native `postTask` is available, we may not even need any additional logic because it uses actual browser tasks.
1 parent b4204ed commit ca990e9

9 files changed

+174
-3
lines changed

packages/scheduler/npm/umd/scheduler.development.js

+8
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,13 @@
5454
);
5555
}
5656

57+
function unstable_requestYield() {
58+
return global.React.__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED.Scheduler.unstable_requestYield.apply(
59+
this,
60+
arguments
61+
);
62+
}
63+
5764
function unstable_runWithPriority() {
5865
return global.React.__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED.Scheduler.unstable_runWithPriority.apply(
5966
this,
@@ -116,6 +123,7 @@
116123
unstable_cancelCallback: unstable_cancelCallback,
117124
unstable_shouldYield: unstable_shouldYield,
118125
unstable_requestPaint: unstable_requestPaint,
126+
unstable_requestYield: unstable_requestYield,
119127
unstable_runWithPriority: unstable_runWithPriority,
120128
unstable_next: unstable_next,
121129
unstable_wrapCallback: unstable_wrapCallback,

packages/scheduler/npm/umd/scheduler.production.min.js

+8
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,13 @@
5454
);
5555
}
5656

57+
function unstable_requestYield() {
58+
return global.React.__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED.Scheduler.unstable_requestYield.apply(
59+
this,
60+
arguments
61+
);
62+
}
63+
5764
function unstable_runWithPriority() {
5865
return global.React.__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED.Scheduler.unstable_runWithPriority.apply(
5966
this,
@@ -110,6 +117,7 @@
110117
unstable_cancelCallback: unstable_cancelCallback,
111118
unstable_shouldYield: unstable_shouldYield,
112119
unstable_requestPaint: unstable_requestPaint,
120+
unstable_requestYield: unstable_requestYield,
113121
unstable_runWithPriority: unstable_runWithPriority,
114122
unstable_next: unstable_next,
115123
unstable_wrapCallback: unstable_wrapCallback,

packages/scheduler/npm/umd/scheduler.profiling.min.js

+8
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,13 @@
5454
);
5555
}
5656

57+
function unstable_requestYield() {
58+
return global.React.__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED.Scheduler.unstable_requestYield.apply(
59+
this,
60+
arguments
61+
);
62+
}
63+
5764
function unstable_runWithPriority() {
5865
return global.React.__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED.Scheduler.unstable_runWithPriority.apply(
5966
this,
@@ -110,6 +117,7 @@
110117
unstable_cancelCallback: unstable_cancelCallback,
111118
unstable_shouldYield: unstable_shouldYield,
112119
unstable_requestPaint: unstable_requestPaint,
120+
unstable_requestYield: unstable_requestYield,
113121
unstable_runWithPriority: unstable_runWithPriority,
114122
unstable_next: unstable_next,
115123
unstable_wrapCallback: unstable_wrapCallback,

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

+49
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,8 @@ let performance;
1818
let cancelCallback;
1919
let scheduleCallback;
2020
let requestPaint;
21+
let requestYield;
22+
let shouldYield;
2123
let NormalPriority;
2224

2325
// The Scheduler implementation uses browser APIs like `MessageChannel` and
@@ -42,6 +44,8 @@ describe('SchedulerBrowser', () => {
4244
scheduleCallback = Scheduler.unstable_scheduleCallback;
4345
NormalPriority = Scheduler.unstable_NormalPriority;
4446
requestPaint = Scheduler.unstable_requestPaint;
47+
requestYield = Scheduler.unstable_requestYield;
48+
shouldYield = Scheduler.unstable_shouldYield;
4549
});
4650

4751
afterEach(() => {
@@ -475,4 +479,49 @@ describe('SchedulerBrowser', () => {
475479
'Yield at 5ms',
476480
]);
477481
});
482+
483+
it('requestYield forces a yield immediately', () => {
484+
scheduleCallback(NormalPriority, () => {
485+
runtime.log('Original Task');
486+
runtime.log('shouldYield: ' + shouldYield());
487+
runtime.log('requestYield');
488+
requestYield();
489+
runtime.log('shouldYield: ' + shouldYield());
490+
return () => {
491+
runtime.log('Continuation Task');
492+
runtime.log('shouldYield: ' + shouldYield());
493+
runtime.log('Advance time past frame deadline');
494+
runtime.advanceTime(10000);
495+
runtime.log('shouldYield: ' + shouldYield());
496+
};
497+
});
498+
runtime.assertLog(['Post Message']);
499+
500+
runtime.fireMessageEvent();
501+
runtime.assertLog([
502+
'Message Event',
503+
'Original Task',
504+
'shouldYield: false',
505+
'requestYield',
506+
// Immediately after calling requestYield, shouldYield starts
507+
// returning true, even though no time has elapsed in the frame
508+
'shouldYield: true',
509+
510+
// The continuation should be scheduled in a separate macrotask.
511+
'Post Message',
512+
]);
513+
514+
// No time has elapsed
515+
expect(performance.now()).toBe(0);
516+
517+
// Subsequent tasks work as normal
518+
runtime.fireMessageEvent();
519+
runtime.assertLog([
520+
'Message Event',
521+
'Continuation Task',
522+
'shouldYield: false',
523+
'Advance time past frame deadline',
524+
'shouldYield: true',
525+
]);
526+
});
478527
});

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

+34
Original file line numberDiff line numberDiff line change
@@ -725,5 +725,39 @@ describe('Scheduler', () => {
725725
scheduleCallback(ImmediatePriority, 42);
726726
expect(Scheduler).toFlushWithoutYielding();
727727
});
728+
729+
it('requestYield forces a yield immediately', () => {
730+
scheduleCallback(NormalPriority, () => {
731+
Scheduler.unstable_yieldValue('Original Task');
732+
Scheduler.unstable_yieldValue(
733+
'shouldYield: ' + Scheduler.unstable_shouldYield(),
734+
);
735+
Scheduler.unstable_yieldValue('requestYield');
736+
Scheduler.unstable_requestYield();
737+
Scheduler.unstable_yieldValue(
738+
'shouldYield: ' + Scheduler.unstable_shouldYield(),
739+
);
740+
return () => {
741+
Scheduler.unstable_yieldValue('Continuation Task');
742+
Scheduler.unstable_yieldValue(
743+
'shouldYield: ' + Scheduler.unstable_shouldYield(),
744+
);
745+
Scheduler.unstable_yieldValue('Advance time past frame deadline');
746+
Scheduler.unstable_yieldValue(
747+
'shouldYield: ' + Scheduler.unstable_shouldYield(),
748+
);
749+
};
750+
});
751+
752+
// The continuation should be scheduled in a separate macrotask.
753+
expect(Scheduler).toFlushUntilNextPaint([
754+
'Original Task',
755+
'shouldYield: false',
756+
'requestYield',
757+
// Immediately after calling requestYield, shouldYield starts
758+
// returning true
759+
'shouldYield: true',
760+
]);
761+
});
728762
});
729763
});

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

+49
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,8 @@ let NormalPriority;
2222
let UserBlockingPriority;
2323
let LowPriority;
2424
let IdlePriority;
25+
let shouldYield;
26+
let requestYield;
2527

2628
// The Scheduler postTask implementation uses a new postTask browser API to
2729
// schedule work on the main thread. This test suite mocks all browser methods
@@ -44,6 +46,8 @@ describe('SchedulerPostTask', () => {
4446
NormalPriority = Scheduler.unstable_NormalPriority;
4547
LowPriority = Scheduler.unstable_LowPriority;
4648
IdlePriority = Scheduler.unstable_IdlePriority;
49+
shouldYield = Scheduler.unstable_shouldYield;
50+
requestYield = Scheduler.unstable_requestYield;
4751
});
4852

4953
afterEach(() => {
@@ -296,4 +300,49 @@ describe('SchedulerPostTask', () => {
296300
'E',
297301
]);
298302
});
303+
304+
it('requestYield forces a yield immediately', () => {
305+
scheduleCallback(NormalPriority, () => {
306+
runtime.log('Original Task');
307+
runtime.log('shouldYield: ' + shouldYield());
308+
runtime.log('requestYield');
309+
requestYield();
310+
runtime.log('shouldYield: ' + shouldYield());
311+
return () => {
312+
runtime.log('Continuation Task');
313+
runtime.log('shouldYield: ' + shouldYield());
314+
runtime.log('Advance time past frame deadline');
315+
runtime.advanceTime(10000);
316+
runtime.log('shouldYield: ' + shouldYield());
317+
};
318+
});
319+
runtime.assertLog(['Post Task 0 [user-visible]']);
320+
321+
runtime.flushTasks();
322+
runtime.assertLog([
323+
'Task 0 Fired',
324+
'Original Task',
325+
'shouldYield: false',
326+
'requestYield',
327+
// Immediately after calling requestYield, shouldYield starts
328+
// returning true, even though no time has elapsed in the frame
329+
'shouldYield: true',
330+
331+
// The continuation should be scheduled in a separate macrotask.
332+
'Post Task 1 [user-visible]',
333+
]);
334+
335+
// No time has elapsed
336+
expect(performance.now()).toBe(0);
337+
338+
// Subsequent tasks work as normal
339+
runtime.flushTasks();
340+
runtime.assertLog([
341+
'Task 1 Fired',
342+
'Continuation Task',
343+
'shouldYield: false',
344+
'Advance time past frame deadline',
345+
'shouldYield: true',
346+
]);
347+
});
299348
});

packages/scheduler/src/forks/Scheduler.js

+7-3
Original file line numberDiff line numberDiff line change
@@ -495,6 +495,11 @@ function requestPaint() {
495495
// Since we yield every frame regardless, `requestPaint` has no effect.
496496
}
497497

498+
function requestYield() {
499+
// Force a yield at the next opportunity.
500+
startTime = -99999;
501+
}
502+
498503
function forceFrameRate(fps) {
499504
if (fps < 0 || fps > 125) {
500505
// Using console['error'] to evade Babel and ESLint
@@ -598,8 +603,6 @@ function cancelHostTimeout() {
598603
taskTimeoutID = -1;
599604
}
600605

601-
const unstable_requestPaint = requestPaint;
602-
603606
export {
604607
ImmediatePriority as unstable_ImmediatePriority,
605608
UserBlockingPriority as unstable_UserBlockingPriority,
@@ -613,7 +616,8 @@ export {
613616
unstable_wrapCallback,
614617
unstable_getCurrentPriorityLevel,
615618
shouldYieldToHost as unstable_shouldYield,
616-
unstable_requestPaint,
619+
requestPaint as unstable_requestPaint,
620+
requestYield as unstable_requestYield,
617621
unstable_continueExecution,
618622
unstable_pauseExecution,
619623
unstable_getFirstCallbackNode,

packages/scheduler/src/forks/SchedulerMock.js

+6
Original file line numberDiff line numberDiff line change
@@ -608,6 +608,11 @@ function requestPaint() {
608608
needsPaint = true;
609609
}
610610

611+
function requestYield() {
612+
// Force a yield at the next opportunity.
613+
shouldYieldForPaint = needsPaint = true;
614+
}
615+
611616
export {
612617
ImmediatePriority as unstable_ImmediatePriority,
613618
UserBlockingPriority as unstable_UserBlockingPriority,
@@ -622,6 +627,7 @@ export {
622627
unstable_getCurrentPriorityLevel,
623628
shouldYieldToHost as unstable_shouldYield,
624629
requestPaint as unstable_requestPaint,
630+
requestYield as unstable_requestYield,
625631
unstable_continueExecution,
626632
unstable_pauseExecution,
627633
unstable_getFirstCallbackNode,

packages/scheduler/src/forks/SchedulerPostTask.js

+5
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,11 @@ export function unstable_requestPaint() {
6767
// Since we yield every frame regardless, `requestPaint` has no effect.
6868
}
6969

70+
export function unstable_requestYield() {
71+
// Force a yield at the next opportunity.
72+
deadline = -99999;
73+
}
74+
7075
type SchedulerCallback<T> = (
7176
didTimeout_DEPRECATED: boolean,
7277
) =>

0 commit comments

Comments
 (0)