Skip to content

Commit 7c2d2bc

Browse files
committed
support web in heap snapshot editor
1 parent 674cd69 commit 7c2d2bc

File tree

13 files changed

+160
-114
lines changed

13 files changed

+160
-114
lines changed

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

+14-11
Original file line numberDiff line numberDiff line change
@@ -3,13 +3,20 @@
33
*--------------------------------------------------------*/
44

55
import * as vscode from 'vscode';
6-
import { Worker } from 'worker_threads';
76
import { bundlePage } from '../bundlePage';
87
import { Message } from '../common/types';
98
import { reopenWithEditor } from '../reopenWithEditor';
9+
import { GraphRPCCall } from './rpc';
10+
import { startWorker } from './startWorker';
11+
12+
export interface Workerish {
13+
postMessage(message: GraphRPCCall): void;
14+
onMessage(listener: (message: unknown) => void): vscode.Disposable;
15+
terminate(): Promise<void>;
16+
}
1017

1118
interface IWorker extends vscode.Disposable {
12-
worker: Worker;
19+
worker: Workerish;
1320
}
1421

1522
class HeapSnapshotDocument implements vscode.CustomDocument {
@@ -35,14 +42,12 @@ class HeapSnapshotDocument implements vscode.CustomDocument {
3542
const workerRegistry = ((globalThis as any).__jsHeapSnapshotWorkers ??= new (class {
3643
private readonly workers = new Map<
3744
/* uri */ string,
38-
{ worker: Worker; rc: number; closer?: NodeJS.Timeout }
45+
{ worker: Workerish; rc: number; closer?: NodeJS.Timeout }
3946
>();
4047
public async create(uri: vscode.Uri): Promise<IWorker> {
4148
let rec = this.workers.get(uri.with({ query: '' }).toString());
4249
if (!rec) {
43-
const worker = new Worker(`${__dirname}/heapsnapshotWorker.js`, {
44-
workerData: uri.scheme === 'file' ? uri.fsPath : await vscode.workspace.fs.readFile(uri),
45-
});
50+
const worker = await startWorker(uri);
4651
rec = { worker, rc: 0 };
4752
this.workers.set(uri.toString(), rec);
4853
}
@@ -111,13 +116,11 @@ export class HeapSnapshotEditorProvider
111116
}
112117
});
113118

114-
const listener = (message: unknown) => {
119+
const listener = document.value.worker.onMessage((message: unknown) => {
115120
webviewPanel.webview.postMessage({ method: 'graphRet', message });
116-
};
117-
118-
document.value.worker.on('message', listener);
121+
});
119122
webviewPanel.onDidDispose(() => {
120-
document.value.worker.removeListener('message', listener);
123+
listener.dispose();
121124
});
122125

123126
webviewPanel.webview.options = { enableScripts: true };
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
/*---------------------------------------------------------
2+
* Copyright (C) Microsoft Corporation. All rights reserved.
3+
*--------------------------------------------------------*/
4+
5+
import { EdgeType, Graph, Node, RetainerNode } from '@vscode/v8-heap-parser';
6+
import { GraphRPCCall, GraphRPCMethods, IClassGroup, INode, IRetainingNode, NodeType } from './rpc';
7+
8+
export const isMethod = <K extends GraphRPCMethods>(
9+
method: K,
10+
t: GraphRPCCall,
11+
): t is GraphRPCCall<K> => t.method === method;
12+
13+
const mapInto = <T extends { free(): void }, R>(
14+
arr: readonly T[],
15+
mapper: (v: T, i: number) => R,
16+
): R[] => {
17+
const transmuted = new Array<R>(arr.length);
18+
for (let i = 0; i < arr.length; i++) {
19+
const value = arr[i];
20+
transmuted[i] = mapper(value, i);
21+
value.free();
22+
}
23+
24+
return transmuted;
25+
};
26+
27+
const processNodes = (nodes: readonly (Node | RetainerNode)[]): (INode | IRetainingNode)[] =>
28+
mapInto(nodes, node => ({
29+
name: node.name(),
30+
childrenLen: node.children_len,
31+
id: node.id,
32+
index: node.index,
33+
retainedSize: Number(node.retained_size),
34+
selfSize: Number(node.self_size),
35+
type: node.typ as unknown as NodeType,
36+
retainsIndex: (node as RetainerNode).retains_index,
37+
edgeType: (node as RetainerNode).edge_typ as unknown as EdgeType,
38+
}));
39+
40+
export const prepareGraphParser = async () => {
41+
const r = await import('@vscode/v8-heap-parser');
42+
const { decode_bytes, init_panic_hook } = await r.default;
43+
init_panic_hook();
44+
return decode_bytes;
45+
};
46+
47+
export const handleMessage = (graph: Promise<Graph>, message: GraphRPCCall) =>
48+
graph
49+
.then(g => {
50+
if (isMethod('getClassGroups', message)) {
51+
return mapInto(
52+
g.get_class_groups(...message.args, false),
53+
(group, index): IClassGroup => ({
54+
name: group.name(),
55+
index,
56+
retainedSize: Number(group.retained_size),
57+
selfSize: Number(group.self_size),
58+
childrenLen: group.children_len,
59+
}),
60+
);
61+
} else if (isMethod('getClassChildren', message)) {
62+
return processNodes(g.class_children(...message.args));
63+
} else if (isMethod('getNodeChildren', message)) {
64+
return processNodes(g.node_children(...message.args));
65+
} else if (isMethod('getRetainers', message)) {
66+
return processNodes(g.get_all_retainers(...message.args));
67+
} else {
68+
throw new Error(`unknown method ${message.method}`);
69+
}
70+
})
71+
.then(ok => ({
72+
id: message.id,
73+
result: { ok },
74+
}))
75+
.catch(err => ({
76+
id: message.id,
77+
result: { err: err.stack || err.message || String(err) },
78+
}));

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

+5-83
Original file line numberDiff line numberDiff line change
@@ -4,98 +4,20 @@
44

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

7-
import type { Node, RetainerNode } from '@vscode/v8-heap-parser';
87
import { readFile } from 'fs/promises';
98
import { parentPort, workerData } from 'worker_threads';
10-
import {
11-
EdgeType,
12-
GraphRPCCall,
13-
GraphRPCMethods,
14-
IClassGroup,
15-
INode,
16-
IRetainingNode,
17-
NodeType,
18-
} from './rpc';
9+
import { handleMessage, prepareGraphParser } from './heapSnapshotLogic';
10+
import { GraphRPCCall } from './rpc';
1911

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

2416
const graph = Promise.all([
2517
typeof workerData === 'string' ? readFile(workerData) : Promise.resolve(workerData as Uint8Array),
26-
import('@vscode/v8-heap-parser'),
27-
]).then(async ([f, r]) => {
28-
const { decode_bytes, init_panic_hook } = await r.default;
29-
init_panic_hook();
30-
return decode_bytes(f);
31-
});
32-
33-
export const isMethod = <K extends GraphRPCMethods>(
34-
method: K,
35-
t: GraphRPCCall,
36-
): t is GraphRPCCall<K> => t.method === method;
37-
38-
const mapInto = <T extends { free(): void }, R>(
39-
arr: readonly T[],
40-
mapper: (v: T, i: number) => R,
41-
): R[] => {
42-
const transmuted = new Array<R>(arr.length);
43-
for (let i = 0; i < arr.length; i++) {
44-
const value = arr[i];
45-
transmuted[i] = mapper(value, i);
46-
value.free();
47-
}
48-
49-
return transmuted;
50-
};
51-
52-
const processNodes = (nodes: readonly (Node | RetainerNode)[]): (INode | IRetainingNode)[] =>
53-
mapInto(nodes, node => ({
54-
name: node.name(),
55-
childrenLen: node.children_len,
56-
id: node.id,
57-
index: node.index,
58-
retainedSize: Number(node.retained_size),
59-
selfSize: Number(node.self_size),
60-
type: node.typ as unknown as NodeType,
61-
retainsIndex: (node as RetainerNode).retains_index,
62-
edgeType: (node as RetainerNode).edge_typ as unknown as EdgeType,
63-
}));
18+
prepareGraphParser(),
19+
]).then(async ([f, r]) => r(f));
6420

6521
parentPort.on('message', (message: GraphRPCCall) => {
66-
graph
67-
.then(g => {
68-
if (isMethod('getClassGroups', message)) {
69-
return mapInto(
70-
g.get_class_groups(...message.args, false),
71-
(group, index): IClassGroup => ({
72-
name: group.name(),
73-
index,
74-
retainedSize: Number(group.retained_size),
75-
selfSize: Number(group.self_size),
76-
childrenLen: group.children_len,
77-
}),
78-
);
79-
} else if (isMethod('getClassChildren', message)) {
80-
return processNodes(g.class_children(...message.args));
81-
} else if (isMethod('getNodeChildren', message)) {
82-
return processNodes(g.node_children(...message.args));
83-
} else if (isMethod('getRetainers', message)) {
84-
return processNodes(g.get_all_retainers(...message.args));
85-
} else {
86-
throw new Error(`unknown method ${message.method}`);
87-
}
88-
})
89-
.then(ok =>
90-
parentPort!.postMessage({
91-
id: message.id,
92-
result: { ok },
93-
}),
94-
)
95-
.catch(err =>
96-
parentPort!.postMessage({
97-
id: message.id,
98-
result: { err: err.stack || err.message || String(err) },
99-
}),
100-
);
22+
handleMessage(graph, message).then(m => parentPort!.postMessage(m));
10123
});
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
/*---------------------------------------------------------
2+
* Copyright (C) Microsoft Corporation. All rights reserved.
3+
*--------------------------------------------------------*/
4+
5+
import * as vscode from 'vscode';
6+
import { Worker } from 'worker_threads';
7+
import { Workerish } from './editorProvider';
8+
9+
export const startWorker = async (uri: vscode.Uri): Promise<Workerish> => {
10+
const w = new Worker(`${__dirname}/heapsnapshotWorker.js`, {
11+
workerData: uri.scheme === 'file' ? uri.fsPath : await vscode.workspace.fs.readFile(uri),
12+
});
13+
14+
return {
15+
postMessage: m => w.postMessage(m),
16+
onMessage: l => {
17+
w.on('message', l);
18+
return { dispose: () => w.off('message', l) };
19+
},
20+
terminate: async () => {
21+
await w.terminate();
22+
},
23+
};
24+
};
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
/*---------------------------------------------------------
2+
* Copyright (C) Microsoft Corporation. All rights reserved.
3+
*--------------------------------------------------------*/
4+
5+
import * as vscode from 'vscode';
6+
import { Workerish } from './editorProvider';
7+
import { handleMessage, prepareGraphParser } from './heapSnapshotLogic';
8+
9+
export const startWorker = async (uri: vscode.Uri): Promise<Workerish> => {
10+
const messageEmitter = new vscode.EventEmitter<unknown>();
11+
const graph = Promise.all([vscode.workspace.fs.readFile(uri), prepareGraphParser()]).then(
12+
([bytes, parse]) => parse(bytes),
13+
);
14+
15+
return {
16+
postMessage: m => handleMessage(graph, m),
17+
onMessage: messageEmitter.event,
18+
terminate: () => Promise.resolve(),
19+
};
20+
};

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

+1-5
Original file line numberDiff line numberDiff line change
@@ -18,11 +18,7 @@ export function properRelative(fromPath: string, toPath: string): string {
1818
}
1919
}
2020

21-
let isCaseSensitive = process.platform !== 'win32';
22-
23-
export function resetCaseSensitivePaths() {
24-
isCaseSensitive = process.platform !== 'win32';
25-
}
21+
let isCaseSensitive = typeof process !== 'undefined' && process.platform !== 'win32';
2622

2723
export function setCaseSensitivePaths(sensitive: boolean) {
2824
isCaseSensitive = sensitive;

packages/vscode-js-profile-flame/ci.yml

+1-1
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ extends:
2929
displayName: Install dependencies
3030

3131
- script: npm run compile
32-
displayName: Install dependencies
32+
displayName: Compile
3333
tsa:
3434
enabled: true
3535
options:

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

+1-1
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ const allConfig = [Config.PollInterval, Config.ViewDuration, Config.Easing];
1313
import * as vscode from 'vscode';
1414
import { CpuProfileEditorProvider } from 'vscode-js-profile-core/out/cpu/editorProvider';
1515
import { HeapProfileEditorProvider } from 'vscode-js-profile-core/out/heap/editorProvider';
16-
import { HeapSnapshotEditorProvider } from 'vscode-js-profile-core/out/heapsnapshot/editorProvider';
16+
import { HeapSnapshotEditorProvider } from 'vscode-js-profile-core/out/esm/heapsnapshot/editorProvider';
1717
import { ProfileCodeLensProvider } from 'vscode-js-profile-core/out/profileCodeLensProvider';
1818
import { createMetrics } from './realtime/metrics';
1919
import { readRealtimeSettings, RealtimeSessionTracker } from './realtimeSessionTracker';

packages/vscode-js-profile-flame/webpack.config.js

+1-1
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ module.exports = [
33
require('../../scripts/webpack.extension')(__dirname, 'node'),
44
...(process.argv.includes('--watch')
55
? []
6-
: [require('../../scripts/webpack.extension')(__dirname, 'web')]),
6+
: [require('../../scripts/webpack.extension')(__dirname, 'webworker')]),
77
{
88
...require('../../scripts/webpack.client')(__dirname, 'realtime'),
99
entry: `./src/realtime/client.ts`,

packages/vscode-js-profile-table/ci.yml

+1-1
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@ extends:
3131
displayName: Install dependencies
3232

3333
- script: npm run compile
34-
displayName: Install dependencies
34+
displayName: Compile
3535
tsa:
3636
enabled: true
3737
options:

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

+2-1
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,9 @@
55
import * as vscode from 'vscode';
66
import { CpuProfileEditorProvider } from 'vscode-js-profile-core/out/cpu/editorProvider';
77
import { DownloadFileProvider } from 'vscode-js-profile-core/out/download-file-provider';
8+
import { HeapSnapshotEditorProvider } from 'vscode-js-profile-core/out/esm/heapsnapshot/editorProvider';
89
import { HeapProfileEditorProvider } from 'vscode-js-profile-core/out/heap/editorProvider';
9-
import { HeapSnapshotEditorProvider } from 'vscode-js-profile-core/out/heapsnapshot/editorProvider';
10+
1011
import { ProfileCodeLensProvider } from 'vscode-js-profile-core/out/profileCodeLensProvider';
1112

1213
export function activate(context: vscode.ExtensionContext) {

packages/vscode-js-profile-table/webpack.config.js

+1-1
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ module.exports = [
33
require('../../scripts/webpack.extension')(__dirname, 'node'),
44
...(process.argv.includes('--watch')
55
? []
6-
: [require('../../scripts/webpack.extension')(__dirname, 'web')]),
6+
: [require('../../scripts/webpack.extension')(__dirname, 'webworker')]),
77
{
88
...require('../../scripts/webpack.client')(__dirname, 'cpu-client'),
99
entry: `./src/cpu-client/client.tsx`,

scripts/webpack.extension.js

+11-9
Original file line numberDiff line numberDiff line change
@@ -8,18 +8,20 @@ module.exports = (dirname, target) => ({
88
target,
99
output: {
1010
path: path.join(dirname, 'out'),
11-
filename: target === 'web' ? 'extension.web.js' : 'extension.js',
11+
filename: target === 'webworker' ? 'extension.web.js' : 'extension.js',
1212
libraryTarget: 'commonjs2',
1313
},
1414
resolve: {
15-
extensions: ['.ts', '.js', '.json'],
16-
...(
17-
target === 'node' ? {} : {
18-
fallback: {
19-
path: require.resolve('path-browserify'),
20-
os: require.resolve('os-browserify/browser'),
21-
}
22-
}),
15+
extensions: (target === 'webworker' ? ['.web.js'] : []).concat(['.ts', '.js', '.json']),
16+
conditionNames: ['bundler', 'module', 'require'],
17+
...(target === 'node'
18+
? {}
19+
: {
20+
fallback: {
21+
path: require.resolve('path-browserify'),
22+
os: require.resolve('os-browserify/browser'),
23+
},
24+
}),
2325
},
2426
node: {
2527
__dirname: false,

0 commit comments

Comments
 (0)