diff --git a/packages/react-devtools-shared/src/backend/fiber/renderer.js b/packages/react-devtools-shared/src/backend/fiber/renderer.js index 1a76c859022..031a18b74d4 100644 --- a/packages/react-devtools-shared/src/backend/fiber/renderer.js +++ b/packages/react-devtools-shared/src/backend/fiber/renderer.js @@ -78,6 +78,7 @@ import { __DEBUG__, PROFILING_FLAG_BASIC_SUPPORT, PROFILING_FLAG_TIMELINE_SUPPORT, + PROFILING_FLAG_PERFORMANCE_TRACKS_SUPPORT, TREE_OPERATION_ADD, TREE_OPERATION_REMOVE, TREE_OPERATION_REORDER_CHILDREN, @@ -1074,6 +1075,7 @@ export function attach( const supportsTogglingSuspense = typeof setSuspenseHandler === 'function' && typeof scheduleUpdate === 'function'; + const supportsPerformanceTracks = gte(version, '19.2.0'); if (typeof scheduleRefresh === 'function') { // When Fast Refresh updates a component, the frontend may need to purge cached information. @@ -2401,6 +2403,9 @@ export function attach( if (typeof injectProfilingHooks === 'function') { profilingFlags |= PROFILING_FLAG_TIMELINE_SUPPORT; } + if (supportsPerformanceTracks) { + profilingFlags |= PROFILING_FLAG_PERFORMANCE_TRACKS_SUPPORT; + } } // Set supportsStrictMode to false for production renderer builds diff --git a/packages/react-devtools-shared/src/constants.js b/packages/react-devtools-shared/src/constants.js index 1a8ef8caa8b..c398e130d84 100644 --- a/packages/react-devtools-shared/src/constants.js +++ b/packages/react-devtools-shared/src/constants.js @@ -30,8 +30,9 @@ export const SUSPENSE_TREE_OPERATION_REORDER_CHILDREN = 10; export const SUSPENSE_TREE_OPERATION_RESIZE = 11; export const SUSPENSE_TREE_OPERATION_SUSPENDERS = 12; -export const PROFILING_FLAG_BASIC_SUPPORT = 0b01; -export const PROFILING_FLAG_TIMELINE_SUPPORT = 0b10; +export const PROFILING_FLAG_BASIC_SUPPORT /*. */ = 0b001; +export const PROFILING_FLAG_TIMELINE_SUPPORT /* */ = 0b010; +export const PROFILING_FLAG_PERFORMANCE_TRACKS_SUPPORT /* */ = 0b100; export const UNKNOWN_SUSPENDERS_NONE: UnknownSuspendersReason = 0; // If we had at least one debugInfo, then that might have been the reason. export const UNKNOWN_SUSPENDERS_REASON_PRODUCTION: UnknownSuspendersReason = 1; // We're running in prod. That might be why we had unknown suspenders. diff --git a/packages/react-devtools-shared/src/devtools/store.js b/packages/react-devtools-shared/src/devtools/store.js index 6ffbb52d94e..0e3373e15e2 100644 --- a/packages/react-devtools-shared/src/devtools/store.js +++ b/packages/react-devtools-shared/src/devtools/store.js @@ -13,6 +13,7 @@ import {inspect} from 'util'; import { PROFILING_FLAG_BASIC_SUPPORT, PROFILING_FLAG_TIMELINE_SUPPORT, + PROFILING_FLAG_PERFORMANCE_TRACKS_SUPPORT, TREE_OPERATION_ADD, TREE_OPERATION_REMOVE, TREE_OPERATION_REMOVE_ROOT, @@ -86,12 +87,17 @@ export type Config = { supportsTraceUpdates?: boolean, }; +const ADVANCED_PROFILING_NONE = 0; +const ADVANCED_PROFILING_TIMELINE = 1; +const ADVANCED_PROFILING_PERFORMANCE_TRACKS = 2; +type AdvancedProfiling = 0 | 1 | 2; + export type Capabilities = { supportsBasicProfiling: boolean, hasOwnerMetadata: boolean, supportsStrictMode: boolean, supportsTogglingSuspense: boolean, - supportsTimeline: boolean, + supportsAdvancedProfiling: AdvancedProfiling, }; /** @@ -112,6 +118,7 @@ export default class Store extends EventEmitter<{ roots: [], rootSupportsBasicProfiling: [], rootSupportsTimelineProfiling: [], + rootSupportsPerformanceTracks: [], suspenseTreeMutated: [[Map]], supportsNativeStyleEditor: [], supportsReloadAndProfile: [], @@ -195,6 +202,7 @@ export default class Store extends EventEmitter<{ // These options default to false but may be updated as roots are added and removed. _rootSupportsBasicProfiling: boolean = false; _rootSupportsTimelineProfiling: boolean = false; + _rootSupportsPerformanceTracks: boolean = false; _bridgeProtocol: BridgeProtocol | null = null; _unsupportedBridgeProtocolDetected: boolean = false; @@ -474,6 +482,11 @@ export default class Store extends EventEmitter<{ return this._rootSupportsTimelineProfiling; } + // At least one of the currently mounted roots support performance tracks. + get rootSupportsPerformanceTracks(): boolean { + return this._rootSupportsPerformanceTracks; + } + get supportsInspectMatchingDOMElement(): boolean { return this._supportsInspectMatchingDOMElement; } @@ -1161,11 +1174,20 @@ export default class Store extends EventEmitter<{ const isStrictModeCompliant = operations[i] > 0; i++; + const profilerFlags = operations[i++]; const supportsBasicProfiling = - (operations[i] & PROFILING_FLAG_BASIC_SUPPORT) !== 0; + (profilerFlags & PROFILING_FLAG_BASIC_SUPPORT) !== 0; const supportsTimeline = - (operations[i] & PROFILING_FLAG_TIMELINE_SUPPORT) !== 0; - i++; + (profilerFlags & PROFILING_FLAG_TIMELINE_SUPPORT) !== 0; + const supportsPerformanceTracks = + (profilerFlags & PROFILING_FLAG_PERFORMANCE_TRACKS_SUPPORT) !== 0; + let supportsAdvancedProfiling: AdvancedProfiling = + ADVANCED_PROFILING_NONE; + if (supportsPerformanceTracks) { + supportsAdvancedProfiling = ADVANCED_PROFILING_PERFORMANCE_TRACKS; + } else if (supportsTimeline) { + supportsAdvancedProfiling = ADVANCED_PROFILING_TIMELINE; + } let supportsStrictMode = false; let hasOwnerMetadata = false; @@ -1194,7 +1216,7 @@ export default class Store extends EventEmitter<{ hasOwnerMetadata, supportsStrictMode, supportsTogglingSuspense, - supportsTimeline, + supportsAdvancedProfiling, }); // Not all roots support StrictMode; @@ -1842,21 +1864,33 @@ export default class Store extends EventEmitter<{ const prevRootSupportsProfiling = this._rootSupportsBasicProfiling; const prevRootSupportsTimelineProfiling = this._rootSupportsTimelineProfiling; + const prevRootSupportsPerformanceTracks = + this._rootSupportsPerformanceTracks; this._hasOwnerMetadata = false; this._rootSupportsBasicProfiling = false; this._rootSupportsTimelineProfiling = false; + this._rootSupportsPerformanceTracks = false; this._rootIDToCapabilities.forEach( - ({supportsBasicProfiling, hasOwnerMetadata, supportsTimeline}) => { + ({ + supportsBasicProfiling, + hasOwnerMetadata, + supportsAdvancedProfiling, + }) => { if (supportsBasicProfiling) { this._rootSupportsBasicProfiling = true; } if (hasOwnerMetadata) { this._hasOwnerMetadata = true; } - if (supportsTimeline) { + if (supportsAdvancedProfiling === ADVANCED_PROFILING_TIMELINE) { this._rootSupportsTimelineProfiling = true; } + if ( + supportsAdvancedProfiling === ADVANCED_PROFILING_PERFORMANCE_TRACKS + ) { + this._rootSupportsPerformanceTracks = true; + } }, ); @@ -1872,6 +1906,12 @@ export default class Store extends EventEmitter<{ ) { this.emit('rootSupportsTimelineProfiling'); } + if ( + this._rootSupportsPerformanceTracks !== + prevRootSupportsPerformanceTracks + ) { + this.emit('rootSupportsPerformanceTracks'); + } } if (hasSuspenseTreeChanged) { diff --git a/packages/react-devtools-shell/src/app/InspectableElements/UseEffectEvent.js b/packages/react-devtools-shell/src/app/InspectableElements/UseEffectEvent.js index e55f6a3c55e..020246e8a4a 100644 --- a/packages/react-devtools-shell/src/app/InspectableElements/UseEffectEvent.js +++ b/packages/react-devtools-shell/src/app/InspectableElements/UseEffectEvent.js @@ -1,6 +1,8 @@ import * as React from 'react'; -const {experimental_useEffectEvent, useState, useEffect} = React; +const {useState, useEffect} = React; +const useEffectEvent = + React.useEffectEvent || React.experimental_useEffectEvent; export default function UseEffectEvent(): React.Node { return ( @@ -12,14 +14,14 @@ export default function UseEffectEvent(): React.Node { } function SingleHookCase() { - const onClick = experimental_useEffectEvent(() => {}); + const onClick = useEffectEvent(() => {}); return
; } function useCustomHook() { const [state, setState] = useState(); - const onClick = experimental_useEffectEvent(() => {}); + const onClick = useEffectEvent(() => {}); useEffect(() => {}); return [state, setState, onClick]; diff --git a/packages/react-devtools-timeline/src/Timeline.js b/packages/react-devtools-timeline/src/Timeline.js index 482375a46f9..f209309bb0a 100644 --- a/packages/react-devtools-timeline/src/Timeline.js +++ b/packages/react-devtools-timeline/src/Timeline.js @@ -33,8 +33,14 @@ import {TimelineSearchContextController} from './TimelineSearchContext'; import styles from './Timeline.css'; export function Timeline(_: {}): React.Node { - const {file, inMemoryTimelineData, isTimelineSupported, setFile, viewState} = - useContext(TimelineContext); + const { + file, + inMemoryTimelineData, + isPerformanceTracksSupported, + isTimelineSupported, + setFile, + viewState, + } = useContext(TimelineContext); const {didRecordCommits, isProfiling} = useContext(ProfilerContext); const ref = useRef(null); @@ -95,7 +101,11 @@ export function Timeline(_: {}): React.Node { } else if (isTimelineSupported) { content = ; } else { - content = ; + content = ( + + ); } return ( diff --git a/packages/react-devtools-timeline/src/TimelineContext.js b/packages/react-devtools-timeline/src/TimelineContext.js index 83813eb267a..7835158e989 100644 --- a/packages/react-devtools-timeline/src/TimelineContext.js +++ b/packages/react-devtools-timeline/src/TimelineContext.js @@ -31,6 +31,7 @@ import type { export type Context = { file: File | null, inMemoryTimelineData: Array | null, + isPerformanceTracksSupported: boolean, isTimelineSupported: boolean, searchInputContainerRef: RefObject, setFile: (file: File | null) => void, @@ -66,6 +67,18 @@ function TimelineContextController({children}: Props): React.Node { }, ); + const isPerformanceTracksSupported = useSyncExternalStore( + function subscribe(callback) { + store.addListener('rootSupportsPerformanceTracks', callback); + return function unsubscribe() { + store.removeListener('rootSupportsPerformanceTracks', callback); + }; + }, + function getState() { + return store.rootSupportsPerformanceTracks; + }, + ); + const inMemoryTimelineData = useSyncExternalStore | null>( function subscribe(callback) { store.profilerStore.addListener('isProcessingData', callback); @@ -135,6 +148,7 @@ function TimelineContextController({children}: Props): React.Node { () => ({ file, inMemoryTimelineData, + isPerformanceTracksSupported, isTimelineSupported, searchInputContainerRef, setFile, @@ -145,6 +159,7 @@ function TimelineContextController({children}: Props): React.Node { [ file, inMemoryTimelineData, + isPerformanceTracksSupported, isTimelineSupported, setFile, viewState, diff --git a/packages/react-devtools-timeline/src/TimelineNotSupported.js b/packages/react-devtools-timeline/src/TimelineNotSupported.js index de13a0439c3..7eac79da94f 100644 --- a/packages/react-devtools-timeline/src/TimelineNotSupported.js +++ b/packages/react-devtools-timeline/src/TimelineNotSupported.js @@ -12,16 +12,48 @@ import {isInternalFacebookBuild} from 'react-devtools-feature-flags'; import styles from './TimelineNotSupported.css'; -export default function TimelineNotSupported(): React.Node { +type Props = { + isPerformanceTracksSupported: boolean, +}; + +function PerformanceTracksSupported() { return ( -
-
Timeline profiling not supported.
+ <>

- Timeline profiler requires a development or profiling build of{' '} - react-dom@^18. + Please use{' '} + + React Performance tracks + {' '} + instead of the Timeline profiler.

+ + ); +} + +function UnknownUnsupportedReason() { + return ( + <> +

+ Timeline profiler requires a development or profiling build of{' '} + react-dom@{'>='}18. +

+

+ In React 19.2 and above{' '} + + React Performance tracks + {' '} + can be used instead. +

+ + ); +} + +export default function TimelineNotSupported({ + isPerformanceTracksSupported, +}: Props): React.Node { + return ( +
+
Timeline profiling not supported.
+ + {isPerformanceTracksSupported ? ( + + ) : ( + + )} {isInternalFacebookBuild && (