Skip to content

Commit c80b336

Browse files
authored
Implement requestPaint in the actual scheduler (facebook#31784)
When implementing passive effects we did a pretty massive oversight. While the passive effect is scheduled into its own scheduler task, the scheduler doesn't always yield to the browser if it has time left. That means that if you have a fast commit phase, it might try to squeeze in the passive effects in the same frame but those then might end being very heavy. We had `requestPaint()` for this but that was only implemented for the `isInputPending` experiment. It wasn't thought we needed it for the regular scheduler because it yields "every frame" anyway - but it doesn't yield every task. While the `isInputPending` experiment showed that it wasn't actually any significant impact, and it was better to keep shorter yield time anyway. Which is why we deleted the code. Whatever small win it did see in some cases might have been actually due to this issue rather than anything to do with `isInputPending` at all. As you can see in facebook#31782 we do have this implemented in the mock scheduler and a lot of behavior that we assert assumes that this works. So this just implements yielding after `requestPaint` is called. Before: <img width="1023" alt="Screenshot 2024-12-14 at 3 40 24 PM" src="https://github.com/user-attachments/assets/d60f4bb2-c8f8-4f91-a402-9ac25b278450" /> After: <img width="1108" alt="Screenshot 2024-12-14 at 3 41 25 PM" src="https://github.com/user-attachments/assets/170cdb90-a049-436f-9501-be3fb9bc04ca" /> Notice how in after the native task is split into two. It might not always actually paint and the native scheduler might make the same mistake and think it has enough time left but it's at least less likely to. We do have another way to do this. When we yield a continuation we also yield to the native browser. This is to enable the Suspense Optimization (currently disabled) to work. We could do the same for passive effects and, in fact, I have a branch that does but because that requires a lot more tests to be fixed it's a lot more invasive of a change. The nice thing about this approach is that this is not even running in tests at all and the tests we do have assert that this is the behavior already. 😬
1 parent c32780e commit c80b336

File tree

2 files changed

+12
-3
lines changed

2 files changed

+12
-3
lines changed

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

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -183,7 +183,7 @@ describe('SchedulerBrowser', () => {
183183
it('task with continuation', () => {
184184
scheduleCallback(NormalPriority, () => {
185185
runtime.log('Task');
186-
// Request paint so that we yield at the end of the frame interval
186+
// Request paint so that we yield immediately
187187
requestPaint();
188188
while (!Scheduler.unstable_shouldYield()) {
189189
runtime.advanceTime(1);
@@ -199,7 +199,7 @@ describe('SchedulerBrowser', () => {
199199
runtime.assertLog([
200200
'Message Event',
201201
'Task',
202-
gate(flags => (flags.www ? 'Yield at 10ms' : 'Yield at 5ms')),
202+
'Yield at 0ms',
203203
'Post Message',
204204
]);
205205

packages/scheduler/src/forks/Scheduler.js

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -93,6 +93,8 @@ var isPerformingWork = false;
9393
var isHostCallbackScheduled = false;
9494
var isHostTimeoutScheduled = false;
9595

96+
var needsPaint = false;
97+
9698
// Capture local references to native APIs, in case a polyfill overrides them.
9799
const localSetTimeout = typeof setTimeout === 'function' ? setTimeout : null;
98100
const localClearTimeout =
@@ -456,6 +458,10 @@ let frameInterval = frameYieldMs;
456458
let startTime = -1;
457459

458460
function shouldYieldToHost(): boolean {
461+
if (needsPaint) {
462+
// Yield now.
463+
return true;
464+
}
459465
const timeElapsed = getCurrentTime() - startTime;
460466
if (timeElapsed < frameInterval) {
461467
// The main thread has only been blocked for a really short amount of time;
@@ -466,7 +472,9 @@ function shouldYieldToHost(): boolean {
466472
return true;
467473
}
468474

469-
function requestPaint() {}
475+
function requestPaint() {
476+
needsPaint = true;
477+
}
470478

471479
function forceFrameRate(fps: number) {
472480
if (fps < 0 || fps > 125) {
@@ -486,6 +494,7 @@ function forceFrameRate(fps: number) {
486494
}
487495

488496
const performWorkUntilDeadline = () => {
497+
needsPaint = false;
489498
if (isMessageLoopRunning) {
490499
const currentTime = getCurrentTime();
491500
// Keep track of the start time so we can measure how long the main thread

0 commit comments

Comments
 (0)