Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions packages/react-devtools-shared/src/backend/fiber/renderer.js
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -2401,6 +2403,9 @@ export function attach(
if (typeof injectProfilingHooks === 'function') {
profilingFlags |= PROFILING_FLAG_TIMELINE_SUPPORT;
}
if (supportsPerformanceTracks) {
profilingFlags |= PROFILING_FLAG_PERFORMANCE_TRACKS_SUPPORT;
}
Comment on lines +2406 to +2408
Copy link
Collaborator Author

@eps1lon eps1lon Oct 2, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Probably best to always set that and have a dedicated message for when you have a prod build with performance tracks and click on the timeline profiler?

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yea, should recommend configuring the profiling build with a link.

}

// Set supportsStrictMode to false for production renderer builds
Expand Down
5 changes: 3 additions & 2 deletions packages/react-devtools-shared/src/constants.js
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
54 changes: 47 additions & 7 deletions packages/react-devtools-shared/src/devtools/store.js
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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,
};

/**
Expand All @@ -112,6 +118,7 @@ export default class Store extends EventEmitter<{
roots: [],
rootSupportsBasicProfiling: [],
rootSupportsTimelineProfiling: [],
rootSupportsPerformanceTracks: [],
suspenseTreeMutated: [[Map<SuspenseNode['id'], SuspenseNode['id']>]],
supportsNativeStyleEditor: [],
supportsReloadAndProfile: [],
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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;
}
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -1194,7 +1216,7 @@ export default class Store extends EventEmitter<{
hasOwnerMetadata,
supportsStrictMode,
supportsTogglingSuspense,
supportsTimeline,
supportsAdvancedProfiling,
});

// Not all roots support StrictMode;
Expand Down Expand Up @@ -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;
}
},
);

Expand All @@ -1872,6 +1906,12 @@ export default class Store extends EventEmitter<{
) {
this.emit('rootSupportsTimelineProfiling');
}
if (
this._rootSupportsPerformanceTracks !==
prevRootSupportsPerformanceTracks
) {
this.emit('rootSupportsPerformanceTracks');
}
}

if (hasSuspenseTreeChanged) {
Expand Down
Original file line number Diff line number Diff line change
@@ -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 (
Expand All @@ -12,14 +14,14 @@ export default function UseEffectEvent(): React.Node {
}

function SingleHookCase() {
const onClick = experimental_useEffectEvent(() => {});
const onClick = useEffectEvent(() => {});

return <div onClick={onClick} />;
}

function useCustomHook() {
const [state, setState] = useState();
const onClick = experimental_useEffectEvent(() => {});
const onClick = useEffectEvent(() => {});
useEffect(() => {});

return [state, setState, onClick];
Expand Down
16 changes: 13 additions & 3 deletions packages/react-devtools-timeline/src/Timeline.js
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down Expand Up @@ -95,7 +101,11 @@ export function Timeline(_: {}): React.Node {
} else if (isTimelineSupported) {
content = <NoProfilingData />;
} else {
content = <TimelineNotSupported />;
content = (
<TimelineNotSupported
isPerformanceTracksSupported={isPerformanceTracksSupported}
/>
);
}

return (
Expand Down
15 changes: 15 additions & 0 deletions packages/react-devtools-timeline/src/TimelineContext.js
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ import type {
export type Context = {
file: File | null,
inMemoryTimelineData: Array<TimelineData> | null,
isPerformanceTracksSupported: boolean,
isTimelineSupported: boolean,
searchInputContainerRef: RefObject,
setFile: (file: File | null) => void,
Expand Down Expand Up @@ -66,6 +67,18 @@ function TimelineContextController({children}: Props): React.Node {
},
);

const isPerformanceTracksSupported = useSyncExternalStore<boolean>(
function subscribe(callback) {
store.addListener('rootSupportsPerformanceTracks', callback);
return function unsubscribe() {
store.removeListener('rootSupportsPerformanceTracks', callback);
};
},
function getState() {
return store.rootSupportsPerformanceTracks;
},
);

const inMemoryTimelineData = useSyncExternalStore<Array<TimelineData> | null>(
function subscribe(callback) {
store.profilerStore.addListener('isProcessingData', callback);
Expand Down Expand Up @@ -135,6 +148,7 @@ function TimelineContextController({children}: Props): React.Node {
() => ({
file,
inMemoryTimelineData,
isPerformanceTracksSupported,
isTimelineSupported,
searchInputContainerRef,
setFile,
Expand All @@ -145,6 +159,7 @@ function TimelineContextController({children}: Props): React.Node {
[
file,
inMemoryTimelineData,
isPerformanceTracksSupported,
isTimelineSupported,
setFile,
viewState,
Expand Down
58 changes: 53 additions & 5 deletions packages/react-devtools-timeline/src/TimelineNotSupported.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 (
<div className={styles.Column}>
<div className={styles.Header}>Timeline profiling not supported.</div>
<>
<p className={styles.Paragraph}>
<span>
Timeline profiler requires a development or profiling build of{' '}
<code className={styles.Code}>react-dom@^18</code>.
Please use{' '}
<a
className={styles.Link}
href="https://react.dev/reference/dev-tools/react-performance-tracks"
rel="noopener noreferrer"
target="_blank">
React Performance tracks
</a>{' '}
instead of the Timeline profiler.
</span>
</p>
</>
);
}

function UnknownUnsupportedReason() {
return (
<>
<p className={styles.Paragraph}>
Timeline profiler requires a development or profiling build of{' '}
<code className={styles.Code}>react-dom@{'>='}18</code>.
</p>
<p className={styles.Paragraph}>
In React 19.2 and above{' '}
<a
className={styles.Link}
href="https://react.dev/reference/dev-tools/react-performance-tracks"
rel="noopener noreferrer"
target="_blank">
React Performance tracks
</a>{' '}
can be used instead.
</p>
<div className={styles.LearnMoreRow}>
Click{' '}
<a
Expand All @@ -33,6 +65,22 @@ export default function TimelineNotSupported(): React.Node {
</a>{' '}
to learn more about profiling.
</div>
</>
);
}

export default function TimelineNotSupported({
isPerformanceTracksSupported,
}: Props): React.Node {
return (
<div className={styles.Column}>
<div className={styles.Header}>Timeline profiling not supported.</div>

{isPerformanceTracksSupported ? (
<PerformanceTracksSupported />
) : (
<UnknownUnsupportedReason />
)}

{isInternalFacebookBuild && (
<div className={styles.MetaGKRow}>
Expand Down
Loading