diff --git a/packages/react-devtools-shared/src/__tests__/profilingHostRoot-test.js b/packages/react-devtools-shared/src/__tests__/profilingHostRoot-test.js new file mode 100644 index 000000000000..a37401eb4a06 --- /dev/null +++ b/packages/react-devtools-shared/src/__tests__/profilingHostRoot-test.js @@ -0,0 +1,144 @@ +/** + * 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 + */ + +describe('profiling HostRoot', () => { + let React; + let ReactDOM; + let Scheduler; + let store: Store; + let utils; + let getEffectDurations; + + let effectDurations; + let passiveEffectDurations; + + beforeEach(() => { + utils = require('./utils'); + utils.beforeEachProfiling(); + + getEffectDurations = require('../backend/utils').getEffectDurations; + + store = global.store; + + React = require('react'); + ReactDOM = require('react-dom'); + Scheduler = require('scheduler'); + + effectDurations = []; + passiveEffectDurations = []; + + // This is the DevTools hook installed by the env.beforEach() + // The hook is installed as a read-only property on the window, + // so for our test purposes we can just override the commit hook. + const hook = global.__REACT_DEVTOOLS_GLOBAL_HOOK__; + hook.onPostCommitFiberRoot = function onPostCommitFiberRoot( + rendererID, + root, + ) { + const {effectDuration, passiveEffectDuration} = getEffectDurations(root); + effectDurations.push(effectDuration); + passiveEffectDurations.push(passiveEffectDuration); + }; + }); + + it('should expose passive and layout effect durations for render()', () => { + function App() { + React.useEffect(() => { + Scheduler.unstable_advanceTime(10); + }); + React.useLayoutEffect(() => { + Scheduler.unstable_advanceTime(100); + }); + return null; + } + + utils.act(() => store.profilerStore.startProfiling()); + utils.act(() => { + const container = document.createElement('div'); + ReactDOM.render(, container); + }); + utils.act(() => store.profilerStore.stopProfiling()); + + expect(effectDurations).toHaveLength(1); + const effectDuration = effectDurations[0]; + expect(effectDuration === null || effectDuration === 100).toBe(true); + expect(passiveEffectDurations).toHaveLength(1); + const passiveEffectDuration = passiveEffectDurations[0]; + expect(passiveEffectDuration === null || passiveEffectDuration === 10).toBe( + true, + ); + }); + + it('should expose passive and layout effect durations for createRoot()', () => { + function App() { + React.useEffect(() => { + Scheduler.unstable_advanceTime(10); + }); + React.useLayoutEffect(() => { + Scheduler.unstable_advanceTime(100); + }); + return null; + } + + utils.act(() => store.profilerStore.startProfiling()); + utils.act(() => { + const container = document.createElement('div'); + const root = ReactDOM.unstable_createRoot(container); + root.render(); + }); + utils.act(() => store.profilerStore.stopProfiling()); + + expect(effectDurations).toHaveLength(1); + const effectDuration = effectDurations[0]; + expect(effectDuration === null || effectDuration === 100).toBe(true); + expect(passiveEffectDurations).toHaveLength(1); + const passiveEffectDuration = passiveEffectDurations[0]; + expect(passiveEffectDuration === null || passiveEffectDuration === 10).toBe( + true, + ); + }); + + it('should properly reset passive and layout effect durations between commits', () => { + function App({shouldCascade}) { + const [, setState] = React.useState(false); + React.useEffect(() => { + Scheduler.unstable_advanceTime(10); + }); + React.useLayoutEffect(() => { + Scheduler.unstable_advanceTime(100); + }); + React.useLayoutEffect(() => { + if (shouldCascade) { + setState(true); + } + }, [shouldCascade]); + return null; + } + + const container = document.createElement('div'); + const root = ReactDOM.unstable_createRoot(container); + + utils.act(() => store.profilerStore.startProfiling()); + utils.act(() => root.render()); + utils.act(() => root.render()); + utils.act(() => store.profilerStore.stopProfiling()); + + expect(effectDurations).toHaveLength(3); + expect(passiveEffectDurations).toHaveLength(3); + + for (let i = 0; i < effectDurations.length; i++) { + const effectDuration = effectDurations[i]; + expect(effectDuration === null || effectDuration === 100).toBe(true); + const passiveEffectDuration = passiveEffectDurations[i]; + expect( + passiveEffectDuration === null || passiveEffectDuration === 10, + ).toBe(true); + } + }); +}); diff --git a/packages/react-devtools-shared/src/backend/legacy/renderer.js b/packages/react-devtools-shared/src/backend/legacy/renderer.js index 08cbfe340fb6..fcc7895356aa 100644 --- a/packages/react-devtools-shared/src/backend/legacy/renderer.js +++ b/packages/react-devtools-shared/src/backend/legacy/renderer.js @@ -1012,6 +1012,9 @@ export function attach( const handleCommitFiberUnmount = () => { throw new Error('handleCommitFiberUnmount not supported by this renderer'); }; + const handlePostCommitFiberRoot = () => { + throw new Error('handlePostCommitFiberRoot not supported by this renderer'); + }; const overrideSuspense = () => { throw new Error('overrideSuspense not supported by this renderer'); }; @@ -1082,6 +1085,7 @@ export function attach( getProfilingData, handleCommitFiberRoot, handleCommitFiberUnmount, + handlePostCommitFiberRoot, inspectElement, logElementToConsole, overrideSuspense, diff --git a/packages/react-devtools-shared/src/backend/renderer.js b/packages/react-devtools-shared/src/backend/renderer.js index c61edb45daa6..09042d0446b2 100644 --- a/packages/react-devtools-shared/src/backend/renderer.js +++ b/packages/react-devtools-shared/src/backend/renderer.js @@ -42,6 +42,7 @@ import { copyWithDelete, copyWithRename, copyWithSet, + getEffectDurations, } from './utils'; import { __DEBUG__, @@ -369,6 +370,7 @@ export function getInternalReactConstants( LegacyHiddenComponent, MemoComponent, OffscreenComponent, + Profiler, ScopeComponent, SimpleMemoComponent, SuspenseComponent, @@ -442,6 +444,8 @@ export function getInternalReactConstants( return 'Scope'; case SuspenseListComponent: return 'SuspenseList'; + case Profiler: + return 'Profiler'; default: const typeSymbol = getTypeSymbol(type); @@ -2154,25 +2158,6 @@ export function attach( // Checking root.memoizedInteractions handles multi-renderer edge-case- // where some v16 renderers support profiling and others don't. if (isProfiling && root.memoizedInteractions != null) { - // Profiling durations are only available for certain builds. - // If available, they'll be stored on the HostRoot. - let effectDuration = null; - let passiveEffectDuration = null; - const hostRoot = root.current; - if (hostRoot != null) { - const stateNode = hostRoot.stateNode; - if (stateNode != null) { - effectDuration = - stateNode.effectDuration != null - ? stateNode.effectDuration - : null; - passiveEffectDuration = - stateNode.passiveEffectDuration != null - ? stateNode.passiveEffectDuration - : null; - } - } - // If profiling is active, store commit time and duration, and the current interactions. // The frontend may request this information after profiling has stopped. currentCommitProfilingMetadata = { @@ -2187,8 +2172,8 @@ export function attach( ), maxActualDuration: 0, priorityLevel: null, - effectDuration, - passiveEffectDuration, + effectDuration: null, + passiveEffectDuration: null, }; } @@ -2206,6 +2191,19 @@ export function attach( recordUnmount(fiber, false); } + function handlePostCommitFiberRoot(root) { + const isProfilingSupported = root.memoizedInteractions != null; + if (isProfiling && isProfilingSupported) { + if (currentCommitProfilingMetadata !== null) { + const {effectDuration, passiveEffectDuration} = getEffectDurations( + root, + ); + currentCommitProfilingMetadata.effectDuration = effectDuration; + currentCommitProfilingMetadata.passiveEffectDuration = passiveEffectDuration; + } + } + } + function handleCommitFiberRoot(root, priorityLevel) { const current = root.current; const alternate = current.alternate; @@ -2227,23 +2225,6 @@ export function attach( const isProfilingSupported = root.memoizedInteractions != null; if (isProfiling && isProfilingSupported) { - // Profiling durations are only available for certain builds. - // If available, they'll be stored on the HostRoot. - let effectDuration = null; - let passiveEffectDuration = null; - const hostRoot = root.current; - if (hostRoot != null) { - const stateNode = hostRoot.stateNode; - if (stateNode != null) { - effectDuration = - stateNode.effectDuration != null ? stateNode.effectDuration : null; - passiveEffectDuration = - stateNode.passiveEffectDuration != null - ? stateNode.passiveEffectDuration - : null; - } - } - // If profiling is active, store commit time and duration, and the current interactions. // The frontend may request this information after profiling has stopped. currentCommitProfilingMetadata = { @@ -2259,8 +2240,11 @@ export function attach( maxActualDuration: 0, priorityLevel: priorityLevel == null ? null : formatPriorityLevel(priorityLevel), - effectDuration, - passiveEffectDuration, + + // Initialize to null; if new enough React version is running, + // these values will be read during separate handlePostCommitFiberRoot() call. + effectDuration: null, + passiveEffectDuration: null, }; } @@ -3856,6 +3840,7 @@ export function attach( getProfilingData, handleCommitFiberRoot, handleCommitFiberUnmount, + handlePostCommitFiberRoot, inspectElement, logElementToConsole, prepareViewAttributeSource, diff --git a/packages/react-devtools-shared/src/backend/types.js b/packages/react-devtools-shared/src/backend/types.js index c0ef91021f07..ea83d7766617 100644 --- a/packages/react-devtools-shared/src/backend/types.js +++ b/packages/react-devtools-shared/src/backend/types.js @@ -326,6 +326,7 @@ export type RendererInterface = { getPathForElement: (id: number) => Array | null, handleCommitFiberRoot: (fiber: Object, commitPriority?: number) => void, handleCommitFiberUnmount: (fiber: Object) => void, + handlePostCommitFiberRoot: (fiber: Object) => void, inspectElement: ( requestID: number, id: number, diff --git a/packages/react-devtools-shared/src/backend/utils.js b/packages/react-devtools-shared/src/backend/utils.js index 593459d78f64..22f70e32e7d0 100644 --- a/packages/react-devtools-shared/src/backend/utils.js +++ b/packages/react-devtools-shared/src/backend/utils.js @@ -118,6 +118,26 @@ export function copyWithSet( return updated; } +export function getEffectDurations(root: Object) { + // Profiling durations are only available for certain builds. + // If available, they'll be stored on the HostRoot. + let effectDuration = null; + let passiveEffectDuration = null; + const hostRoot = root.current; + if (hostRoot != null) { + const stateNode = hostRoot.stateNode; + if (stateNode != null) { + effectDuration = + stateNode.effectDuration != null ? stateNode.effectDuration : null; + passiveEffectDuration = + stateNode.passiveEffectDuration != null + ? stateNode.passiveEffectDuration + : null; + } + } + return {effectDuration, passiveEffectDuration}; +} + export function serializeToString(data: any): string { const cache = new Set(); // Use a custom replacer function to protect against circular references. diff --git a/packages/react-devtools-shared/src/hook.js b/packages/react-devtools-shared/src/hook.js index e29ad7abe956..a4724f6f6b92 100644 --- a/packages/react-devtools-shared/src/hook.js +++ b/packages/react-devtools-shared/src/hook.js @@ -287,6 +287,13 @@ export function installHook(target: any): DevToolsHook | null { } } + function onPostCommitFiberRoot(rendererID, root) { + const rendererInterface = rendererInterfaces.get(rendererID); + if (rendererInterface != null) { + rendererInterface.handlePostCommitFiberRoot(root); + } + } + // TODO: More meaningful names for "rendererInterfaces" and "renderers". const fiberRoots = {}; const rendererInterfaces = new Map(); @@ -315,6 +322,7 @@ export function installHook(target: any): DevToolsHook | null { checkDCE, onCommitFiberUnmount, onCommitFiberRoot, + onPostCommitFiberRoot, }; Object.defineProperty( diff --git a/packages/react-devtools-shell/src/app/InteractionTracing/index.js b/packages/react-devtools-shell/src/app/InteractionTracing/index.js index 544b335d0b36..aab8e77e9091 100644 --- a/packages/react-devtools-shell/src/app/InteractionTracing/index.js +++ b/packages/react-devtools-shell/src/app/InteractionTracing/index.js @@ -21,6 +21,14 @@ import { unstable_wrap as wrap, } from 'scheduler/tracing'; +function sleep(ms) { + const start = performance.now(); + let now; + do { + now = performance.now(); + } while (now - ms < start); +} + export default function InteractionTracing() { const [count, setCount] = useState(0); const [shouldCascade, setShouldCascade] = useState(false); @@ -75,7 +83,11 @@ export default function InteractionTracing() { }, [count, shouldCascade]); useLayoutEffect(() => { - Math.sqrt(100 * 100 * 100 * 100 * 100); + sleep(150); + }); + + useEffect(() => { + sleep(300); }); return ( diff --git a/packages/react-reconciler/src/ReactFiberDevToolsHook.new.js b/packages/react-reconciler/src/ReactFiberDevToolsHook.new.js index a98e569b095b..e7bde59e20cb 100644 --- a/packages/react-reconciler/src/ReactFiberDevToolsHook.new.js +++ b/packages/react-reconciler/src/ReactFiberDevToolsHook.new.js @@ -134,6 +134,24 @@ export function onCommitRoot(root: FiberRoot, eventPriority: EventPriority) { } } +export function onPostCommitRoot(root: FiberRoot) { + if ( + injectedHook && + typeof injectedHook.onPostCommitFiberRoot === 'function' + ) { + try { + injectedHook.onPostCommitFiberRoot(rendererID, root); + } catch (err) { + if (__DEV__) { + if (!hasLoggedError) { + hasLoggedError = true; + console.error('React instrumentation encountered an error: %s', err); + } + } + } + } +} + export function onCommitUnmount(fiber: Fiber) { if (injectedHook && typeof injectedHook.onCommitFiberUnmount === 'function') { try { diff --git a/packages/react-reconciler/src/ReactFiberDevToolsHook.old.js b/packages/react-reconciler/src/ReactFiberDevToolsHook.old.js index 43b9f360faa3..494138685e10 100644 --- a/packages/react-reconciler/src/ReactFiberDevToolsHook.old.js +++ b/packages/react-reconciler/src/ReactFiberDevToolsHook.old.js @@ -134,6 +134,24 @@ export function onCommitRoot(root: FiberRoot, eventPriority: EventPriority) { } } +export function onPostCommitRoot(root: FiberRoot) { + if ( + injectedHook && + typeof injectedHook.onPostCommitFiberRoot === 'function' + ) { + try { + injectedHook.onPostCommitFiberRoot(rendererID, root); + } catch (err) { + if (__DEV__) { + if (!hasLoggedError) { + hasLoggedError = true; + console.error('React instrumentation encountered an error: %s', err); + } + } + } + } +} + export function onCommitUnmount(fiber: Fiber) { if (injectedHook && typeof injectedHook.onCommitFiberUnmount === 'function') { try { diff --git a/packages/react-reconciler/src/ReactFiberWorkLoop.new.js b/packages/react-reconciler/src/ReactFiberWorkLoop.new.js index 425c46851aab..3344361075e6 100644 --- a/packages/react-reconciler/src/ReactFiberWorkLoop.new.js +++ b/packages/react-reconciler/src/ReactFiberWorkLoop.new.js @@ -229,7 +229,10 @@ import { hasCaughtError, clearCaughtError, } from 'shared/ReactErrorUtils'; -import {onCommitRoot as onCommitRootDevTools} from './ReactFiberDevToolsHook.new'; +import { + onCommitRoot as onCommitRootDevTools, + onPostCommitRoot as onPostCommitRootDevTools, +} from './ReactFiberDevToolsHook.new'; import {onCommitRoot as onCommitRootTestSelector} from './ReactTestSelectors'; // Used by `act` @@ -2156,6 +2159,14 @@ function flushPassiveEffectsImpl() { nestedPassiveUpdateCount = rootWithPendingPassiveEffects === null ? 0 : nestedPassiveUpdateCount + 1; + // TODO: Move to commitPassiveMountEffects + onPostCommitRootDevTools(root); + if (enableProfilerTimer && enableProfilerCommitHooks) { + const stateNode = root.current.stateNode; + stateNode.effectDuration = 0; + stateNode.passiveEffectDuration = 0; + } + return true; } diff --git a/packages/react-reconciler/src/ReactFiberWorkLoop.old.js b/packages/react-reconciler/src/ReactFiberWorkLoop.old.js index b22901ff84ec..73230f578777 100644 --- a/packages/react-reconciler/src/ReactFiberWorkLoop.old.js +++ b/packages/react-reconciler/src/ReactFiberWorkLoop.old.js @@ -229,7 +229,10 @@ import { hasCaughtError, clearCaughtError, } from 'shared/ReactErrorUtils'; -import {onCommitRoot as onCommitRootDevTools} from './ReactFiberDevToolsHook.old'; +import { + onCommitRoot as onCommitRootDevTools, + onPostCommitRoot as onPostCommitRootDevTools, +} from './ReactFiberDevToolsHook.old'; import {onCommitRoot as onCommitRootTestSelector} from './ReactTestSelectors'; // Used by `act` @@ -2156,6 +2159,14 @@ function flushPassiveEffectsImpl() { nestedPassiveUpdateCount = rootWithPendingPassiveEffects === null ? 0 : nestedPassiveUpdateCount + 1; + // TODO: Move to commitPassiveMountEffects + onPostCommitRootDevTools(root); + if (enableProfilerTimer && enableProfilerCommitHooks) { + const stateNode = root.current.stateNode; + stateNode.effectDuration = 0; + stateNode.passiveEffectDuration = 0; + } + return true; }