@samitouri / QOS-React / commits / 1e3b6192b5

Import Scheduler directly, not via host config (#14984)

* Import Scheduler directly, not via host config We currently schedule asynchronous tasks via the host config. (The host config is a static/build-time dependency injection system that varies across different renderers — DOM, native, test, and so on.) Instead of calling platform APIs like `requestIdleCallback` directly, each renderer implements a method called `scheduleDeferredCallback`. We've since discovered that when scheduling tasks, it's crucial that React work is placed in the same queue as other, non-React work on the main thread. Otherwise, you easily end up in a starvation scenario where rendering is constantly interrupted by less important tasks. You need a centralized coordinator that is used both by React and by other frameworks and application code. This coordinator must also have a consistent API across all the different host environments, for convention's sake and so product code is portable — e.g. so the same component can work in both React Native and React Native Web. This turned into the Scheduler package. We will have different builds of Scheduler for each of our target platforms. With this approach, we treat Scheduler like a built-in platform primitive that exists wherever React is supported. Now that we have this consistent interface, the indirection of the host config no longer makes sense for the purpose of scheduling tasks. In fact, we explicitly do not want renderers to scheduled task via any system except the Scheduler package. So, this PR removes `scheduleDeferredCallback` and its associated methods from the host config in favor of directly importing Scheduler. * Missed an extraneous export

Andrew Clark committed Mar 6, 2019 at 14:41 UTC 1e3b6192b54df20ac117a2af56afbe00ac9487b7
11 files changed +24 -123
packages/react-art/src/ReactARTHostConfig.js
-2
@@ -343,8 +343,6 @@ export function getChildHostContext() {
343 export const scheduleTimeout = setTimeout;
344 export const cancelTimeout = clearTimeout;
345 export const noTimeout = -1;
346 -export const schedulePassiveEffects = scheduleDeferredCallback;
347 -export const cancelPassiveEffects = cancelDeferredCallback;
346
347 export function shouldSetTextContent(type, props) {
348 return (
packages/react-dom/src/client/ReactDOMHostConfig.js
-2
@@ -310,8 +310,6 @@ export const scheduleTimeout =
310 export const cancelTimeout =
311 typeof clearTimeout === 'function' ? clearTimeout : (undefined: any);
312 export const noTimeout = -1;
313 -export const schedulePassiveEffects = scheduleDeferredCallback;
314 -export const cancelPassiveEffects = cancelDeferredCallback;
313
314 // -------------------
315 // Mutation
packages/react-native-renderer/src/ReactFabricHostConfig.js
-12
@@ -20,12 +20,6 @@ import {
20 warnForStyleProps,
21 } from './NativeMethodsMixinUtils';
22 import {create, diff} from './ReactNativeAttributePayload';
23 -import {
24 - now as ReactNativeFrameSchedulingNow,
25 - cancelDeferredCallback as ReactNativeFrameSchedulingCancelDeferredCallback,
26 - scheduleDeferredCallback as ReactNativeFrameSchedulingScheduleDeferredCallback,
27 - shouldYield as ReactNativeFrameSchedulingShouldYield,
28 -} from './ReactNativeFrameScheduling';
23 import {get as getViewConfigForType} from 'ReactNativeViewConfigRegistry';
24
25 import deepFreezeAndThrowOnMutationInDev from 'deepFreezeAndThrowOnMutationInDev';
@@ -333,16 +327,10 @@ export function shouldSetTextContent(type: string, props: Props): boolean {
327
328 // The Fabric renderer is secondary to the existing React Native renderer.
329 export const isPrimaryRenderer = false;
336 -export const now = ReactNativeFrameSchedulingNow;
337 -export const scheduleDeferredCallback = ReactNativeFrameSchedulingScheduleDeferredCallback;
338 -export const cancelDeferredCallback = ReactNativeFrameSchedulingCancelDeferredCallback;
339 -export const shouldYield = ReactNativeFrameSchedulingShouldYield;
330
331 export const scheduleTimeout = setTimeout;
332 export const cancelTimeout = clearTimeout;
333 export const noTimeout = -1;
344 -export const schedulePassiveEffects = scheduleDeferredCallback;
345 -export const cancelPassiveEffects = cancelDeferredCallback;
334
335 // -------------------
336 // Persistence
packages/react-native-renderer/src/ReactNativeFrameScheduling.js deleted
-56
@@ -1,56 +0,0 @@
1 -/**
2 - * Copyright (c) Facebook, Inc. and its affiliates.
3 - *
4 - * This source code is licensed under the MIT license found in the
5 - * LICENSE file in the root directory of this source tree.
6 - *
7 - * @flow
8 - */
9 -
10 -const hasNativePerformanceNow =
11 - typeof performance === 'object' && typeof performance.now === 'function';
12 -
13 -const now = hasNativePerformanceNow
14 - ? () => performance.now()
15 - : () => Date.now();
16 -
17 -let scheduledCallback: (() => mixed) | null = null;
18 -let frameDeadline: number = 0;
19 -
20 -function setTimeoutCallback() {
21 - // TODO (bvaughn) Hard-coded 5ms unblocks initial async testing.
22 - // React API probably changing to boolean rather than time remaining.
23 - // Longer-term plan is to rewrite this using shared memory,
24 - // And just return the value of the bit as the boolean.
25 - frameDeadline = now() + 5;
26 -
27 - const callback = scheduledCallback;
28 - scheduledCallback = null;
29 - if (callback !== null) {
30 - callback();
31 - }
32 -}
33 -
34 -// RN has a poor polyfill for requestIdleCallback so we aren't using it.
35 -// This implementation is only intended for short-term use anyway.
36 -// We also don't implement cancel functionality b'c Fiber doesn't currently need it.
37 -function scheduleDeferredCallback(
38 - callback: () => mixed,
39 - options?: {timeout: number},
40 -): number {
41 - // We assume only one callback is scheduled at a time b'c that's how Fiber works.
42 - scheduledCallback = callback;
43 - const timeoutId = setTimeout(setTimeoutCallback, 1);
44 - return (timeoutId: any); // Timeouts are always numbers on RN
45 -}
46 -
47 -function cancelDeferredCallback(callbackID: number) {
48 - scheduledCallback = null;
49 - clearTimeout((callbackID: any)); // Timeouts are always numbers on RN
50 -}
51 -
52 -function shouldYield() {
53 - return frameDeadline <= now();
54 -}
55 -
56 -export {now, scheduleDeferredCallback, cancelDeferredCallback, shouldYield};
packages/react-native-renderer/src/ReactNativeHostConfig.js
-12
@@ -23,12 +23,6 @@ import {
23 updateFiberProps,
24 } from './ReactNativeComponentTree';
25 import ReactNativeFiberHostComponent from './ReactNativeFiberHostComponent';
26 -import {
27 - now as ReactNativeFrameSchedulingNow,
28 - cancelDeferredCallback as ReactNativeFrameSchedulingCancelDeferredCallback,
29 - scheduleDeferredCallback as ReactNativeFrameSchedulingScheduleDeferredCallback,
30 - shouldYield as ReactNativeFrameSchedulingShouldYield,
31 -} from './ReactNativeFrameScheduling';
26
27 export type Type = string;
28 export type Props = Object;
@@ -234,17 +228,11 @@ export function resetAfterCommit(containerInfo: Container): void {
228 // Noop
229 }
230
237 -export const now = ReactNativeFrameSchedulingNow;
231 export const isPrimaryRenderer = true;
239 -export const scheduleDeferredCallback = ReactNativeFrameSchedulingScheduleDeferredCallback;
240 -export const cancelDeferredCallback = ReactNativeFrameSchedulingCancelDeferredCallback;
241 -export const shouldYield = ReactNativeFrameSchedulingShouldYield;
232
233 export const scheduleTimeout = setTimeout;
234 export const cancelTimeout = clearTimeout;
235 export const noTimeout = -1;
246 -export const schedulePassiveEffects = scheduleDeferredCallback;
247 -export const cancelPassiveEffects = cancelDeferredCallback;
236
237 export function shouldDeprioritizeSubtree(type: string, props: Props): boolean {
238 return false;
packages/react-noop-renderer/src/createReactNoop.js
-8
@@ -304,18 +304,10 @@ function createReactNoop(reconciler: Function, useMutation: boolean) {
304 return inst;
305 },
306
307 - scheduleDeferredCallback: Scheduler.unstable_scheduleCallback,
308 - cancelDeferredCallback: Scheduler.unstable_cancelCallback,
309 -
310 - shouldYield: Scheduler.unstable_shouldYield,
311 -
307 scheduleTimeout: setTimeout,
308 cancelTimeout: clearTimeout,
309 noTimeout: -1,
310
316 - schedulePassiveEffects: Scheduler.unstable_scheduleCallback,
317 - cancelPassiveEffects: Scheduler.unstable_cancelCallback,
318 -
311 prepareForCommit(): void {},
312
313 resetAfterCommit(): void {},
packages/react-reconciler/src/ReactFiberScheduler.js
+14 -13
@@ -14,9 +14,7 @@ import type {Interaction} from 'scheduler/src/Tracing';
14
15 // Intentionally not named imports because Rollup would use dynamic dispatch for
16 // CommonJS interop named imports.
17 -// TODO: We're not using this import anymore, but I've left this here so we
18 -// don't accidentally use named imports when we add it back.
19 -// import * as Scheduler from 'scheduler';
17 +import * as Scheduler from 'scheduler';
18 import {
19 __interactionsRef,
20 __subscriberRef,
@@ -78,17 +76,11 @@ import {
76 setCurrentFiber,
77 } from './ReactCurrentFiber';
78 import {
81 - now,
82 - scheduleDeferredCallback,
83 - cancelDeferredCallback,
84 - shouldYield,
79 prepareForCommit,
80 resetAfterCommit,
81 scheduleTimeout,
82 cancelTimeout,
83 noTimeout,
90 - schedulePassiveEffects,
91 - cancelPassiveEffects,
84 } from './ReactFiberHostConfig';
85 import {
86 markPendingPriorityLevel,
@@ -172,6 +164,15 @@ import {
164 } from './ReactFiberCommitWork';
165 import {ContextOnlyDispatcher} from './ReactFiberHooks';
166
167 +// Intentionally not named imports because Rollup would use dynamic dispatch for
168 +// CommonJS interop named imports.
169 +const {
170 + unstable_scheduleCallback: scheduleCallback,
171 + unstable_cancelCallback: cancelCallback,
172 + unstable_shouldYield: shouldYield,
173 + unstable_now: now,
174 +} = Scheduler;
175 +
176 export type Thenable = {
177 then(resolve: () => mixed, reject?: () => mixed): mixed,
178 };
@@ -598,7 +599,7 @@ function markLegacyErrorBoundaryAsFailed(instance: mixed) {
599
600 function flushPassiveEffects() {
601 if (passiveEffectCallbackHandle !== null) {
601 - cancelPassiveEffects(passiveEffectCallbackHandle);
602 + cancelCallback(passiveEffectCallbackHandle);
603 }
604 if (passiveEffectCallback !== null) {
605 // We call the scheduled callback instead of commitPassiveEffects directly
@@ -807,7 +808,7 @@ function commitRoot(root: FiberRoot, finishedWork: Fiber): void {
808 // here because that code is still in flux.
809 callback = Scheduler_tracing_wrap(callback);
810 }
810 - passiveEffectCallbackHandle = schedulePassiveEffects(callback);
811 + passiveEffectCallbackHandle = scheduleCallback(callback);
812 passiveEffectCallback = callback;
813 }
814
@@ -1978,7 +1979,7 @@ function scheduleCallbackWithExpirationTime(
1979 if (callbackID !== null) {
1980 // Existing callback has insufficient timeout. Cancel and schedule a
1981 // new one.
1981 - cancelDeferredCallback(callbackID);
1982 + cancelCallback(callbackID);
1983 }
1984 }
1985 // The request callback timer is already running. Don't start a new one.
@@ -1990,7 +1991,7 @@ function scheduleCallbackWithExpirationTime(
1991 const currentMs = now() - originalStartTimeMs;
1992 const expirationTimeMs = expirationTimeToMs(expirationTime);
1993 const timeout = expirationTimeMs - currentMs;
1993 - callbackID = scheduleDeferredCallback(performAsyncWork, {timeout});
1994 + callbackID = scheduleCallback(performAsyncWork, {timeout});
1995 }
1996
1997 // For every call to renderRoot, one of onFatal, onComplete, onSuspend, and
packages/react-reconciler/src/ReactProfilerTimer.js
+5 -1
@@ -11,7 +11,11 @@ import type {Fiber} from './ReactFiber';
11
12 import {enableProfilerTimer} from 'shared/ReactFeatureFlags';
13
14 -import {now} from './ReactFiberHostConfig';
14 +// Intentionally not named imports because Rollup would use dynamic dispatch for
15 +// CommonJS interop named imports.
16 +import * as Scheduler from 'scheduler';
17 +
18 +const {unstable_now: now} = Scheduler;
19
20 export type ProfilerTimer = {
21 getCommitTime(): number,
packages/react-reconciler/src/forks/ReactFiberHostConfig.custom.js
-5
@@ -51,14 +51,9 @@ export const shouldSetTextContent = $$$hostConfig.shouldSetTextContent;
51 export const shouldDeprioritizeSubtree =
52 $$$hostConfig.shouldDeprioritizeSubtree;
53 export const createTextInstance = $$$hostConfig.createTextInstance;
54 -export const scheduleDeferredCallback = $$$hostConfig.scheduleDeferredCallback;
55 -export const cancelDeferredCallback = $$$hostConfig.cancelDeferredCallback;
56 -export const shouldYield = $$$hostConfig.shouldYield;
54 export const scheduleTimeout = $$$hostConfig.setTimeout;
55 export const cancelTimeout = $$$hostConfig.clearTimeout;
56 export const noTimeout = $$$hostConfig.noTimeout;
60 -export const schedulePassiveEffects = $$$hostConfig.schedulePassiveEffects;
61 -export const cancelPassiveEffects = $$$hostConfig.cancelPassiveEffects;
57 export const now = $$$hostConfig.now;
58 export const isPrimaryRenderer = $$$hostConfig.isPrimaryRenderer;
59 export const supportsMutation = $$$hostConfig.supportsMutation;
packages/react-test-renderer/src/ReactTestHostConfig.js
-9
@@ -7,7 +7,6 @@
7 * @flow
8 */
9
10 -import * as Scheduler from 'scheduler/unstable_mock';
10 import warning from 'shared/warning';
11
12 export type Type = string;
@@ -195,18 +194,10 @@ export function createTextInstance(
194 }
195
196 export const isPrimaryRenderer = false;
198 -// This approach enables `now` to be mocked by tests,
199 -// Even after the reconciler has initialized and read host config values.
200 -export const now = Scheduler.unstable_now;
201 -export const scheduleDeferredCallback = Scheduler.unstable_scheduleCallback;
202 -export const cancelDeferredCallback = Scheduler.unstable_cancelCallback;
203 -export const shouldYield = Scheduler.unstable_shouldYield;
197
198 export const scheduleTimeout = setTimeout;
199 export const cancelTimeout = clearTimeout;
200 export const noTimeout = -1;
208 -export const schedulePassiveEffects = Scheduler.unstable_scheduleCallback;
209 -export const cancelPassiveEffects = Scheduler.unstable_cancelCallback;
201
202 // -------------------
203 // Mutation
packages/react/src/__tests__/ReactProfiler-test.internal.js
+5 -3
@@ -261,7 +261,7 @@ describe('Profiler', () => {
261 it('does not record times for components outside of Profiler tree', () => {
262 // Mock the Scheduler module so we can track how many times the current
263 // time is read
264 - jest.mock('scheduler/unstable_mock', obj => {
264 + jest.mock('scheduler', obj => {
265 const ActualScheduler = require.requireActual(
266 'scheduler/unstable_mock',
267 );
@@ -300,8 +300,10 @@ describe('Profiler', () => {
300 'read current time',
301 ]);
302
303 - // Remove mock
304 - jest.unmock('scheduler/unstable_mock');
303 + // Restore original mock
304 + jest.mock('scheduler', () =>
305 + require.requireActual('scheduler/unstable_mock'),
306 + );
307 });
308
309 it('logs render times for both mount and update', () => {