|
| 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 | +} |
0 commit comments