Skip to content

Commit 8743634

Browse files
committed
benchmark: add heap profiler labels overhead benchmarks
Three benchmarks: - v8/heap-profiler-labels: micro benchmark (1M allocations) - http/heap-profiler-labels: single-server with ~150KB mixed workload - http/heap-profiler-realistic: two-server (app + DB) with JSON parse, column aggregation, and Buffer allocation per request Statistical results (20 runs, 1000 rows, Welch t-test): none -> sampling: ~1% overhead sampling -> labels: ~1% overhead none -> labels (total): ~2% overhead Signed-off-by: Rudolf Meijering <skaapgif@gmail.com>
1 parent 15cedaf commit 8743634

File tree

3 files changed

+297
-0
lines changed

3 files changed

+297
-0
lines changed
Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
'use strict';
2+
3+
// Benchmark: HTTP server throughput impact of heap profiler with labels.
4+
//
5+
// Measures requests/sec across three modes:
6+
// - none: no profiler (baseline)
7+
// - sampling: profiler active, no labels
8+
// - sampling-with-labels: profiler active with labels via withHeapProfileLabels
9+
//
10+
// Workload per request: ~100KB V8 heap (JSON parse/stringify) + ~50KB Buffer
11+
// to exercise both HeapProfileLabelsCallback and ProfilingArrayBufferAllocator.
12+
//
13+
// Run with compare.js:
14+
// node benchmark/compare.js --old ./out/Release/node --new ./out/Release/node \
15+
// --runs 10 --filter heap-profiler-labels --set c=50 -- http
16+
17+
const common = require('../common.js');
18+
const { PORT } = require('../_http-benchmarkers.js');
19+
const v8 = require('v8');
20+
21+
const bench = common.createBenchmark(main, {
22+
mode: ['none', 'sampling', 'sampling-with-labels'],
23+
c: [50],
24+
duration: 10,
25+
});
26+
27+
// Build a ~100KB realistic JSON payload template (API response shape).
28+
const items = [];
29+
for (let i = 0; i < 200; i++) {
30+
items.push({
31+
id: i,
32+
name: `user-${i}`,
33+
email: `user${i}@example.com`,
34+
role: 'admin',
35+
metadata: { created: '2024-01-01', tags: ['a', 'b', 'c'] },
36+
});
37+
}
38+
const payloadTemplate = JSON.stringify({ data: items, total: 200 });
39+
40+
function main({ mode, c, duration }) {
41+
const http = require('http');
42+
43+
const interval = 512 * 1024; // 512KB — V8 default, production-realistic.
44+
45+
if (mode !== 'none') {
46+
v8.startSamplingHeapProfiler(interval);
47+
}
48+
49+
const server = http.createServer((req, res) => {
50+
const handler = () => {
51+
// Realistic mixed workload:
52+
// 1. ~100KB V8 heap: JSON parse + stringify (simulates API response building)
53+
const parsed = JSON.parse(payloadTemplate);
54+
parsed.requestId = Math.random();
55+
const body = JSON.stringify(parsed);
56+
57+
// 2. ~50KB Buffer (simulates response buffering / crypto / compression)
58+
const buf = Buffer.alloc(50 * 1024, 0x42);
59+
60+
// Keep buf reference alive until response is sent.
61+
res.writeHead(200, {
62+
'Content-Type': 'application/json',
63+
'Content-Length': body.length,
64+
'X-Buf-Check': buf[0],
65+
});
66+
res.end(body);
67+
};
68+
69+
if (mode === 'sampling-with-labels') {
70+
v8.withHeapProfileLabels({ route: req.url }, handler);
71+
} else {
72+
handler();
73+
}
74+
});
75+
76+
server.listen(PORT, () => {
77+
bench.http({
78+
path: '/api/bench',
79+
connections: c,
80+
duration,
81+
}, () => {
82+
if (mode !== 'none') {
83+
v8.stopSamplingHeapProfiler();
84+
}
85+
server.close();
86+
});
87+
});
88+
}
Lines changed: 150 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,150 @@
1+
'use strict';
2+
3+
// Benchmark: realistic app-server + DB-server heap profiler overhead.
4+
//
5+
// Architecture: wrk → [App Server :PORT] → [DB Server :PORT+1]
6+
//
7+
// The app server fetches JSON rows from the DB server, parses,
8+
// sums two columns over all rows, and returns the result. This exercises:
9+
// - http.get (async I/O + Buffer allocation for response body)
10+
// - JSON.parse of realistic DB response (V8 heap allocation)
11+
// - Two iteration passes over rows (intermediate values)
12+
// - ALS label propagation across async I/O boundary
13+
//
14+
// Run with compare.js for statistical significance:
15+
// node benchmark/compare.js --old ./out/Release/node --new ./out/Release/node \
16+
// --runs 30 --filter heap-profiler-realistic --set rows=1000 -- http
17+
18+
const common = require('../common.js');
19+
const { PORT } = require('../_http-benchmarkers.js');
20+
const v8 = require('v8');
21+
const http = require('http');
22+
23+
const DB_PORT = PORT + 1;
24+
25+
const bench = common.createBenchmark(main, {
26+
mode: ['none', 'sampling', 'sampling-with-labels'],
27+
rows: [100, 1000],
28+
c: [50],
29+
duration: 10,
30+
});
31+
32+
// --- DB Server: pre-built JSON responses keyed by row count ---
33+
34+
function buildDBResponse(n) {
35+
const categories = ['electronics', 'clothing', 'food', 'books', 'tools'];
36+
const rows = [];
37+
for (let i = 0; i < n; i++) {
38+
rows.push({
39+
id: i,
40+
amount: Math.round(Math.random() * 10000) / 100,
41+
quantity: Math.floor(Math.random() * 500),
42+
name: `user-${String(i).padStart(6, '0')}`,
43+
email: `user${i}@example.com`,
44+
category: categories[i % categories.length],
45+
});
46+
}
47+
const body = JSON.stringify({ rows, total: n });
48+
return { body, len: Buffer.byteLength(body) };
49+
}
50+
51+
// --- App Server helpers ---
52+
53+
function fetchFromDB(rows) {
54+
return new Promise((resolve, reject) => {
55+
const req = http.get(
56+
`http://127.0.0.1:${DB_PORT}/?rows=${rows}`,
57+
(res) => {
58+
const chunks = [];
59+
res.on('data', (chunk) => chunks.push(chunk));
60+
res.on('end', () => {
61+
try {
62+
resolve(JSON.parse(Buffer.concat(chunks).toString()));
63+
} catch (e) {
64+
reject(e);
65+
}
66+
});
67+
},
68+
);
69+
req.on('error', reject);
70+
});
71+
}
72+
73+
function processRows(data) {
74+
const { rows } = data;
75+
// Two passes — simulates light business logic (column aggregation).
76+
let totalAmount = 0;
77+
for (let i = 0; i < rows.length; i++) {
78+
totalAmount += rows[i].amount;
79+
}
80+
let totalQuantity = 0;
81+
for (let i = 0; i < rows.length; i++) {
82+
totalQuantity += rows[i].quantity;
83+
}
84+
return {
85+
totalAmount: Math.round(totalAmount * 100) / 100,
86+
totalQuantity,
87+
count: rows.length,
88+
};
89+
}
90+
91+
function main({ mode, rows, c, duration }) {
92+
// Pre-build DB responses.
93+
const dbResponses = {};
94+
for (const n of [100, 1000]) {
95+
dbResponses[n] = buildDBResponse(n);
96+
}
97+
98+
// Start DB server.
99+
const dbServer = http.createServer((req, res) => {
100+
const url = new URL(req.url, `http://127.0.0.1:${DB_PORT}`);
101+
const n = parseInt(url.searchParams.get('rows') || '1000', 10);
102+
const resp = dbResponses[n] || dbResponses[1000];
103+
res.writeHead(200, {
104+
'Content-Type': 'application/json',
105+
'Content-Length': resp.len,
106+
});
107+
res.end(resp.body);
108+
});
109+
110+
dbServer.listen(DB_PORT, () => {
111+
const interval = 512 * 1024;
112+
if (mode !== 'none') {
113+
v8.startSamplingHeapProfiler(interval);
114+
}
115+
116+
// Start app server.
117+
const appServer = http.createServer((req, res) => {
118+
const handler = async () => {
119+
const data = await fetchFromDB(rows);
120+
const result = processRows(data);
121+
const body = JSON.stringify(result);
122+
res.writeHead(200, {
123+
'Content-Type': 'application/json',
124+
'Content-Length': Buffer.byteLength(body),
125+
});
126+
res.end(body);
127+
};
128+
129+
if (mode === 'sampling-with-labels') {
130+
v8.withHeapProfileLabels({ route: req.url }, handler);
131+
} else {
132+
handler();
133+
}
134+
});
135+
136+
appServer.listen(PORT, () => {
137+
bench.http({
138+
path: '/api/data',
139+
connections: c,
140+
duration,
141+
}, () => {
142+
if (mode !== 'none') {
143+
v8.stopSamplingHeapProfiler();
144+
}
145+
appServer.close();
146+
dbServer.close();
147+
});
148+
});
149+
});
150+
}
Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
'use strict';
2+
3+
// Benchmark: overhead of V8 sampling heap profiler with and without labels.
4+
//
5+
// Measures per-allocation cost across three modes:
6+
// - none: no profiler running (baseline)
7+
// - sampling: profiler active, no labels callback
8+
// - sampling-with-labels: profiler active with labels via withHeapProfileLabels
9+
//
10+
// Run standalone:
11+
// node benchmark/v8/heap-profiler-labels.js
12+
//
13+
// Run with compare.js for statistical analysis:
14+
// node benchmark/compare.js --old ./node-baseline --new ./node-with-labels \
15+
// --filter heap-profiler-labels
16+
17+
const common = require('../common.js');
18+
const v8 = require('v8');
19+
20+
const bench = common.createBenchmark(main, {
21+
mode: ['none', 'sampling', 'sampling-with-labels'],
22+
n: [1e6],
23+
});
24+
25+
function main({ mode, n }) {
26+
const interval = 512 * 1024; // 512KB — V8 default, production-realistic.
27+
28+
if (mode === 'sampling') {
29+
v8.startSamplingHeapProfiler(interval);
30+
} else if (mode === 'sampling-with-labels') {
31+
v8.startSamplingHeapProfiler(interval);
32+
}
33+
34+
if (mode === 'sampling-with-labels') {
35+
v8.withHeapProfileLabels({ route: '/bench' }, () => {
36+
runWorkload(n);
37+
});
38+
} else {
39+
runWorkload(n);
40+
}
41+
42+
if (mode !== 'none') {
43+
v8.stopSamplingHeapProfiler();
44+
}
45+
}
46+
47+
function runWorkload(n) {
48+
const arr = [];
49+
bench.start();
50+
for (let i = 0; i < n; i++) {
51+
// Allocate objects with string properties — representative of JSON API
52+
// workloads. Each object is ~100-200 bytes on the V8 heap.
53+
arr.push({ id: i, name: `item-${i}`, value: Math.random() });
54+
// Prevent unbounded growth — keep last 1000 to maintain GC pressure
55+
// without running out of memory.
56+
if (arr.length > 1000) arr.shift();
57+
}
58+
bench.end(n);
59+
}

0 commit comments

Comments
 (0)