Skip to content

Commit 9e48be1

Browse files
authored
fix: make heap snapshot graph a panel instead of an editor (#179)
* fix: make heap snapshot graph a panel instead of an editor It's not valid to open a file directly versus focusing a single element. Make it a webview panel instead to avoid the usability issue. For #175 * add mention of retainers graph
1 parent 460f590 commit 9e48be1

File tree

9 files changed

+147
-71
lines changed

9 files changed

+147
-71
lines changed

packages/vscode-js-profile-core/src/common/types.ts

+12-1
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,17 @@ export interface IReopenWithEditor {
5656
requireExtension?: string;
5757
}
5858

59+
/**
60+
* Reopens the current document with the given editor, optionally only if
61+
* the given extension is installed.
62+
*/
63+
export interface IRunCommand {
64+
type: 'command';
65+
command: string;
66+
args: unknown[];
67+
requireExtension?: string;
68+
}
69+
5970
/**
6071
* Calls a graph method, used in the heapsnapshot.
6172
*/
@@ -64,4 +75,4 @@ export interface ICallHeapGraph {
6475
inner: GraphRPCCall;
6576
}
6677

67-
export type Message = IOpenDocumentMessage | IReopenWithEditor | ICallHeapGraph;
78+
export type Message = IOpenDocumentMessage | IRunCommand | IReopenWithEditor | ICallHeapGraph;

packages/vscode-js-profile-core/src/heapsnapshot/editorProvider.ts

+58-31
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
import * as vscode from 'vscode';
66
import { bundlePage } from '../bundlePage';
77
import { Message } from '../common/types';
8-
import { reopenWithEditor } from '../reopenWithEditor';
8+
import { reopenWithEditor, requireExtension } from '../reopenWithEditor';
99
import { GraphRPCCall } from './rpc';
1010
import { startWorker } from './startWorker';
1111

@@ -19,7 +19,7 @@ interface IWorker extends vscode.Disposable {
1919
worker: Workerish;
2020
}
2121

22-
class HeapSnapshotDocument implements vscode.CustomDocument {
22+
export class HeapSnapshotDocument implements vscode.CustomDocument {
2323
constructor(
2424
public readonly uri: vscode.Uri,
2525
public readonly value: IWorker,
@@ -73,6 +73,53 @@ const workerRegistry = ((globalThis as any).__jsHeapSnapshotWorkers ??= new (cla
7373
}
7474
})());
7575

76+
export const createHeapSnapshotWorker = (uri: vscode.Uri): Promise<IWorker> =>
77+
workerRegistry.create(uri);
78+
79+
export const setupHeapSnapshotWebview = async (
80+
{ worker }: IWorker,
81+
bundle: vscode.Uri,
82+
uri: vscode.Uri,
83+
webview: vscode.Webview,
84+
extraConsts: Record<string, unknown>,
85+
) => {
86+
webview.onDidReceiveMessage((message: Message) => {
87+
switch (message.type) {
88+
case 'reopenWith':
89+
reopenWithEditor(
90+
uri.with({ query: message.withQuery }),
91+
message.viewType,
92+
message.requireExtension,
93+
message.toSide,
94+
);
95+
return;
96+
case 'command':
97+
requireExtension(message.requireExtension, () =>
98+
vscode.commands.executeCommand(message.command, ...message.args),
99+
);
100+
return;
101+
case 'callGraph':
102+
worker.postMessage(message.inner);
103+
return;
104+
default:
105+
console.warn(`Unknown request from webview: ${JSON.stringify(message)}`);
106+
}
107+
});
108+
109+
const listener = worker.onMessage((message: unknown) => {
110+
webview.postMessage({ method: 'graphRet', message });
111+
});
112+
113+
webview.options = { enableScripts: true };
114+
webview.html = await bundlePage(webview.asWebviewUri(bundle), {
115+
SNAPSHOT_URI: webview.asWebviewUri(uri).toString(),
116+
DOCUMENT_URI: uri.toString(),
117+
...extraConsts,
118+
});
119+
120+
return listener;
121+
};
122+
76123
export class HeapSnapshotEditorProvider
77124
implements vscode.CustomEditorProvider<HeapSnapshotDocument>
78125
{
@@ -87,7 +134,7 @@ export class HeapSnapshotEditorProvider
87134
* @inheritdoc
88135
*/
89136
async openCustomDocument(uri: vscode.Uri) {
90-
const worker = await workerRegistry.create(uri);
137+
const worker = await createHeapSnapshotWorker(uri);
91138
return new HeapSnapshotDocument(uri, worker);
92139
}
93140

@@ -98,36 +145,16 @@ export class HeapSnapshotEditorProvider
98145
document: HeapSnapshotDocument,
99146
webviewPanel: vscode.WebviewPanel,
100147
): Promise<void> {
101-
webviewPanel.webview.onDidReceiveMessage((message: Message) => {
102-
switch (message.type) {
103-
case 'reopenWith':
104-
reopenWithEditor(
105-
document.uri.with({ query: message.withQuery }),
106-
message.viewType,
107-
message.requireExtension,
108-
message.toSide,
109-
);
110-
return;
111-
case 'callGraph':
112-
document.value.worker.postMessage(message.inner);
113-
return;
114-
default:
115-
console.warn(`Unknown request from webview: ${JSON.stringify(message)}`);
116-
}
117-
});
148+
const disposable = await setupHeapSnapshotWebview(
149+
document.value,
150+
this.bundle,
151+
document.uri,
152+
webviewPanel.webview,
153+
this.extraConsts,
154+
);
118155

119-
const listener = document.value.worker.onMessage((message: unknown) => {
120-
webviewPanel.webview.postMessage({ method: 'graphRet', message });
121-
});
122156
webviewPanel.onDidDispose(() => {
123-
listener.dispose();
124-
});
125-
126-
webviewPanel.webview.options = { enableScripts: true };
127-
webviewPanel.webview.html = await bundlePage(webviewPanel.webview.asWebviewUri(this.bundle), {
128-
SNAPSHOT_URI: webviewPanel.webview.asWebviewUri(document.uri).toString(),
129-
DOCUMENT_URI: document.uri.toString(),
130-
...this.extraConsts,
157+
disposable.dispose();
131158
});
132159
}
133160

packages/vscode-js-profile-core/src/reopenWithEditor.ts

+15-8
Original file line numberDiff line numberDiff line change
@@ -4,22 +4,29 @@
44

55
import * as vscode from 'vscode';
66

7+
export function requireExtension<T>(extension: string | undefined, thenDo: () => T): T | undefined {
8+
if (requireExtension && !vscode.extensions.all.some(e => e.id === extension)) {
9+
vscode.commands.executeCommand('workbench.extensions.action.showExtensionsWithIds', [
10+
requireExtension,
11+
]);
12+
return undefined;
13+
}
14+
15+
return thenDo();
16+
}
17+
718
export function reopenWithEditor(
819
uri: vscode.Uri,
920
viewType: string,
10-
requireExtension?: string,
21+
requireExtensionId?: string,
1122
toSide?: boolean,
1223
) {
13-
if (requireExtension && !vscode.extensions.all.some(e => e.id === requireExtension)) {
14-
vscode.commands.executeCommand('workbench.extensions.action.showExtensionsWithIds', [
15-
requireExtension,
16-
]);
17-
} else {
24+
return requireExtension(requireExtensionId, () =>
1825
vscode.commands.executeCommand(
1926
'vscode.openWith',
2027
uri,
2128
viewType,
2229
toSide ? vscode.ViewColumn.Beside : vscode.ViewColumn.Active,
23-
);
24-
}
30+
),
31+
);
2532
}

packages/vscode-js-profile-flame/README.md

+7-1
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ You can further configure the realtime performance view with the following user
2020

2121
### Flame Chart View
2222

23-
You can open a `.cpuprofile` file (such as one taken by clicking the "profile" button in the realtime performance view), then click the 🔥 button in the upper right to open a flame chart view.
23+
You can open a `.cpuprofile` or `.heapprofile` file (such as one taken by clicking the "profile" button in the realtime performance view), then click the 🔥 button in the upper right to open a flame chart view.
2424

2525
By default, this view shows chronological "snapshots" of your program's stack taken roughly each millisecond. You can zoom and explore the flamechart, and ctrl or cmd+click on stacks to jump to the stack location.
2626

@@ -33,3 +33,9 @@ This view groups call stacks and orders them by time, creating a visual represen
3333
![](/packages/vscode-js-profile-flame/resources/flame-leftheavy.png)
3434

3535
The flame chart color is tweakable via the `charts-red` color token in your VS Code theme.
36+
37+
### Memory Graph View
38+
39+
You can open a `.heapsnapshot` file in VS Code and click on the "graph" icon beside an object in memory to view a chart of its retainers:
40+
41+
![](/packages/vscode-js-profile-flame/resources/retainers.png)

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

+4-10
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,10 @@
3232
"watch": "webpack --mode development --watch"
3333
},
3434
"icon": "resources/logo.png",
35+
"activationEvents": [
36+
"onCommand:jsProfileVisualizer.heapsnapshot.flame.show",
37+
"onWebviewPanel:jsProfileVisualizer.heapsnapshot.flame.show"
38+
],
3539
"contributes": {
3640
"customEditors": [
3741
{
@@ -53,16 +57,6 @@
5357
"filenamePattern": "*.heapprofile"
5458
}
5559
]
56-
},
57-
{
58-
"viewType": "jsProfileVisualizer.heapsnapshot.flame",
59-
"displayName": "Heap Snapshot Retainers Graph Visualizer",
60-
"priority": "option",
61-
"selector": [
62-
{
63-
"filenamePattern": "*.heapsnapshot"
64-
}
65-
]
6660
}
6761
],
6862
"views": {
Loading

packages/vscode-js-profile-flame/src/extension.ts

+41-9
Original file line numberDiff line numberDiff line change
@@ -12,11 +12,14 @@ const allConfig = [Config.PollInterval, Config.ViewDuration, Config.Easing];
1212

1313
import * as vscode from 'vscode';
1414
import { CpuProfileEditorProvider } from 'vscode-js-profile-core/out/cpu/editorProvider';
15+
import {
16+
createHeapSnapshotWorker,
17+
setupHeapSnapshotWebview,
18+
} from 'vscode-js-profile-core/out/esm/heapsnapshot/editorProvider';
1519
import { HeapProfileEditorProvider } from 'vscode-js-profile-core/out/heap/editorProvider';
16-
import { HeapSnapshotEditorProvider } from 'vscode-js-profile-core/out/esm/heapsnapshot/editorProvider';
1720
import { ProfileCodeLensProvider } from 'vscode-js-profile-core/out/profileCodeLensProvider';
1821
import { createMetrics } from './realtime/metrics';
19-
import { readRealtimeSettings, RealtimeSessionTracker } from './realtimeSessionTracker';
22+
import { RealtimeSessionTracker, readRealtimeSettings } from './realtimeSessionTracker';
2023
import { RealtimeWebviewProvider } from './realtimeWebviewProvider';
2124

2225
export function activate(context: vscode.ExtensionContext) {
@@ -50,15 +53,44 @@ export function activate(context: vscode.ExtensionContext) {
5053
},
5154
),
5255

53-
vscode.window.registerCustomEditorProvider(
54-
'jsProfileVisualizer.heapsnapshot.flame',
55-
new HeapSnapshotEditorProvider(
56-
vscode.Uri.joinPath(context.extensionUri, 'out', 'heapsnapshot-client.bundle.js'),
57-
),
58-
// note: context is not retained when hidden, unlike other editors, because
59-
// the model is kept in a worker_thread and accessed via RPC
56+
vscode.commands.registerCommand(
57+
'jsProfileVisualizer.heapsnapshot.flame.show',
58+
async ({ uri: rawUri, index, name }) => {
59+
const panel = vscode.window.createWebviewPanel(
60+
'jsProfileVisualizer.heapsnapshot.flame',
61+
vscode.l10n.t('Memory Graph: {0}', name),
62+
vscode.ViewColumn.Beside,
63+
{
64+
enableScripts: true,
65+
localResourceRoots: [vscode.Uri.joinPath(context.extensionUri, 'out')],
66+
},
67+
);
68+
69+
const uri = vscode.Uri.parse(rawUri);
70+
const worker = await createHeapSnapshotWorker(uri);
71+
const webviewDisposable = await setupHeapSnapshotWebview(
72+
worker,
73+
vscode.Uri.joinPath(context.extensionUri, 'out', 'heapsnapshot-client.bundle.js'),
74+
uri,
75+
panel.webview,
76+
{ SNAPSHOT_INDEX: index },
77+
);
78+
79+
panel.onDidDispose(() => {
80+
worker.dispose();
81+
webviewDisposable.dispose();
82+
});
83+
},
6084
),
6185

86+
// there's no state we actually need to serialize/deserialize, but register
87+
// this so VS Code knows that it can
88+
vscode.window.registerWebviewPanelSerializer('jsProfileVisualizer.heapsnapshot.flame.show', {
89+
deserializeWebviewPanel() {
90+
return Promise.resolve();
91+
},
92+
}),
93+
6294
vscode.window.registerWebviewViewProvider(RealtimeWebviewProvider.viewType, realtime),
6395

6496
vscode.workspace.onDidChangeConfiguration(evt => {

packages/vscode-js-profile-flame/src/heapsnapshot-client/client.tsx

+3-5
Original file line numberDiff line numberDiff line change
@@ -19,9 +19,7 @@ import styles from './client.css';
1919
// eslint-disable-next-line @typescript-eslint/no-var-requires
2020
cytoscape.use(require('cytoscape-klay'));
2121

22-
declare const DOCUMENT_URI: string;
23-
const snapshotUri = new URL(DOCUMENT_URI.replace(/\%3D/g, '='));
24-
const index = snapshotUri.searchParams.get('index');
22+
declare const SNAPSHOT_INDEX: number;
2523

2624
const DEFAULT_RETAINER_DISTANCE = 4;
2725

@@ -52,7 +50,7 @@ const Graph: FunctionComponent<{ maxDistance: number }> = ({ maxDistance }) => {
5250
const [nodes, setNodes] = useState<IRetainingNode[]>();
5351

5452
useEffect(() => {
55-
doGraphRpc(vscodeApi, 'getRetainers', [Number(index), maxDistance]).then(r =>
53+
doGraphRpc(vscodeApi, 'getRetainers', [Number(SNAPSHOT_INDEX), maxDistance]).then(r =>
5654
setNodes(r as IRetainingNode[]),
5755
);
5856
}, [maxDistance]);
@@ -150,7 +148,7 @@ const Graph: FunctionComponent<{ maxDistance: number }> = ({ maxDistance }) => {
150148
} as any,
151149
});
152150

153-
const root = cy.$(`#${index}`);
151+
const root = cy.$(`#${SNAPSHOT_INDEX}`);
154152
root.style('background-color', colors['charts-blue']);
155153

156154
attachPathHoverHandle(root, cy);

packages/vscode-js-profile-table/src/heapsnapshot-client/time-view.tsx

+7-6
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ import prettyBytes from 'pretty-bytes';
1010
import { Icon } from 'vscode-js-profile-core/out/esm/client/icons';
1111
import { classes } from 'vscode-js-profile-core/out/esm/client/util';
1212
import { VsCodeApi } from 'vscode-js-profile-core/out/esm/client/vscodeApi';
13-
import { IReopenWithEditor } from 'vscode-js-profile-core/out/esm/common/types';
13+
import { IRunCommand } from 'vscode-js-profile-core/out/esm/common/types';
1414
import { IClassGroup, INode } from 'vscode-js-profile-core/out/esm/heapsnapshot/rpc';
1515
import { DataProvider, IQueryResults } from 'vscode-js-profile-core/out/esm/ql';
1616
import { IRowProps, makeBaseTimeView } from '../common/base-time-view';
@@ -26,6 +26,8 @@ export type TableNode = (IClassGroup | INode) & {
2626

2727
const BaseTimeView = makeBaseTimeView<TableNode>();
2828

29+
declare const DOCUMENT_URI: string;
30+
2931
export const sortBySelfSize: SortFn<TableNode> = (a, b) => b.selfSize - a.selfSize;
3032
export const sortByRetainedSize: SortFn<TableNode> = (a, b) => b.retainedSize - a.retainedSize;
3133
export const sortByName: SortFn<TableNode> = (a, b) => a.name.localeCompare(b.name);
@@ -100,11 +102,10 @@ const timeViewRow =
100102
const onClick = useCallback(
101103
(evt: MouseEvent) => {
102104
evt.stopPropagation();
103-
vscode.postMessage<IReopenWithEditor>({
104-
type: 'reopenWith',
105-
withQuery: `index=${node.index}`,
106-
toSide: true,
107-
viewType: 'jsProfileVisualizer.heapsnapshot.flame',
105+
vscode.postMessage<IRunCommand>({
106+
type: 'command',
107+
command: 'jsProfileVisualizer.heapsnapshot.flame.show',
108+
args: [{ uri: DOCUMENT_URI, index: node.index, name: node.name }],
108109
requireExtension: 'ms-vscode.vscode-js-profile-flame',
109110
});
110111
},

0 commit comments

Comments
 (0)