Skip to content

Commit f2f6b5b

Browse files
committed
feat: heap flame
1 parent 063faf5 commit f2f6b5b

29 files changed

+1385
-420
lines changed

packages/vscode-js-profile-core/src/heap/tree.ts

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -37,9 +37,7 @@ export class TreeNode implements ITreeNode {
3737
constructor(
3838
public readonly node: Cdp.HeapProfiler.SamplingHeapProfileNode,
3939
public readonly parent?: TreeNode,
40-
) {
41-
this.parent = parent;
42-
}
40+
) {}
4341

4442
public toJSON(): ITreeNode {
4543
return {
@@ -89,6 +87,5 @@ export const createTree = (model: IProfileModel) => {
8987
root.totalSize += root.children[child].totalSize;
9088
}
9189

92-
console.log(root.totalSize);
9390
return root;
9491
};

packages/vscode-js-profile-flame/package.json

Lines changed: 16 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -29,19 +29,22 @@
2929
"scripts": {
3030
"pack": "vsce package --yarn",
3131
"compile": "rimraf out && concurrently \"npm:compile:*\"",
32-
"compile:client": "webpack --mode production --config webpack.client.js",
32+
"compile:cpu-client": "webpack --mode production --config webpack.cpu-client.js",
33+
"compile:heap-client": "webpack --mode production --config webpack.heap-client.js",
3334
"compile:realtime": "webpack --mode production --config webpack.realtime.js",
3435
"compile:ext": "webpack --mode production --config webpack.extension.js --target node",
3536
"compile:ext:web": "webpack --mode production --config webpack.extension.js --target web",
3637
"watch": "concurrently \"npm:watch:*\"",
37-
"watch:client": "webpack --mode development --config webpack.client.js --watch",
38+
"watch:cpu-client": "webpack --mode development --config webpack.cpu-client.js --watch",
39+
"watch:heap-client": "webpack --mode development --config webpack.heap-client.js --watch",
3840
"watch:realtime": "webpack --mode development --config webpack.realtime.js --watch",
3941
"watch:ext": "webpack --mode development --config webpack.extension.js --watch --target node"
4042
},
4143
"icon": "resources/logo.png",
4244
"activationEvents": [
4345
"onView:vscode-js-profile-flame.realtime",
44-
"onCustomEditor:jsProfileVisualizer.cpuprofile.flame"
46+
"onCustomEditor:jsProfileVisualizer.cpuprofile.flame",
47+
"onCustomEditor:jsProfileVisualizer.heapprofile.flame"
4548
],
4649
"contributes": {
4750
"customEditors": [
@@ -54,6 +57,16 @@
5457
"filenamePattern": "*.cpuprofile"
5558
}
5659
]
60+
},
61+
{
62+
"viewType": "jsProfileVisualizer.heapprofile.flame",
63+
"displayName": "Heap Profile Flame Graph Visualizer",
64+
"priority": "option",
65+
"selector": [
66+
{
67+
"filenamePattern": "*.heapprofile"
68+
}
69+
]
5770
}
5871
],
5972
"views": {
Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
/*---------------------------------------------------------
2+
* Copyright (C) Microsoft Corporation. All rights reserved.
3+
*--------------------------------------------------------*/
4+
5+
import { Category } from 'vscode-js-profile-core/out/esm/cpu/model';
6+
import { Constants } from './constants';
7+
import { IBox, IColumn, IColumnRow } from './types';
8+
9+
const getBoxInRowColumn = (
10+
columns: ReadonlyArray<IColumn>,
11+
boxes: ReadonlyMap<number, IBox>,
12+
column: number,
13+
row: number,
14+
) => {
15+
let candidate = columns[column]?.rows[row];
16+
if (typeof candidate === 'number') {
17+
candidate = columns[candidate].rows[row];
18+
}
19+
20+
return candidate !== undefined
21+
? boxes.get((candidate as { graphId: number }).graphId)
22+
: undefined;
23+
};
24+
25+
const pickColor = (row: IColumnRow): number => {
26+
if (row.category === Category.System) {
27+
return -1;
28+
}
29+
30+
const hash = row.graphId * 5381; // djb2's prime, just some bogus stuff
31+
return hash & 0xff;
32+
};
33+
34+
export default (columns: ReadonlyArray<IColumn>, filtered: ReadonlyArray<number>) => {
35+
const boxes: Map<number, IBox> = new Map();
36+
let maxY = 0;
37+
for (let x = 0; x < columns.length; x++) {
38+
const col = columns[x];
39+
const highlightY = filtered[x];
40+
for (let y = 0; y < col.rows.length; y++) {
41+
const loc = col.rows[y];
42+
if (typeof loc === 'number') {
43+
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
44+
getBoxInRowColumn(columns, boxes, x, y)!.x2 = col.x2;
45+
} else {
46+
const y1 = Constants.BoxHeight * y + Constants.TimelineHeight;
47+
const y2 = y1 + Constants.BoxHeight;
48+
boxes.set(loc.graphId, {
49+
column: x,
50+
row: y,
51+
x1: col.x1,
52+
x2: col.x2,
53+
y1,
54+
y2,
55+
level: y,
56+
text: loc.callFrame.functionName,
57+
color: pickColor(loc),
58+
category: y <= highlightY ? loc.category : Category.Deemphasized,
59+
loc,
60+
});
61+
62+
maxY = Math.max(y2, maxY);
63+
}
64+
}
65+
}
66+
67+
return {
68+
boxById: boxes,
69+
maxY,
70+
};
71+
};
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
.goToFile {
2+
width: 1em;
3+
display: inline-block;
4+
cursor: pointer;
5+
}
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
/*---------------------------------------------------------
2+
* Copyright (C) Microsoft Corporation. All rights reserved.
3+
*--------------------------------------------------------*/
4+
5+
export const enum Constants {
6+
BoxHeight = 20,
7+
TextColor = '#fff',
8+
BoxColor = '#000',
9+
TimelineHeight = 22,
10+
TimelineLabelSpacing = 200,
11+
MinWindow = 0.005,
12+
ExtraYBuffer = 300,
13+
DefaultStackLimit = 7,
14+
}
Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
/*---------------------------------------------------------
2+
* Copyright (C) Microsoft Corporation. All rights reserved.
3+
*--------------------------------------------------------*/
4+
5+
import { FunctionComponent, h } from 'preact';
6+
import { useCallback } from 'preact/hooks';
7+
import { classes } from 'vscode-js-profile-core/out/esm/client/util';
8+
import { Constants } from '../common/constants';
9+
import { IBounds, IDrag, LockBound } from '../common/types';
10+
import styles from './common.css';
11+
12+
const DragHandle: FunctionComponent<{
13+
canvasWidth: number;
14+
bounds: IBounds;
15+
current: IDrag | undefined;
16+
startDrag: (bounds: IDrag) => void;
17+
}> = ({ current, bounds, startDrag, canvasWidth }) => {
18+
const start = useCallback(
19+
(evt: MouseEvent, lock: LockBound, original: IBounds = bounds) => {
20+
startDrag({
21+
timestamp: Date.now(),
22+
pageXOrigin: evt.pageX,
23+
pageYOrigin: evt.pageY,
24+
original,
25+
xPerPixel: -1 / canvasWidth,
26+
lock: lock | LockBound.Y,
27+
});
28+
evt.preventDefault();
29+
evt.stopPropagation();
30+
},
31+
[canvasWidth, bounds],
32+
);
33+
34+
const range = bounds.maxX - bounds.minX;
35+
const lock = current?.lock ?? 0;
36+
37+
return (
38+
<div
39+
className={classes(styles.handle, current && styles.active)}
40+
style={{ height: Constants.TimelineHeight }}
41+
>
42+
<div
43+
className={classes(styles.bg, lock === LockBound.Y && styles.active)}
44+
onMouseDown={useCallback((evt: MouseEvent) => start(evt, LockBound.Y), [start])}
45+
style={{ transform: `scaleX(${range}) translateX(${(bounds.minX / range) * 100}%)` }}
46+
/>
47+
<div
48+
className={classes(styles.bookend, lock & LockBound.MaxX && styles.active)}
49+
style={{ transform: `translateX(${bounds.minX * 100}%)` }}
50+
>
51+
<div
52+
style={{ left: 0 }}
53+
onMouseDown={useCallback((evt: MouseEvent) => start(evt, LockBound.MaxX), [start])}
54+
/>
55+
</div>
56+
<div
57+
className={classes(styles.bookend, lock & LockBound.MinX && styles.active)}
58+
style={{ transform: `translateX(${(bounds.maxX - 1) * 100}%)` }}
59+
>
60+
<div
61+
style={{ right: 0 }}
62+
onMouseDown={useCallback((evt: MouseEvent) => start(evt, LockBound.MinX), [start])}
63+
/>
64+
</div>
65+
</div>
66+
);
67+
};
68+
69+
export default DragHandle;
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
/*---------------------------------------------------------
2+
* Copyright (C) Microsoft Corporation. All rights reserved.
3+
*--------------------------------------------------------*/
4+
5+
import { IBox, IColumn } from './types';
6+
7+
export default (
8+
columns: ReadonlyArray<IColumn>,
9+
boxes: ReadonlyMap<number, IBox>,
10+
column: number,
11+
row: number,
12+
) => {
13+
let candidate = columns[column]?.rows[row];
14+
if (typeof candidate === 'number') {
15+
candidate = columns[candidate].rows[row];
16+
}
17+
18+
return candidate !== undefined
19+
? boxes.get((candidate as { graphId: number }).graphId)
20+
: undefined;
21+
};
Lines changed: 109 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,109 @@
1+
/*---------------------------------------------------------
2+
* Copyright (C) Microsoft Corporation. All rights reserved.
3+
*--------------------------------------------------------*/
4+
5+
import { Fragment, FunctionComponent, h } from 'preact';
6+
import { useCallback, useContext, useEffect, useMemo, useState } from 'preact/hooks';
7+
import * as GoToFileIcon from 'vscode-codicons/src/icons/go-to-file.svg';
8+
import { Icon } from 'vscode-js-profile-core/out/esm/client/icons';
9+
import { IVscodeApi, VsCodeApi } from 'vscode-js-profile-core/out/esm/client/vscodeApi';
10+
import { getLocationText } from 'vscode-js-profile-core/out/esm/cpu/display';
11+
import { IOpenDocumentMessage } from 'vscode-js-profile-core/out/esm/cpu/types';
12+
import { Constants } from '../common/constants';
13+
import getBoxInRowColumn from '../common/get-boxIn-row-column';
14+
import styles from './common.css';
15+
import { IBox, IColumn } from './types';
16+
17+
const StackList: FunctionComponent<{
18+
box: IBox;
19+
columns: ReadonlyArray<IColumn>;
20+
boxes: ReadonlyMap<number, IBox>;
21+
setFocused(box: IBox): void;
22+
}> = ({ columns, boxes, box, setFocused }) => {
23+
const [limitedStack, setLimitedStack] = useState(true);
24+
25+
useEffect(() => setLimitedStack(true), [box]);
26+
27+
const stack = useMemo(() => {
28+
const stack: IBox[] = [box];
29+
for (let row = box.row - 1; row >= 0 && stack.length; row--) {
30+
const b = getBoxInRowColumn(columns, boxes, box.column, row);
31+
if (b) {
32+
stack.push(b);
33+
}
34+
}
35+
36+
return stack;
37+
}, [box, columns, boxes]);
38+
39+
const shouldTruncateStack = stack.length >= Constants.DefaultStackLimit + 3 && limitedStack;
40+
41+
return (
42+
<ol start={0}>
43+
{stack.map(
44+
(b, i) =>
45+
(!shouldTruncateStack || i < Constants.DefaultStackLimit) && (
46+
<li key={i}>
47+
<BoxLink box={b} onClick={setFocused} link={i > 0} />
48+
</li>
49+
),
50+
)}
51+
{shouldTruncateStack && (
52+
<li>
53+
<a onClick={() => setLimitedStack(false)}>
54+
<em>{stack.length - Constants.DefaultStackLimit} more...</em>
55+
</a>
56+
</li>
57+
)}
58+
</ol>
59+
);
60+
};
61+
62+
const BoxLink: FunctionComponent<{ box: IBox; onClick(box: IBox): void; link?: boolean }> = ({
63+
box,
64+
onClick,
65+
link,
66+
}) => {
67+
const vscode = useContext(VsCodeApi) as IVscodeApi;
68+
const open = useCallback(
69+
(evt: { altKey: boolean }) => {
70+
const src = box.loc.src;
71+
if (!src?.source.path) {
72+
return;
73+
}
74+
75+
vscode.postMessage<IOpenDocumentMessage>({
76+
type: 'openDocument',
77+
location: src,
78+
callFrame: box.loc.callFrame,
79+
toSide: evt.altKey,
80+
});
81+
},
82+
[vscode, box],
83+
);
84+
85+
const click = useCallback(() => onClick(box), [box, onClick]);
86+
const locText = getLocationText(box.loc);
87+
const linkContent = (
88+
<Fragment>
89+
{box.loc.callFrame.functionName} <em>({locText})</em>
90+
</Fragment>
91+
);
92+
93+
return (
94+
<Fragment>
95+
{link ? <a onClick={click}>{linkContent}</a> : linkContent}
96+
{box.loc.src?.source.path && (
97+
<Icon
98+
i={GoToFileIcon}
99+
className={styles.goToFile}
100+
onClick={open}
101+
role="button"
102+
title="Go to File"
103+
/>
104+
)}
105+
</Fragment>
106+
);
107+
};
108+
109+
export default StackList;
File renamed without changes.

0 commit comments

Comments
 (0)