Skip to content

Commit 2e1c884

Browse files
authored
[DevTools] front-end for profiling event stack (#24805)
* [DevTools] front-end for profiling event stack Adds a side-bar to the profiling tab. Users can now select an update event, and are shown the callstack from the originating component. When a source path is available there is now UI to jump to source. Add FB enabled feature flag: enableProfilerComponentTree for the side-bar. resolves #24170
1 parent 88574c1 commit 2e1c884

16 files changed

+346
-48
lines changed

packages/react-devtools-extensions/src/main.js

+5
Original file line numberDiff line numberDiff line change
@@ -245,6 +245,10 @@ function createPanelIfReactLoaded() {
245245
}
246246
};
247247

248+
const viewSourceLineFunction = (url, line) => {
249+
chrome.devtools.panels.openResource(url, line);
250+
};
251+
248252
let debugIDCounter = 0;
249253

250254
// For some reason in Firefox, chrome.runtime.sendMessage() from a content script
@@ -381,6 +385,7 @@ function createPanelIfReactLoaded() {
381385
warnIfUnsupportedVersionDetected: true,
382386
viewAttributeSourceFunction,
383387
viewElementSourceFunction,
388+
viewSourceLineFunction,
384389
}),
385390
);
386391
};

packages/react-devtools-shared/src/__tests__/utils-test.js

+18
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import {
1111
getDisplayName,
1212
getDisplayNameForReactElement,
1313
} from 'react-devtools-shared/src/utils';
14+
import {stackToComponentSources} from 'react-devtools-shared/src/devtools/utils';
1415
import {
1516
format,
1617
formatWithStyles,
@@ -53,6 +54,23 @@ describe('utils', () => {
5354
const FauxComponent = {name: {}};
5455
expect(getDisplayName(FauxComponent, 'Fallback')).toEqual('Fallback');
5556
});
57+
58+
it('should parse a component stack trace', () => {
59+
expect(
60+
stackToComponentSources(`
61+
at Foobar (http://localhost:3000/static/js/bundle.js:103:74)
62+
at a
63+
at header
64+
at div
65+
at App`),
66+
).toEqual([
67+
['Foobar', ['http://localhost:3000/static/js/bundle.js', 103, 74]],
68+
['a', null],
69+
['header', null],
70+
['div', null],
71+
['App', null],
72+
]);
73+
});
5674
});
5775

5876
describe('getDisplayNameForReactElement', () => {

packages/react-devtools-shared/src/config/DevToolsFeatureFlags.core-fb.js

+1
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ export const enableNamedHooksFeature = true;
1919
export const enableProfilerChangedHookIndices = true;
2020
export const enableStyleXFeatures = true;
2121
export const isInternalFacebookBuild = true;
22+
export const enableProfilerComponentTree = true;
2223

2324
/************************************************************************
2425
* Do not edit the code below.

packages/react-devtools-shared/src/config/DevToolsFeatureFlags.core-oss.js

+1
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ export const enableNamedHooksFeature = true;
1919
export const enableProfilerChangedHookIndices = true;
2020
export const enableStyleXFeatures = false;
2121
export const isInternalFacebookBuild = false;
22+
export const enableProfilerComponentTree = false;
2223

2324
/************************************************************************
2425
* Do not edit the code below.

packages/react-devtools-shared/src/config/DevToolsFeatureFlags.default.js

+1
Original file line numberDiff line numberDiff line change
@@ -19,3 +19,4 @@ export const enableNamedHooksFeature = true;
1919
export const enableProfilerChangedHookIndices = true;
2020
export const enableStyleXFeatures = false;
2121
export const isInternalFacebookBuild = false;
22+
export const enableProfilerComponentTree = false;

packages/react-devtools-shared/src/config/DevToolsFeatureFlags.extension-fb.js

+1
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ export const enableNamedHooksFeature = true;
1919
export const enableProfilerChangedHookIndices = true;
2020
export const enableStyleXFeatures = true;
2121
export const isInternalFacebookBuild = true;
22+
export const enableProfilerComponentTree = true;
2223

2324
/************************************************************************
2425
* Do not edit the code below.

packages/react-devtools-shared/src/config/DevToolsFeatureFlags.extension-oss.js

+1
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ export const enableNamedHooksFeature = true;
1919
export const enableProfilerChangedHookIndices = true;
2020
export const enableStyleXFeatures = false;
2121
export const isInternalFacebookBuild = false;
22+
export const enableProfilerComponentTree = false;
2223

2324
/************************************************************************
2425
* Do not edit the code below.

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

+25
Original file line numberDiff line numberDiff line change
@@ -185,3 +185,28 @@ export function smartStringify(value: any) {
185185

186186
return JSON.stringify(value);
187187
}
188+
189+
// [url, row, column]
190+
export type Stack = [string, number, number];
191+
192+
const STACK_DELIMETER = /\n\s+at /;
193+
const STACK_SOURCE_LOCATION = /([^\s]+) \((.+):(.+):(.+)\)/;
194+
195+
export function stackToComponentSources(
196+
stack: string,
197+
): Array<[string, ?Stack]> {
198+
const out = [];
199+
stack
200+
.split(STACK_DELIMETER)
201+
.slice(1)
202+
.forEach(entry => {
203+
const match = STACK_SOURCE_LOCATION.exec(entry);
204+
if (match) {
205+
const [, component, url, row, column] = match;
206+
out.push([component, [url, parseInt(row, 10), parseInt(column, 10)]]);
207+
} else {
208+
out.push([entry, null]);
209+
}
210+
});
211+
return out;
212+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
/**
2+
* Copyright (c) Facebook, Inc. and its affiliates.
3+
*
4+
* This source code is licensed under the MIT license found in the
5+
* LICENSE file in the root directory of this source tree.
6+
*
7+
* @flow
8+
*/
9+
10+
import {createContext} from 'react';
11+
12+
import type {ViewSourceLine} from 'react-devtools-shared/src/devtools/views/DevTools';
13+
14+
export type Context = {|
15+
viewSourceLineFunction: ViewSourceLine | null,
16+
|};
17+
18+
const ViewSourceContext = createContext<Context>(((null: any): Context));
19+
ViewSourceContext.displayName = 'ViewSourceContext';
20+
21+
export default ViewSourceContext;

packages/react-devtools-shared/src/devtools/views/DevTools.js

+63-46
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ import TabBar from './TabBar';
2727
import {SettingsContextController} from './Settings/SettingsContext';
2828
import {TreeContextController} from './Components/TreeContext';
2929
import ViewElementSourceContext from './Components/ViewElementSourceContext';
30+
import ViewSourceContext from './Components/ViewSourceContext';
3031
import FetchFileWithCachingContext from './Components/FetchFileWithCachingContext';
3132
import HookNamesModuleLoaderContext from 'react-devtools-shared/src/devtools/views/Components/HookNamesModuleLoaderContext';
3233
import {ProfilerContextController} from './Profiler/ProfilerContext';
@@ -57,6 +58,7 @@ export type ViewElementSource = (
5758
id: number,
5859
inspectedElement: InspectedElement,
5960
) => void;
61+
export type ViewSourceLine = (url: string, row: number, column: number) => void;
6062
export type ViewAttributeSource = (
6163
id: number,
6264
path: Array<string | number>,
@@ -77,6 +79,7 @@ export type Props = {|
7779
warnIfUnsupportedVersionDetected?: boolean,
7880
viewAttributeSourceFunction?: ?ViewAttributeSource,
7981
viewElementSourceFunction?: ?ViewElementSource,
82+
viewSourceLineFunction?: ?ViewSourceLine,
8083
readOnly?: boolean,
8184
hideSettings?: boolean,
8285
hideToggleErrorAction?: boolean,
@@ -136,6 +139,7 @@ export default function DevTools({
136139
warnIfUnsupportedVersionDetected = false,
137140
viewAttributeSourceFunction,
138141
viewElementSourceFunction,
142+
viewSourceLineFunction,
139143
readOnly,
140144
hideSettings,
141145
hideToggleErrorAction,
@@ -199,6 +203,15 @@ export default function DevTools({
199203
[canViewElementSourceFunction, viewElementSourceFunction],
200204
);
201205

206+
const viewSource = useMemo(
207+
() => ({
208+
viewSourceLineFunction: viewSourceLineFunction || null,
209+
// todo(blakef): Add inspect(...) method here and remove viewElementSource
210+
// to consolidate source code inspection.
211+
}),
212+
[viewSourceLineFunction],
213+
);
214+
202215
const contextMenu = useMemo(
203216
() => ({
204217
isEnabledForInspectedElement: enabledInspectedElementContextMenu,
@@ -267,55 +280,59 @@ export default function DevTools({
267280
componentsPortalContainer={componentsPortalContainer}
268281
profilerPortalContainer={profilerPortalContainer}>
269282
<ViewElementSourceContext.Provider value={viewElementSource}>
270-
<HookNamesModuleLoaderContext.Provider
271-
value={hookNamesModuleLoaderFunction || null}>
272-
<FetchFileWithCachingContext.Provider
273-
value={fetchFileWithCaching || null}>
274-
<TreeContextController>
275-
<ProfilerContextController>
276-
<TimelineContextController>
277-
<ThemeProvider>
278-
<div
279-
className={styles.DevTools}
280-
ref={devToolsRef}
281-
data-react-devtools-portal-root={true}>
282-
{showTabBar && (
283-
<div className={styles.TabBar}>
284-
<ReactLogo />
285-
<span className={styles.DevToolsVersion}>
286-
{process.env.DEVTOOLS_VERSION}
287-
</span>
288-
<div className={styles.Spacer} />
289-
<TabBar
290-
currentTab={tab}
291-
id="DevTools"
292-
selectTab={selectTab}
293-
tabs={tabs}
294-
type="navigation"
283+
<ViewSourceContext.Provider value={viewSource}>
284+
<HookNamesModuleLoaderContext.Provider
285+
value={hookNamesModuleLoaderFunction || null}>
286+
<FetchFileWithCachingContext.Provider
287+
value={fetchFileWithCaching || null}>
288+
<TreeContextController>
289+
<ProfilerContextController>
290+
<TimelineContextController>
291+
<ThemeProvider>
292+
<div
293+
className={styles.DevTools}
294+
ref={devToolsRef}
295+
data-react-devtools-portal-root={true}>
296+
{showTabBar && (
297+
<div className={styles.TabBar}>
298+
<ReactLogo />
299+
<span className={styles.DevToolsVersion}>
300+
{process.env.DEVTOOLS_VERSION}
301+
</span>
302+
<div className={styles.Spacer} />
303+
<TabBar
304+
currentTab={tab}
305+
id="DevTools"
306+
selectTab={selectTab}
307+
tabs={tabs}
308+
type="navigation"
309+
/>
310+
</div>
311+
)}
312+
<div
313+
className={styles.TabContent}
314+
hidden={tab !== 'components'}>
315+
<Components
316+
portalContainer={
317+
componentsPortalContainer
318+
}
319+
/>
320+
</div>
321+
<div
322+
className={styles.TabContent}
323+
hidden={tab !== 'profiler'}>
324+
<Profiler
325+
portalContainer={profilerPortalContainer}
295326
/>
296327
</div>
297-
)}
298-
<div
299-
className={styles.TabContent}
300-
hidden={tab !== 'components'}>
301-
<Components
302-
portalContainer={componentsPortalContainer}
303-
/>
304-
</div>
305-
<div
306-
className={styles.TabContent}
307-
hidden={tab !== 'profiler'}>
308-
<Profiler
309-
portalContainer={profilerPortalContainer}
310-
/>
311328
</div>
312-
</div>
313-
</ThemeProvider>
314-
</TimelineContextController>
315-
</ProfilerContextController>
316-
</TreeContextController>
317-
</FetchFileWithCachingContext.Provider>
318-
</HookNamesModuleLoaderContext.Provider>
329+
</ThemeProvider>
330+
</TimelineContextController>
331+
</ProfilerContextController>
332+
</TreeContextController>
333+
</FetchFileWithCachingContext.Provider>
334+
</HookNamesModuleLoaderContext.Provider>
335+
</ViewSourceContext.Provider>
319336
</ViewElementSourceContext.Provider>
320337
</SettingsContextController>
321338
<UnsupportedBridgeProtocolDialog />

packages/react-devtools-shared/src/devtools/views/Profiler/Profiler.js

+8-1
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ import CommitFlamegraph from './CommitFlamegraph';
1717
import CommitRanked from './CommitRanked';
1818
import RootSelector from './RootSelector';
1919
import {Timeline} from 'react-devtools-timeline/src/Timeline';
20+
import SidebarEventInfo from './SidebarEventInfo';
2021
import RecordToggle from './RecordToggle';
2122
import ReloadAndProfileButton from './ReloadAndProfileButton';
2223
import ProfilingImportExportButtons from './ProfilingImportExportButtons';
@@ -33,6 +34,7 @@ import {SettingsModalContextController} from 'react-devtools-shared/src/devtools
3334
import portaledContent from '../portaledContent';
3435
import {StoreContext} from '../context';
3536
import {TimelineContext} from 'react-devtools-timeline/src/TimelineContext';
37+
import {enableProfilerComponentTree} from 'react-devtools-feature-flags';
3638

3739
import styles from './Profiler.css';
3840

@@ -55,6 +57,8 @@ function Profiler(_: {||}) {
5557
const {supportsTimeline} = useContext(StoreContext);
5658

5759
const isLegacyProfilerSelected = selectedTabID !== 'timeline';
60+
const isRightColumnVisible =
61+
isLegacyProfilerSelected || enableProfilerComponentTree;
5862

5963
let view = null;
6064
if (didRecordCommits || selectedTabID === 'timeline') {
@@ -102,6 +106,9 @@ function Profiler(_: {||}) {
102106
}
103107
}
104108
break;
109+
case 'timeline':
110+
sidebar = <SidebarEventInfo />;
111+
break;
105112
default:
106113
break;
107114
}
@@ -145,7 +152,7 @@ function Profiler(_: {||}) {
145152
<ModalDialog />
146153
</div>
147154
</div>
148-
{isLegacyProfilerSelected && (
155+
{isRightColumnVisible && (
149156
<div className={styles.RightColumn}>{sidebar}</div>
150157
)}
151158
<SettingsModal />

0 commit comments

Comments
 (0)