Skip to content

Commit 063faf5

Browse files
committed
feat: heap viz
1 parent b12d654 commit 063faf5

20 files changed

+910
-59
lines changed

packages/vscode-js-profile-core/src/cpu/layout.tsx

+30-23
Original file line numberDiff line numberDiff line change
@@ -14,32 +14,39 @@ export interface IBodyProps<T> {
1414
type CpuProfileLayoutComponent<T> = FunctionComponent<{
1515
data: IDataSource<T>;
1616
body: ComponentType<IBodyProps<T>>;
17-
filterFooter?: ComponentType<{}>;
17+
filterFooter?: ComponentType<{ viewType: string; requireExtension: string }>;
1818
}>;
1919

2020
/**
2121
* Base layout component to display CPU-profile related info.
2222
*/
23-
export const cpuProfileLayoutFactory = <T extends {}>(): CpuProfileLayoutComponent<T> => ({
24-
data,
25-
body: RowBody,
26-
filterFooter: FilterFooter,
27-
}) => {
28-
const RichFilter = useMemo<RichFilterComponent<T>>(richFilter, []);
29-
const [filteredData, setFilteredData] = useState<IQueryResults<T> | undefined>(undefined);
30-
const footer = useMemo(() => (FilterFooter ? <FilterFooter /> : undefined), [FilterFooter]);
23+
export const cpuProfileLayoutFactory =
24+
<T extends {}>(): CpuProfileLayoutComponent<T> =>
25+
({ data, body: RowBody, filterFooter: FilterFooter }) => {
26+
const RichFilter = useMemo<RichFilterComponent<T>>(richFilter, []);
27+
const [filteredData, setFilteredData] = useState<IQueryResults<T> | undefined>(undefined);
28+
const footer = useMemo(
29+
() =>
30+
FilterFooter ? (
31+
<FilterFooter
32+
viewType="jsProfileVisualizer.cpuprofile.flame"
33+
requireExtension="ms-vscode.vscode-js-profile-flame"
34+
/>
35+
) : undefined,
36+
[FilterFooter],
37+
);
3138

32-
return (
33-
<Fragment>
34-
<div className={styles.filter}>
35-
<RichFilter
36-
data={data}
37-
onChange={setFilteredData}
38-
placeholder="Filter functions or files, or start a query()"
39-
foot={footer}
40-
/>
41-
</div>
42-
<div className={styles.rows}>{filteredData && <RowBody data={filteredData} />}</div>
43-
</Fragment>
44-
);
45-
};
39+
return (
40+
<Fragment>
41+
<div className={styles.filter}>
42+
<RichFilter
43+
data={data}
44+
onChange={setFilteredData}
45+
placeholder="Filter functions or files, or start a query()"
46+
foot={footer}
47+
/>
48+
</div>
49+
<div className={styles.rows}>{filteredData && <RowBody data={filteredData} />}</div>
50+
</Fragment>
51+
);
52+
};
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
/*---------------------------------------------------------
2+
* Copyright (C) Microsoft Corporation. All rights reserved.
3+
*--------------------------------------------------------*/
4+
5+
import { ITreeNode } from './model';
6+
7+
/**
8+
* Gets the human-readable label for the given location.
9+
*/
10+
export const getNodeText = (node: ITreeNode) => {
11+
if (!node.callFrame.url) {
12+
return; // 'virtual' frames like (program) or (idle)
13+
}
14+
15+
let text = `${node.callFrame.url}`;
16+
if (node.callFrame.lineNumber >= 0) {
17+
text += `:${node.callFrame.lineNumber}`;
18+
}
19+
20+
return text;
21+
};
22+
23+
export const decimalFormat = new Intl.NumberFormat(undefined, {
24+
maximumFractionDigits: 0,
25+
minimumFractionDigits: 0,
26+
});
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,105 @@
1+
/*---------------------------------------------------------
2+
* Copyright (C) Microsoft Corporation. All rights reserved.
3+
*--------------------------------------------------------*/
4+
5+
import * as vscode from 'vscode';
6+
import { bundlePage } from '../bundlePage';
7+
import { ProfileAnnotations } from '../profileAnnotations';
8+
import { ProfileCodeLensProvider } from '../profileCodeLensProvider';
9+
import { ReadonlyCustomDocument } from '../readonly-custom-document';
10+
import { reopenWithEditor } from '../reopenWithEditor';
11+
import { buildModel, IProfileModel } from './model';
12+
import { IHeapProfileRaw, Message } from './types';
13+
14+
export class HeapProfileEditorProvider
15+
implements vscode.CustomEditorProvider<ReadonlyCustomDocument<IProfileModel>>
16+
{
17+
public readonly onDidChangeCustomDocument = new vscode.EventEmitter<never>().event;
18+
19+
constructor(
20+
private readonly lens: ProfileCodeLensProvider,
21+
private readonly bundle: vscode.Uri,
22+
private readonly extraConsts: Record<string, unknown> = {},
23+
) {}
24+
25+
/**
26+
* @inheritdoc
27+
*/
28+
async openCustomDocument(uri: vscode.Uri) {
29+
const content = await vscode.workspace.fs.readFile(uri);
30+
const raw: IHeapProfileRaw = JSON.parse(new TextDecoder().decode(content));
31+
const document = new ReadonlyCustomDocument(uri, buildModel(raw));
32+
33+
const annotations = new ProfileAnnotations();
34+
// const rootPath = document.userData.rootPath;
35+
// for (const location of document.userData.locations) {
36+
// annotations.add(rootPath, location);
37+
// }
38+
39+
// this.lens.registerLenses(annotations);
40+
return document;
41+
}
42+
43+
/**
44+
* @inheritdoc
45+
*/
46+
public async resolveCustomEditor(
47+
document: ReadonlyCustomDocument<IProfileModel>,
48+
webviewPanel: vscode.WebviewPanel,
49+
): Promise<void> {
50+
webviewPanel.webview.onDidReceiveMessage((message: Message) => {
51+
switch (message.type) {
52+
case 'openDocument':
53+
// openLocation({
54+
// rootPath: document.userData?.rootPath,
55+
// viewColumn: message.toSide ? vscode.ViewColumn.Beside : vscode.ViewColumn.Active,
56+
// callFrame: message.callFrame,
57+
// location: message.location,
58+
// });
59+
return;
60+
case 'reopenWith':
61+
reopenWithEditor(document.uri, message.viewType, message.requireExtension);
62+
return;
63+
default:
64+
console.warn(`Unknown request from webview: ${JSON.stringify(message)}`);
65+
}
66+
});
67+
68+
webviewPanel.webview.options = { enableScripts: true };
69+
webviewPanel.webview.html = await bundlePage(webviewPanel.webview.asWebviewUri(this.bundle), {
70+
MODEL: document.userData,
71+
...this.extraConsts,
72+
});
73+
}
74+
75+
/**
76+
* @inheritdoc
77+
*/
78+
public async saveCustomDocument() {
79+
// no-op
80+
}
81+
82+
/**
83+
* @inheritdoc
84+
*/
85+
public async revertCustomDocument() {
86+
// no-op
87+
}
88+
89+
/**
90+
* @inheritdoc
91+
*/
92+
public async backupCustomDocument() {
93+
return { id: '', delete: () => undefined };
94+
}
95+
96+
/**
97+
* @inheritdoc
98+
*/
99+
public saveCustomDocumentAs(
100+
document: ReadonlyCustomDocument<IProfileModel>,
101+
destination: vscode.Uri,
102+
) {
103+
return vscode.workspace.fs.copy(document.uri, destination, { overwrite: true });
104+
}
105+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
/*---------------------------------------------------------
2+
* Copyright (C) Microsoft Corporation. All rights reserved.
3+
*--------------------------------------------------------*/
4+
import { ComponentType, Fragment, FunctionComponent, h } from 'preact';
5+
import { useMemo, useState } from 'preact/hooks';
6+
import { richFilter, RichFilterComponent } from '../client/rich-filter';
7+
import { IDataSource, IQueryResults } from '../ql';
8+
import styles from './layout.css';
9+
10+
export interface IBodyProps<T> {
11+
data: IQueryResults<T>;
12+
}
13+
14+
type HeapProfileLayoutComponent<T> = FunctionComponent<{
15+
data: IDataSource<T>;
16+
body: ComponentType<IBodyProps<T>>;
17+
filterFooter?: ComponentType<{ viewType: string; requireExtension: string }>;
18+
}>;
19+
20+
/**
21+
* Base layout component to display CPU-profile related info.
22+
*/
23+
export const heapProfileLayoutFactory =
24+
<T extends {}>(): HeapProfileLayoutComponent<T> =>
25+
({ data, body: RowBody, filterFooter: FilterFooter }) => {
26+
const RichFilter = useMemo<RichFilterComponent<T>>(richFilter, []);
27+
const [filteredData, setFilteredData] = useState<IQueryResults<T> | undefined>(undefined);
28+
const footer = useMemo(
29+
() =>
30+
FilterFooter ? (
31+
<FilterFooter
32+
viewType="jsProfileVisualizer.heapprofile.flame"
33+
requireExtension="ms-vscode.vscode-js-profile-flame"
34+
/>
35+
) : undefined,
36+
[FilterFooter],
37+
);
38+
39+
return (
40+
<Fragment>
41+
<div className={styles.filter}>
42+
<RichFilter
43+
data={data}
44+
onChange={setFilteredData}
45+
placeholder="Filter functions or files, or start a query()"
46+
foot={footer}
47+
/>
48+
</div>
49+
<div className={styles.rows}>{filteredData && <RowBody data={filteredData} />}</div>
50+
</Fragment>
51+
);
52+
};
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
/*---------------------------------------------------------
2+
* Copyright (C) Microsoft Corporation. All rights reserved.
3+
*--------------------------------------------------------*/
4+
5+
import { Protocol as Cdp } from 'devtools-protocol';
6+
import { IHeapProfileRaw } from './types';
7+
8+
export interface IHeapProfileNode extends Cdp.HeapProfiler.SamplingHeapProfileNode {
9+
totalSize: number;
10+
}
11+
12+
export interface ITreeNode extends Omit<IHeapProfileNode, 'children'> {
13+
children: { [id: number]: ITreeNode };
14+
childrenSize: number;
15+
parent?: ITreeNode;
16+
}
17+
18+
/**
19+
* Data model for the profile.
20+
*/
21+
export type IProfileModel = Cdp.HeapProfiler.SamplingHeapProfile;
22+
23+
/**
24+
* Computes the model for the given profile.
25+
*/
26+
export const buildModel = (profile: IHeapProfileRaw): IProfileModel => {
27+
return profile;
28+
// return {
29+
// head: profile.head,
30+
// rootPath: profile.$vscode?.rootPath,
31+
// };
32+
};
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
1+
/*---------------------------------------------------------
2+
* Copyright (C) Microsoft Corporation. All rights reserved.
3+
*--------------------------------------------------------*/
4+
5+
import { Protocol as Cdp } from 'devtools-protocol';
6+
import { IProfileModel, ITreeNode } from './model';
7+
8+
export class TreeNode implements ITreeNode {
9+
public static root() {
10+
return new TreeNode({
11+
id: -1,
12+
selfSize: 0,
13+
children: [],
14+
callFrame: {
15+
functionName: '(root)',
16+
lineNumber: -1,
17+
columnNumber: -1,
18+
scriptId: '0',
19+
url: '',
20+
},
21+
});
22+
}
23+
24+
public children: { [id: number]: TreeNode } = {};
25+
public totalSize = 0;
26+
public selfSize = 0;
27+
public childrenSize = 0;
28+
29+
public get id() {
30+
return this.node.id;
31+
}
32+
33+
public get callFrame() {
34+
return this.node.callFrame;
35+
}
36+
37+
constructor(
38+
public readonly node: Cdp.HeapProfiler.SamplingHeapProfileNode,
39+
public readonly parent?: TreeNode,
40+
) {
41+
this.parent = parent;
42+
}
43+
44+
public toJSON(): ITreeNode {
45+
return {
46+
children: this.children,
47+
childrenSize: this.childrenSize,
48+
selfSize: this.selfSize,
49+
totalSize: this.totalSize,
50+
id: this.id,
51+
callFrame: this.callFrame,
52+
};
53+
}
54+
}
55+
56+
const processNode = (node: Cdp.HeapProfiler.SamplingHeapProfileNode, parent: TreeNode) => {
57+
const treeNode = new TreeNode(node, parent);
58+
59+
node.children.forEach(child => {
60+
const childTreeNode = processNode(child, treeNode);
61+
62+
treeNode.children[childTreeNode.id] = childTreeNode;
63+
treeNode.childrenSize++;
64+
});
65+
66+
treeNode.selfSize = node.selfSize;
67+
treeNode.totalSize = node.selfSize;
68+
69+
for (const child in treeNode.children) {
70+
treeNode.totalSize += treeNode.children[child].totalSize;
71+
}
72+
73+
return treeNode;
74+
};
75+
76+
/**
77+
* Creates a bottom-up graph of the process information
78+
*/
79+
export const createTree = (model: IProfileModel) => {
80+
const root = TreeNode.root();
81+
82+
for (const node of model.head.children) {
83+
const child = processNode(node, root);
84+
root.children[child.id] = child;
85+
root.childrenSize++;
86+
}
87+
88+
for (const child in root.children) {
89+
root.totalSize += root.children[child].totalSize;
90+
}
91+
92+
console.log(root.totalSize);
93+
return root;
94+
};

0 commit comments

Comments
 (0)