[DevTools] Recommend React Performance tracks if supported when Timeline profiler is not supported (#34684)

This commit is contained in:
Sebastian "Sebbie" Silbermann 2025-10-02 18:33:50 +02:00 committed by GitHub
parent a757cb7667
commit bc828bf6e3
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
6 changed files with 136 additions and 17 deletions

View File

@ -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

View File

@ -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.

View File

@ -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<SuspenseNode['id'], SuspenseNode['id']>]],
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) {

View File

@ -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 = <NoProfilingData />;
} else {
content = <TimelineNotSupported />;
content = (
<TimelineNotSupported
isPerformanceTracksSupported={isPerformanceTracksSupported}
/>
);
}
return (

View File

@ -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,
@ -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);
@ -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,

View File

@ -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
@ -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}>