Skip to content

Commit

Permalink
fix: make heap snapshot graph a panel instead of an editor (#179)
Browse files Browse the repository at this point in the history
* 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
  • Loading branch information
connor4312 authored Apr 4, 2024
1 parent 460f590 commit 9e48be1
Show file tree
Hide file tree
Showing 9 changed files with 147 additions and 71 deletions.
13 changes: 12 additions & 1 deletion packages/vscode-js-profile-core/src/common/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,17 @@ export interface IReopenWithEditor {
requireExtension?: string;
}

/**
* Reopens the current document with the given editor, optionally only if
* the given extension is installed.
*/
export interface IRunCommand {
type: 'command';
command: string;
args: unknown[];
requireExtension?: string;
}

/**
* Calls a graph method, used in the heapsnapshot.
*/
Expand All @@ -64,4 +75,4 @@ export interface ICallHeapGraph {
inner: GraphRPCCall;
}

export type Message = IOpenDocumentMessage | IReopenWithEditor | ICallHeapGraph;
export type Message = IOpenDocumentMessage | IRunCommand | IReopenWithEditor | ICallHeapGraph;
89 changes: 58 additions & 31 deletions packages/vscode-js-profile-core/src/heapsnapshot/editorProvider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
import * as vscode from 'vscode';
import { bundlePage } from '../bundlePage';
import { Message } from '../common/types';
import { reopenWithEditor } from '../reopenWithEditor';
import { reopenWithEditor, requireExtension } from '../reopenWithEditor';
import { GraphRPCCall } from './rpc';
import { startWorker } from './startWorker';

Expand All @@ -19,7 +19,7 @@ interface IWorker extends vscode.Disposable {
worker: Workerish;
}

class HeapSnapshotDocument implements vscode.CustomDocument {
export class HeapSnapshotDocument implements vscode.CustomDocument {
constructor(
public readonly uri: vscode.Uri,
public readonly value: IWorker,
Expand Down Expand Up @@ -73,6 +73,53 @@ const workerRegistry = ((globalThis as any).__jsHeapSnapshotWorkers ??= new (cla
}
})());

export const createHeapSnapshotWorker = (uri: vscode.Uri): Promise<IWorker> =>
workerRegistry.create(uri);

export const setupHeapSnapshotWebview = async (
{ worker }: IWorker,
bundle: vscode.Uri,
uri: vscode.Uri,
webview: vscode.Webview,
extraConsts: Record<string, unknown>,
) => {
webview.onDidReceiveMessage((message: Message) => {
switch (message.type) {
case 'reopenWith':
reopenWithEditor(
uri.with({ query: message.withQuery }),
message.viewType,
message.requireExtension,
message.toSide,
);
return;
case 'command':
requireExtension(message.requireExtension, () =>
vscode.commands.executeCommand(message.command, ...message.args),
);
return;
case 'callGraph':
worker.postMessage(message.inner);
return;
default:
console.warn(`Unknown request from webview: ${JSON.stringify(message)}`);
}
});

const listener = worker.onMessage((message: unknown) => {
webview.postMessage({ method: 'graphRet', message });
});

webview.options = { enableScripts: true };
webview.html = await bundlePage(webview.asWebviewUri(bundle), {
SNAPSHOT_URI: webview.asWebviewUri(uri).toString(),
DOCUMENT_URI: uri.toString(),
...extraConsts,
});

return listener;
};

export class HeapSnapshotEditorProvider
implements vscode.CustomEditorProvider<HeapSnapshotDocument>
{
Expand All @@ -87,7 +134,7 @@ export class HeapSnapshotEditorProvider
* @inheritdoc
*/
async openCustomDocument(uri: vscode.Uri) {
const worker = await workerRegistry.create(uri);
const worker = await createHeapSnapshotWorker(uri);
return new HeapSnapshotDocument(uri, worker);
}

Expand All @@ -98,36 +145,16 @@ export class HeapSnapshotEditorProvider
document: HeapSnapshotDocument,
webviewPanel: vscode.WebviewPanel,
): Promise<void> {
webviewPanel.webview.onDidReceiveMessage((message: Message) => {
switch (message.type) {
case 'reopenWith':
reopenWithEditor(
document.uri.with({ query: message.withQuery }),
message.viewType,
message.requireExtension,
message.toSide,
);
return;
case 'callGraph':
document.value.worker.postMessage(message.inner);
return;
default:
console.warn(`Unknown request from webview: ${JSON.stringify(message)}`);
}
});
const disposable = await setupHeapSnapshotWebview(
document.value,
this.bundle,
document.uri,
webviewPanel.webview,
this.extraConsts,
);

const listener = document.value.worker.onMessage((message: unknown) => {
webviewPanel.webview.postMessage({ method: 'graphRet', message });
});
webviewPanel.onDidDispose(() => {
listener.dispose();
});

webviewPanel.webview.options = { enableScripts: true };
webviewPanel.webview.html = await bundlePage(webviewPanel.webview.asWebviewUri(this.bundle), {
SNAPSHOT_URI: webviewPanel.webview.asWebviewUri(document.uri).toString(),
DOCUMENT_URI: document.uri.toString(),
...this.extraConsts,
disposable.dispose();
});
}

Expand Down
23 changes: 15 additions & 8 deletions packages/vscode-js-profile-core/src/reopenWithEditor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,22 +4,29 @@

import * as vscode from 'vscode';

export function requireExtension<T>(extension: string | undefined, thenDo: () => T): T | undefined {
if (requireExtension && !vscode.extensions.all.some(e => e.id === extension)) {
vscode.commands.executeCommand('workbench.extensions.action.showExtensionsWithIds', [
requireExtension,
]);
return undefined;
}

return thenDo();
}

export function reopenWithEditor(
uri: vscode.Uri,
viewType: string,
requireExtension?: string,
requireExtensionId?: string,
toSide?: boolean,
) {
if (requireExtension && !vscode.extensions.all.some(e => e.id === requireExtension)) {
vscode.commands.executeCommand('workbench.extensions.action.showExtensionsWithIds', [
requireExtension,
]);
} else {
return requireExtension(requireExtensionId, () =>
vscode.commands.executeCommand(
'vscode.openWith',
uri,
viewType,
toSide ? vscode.ViewColumn.Beside : vscode.ViewColumn.Active,
);
}
),
);
}
8 changes: 7 additions & 1 deletion packages/vscode-js-profile-flame/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ You can further configure the realtime performance view with the following user

### Flame Chart View

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.
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.

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.

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

The flame chart color is tweakable via the `charts-red` color token in your VS Code theme.

### Memory Graph View

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:

![](/packages/vscode-js-profile-flame/resources/retainers.png)
14 changes: 4 additions & 10 deletions packages/vscode-js-profile-flame/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,10 @@
"watch": "webpack --mode development --watch"
},
"icon": "resources/logo.png",
"activationEvents": [
"onCommand:jsProfileVisualizer.heapsnapshot.flame.show",
"onWebviewPanel:jsProfileVisualizer.heapsnapshot.flame.show"
],
"contributes": {
"customEditors": [
{
Expand All @@ -53,16 +57,6 @@
"filenamePattern": "*.heapprofile"
}
]
},
{
"viewType": "jsProfileVisualizer.heapsnapshot.flame",
"displayName": "Heap Snapshot Retainers Graph Visualizer",
"priority": "option",
"selector": [
{
"filenamePattern": "*.heapsnapshot"
}
]
}
],
"views": {
Expand Down
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
50 changes: 41 additions & 9 deletions packages/vscode-js-profile-flame/src/extension.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,11 +12,14 @@ const allConfig = [Config.PollInterval, Config.ViewDuration, Config.Easing];

import * as vscode from 'vscode';
import { CpuProfileEditorProvider } from 'vscode-js-profile-core/out/cpu/editorProvider';
import {
createHeapSnapshotWorker,
setupHeapSnapshotWebview,
} from 'vscode-js-profile-core/out/esm/heapsnapshot/editorProvider';
import { HeapProfileEditorProvider } from 'vscode-js-profile-core/out/heap/editorProvider';
import { HeapSnapshotEditorProvider } from 'vscode-js-profile-core/out/esm/heapsnapshot/editorProvider';
import { ProfileCodeLensProvider } from 'vscode-js-profile-core/out/profileCodeLensProvider';
import { createMetrics } from './realtime/metrics';
import { readRealtimeSettings, RealtimeSessionTracker } from './realtimeSessionTracker';
import { RealtimeSessionTracker, readRealtimeSettings } from './realtimeSessionTracker';
import { RealtimeWebviewProvider } from './realtimeWebviewProvider';

export function activate(context: vscode.ExtensionContext) {
Expand Down Expand Up @@ -50,15 +53,44 @@ export function activate(context: vscode.ExtensionContext) {
},
),

vscode.window.registerCustomEditorProvider(
'jsProfileVisualizer.heapsnapshot.flame',
new HeapSnapshotEditorProvider(
vscode.Uri.joinPath(context.extensionUri, 'out', 'heapsnapshot-client.bundle.js'),
),
// note: context is not retained when hidden, unlike other editors, because
// the model is kept in a worker_thread and accessed via RPC
vscode.commands.registerCommand(
'jsProfileVisualizer.heapsnapshot.flame.show',
async ({ uri: rawUri, index, name }) => {
const panel = vscode.window.createWebviewPanel(
'jsProfileVisualizer.heapsnapshot.flame',
vscode.l10n.t('Memory Graph: {0}', name),
vscode.ViewColumn.Beside,
{
enableScripts: true,
localResourceRoots: [vscode.Uri.joinPath(context.extensionUri, 'out')],
},
);

const uri = vscode.Uri.parse(rawUri);
const worker = await createHeapSnapshotWorker(uri);
const webviewDisposable = await setupHeapSnapshotWebview(
worker,
vscode.Uri.joinPath(context.extensionUri, 'out', 'heapsnapshot-client.bundle.js'),
uri,
panel.webview,
{ SNAPSHOT_INDEX: index },
);

panel.onDidDispose(() => {
worker.dispose();
webviewDisposable.dispose();
});
},
),

// there's no state we actually need to serialize/deserialize, but register
// this so VS Code knows that it can
vscode.window.registerWebviewPanelSerializer('jsProfileVisualizer.heapsnapshot.flame.show', {
deserializeWebviewPanel() {
return Promise.resolve();
},
}),

vscode.window.registerWebviewViewProvider(RealtimeWebviewProvider.viewType, realtime),

vscode.workspace.onDidChangeConfiguration(evt => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,9 +19,7 @@ import styles from './client.css';
// eslint-disable-next-line @typescript-eslint/no-var-requires
cytoscape.use(require('cytoscape-klay'));

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

const DEFAULT_RETAINER_DISTANCE = 4;

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

useEffect(() => {
doGraphRpc(vscodeApi, 'getRetainers', [Number(index), maxDistance]).then(r =>
doGraphRpc(vscodeApi, 'getRetainers', [Number(SNAPSHOT_INDEX), maxDistance]).then(r =>
setNodes(r as IRetainingNode[]),
);
}, [maxDistance]);
Expand Down Expand Up @@ -150,7 +148,7 @@ const Graph: FunctionComponent<{ maxDistance: number }> = ({ maxDistance }) => {
} as any,
});

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

attachPathHoverHandle(root, cy);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import prettyBytes from 'pretty-bytes';
import { Icon } from 'vscode-js-profile-core/out/esm/client/icons';
import { classes } from 'vscode-js-profile-core/out/esm/client/util';
import { VsCodeApi } from 'vscode-js-profile-core/out/esm/client/vscodeApi';
import { IReopenWithEditor } from 'vscode-js-profile-core/out/esm/common/types';
import { IRunCommand } from 'vscode-js-profile-core/out/esm/common/types';
import { IClassGroup, INode } from 'vscode-js-profile-core/out/esm/heapsnapshot/rpc';
import { DataProvider, IQueryResults } from 'vscode-js-profile-core/out/esm/ql';
import { IRowProps, makeBaseTimeView } from '../common/base-time-view';
Expand All @@ -26,6 +26,8 @@ export type TableNode = (IClassGroup | INode) & {

const BaseTimeView = makeBaseTimeView<TableNode>();

declare const DOCUMENT_URI: string;

export const sortBySelfSize: SortFn<TableNode> = (a, b) => b.selfSize - a.selfSize;
export const sortByRetainedSize: SortFn<TableNode> = (a, b) => b.retainedSize - a.retainedSize;
export const sortByName: SortFn<TableNode> = (a, b) => a.name.localeCompare(b.name);
Expand Down Expand Up @@ -100,11 +102,10 @@ const timeViewRow =
const onClick = useCallback(
(evt: MouseEvent) => {
evt.stopPropagation();
vscode.postMessage<IReopenWithEditor>({
type: 'reopenWith',
withQuery: `index=${node.index}`,
toSide: true,
viewType: 'jsProfileVisualizer.heapsnapshot.flame',
vscode.postMessage<IRunCommand>({
type: 'command',
command: 'jsProfileVisualizer.heapsnapshot.flame.show',
args: [{ uri: DOCUMENT_URI, index: node.index, name: node.name }],
requireExtension: 'ms-vscode.vscode-js-profile-flame',
});
},
Expand Down

0 comments on commit 9e48be1

Please sign in to comment.