Skip to content

Commit bc828bf

Browse files
authored
[DevTools] Recommend React Performance tracks if supported when Timeline profiler is not supported (#34684)
1 parent a757cb7 commit bc828bf

File tree

6 files changed

+136
-17
lines changed

6 files changed

+136
-17
lines changed

packages/react-devtools-shared/src/backend/fiber/renderer.js

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,7 @@ import {
7878
__DEBUG__,
7979
PROFILING_FLAG_BASIC_SUPPORT,
8080
PROFILING_FLAG_TIMELINE_SUPPORT,
81+
PROFILING_FLAG_PERFORMANCE_TRACKS_SUPPORT,
8182
TREE_OPERATION_ADD,
8283
TREE_OPERATION_REMOVE,
8384
TREE_OPERATION_REORDER_CHILDREN,
@@ -1074,6 +1075,7 @@ export function attach(
10741075
const supportsTogglingSuspense =
10751076
typeof setSuspenseHandler === 'function' &&
10761077
typeof scheduleUpdate === 'function';
1078+
const supportsPerformanceTracks = gte(version, '19.2.0');
10771079
10781080
if (typeof scheduleRefresh === 'function') {
10791081
// When Fast Refresh updates a component, the frontend may need to purge cached information.
@@ -2401,6 +2403,9 @@ export function attach(
24012403
if (typeof injectProfilingHooks === 'function') {
24022404
profilingFlags |= PROFILING_FLAG_TIMELINE_SUPPORT;
24032405
}
2406+
if (supportsPerformanceTracks) {
2407+
profilingFlags |= PROFILING_FLAG_PERFORMANCE_TRACKS_SUPPORT;
2408+
}
24042409
}
24052410
24062411
// Set supportsStrictMode to false for production renderer builds

packages/react-devtools-shared/src/constants.js

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -30,8 +30,9 @@ export const SUSPENSE_TREE_OPERATION_REORDER_CHILDREN = 10;
3030
export const SUSPENSE_TREE_OPERATION_RESIZE = 11;
3131
export const SUSPENSE_TREE_OPERATION_SUSPENDERS = 12;
3232

33-
export const PROFILING_FLAG_BASIC_SUPPORT = 0b01;
34-
export const PROFILING_FLAG_TIMELINE_SUPPORT = 0b10;
33+
export const PROFILING_FLAG_BASIC_SUPPORT /*. */ = 0b001;
34+
export const PROFILING_FLAG_TIMELINE_SUPPORT /* */ = 0b010;
35+
export const PROFILING_FLAG_PERFORMANCE_TRACKS_SUPPORT /* */ = 0b100;
3536

3637
export const UNKNOWN_SUSPENDERS_NONE: UnknownSuspendersReason = 0; // If we had at least one debugInfo, then that might have been the reason.
3738
export const UNKNOWN_SUSPENDERS_REASON_PRODUCTION: UnknownSuspendersReason = 1; // We're running in prod. That might be why we had unknown suspenders.

packages/react-devtools-shared/src/devtools/store.js

Lines changed: 47 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import {inspect} from 'util';
1313
import {
1414
PROFILING_FLAG_BASIC_SUPPORT,
1515
PROFILING_FLAG_TIMELINE_SUPPORT,
16+
PROFILING_FLAG_PERFORMANCE_TRACKS_SUPPORT,
1617
TREE_OPERATION_ADD,
1718
TREE_OPERATION_REMOVE,
1819
TREE_OPERATION_REMOVE_ROOT,
@@ -86,12 +87,17 @@ export type Config = {
8687
supportsTraceUpdates?: boolean,
8788
};
8889

90+
const ADVANCED_PROFILING_NONE = 0;
91+
const ADVANCED_PROFILING_TIMELINE = 1;
92+
const ADVANCED_PROFILING_PERFORMANCE_TRACKS = 2;
93+
type AdvancedProfiling = 0 | 1 | 2;
94+
8995
export type Capabilities = {
9096
supportsBasicProfiling: boolean,
9197
hasOwnerMetadata: boolean,
9298
supportsStrictMode: boolean,
9399
supportsTogglingSuspense: boolean,
94-
supportsTimeline: boolean,
100+
supportsAdvancedProfiling: AdvancedProfiling,
95101
};
96102

97103
/**
@@ -112,6 +118,7 @@ export default class Store extends EventEmitter<{
112118
roots: [],
113119
rootSupportsBasicProfiling: [],
114120
rootSupportsTimelineProfiling: [],
121+
rootSupportsPerformanceTracks: [],
115122
suspenseTreeMutated: [[Map<SuspenseNode['id'], SuspenseNode['id']>]],
116123
supportsNativeStyleEditor: [],
117124
supportsReloadAndProfile: [],
@@ -195,6 +202,7 @@ export default class Store extends EventEmitter<{
195202
// These options default to false but may be updated as roots are added and removed.
196203
_rootSupportsBasicProfiling: boolean = false;
197204
_rootSupportsTimelineProfiling: boolean = false;
205+
_rootSupportsPerformanceTracks: boolean = false;
198206

199207
_bridgeProtocol: BridgeProtocol | null = null;
200208
_unsupportedBridgeProtocolDetected: boolean = false;
@@ -474,6 +482,11 @@ export default class Store extends EventEmitter<{
474482
return this._rootSupportsTimelineProfiling;
475483
}
476484

485+
// At least one of the currently mounted roots support performance tracks.
486+
get rootSupportsPerformanceTracks(): boolean {
487+
return this._rootSupportsPerformanceTracks;
488+
}
489+
477490
get supportsInspectMatchingDOMElement(): boolean {
478491
return this._supportsInspectMatchingDOMElement;
479492
}
@@ -1161,11 +1174,20 @@ export default class Store extends EventEmitter<{
11611174
const isStrictModeCompliant = operations[i] > 0;
11621175
i++;
11631176

1177+
const profilerFlags = operations[i++];
11641178
const supportsBasicProfiling =
1165-
(operations[i] & PROFILING_FLAG_BASIC_SUPPORT) !== 0;
1179+
(profilerFlags & PROFILING_FLAG_BASIC_SUPPORT) !== 0;
11661180
const supportsTimeline =
1167-
(operations[i] & PROFILING_FLAG_TIMELINE_SUPPORT) !== 0;
1168-
i++;
1181+
(profilerFlags & PROFILING_FLAG_TIMELINE_SUPPORT) !== 0;
1182+
const supportsPerformanceTracks =
1183+
(profilerFlags & PROFILING_FLAG_PERFORMANCE_TRACKS_SUPPORT) !== 0;
1184+
let supportsAdvancedProfiling: AdvancedProfiling =
1185+
ADVANCED_PROFILING_NONE;
1186+
if (supportsPerformanceTracks) {
1187+
supportsAdvancedProfiling = ADVANCED_PROFILING_PERFORMANCE_TRACKS;
1188+
} else if (supportsTimeline) {
1189+
supportsAdvancedProfiling = ADVANCED_PROFILING_TIMELINE;
1190+
}
11691191

11701192
let supportsStrictMode = false;
11711193
let hasOwnerMetadata = false;
@@ -1194,7 +1216,7 @@ export default class Store extends EventEmitter<{
11941216
hasOwnerMetadata,
11951217
supportsStrictMode,
11961218
supportsTogglingSuspense,
1197-
supportsTimeline,
1219+
supportsAdvancedProfiling,
11981220
});
11991221

12001222
// Not all roots support StrictMode;
@@ -1842,21 +1864,33 @@ export default class Store extends EventEmitter<{
18421864
const prevRootSupportsProfiling = this._rootSupportsBasicProfiling;
18431865
const prevRootSupportsTimelineProfiling =
18441866
this._rootSupportsTimelineProfiling;
1867+
const prevRootSupportsPerformanceTracks =
1868+
this._rootSupportsPerformanceTracks;
18451869

18461870
this._hasOwnerMetadata = false;
18471871
this._rootSupportsBasicProfiling = false;
18481872
this._rootSupportsTimelineProfiling = false;
1873+
this._rootSupportsPerformanceTracks = false;
18491874
this._rootIDToCapabilities.forEach(
1850-
({supportsBasicProfiling, hasOwnerMetadata, supportsTimeline}) => {
1875+
({
1876+
supportsBasicProfiling,
1877+
hasOwnerMetadata,
1878+
supportsAdvancedProfiling,
1879+
}) => {
18511880
if (supportsBasicProfiling) {
18521881
this._rootSupportsBasicProfiling = true;
18531882
}
18541883
if (hasOwnerMetadata) {
18551884
this._hasOwnerMetadata = true;
18561885
}
1857-
if (supportsTimeline) {
1886+
if (supportsAdvancedProfiling === ADVANCED_PROFILING_TIMELINE) {
18581887
this._rootSupportsTimelineProfiling = true;
18591888
}
1889+
if (
1890+
supportsAdvancedProfiling === ADVANCED_PROFILING_PERFORMANCE_TRACKS
1891+
) {
1892+
this._rootSupportsPerformanceTracks = true;
1893+
}
18601894
},
18611895
);
18621896

@@ -1872,6 +1906,12 @@ export default class Store extends EventEmitter<{
18721906
) {
18731907
this.emit('rootSupportsTimelineProfiling');
18741908
}
1909+
if (
1910+
this._rootSupportsPerformanceTracks !==
1911+
prevRootSupportsPerformanceTracks
1912+
) {
1913+
this.emit('rootSupportsPerformanceTracks');
1914+
}
18751915
}
18761916

18771917
if (hasSuspenseTreeChanged) {

packages/react-devtools-timeline/src/Timeline.js

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -33,8 +33,14 @@ import {TimelineSearchContextController} from './TimelineSearchContext';
3333
import styles from './Timeline.css';
3434

3535
export function Timeline(_: {}): React.Node {
36-
const {file, inMemoryTimelineData, isTimelineSupported, setFile, viewState} =
37-
useContext(TimelineContext);
36+
const {
37+
file,
38+
inMemoryTimelineData,
39+
isPerformanceTracksSupported,
40+
isTimelineSupported,
41+
setFile,
42+
viewState,
43+
} = useContext(TimelineContext);
3844
const {didRecordCommits, isProfiling} = useContext(ProfilerContext);
3945

4046
const ref = useRef(null);
@@ -95,7 +101,11 @@ export function Timeline(_: {}): React.Node {
95101
} else if (isTimelineSupported) {
96102
content = <NoProfilingData />;
97103
} else {
98-
content = <TimelineNotSupported />;
104+
content = (
105+
<TimelineNotSupported
106+
isPerformanceTracksSupported={isPerformanceTracksSupported}
107+
/>
108+
);
99109
}
100110

101111
return (

packages/react-devtools-timeline/src/TimelineContext.js

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ import type {
3131
export type Context = {
3232
file: File | null,
3333
inMemoryTimelineData: Array<TimelineData> | null,
34+
isPerformanceTracksSupported: boolean,
3435
isTimelineSupported: boolean,
3536
searchInputContainerRef: RefObject,
3637
setFile: (file: File | null) => void,
@@ -66,6 +67,18 @@ function TimelineContextController({children}: Props): React.Node {
6667
},
6768
);
6869

70+
const isPerformanceTracksSupported = useSyncExternalStore<boolean>(
71+
function subscribe(callback) {
72+
store.addListener('rootSupportsPerformanceTracks', callback);
73+
return function unsubscribe() {
74+
store.removeListener('rootSupportsPerformanceTracks', callback);
75+
};
76+
},
77+
function getState() {
78+
return store.rootSupportsPerformanceTracks;
79+
},
80+
);
81+
6982
const inMemoryTimelineData = useSyncExternalStore<Array<TimelineData> | null>(
7083
function subscribe(callback) {
7184
store.profilerStore.addListener('isProcessingData', callback);
@@ -135,6 +148,7 @@ function TimelineContextController({children}: Props): React.Node {
135148
() => ({
136149
file,
137150
inMemoryTimelineData,
151+
isPerformanceTracksSupported,
138152
isTimelineSupported,
139153
searchInputContainerRef,
140154
setFile,
@@ -145,6 +159,7 @@ function TimelineContextController({children}: Props): React.Node {
145159
[
146160
file,
147161
inMemoryTimelineData,
162+
isPerformanceTracksSupported,
148163
isTimelineSupported,
149164
setFile,
150165
viewState,

packages/react-devtools-timeline/src/TimelineNotSupported.js

Lines changed: 53 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -12,16 +12,48 @@ import {isInternalFacebookBuild} from 'react-devtools-feature-flags';
1212

1313
import styles from './TimelineNotSupported.css';
1414

15-
export default function TimelineNotSupported(): React.Node {
15+
type Props = {
16+
isPerformanceTracksSupported: boolean,
17+
};
18+
19+
function PerformanceTracksSupported() {
1620
return (
17-
<div className={styles.Column}>
18-
<div className={styles.Header}>Timeline profiling not supported.</div>
21+
<>
1922
<p className={styles.Paragraph}>
2023
<span>
21-
Timeline profiler requires a development or profiling build of{' '}
22-
<code className={styles.Code}>react-dom@^18</code>.
24+
Please use{' '}
25+
<a
26+
className={styles.Link}
27+
href="https://react.dev/reference/dev-tools/react-performance-tracks"
28+
rel="noopener noreferrer"
29+
target="_blank">
30+
React Performance tracks
31+
</a>{' '}
32+
instead of the Timeline profiler.
2333
</span>
2434
</p>
35+
</>
36+
);
37+
}
38+
39+
function UnknownUnsupportedReason() {
40+
return (
41+
<>
42+
<p className={styles.Paragraph}>
43+
Timeline profiler requires a development or profiling build of{' '}
44+
<code className={styles.Code}>react-dom@{'>='}18</code>.
45+
</p>
46+
<p className={styles.Paragraph}>
47+
In React 19.2 and above{' '}
48+
<a
49+
className={styles.Link}
50+
href="https://react.dev/reference/dev-tools/react-performance-tracks"
51+
rel="noopener noreferrer"
52+
target="_blank">
53+
React Performance tracks
54+
</a>{' '}
55+
can be used instead.
56+
</p>
2557
<div className={styles.LearnMoreRow}>
2658
Click{' '}
2759
<a
@@ -33,6 +65,22 @@ export default function TimelineNotSupported(): React.Node {
3365
</a>{' '}
3466
to learn more about profiling.
3567
</div>
68+
</>
69+
);
70+
}
71+
72+
export default function TimelineNotSupported({
73+
isPerformanceTracksSupported,
74+
}: Props): React.Node {
75+
return (
76+
<div className={styles.Column}>
77+
<div className={styles.Header}>Timeline profiling not supported.</div>
78+
79+
{isPerformanceTracksSupported ? (
80+
<PerformanceTracksSupported />
81+
) : (
82+
<UnknownUnsupportedReason />
83+
)}
3684

3785
{isInternalFacebookBuild && (
3886
<div className={styles.MetaGKRow}>

0 commit comments

Comments
 (0)