@samitouri / QOS-React-2 / commits / 6568a79931

[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

Andrew Clark committed Jun 22, 2019 at 00:15 UTC 6568a79931bb335f2c997ed9efbc533f2677df5c
13 files changed +119 -35
packages/react-art/src/ReactARTHostConfig.js
-12
@@ -7,23 +7,11 @@
7
8 import Transform from 'art/core/transform';
9 import Mode from 'art/modes/current';
10 -import * as Scheduler from 'scheduler';
10 import invariant from 'shared/invariant';
11
12 import {TYPES, EVENT_TYPES, childrenAsString} from './ReactARTInternals';
13 import type {ReactEventComponentInstance} from 'shared/ReactTypes';
14
16 -// Intentionally not named imports because Rollup would
17 -// use dynamic dispatch for CommonJS interop named imports.
18 -const {
19 - unstable_now: now,
20 - unstable_scheduleCallback: scheduleDeferredCallback,
21 - unstable_shouldYield: shouldYield,
22 - unstable_cancelCallback: cancelDeferredCallback,
23 -} = Scheduler;
24 -
25 -export {now, scheduleDeferredCallback, shouldYield, cancelDeferredCallback};
26 -
15 const pooledTransform = new Transform();
16
17 const NO_CONTEXT = {};
packages/react-dom/src/client/ReactDOMHostConfig.js
-13
@@ -7,8 +7,6 @@
7 * @flow
8 */
9
10 -import * as Scheduler from 'scheduler';
11 -
10 import {precacheFiberNode, updateFiberProps} from './ReactDOMComponentTree';
11 import {
12 createElement,
@@ -113,17 +111,6 @@ import warning from 'shared/warning';
111
112 const {html: HTML_NAMESPACE} = Namespaces;
113
116 -// Intentionally not named imports because Rollup would
117 -// use dynamic dispatch for CommonJS interop named imports.
118 -const {
119 - unstable_now: now,
120 - unstable_scheduleCallback: scheduleDeferredCallback,
121 - unstable_shouldYield: shouldYield,
122 - unstable_cancelCallback: cancelDeferredCallback,
123 -} = Scheduler;
124 -
125 -export {now, scheduleDeferredCallback, shouldYield, cancelDeferredCallback};
126 -
114 let SUPPRESS_HYDRATION_WARNING;
115 if (__DEV__) {
116 SUPPRESS_HYDRATION_WARNING = 'suppressHydrationWarning';
packages/react-reconciler/src/ReactFiberWorkLoop.js
+5
@@ -36,6 +36,7 @@ import {
36 getCurrentPriorityLevel,
37 runWithPriority,
38 shouldYield,
39 + requestPaint,
40 now,
41 ImmediatePriority,
42 UserBlockingPriority,
@@ -1666,6 +1667,10 @@ function commitRootImpl(root) {
1667
1668 nextEffect = null;
1669
1670 + // Tell Scheduler to yield at the end of the frame, so the browser has an
1671 + // opportunity to paint.
1672 + requestPaint();
1673 +
1674 if (enableSchedulerTracing) {
1675 __interactionsRef.current = ((prevInteractions: any): Set<Interaction>);
1676 }
packages/react-reconciler/src/SchedulerWithReactIntegration.js
+4
@@ -19,6 +19,7 @@ const {
19 unstable_scheduleCallback: Scheduler_scheduleCallback,
20 unstable_cancelCallback: Scheduler_cancelCallback,
21 unstable_shouldYield: Scheduler_shouldYield,
22 + unstable_requestPaint: Scheduler_requestPaint,
23 unstable_now: Scheduler_now,
24 unstable_getCurrentPriorityLevel: Scheduler_getCurrentPriorityLevel,
25 unstable_ImmediatePriority: Scheduler_ImmediatePriority,
@@ -63,6 +64,9 @@ export const IdlePriority: ReactPriorityLevel = 95;
64 export const NoPriority: ReactPriorityLevel = 90;
65
66 export const shouldYield = Scheduler_shouldYield;
67 +export const requestPaint =
68 + // Fall back gracefully if we're running an older verison of Scheduler.
69 + Scheduler_requestPaint !== undefined ? Scheduler_requestPaint : () => {};
70
71 let syncQueue: Array<SchedulerCallback> | null = null;
72 let immediateQueueCallbackNode: mixed | null = null;
packages/react-reconciler/src/__tests__/ReactSchedulerIntegration-test.internal.js
+26
@@ -166,6 +166,32 @@ describe('ReactSchedulerIntegration', () => {
166 expect(Scheduler).toFlushAndYield(['A [UserBlocking]', 'B [Normal]']);
167 });
168
169 + it('requests a paint after committing', () => {
170 + const scheduleCallback = Scheduler.unstable_scheduleCallback;
171 +
172 + const root = ReactNoop.createRoot();
173 + root.render('Initial');
174 + Scheduler.flushAll();
175 +
176 + scheduleCallback(NormalPriority, () => Scheduler.yieldValue('A'));
177 + scheduleCallback(NormalPriority, () => Scheduler.yieldValue('B'));
178 + scheduleCallback(NormalPriority, () => Scheduler.yieldValue('C'));
179 +
180 + // Schedule a React render. React will request a paint after committing it.
181 + root.render('Update');
182 +
183 + // Advance time just to be sure the next tasks have lower priority
184 + Scheduler.advanceTime(2000);
185 +
186 + scheduleCallback(NormalPriority, () => Scheduler.yieldValue('D'));
187 + scheduleCallback(NormalPriority, () => Scheduler.yieldValue('E'));
188 +
189 + // Flush everything up to the next paint. Should yield after the
190 + // React commit.
191 + Scheduler.unstable_flushUntilNextPaint();
192 + expect(Scheduler).toHaveYielded(['A', 'B', 'C']);
193 + });
194 +
195 // TODO
196 it.skip('passive effects have render priority even if they are flushed early', () => {});
197 });
packages/scheduler/npm/umd/scheduler.development.js
+8
@@ -47,6 +47,13 @@
47 );
48 }
49
50 + function unstable_requestPaint() {
51 + return global.React.__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED.Scheduler.unstable_requestPaint.apply(
52 + this,
53 + arguments
54 + );
55 + }
56 +
57 function unstable_runWithPriority() {
58 return global.React.__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED.Scheduler.unstable_runWithPriority.apply(
59 this,
@@ -108,6 +115,7 @@
115 unstable_scheduleCallback: unstable_scheduleCallback,
116 unstable_cancelCallback: unstable_cancelCallback,
117 unstable_shouldYield: unstable_shouldYield,
118 + unstable_requestPaint: unstable_requestPaint,
119 unstable_runWithPriority: unstable_runWithPriority,
120 unstable_next: unstable_next,
121 unstable_wrapCallback: unstable_wrapCallback,
packages/scheduler/npm/umd/scheduler.production.min.js
+8
@@ -47,6 +47,13 @@
47 );
48 }
49
50 + function unstable_requestPaint() {
51 + return global.React.__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED.Scheduler.unstable_requestPaint.apply(
52 + this,
53 + arguments
54 + );
55 + }
56 +
57 function unstable_runWithPriority() {
58 return global.React.__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED.Scheduler.unstable_runWithPriority.apply(
59 this,
@@ -102,6 +109,7 @@
109 unstable_scheduleCallback: unstable_scheduleCallback,
110 unstable_cancelCallback: unstable_cancelCallback,
111 unstable_shouldYield: unstable_shouldYield,
112 + unstable_requestPaint: unstable_requestPaint,
113 unstable_runWithPriority: unstable_runWithPriority,
114 unstable_next: unstable_next,
115 unstable_wrapCallback: unstable_wrapCallback,
packages/scheduler/npm/umd/scheduler.profiling.min.js
+8
@@ -47,6 +47,13 @@
47 );
48 }
49
50 + function unstable_requestPaint() {
51 + return global.React.__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED.Scheduler.unstable_requestPaint.apply(
52 + this,
53 + arguments
54 + );
55 + }
56 +
57 function unstable_runWithPriority() {
58 return global.React.__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED.Scheduler.unstable_runWithPriority.apply(
59 this,
@@ -102,6 +109,7 @@
109 unstable_scheduleCallback: unstable_scheduleCallback,
110 unstable_cancelCallback: unstable_cancelCallback,
111 unstable_shouldYield: unstable_shouldYield,
112 + unstable_requestPaint: unstable_requestPaint,
113 unstable_runWithPriority: unstable_runWithPriority,
114 unstable_next: unstable_next,
115 unstable_wrapCallback: unstable_wrapCallback,
packages/scheduler/src/Scheduler.js
+4
@@ -16,6 +16,7 @@ import {
16 shouldYieldToHost,
17 getCurrentTime,
18 forceFrameRate,
19 + requestPaint,
20 } from './SchedulerHostConfig';
21
22 // TODO: Use symbols?
@@ -506,6 +507,8 @@ function unstable_shouldYield() {
507 );
508 }
509
510 +const unstable_requestPaint = requestPaint;
511 +
512 export {
513 ImmediatePriority as unstable_ImmediatePriority,
514 UserBlockingPriority as unstable_UserBlockingPriority,
@@ -519,6 +522,7 @@ export {
522 unstable_wrapCallback,
523 unstable_getCurrentPriorityLevel,
524 unstable_shouldYield,
525 + unstable_requestPaint,
526 unstable_continueExecution,
527 unstable_pauseExecution,
528 unstable_getFirstCallbackNode,
packages/scheduler/src/forks/SchedulerHostConfig.default.js
+17 -7
@@ -18,6 +18,7 @@ export let cancelHostCallback;
18 export let requestHostTimeout;
19 export let cancelHostTimeout;
20 export let shouldYieldToHost;
21 +export let requestPaint;
22 export let getCurrentTime;
23 export let forceFrameRate;
24
@@ -125,7 +126,7 @@ if (
126 shouldYieldToHost = function() {
127 return false;
128 };
128 - forceFrameRate = function() {};
129 + requestPaint = forceFrameRate = function() {};
130 } else {
131 if (typeof console !== 'undefined') {
132 // TODO: Remove fb.me link
@@ -162,7 +163,8 @@ if (
163
164 // TODO: Make this configurable
165 // TODO: Adjust this based on priority?
165 - let maxFrameLength = 300;
166 + let maxFrameLength = 150;
167 + let needsPaint = false;
168
169 const isInputPending =
170 navigator !== undefined &&
@@ -181,11 +183,12 @@ if (
183 // main thread, so the browser can perform high priority tasks. The main
184 // ones are painting and user input. If we're certain there's no user
185 // input, then we can yield less often without making the app less
184 - // responsive. We'll eventually yield regardless, since there could be
185 - // other main thread tasks that we don't know about.
186 - if (isInputPending !== null && !isInputPending()) {
187 - // There's no pending input. Only yield if we've reached the max
188 - // frame length.
186 + // responsive. We'll also check if a paint was requested. We'll eventually
187 + // yield regardless, since there could be other main thread tasks that we
188 + // don't know about.
189 + if (!needsPaint && isInputPending !== null && !isInputPending()) {
190 + // There's no pending input, and no task requested a paint. Only yield
191 + // if we've reached the max frame length.
192 return currentTime >= frameDeadline + maxFrameLength;
193 }
194 // Either there is pending input, or there's no way for us to be sure
@@ -242,6 +245,9 @@ if (
245 port.postMessage(undefined);
246 throw error;
247 }
248 + // Yielding to the browser will give it a chance to paint, so we can
249 + // reset this.
250 + needsPaint = false;
251 }
252 };
253
@@ -321,4 +327,8 @@ if (
327 localClearTimeout(timeoutID);
328 timeoutID = -1;
329 };
330 +
331 + requestPaint = function() {
332 + needsPaint = true;
333 + };
334 }
packages/scheduler/src/forks/SchedulerHostConfig.mock.js
+36 -3
@@ -15,6 +15,8 @@ let yieldedValues: Array<mixed> | null = null;
15 let expectedNumberOfYields: number = -1;
16 let didStop: boolean = false;
17 let isFlushing: boolean = false;
18 +let needsPaint: boolean = false;
19 +let shouldYieldForPaint: boolean = false;
20
21 export function requestHostCallback(callback: boolean => void) {
22 scheduledCallback = callback;
@@ -36,9 +38,10 @@ export function cancelHostTimeout(): void {
38
39 export function shouldYieldToHost(): boolean {
40 if (
39 - expectedNumberOfYields !== -1 &&
40 - yieldedValues !== null &&
41 - yieldedValues.length >= expectedNumberOfYields
41 + (expectedNumberOfYields !== -1 &&
42 + yieldedValues !== null &&
43 + yieldedValues.length >= expectedNumberOfYields) ||
44 + (shouldYieldForPaint && needsPaint)
45 ) {
46 // We yielded at least as many values as expected. Stop flushing.
47 didStop = true;
@@ -67,6 +70,7 @@ export function reset() {
70 expectedNumberOfYields = -1;
71 didStop = false;
72 isFlushing = false;
73 + needsPaint = false;
74 }
75
76 // Should only be used via an assertion helper that inspects the yielded values.
@@ -94,6 +98,31 @@ export function unstable_flushNumberOfYields(count: number): void {
98 }
99 }
100
101 +export function unstable_flushUntilNextPaint(): void {
102 + if (isFlushing) {
103 + throw new Error('Already flushing work.');
104 + }
105 + if (scheduledCallback !== null) {
106 + const cb = scheduledCallback;
107 + shouldYieldForPaint = true;
108 + needsPaint = false;
109 + isFlushing = true;
110 + try {
111 + let hasMoreWork = true;
112 + do {
113 + hasMoreWork = cb(true, currentTime);
114 + } while (hasMoreWork && !didStop);
115 + if (!hasMoreWork) {
116 + scheduledCallback = null;
117 + }
118 + } finally {
119 + shouldYieldForPaint = false;
120 + didStop = false;
121 + isFlushing = false;
122 + }
123 + }
124 +}
125 +
126 export function unstable_flushExpired() {
127 if (isFlushing) {
128 throw new Error('Already flushing work.');
@@ -181,3 +210,7 @@ export function advanceTime(ms: number) {
210 unstable_flushExpired();
211 }
212 }
213 +
214 +export function requestPaint() {
215 + needsPaint = true;
216 +}
packages/scheduler/unstable_mock.js
+1
@@ -14,6 +14,7 @@ export {
14 unstable_flushNumberOfYields,
15 unstable_flushExpired,
16 unstable_clearYields,
17 + unstable_flushUntilNextPaint,
18 flushAll,
19 yieldValue,
20 advanceTime,
packages/shared/forks/Scheduler.umd.js
+2
@@ -16,6 +16,7 @@ const {
16 unstable_now,
17 unstable_scheduleCallback,
18 unstable_shouldYield,
19 + unstable_requestPaint,
20 unstable_getFirstCallbackNode,
21 unstable_runWithPriority,
22 unstable_next,
@@ -39,6 +40,7 @@ export {
40 unstable_now,
41 unstable_scheduleCallback,
42 unstable_shouldYield,
43 + unstable_requestPaint,
44 unstable_getFirstCallbackNode,
45 unstable_runWithPriority,
46 unstable_next,