Skip to content

Commit 23eab92

Browse files
committed
feat: use inspector for heap profiles
1 parent 0eabf2d commit 23eab92

File tree

3 files changed

+209
-30
lines changed

3 files changed

+209
-30
lines changed

ts/src/heap-profiler-inspector.ts

Lines changed: 180 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,180 @@
1+
import * as inspector from 'node:inspector';
2+
import {AllocationProfileNode, Allocation} from './v8-types';
3+
4+
const session = new inspector.Session();
5+
6+
export interface SamplingHeapProfileSample {
7+
size: number;
8+
nodeId: number;
9+
ordinal: number;
10+
}
11+
12+
export interface SamplingHeapProfileNode {
13+
callFrame: inspector.Runtime.CallFrame;
14+
selfSize: number;
15+
id: number;
16+
children: SamplingHeapProfileNode[];
17+
}
18+
19+
/**
20+
* Need to create this interface since the type definitions file for node inspector
21+
* at https://github.com/DefinitelyTyped/DefinitelyTyped/blob/master/types/node/inspector.d.ts
22+
* has not been updated with the latest changes yet.
23+
*
24+
* The types defined through this interface are in sync with the documentation found at -
25+
* https://chromedevtools.github.io/devtools-protocol/v8/HeapProfiler/
26+
*/
27+
export interface CompatibleSamplingHeapProfile {
28+
head: SamplingHeapProfileNode;
29+
samples: SamplingHeapProfileSample[];
30+
}
31+
32+
export function startSamplingHeapProfiler(
33+
heapIntervalBytes: number,
34+
stackDepth: number
35+
): Promise<void> {
36+
session.connect();
37+
return new Promise<void>((resolve, reject) => {
38+
session.post(
39+
'HeapProfiler.startSampling',
40+
{heapIntervalBytes},
41+
(err: Error | null): void => {
42+
if (err !== null) {
43+
console.error(`Error starting heap sampling: ${err}`);
44+
reject(err);
45+
return;
46+
}
47+
console.log(
48+
`Started Heap Sampling with interval bytes ${heapIntervalBytes}`
49+
);
50+
resolve();
51+
}
52+
);
53+
});
54+
}
55+
56+
/**
57+
* Stops the sampling heap profile and discards the current profile.
58+
*/
59+
export function stopSamplingHeapProfiler(): Promise<void> {
60+
return new Promise<void>((resolve, reject) => {
61+
session.post(
62+
'HeapProfiler.stopSampling',
63+
(
64+
err: Error | null,
65+
profile: inspector.HeapProfiler.StopSamplingReturnType
66+
) => {
67+
if (err !== null) {
68+
console.error(`Error stopping heap sampling ${err}`);
69+
reject(err);
70+
return;
71+
}
72+
console.log(
73+
`Stopped sampling heap, discarding current profile: ${profile}`
74+
);
75+
session.disconnect();
76+
console.log('Disconnected from current profiling session');
77+
resolve();
78+
}
79+
);
80+
});
81+
}
82+
83+
export async function getAllocationProfile(): Promise<AllocationProfileNode> {
84+
return new Promise<AllocationProfileNode>((resolve, reject) => {
85+
session.post(
86+
'HeapProfiler.getSamplingProfile',
87+
(
88+
err: Error | null,
89+
result: inspector.HeapProfiler.GetSamplingProfileReturnType
90+
) => {
91+
if (err !== null) {
92+
console.error(`Error getting sampling profile ${err}`);
93+
reject(err);
94+
return;
95+
}
96+
const compatibleHeapProfile =
97+
result.profile as CompatibleSamplingHeapProfile;
98+
resolve(
99+
translateToAllocationProfileNode(
100+
compatibleHeapProfile.head,
101+
compatibleHeapProfile.samples
102+
)
103+
);
104+
}
105+
);
106+
});
107+
}
108+
109+
function translateToAllocationProfileNode(
110+
node: SamplingHeapProfileNode,
111+
samples: SamplingHeapProfileSample[]
112+
): AllocationProfileNode {
113+
const allocationProfileNode: AllocationProfileNode = {
114+
allocations: [],
115+
name: node.callFrame.functionName,
116+
scriptName: node.callFrame.url,
117+
scriptId: Number(node.callFrame.scriptId),
118+
lineNumber: node.callFrame.lineNumber,
119+
columnNumber: node.callFrame.columnNumber,
120+
children: [],
121+
};
122+
123+
const children: AllocationProfileNode[] = new Array<AllocationProfileNode>(
124+
node.children.length
125+
);
126+
for (let i = 0; i < node.children.length; i++) {
127+
children.splice(
128+
i,
129+
1,
130+
translateToAllocationProfileNode(node.children[i], samples)
131+
);
132+
}
133+
allocationProfileNode.children = children;
134+
135+
// find all samples belonging to this node Id
136+
const samplesForCurrentNodeId: SamplingHeapProfileSample[] =
137+
filterSamplesBasedOnNodeId(node.id, samples);
138+
const mappedAllocationsForNodeId: Allocation[] =
139+
createAllocationsFromSamplesForNode(samplesForCurrentNodeId);
140+
141+
allocationProfileNode.allocations = mappedAllocationsForNodeId;
142+
return allocationProfileNode;
143+
}
144+
145+
function filterSamplesBasedOnNodeId(
146+
nodeId: number,
147+
samples: SamplingHeapProfileSample[]
148+
): SamplingHeapProfileSample[] {
149+
const filtered = samples.filter((sample: SamplingHeapProfileSample) => {
150+
return sample.nodeId === nodeId;
151+
});
152+
return filtered;
153+
}
154+
155+
function createAllocationsFromSamplesForNode(
156+
samplesForNode: SamplingHeapProfileSample[]
157+
): Allocation[] {
158+
const sampleSizeToCountMap = new Map<number, number>();
159+
samplesForNode.forEach((sample: SamplingHeapProfileSample) => {
160+
const currentCountForSize: number | undefined = sampleSizeToCountMap.get(
161+
sample.size
162+
);
163+
if (currentCountForSize !== undefined) {
164+
sampleSizeToCountMap.set(sample.size, currentCountForSize + 1);
165+
} else {
166+
sampleSizeToCountMap.set(sample.size, 1);
167+
}
168+
});
169+
170+
const mappedAllocations: Allocation[] = [];
171+
sampleSizeToCountMap.forEach((size: number, count: number) => {
172+
const mappedAllocation: Allocation = {
173+
sizeBytes: size,
174+
count: count,
175+
};
176+
mappedAllocations.push(mappedAllocation);
177+
});
178+
179+
return mappedAllocations;
180+
}

ts/src/heap-profiler.ts

Lines changed: 10 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -20,26 +20,25 @@ import {
2020
getAllocationProfile,
2121
startSamplingHeapProfiler,
2222
stopSamplingHeapProfiler,
23-
} from './heap-profiler-bindings';
23+
} from './heap-profiler-inspector';
2424
import {serializeHeapProfile} from './profile-serializer';
2525
import {SourceMapper} from './sourcemapper/sourcemapper';
2626
import {AllocationProfileNode} from './v8-types';
2727

2828
let enabled = false;
2929
let heapIntervalBytes = 0;
30-
let heapStackDepth = 0;
3130

3231
/*
3332
* Collects a heap profile when heapProfiler is enabled. Otherwise throws
3433
* an error.
3534
*
3635
* Data is returned in V8 allocation profile format.
3736
*/
38-
export function v8Profile(): AllocationProfileNode {
37+
export async function v8Profile(): Promise<AllocationProfileNode> {
3938
if (!enabled) {
4039
throw new Error('Heap profiler is not enabled.');
4140
}
42-
return getAllocationProfile();
41+
return await getAllocationProfile();
4342
}
4443

4544
/**
@@ -49,12 +48,12 @@ export function v8Profile(): AllocationProfileNode {
4948
* @param ignoreSamplePath
5049
* @param sourceMapper
5150
*/
52-
export function profile(
51+
export async function profile(
5352
ignoreSamplePath?: string,
5453
sourceMapper?: SourceMapper
55-
): perftools.profiles.IProfile {
54+
): Promise<perftools.profiles.IProfile> {
5655
const startTimeNanos = Date.now() * 1000 * 1000;
57-
const result = v8Profile();
56+
const result = await v8Profile();
5857
// Add node for external memory usage.
5958
// Current type definitions do not have external.
6059
// TODO: remove any once type definition is updated to include external.
@@ -84,17 +83,17 @@ export function profile(
8483
* started with different parameters, this throws an error.
8584
*
8685
* @param intervalBytes - average number of bytes between samples.
87-
* @param stackDepth - maximum stack depth for samples collected.
86+
* @param stackDepth - maximum stack depth for samples collected. This is currently no-op.
87+
* Default stack depth of 128 will be used. Kept to avoid making breaking change.
8888
*/
8989
export function start(intervalBytes: number, stackDepth: number) {
9090
if (enabled) {
9191
throw new Error(
92-
`Heap profiler is already started with intervalBytes ${heapIntervalBytes} and stackDepth ${stackDepth}`
92+
`Heap profiler is already started with intervalBytes ${heapIntervalBytes} and stackDepth 128`
9393
);
9494
}
9595
heapIntervalBytes = intervalBytes;
96-
heapStackDepth = stackDepth;
97-
startSamplingHeapProfiler(heapIntervalBytes, heapStackDepth);
96+
startSamplingHeapProfiler(heapIntervalBytes, stackDepth);
9897
enabled = true;
9998
}
10099

ts/test/test-heap-profiler.ts

Lines changed: 19 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@
1717
import * as sinon from 'sinon';
1818

1919
import * as heapProfiler from '../src/heap-profiler';
20-
import * as v8HeapProfiler from '../src/heap-profiler-bindings';
20+
import * as inspectorHeapProfiler from '../src/heap-profiler-inspector';
2121
import {AllocationProfileNode} from '../src/v8-types';
2222

2323
import {
@@ -32,14 +32,14 @@ const copy = require('deep-copy');
3232
const assert = require('assert');
3333

3434
describe('HeapProfiler', () => {
35-
let startStub: sinon.SinonStub<[number, number], void>;
36-
let stopStub: sinon.SinonStub<[], void>;
37-
let profileStub: sinon.SinonStub<[], AllocationProfileNode>;
35+
let startStub: sinon.SinonStub<[number, number], Promise<void>>;
36+
let stopStub: sinon.SinonStub<[], Promise<void>>;
37+
let profileStub: sinon.SinonStub<[], Promise<AllocationProfileNode>>;
3838
let dateStub: sinon.SinonStub<[], number>;
3939
let memoryUsageStub: sinon.SinonStub<[], NodeJS.MemoryUsage>;
4040
beforeEach(() => {
41-
startStub = sinon.stub(v8HeapProfiler, 'startSamplingHeapProfiler');
42-
stopStub = sinon.stub(v8HeapProfiler, 'stopSamplingHeapProfiler');
41+
startStub = sinon.stub(inspectorHeapProfiler, 'startSamplingHeapProfiler');
42+
stopStub = sinon.stub(inspectorHeapProfiler, 'stopSamplingHeapProfiler');
4343
dateStub = sinon.stub(Date, 'now').returns(0);
4444
});
4545

@@ -54,7 +54,7 @@ describe('HeapProfiler', () => {
5454
describe('profile', () => {
5555
it('should return a profile equal to the expected profile when external memory is allocated', async () => {
5656
profileStub = sinon
57-
.stub(v8HeapProfiler, 'getAllocationProfile')
57+
.stub(inspectorHeapProfiler, 'getAllocationProfile')
5858
.returns(copy(v8HeapProfile));
5959
memoryUsageStub = sinon.stub(process, 'memoryUsage').returns({
6060
external: 1024,
@@ -66,13 +66,13 @@ describe('HeapProfiler', () => {
6666
const intervalBytes = 1024 * 512;
6767
const stackDepth = 32;
6868
heapProfiler.start(intervalBytes, stackDepth);
69-
const profile = heapProfiler.profile();
69+
const profile = await heapProfiler.profile();
7070
assert.deepEqual(heapProfileWithExternal, profile);
7171
});
7272

7373
it('should return a profile equal to the expected profile when including all samples', async () => {
7474
profileStub = sinon
75-
.stub(v8HeapProfiler, 'getAllocationProfile')
75+
.stub(inspectorHeapProfiler, 'getAllocationProfile')
7676
.returns(copy(v8HeapWithPathProfile));
7777
memoryUsageStub = sinon.stub(process, 'memoryUsage').returns({
7878
external: 0,
@@ -84,13 +84,13 @@ describe('HeapProfiler', () => {
8484
const intervalBytes = 1024 * 512;
8585
const stackDepth = 32;
8686
heapProfiler.start(intervalBytes, stackDepth);
87-
const profile = heapProfiler.profile();
87+
const profile = await heapProfiler.profile();
8888
assert.deepEqual(heapProfileIncludePath, profile);
8989
});
9090

9191
it('should return a profile equal to the expected profile when excluding profiler samples', async () => {
9292
profileStub = sinon
93-
.stub(v8HeapProfiler, 'getAllocationProfile')
93+
.stub(inspectorHeapProfiler, 'getAllocationProfile')
9494
.returns(copy(v8HeapWithPathProfile));
9595
memoryUsageStub = sinon.stub(process, 'memoryUsage').returns({
9696
external: 0,
@@ -102,14 +102,14 @@ describe('HeapProfiler', () => {
102102
const intervalBytes = 1024 * 512;
103103
const stackDepth = 32;
104104
heapProfiler.start(intervalBytes, stackDepth);
105-
const profile = heapProfiler.profile('@google-cloud/profiler');
105+
const profile = await heapProfiler.profile('@google-cloud/profiler');
106106
assert.deepEqual(heapProfileExcludePath, profile);
107107
});
108108

109109
it('should throw error when not started', () => {
110-
assert.throws(
111-
() => {
112-
heapProfiler.profile();
110+
assert.rejects(
111+
async () => {
112+
await heapProfiler.profile();
113113
},
114114
(err: Error) => {
115115
return err.message === 'Heap profiler is not enabled.';
@@ -122,9 +122,9 @@ describe('HeapProfiler', () => {
122122
const stackDepth = 32;
123123
heapProfiler.start(intervalBytes, stackDepth);
124124
heapProfiler.stop();
125-
assert.throws(
126-
() => {
127-
heapProfiler.profile();
125+
assert.rejects(
126+
async () => {
127+
await heapProfiler.profile();
128128
},
129129
(err: Error) => {
130130
return err.message === 'Heap profiler is not enabled.';
@@ -160,7 +160,7 @@ describe('HeapProfiler', () => {
160160
assert.strictEqual(
161161
e.message,
162162
'Heap profiler is already started with intervalBytes 524288 and' +
163-
' stackDepth 64'
163+
' stackDepth 128'
164164
);
165165
}
166166
assert.ok(

0 commit comments

Comments
 (0)