@samitouri / QOS-React / commits / 3bcd2de01b

clean up isInputPending in Scheduler (#28444)

## Summary `isInputPending` is not in use. This PR cleans up the flags controlling its gating and parameters to simplify Scheduler. Makes `frameYieldMs` feature flag static, set to 10ms in www, which we found built on the wins provided by a broader yield interval via `isInputPending`. Flag remains set to 5ms in OSS builds. ## How did you test this change? `yarn test Scheduler`

Noah Lemen committed Feb 27, 2024 at 10:39 UTC 3bcd2de01b5716202eabe8faa338f51bdc59ce26
7 files changed +7 -265
packages/scheduler/src/SchedulerFeatureFlags.js
-4
@@ -8,12 +8,8 @@
8 */
9
10 export const enableSchedulerDebugging = false;
11 -export const enableIsInputPending = false;
11 export const enableProfiling = false;
13 -export const enableIsInputPendingContinuous = false;
12 export const frameYieldMs = 5;
15 -export const continuousYieldMs = 50;
16 -export const maxYieldMs = 300;
13
14 export const userBlockingPriorityTimeout = 250;
15 export const normalPriorityTimeout = 5000;
packages/scheduler/src/__tests__/Scheduler-test.js
+1 -176
@@ -101,23 +101,6 @@ describe('SchedulerBrowser', () => {
101 this.port2 = port2;
102 };
103
104 - const scheduling = {
105 - isInputPending(options) {
106 - if (this !== scheduling) {
107 - throw new Error(
108 - 'isInputPending called with incorrect `this` context',
109 - );
110 - }
111 -
112 - return (
113 - hasPendingDiscreteEvent ||
114 - (options && options.includeContinuous && hasPendingContinuousEvent)
115 - );
116 - },
117 - };
118 -
119 - global.navigator = {scheduling};
120 -
104 function ensureLogIsEmpty() {
105 if (eventLog.length !== 0) {
106 throw Error('Log is not empty. Call assertLog before continuing.');
@@ -218,7 +201,7 @@ describe('SchedulerBrowser', () => {
201 runtime.assertLog([
202 'Message Event',
203 'Task',
221 - 'Yield at 5ms',
204 + gate(flags => (flags.www ? 'Yield at 10ms' : 'Yield at 5ms')),
205 'Post Message',
206 ]);
207
@@ -320,164 +303,6 @@ describe('SchedulerBrowser', () => {
303 runtime.assertLog(['Message Event', 'B']);
304 });
305
323 - it('when isInputPending is available, we can wait longer before yielding', () => {
324 - function blockUntilSchedulerAsksToYield() {
325 - while (!Scheduler.unstable_shouldYield()) {
326 - runtime.advanceTime(1);
327 - }
328 - runtime.log(`Yield at ${performance.now()}ms`);
329 - }
330 -
331 - // First show what happens when we don't request a paint
332 - scheduleCallback(NormalPriority, () => {
333 - runtime.log('Task with no pending input');
334 - blockUntilSchedulerAsksToYield();
335 - });
336 - runtime.assertLog(['Post Message']);
337 -
338 - runtime.fireMessageEvent();
339 - runtime.assertLog([
340 - 'Message Event',
341 - 'Task with no pending input',
342 - // Even though there's no input, eventually Scheduler will yield
343 - // regardless in case there's a pending main thread task we don't know
344 - // about, like a network event.
345 - gate(flags =>
346 - flags.enableIsInputPending
347 - ? 'Yield at 10ms'
348 - : // When isInputPending is disabled, we always yield quickly
349 - 'Yield at 5ms',
350 - ),
351 - ]);
352 -
353 - runtime.resetTime();
354 -
355 - // Now do the same thing, but while the task is running, simulate an
356 - // input event.
357 - scheduleCallback(NormalPriority, () => {
358 - runtime.log('Task with pending input');
359 - runtime.scheduleDiscreteEvent();
360 - blockUntilSchedulerAsksToYield();
361 - });
362 - runtime.assertLog(['Post Message']);
363 -
364 - runtime.fireMessageEvent();
365 - runtime.assertLog([
366 - 'Message Event',
367 - 'Task with pending input',
368 - // This time we yielded quickly to unblock the discrete event.
369 - 'Yield at 5ms',
370 - 'Discrete Event',
371 - ]);
372 - });
373 -
374 - it(
375 - 'isInputPending will also check for continuous inputs, but after a ' +
376 - 'slightly larger threshold',
377 - () => {
378 - function blockUntilSchedulerAsksToYield() {
379 - while (!Scheduler.unstable_shouldYield()) {
380 - runtime.advanceTime(1);
381 - }
382 - runtime.log(`Yield at ${performance.now()}ms`);
383 - }
384 -
385 - // First show what happens when we don't request a paint
386 - scheduleCallback(NormalPriority, () => {
387 - runtime.log('Task with no pending input');
388 - blockUntilSchedulerAsksToYield();
389 - });
390 - runtime.assertLog(['Post Message']);
391 -
392 - runtime.fireMessageEvent();
393 - runtime.assertLog([
394 - 'Message Event',
395 - 'Task with no pending input',
396 - // Even though there's no input, eventually Scheduler will yield
397 - // regardless in case there's a pending main thread task we don't know
398 - // about, like a network event.
399 - gate(flags =>
400 - flags.enableIsInputPending
401 - ? 'Yield at 10ms'
402 - : // When isInputPending is disabled, we always yield quickly
403 - 'Yield at 5ms',
404 - ),
405 - ]);
406 -
407 - runtime.resetTime();
408 -
409 - // Now do the same thing, but while the task is running, simulate a
410 - // continuous input event.
411 - scheduleCallback(NormalPriority, () => {
412 - runtime.log('Task with continuous input');
413 - runtime.scheduleContinuousEvent();
414 - blockUntilSchedulerAsksToYield();
415 - });
416 - runtime.assertLog(['Post Message']);
417 -
418 - runtime.fireMessageEvent();
419 - runtime.assertLog([
420 - 'Message Event',
421 - 'Task with continuous input',
422 - // This time we yielded quickly to unblock the continuous event. But not
423 - // as quickly as for a discrete event.
424 - gate(flags =>
425 - flags.enableIsInputPending
426 - ? 'Yield at 10ms'
427 - : // When isInputPending is disabled, we always yield quickly
428 - 'Yield at 5ms',
429 - ),
430 - 'Continuous Event',
431 - ]);
432 - },
433 - );
434 -
435 - it('requestPaint forces a yield at the end of the next frame interval', () => {
436 - function blockUntilSchedulerAsksToYield() {
437 - while (!Scheduler.unstable_shouldYield()) {
438 - runtime.advanceTime(1);
439 - }
440 - runtime.log(`Yield at ${performance.now()}ms`);
441 - }
442 -
443 - // First show what happens when we don't request a paint
444 - scheduleCallback(NormalPriority, () => {
445 - runtime.log('Task with no paint');
446 - blockUntilSchedulerAsksToYield();
447 - });
448 - runtime.assertLog(['Post Message']);
449 -
450 - runtime.fireMessageEvent();
451 - runtime.assertLog([
452 - 'Message Event',
453 - 'Task with no paint',
454 - gate(flags =>
455 - flags.enableIsInputPending
456 - ? 'Yield at 10ms'
457 - : // When isInputPending is disabled, we always yield quickly
458 - 'Yield at 5ms',
459 - ),
460 - ]);
461 -
462 - runtime.resetTime();
463 -
464 - // Now do the same thing, but call requestPaint inside the task
465 - scheduleCallback(NormalPriority, () => {
466 - runtime.log('Task with paint');
467 - requestPaint();
468 - blockUntilSchedulerAsksToYield();
469 - });
470 - runtime.assertLog(['Post Message']);
471 -
472 - runtime.fireMessageEvent();
473 - runtime.assertLog([
474 - 'Message Event',
475 - 'Task with paint',
476 - // This time we yielded quickly (5ms) because we requested a paint.
477 - 'Yield at 5ms',
478 - ]);
479 - });
480 -
306 it('yielding continues in a new task regardless of how much time is remaining', () => {
307 scheduleCallback(NormalPriority, () => {
308 runtime.log('Original Task');
packages/scheduler/src/__tests__/SchedulerSetImmediate-test.js
+1 -1
@@ -173,7 +173,7 @@ describe('SchedulerDOMSetImmediate', () => {
173 runtime.assertLog([
174 'setImmediate Callback',
175 'Task',
176 - 'Yield at 5ms',
176 + gate(flags => (flags.www ? 'Yield at 10ms' : 'Yield at 5ms')),
177 'Set Immediate',
178 ]);
179
packages/scheduler/src/forks/Scheduler.js
+2 -71
@@ -14,11 +14,7 @@ import type {PriorityLevel} from '../SchedulerPriorities';
14 import {
15 enableSchedulerDebugging,
16 enableProfiling,
17 - enableIsInputPending,
18 - enableIsInputPendingContinuous,
17 frameYieldMs,
20 - continuousYieldMs,
21 - maxYieldMs,
18 userBlockingPriorityTimeout,
19 lowPriorityTimeout,
20 normalPriorityTimeout,
@@ -104,17 +100,6 @@ const localClearTimeout =
100 const localSetImmediate =
101 typeof setImmediate !== 'undefined' ? setImmediate : null; // IE and Node.js + jsdom
102
107 -const isInputPending =
108 - typeof navigator !== 'undefined' &&
109 - // $FlowFixMe[prop-missing]
110 - navigator.scheduling !== undefined &&
111 - // $FlowFixMe[incompatible-type]
112 - navigator.scheduling.isInputPending !== undefined
113 - ? navigator.scheduling.isInputPending.bind(navigator.scheduling)
114 - : null;
115 -
116 -const continuousOptions = {includeContinuous: enableIsInputPendingContinuous};
117 -
103 function advanceTimers(currentTime: number) {
104 // Check for tasks that are no longer delayed and add them to the queue.
105 let timer = peek(timerQueue);
@@ -468,12 +453,8 @@ let taskTimeoutID: TimeoutID = (-1: any);
453 // It does not attempt to align with frame boundaries, since most tasks don't
454 // need to be frame aligned; for those that do, use requestAnimationFrame.
455 let frameInterval = frameYieldMs;
471 -const continuousInputInterval = continuousYieldMs;
472 -const maxInterval = maxYieldMs;
456 let startTime = -1;
457
475 -let needsPaint = false;
476 -
458 function shouldYieldToHost(): boolean {
459 const timeElapsed = getCurrentTime() - startTime;
460 if (timeElapsed < frameInterval) {
@@ -481,58 +462,11 @@ function shouldYieldToHost(): boolean {
462 // smaller than a single frame. Don't yield yet.
463 return false;
464 }
484 -
485 - // The main thread has been blocked for a non-negligible amount of time. We
486 - // may want to yield control of the main thread, so the browser can perform
487 - // high priority tasks. The main ones are painting and user input. If there's
488 - // a pending paint or a pending input, then we should yield. But if there's
489 - // neither, then we can yield less often while remaining responsive. We'll
490 - // eventually yield regardless, since there could be a pending paint that
491 - // wasn't accompanied by a call to `requestPaint`, or other main thread tasks
492 - // like network events.
493 - if (enableIsInputPending) {
494 - if (needsPaint) {
495 - // There's a pending paint (signaled by `requestPaint`). Yield now.
496 - return true;
497 - }
498 - if (timeElapsed < continuousInputInterval) {
499 - // We haven't blocked the thread for that long. Only yield if there's a
500 - // pending discrete input (e.g. click). It's OK if there's pending
501 - // continuous input (e.g. mouseover).
502 - if (isInputPending !== null) {
503 - return isInputPending();
504 - }
505 - } else if (timeElapsed < maxInterval) {
506 - // Yield if there's either a pending discrete or continuous input.
507 - if (isInputPending !== null) {
508 - return isInputPending(continuousOptions);
509 - }
510 - } else {
511 - // We've blocked the thread for a long time. Even if there's no pending
512 - // input, there may be some other scheduled work that we don't know about,
513 - // like a network event. Yield now.
514 - return true;
515 - }
516 - }
517 -
518 - // `isInputPending` isn't available. Yield now.
465 + // Yield now.
466 return true;
467 }
468
522 -function requestPaint() {
523 - if (
524 - enableIsInputPending &&
525 - navigator !== undefined &&
526 - // $FlowFixMe[prop-missing]
527 - navigator.scheduling !== undefined &&
528 - // $FlowFixMe[incompatible-type]
529 - navigator.scheduling.isInputPending !== undefined
530 - ) {
531 - needsPaint = true;
532 - }
533 -
534 - // Since we yield every frame regardless, `requestPaint` has no effect.
535 -}
469 +function requestPaint() {}
470
471 function forceFrameRate(fps: number) {
472 if (fps < 0 || fps > 125) {
@@ -577,9 +511,6 @@ const performWorkUntilDeadline = () => {
511 }
512 }
513 }
580 - // Yielding to the browser will give it a chance to paint, so we can
581 - // reset this.
582 - needsPaint = false;
514 };
515
516 let schedulePerformWorkUntilDeadline;
packages/scheduler/src/forks/SchedulerFeatureFlags.www-dynamic.js
-6
@@ -13,12 +13,6 @@
13
14 export const enableProfiling = __VARIANT__;
15
16 -export const enableIsInputPending = __VARIANT__;
17 -export const enableIsInputPendingContinuous = __VARIANT__;
18 -export const frameYieldMs = 5;
19 -export const continuousYieldMs = 10;
20 -export const maxYieldMs = 10;
21 -
16 export const userBlockingPriorityTimeout = 250;
17 export const normalPriorityTimeout = 5000;
18 export const lowPriorityTimeout = 10000;
packages/scheduler/src/forks/SchedulerFeatureFlags.www.js
+2 -5
@@ -16,12 +16,9 @@ export const {
16 userBlockingPriorityTimeout,
17 normalPriorityTimeout,
18 lowPriorityTimeout,
19 - enableIsInputPending,
20 - enableIsInputPendingContinuous,
21 - frameYieldMs,
22 - continuousYieldMs,
23 - maxYieldMs,
19 } = dynamicFeatureFlags;
20 +
21 +export const frameYieldMs = 10;
22 export const enableSchedulerDebugging = true;
23 export const enableProfiling: boolean =
24 __PROFILE__ && enableProfilingFeatureFlag;
packages/scheduler/src/forks/SchedulerPostTask.js
+1 -2
@@ -57,8 +57,7 @@ let deadline = 0;
57
58 let currentPriorityLevel_DEPRECATED = NormalPriority;
59
60 -// `isInputPending` is not available. Since we have no way of knowing if
61 -// there's pending input, always yield at the end of the frame.
60 +// Always yield at the end of the frame.
61 export function unstable_shouldYield(): boolean {
62 return getCurrentTime() >= deadline;
63 }