Skip to content

support web in heap snapshot editor #158

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

Merged
merged 1 commit into from
Nov 15, 2023
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
2 changes: 1 addition & 1 deletion .vscode/settings.json
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
"eslint.enable": true,
"editor.formatOnSave": true,
"editor.codeActionsOnSave": {
"source.organizeImports": true
"source.organizeImports": "explicit"
},
"editor.defaultFormatter": "esbenp.prettier-vscode"
}
25 changes: 14 additions & 11 deletions packages/vscode-js-profile-core/src/heapsnapshot/editorProvider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,20 @@
*--------------------------------------------------------*/

import * as vscode from 'vscode';
import { Worker } from 'worker_threads';
import { bundlePage } from '../bundlePage';
import { Message } from '../common/types';
import { reopenWithEditor } from '../reopenWithEditor';
import { GraphRPCCall } from './rpc';
import { startWorker } from './startWorker';

export interface Workerish {
postMessage(message: GraphRPCCall): void;
onMessage(listener: (message: unknown) => void): vscode.Disposable;
terminate(): Promise<void>;
}

interface IWorker extends vscode.Disposable {
worker: Worker;
worker: Workerish;
}

class HeapSnapshotDocument implements vscode.CustomDocument {
Expand All @@ -35,14 +42,12 @@ class HeapSnapshotDocument implements vscode.CustomDocument {
const workerRegistry = ((globalThis as any).__jsHeapSnapshotWorkers ??= new (class {
private readonly workers = new Map<
/* uri */ string,
{ worker: Worker; rc: number; closer?: NodeJS.Timeout }
{ worker: Workerish; rc: number; closer?: NodeJS.Timeout }
>();
public async create(uri: vscode.Uri): Promise<IWorker> {
let rec = this.workers.get(uri.with({ query: '' }).toString());
if (!rec) {
const worker = new Worker(`${__dirname}/heapsnapshotWorker.js`, {
workerData: uri.scheme === 'file' ? uri.fsPath : await vscode.workspace.fs.readFile(uri),
});
const worker = await startWorker(uri);
rec = { worker, rc: 0 };
this.workers.set(uri.toString(), rec);
}
Expand Down Expand Up @@ -111,13 +116,11 @@ export class HeapSnapshotEditorProvider
}
});

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

document.value.worker.on('message', listener);
});
webviewPanel.onDidDispose(() => {
document.value.worker.removeListener('message', listener);
listener.dispose();
});

webviewPanel.webview.options = { enableScripts: true };
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
/*---------------------------------------------------------
* Copyright (C) Microsoft Corporation. All rights reserved.
*--------------------------------------------------------*/

import { EdgeType, Graph, Node, RetainerNode } from '@vscode/v8-heap-parser';
import { GraphRPCCall, GraphRPCMethods, IClassGroup, INode, IRetainingNode, NodeType } from './rpc';

export const isMethod = <K extends GraphRPCMethods>(
method: K,
t: GraphRPCCall,
): t is GraphRPCCall<K> => t.method === method;

const mapInto = <T extends { free(): void }, R>(
arr: readonly T[],
mapper: (v: T, i: number) => R,
): R[] => {
const transmuted = new Array<R>(arr.length);
for (let i = 0; i < arr.length; i++) {
const value = arr[i];
transmuted[i] = mapper(value, i);
value.free();
}

return transmuted;
};

const processNodes = (nodes: readonly (Node | RetainerNode)[]): (INode | IRetainingNode)[] =>
mapInto(nodes, node => ({
name: node.name(),
childrenLen: node.children_len,
id: node.id,
index: node.index,
retainedSize: Number(node.retained_size),
selfSize: Number(node.self_size),
type: node.typ as unknown as NodeType,
retainsIndex: (node as RetainerNode).retains_index,
edgeType: (node as RetainerNode).edge_typ as unknown as EdgeType,
}));

export const prepareGraphParser = async () => {
const r = await import('@vscode/v8-heap-parser');
const { decode_bytes, init_panic_hook } = await r.default;
init_panic_hook();
return decode_bytes;
};

export const handleMessage = (graph: Promise<Graph>, message: GraphRPCCall) =>
graph
.then(g => {
if (isMethod('getClassGroups', message)) {
return mapInto(
g.get_class_groups(...message.args, false),
(group, index): IClassGroup => ({
name: group.name(),
index,
retainedSize: Number(group.retained_size),
selfSize: Number(group.self_size),
childrenLen: group.children_len,
}),
);
} else if (isMethod('getClassChildren', message)) {
return processNodes(g.class_children(...message.args));
} else if (isMethod('getNodeChildren', message)) {
return processNodes(g.node_children(...message.args));
} else if (isMethod('getRetainers', message)) {
return processNodes(g.get_all_retainers(...message.args));
} else {
throw new Error(`unknown method ${message.method}`);
}
})
.then(ok => ({
id: message.id,
result: { ok },
}))
.catch(err => ({
id: message.id,
result: { err: err.stack || err.message || String(err) },
}));
Original file line number Diff line number Diff line change
Expand Up @@ -4,98 +4,20 @@

/* eslint-disable @typescript-eslint/no-explicit-any */

import type { Node, RetainerNode } from '@vscode/v8-heap-parser';
import { readFile } from 'fs/promises';
import { parentPort, workerData } from 'worker_threads';
import {
EdgeType,
GraphRPCCall,
GraphRPCMethods,
IClassGroup,
INode,
IRetainingNode,
NodeType,
} from './rpc';
import { handleMessage, prepareGraphParser } from './heapSnapshotLogic';
import { GraphRPCCall } from './rpc';

if (!parentPort) {
throw new Error('must be run in worker thread');
}

const graph = Promise.all([
typeof workerData === 'string' ? readFile(workerData) : Promise.resolve(workerData as Uint8Array),
import('@vscode/v8-heap-parser'),
]).then(async ([f, r]) => {
const { decode_bytes, init_panic_hook } = await r.default;
init_panic_hook();
return decode_bytes(f);
});

export const isMethod = <K extends GraphRPCMethods>(
method: K,
t: GraphRPCCall,
): t is GraphRPCCall<K> => t.method === method;

const mapInto = <T extends { free(): void }, R>(
arr: readonly T[],
mapper: (v: T, i: number) => R,
): R[] => {
const transmuted = new Array<R>(arr.length);
for (let i = 0; i < arr.length; i++) {
const value = arr[i];
transmuted[i] = mapper(value, i);
value.free();
}

return transmuted;
};

const processNodes = (nodes: readonly (Node | RetainerNode)[]): (INode | IRetainingNode)[] =>
mapInto(nodes, node => ({
name: node.name(),
childrenLen: node.children_len,
id: node.id,
index: node.index,
retainedSize: Number(node.retained_size),
selfSize: Number(node.self_size),
type: node.typ as unknown as NodeType,
retainsIndex: (node as RetainerNode).retains_index,
edgeType: (node as RetainerNode).edge_typ as unknown as EdgeType,
}));
prepareGraphParser(),
]).then(async ([f, r]) => r(f));

parentPort.on('message', (message: GraphRPCCall) => {
graph
.then(g => {
if (isMethod('getClassGroups', message)) {
return mapInto(
g.get_class_groups(...message.args, false),
(group, index): IClassGroup => ({
name: group.name(),
index,
retainedSize: Number(group.retained_size),
selfSize: Number(group.self_size),
childrenLen: group.children_len,
}),
);
} else if (isMethod('getClassChildren', message)) {
return processNodes(g.class_children(...message.args));
} else if (isMethod('getNodeChildren', message)) {
return processNodes(g.node_children(...message.args));
} else if (isMethod('getRetainers', message)) {
return processNodes(g.get_all_retainers(...message.args));
} else {
throw new Error(`unknown method ${message.method}`);
}
})
.then(ok =>
parentPort!.postMessage({
id: message.id,
result: { ok },
}),
)
.catch(err =>
parentPort!.postMessage({
id: message.id,
result: { err: err.stack || err.message || String(err) },
}),
);
handleMessage(graph, message).then(m => parentPort!.postMessage(m));
});
24 changes: 24 additions & 0 deletions packages/vscode-js-profile-core/src/heapsnapshot/startWorker.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
/*---------------------------------------------------------
* Copyright (C) Microsoft Corporation. All rights reserved.
*--------------------------------------------------------*/

import * as vscode from 'vscode';
import { Worker } from 'worker_threads';
import { Workerish } from './editorProvider';

export const startWorker = async (uri: vscode.Uri): Promise<Workerish> => {
const w = new Worker(`${__dirname}/heapsnapshotWorker.js`, {
workerData: uri.scheme === 'file' ? uri.fsPath : await vscode.workspace.fs.readFile(uri),
});

return {
postMessage: m => w.postMessage(m),
onMessage: l => {
w.on('message', l);
return { dispose: () => w.off('message', l) };
},
terminate: async () => {
await w.terminate();
},
};
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
/*---------------------------------------------------------
* Copyright (C) Microsoft Corporation. All rights reserved.
*--------------------------------------------------------*/

import * as vscode from 'vscode';
import { Workerish } from './editorProvider';
import { handleMessage, prepareGraphParser } from './heapSnapshotLogic';

export const startWorker = async (uri: vscode.Uri): Promise<Workerish> => {
const messageEmitter = new vscode.EventEmitter<unknown>();
const graph = Promise.all([vscode.workspace.fs.readFile(uri), prepareGraphParser()]).then(
([bytes, parse]) => parse(bytes),
);

return {
postMessage: m => handleMessage(graph, m).then(m => messageEmitter.fire(m)),
onMessage: messageEmitter.event,
terminate: () => graph.then(g => g.free()),
};
};
6 changes: 1 addition & 5 deletions packages/vscode-js-profile-core/src/path.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,11 +18,7 @@ export function properRelative(fromPath: string, toPath: string): string {
}
}

let isCaseSensitive = process.platform !== 'win32';

export function resetCaseSensitivePaths() {
isCaseSensitive = process.platform !== 'win32';
}
let isCaseSensitive = typeof process !== 'undefined' && process.platform !== 'win32';

export function setCaseSensitivePaths(sensitive: boolean) {
isCaseSensitive = sensitive;
Expand Down
2 changes: 1 addition & 1 deletion packages/vscode-js-profile-flame/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ extends:
displayName: Install dependencies

- script: npm run compile
displayName: Install dependencies
displayName: Compile
tsa:
enabled: true
options:
Expand Down
2 changes: 1 addition & 1 deletion packages/vscode-js-profile-flame/src/extension.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ const allConfig = [Config.PollInterval, Config.ViewDuration, Config.Easing];
import * as vscode from 'vscode';
import { CpuProfileEditorProvider } from 'vscode-js-profile-core/out/cpu/editorProvider';
import { HeapProfileEditorProvider } from 'vscode-js-profile-core/out/heap/editorProvider';
import { HeapSnapshotEditorProvider } from 'vscode-js-profile-core/out/heapsnapshot/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';
Expand Down
2 changes: 1 addition & 1 deletion packages/vscode-js-profile-flame/webpack.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ module.exports = [
require('../../scripts/webpack.extension')(__dirname, 'node'),
...(process.argv.includes('--watch')
? []
: [require('../../scripts/webpack.extension')(__dirname, 'web')]),
: [require('../../scripts/webpack.extension')(__dirname, 'webworker')]),
{
...require('../../scripts/webpack.client')(__dirname, 'realtime'),
entry: `./src/realtime/client.ts`,
Expand Down
2 changes: 1 addition & 1 deletion packages/vscode-js-profile-table/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ extends:
displayName: Install dependencies

- script: npm run compile
displayName: Install dependencies
displayName: Compile
tsa:
enabled: true
options:
Expand Down
3 changes: 2 additions & 1 deletion packages/vscode-js-profile-table/src/extension.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,9 @@
import * as vscode from 'vscode';
import { CpuProfileEditorProvider } from 'vscode-js-profile-core/out/cpu/editorProvider';
import { DownloadFileProvider } from 'vscode-js-profile-core/out/download-file-provider';
import { HeapSnapshotEditorProvider } 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/heapsnapshot/editorProvider';

import { ProfileCodeLensProvider } from 'vscode-js-profile-core/out/profileCodeLensProvider';

export function activate(context: vscode.ExtensionContext) {
Expand Down
2 changes: 1 addition & 1 deletion packages/vscode-js-profile-table/webpack.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ module.exports = [
require('../../scripts/webpack.extension')(__dirname, 'node'),
...(process.argv.includes('--watch')
? []
: [require('../../scripts/webpack.extension')(__dirname, 'web')]),
: [require('../../scripts/webpack.extension')(__dirname, 'webworker')]),
{
...require('../../scripts/webpack.client')(__dirname, 'cpu-client'),
entry: `./src/cpu-client/client.tsx`,
Expand Down
20 changes: 11 additions & 9 deletions scripts/webpack.extension.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,18 +8,20 @@ module.exports = (dirname, target) => ({
target,
output: {
path: path.join(dirname, 'out'),
filename: target === 'web' ? 'extension.web.js' : 'extension.js',
filename: target === 'webworker' ? 'extension.web.js' : 'extension.js',
libraryTarget: 'commonjs2',
},
resolve: {
extensions: ['.ts', '.js', '.json'],
...(
target === 'node' ? {} : {
fallback: {
path: require.resolve('path-browserify'),
os: require.resolve('os-browserify/browser'),
}
}),
extensions: (target === 'webworker' ? ['.web.js'] : []).concat(['.ts', '.js', '.json']),
conditionNames: ['bundler', 'module', 'require'],
...(target === 'node'
? {}
: {
fallback: {
path: require.resolve('path-browserify'),
os: require.resolve('os-browserify/browser'),
},
}),
},
node: {
__dirname: false,
Expand Down