scripts/s2-b-bench.mjs
Rama master · réplica de lectura del repositorio original.
import assert from 'node:assert/strict';
import { performance } from 'node:perf_hooks';
import { gunzipSync } from 'node:zlib';
import os from 'node:os';
import { StreamingFrameParser } from '../src/connect.js';
const MAX_FRAME_SIZE = 16 * 1024 * 1024;
// Frozen 014204f parser, not a reimplementation of its benchmark result.
class BeforeParser {
constructor() {
this.buffer = Buffer.alloc(0);
}
push(chunk) {
this.buffer = Buffer.concat([this.buffer, chunk]);
}
/** Drain all complete frames. Returns [{ flags, isEndStream, payload }]. */
drain() {
// Guard against malformed upstream frames that advertise absurd lengths —
// without this, Buffer.concat() will happily try to allocate gigabytes.
const frames = [];
while (this.buffer.length >= 5) {
const len = this.buffer.readUInt32BE(1);
if (len > MAX_FRAME_SIZE) {
throw new Error(`HTTP/2 frame size ${len} exceeds ${MAX_FRAME_SIZE}`);
}
if (this.buffer.length < 5 + len) break;
const flags = this.buffer[0];
let payload = this.buffer.subarray(5, 5 + len);
if (flags & 0x01) {
// Bound the DECOMPRESSED size too: a high-ratio gzip frame that passes the
// ≤16MB wire-length check above can still inflate to gigabytes. Without
// maxOutputLength, gunzipSync accumulates up to kMaxLength (~2GB) and on a
// memory-constrained host the OOM-killer can reap the process before the
// throw below is caught (audit CONN-1).
try { payload = gunzipSync(payload, { maxOutputLength: MAX_FRAME_SIZE }); }
catch (err) {
// Don't silently drop a frame whose body we couldn't decompress —
// upstream is sending something we don't understand and the caller
// needs to know so the stream can be cancelled rather than continued
// with missing data.
throw new Error(`Connect frame decompression failed: ${err.message}`);
}
}
frames.push({
flags,
isEndStream: !!(flags & 0x02),
payload,
});
this.buffer = this.buffer.subarray(5 + len);
}
return frames;
}
}
const samples = Number(process.argv[2] || 3);
assert.ok(Number.isInteger(samples) && samples > 0, 'Pass a positive sample count');
console.log(JSON.stringify({ node: process.version, v8: process.versions.v8, platform: process.platform,
arch: process.arch, cpu: os.cpus()[0]?.model, samples, gc_exposed: !!global.gc }));
let sink = 0;
for (const mib of [1, 2, 4]) {
// Includes the five-byte header: the 4 MiB case is exactly 256 x 16 KiB chunks.
const frame = Buffer.alloc(mib * 1024 * 1024, 0x61);
frame[0] = 0; frame.writeUInt32BE(frame.length - 5, 1);
for (const chunkBytes of [frame.length, 16 * 1024]) {
const chunks = [];
for (let i = 0; i < frame.length; i += chunkBytes) chunks.push(frame.subarray(i, i + chunkBytes));
for (const [implementation, Parser] of [['before', BeforeParser], ['candidate', StreamingFrameParser]]) {
const validate = new Parser(); let decoded = [];
for (const chunk of chunks) { validate.push(chunk); decoded.push(...validate.drain()); }
assert.equal(decoded.length, 1); assert.deepEqual(decoded[0].payload, frame.subarray(5));
const once = () => {
const parser = new Parser(); let count = 0, length = 0;
for (const chunk of chunks) {
parser.push(chunk);
for (const out of parser.drain()) { count++; length += out.payload.length; sink ^= out.payload[0]; }
}
if (count !== 1 || length !== frame.length - 5) throw new Error('Benchmark parser output changed');
};
once();
const samples_ms = [];
for (let i = 0; i < samples; i++) {
global.gc?.(); const start = performance.now(); once(); samples_ms.push(performance.now() - start);
}
const sorted = [...samples_ms].sort((a, b) => a - b);
console.log(JSON.stringify({ implementation, frame_bytes: frame.length, chunk_bytes: chunkBytes,
chunks: chunks.length, samples_ms, median_ms: sorted[Math.floor(sorted.length / 2)] }));
}
}
}
console.log(JSON.stringify({ sink }));