Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix: make heap snapshot graph a panel instead of an editor #179

Merged
merged 2 commits into from
Apr 4, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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