Skip to content

Commit 34b5ffd

Browse files
committed
test: add heap profile labels tests
JS parallel tests: - test-v8-heap-profile-labels: basic labeling, multi-key, JSON round-trip, GC retention/removal, includeCollectedObjects flag, setHeapProfileLabels leak check, CPED identity test (verifies labels survive when another ALS store changes the CPED address) - test-v8-heap-profile-labels-async: await boundary propagation, concurrent contexts, Hapi-style setHeapProfileLabels - test-v8-heap-profile-external: Buffer/ArrayBuffer per-label externalBytes tracking, GC cleanup, unlabeled isolation Signed-off-by: Rudolf Meijering <[email protected]>
1 parent ad489ed commit 34b5ffd

3 files changed

Lines changed: 755 additions & 0 deletions

File tree

Lines changed: 257 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,257 @@
1+
// Flags: --expose-gc
2+
'use strict';
3+
4+
require('../common');
5+
const assert = require('assert');
6+
const v8 = require('v8');
7+
8+
// Helper: find an externalBytes entry whose labels match a predicate.
9+
function findExternal(profile, predicate) {
10+
if (!Array.isArray(profile.externalBytes)) return undefined;
11+
return profile.externalBytes.find(predicate);
12+
}
13+
14+
// Helper: find an externalBytes entry by a single label key-value pair.
15+
function findByLabel(profile, key, value) {
16+
return findExternal(profile, (e) => e.labels[key] === value);
17+
}
18+
19+
// Test 1: Buffer.alloc() inside withHeapProfileLabels is attributed to the
20+
// correct label in externalBytes.
21+
{
22+
v8.startSamplingHeapProfiler(512 * 1024); // 512KB interval (default)
23+
24+
// Allocate 10MB Buffer inside a labeled context.
25+
const buf = v8.withHeapProfileLabels({ route: '/heavy' }, () => {
26+
const b = Buffer.alloc(10 * 1024 * 1024);
27+
// Keep buf alive.
28+
assert.strictEqual(b.length, 10 * 1024 * 1024);
29+
return b;
30+
});
31+
32+
const profile = v8.getAllocationProfile();
33+
assert.ok(profile, 'profile should exist');
34+
35+
assert.ok(Array.isArray(profile.externalBytes),
36+
'externalBytes should be an array (ProfilingArrayBufferAllocator active)');
37+
{
38+
const entry = findByLabel(profile, 'route', '/heavy');
39+
assert.ok(entry, 'Expected entry for route=/heavy in externalBytes');
40+
assert.ok(entry.bytes > 0,
41+
`Expected /heavy external bytes > 0, got ${entry.bytes}`);
42+
// The 10MB Buffer should show up (allow some tolerance for overhead).
43+
assert.ok(entry.bytes >= 9 * 1024 * 1024,
44+
`Expected /heavy >= 9MB, got ${entry.bytes}`);
45+
}
46+
47+
// Keep buf alive until after profile is read.
48+
assert.ok(buf.length > 0);
49+
v8.stopSamplingHeapProfiler();
50+
}
51+
52+
// Test 2: Buffer.alloc() outside any label context is not tracked.
53+
{
54+
v8.startSamplingHeapProfiler(512 * 1024);
55+
56+
// Allocate outside any label context.
57+
const buf = Buffer.alloc(5 * 1024 * 1024);
58+
assert.strictEqual(buf.length, 5 * 1024 * 1024);
59+
60+
const profile = v8.getAllocationProfile();
61+
assert.ok(profile, 'profile should exist');
62+
63+
// externalBytes should be empty or have zero bytes (no labeled allocations).
64+
if (Array.isArray(profile.externalBytes)) {
65+
const totalLabeled = profile.externalBytes
66+
.reduce((a, e) => a + e.bytes, 0);
67+
assert.strictEqual(totalLabeled, 0,
68+
`Expected 0 labeled external bytes, got ${totalLabeled}`);
69+
}
70+
71+
v8.stopSamplingHeapProfiler();
72+
}
73+
74+
// Test 3: After dropping Buffer references and forcing GC, per-label bytes
75+
// decrease (Free is called).
76+
{
77+
v8.startSamplingHeapProfiler(512 * 1024);
78+
79+
let profile;
80+
81+
v8.withHeapProfileLabels({ route: '/gc-test' }, () => {
82+
// Create a Buffer, then let it be GC'd.
83+
let buf = Buffer.alloc(8 * 1024 * 1024);
84+
assert.strictEqual(buf.length, 8 * 1024 * 1024);
85+
86+
profile = v8.getAllocationProfile();
87+
assert.ok(Array.isArray(profile.externalBytes),
88+
'externalBytes should be an array (ProfilingArrayBufferAllocator active)');
89+
{
90+
const entry = findByLabel(profile, 'route', '/gc-test');
91+
if (entry) {
92+
assert.ok(entry.bytes >= 7 * 1024 * 1024,
93+
`Expected /gc-test >= 7MB before GC, got ${entry.bytes}`);
94+
}
95+
}
96+
97+
// Drop reference and force GC.
98+
buf = null;
99+
});
100+
101+
global.gc();
102+
global.gc();
103+
104+
profile = v8.getAllocationProfile();
105+
// After GC, externalBytes may be absent if all labeled allocations were freed.
106+
{
107+
const entry = Array.isArray(profile.externalBytes)
108+
? findByLabel(profile, 'route', '/gc-test') : undefined;
109+
const afterGC = entry ? entry.bytes : 0;
110+
// After GC, the buffer should be freed and the count should decrease.
111+
// It may not go to exactly 0 due to other small allocations.
112+
assert.ok(afterGC < 8 * 1024 * 1024,
113+
`Expected /gc-test < 8MB after GC, got ${afterGC}`);
114+
}
115+
116+
v8.stopSamplingHeapProfiler();
117+
}
118+
119+
// Test 4: Multiple labels — allocate Buffers with different labels, verify
120+
// externalBytes shows correct per-label totals.
121+
{
122+
v8.startSamplingHeapProfiler(512 * 1024);
123+
124+
const bufs = [];
125+
v8.withHeapProfileLabels({ route: '/api/users' }, () => {
126+
bufs.push(Buffer.alloc(4 * 1024 * 1024));
127+
});
128+
129+
v8.withHeapProfileLabels({ route: '/api/orders' }, () => {
130+
bufs.push(Buffer.alloc(6 * 1024 * 1024));
131+
});
132+
133+
const profile = v8.getAllocationProfile();
134+
assert.ok(profile, 'profile should exist');
135+
136+
assert.ok(Array.isArray(profile.externalBytes),
137+
'externalBytes should be an array (ProfilingArrayBufferAllocator active)');
138+
{
139+
const usersEntry = findByLabel(profile, 'route', '/api/users');
140+
const ordersEntry = findByLabel(profile, 'route', '/api/orders');
141+
const usersBytes = usersEntry ? usersEntry.bytes : 0;
142+
const ordersBytes = ordersEntry ? ordersEntry.bytes : 0;
143+
assert.ok(usersBytes >= 3 * 1024 * 1024,
144+
`Expected /api/users >= 3MB, got ${usersBytes}`);
145+
assert.ok(ordersBytes >= 5 * 1024 * 1024,
146+
`Expected /api/orders >= 5MB, got ${ordersBytes}`);
147+
// Orders should have more external memory than users.
148+
assert.ok(ordersBytes > usersBytes,
149+
`Expected /api/orders (${ordersBytes}) > /api/users (${usersBytes})`);
150+
}
151+
152+
// Keep bufs alive.
153+
assert.ok(bufs.length === 2);
154+
v8.stopSamplingHeapProfiler();
155+
}
156+
157+
// Test 5: JSON serialization of the profile includes externalBytes.
158+
{
159+
v8.startSamplingHeapProfiler(512 * 1024);
160+
161+
const buf = v8.withHeapProfileLabels({ route: '/json-test' }, () => {
162+
return Buffer.alloc(2 * 1024 * 1024);
163+
});
164+
165+
const profile = v8.getAllocationProfile();
166+
const json = JSON.stringify(profile);
167+
const parsed = JSON.parse(json);
168+
169+
assert.ok(Array.isArray(parsed.samples), 'samples should be an array');
170+
if (Array.isArray(parsed.externalBytes)) {
171+
const entry = parsed.externalBytes.find(
172+
(e) => e.labels && e.labels.route === '/json-test'
173+
);
174+
assert.ok(entry, 'Expected /json-test in serialized externalBytes');
175+
assert.ok(entry.bytes > 0,
176+
`Expected /json-test bytes > 0, got ${entry.bytes}`);
177+
}
178+
179+
// Keep buf alive.
180+
assert.ok(buf.length > 0);
181+
v8.stopSamplingHeapProfiler();
182+
}
183+
184+
// Test 6: Multi-label context — both key-value pairs appear in externalBytes.
185+
{
186+
v8.startSamplingHeapProfiler(512 * 1024);
187+
188+
const buf = v8.withHeapProfileLabels(
189+
{ route: '/foo', handler: 'bar' }, () => {
190+
return Buffer.alloc(3 * 1024 * 1024);
191+
});
192+
193+
const profile = v8.getAllocationProfile();
194+
assert.ok(profile, 'profile should exist');
195+
196+
assert.ok(Array.isArray(profile.externalBytes),
197+
'externalBytes should be an array (ProfilingArrayBufferAllocator active)');
198+
{
199+
const entry = findExternal(profile,
200+
(e) => e.labels.route === '/foo' && e.labels.handler === 'bar');
201+
assert.ok(entry,
202+
'Expected entry with both route=/foo and handler=bar');
203+
assert.ok(entry.bytes >= 2 * 1024 * 1024,
204+
`Expected multi-label entry >= 2MB, got ${entry.bytes}`);
205+
// Verify both keys are present.
206+
assert.strictEqual(entry.labels.route, '/foo');
207+
assert.strictEqual(entry.labels.handler, 'bar');
208+
}
209+
210+
// Keep buf alive.
211+
assert.ok(buf.length > 0);
212+
v8.stopSamplingHeapProfiler();
213+
}
214+
215+
// Test 7: externalBytes labels match heap sample labels for same context.
216+
{
217+
v8.startSamplingHeapProfiler(64); // Small interval to increase sample hits
218+
219+
const buf = v8.withHeapProfileLabels({ route: '/match-test' }, () => {
220+
// Allocate both heap objects and a Buffer in the same label context.
221+
const arr = [];
222+
for (let i = 0; i < 1000; i++) {
223+
arr.push({ data: new Array(100).fill(i) });
224+
}
225+
const b = Buffer.alloc(5 * 1024 * 1024);
226+
// Keep arr alive.
227+
assert.ok(arr.length > 0);
228+
return b;
229+
});
230+
231+
const profile = v8.getAllocationProfile();
232+
233+
// Find heap samples with matching labels.
234+
const labeledSamples = profile.samples.filter(
235+
(s) => s.labels && s.labels.route === '/match-test'
236+
);
237+
238+
// Find externalBytes entry with matching labels.
239+
assert.ok(Array.isArray(profile.externalBytes),
240+
'externalBytes should be an array (ProfilingArrayBufferAllocator active)');
241+
{
242+
const extEntry = findByLabel(profile, 'route', '/match-test');
243+
if (extEntry && labeledSamples.length > 0) {
244+
// The label keys should match between heap samples and externalBytes.
245+
const sampleLabelKeys = Object.keys(labeledSamples[0].labels).sort();
246+
const extLabelKeys = Object.keys(extEntry.labels).sort();
247+
assert.deepStrictEqual(extLabelKeys, sampleLabelKeys,
248+
'externalBytes label keys should match heap sample label keys');
249+
}
250+
}
251+
252+
// Keep buf alive.
253+
assert.ok(buf.length > 0);
254+
v8.stopSamplingHeapProfiler();
255+
}
256+
257+
console.log('All external memory tracking tests passed.');

0 commit comments

Comments
 (0)