Skip to content

Commit

Permalink
[DevTools] front-end for profiling event stack (#24805)
Browse files Browse the repository at this point in the history
* [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
  • Loading branch information
blakef authored Jun 28, 2022
1 parent 88574c1 commit 2e1c884
Show file tree
Hide file tree
Showing 16 changed files with 346 additions and 48 deletions.
5 changes: 5 additions & 0 deletions packages/react-devtools-extensions/src/main.js
Original file line number Diff line number Diff line change
Expand Up @@ -245,6 +245,10 @@ function createPanelIfReactLoaded() {
}
};

const viewSourceLineFunction = (url, line) => {
chrome.devtools.panels.openResource(url, line);
};

let debugIDCounter = 0;

// For some reason in Firefox, chrome.runtime.sendMessage() from a content script
Expand Down Expand Up @@ -381,6 +385,7 @@ function createPanelIfReactLoaded() {
warnIfUnsupportedVersionDetected: true,
viewAttributeSourceFunction,
viewElementSourceFunction,
viewSourceLineFunction,
}),
);
};
Expand Down
18 changes: 18 additions & 0 deletions packages/react-devtools-shared/src/__tests__/utils-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import {
getDisplayName,
getDisplayNameForReactElement,
} from 'react-devtools-shared/src/utils';
import {stackToComponentSources} from 'react-devtools-shared/src/devtools/utils';
import {
format,
formatWithStyles,
Expand Down Expand Up @@ -53,6 +54,23 @@ describe('utils', () => {
const FauxComponent = {name: {}};
expect(getDisplayName(FauxComponent, 'Fallback')).toEqual('Fallback');
});

it('should parse a component stack trace', () => {
expect(
stackToComponentSources(`
at Foobar (http://localhost:3000/static/js/bundle.js:103:74)
at a
at header
at div
at App`),
).toEqual([
['Foobar', ['http://localhost:3000/static/js/bundle.js', 103, 74]],
['a', null],
['header', null],
['div', null],
['App', null],
]);
});
});

describe('getDisplayNameForReactElement', () => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ export const enableNamedHooksFeature = true;
export const enableProfilerChangedHookIndices = true;
export const enableStyleXFeatures = true;
export const isInternalFacebookBuild = true;
export const enableProfilerComponentTree = true;

/************************************************************************
* Do not edit the code below.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ export const enableNamedHooksFeature = true;
export const enableProfilerChangedHookIndices = true;
export const enableStyleXFeatures = false;
export const isInternalFacebookBuild = false;
export const enableProfilerComponentTree = false;

/************************************************************************
* Do not edit the code below.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,3 +19,4 @@ export const enableNamedHooksFeature = true;
export const enableProfilerChangedHookIndices = true;
export const enableStyleXFeatures = false;
export const isInternalFacebookBuild = false;
export const enableProfilerComponentTree = false;
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ export const enableNamedHooksFeature = true;
export const enableProfilerChangedHookIndices = true;
export const enableStyleXFeatures = true;
export const isInternalFacebookBuild = true;
export const enableProfilerComponentTree = true;

/************************************************************************
* Do not edit the code below.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ export const enableNamedHooksFeature = true;
export const enableProfilerChangedHookIndices = true;
export const enableStyleXFeatures = false;
export const isInternalFacebookBuild = false;
export const enableProfilerComponentTree = false;

/************************************************************************
* Do not edit the code below.
Expand Down
25 changes: 25 additions & 0 deletions packages/react-devtools-shared/src/devtools/utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -185,3 +185,28 @@ export function smartStringify(value: any) {

return JSON.stringify(value);
}

// [url, row, column]
export type Stack = [string, number, number];

const STACK_DELIMETER = /\n\s+at /;
const STACK_SOURCE_LOCATION = /([^\s]+) \((.+):(.+):(.+)\)/;

export function stackToComponentSources(
stack: string,
): Array<[string, ?Stack]> {
const out = [];
stack
.split(STACK_DELIMETER)
.slice(1)
.forEach(entry => {
const match = STACK_SOURCE_LOCATION.exec(entry);
if (match) {
const [, component, url, row, column] = match;
out.push([component, [url, parseInt(row, 10), parseInt(column, 10)]]);
} else {
out.push([entry, null]);
}
});
return out;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
/**
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @flow
*/

import {createContext} from 'react';

import type {ViewSourceLine} from 'react-devtools-shared/src/devtools/views/DevTools';

export type Context = {|
viewSourceLineFunction: ViewSourceLine | null,
|};

const ViewSourceContext = createContext<Context>(((null: any): Context));
ViewSourceContext.displayName = 'ViewSourceContext';

export default ViewSourceContext;
109 changes: 63 additions & 46 deletions packages/react-devtools-shared/src/devtools/views/DevTools.js
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ import TabBar from './TabBar';
import {SettingsContextController} from './Settings/SettingsContext';
import {TreeContextController} from './Components/TreeContext';
import ViewElementSourceContext from './Components/ViewElementSourceContext';
import ViewSourceContext from './Components/ViewSourceContext';
import FetchFileWithCachingContext from './Components/FetchFileWithCachingContext';
import HookNamesModuleLoaderContext from 'react-devtools-shared/src/devtools/views/Components/HookNamesModuleLoaderContext';
import {ProfilerContextController} from './Profiler/ProfilerContext';
Expand Down Expand Up @@ -57,6 +58,7 @@ export type ViewElementSource = (
id: number,
inspectedElement: InspectedElement,
) => void;
export type ViewSourceLine = (url: string, row: number, column: number) => void;
export type ViewAttributeSource = (
id: number,
path: Array<string | number>,
Expand All @@ -77,6 +79,7 @@ export type Props = {|
warnIfUnsupportedVersionDetected?: boolean,
viewAttributeSourceFunction?: ?ViewAttributeSource,
viewElementSourceFunction?: ?ViewElementSource,
viewSourceLineFunction?: ?ViewSourceLine,
readOnly?: boolean,
hideSettings?: boolean,
hideToggleErrorAction?: boolean,
Expand Down Expand Up @@ -136,6 +139,7 @@ export default function DevTools({
warnIfUnsupportedVersionDetected = false,
viewAttributeSourceFunction,
viewElementSourceFunction,
viewSourceLineFunction,
readOnly,
hideSettings,
hideToggleErrorAction,
Expand Down Expand Up @@ -199,6 +203,15 @@ export default function DevTools({
[canViewElementSourceFunction, viewElementSourceFunction],
);

const viewSource = useMemo(
() => ({
viewSourceLineFunction: viewSourceLineFunction || null,
// todo(blakef): Add inspect(...) method here and remove viewElementSource
// to consolidate source code inspection.
}),
[viewSourceLineFunction],
);

const contextMenu = useMemo(
() => ({
isEnabledForInspectedElement: enabledInspectedElementContextMenu,
Expand Down Expand Up @@ -267,55 +280,59 @@ export default function DevTools({
componentsPortalContainer={componentsPortalContainer}
profilerPortalContainer={profilerPortalContainer}>
<ViewElementSourceContext.Provider value={viewElementSource}>
<HookNamesModuleLoaderContext.Provider
value={hookNamesModuleLoaderFunction || null}>
<FetchFileWithCachingContext.Provider
value={fetchFileWithCaching || null}>
<TreeContextController>
<ProfilerContextController>
<TimelineContextController>
<ThemeProvider>
<div
className={styles.DevTools}
ref={devToolsRef}
data-react-devtools-portal-root={true}>
{showTabBar && (
<div className={styles.TabBar}>
<ReactLogo />
<span className={styles.DevToolsVersion}>
{process.env.DEVTOOLS_VERSION}
</span>
<div className={styles.Spacer} />
<TabBar
currentTab={tab}
id="DevTools"
selectTab={selectTab}
tabs={tabs}
type="navigation"
<ViewSourceContext.Provider value={viewSource}>
<HookNamesModuleLoaderContext.Provider
value={hookNamesModuleLoaderFunction || null}>
<FetchFileWithCachingContext.Provider
value={fetchFileWithCaching || null}>
<TreeContextController>
<ProfilerContextController>
<TimelineContextController>
<ThemeProvider>
<div
className={styles.DevTools}
ref={devToolsRef}
data-react-devtools-portal-root={true}>
{showTabBar && (
<div className={styles.TabBar}>
<ReactLogo />
<span className={styles.DevToolsVersion}>
{process.env.DEVTOOLS_VERSION}
</span>
<div className={styles.Spacer} />
<TabBar
currentTab={tab}
id="DevTools"
selectTab={selectTab}
tabs={tabs}
type="navigation"
/>
</div>
)}
<div
className={styles.TabContent}
hidden={tab !== 'components'}>
<Components
portalContainer={
componentsPortalContainer
}
/>
</div>
<div
className={styles.TabContent}
hidden={tab !== 'profiler'}>
<Profiler
portalContainer={profilerPortalContainer}
/>
</div>
)}
<div
className={styles.TabContent}
hidden={tab !== 'components'}>
<Components
portalContainer={componentsPortalContainer}
/>
</div>
<div
className={styles.TabContent}
hidden={tab !== 'profiler'}>
<Profiler
portalContainer={profilerPortalContainer}
/>
</div>
</div>
</ThemeProvider>
</TimelineContextController>
</ProfilerContextController>
</TreeContextController>
</FetchFileWithCachingContext.Provider>
</HookNamesModuleLoaderContext.Provider>
</ThemeProvider>
</TimelineContextController>
</ProfilerContextController>
</TreeContextController>
</FetchFileWithCachingContext.Provider>
</HookNamesModuleLoaderContext.Provider>
</ViewSourceContext.Provider>
</ViewElementSourceContext.Provider>
</SettingsContextController>
<UnsupportedBridgeProtocolDialog />
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import CommitFlamegraph from './CommitFlamegraph';
import CommitRanked from './CommitRanked';
import RootSelector from './RootSelector';
import {Timeline} from 'react-devtools-timeline/src/Timeline';
import SidebarEventInfo from './SidebarEventInfo';
import RecordToggle from './RecordToggle';
import ReloadAndProfileButton from './ReloadAndProfileButton';
import ProfilingImportExportButtons from './ProfilingImportExportButtons';
Expand All @@ -33,6 +34,7 @@ import {SettingsModalContextController} from 'react-devtools-shared/src/devtools
import portaledContent from '../portaledContent';
import {StoreContext} from '../context';
import {TimelineContext} from 'react-devtools-timeline/src/TimelineContext';
import {enableProfilerComponentTree} from 'react-devtools-feature-flags';

import styles from './Profiler.css';

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

const isLegacyProfilerSelected = selectedTabID !== 'timeline';
const isRightColumnVisible =
isLegacyProfilerSelected || enableProfilerComponentTree;

let view = null;
if (didRecordCommits || selectedTabID === 'timeline') {
Expand Down Expand Up @@ -102,6 +106,9 @@ function Profiler(_: {||}) {
}
}
break;
case 'timeline':
sidebar = <SidebarEventInfo />;
break;
default:
break;
}
Expand Down Expand Up @@ -145,7 +152,7 @@ function Profiler(_: {||}) {
<ModalDialog />
</div>
</div>
{isLegacyProfilerSelected && (
{isRightColumnVisible && (
<div className={styles.RightColumn}>{sidebar}</div>
)}
<SettingsModal />
Expand Down
Loading

0 comments on commit 2e1c884

Please sign in to comment.