[Scheduler] requestPaint (#15960)

* [Scheduler] requestPaint

Signals to Scheduler that the browser needs to paint the screen. React
will call it in the commit phase. Scheduler will yield at the end of
the current frame, even if there is no pending input.

When `isInputPending` is not available, this has no effect, because we
yield at the end of every frame regardless.

React will call `requestPaint` in the commit phase as long as there's at
least one effect. We could choose not to call it if none of the effects
are DOM mutations, but this is so rare that it doesn't seem worthwhile
to bother checking.

* Fall back gracefully if requestPaint is missing
This commit is contained in:
Andrew Clark 2019-06-22 00:15:09 -07:00 committed by GitHub
parent 8d4ddd33ac
commit 6568a79931
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
13 changed files with 119 additions and 35 deletions

View File

@ -7,23 +7,11 @@
import Transform from 'art/core/transform'; import Transform from 'art/core/transform';
import Mode from 'art/modes/current'; import Mode from 'art/modes/current';
import * as Scheduler from 'scheduler';
import invariant from 'shared/invariant'; import invariant from 'shared/invariant';
import {TYPES, EVENT_TYPES, childrenAsString} from './ReactARTInternals'; import {TYPES, EVENT_TYPES, childrenAsString} from './ReactARTInternals';
import type {ReactEventComponentInstance} from 'shared/ReactTypes'; import type {ReactEventComponentInstance} from 'shared/ReactTypes';
// Intentionally not named imports because Rollup would
// use dynamic dispatch for CommonJS interop named imports.
const {
unstable_now: now,
unstable_scheduleCallback: scheduleDeferredCallback,
unstable_shouldYield: shouldYield,
unstable_cancelCallback: cancelDeferredCallback,
} = Scheduler;
export {now, scheduleDeferredCallback, shouldYield, cancelDeferredCallback};
const pooledTransform = new Transform(); const pooledTransform = new Transform();
const NO_CONTEXT = {}; const NO_CONTEXT = {};

View File

@ -7,8 +7,6 @@
* @flow * @flow
*/ */
import * as Scheduler from 'scheduler';
import {precacheFiberNode, updateFiberProps} from './ReactDOMComponentTree'; import {precacheFiberNode, updateFiberProps} from './ReactDOMComponentTree';
import { import {
createElement, createElement,
@ -113,17 +111,6 @@ import warning from 'shared/warning';
const {html: HTML_NAMESPACE} = Namespaces; const {html: HTML_NAMESPACE} = Namespaces;
// Intentionally not named imports because Rollup would
// use dynamic dispatch for CommonJS interop named imports.
const {
unstable_now: now,
unstable_scheduleCallback: scheduleDeferredCallback,
unstable_shouldYield: shouldYield,
unstable_cancelCallback: cancelDeferredCallback,
} = Scheduler;
export {now, scheduleDeferredCallback, shouldYield, cancelDeferredCallback};
let SUPPRESS_HYDRATION_WARNING; let SUPPRESS_HYDRATION_WARNING;
if (__DEV__) { if (__DEV__) {
SUPPRESS_HYDRATION_WARNING = 'suppressHydrationWarning'; SUPPRESS_HYDRATION_WARNING = 'suppressHydrationWarning';

View File

@ -36,6 +36,7 @@ import {
getCurrentPriorityLevel, getCurrentPriorityLevel,
runWithPriority, runWithPriority,
shouldYield, shouldYield,
requestPaint,
now, now,
ImmediatePriority, ImmediatePriority,
UserBlockingPriority, UserBlockingPriority,
@ -1666,6 +1667,10 @@ function commitRootImpl(root) {
nextEffect = null; nextEffect = null;
// Tell Scheduler to yield at the end of the frame, so the browser has an
// opportunity to paint.
requestPaint();
if (enableSchedulerTracing) { if (enableSchedulerTracing) {
__interactionsRef.current = ((prevInteractions: any): Set<Interaction>); __interactionsRef.current = ((prevInteractions: any): Set<Interaction>);
} }

View File

@ -19,6 +19,7 @@ const {
unstable_scheduleCallback: Scheduler_scheduleCallback, unstable_scheduleCallback: Scheduler_scheduleCallback,
unstable_cancelCallback: Scheduler_cancelCallback, unstable_cancelCallback: Scheduler_cancelCallback,
unstable_shouldYield: Scheduler_shouldYield, unstable_shouldYield: Scheduler_shouldYield,
unstable_requestPaint: Scheduler_requestPaint,
unstable_now: Scheduler_now, unstable_now: Scheduler_now,
unstable_getCurrentPriorityLevel: Scheduler_getCurrentPriorityLevel, unstable_getCurrentPriorityLevel: Scheduler_getCurrentPriorityLevel,
unstable_ImmediatePriority: Scheduler_ImmediatePriority, unstable_ImmediatePriority: Scheduler_ImmediatePriority,
@ -63,6 +64,9 @@ export const IdlePriority: ReactPriorityLevel = 95;
export const NoPriority: ReactPriorityLevel = 90; export const NoPriority: ReactPriorityLevel = 90;
export const shouldYield = Scheduler_shouldYield; export const shouldYield = Scheduler_shouldYield;
export const requestPaint =
// Fall back gracefully if we're running an older verison of Scheduler.
Scheduler_requestPaint !== undefined ? Scheduler_requestPaint : () => {};
let syncQueue: Array<SchedulerCallback> | null = null; let syncQueue: Array<SchedulerCallback> | null = null;
let immediateQueueCallbackNode: mixed | null = null; let immediateQueueCallbackNode: mixed | null = null;

View File

@ -166,6 +166,32 @@ describe('ReactSchedulerIntegration', () => {
expect(Scheduler).toFlushAndYield(['A [UserBlocking]', 'B [Normal]']); expect(Scheduler).toFlushAndYield(['A [UserBlocking]', 'B [Normal]']);
}); });
it('requests a paint after committing', () => {
const scheduleCallback = Scheduler.unstable_scheduleCallback;
const root = ReactNoop.createRoot();
root.render('Initial');
Scheduler.flushAll();
scheduleCallback(NormalPriority, () => Scheduler.yieldValue('A'));
scheduleCallback(NormalPriority, () => Scheduler.yieldValue('B'));
scheduleCallback(NormalPriority, () => Scheduler.yieldValue('C'));
// Schedule a React render. React will request a paint after committing it.
root.render('Update');
// Advance time just to be sure the next tasks have lower priority
Scheduler.advanceTime(2000);
scheduleCallback(NormalPriority, () => Scheduler.yieldValue('D'));
scheduleCallback(NormalPriority, () => Scheduler.yieldValue('E'));
// Flush everything up to the next paint. Should yield after the
// React commit.
Scheduler.unstable_flushUntilNextPaint();
expect(Scheduler).toHaveYielded(['A', 'B', 'C']);
});
// TODO // TODO
it.skip('passive effects have render priority even if they are flushed early', () => {}); it.skip('passive effects have render priority even if they are flushed early', () => {});
}); });

View File

@ -47,6 +47,13 @@
); );
} }
function unstable_requestPaint() {
return global.React.__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED.Scheduler.unstable_requestPaint.apply(
this,
arguments
);
}
function unstable_runWithPriority() { function unstable_runWithPriority() {
return global.React.__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED.Scheduler.unstable_runWithPriority.apply( return global.React.__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED.Scheduler.unstable_runWithPriority.apply(
this, this,
@ -108,6 +115,7 @@
unstable_scheduleCallback: unstable_scheduleCallback, unstable_scheduleCallback: unstable_scheduleCallback,
unstable_cancelCallback: unstable_cancelCallback, unstable_cancelCallback: unstable_cancelCallback,
unstable_shouldYield: unstable_shouldYield, unstable_shouldYield: unstable_shouldYield,
unstable_requestPaint: unstable_requestPaint,
unstable_runWithPriority: unstable_runWithPriority, unstable_runWithPriority: unstable_runWithPriority,
unstable_next: unstable_next, unstable_next: unstable_next,
unstable_wrapCallback: unstable_wrapCallback, unstable_wrapCallback: unstable_wrapCallback,

View File

@ -47,6 +47,13 @@
); );
} }
function unstable_requestPaint() {
return global.React.__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED.Scheduler.unstable_requestPaint.apply(
this,
arguments
);
}
function unstable_runWithPriority() { function unstable_runWithPriority() {
return global.React.__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED.Scheduler.unstable_runWithPriority.apply( return global.React.__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED.Scheduler.unstable_runWithPriority.apply(
this, this,
@ -102,6 +109,7 @@
unstable_scheduleCallback: unstable_scheduleCallback, unstable_scheduleCallback: unstable_scheduleCallback,
unstable_cancelCallback: unstable_cancelCallback, unstable_cancelCallback: unstable_cancelCallback,
unstable_shouldYield: unstable_shouldYield, unstable_shouldYield: unstable_shouldYield,
unstable_requestPaint: unstable_requestPaint,
unstable_runWithPriority: unstable_runWithPriority, unstable_runWithPriority: unstable_runWithPriority,
unstable_next: unstable_next, unstable_next: unstable_next,
unstable_wrapCallback: unstable_wrapCallback, unstable_wrapCallback: unstable_wrapCallback,

View File

@ -47,6 +47,13 @@
); );
} }
function unstable_requestPaint() {
return global.React.__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED.Scheduler.unstable_requestPaint.apply(
this,
arguments
);
}
function unstable_runWithPriority() { function unstable_runWithPriority() {
return global.React.__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED.Scheduler.unstable_runWithPriority.apply( return global.React.__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED.Scheduler.unstable_runWithPriority.apply(
this, this,
@ -102,6 +109,7 @@
unstable_scheduleCallback: unstable_scheduleCallback, unstable_scheduleCallback: unstable_scheduleCallback,
unstable_cancelCallback: unstable_cancelCallback, unstable_cancelCallback: unstable_cancelCallback,
unstable_shouldYield: unstable_shouldYield, unstable_shouldYield: unstable_shouldYield,
unstable_requestPaint: unstable_requestPaint,
unstable_runWithPriority: unstable_runWithPriority, unstable_runWithPriority: unstable_runWithPriority,
unstable_next: unstable_next, unstable_next: unstable_next,
unstable_wrapCallback: unstable_wrapCallback, unstable_wrapCallback: unstable_wrapCallback,

View File

@ -16,6 +16,7 @@ import {
shouldYieldToHost, shouldYieldToHost,
getCurrentTime, getCurrentTime,
forceFrameRate, forceFrameRate,
requestPaint,
} from './SchedulerHostConfig'; } from './SchedulerHostConfig';
// TODO: Use symbols? // TODO: Use symbols?
@ -506,6 +507,8 @@ function unstable_shouldYield() {
); );
} }
const unstable_requestPaint = requestPaint;
export { export {
ImmediatePriority as unstable_ImmediatePriority, ImmediatePriority as unstable_ImmediatePriority,
UserBlockingPriority as unstable_UserBlockingPriority, UserBlockingPriority as unstable_UserBlockingPriority,
@ -519,6 +522,7 @@ export {
unstable_wrapCallback, unstable_wrapCallback,
unstable_getCurrentPriorityLevel, unstable_getCurrentPriorityLevel,
unstable_shouldYield, unstable_shouldYield,
unstable_requestPaint,
unstable_continueExecution, unstable_continueExecution,
unstable_pauseExecution, unstable_pauseExecution,
unstable_getFirstCallbackNode, unstable_getFirstCallbackNode,

View File

@ -18,6 +18,7 @@ export let cancelHostCallback;
export let requestHostTimeout; export let requestHostTimeout;
export let cancelHostTimeout; export let cancelHostTimeout;
export let shouldYieldToHost; export let shouldYieldToHost;
export let requestPaint;
export let getCurrentTime; export let getCurrentTime;
export let forceFrameRate; export let forceFrameRate;
@ -125,7 +126,7 @@ if (
shouldYieldToHost = function() { shouldYieldToHost = function() {
return false; return false;
}; };
forceFrameRate = function() {}; requestPaint = forceFrameRate = function() {};
} else { } else {
if (typeof console !== 'undefined') { if (typeof console !== 'undefined') {
// TODO: Remove fb.me link // TODO: Remove fb.me link
@ -162,7 +163,8 @@ if (
// TODO: Make this configurable // TODO: Make this configurable
// TODO: Adjust this based on priority? // TODO: Adjust this based on priority?
let maxFrameLength = 300; let maxFrameLength = 150;
let needsPaint = false;
const isInputPending = const isInputPending =
navigator !== undefined && navigator !== undefined &&
@ -181,11 +183,12 @@ if (
// main thread, so the browser can perform high priority tasks. The main // main thread, so the browser can perform high priority tasks. The main
// ones are painting and user input. If we're certain there's no user // ones are painting and user input. If we're certain there's no user
// input, then we can yield less often without making the app less // input, then we can yield less often without making the app less
// responsive. We'll eventually yield regardless, since there could be // responsive. We'll also check if a paint was requested. We'll eventually
// other main thread tasks that we don't know about. // yield regardless, since there could be other main thread tasks that we
if (isInputPending !== null && !isInputPending()) { // don't know about.
// There's no pending input. Only yield if we've reached the max if (!needsPaint && isInputPending !== null && !isInputPending()) {
// frame length. // There's no pending input, and no task requested a paint. Only yield
// if we've reached the max frame length.
return currentTime >= frameDeadline + maxFrameLength; return currentTime >= frameDeadline + maxFrameLength;
} }
// Either there is pending input, or there's no way for us to be sure // Either there is pending input, or there's no way for us to be sure
@ -242,6 +245,9 @@ if (
port.postMessage(undefined); port.postMessage(undefined);
throw error; throw error;
} }
// Yielding to the browser will give it a chance to paint, so we can
// reset this.
needsPaint = false;
} }
}; };
@ -321,4 +327,8 @@ if (
localClearTimeout(timeoutID); localClearTimeout(timeoutID);
timeoutID = -1; timeoutID = -1;
}; };
requestPaint = function() {
needsPaint = true;
};
} }

View File

@ -15,6 +15,8 @@ let yieldedValues: Array<mixed> | null = null;
let expectedNumberOfYields: number = -1; let expectedNumberOfYields: number = -1;
let didStop: boolean = false; let didStop: boolean = false;
let isFlushing: boolean = false; let isFlushing: boolean = false;
let needsPaint: boolean = false;
let shouldYieldForPaint: boolean = false;
export function requestHostCallback(callback: boolean => void) { export function requestHostCallback(callback: boolean => void) {
scheduledCallback = callback; scheduledCallback = callback;
@ -36,9 +38,10 @@ export function cancelHostTimeout(): void {
export function shouldYieldToHost(): boolean { export function shouldYieldToHost(): boolean {
if ( if (
expectedNumberOfYields !== -1 && (expectedNumberOfYields !== -1 &&
yieldedValues !== null && yieldedValues !== null &&
yieldedValues.length >= expectedNumberOfYields yieldedValues.length >= expectedNumberOfYields) ||
(shouldYieldForPaint && needsPaint)
) { ) {
// We yielded at least as many values as expected. Stop flushing. // We yielded at least as many values as expected. Stop flushing.
didStop = true; didStop = true;
@ -67,6 +70,7 @@ export function reset() {
expectedNumberOfYields = -1; expectedNumberOfYields = -1;
didStop = false; didStop = false;
isFlushing = false; isFlushing = false;
needsPaint = false;
} }
// Should only be used via an assertion helper that inspects the yielded values. // Should only be used via an assertion helper that inspects the yielded values.
@ -94,6 +98,31 @@ export function unstable_flushNumberOfYields(count: number): void {
} }
} }
export function unstable_flushUntilNextPaint(): void {
if (isFlushing) {
throw new Error('Already flushing work.');
}
if (scheduledCallback !== null) {
const cb = scheduledCallback;
shouldYieldForPaint = true;
needsPaint = false;
isFlushing = true;
try {
let hasMoreWork = true;
do {
hasMoreWork = cb(true, currentTime);
} while (hasMoreWork && !didStop);
if (!hasMoreWork) {
scheduledCallback = null;
}
} finally {
shouldYieldForPaint = false;
didStop = false;
isFlushing = false;
}
}
}
export function unstable_flushExpired() { export function unstable_flushExpired() {
if (isFlushing) { if (isFlushing) {
throw new Error('Already flushing work.'); throw new Error('Already flushing work.');
@ -181,3 +210,7 @@ export function advanceTime(ms: number) {
unstable_flushExpired(); unstable_flushExpired();
} }
} }
export function requestPaint() {
needsPaint = true;
}

View File

@ -14,6 +14,7 @@ export {
unstable_flushNumberOfYields, unstable_flushNumberOfYields,
unstable_flushExpired, unstable_flushExpired,
unstable_clearYields, unstable_clearYields,
unstable_flushUntilNextPaint,
flushAll, flushAll,
yieldValue, yieldValue,
advanceTime, advanceTime,

View File

@ -16,6 +16,7 @@ const {
unstable_now, unstable_now,
unstable_scheduleCallback, unstable_scheduleCallback,
unstable_shouldYield, unstable_shouldYield,
unstable_requestPaint,
unstable_getFirstCallbackNode, unstable_getFirstCallbackNode,
unstable_runWithPriority, unstable_runWithPriority,
unstable_next, unstable_next,
@ -39,6 +40,7 @@ export {
unstable_now, unstable_now,
unstable_scheduleCallback, unstable_scheduleCallback,
unstable_shouldYield, unstable_shouldYield,
unstable_requestPaint,
unstable_getFirstCallbackNode, unstable_getFirstCallbackNode,
unstable_runWithPriority, unstable_runWithPriority,
unstable_next, unstable_next,