From b1a56abd6aec4379c2fc9400b413f6f42d0c9b1f Mon Sep 17 00:00:00 2001 From: Andrew Clark Date: Wed, 20 Mar 2019 16:28:33 -0700 Subject: [PATCH] Fork ReactFiberScheduler with feature flag Adds a feature flag `enableNewScheduler` that toggles between two implementations of ReactFiberScheduler. This will let us land changes in master while preserving the ability to quickly rollback. Ideally this will be a short-lived fork. Once we've tested the new scheduler for a week or so without issues, we will get rid of it. Until then, we'll need to maintain two parallel implementations and run tests against both of them. We rarely land changes to ReactFiberScheduler, so I don't expect this will be a huge burden. This commit does not implement anything new. The flag is still off and tests run against the existing implementation. Use `yarn test-new-scheduler` to run tests against the new one. --- package.json | 1 + .../src/ReactFiberBeginWork.js | 2 +- .../src/ReactFiberClassComponent.js | 2 +- .../src/ReactFiberCommitWork.js | 4 +- .../react-reconciler/src/ReactFiberHooks.js | 2 +- .../src/ReactFiberReconciler.js | 2 +- .../react-reconciler/src/ReactFiberRoot.js | 2 +- .../src/ReactFiberScheduler.js | 124 ++++++++++++++++++ .../src/ReactFiberScheduler.new.js | 36 +++++ .../src/ReactFiberUnwindWork.js | 4 +- packages/shared/ReactFeatureFlags.js | 4 + .../forks/ReactFeatureFlags.native-fb.js | 1 + .../forks/ReactFeatureFlags.native-oss.js | 1 + .../forks/ReactFeatureFlags.persistent.js | 1 + .../forks/ReactFeatureFlags.test-renderer.js | 1 + .../ReactFeatureFlags.test-renderer.www.js | 1 + .../shared/forks/ReactFeatureFlags.www.js | 5 + scripts/jest/config.source-new-scheduler.js | 11 ++ scripts/jest/setupNewScheduler.js | 7 + 19 files changed, 202 insertions(+), 9 deletions(-) create mode 100644 packages/react-reconciler/src/ReactFiberScheduler.js create mode 100644 packages/react-reconciler/src/ReactFiberScheduler.new.js create mode 100644 scripts/jest/config.source-new-scheduler.js create mode 100644 scripts/jest/setupNewScheduler.js diff --git a/package.json b/package.json index 29e3aaea389dc..1699d6847ef41 100644 --- a/package.json +++ b/package.json @@ -102,6 +102,7 @@ "test": "cross-env NODE_ENV=development jest --config ./scripts/jest/config.source.js", "test-persistent": "cross-env NODE_ENV=development jest --config ./scripts/jest/config.source-persistent.js", "test-fire": "cross-env NODE_ENV=development jest --config ./scripts/jest/config.source-fire.js", + "test-new-scheduler": "cross-env NODE_ENV=development jest --config ./scripts/jest/config.source-new-scheduler.js", "test-prod": "cross-env NODE_ENV=production jest --config ./scripts/jest/config.source.js", "test-fire-prod": "cross-env NODE_ENV=production jest --config ./scripts/jest/config.source-fire.js", "test-prod-build": "yarn test-build-prod", diff --git a/packages/react-reconciler/src/ReactFiberBeginWork.js b/packages/react-reconciler/src/ReactFiberBeginWork.js index ff2eae797e8bb..3c5e0fddae0e1 100644 --- a/packages/react-reconciler/src/ReactFiberBeginWork.js +++ b/packages/react-reconciler/src/ReactFiberBeginWork.js @@ -144,7 +144,7 @@ import { createWorkInProgress, isSimpleFunctionComponent, } from './ReactFiber'; -import {requestCurrentTime, retryTimedOutBoundary} from './ReactFiberScheduler.old'; +import {requestCurrentTime, retryTimedOutBoundary} from './ReactFiberScheduler'; const ReactCurrentOwner = ReactSharedInternals.ReactCurrentOwner; diff --git a/packages/react-reconciler/src/ReactFiberClassComponent.js b/packages/react-reconciler/src/ReactFiberClassComponent.js index 9ceb7e0302207..17fd298ef7fce 100644 --- a/packages/react-reconciler/src/ReactFiberClassComponent.js +++ b/packages/react-reconciler/src/ReactFiberClassComponent.js @@ -53,7 +53,7 @@ import { computeExpirationForFiber, scheduleWork, flushPassiveEffects, -} from './ReactFiberScheduler.old'; +} from './ReactFiberScheduler'; const fakeInternalInstance = {}; const isArray = Array.isArray; diff --git a/packages/react-reconciler/src/ReactFiberCommitWork.js b/packages/react-reconciler/src/ReactFiberCommitWork.js index e8f7351722e47..a3e479c20ad62 100644 --- a/packages/react-reconciler/src/ReactFiberCommitWork.js +++ b/packages/react-reconciler/src/ReactFiberCommitWork.js @@ -21,7 +21,7 @@ import type {ExpirationTime} from './ReactFiberExpirationTime'; import type {CapturedValue, CapturedError} from './ReactCapturedValue'; import type {SuspenseState} from './ReactFiberSuspenseComponent'; import type {FunctionComponentUpdateQueue} from './ReactFiberHooks'; -import type {Thenable} from './ReactFiberScheduler.old'; +import type {Thenable} from './ReactFiberScheduler'; import {unstable_wrap as Schedule_tracing_wrap} from 'scheduler/tracing'; import { @@ -95,7 +95,7 @@ import { captureCommitPhaseError, requestCurrentTime, resolveRetryThenable, -} from './ReactFiberScheduler.old'; +} from './ReactFiberScheduler'; import { NoEffect as NoHookEffect, UnmountSnapshot, diff --git a/packages/react-reconciler/src/ReactFiberHooks.js b/packages/react-reconciler/src/ReactFiberHooks.js index 541a1732dc8c5..57d8a7c4edb71 100644 --- a/packages/react-reconciler/src/ReactFiberHooks.js +++ b/packages/react-reconciler/src/ReactFiberHooks.js @@ -34,7 +34,7 @@ import { computeExpirationForFiber, flushPassiveEffects, requestCurrentTime, -} from './ReactFiberScheduler.old'; +} from './ReactFiberScheduler'; import invariant from 'shared/invariant'; import warning from 'shared/warning'; diff --git a/packages/react-reconciler/src/ReactFiberReconciler.js b/packages/react-reconciler/src/ReactFiberReconciler.js index bb4b52cb6d451..01e303c74bc42 100644 --- a/packages/react-reconciler/src/ReactFiberReconciler.js +++ b/packages/react-reconciler/src/ReactFiberReconciler.js @@ -54,7 +54,7 @@ import { interactiveUpdates, flushInteractiveUpdates, flushPassiveEffects, -} from './ReactFiberScheduler.old'; +} from './ReactFiberScheduler'; import {createUpdate, enqueueUpdate} from './ReactUpdateQueue'; import ReactFiberInstrumentation from './ReactFiberInstrumentation'; import { diff --git a/packages/react-reconciler/src/ReactFiberRoot.js b/packages/react-reconciler/src/ReactFiberRoot.js index 2405bb40ee921..e3e445b46ef7f 100644 --- a/packages/react-reconciler/src/ReactFiberRoot.js +++ b/packages/react-reconciler/src/ReactFiberRoot.js @@ -10,7 +10,7 @@ import type {Fiber} from './ReactFiber'; import type {ExpirationTime} from './ReactFiberExpirationTime'; import type {TimeoutHandle, NoTimeout} from './ReactFiberHostConfig'; -import type {Thenable} from './ReactFiberScheduler.old'; +import type {Thenable} from './ReactFiberScheduler'; import type {Interaction} from 'scheduler/src/Tracing'; import {noTimeout} from './ReactFiberHostConfig'; diff --git a/packages/react-reconciler/src/ReactFiberScheduler.js b/packages/react-reconciler/src/ReactFiberScheduler.js new file mode 100644 index 0000000000000..5bea74a78b5ff --- /dev/null +++ b/packages/react-reconciler/src/ReactFiberScheduler.js @@ -0,0 +1,124 @@ +/** + * Copyright (c) Facebook, Inc. and its affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + * @flow + */ + +import {enableNewScheduler} from 'shared/ReactFeatureFlags'; + +import { + requestCurrentTime as requestCurrentTime_old, + computeExpirationForFiber as computeExpirationForFiber_old, + captureCommitPhaseError as captureCommitPhaseError_old, + onUncaughtError as onUncaughtError_old, + renderDidSuspend as renderDidSuspend_old, + renderDidError as renderDidError_old, + pingSuspendedRoot as pingSuspendedRoot_old, + retryTimedOutBoundary as retryTimedOutBoundary_old, + resolveRetryThenable as resolveRetryThenable_old, + markLegacyErrorBoundaryAsFailed as markLegacyErrorBoundaryAsFailed_old, + isAlreadyFailedLegacyErrorBoundary as isAlreadyFailedLegacyErrorBoundary_old, + scheduleWork as scheduleWork_old, + requestWork as requestWork_old, + flushRoot as flushRoot_old, + batchedUpdates as batchedUpdates_old, + unbatchedUpdates as unbatchedUpdates_old, + flushSync as flushSync_old, + flushControlled as flushControlled_old, + deferredUpdates as deferredUpdates_old, + syncUpdates as syncUpdates_old, + interactiveUpdates as interactiveUpdates_old, + flushInteractiveUpdates as flushInteractiveUpdates_old, + computeUniqueAsyncExpiration as computeUniqueAsyncExpiration_old, + flushPassiveEffects as flushPassiveEffects_old, + warnIfNotCurrentlyBatchingInDev as warnIfNotCurrentlyBatchingInDev_old, +} from './ReactFiberScheduler.old'; + +import { + requestCurrentTime as requestCurrentTime_new, + computeExpirationForFiber as computeExpirationForFiber_new, + captureCommitPhaseError as captureCommitPhaseError_new, + onUncaughtError as onUncaughtError_new, + renderDidSuspend as renderDidSuspend_new, + renderDidError as renderDidError_new, + pingSuspendedRoot as pingSuspendedRoot_new, + retryTimedOutBoundary as retryTimedOutBoundary_new, + resolveRetryThenable as resolveRetryThenable_new, + markLegacyErrorBoundaryAsFailed as markLegacyErrorBoundaryAsFailed_new, + isAlreadyFailedLegacyErrorBoundary as isAlreadyFailedLegacyErrorBoundary_new, + scheduleWork as scheduleWork_new, + requestWork as requestWork_new, + flushRoot as flushRoot_new, + batchedUpdates as batchedUpdates_new, + unbatchedUpdates as unbatchedUpdates_new, + flushSync as flushSync_new, + flushControlled as flushControlled_new, + deferredUpdates as deferredUpdates_new, + syncUpdates as syncUpdates_new, + interactiveUpdates as interactiveUpdates_new, + flushInteractiveUpdates as flushInteractiveUpdates_new, + computeUniqueAsyncExpiration as computeUniqueAsyncExpiration_new, + flushPassiveEffects as flushPassiveEffects_new, + warnIfNotCurrentlyBatchingInDev as warnIfNotCurrentlyBatchingInDev_new, +} from './ReactFiberScheduler.new'; + +export let requestCurrentTime = requestCurrentTime_old; +export let computeExpirationForFiber = computeExpirationForFiber_old; +export let captureCommitPhaseError = captureCommitPhaseError_old; +export let onUncaughtError = onUncaughtError_old; +export let renderDidSuspend = renderDidSuspend_old; +export let renderDidError = renderDidError_old; +export let pingSuspendedRoot = pingSuspendedRoot_old; +export let retryTimedOutBoundary = retryTimedOutBoundary_old; +export let resolveRetryThenable = resolveRetryThenable_old; +export let markLegacyErrorBoundaryAsFailed = markLegacyErrorBoundaryAsFailed_old; +export let isAlreadyFailedLegacyErrorBoundary = isAlreadyFailedLegacyErrorBoundary_old; +export let scheduleWork = scheduleWork_old; +export let requestWork = requestWork_old; +export let flushRoot = flushRoot_old; +export let batchedUpdates = batchedUpdates_old; +export let unbatchedUpdates = unbatchedUpdates_old; +export let flushSync = flushSync_old; +export let flushControlled = flushControlled_old; +export let deferredUpdates = deferredUpdates_old; +export let syncUpdates = syncUpdates_old; +export let interactiveUpdates = interactiveUpdates_old; +export let flushInteractiveUpdates = flushInteractiveUpdates_old; +export let computeUniqueAsyncExpiration = computeUniqueAsyncExpiration_old; +export let flushPassiveEffects = flushPassiveEffects_old; +export let warnIfNotCurrentlyBatchingInDev = warnIfNotCurrentlyBatchingInDev_old; + +if (enableNewScheduler) { + requestCurrentTime = requestCurrentTime_new; + computeExpirationForFiber = computeExpirationForFiber_new; + captureCommitPhaseError = captureCommitPhaseError_new; + onUncaughtError = onUncaughtError_new; + renderDidSuspend = renderDidSuspend_new; + renderDidError = renderDidError_new; + pingSuspendedRoot = pingSuspendedRoot_new; + retryTimedOutBoundary = retryTimedOutBoundary_new; + resolveRetryThenable = resolveRetryThenable_new; + markLegacyErrorBoundaryAsFailed = markLegacyErrorBoundaryAsFailed_new; + isAlreadyFailedLegacyErrorBoundary = isAlreadyFailedLegacyErrorBoundary_new; + scheduleWork = scheduleWork_new; + requestWork = requestWork_new; + flushRoot = flushRoot_new; + batchedUpdates = batchedUpdates_new; + unbatchedUpdates = unbatchedUpdates_new; + flushSync = flushSync_new; + flushControlled = flushControlled_new; + deferredUpdates = deferredUpdates_new; + syncUpdates = syncUpdates_new; + interactiveUpdates = interactiveUpdates_new; + flushInteractiveUpdates = flushInteractiveUpdates_new; + computeUniqueAsyncExpiration = computeUniqueAsyncExpiration_new; + flushPassiveEffects = flushPassiveEffects_new; + warnIfNotCurrentlyBatchingInDev = warnIfNotCurrentlyBatchingInDev_new; +} + +export type Thenable = { + then(resolve: () => mixed, reject?: () => mixed): mixed, +}; diff --git a/packages/react-reconciler/src/ReactFiberScheduler.new.js b/packages/react-reconciler/src/ReactFiberScheduler.new.js new file mode 100644 index 0000000000000..e4df9dad4fe72 --- /dev/null +++ b/packages/react-reconciler/src/ReactFiberScheduler.new.js @@ -0,0 +1,36 @@ +/** + * Copyright (c) Facebook, Inc. and its affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +function notYetImplemented() { + throw new Error('Not yet implemented.'); +} + +export const requestCurrentTime = notYetImplemented; +export const computeExpirationForFiber = notYetImplemented; +export const captureCommitPhaseError = notYetImplemented; +export const onUncaughtError = notYetImplemented; +export const renderDidSuspend = notYetImplemented; +export const renderDidError = notYetImplemented; +export const pingSuspendedRoot = notYetImplemented; +export const retryTimedOutBoundary = notYetImplemented; +export const resolveRetryThenable = notYetImplemented; +export const markLegacyErrorBoundaryAsFailed = notYetImplemented; +export const isAlreadyFailedLegacyErrorBoundary = notYetImplemented; +export const scheduleWork = notYetImplemented; +export const requestWork = notYetImplemented; +export const flushRoot = notYetImplemented; +export const batchedUpdates = notYetImplemented; +export const unbatchedUpdates = notYetImplemented; +export const flushSync = notYetImplemented; +export const flushControlled = notYetImplemented; +export const deferredUpdates = notYetImplemented; +export const syncUpdates = notYetImplemented; +export const interactiveUpdates = notYetImplemented; +export const flushInteractiveUpdates = notYetImplemented; +export const computeUniqueAsyncExpiration = notYetImplemented; +export const flushPassiveEffects = notYetImplemented; +export const warnIfNotCurrentlyBatchingInDev = notYetImplemented; diff --git a/packages/react-reconciler/src/ReactFiberUnwindWork.js b/packages/react-reconciler/src/ReactFiberUnwindWork.js index 65f4c333f82d1..2d76a3b6b5307 100644 --- a/packages/react-reconciler/src/ReactFiberUnwindWork.js +++ b/packages/react-reconciler/src/ReactFiberUnwindWork.js @@ -12,7 +12,7 @@ import type {FiberRoot} from './ReactFiberRoot'; import type {ExpirationTime} from './ReactFiberExpirationTime'; import type {CapturedValue} from './ReactCapturedValue'; import type {Update} from './ReactUpdateQueue'; -import type {Thenable} from './ReactFiberScheduler.old'; +import type {Thenable} from './ReactFiberScheduler'; import type {SuspenseState} from './ReactFiberSuspenseComponent'; import {unstable_wrap as Schedule_tracing_wrap} from 'scheduler/tracing'; @@ -67,7 +67,7 @@ import { isAlreadyFailedLegacyErrorBoundary, pingSuspendedRoot, resolveRetryThenable, -} from './ReactFiberScheduler.old'; +} from './ReactFiberScheduler'; import invariant from 'shared/invariant'; import maxSigned31BitInt from './maxSigned31BitInt'; diff --git a/packages/shared/ReactFeatureFlags.js b/packages/shared/ReactFeatureFlags.js index 95108e7d96239..9ace0090d6ca7 100644 --- a/packages/shared/ReactFeatureFlags.js +++ b/packages/shared/ReactFeatureFlags.js @@ -64,3 +64,7 @@ export const warnAboutDeprecatedSetNativeProps = false; // Experimental React Events support. Only used in www builds for now. export const enableEventAPI = false; + +// Enables rewritten version of ReactFiberScheduler. Added in case we need to +// quickly revert it. +export const enableNewScheduler = false; diff --git a/packages/shared/forks/ReactFeatureFlags.native-fb.js b/packages/shared/forks/ReactFeatureFlags.native-fb.js index 64db95b8d8484..658f7781ff313 100644 --- a/packages/shared/forks/ReactFeatureFlags.native-fb.js +++ b/packages/shared/forks/ReactFeatureFlags.native-fb.js @@ -31,6 +31,7 @@ export const replayFailedUnitOfWorkWithInvokeGuardedCallback = __DEV__; export const warnAboutDeprecatedLifecycles = true; export const warnAboutDeprecatedSetNativeProps = true; export const enableEventAPI = false; +export const enableNewScheduler = false; // Only used in www builds. export function addUserTimingListener() { diff --git a/packages/shared/forks/ReactFeatureFlags.native-oss.js b/packages/shared/forks/ReactFeatureFlags.native-oss.js index a5cfb3d29435d..e264fd3a34419 100644 --- a/packages/shared/forks/ReactFeatureFlags.native-oss.js +++ b/packages/shared/forks/ReactFeatureFlags.native-oss.js @@ -28,6 +28,7 @@ export const warnAboutShorthandPropertyCollision = false; export const enableSchedulerDebugging = false; export const warnAboutDeprecatedSetNativeProps = false; export const enableEventAPI = false; +export const enableNewScheduler = false; // Only used in www builds. export function addUserTimingListener() { diff --git a/packages/shared/forks/ReactFeatureFlags.persistent.js b/packages/shared/forks/ReactFeatureFlags.persistent.js index 692864b4cfd57..2b048a082236f 100644 --- a/packages/shared/forks/ReactFeatureFlags.persistent.js +++ b/packages/shared/forks/ReactFeatureFlags.persistent.js @@ -28,6 +28,7 @@ export const warnAboutShorthandPropertyCollision = false; export const enableSchedulerDebugging = false; export const warnAboutDeprecatedSetNativeProps = false; export const enableEventAPI = false; +export const enableNewScheduler = false; // Only used in www builds. export function addUserTimingListener() { diff --git a/packages/shared/forks/ReactFeatureFlags.test-renderer.js b/packages/shared/forks/ReactFeatureFlags.test-renderer.js index bd9aa64dd4764..08fe309d20b79 100644 --- a/packages/shared/forks/ReactFeatureFlags.test-renderer.js +++ b/packages/shared/forks/ReactFeatureFlags.test-renderer.js @@ -28,6 +28,7 @@ export const warnAboutShorthandPropertyCollision = false; export const enableSchedulerDebugging = false; export const warnAboutDeprecatedSetNativeProps = false; export const enableEventAPI = false; +export const enableNewScheduler = false; // Only used in www builds. export function addUserTimingListener() { diff --git a/packages/shared/forks/ReactFeatureFlags.test-renderer.www.js b/packages/shared/forks/ReactFeatureFlags.test-renderer.www.js index 096494b839ef7..4b5df2e904080 100644 --- a/packages/shared/forks/ReactFeatureFlags.test-renderer.www.js +++ b/packages/shared/forks/ReactFeatureFlags.test-renderer.www.js @@ -26,6 +26,7 @@ export const warnAboutDeprecatedSetNativeProps = false; export const disableJavaScriptURLs = false; export const disableYielding = false; export const enableEventAPI = true; +export const enableNewScheduler = false; // Only used in www builds. export function addUserTimingListener() { diff --git a/packages/shared/forks/ReactFeatureFlags.www.js b/packages/shared/forks/ReactFeatureFlags.www.js index e47428a52a3c3..f47b6097e4027 100644 --- a/packages/shared/forks/ReactFeatureFlags.www.js +++ b/packages/shared/forks/ReactFeatureFlags.www.js @@ -39,6 +39,11 @@ export const enableStableConcurrentModeAPIs = false; export const enableSuspenseServerRenderer = true; +// I've chosen to make this a static flag instead of a dynamic flag controlled +// by a GK so that it doesn't increase bundle size. It should still be easy +// to rollback by reverting the commit that turns this on. +export const enableNewScheduler = false; + let refCount = 0; export function addUserTimingListener() { if (__DEV__) { diff --git a/scripts/jest/config.source-new-scheduler.js b/scripts/jest/config.source-new-scheduler.js new file mode 100644 index 0000000000000..6d74d5bb1b0fa --- /dev/null +++ b/scripts/jest/config.source-new-scheduler.js @@ -0,0 +1,11 @@ +'use strict'; + +const baseConfig = require('./config.base'); + +module.exports = Object.assign({}, baseConfig, { + setupFiles: [ + ...baseConfig.setupFiles, + require.resolve('./setupNewScheduler.js'), + require.resolve('./setupHostConfigs.js'), + ], +}); diff --git a/scripts/jest/setupNewScheduler.js b/scripts/jest/setupNewScheduler.js new file mode 100644 index 0000000000000..d3d58bd5653db --- /dev/null +++ b/scripts/jest/setupNewScheduler.js @@ -0,0 +1,7 @@ +'use strict'; + +jest.mock('shared/ReactFeatureFlags', () => { + const ReactFeatureFlags = require.requireActual('shared/ReactFeatureFlags'); + ReactFeatureFlags.enableNewScheduler = true; + return ReactFeatureFlags; +});