diff --git a/packages/react-cs-renderer/src/ReactNativeCSFeatureFlags.js b/packages/react-cs-renderer/src/ReactNativeCSFeatureFlags.js index 7922bd373d4..a7e3f5fc56e 100644 --- a/packages/react-cs-renderer/src/ReactNativeCSFeatureFlags.js +++ b/packages/react-cs-renderer/src/ReactNativeCSFeatureFlags.js @@ -7,6 +7,8 @@ * @flow */ +import invariant from 'fbjs/lib/invariant'; + import typeof * as FeatureFlagsType from 'shared/ReactFeatureFlags'; import typeof * as CSFeatureFlagsType from './ReactNativeCSFeatureFlags'; @@ -14,12 +16,18 @@ export const enableAsyncSubtreeAPI = true; export const enableAsyncSchedulingByDefaultInReactDOM = false; export const enableReactFragment = false; export const enableCreateRoot = false; +export const enableUserTimingAPI = __DEV__; // React Native CS uses persistent reconciler. export const enableMutatingReconciler = false; export const enableNoopReconciler = false; export const enablePersistentReconciler = true; +// Only used in www builds. +export function addUserTimingListener() { + invariant(false, 'Not implemented.'); +} + // Flow magic to verify the exports of this file match the original version. // eslint-disable-next-line no-unused-vars type Check<_X, Y: _X, X: Y=_X> = null; diff --git a/packages/react-dom/src/client/ReactDOMFB.js b/packages/react-dom/src/client/ReactDOMFB.js index 36688af6258..2be4ee489d1 100644 --- a/packages/react-dom/src/client/ReactDOMFB.js +++ b/packages/react-dom/src/client/ReactDOMFB.js @@ -13,6 +13,7 @@ import * as ReactInstanceMap from 'shared/ReactInstanceMap'; import * as ReactFiberErrorLogger from 'react-reconciler/src/ReactFiberErrorLogger'; import ReactErrorUtils from 'shared/ReactErrorUtils'; +import {addUserTimingListener} from 'shared/ReactFeatureFlags'; import ReactDOM from './ReactDOM'; import * as ReactBrowserEventEmitter from '../events/ReactBrowserEventEmitter'; @@ -31,6 +32,8 @@ Object.assign( ReactInstanceMap, // Used by www msite: TapEventPlugin, + // Perf experiment + addUserTimingListener, }, ); diff --git a/packages/react-reconciler/src/ReactDebugFiberPerf.js b/packages/react-reconciler/src/ReactDebugFiberPerf.js index 8cbfcb724b8..ad04bc9ceca 100644 --- a/packages/react-reconciler/src/ReactDebugFiberPerf.js +++ b/packages/react-reconciler/src/ReactDebugFiberPerf.js @@ -9,6 +9,7 @@ import type {Fiber} from './ReactFiber'; +import {enableUserTimingAPI} from 'shared/ReactFeatureFlags'; import getComponentName from 'shared/getComponentName'; import { HostRoot, @@ -19,9 +20,6 @@ import { Fragment, } from 'shared/ReactTypeOfWork'; -// Trust the developer to only use this with a __DEV__ check -let ReactDebugFiberPerf = (({}: any): typeof ReactDebugFiberPerf); - type MeasurementPhase = | 'componentWillMount' | 'componentWillUnmount' @@ -32,382 +30,406 @@ type MeasurementPhase = | 'componentDidMount' | 'getChildContext'; -if (__DEV__) { - // Prefix measurements so that it's possible to filter them. - // Longer prefixes are hard to read in DevTools. - const reactEmoji = '\u269B'; - const warningEmoji = '\u26D4'; - const supportsUserTiming = - typeof performance !== 'undefined' && - typeof performance.mark === 'function' && - typeof performance.clearMarks === 'function' && - typeof performance.measure === 'function' && - typeof performance.clearMeasures === 'function'; - - // Keep track of current fiber so that we know the path to unwind on pause. - // TODO: this looks the same as nextUnitOfWork in scheduler. Can we unify them? - let currentFiber: Fiber | null = null; - // If we're in the middle of user code, which fiber and method is it? - // Reusing `currentFiber` would be confusing for this because user code fiber - // can change during commit phase too, but we don't need to unwind it (since - // lifecycles in the commit phase don't resemble a tree). - let currentPhase: MeasurementPhase | null = null; - let currentPhaseFiber: Fiber | null = null; - // Did lifecycle hook schedule an update? This is often a performance problem, - // so we will keep track of it, and include it in the report. - // Track commits caused by cascading updates. - let isCommitting: boolean = false; - let hasScheduledUpdateInCurrentCommit: boolean = false; - let hasScheduledUpdateInCurrentPhase: boolean = false; - let commitCountInCurrentWorkLoop: number = 0; - let effectCountInCurrentCommit: number = 0; - // During commits, we only show a measurement once per method name - // to avoid stretch the commit phase with measurement overhead. - const labelsInCurrentCommit: Set = new Set(); - - const formatMarkName = (markName: string) => { - return `${reactEmoji} ${markName}`; - }; - - const formatLabel = (label: string, warning: string | null) => { - const prefix = warning ? `${warningEmoji} ` : `${reactEmoji} `; - const suffix = warning ? ` Warning: ${warning}` : ''; - return `${prefix}${label}${suffix}`; - }; - - const beginMark = (markName: string) => { - performance.mark(formatMarkName(markName)); - }; - - const clearMark = (markName: string) => { - performance.clearMarks(formatMarkName(markName)); - }; - - const endMark = (label: string, markName: string, warning: string | null) => { - const formattedMarkName = formatMarkName(markName); - const formattedLabel = formatLabel(label, warning); - try { - performance.measure(formattedLabel, formattedMarkName); - } catch (err) { - // If previous mark was missing for some reason, this will throw. - // This could only happen if React crashed in an unexpected place earlier. - // Don't pile on with more errors. +// Prefix measurements so that it's possible to filter them. +// Longer prefixes are hard to read in DevTools. +const reactEmoji = '\u269B'; +const warningEmoji = '\u26D4'; +const supportsUserTiming = + typeof performance !== 'undefined' && + typeof performance.mark === 'function' && + typeof performance.clearMarks === 'function' && + typeof performance.measure === 'function' && + typeof performance.clearMeasures === 'function'; + +// Keep track of current fiber so that we know the path to unwind on pause. +// TODO: this looks the same as nextUnitOfWork in scheduler. Can we unify them? +let currentFiber: Fiber | null = null; +// If we're in the middle of user code, which fiber and method is it? +// Reusing `currentFiber` would be confusing for this because user code fiber +// can change during commit phase too, but we don't need to unwind it (since +// lifecycles in the commit phase don't resemble a tree). +let currentPhase: MeasurementPhase | null = null; +let currentPhaseFiber: Fiber | null = null; +// Did lifecycle hook schedule an update? This is often a performance problem, +// so we will keep track of it, and include it in the report. +// Track commits caused by cascading updates. +let isCommitting: boolean = false; +let hasScheduledUpdateInCurrentCommit: boolean = false; +let hasScheduledUpdateInCurrentPhase: boolean = false; +let commitCountInCurrentWorkLoop: number = 0; +let effectCountInCurrentCommit: number = 0; +// During commits, we only show a measurement once per method name +// to avoid stretch the commit phase with measurement overhead. +const labelsInCurrentCommit: Set = new Set(); + +const formatMarkName = (markName: string) => { + return `${reactEmoji} ${markName}`; +}; + +const formatLabel = (label: string, warning: string | null) => { + const prefix = warning ? `${warningEmoji} ` : `${reactEmoji} `; + const suffix = warning ? ` Warning: ${warning}` : ''; + return `${prefix}${label}${suffix}`; +}; + +const beginMark = (markName: string) => { + performance.mark(formatMarkName(markName)); +}; + +const clearMark = (markName: string) => { + performance.clearMarks(formatMarkName(markName)); +}; + +const endMark = (label: string, markName: string, warning: string | null) => { + const formattedMarkName = formatMarkName(markName); + const formattedLabel = formatLabel(label, warning); + try { + performance.measure(formattedLabel, formattedMarkName); + } catch (err) { + // If previous mark was missing for some reason, this will throw. + // This could only happen if React crashed in an unexpected place earlier. + // Don't pile on with more errors. + } + // Clear marks immediately to avoid growing buffer. + performance.clearMarks(formattedMarkName); + performance.clearMeasures(formattedLabel); +}; + +const getFiberMarkName = (label: string, debugID: number) => { + return `${label} (#${debugID})`; +}; + +const getFiberLabel = ( + componentName: string, + isMounted: boolean, + phase: MeasurementPhase | null, +) => { + if (phase === null) { + // These are composite component total time measurements. + return `${componentName} [${isMounted ? 'update' : 'mount'}]`; + } else { + // Composite component methods. + return `${componentName}.${phase}`; + } +}; + +const beginFiberMark = ( + fiber: Fiber, + phase: MeasurementPhase | null, +): boolean => { + const componentName = getComponentName(fiber) || 'Unknown'; + const debugID = ((fiber._debugID: any): number); + const isMounted = fiber.alternate !== null; + const label = getFiberLabel(componentName, isMounted, phase); + + if (isCommitting && labelsInCurrentCommit.has(label)) { + // During the commit phase, we don't show duplicate labels because + // there is a fixed overhead for every measurement, and we don't + // want to stretch the commit phase beyond necessary. + return false; + } + labelsInCurrentCommit.add(label); + + const markName = getFiberMarkName(label, debugID); + beginMark(markName); + return true; +}; + +const clearFiberMark = (fiber: Fiber, phase: MeasurementPhase | null) => { + const componentName = getComponentName(fiber) || 'Unknown'; + const debugID = ((fiber._debugID: any): number); + const isMounted = fiber.alternate !== null; + const label = getFiberLabel(componentName, isMounted, phase); + const markName = getFiberMarkName(label, debugID); + clearMark(markName); +}; + +const endFiberMark = ( + fiber: Fiber, + phase: MeasurementPhase | null, + warning: string | null, +) => { + const componentName = getComponentName(fiber) || 'Unknown'; + const debugID = ((fiber._debugID: any): number); + const isMounted = fiber.alternate !== null; + const label = getFiberLabel(componentName, isMounted, phase); + const markName = getFiberMarkName(label, debugID); + endMark(label, markName, warning); +}; + +const shouldIgnoreFiber = (fiber: Fiber): boolean => { + // Host components should be skipped in the timeline. + // We could check typeof fiber.type, but does this work with RN? + switch (fiber.tag) { + case HostRoot: + case HostComponent: + case HostText: + case HostPortal: + case ReturnComponent: + case Fragment: + return true; + default: + return false; + } +}; + +const clearPendingPhaseMeasurement = () => { + if (currentPhase !== null && currentPhaseFiber !== null) { + clearFiberMark(currentPhaseFiber, currentPhase); + } + currentPhaseFiber = null; + currentPhase = null; + hasScheduledUpdateInCurrentPhase = false; +}; + +const pauseTimers = () => { + // Stops all currently active measurements so that they can be resumed + // if we continue in a later deferred loop from the same unit of work. + let fiber = currentFiber; + while (fiber) { + if (fiber._debugIsCurrentlyTiming) { + endFiberMark(fiber, null, null); } - // Clear marks immediately to avoid growing buffer. - performance.clearMarks(formattedMarkName); - performance.clearMeasures(formattedLabel); - }; - - const getFiberMarkName = (label: string, debugID: number) => { - return `${label} (#${debugID})`; - }; - - const getFiberLabel = ( - componentName: string, - isMounted: boolean, - phase: MeasurementPhase | null, - ) => { - if (phase === null) { - // These are composite component total time measurements. - return `${componentName} [${isMounted ? 'update' : 'mount'}]`; - } else { - // Composite component methods. - return `${componentName}.${phase}`; + fiber = fiber.return; + } +}; + +const resumeTimersRecursively = (fiber: Fiber) => { + if (fiber.return !== null) { + resumeTimersRecursively(fiber.return); + } + if (fiber._debugIsCurrentlyTiming) { + beginFiberMark(fiber, null); + } +}; + +const resumeTimers = () => { + // Resumes all measurements that were active during the last deferred loop. + if (currentFiber !== null) { + resumeTimersRecursively(currentFiber); + } +}; + +export function recordEffect(): void { + if (enableUserTimingAPI) { + effectCountInCurrentCommit++; + } +} + +export function recordScheduleUpdate(): void { + if (enableUserTimingAPI) { + if (isCommitting) { + hasScheduledUpdateInCurrentCommit = true; } - }; - - const beginFiberMark = ( - fiber: Fiber, - phase: MeasurementPhase | null, - ): boolean => { - const componentName = getComponentName(fiber) || 'Unknown'; - const debugID = ((fiber._debugID: any): number); - const isMounted = fiber.alternate !== null; - const label = getFiberLabel(componentName, isMounted, phase); - - if (isCommitting && labelsInCurrentCommit.has(label)) { - // During the commit phase, we don't show duplicate labels because - // there is a fixed overhead for every measurement, and we don't - // want to stretch the commit phase beyond necessary. - return false; + if ( + currentPhase !== null && + currentPhase !== 'componentWillMount' && + currentPhase !== 'componentWillReceiveProps' + ) { + hasScheduledUpdateInCurrentPhase = true; } - labelsInCurrentCommit.add(label); - - const markName = getFiberMarkName(label, debugID); - beginMark(markName); - return true; - }; - - const clearFiberMark = (fiber: Fiber, phase: MeasurementPhase | null) => { - const componentName = getComponentName(fiber) || 'Unknown'; - const debugID = ((fiber._debugID: any): number); - const isMounted = fiber.alternate !== null; - const label = getFiberLabel(componentName, isMounted, phase); - const markName = getFiberMarkName(label, debugID); - clearMark(markName); - }; - - const endFiberMark = ( - fiber: Fiber, - phase: MeasurementPhase | null, - warning: string | null, - ) => { - const componentName = getComponentName(fiber) || 'Unknown'; - const debugID = ((fiber._debugID: any): number); - const isMounted = fiber.alternate !== null; - const label = getFiberLabel(componentName, isMounted, phase); - const markName = getFiberMarkName(label, debugID); - endMark(label, markName, warning); - }; - - const shouldIgnoreFiber = (fiber: Fiber): boolean => { - // Host components should be skipped in the timeline. - // We could check typeof fiber.type, but does this work with RN? - switch (fiber.tag) { - case HostRoot: - case HostComponent: - case HostText: - case HostPortal: - case ReturnComponent: - case Fragment: - return true; - default: - return false; + } +} + +export function startWorkTimer(fiber: Fiber): void { + if (enableUserTimingAPI) { + if (!supportsUserTiming || shouldIgnoreFiber(fiber)) { + return; } - }; + // If we pause, this is the fiber to unwind from. + currentFiber = fiber; + if (!beginFiberMark(fiber, null)) { + return; + } + fiber._debugIsCurrentlyTiming = true; + } +} + +export function cancelWorkTimer(fiber: Fiber): void { + if (enableUserTimingAPI) { + if (!supportsUserTiming || shouldIgnoreFiber(fiber)) { + return; + } + // Remember we shouldn't complete measurement for this fiber. + // Otherwise flamechart will be deep even for small updates. + fiber._debugIsCurrentlyTiming = false; + clearFiberMark(fiber, null); + } +} - const clearPendingPhaseMeasurement = () => { +export function stopWorkTimer(fiber: Fiber): void { + if (enableUserTimingAPI) { + if (!supportsUserTiming || shouldIgnoreFiber(fiber)) { + return; + } + // If we pause, its parent is the fiber to unwind from. + currentFiber = fiber.return; + if (!fiber._debugIsCurrentlyTiming) { + return; + } + fiber._debugIsCurrentlyTiming = false; + endFiberMark(fiber, null, null); + } +} + +export function stopFailedWorkTimer(fiber: Fiber): void { + if (enableUserTimingAPI) { + if (!supportsUserTiming || shouldIgnoreFiber(fiber)) { + return; + } + // If we pause, its parent is the fiber to unwind from. + currentFiber = fiber.return; + if (!fiber._debugIsCurrentlyTiming) { + return; + } + fiber._debugIsCurrentlyTiming = false; + const warning = 'An error was thrown inside this error boundary'; + endFiberMark(fiber, null, warning); + } +} + +export function startPhaseTimer(fiber: Fiber, phase: MeasurementPhase): void { + if (enableUserTimingAPI) { + if (!supportsUserTiming) { + return; + } + clearPendingPhaseMeasurement(); + if (!beginFiberMark(fiber, phase)) { + return; + } + currentPhaseFiber = fiber; + currentPhase = phase; + } +} + +export function stopPhaseTimer(): void { + if (enableUserTimingAPI) { + if (!supportsUserTiming) { + return; + } if (currentPhase !== null && currentPhaseFiber !== null) { - clearFiberMark(currentPhaseFiber, currentPhase); + const warning = hasScheduledUpdateInCurrentPhase + ? 'Scheduled a cascading update' + : null; + endFiberMark(currentPhaseFiber, currentPhase, warning); } - currentPhaseFiber = null; currentPhase = null; - hasScheduledUpdateInCurrentPhase = false; - }; - - const pauseTimers = () => { - // Stops all currently active measurements so that they can be resumed - // if we continue in a later deferred loop from the same unit of work. - let fiber = currentFiber; - while (fiber) { - if (fiber._debugIsCurrentlyTiming) { - endFiberMark(fiber, null, null); - } - fiber = fiber.return; + currentPhaseFiber = null; + } +} + +export function startWorkLoopTimer(): void { + if (enableUserTimingAPI) { + if (!supportsUserTiming) { + return; } - }; + commitCountInCurrentWorkLoop = 0; + // This is top level call. + // Any other measurements are performed within. + beginMark('(React Tree Reconciliation)'); + // Resume any measurements that were in progress during the last loop. + resumeTimers(); + } +} - const resumeTimersRecursively = (fiber: Fiber) => { - if (fiber.return !== null) { - resumeTimersRecursively(fiber.return); +export function stopWorkLoopTimer(): void { + if (enableUserTimingAPI) { + if (!supportsUserTiming) { + return; } - if (fiber._debugIsCurrentlyTiming) { - beginFiberMark(fiber, null); + const warning = commitCountInCurrentWorkLoop > 1 + ? 'There were cascading updates' + : null; + commitCountInCurrentWorkLoop = 0; + // Pause any measurements until the next loop. + pauseTimers(); + endMark( + '(React Tree Reconciliation)', + '(React Tree Reconciliation)', + warning, + ); + } +} + +export function startCommitTimer(): void { + if (enableUserTimingAPI) { + if (!supportsUserTiming) { + return; } - }; + isCommitting = true; + hasScheduledUpdateInCurrentCommit = false; + labelsInCurrentCommit.clear(); + beginMark('(Committing Changes)'); + } +} - const resumeTimers = () => { - // Resumes all measurements that were active during the last deferred loop. - if (currentFiber !== null) { - resumeTimersRecursively(currentFiber); +export function stopCommitTimer(): void { + if (enableUserTimingAPI) { + if (!supportsUserTiming) { + return; } - }; - - ReactDebugFiberPerf = { - recordEffect(): void { - effectCountInCurrentCommit++; - }, - - recordScheduleUpdate(): void { - if (isCommitting) { - hasScheduledUpdateInCurrentCommit = true; - } - if ( - currentPhase !== null && - currentPhase !== 'componentWillMount' && - currentPhase !== 'componentWillReceiveProps' - ) { - hasScheduledUpdateInCurrentPhase = true; - } - }, - - startWorkTimer(fiber: Fiber): void { - if (!supportsUserTiming || shouldIgnoreFiber(fiber)) { - return; - } - // If we pause, this is the fiber to unwind from. - currentFiber = fiber; - if (!beginFiberMark(fiber, null)) { - return; - } - fiber._debugIsCurrentlyTiming = true; - }, - - cancelWorkTimer(fiber: Fiber): void { - if (!supportsUserTiming || shouldIgnoreFiber(fiber)) { - return; - } - // Remember we shouldn't complete measurement for this fiber. - // Otherwise flamechart will be deep even for small updates. - fiber._debugIsCurrentlyTiming = false; - clearFiberMark(fiber, null); - }, - - stopWorkTimer(fiber: Fiber): void { - if (!supportsUserTiming || shouldIgnoreFiber(fiber)) { - return; - } - // If we pause, its parent is the fiber to unwind from. - currentFiber = fiber.return; - if (!fiber._debugIsCurrentlyTiming) { - return; - } - fiber._debugIsCurrentlyTiming = false; - endFiberMark(fiber, null, null); - }, - - stopFailedWorkTimer(fiber: Fiber): void { - if (!supportsUserTiming || shouldIgnoreFiber(fiber)) { - return; - } - // If we pause, its parent is the fiber to unwind from. - currentFiber = fiber.return; - if (!fiber._debugIsCurrentlyTiming) { - return; - } - fiber._debugIsCurrentlyTiming = false; - const warning = 'An error was thrown inside this error boundary'; - endFiberMark(fiber, null, warning); - }, - - startPhaseTimer(fiber: Fiber, phase: MeasurementPhase): void { - if (!supportsUserTiming) { - return; - } - clearPendingPhaseMeasurement(); - if (!beginFiberMark(fiber, phase)) { - return; - } - currentPhaseFiber = fiber; - currentPhase = phase; - }, - - stopPhaseTimer(): void { - if (!supportsUserTiming) { - return; - } - if (currentPhase !== null && currentPhaseFiber !== null) { - const warning = hasScheduledUpdateInCurrentPhase - ? 'Scheduled a cascading update' - : null; - endFiberMark(currentPhaseFiber, currentPhase, warning); - } - currentPhase = null; - currentPhaseFiber = null; - }, - - startWorkLoopTimer(): void { - if (!supportsUserTiming) { - return; - } - commitCountInCurrentWorkLoop = 0; - // This is top level call. - // Any other measurements are performed within. - beginMark('(React Tree Reconciliation)'); - // Resume any measurements that were in progress during the last loop. - resumeTimers(); - }, - - stopWorkLoopTimer(): void { - if (!supportsUserTiming) { - return; - } - const warning = commitCountInCurrentWorkLoop > 1 - ? 'There were cascading updates' - : null; - commitCountInCurrentWorkLoop = 0; - // Pause any measurements until the next loop. - pauseTimers(); - endMark( - '(React Tree Reconciliation)', - '(React Tree Reconciliation)', - warning, - ); - }, - - startCommitTimer(): void { - if (!supportsUserTiming) { - return; - } - isCommitting = true; - hasScheduledUpdateInCurrentCommit = false; - labelsInCurrentCommit.clear(); - beginMark('(Committing Changes)'); - }, - - stopCommitTimer(): void { - if (!supportsUserTiming) { - return; - } - - let warning = null; - if (hasScheduledUpdateInCurrentCommit) { - warning = 'Lifecycle hook scheduled a cascading update'; - } else if (commitCountInCurrentWorkLoop > 0) { - warning = 'Caused by a cascading update in earlier commit'; - } - hasScheduledUpdateInCurrentCommit = false; - commitCountInCurrentWorkLoop++; - isCommitting = false; - labelsInCurrentCommit.clear(); - - endMark('(Committing Changes)', '(Committing Changes)', warning); - }, - - startCommitHostEffectsTimer(): void { - if (!supportsUserTiming) { - return; - } - effectCountInCurrentCommit = 0; - beginMark('(Committing Host Effects)'); - }, - - stopCommitHostEffectsTimer(): void { - if (!supportsUserTiming) { - return; - } - const count = effectCountInCurrentCommit; - effectCountInCurrentCommit = 0; - endMark( - `(Committing Host Effects: ${count} Total)`, - '(Committing Host Effects)', - null, - ); - }, - - startCommitLifeCyclesTimer(): void { - if (!supportsUserTiming) { - return; - } - effectCountInCurrentCommit = 0; - beginMark('(Calling Lifecycle Methods)'); - }, - - stopCommitLifeCyclesTimer(): void { - if (!supportsUserTiming) { - return; - } - const count = effectCountInCurrentCommit; - effectCountInCurrentCommit = 0; - endMark( - `(Calling Lifecycle Methods: ${count} Total)`, - '(Calling Lifecycle Methods)', - null, - ); - }, - }; + + let warning = null; + if (hasScheduledUpdateInCurrentCommit) { + warning = 'Lifecycle hook scheduled a cascading update'; + } else if (commitCountInCurrentWorkLoop > 0) { + warning = 'Caused by a cascading update in earlier commit'; + } + hasScheduledUpdateInCurrentCommit = false; + commitCountInCurrentWorkLoop++; + isCommitting = false; + labelsInCurrentCommit.clear(); + + endMark('(Committing Changes)', '(Committing Changes)', warning); + } } -// TODO: convert to named exports -// if this doesn't inflate the bundle. -export default ReactDebugFiberPerf; +export function startCommitHostEffectsTimer(): void { + if (enableUserTimingAPI) { + if (!supportsUserTiming) { + return; + } + effectCountInCurrentCommit = 0; + beginMark('(Committing Host Effects)'); + } +} + +export function stopCommitHostEffectsTimer(): void { + if (enableUserTimingAPI) { + if (!supportsUserTiming) { + return; + } + const count = effectCountInCurrentCommit; + effectCountInCurrentCommit = 0; + endMark( + `(Committing Host Effects: ${count} Total)`, + '(Committing Host Effects)', + null, + ); + } +} + +export function startCommitLifeCyclesTimer(): void { + if (enableUserTimingAPI) { + if (!supportsUserTiming) { + return; + } + effectCountInCurrentCommit = 0; + beginMark('(Calling Lifecycle Methods)'); + } +} + +export function stopCommitLifeCyclesTimer(): void { + if (enableUserTimingAPI) { + if (!supportsUserTiming) { + return; + } + const count = effectCountInCurrentCommit; + effectCountInCurrentCommit = 0; + endMark( + `(Calling Lifecycle Methods: ${count} Total)`, + '(Calling Lifecycle Methods)', + null, + ); + } +} diff --git a/packages/react-reconciler/src/ReactFiberBeginWork.js b/packages/react-reconciler/src/ReactFiberBeginWork.js index f8f72a23700..213ae7cdf8e 100644 --- a/packages/react-reconciler/src/ReactFiberBeginWork.js +++ b/packages/react-reconciler/src/ReactFiberBeginWork.js @@ -40,7 +40,7 @@ import invariant from 'fbjs/lib/invariant'; import getComponentName from 'shared/getComponentName'; import warning from 'fbjs/lib/warning'; import ReactDebugCurrentFiber from './ReactDebugCurrentFiber'; -import ReactDebugFiberPerf from './ReactDebugFiberPerf'; +import {cancelWorkTimer} from './ReactDebugFiberPerf'; import ReactFiberClassComponent from './ReactFiberClassComponent'; import { @@ -60,7 +60,6 @@ import { } from './ReactFiberContext'; import {NoWork, Never} from './ReactFiberExpirationTime'; -var {cancelWorkTimer} = ReactDebugFiberPerf; if (__DEV__) { var warnedAboutStatelessRefs = {}; } @@ -666,9 +665,7 @@ export default function( current, workInProgress: Fiber, ): Fiber | null { - if (__DEV__) { - cancelWorkTimer(workInProgress); - } + cancelWorkTimer(workInProgress); // TODO: We should ideally be able to bail out early if the children have no // more work to do. However, since we don't have a separation of this @@ -689,9 +686,7 @@ export default function( } function bailoutOnLowPriority(current, workInProgress) { - if (__DEV__) { - cancelWorkTimer(workInProgress); - } + cancelWorkTimer(workInProgress); // TODO: Handle HostComponent tags here as well and call pushHostContext()? // See PR 8590 discussion for context diff --git a/packages/react-reconciler/src/ReactFiberClassComponent.js b/packages/react-reconciler/src/ReactFiberClassComponent.js index 39e3993a1c0..e93a2310b09 100644 --- a/packages/react-reconciler/src/ReactFiberClassComponent.js +++ b/packages/react-reconciler/src/ReactFiberClassComponent.js @@ -20,7 +20,7 @@ import shallowEqual from 'fbjs/lib/shallowEqual'; import invariant from 'fbjs/lib/invariant'; import warning from 'fbjs/lib/warning'; -import ReactDebugFiberPerf from './ReactDebugFiberPerf'; +import {startPhaseTimer, stopPhaseTimer} from './ReactDebugFiberPerf'; import {AsyncUpdates} from './ReactTypeOfInternalContext'; import { cacheContext, @@ -34,7 +34,6 @@ import { } from './ReactFiberUpdateQueue'; import {hasContextChanged} from './ReactFiberContext'; -var {startPhaseTimer, stopPhaseTimer} = ReactDebugFiberPerf; const fakeInternalInstance = {}; const isArray = Array.isArray; @@ -161,17 +160,13 @@ export default function( const instance = workInProgress.stateNode; const type = workInProgress.type; if (typeof instance.shouldComponentUpdate === 'function') { - if (__DEV__) { - startPhaseTimer(workInProgress, 'shouldComponentUpdate'); - } + startPhaseTimer(workInProgress, 'shouldComponentUpdate'); const shouldUpdate = instance.shouldComponentUpdate( newProps, newState, newContext, ); - if (__DEV__) { - stopPhaseTimer(); - } + stopPhaseTimer(); if (__DEV__) { warning( @@ -365,14 +360,11 @@ export default function( } function callComponentWillMount(workInProgress, instance) { - if (__DEV__) { - startPhaseTimer(workInProgress, 'componentWillMount'); - } + startPhaseTimer(workInProgress, 'componentWillMount'); const oldState = instance.state; instance.componentWillMount(); - if (__DEV__) { - stopPhaseTimer(); - } + + stopPhaseTimer(); if (oldState !== instance.state) { if (__DEV__) { @@ -394,14 +386,10 @@ export default function( newProps, newContext, ) { - if (__DEV__) { - startPhaseTimer(workInProgress, 'componentWillReceiveProps'); - } + startPhaseTimer(workInProgress, 'componentWillReceiveProps'); const oldState = instance.state; instance.componentWillReceiveProps(newProps, newContext); - if (__DEV__) { - stopPhaseTimer(); - } + stopPhaseTimer(); if (instance.state !== oldState) { if (__DEV__) { @@ -673,13 +661,9 @@ export default function( if (shouldUpdate) { if (typeof instance.componentWillUpdate === 'function') { - if (__DEV__) { - startPhaseTimer(workInProgress, 'componentWillUpdate'); - } + startPhaseTimer(workInProgress, 'componentWillUpdate'); instance.componentWillUpdate(newProps, newState, newContext); - if (__DEV__) { - stopPhaseTimer(); - } + stopPhaseTimer(); } if (typeof instance.componentDidUpdate === 'function') { workInProgress.effectTag |= Update; diff --git a/packages/react-reconciler/src/ReactFiberCommitWork.js b/packages/react-reconciler/src/ReactFiberCommitWork.js index 86be8e591de..78da5774517 100644 --- a/packages/react-reconciler/src/ReactFiberCommitWork.js +++ b/packages/react-reconciler/src/ReactFiberCommitWork.js @@ -29,10 +29,9 @@ import invariant from 'fbjs/lib/invariant'; import {commitCallbacks} from './ReactFiberUpdateQueue'; import {onCommitUnmount} from './ReactFiberDevToolsHook'; -import ReactDebugFiberPerf from './ReactDebugFiberPerf'; +import {startPhaseTimer, stopPhaseTimer} from './ReactDebugFiberPerf'; var {invokeGuardedCallback, hasCaughtError, clearCaughtError} = ReactErrorUtils; -var {startPhaseTimer, stopPhaseTimer} = ReactDebugFiberPerf; export default function( config: HostConfig, @@ -40,22 +39,20 @@ export default function( ) { const {getPublicInstance, mutation, persistence} = config; - if (__DEV__) { - var callComponentWillUnmountWithTimerInDev = function(current, instance) { - startPhaseTimer(current, 'componentWillUnmount'); - instance.props = current.memoizedProps; - instance.state = current.memoizedState; - instance.componentWillUnmount(); - stopPhaseTimer(); - }; - } + var callComponentWillUnmountWithTimer = function(current, instance) { + startPhaseTimer(current, 'componentWillUnmount'); + instance.props = current.memoizedProps; + instance.state = current.memoizedState; + instance.componentWillUnmount(); + stopPhaseTimer(); + }; // Capture errors so they don't interrupt unmounting. function safelyCallComponentWillUnmount(current, instance) { if (__DEV__) { invokeGuardedCallback( null, - callComponentWillUnmountWithTimerInDev, + callComponentWillUnmountWithTimer, null, current, instance, @@ -66,9 +63,7 @@ export default function( } } else { try { - instance.props = current.memoizedProps; - instance.state = current.memoizedState; - instance.componentWillUnmount(); + callComponentWillUnmountWithTimer(current, instance); } catch (unmountError) { captureError(current, unmountError); } @@ -100,27 +95,19 @@ export default function( const instance = finishedWork.stateNode; if (finishedWork.effectTag & Update) { if (current === null) { - if (__DEV__) { - startPhaseTimer(finishedWork, 'componentDidMount'); - } + startPhaseTimer(finishedWork, 'componentDidMount'); instance.props = finishedWork.memoizedProps; instance.state = finishedWork.memoizedState; instance.componentDidMount(); - if (__DEV__) { - stopPhaseTimer(); - } + stopPhaseTimer(); } else { const prevProps = current.memoizedProps; const prevState = current.memoizedState; - if (__DEV__) { - startPhaseTimer(finishedWork, 'componentDidUpdate'); - } + startPhaseTimer(finishedWork, 'componentDidUpdate'); instance.props = finishedWork.memoizedProps; instance.state = finishedWork.memoizedState; instance.componentDidUpdate(prevProps, prevState); - if (__DEV__) { - stopPhaseTimer(); - } + stopPhaseTimer(); } } const updateQueue = finishedWork.updateQueue; diff --git a/packages/react-reconciler/src/ReactFiberContext.js b/packages/react-reconciler/src/ReactFiberContext.js index b28fb478b21..231ee6da51d 100644 --- a/packages/react-reconciler/src/ReactFiberContext.js +++ b/packages/react-reconciler/src/ReactFiberContext.js @@ -20,9 +20,7 @@ import checkPropTypes from 'prop-types/checkPropTypes'; import {createCursor, pop, push} from './ReactFiberStack'; import ReactDebugCurrentFiber from './ReactDebugCurrentFiber'; -import ReactDebugFiberPerf from './ReactDebugFiberPerf'; - -const {startPhaseTimer, stopPhaseTimer} = ReactDebugFiberPerf; +import {startPhaseTimer, stopPhaseTimer} from './ReactDebugFiberPerf'; if (__DEV__) { var warnedAboutMissingGetChildContext = {}; @@ -177,12 +175,12 @@ export function processChildContext( let childContext; if (__DEV__) { ReactDebugCurrentFiber.setCurrentPhase('getChildContext'); - startPhaseTimer(fiber, 'getChildContext'); - childContext = instance.getChildContext(); - stopPhaseTimer(); + } + startPhaseTimer(fiber, 'getChildContext'); + childContext = instance.getChildContext(); + stopPhaseTimer(); + if (__DEV__) { ReactDebugCurrentFiber.setCurrentPhase(null); - } else { - childContext = instance.getChildContext(); } for (let contextKey in childContext) { invariant( diff --git a/packages/react-reconciler/src/ReactFiberScheduler.js b/packages/react-reconciler/src/ReactFiberScheduler.js index 406ab8513b5..19dfef14aa6 100644 --- a/packages/react-reconciler/src/ReactFiberScheduler.js +++ b/packages/react-reconciler/src/ReactFiberScheduler.js @@ -46,7 +46,21 @@ import ReactFiberHostContext from './ReactFiberHostContext'; import ReactFiberHydrationContext from './ReactFiberHydrationContext'; import ReactFiberInstrumentation from './ReactFiberInstrumentation'; import ReactDebugCurrentFiber from './ReactDebugCurrentFiber'; -import ReactDebugFiberPerf from './ReactDebugFiberPerf'; +import { + recordEffect, + recordScheduleUpdate, + startWorkTimer, + stopWorkTimer, + stopFailedWorkTimer, + startWorkLoopTimer, + stopWorkLoopTimer, + startCommitTimer, + stopCommitTimer, + startCommitHostEffectsTimer, + stopCommitHostEffectsTimer, + startCommitLifeCyclesTimer, + stopCommitLifeCyclesTimer, +} from './ReactDebugFiberPerf'; import {popContextProvider} from './ReactFiberContext'; import {reset} from './ReactFiberStack'; import {logCapturedError} from './ReactFiberErrorLogger'; @@ -64,21 +78,6 @@ import {getUpdateExpirationTime} from './ReactFiberUpdateQueue'; import {resetContext} from './ReactFiberContext'; var {invokeGuardedCallback, hasCaughtError, clearCaughtError} = ReactErrorUtils; -var { - recordEffect, - recordScheduleUpdate, - startWorkTimer, - stopWorkTimer, - stopFailedWorkTimer, - startWorkLoopTimer, - stopWorkLoopTimer, - startCommitTimer, - stopCommitTimer, - startCommitHostEffectsTimer, - stopCommitHostEffectsTimer, - startCommitLifeCyclesTimer, - stopCommitLifeCyclesTimer, -} = ReactDebugFiberPerf; export type CapturedError = { componentName: ?string, @@ -229,8 +228,8 @@ export default function( while (nextEffect !== null) { if (__DEV__) { ReactDebugCurrentFiber.setCurrentFiber(nextEffect); - recordEffect(); } + recordEffect(); const effectTag = nextEffect.effectTag; if (effectTag & ContentReset) { @@ -298,24 +297,18 @@ export default function( const effectTag = nextEffect.effectTag; if (effectTag & (Update | Callback)) { - if (__DEV__) { - recordEffect(); - } + recordEffect(); const current = nextEffect.alternate; commitLifeCycles(current, nextEffect); } if (effectTag & Ref) { - if (__DEV__) { - recordEffect(); - } + recordEffect(); commitAttachRef(nextEffect); } if (effectTag & Err) { - if (__DEV__) { - recordEffect(); - } + recordEffect(); commitErrorHandling(nextEffect); } @@ -338,9 +331,7 @@ export default function( // captured elsewhere, to prevent the unmount from being interrupted. isWorking = true; isCommitting = true; - if (__DEV__) { - startCommitTimer(); - } + startCommitTimer(); const root: FiberRoot = finishedWork.stateNode; invariant( @@ -377,9 +368,7 @@ export default function( // The first pass performs all the host insertions, updates, deletions and // ref unmounts. nextEffect = firstEffect; - if (__DEV__) { - startCommitHostEffectsTimer(); - } + startCommitHostEffectsTimer(); while (nextEffect !== null) { let didError = false; let error; @@ -410,9 +399,7 @@ export default function( } } } - if (__DEV__) { - stopCommitHostEffectsTimer(); - } + stopCommitHostEffectsTimer(); resetAfterCommit(); @@ -427,9 +414,7 @@ export default function( // and deletions in the entire tree have already been invoked. // This pass also triggers any renderer-specific initial effects. nextEffect = firstEffect; - if (__DEV__) { - startCommitLifeCyclesTimer(); - } + startCommitLifeCyclesTimer(); while (nextEffect !== null) { let didError = false; let error; @@ -462,10 +447,8 @@ export default function( isCommitting = false; isWorking = false; - if (__DEV__) { - stopCommitLifeCyclesTimer(); - stopCommitTimer(); - } + stopCommitLifeCyclesTimer(); + stopCommitTimer(); if (typeof onCommitRoot === 'function') { onCommitRoot(finishedWork.stateNode); } @@ -551,9 +534,7 @@ export default function( resetExpirationTime(workInProgress, nextRenderExpirationTime); if (next !== null) { - if (__DEV__) { - stopWorkTimer(workInProgress); - } + stopWorkTimer(workInProgress); if (__DEV__ && ReactFiberInstrumentation.debugTool) { ReactFiberInstrumentation.debugTool.onCompleteWork(workInProgress); } @@ -595,9 +576,7 @@ export default function( } } - if (__DEV__) { - stopWorkTimer(workInProgress); - } + stopWorkTimer(workInProgress); if (__DEV__ && ReactFiberInstrumentation.debugTool) { ReactFiberInstrumentation.debugTool.onCompleteWork(workInProgress); } @@ -631,8 +610,8 @@ export default function( const current = workInProgress.alternate; // See if beginning this work spawns more work. + startWorkTimer(workInProgress); if (__DEV__) { - startWorkTimer(workInProgress); ReactDebugCurrentFiber.setCurrentFiber(workInProgress); } let next = beginWork(current, workInProgress, nextRenderExpirationTime); @@ -661,8 +640,8 @@ export default function( const current = workInProgress.alternate; // See if beginning this work spawns more work. + startWorkTimer(workInProgress); if (__DEV__) { - startWorkTimer(workInProgress); ReactDebugCurrentFiber.setCurrentFiber(workInProgress); } let next = beginFailedWork( @@ -773,9 +752,7 @@ export default function( root: FiberRoot, expirationTime: ExpirationTime, ): Fiber | null { - if (__DEV__) { - startWorkLoopTimer(); - } + startWorkLoopTimer(); invariant( !isWorking, @@ -892,9 +869,7 @@ export default function( didFatal = false; firstUncaughtError = null; - if (__DEV__) { - stopWorkLoopTimer(); - } + stopWorkLoopTimer(); if (uncaughtError !== null) { onUncaughtError(uncaughtError); @@ -1131,11 +1106,9 @@ export default function( break; } if (node === to || node.alternate === to) { - if (__DEV__) { - stopFailedWorkTimer(node); - } + stopFailedWorkTimer(node); break; - } else if (__DEV__) { + } else { stopWorkTimer(node); } node = node.return; @@ -1190,9 +1163,7 @@ export default function( expirationTime: ExpirationTime, isErrorRecovery: boolean, ) { - if (__DEV__) { - recordScheduleUpdate(); - } + recordScheduleUpdate(); if (__DEV__) { if (!isErrorRecovery && fiber.tag === ClassComponent) { diff --git a/packages/shared/ReactFeatureFlags.js b/packages/shared/ReactFeatureFlags.js index 12c109e30f9..4c86b348e95 100644 --- a/packages/shared/ReactFeatureFlags.js +++ b/packages/shared/ReactFeatureFlags.js @@ -7,12 +7,15 @@ * @flow */ +import invariant from 'fbjs/lib/invariant'; + export const enableAsyncSubtreeAPI = true; export const enableAsyncSchedulingByDefaultInReactDOM = false; // Exports React.Fragment export const enableReactFragment = false; // Exports ReactDOM.createRoot export const enableCreateRoot = false; +export const enableUserTimingAPI = __DEV__; // Mutating mode (React DOM, React ART, React Native): export const enableMutatingReconciler = true; @@ -20,3 +23,8 @@ export const enableMutatingReconciler = true; export const enableNoopReconciler = false; // Experimental persistent mode (CS): export const enablePersistentReconciler = false; + +// Only used in www builds. +export function addUserTimingListener() { + invariant(false, 'Not implemented.'); +} diff --git a/scripts/rollup/shims/rollup/ReactFeatureFlags-www.js b/scripts/rollup/shims/rollup/ReactFeatureFlags-www.js index 9958c526734..90b1cad8854 100644 --- a/scripts/rollup/shims/rollup/ReactFeatureFlags-www.js +++ b/scripts/rollup/shims/rollup/ReactFeatureFlags-www.js @@ -16,11 +16,38 @@ export const { enableAsyncSchedulingByDefaultInReactDOM, enableReactFragment, enableCreateRoot, + // Reconciler flags enableMutatingReconciler, enableNoopReconciler, enablePersistentReconciler, } = require('ReactFeatureFlags'); +export let enableUserTimingAPI = __DEV__; + +let refCount = 0; +export function addUserTimingListener() { + if (__DEV__) { + // Noop. + return () => {}; + } + refCount++; + updateFlagOutsideOfReactCallStack(); + return () => { + refCount--; + updateFlagOutsideOfReactCallStack(); + }; +} + +let timeout = null; +function updateFlagOutsideOfReactCallStack() { + if (!timeout) { + timeout = setTimeout(() => { + timeout = null; + enableUserTimingAPI = refCount > 0; + }); + } +} + // Flow magic to verify the exports of this file match the original version. // eslint-disable-next-line no-unused-vars type Check<_X, Y: _X, X: Y=_X> = null;