Files
node/tools/pgo/pgo-compression.js

418 lines
12 KiB
JavaScript

'use strict';
// PGO Training Script: Compression (zlib/brotli)
//
// HTTP response compression is used on virtually every production Node.js server.
// This script exercises:
// - gzip compression/decompression (the most common HTTP content-encoding)
// - deflate compression/decompression
// - brotli compression/decompression (modern, better ratio)
// - Streaming compression (piped through HTTP responses)
// - One-shot compression (in-memory buffers)
// - Various compression levels and data types
// - CRC-32 computation
//
// This exercises: zlib C library, brotli C library, libuv thread pool
// (async compression), stream infrastructure, Buffer allocation.
const zlib = require('zlib');
const crypto = require('crypto');
const { pipeline } = require('stream/promises');
const { Readable, Writable } = require('stream');
const DURATION_MS = parseInt(process.env.PGO_TRAINING_DURATION, 10) || 15_000;
// Test data of different types and sizes (compression ratio varies)
const JSON_DATA = Buffer.from(
JSON.stringify({
users: Array.from({ length: 200 }, (_, i) => ({
id: i,
name: `User ${i}`,
email: `user${i}@example.com`,
role: ['admin', 'editor', 'viewer'][i % 3],
active: i % 4 !== 0,
profile: {
bio: `This is the biography for user ${i}. It contains some repetitive text to demonstrate compression.`,
location: ['New York', 'London', 'Tokyo', 'Berlin', 'Sydney'][i % 5],
joinDate: `2024-${String((i % 12) + 1).padStart(2, '0')}-${String((i % 28) + 1).padStart(2, '0')}`,
},
})),
}),
);
const HTML_DATA = Buffer.from(`<!DOCTYPE html>
<html lang="en">
<head><meta charset="UTF-8"><title>Test Page</title></head>
<body>
${Array.from(
{ length: 100 },
(_, i) => `
<div class="card" id="card-${i}">
<h2 class="card-title">Card Title ${i}</h2>
<p class="card-body">This is the body text for card number ${i}. It contains enough text to be meaningful for compression benchmarks.</p>
<div class="card-footer">
<button class="btn btn-primary">Action ${i}</button>
<span class="badge">${i % 5}</span>
</div>
</div>`,
).join('\n')}
</body></html>`);
const CSS_DATA = Buffer.from(
Array.from(
{ length: 200 },
(_, i) => `
.component-${i} { display: flex; align-items: center; padding: ${i}px; margin: ${i % 20}px; }
.component-${i}:hover { background-color: #${String(i * 111)
.padStart(6, '0')
.slice(0, 6)}; transition: all 0.3s ease; }
.component-${i} .title { font-size: ${12 + (i % 8)}px; font-weight: ${i % 2 === 0 ? 'bold' : 'normal'}; }
.component-${i} .description { color: #666; line-height: 1.5; max-width: ${200 + i * 5}px; }
`,
).join('\n'),
);
const JS_DATA = Buffer.from(
Array.from(
{ length: 100 },
(_, i) => `
function handler${i}(req, res) {
const data = req.body;
if (!data || !data.id) {
return res.status(400).json({ error: 'Missing id' });
}
const result = processData${i}(data);
return res.json({ success: true, data: result, timestamp: Date.now() });
}
function processData${i}(data) {
return { ...data, processed: true, handler: 'handler${i}' };
}
module.exports = { handler${i}, processData${i} };
`,
).join('\n'),
);
// Binary data (images, wasm — less compressible)
const BINARY_DATA = crypto.randomBytes(32768);
const TEST_DATA = [
{ name: 'JSON', data: JSON_DATA, weight: 5 },
{ name: 'HTML', data: HTML_DATA, weight: 3 },
{ name: 'CSS', data: CSS_DATA, weight: 2 },
{ name: 'JS', data: JS_DATA, weight: 2 },
{ name: 'Binary', data: BINARY_DATA, weight: 1 },
];
const weightedData = [];
for (const item of TEST_DATA) {
for (let i = 0; i < item.weight; i++) {
weightedData.push(item);
}
}
// Workload 1: Gzip compress/decompress (one-shot, most common pattern)
async function workloadGzip(iterations) {
let ops = 0;
for (let i = 0; i < iterations; i++) {
const item = weightedData[i % weightedData.length];
// Compress (HTTP response compression)
const compressed = await new Promise((resolve, reject) => {
zlib.gzip(item.data, (err, result) => {
if (err) reject(err);
else resolve(result);
});
});
ops++;
// Decompress (HTTP response decompression on client)
await new Promise((resolve, reject) => {
zlib.gunzip(compressed, (err, result) => {
if (err) reject(err);
else resolve(result);
});
});
ops++;
// Sync variant (used in some build tools)
if (i % 5 === 0) {
const compSync = zlib.gzipSync(item.data);
zlib.gunzipSync(compSync);
ops += 2;
}
// Different compression levels
if (i % 3 === 0) {
await new Promise((resolve, reject) => {
zlib.gzip(item.data, { level: 1 }, (err, result) => {
// Fast
if (err) reject(err);
else resolve(result);
});
});
ops++;
}
if (i % 7 === 0) {
await new Promise((resolve, reject) => {
zlib.gzip(item.data, { level: 9 }, (err, result) => {
// Best
if (err) reject(err);
else resolve(result);
});
});
ops++;
}
}
return ops;
}
// Workload 2: Deflate compress/decompress
async function workloadDeflate(iterations) {
let ops = 0;
for (let i = 0; i < iterations; i++) {
const item = weightedData[i % weightedData.length];
// deflate (used in some HTTP implementations)
const compressed = await new Promise((resolve, reject) => {
zlib.deflate(item.data, (err, result) => {
if (err) reject(err);
else resolve(result);
});
});
ops++;
await new Promise((resolve, reject) => {
zlib.inflate(compressed, (err, result) => {
if (err) reject(err);
else resolve(result);
});
});
ops++;
// deflateRaw (no header — used in some protocols)
if (i % 3 === 0) {
const raw = zlib.deflateRawSync(item.data);
zlib.inflateRawSync(raw);
ops += 2;
}
}
return ops;
}
// Workload 3: Brotli compress/decompress (modern HTTP content-encoding)
async function workloadBrotli(iterations) {
let ops = 0;
for (let i = 0; i < iterations; i++) {
const item = weightedData[i % weightedData.length];
// Brotli compress (response compression for supported clients)
const compressed = await new Promise((resolve, reject) => {
zlib.brotliCompress(item.data, (err, result) => {
if (err) reject(err);
else resolve(result);
});
});
ops++;
// Brotli decompress
await new Promise((resolve, reject) => {
zlib.brotliDecompress(compressed, (err, result) => {
if (err) reject(err);
else resolve(result);
});
});
ops++;
// Different quality levels
if (i % 3 === 0) {
await new Promise((resolve, reject) => {
zlib.brotliCompress(
item.data,
{
params: { [zlib.constants.BROTLI_PARAM_QUALITY]: 1 }, // Fast
},
(err, result) => {
if (err) reject(err);
else resolve(result);
},
);
});
ops++;
}
}
return ops;
}
// Workload 4: Streaming compression (piped HTTP responses)
async function workloadStreamCompress(iterations) {
let ops = 0;
for (let i = 0; i < iterations; i++) {
const item = weightedData[i % weightedData.length];
// Gzip stream (Express compression middleware pattern)
await pipeline(
Readable.from([item.data]),
zlib.createGzip(),
new Writable({
write(chunk, encoding, callback) {
callback();
},
}),
);
ops++;
// Brotli stream
if (i % 2 === 0) {
await pipeline(
Readable.from([item.data]),
zlib.createBrotliCompress(),
new Writable({
write(chunk, encoding, callback) {
callback();
},
}),
);
ops++;
}
// Chunked stream compression (large response simulation)
if (i % 3 === 0) {
const chunkSize = 4096;
const chunks = [];
for (let offset = 0; offset < item.data.length; offset += chunkSize) {
chunks.push(
item.data.subarray(
offset,
Math.min(offset + chunkSize, item.data.length),
),
);
}
await pipeline(
Readable.from(chunks),
zlib.createGzip({ flush: zlib.constants.Z_SYNC_FLUSH }),
new Writable({
write(chunk, encoding, callback) {
callback();
},
}),
);
ops++;
}
}
return ops;
}
// Workload 5: CRC-32 (used in gzip, PNG, etc.)
function workloadCRC32(iterations) {
let ops = 0;
for (let i = 0; i < iterations; i++) {
const item = weightedData[i % weightedData.length];
zlib.crc32(item.data);
ops++;
// Incremental CRC (streaming pattern)
if (i % 3 === 0) {
const chunkSize = 1024;
let crc = 0;
for (let offset = 0; offset < item.data.length; offset += chunkSize) {
const chunk = item.data.subarray(
offset,
Math.min(offset + chunkSize, item.data.length),
);
crc = zlib.crc32(chunk, crc);
}
ops++;
}
}
return ops;
}
// Workload 6: Compression object creation (middleware initialization)
function workloadCompressionCreation(iterations) {
let ops = 0;
for (let i = 0; i < iterations; i++) {
// Express compression middleware creates these per-request
zlib.createGzip();
zlib.createGunzip();
zlib.createDeflate();
zlib.createInflate();
zlib.createBrotliCompress();
zlib.createBrotliDecompress();
ops += 6;
// With options (common in production)
zlib.createGzip({ level: 6, memLevel: 8, windowBits: 15 });
zlib.createBrotliCompress({
params: {
[zlib.constants.BROTLI_PARAM_QUALITY]: 4,
[zlib.constants.BROTLI_PARAM_MODE]: zlib.constants.BROTLI_MODE_TEXT,
},
});
ops += 2;
}
return ops;
}
async function main() {
console.log('[pgo-compression] Starting compression workload...');
const startTime = Date.now();
let totalOps = 0;
let round = 0;
const remaining = () => DURATION_MS - (Date.now() - startTime);
while (remaining() > 0) {
round++;
const scale = Math.max(0.1, remaining() / DURATION_MS);
const iterScale = (base) => Math.max(1, Math.floor(base * scale));
// Gzip (most common — Accept-Encoding: gzip is universal)
if (round === 1) console.log('[pgo-compression] Running gzip...');
totalOps += await workloadGzip(iterScale(100));
if (remaining() <= 0) break;
// Deflate
if (round === 1) console.log('[pgo-compression] Running deflate...');
totalOps += await workloadDeflate(iterScale(50));
if (remaining() <= 0) break;
// Brotli (growing rapidly)
if (round === 1) console.log('[pgo-compression] Running brotli...');
totalOps += await workloadBrotli(iterScale(50));
if (remaining() <= 0) break;
// Streaming compression
if (round === 1)
console.log('[pgo-compression] Running stream compression...');
totalOps += await workloadStreamCompress(iterScale(30));
if (remaining() <= 0) break;
// CRC-32
if (round === 1) console.log('[pgo-compression] Running CRC-32...');
totalOps += workloadCRC32(iterScale(1000));
if (remaining() <= 0) break;
// Object creation
if (round === 1)
console.log('[pgo-compression] Running compression object creation...');
totalOps += workloadCompressionCreation(iterScale(500));
}
const elapsed = (Date.now() - startTime) / 1000;
console.log(
`[pgo-compression] Completed ${totalOps} ops in ${elapsed.toFixed(1)}s (${(totalOps / elapsed).toFixed(0)} ops/s) [${round} rounds]`,
);
}
main().catch((err) => {
console.error('[pgo-compression] Error:', err);
process.exit(1);
});