mirror of
https://github.com/LukeHagar/wasm-overhead-research.git
synced 2025-12-06 04:22:06 +00:00
219 lines
7.5 KiB
JavaScript
219 lines
7.5 KiB
JavaScript
#!/usr/bin/env node
|
|
|
|
import fs from 'fs';
|
|
import path from 'path';
|
|
import { execSync } from 'child_process';
|
|
import { performance } from 'perf_hooks';
|
|
import { fileURLToPath } from 'url';
|
|
import zlib from 'zlib';
|
|
|
|
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
|
|
// Test data
|
|
const testData = {
|
|
tiny: JSON.stringify({ name: 'test', value: 42 }), // ~30 bytes
|
|
small: JSON.stringify({ users: Array(10).fill(null).map((_, i) => ({ id: i, name: `User${i}` })) }), // ~300 bytes
|
|
medium: JSON.stringify({ records: Array(100).fill(null).map((_, i) => ({ id: i, data: `Data${i}`.repeat(10) })) }), // ~5KB
|
|
large: JSON.stringify({ items: Array(1000).fill(null).map((_, i) => ({ id: i, payload: `Payload${i}`.repeat(50) })) }) // ~500KB
|
|
};
|
|
|
|
// Calculate data sizes
|
|
const dataSizes = {};
|
|
for (const [key, value] of Object.entries(testData)) {
|
|
dataSizes[key] = new TextEncoder().encode(value).length;
|
|
}
|
|
|
|
async function measureWasmSize(wasmPath) {
|
|
if (!fs.existsSync(wasmPath)) {
|
|
return { raw: 0, gzipped: 0 };
|
|
}
|
|
|
|
const rawSize = fs.statSync(wasmPath).size;
|
|
const wasmBuffer = fs.readFileSync(wasmPath);
|
|
const gzipped = zlib.gzipSync(wasmBuffer, { level: 9 });
|
|
|
|
return {
|
|
raw: rawSize,
|
|
gzipped: gzipped.length
|
|
};
|
|
}
|
|
|
|
async function benchmarkImplementation(name, wasmPath, iterations = 10) {
|
|
console.log(`\nBenchmarking ${name}...`);
|
|
|
|
const results = {
|
|
name,
|
|
sizes: await measureWasmSize(wasmPath),
|
|
coldStart: [],
|
|
warmExecution: {},
|
|
throughput: {},
|
|
memory: {}
|
|
};
|
|
|
|
// Measure cold start (WebAssembly compilation)
|
|
for (let i = 0; i < 5; i++) {
|
|
const start = performance.now();
|
|
try {
|
|
const wasmBuffer = fs.readFileSync(wasmPath);
|
|
await WebAssembly.compile(wasmBuffer);
|
|
const elapsed = performance.now() - start;
|
|
results.coldStart.push(elapsed);
|
|
} catch (error) {
|
|
console.error(` Cold start error: ${error.message}`);
|
|
}
|
|
}
|
|
|
|
// Load module once for warm benchmarks
|
|
let module, instance;
|
|
try {
|
|
const wasmBuffer = fs.readFileSync(wasmPath);
|
|
module = await WebAssembly.compile(wasmBuffer);
|
|
|
|
// Try to instantiate with minimal imports
|
|
const imports = {
|
|
env: { memory: new WebAssembly.Memory({ initial: 1 }) },
|
|
wasi_snapshot_preview1: {}
|
|
};
|
|
instance = await WebAssembly.instantiate(module, imports);
|
|
} catch (error) {
|
|
console.log(` Instantiation requires specific runtime: ${error.message}`);
|
|
}
|
|
|
|
// For each data size, measure execution time (if we can instantiate)
|
|
if (instance) {
|
|
for (const [size, data] of Object.entries(testData)) {
|
|
results.warmExecution[size] = [];
|
|
|
|
for (let i = 0; i < iterations; i++) {
|
|
const start = performance.now();
|
|
// Try to find and call the transform function
|
|
try {
|
|
if (instance.exports.transform) {
|
|
instance.exports.transform(data);
|
|
}
|
|
} catch (error) {
|
|
// Expected for most implementations without proper setup
|
|
}
|
|
const elapsed = performance.now() - start;
|
|
results.warmExecution[size].push(elapsed);
|
|
}
|
|
|
|
// Calculate throughput
|
|
const avgTime = results.warmExecution[size].reduce((a, b) => a + b, 0) / results.warmExecution[size].length;
|
|
results.throughput[size] = dataSizes[size] / (avgTime / 1000) / (1024 * 1024); // MB/s
|
|
}
|
|
}
|
|
|
|
// Memory usage (baseline)
|
|
const memUsage = process.memoryUsage();
|
|
results.memory = {
|
|
heapUsed: Math.round(memUsage.heapUsed / 1024 / 1024 * 10) / 10,
|
|
external: Math.round(memUsage.external / 1024 / 1024 * 10) / 10
|
|
};
|
|
|
|
return results;
|
|
}
|
|
|
|
async function main() {
|
|
console.log('JavaScript to WebAssembly Benchmark');
|
|
console.log('====================================');
|
|
console.log('\nData sizes:');
|
|
for (const [key, size] of Object.entries(dataSizes)) {
|
|
console.log(` ${key}: ${size} bytes (${(size/1024).toFixed(2)} KB)`);
|
|
}
|
|
|
|
const implementations = [
|
|
{ name: 'AssemblyScript', path: 'implementations/assemblyscript/build/release.wasm' },
|
|
{ name: 'QuickJS', path: 'implementations/quickjs/target/wasm32-wasip1/release/quickjs_transform.wasm' },
|
|
{ name: 'Porffor', path: 'implementations/porffor/transform.wasm' },
|
|
];
|
|
|
|
// Check for Go/TinyGo builds
|
|
if (fs.existsSync('assets/wasm/lib.wasm.gz')) {
|
|
// Decompress first
|
|
try {
|
|
const gzipped = fs.readFileSync('assets/wasm/lib.wasm.gz');
|
|
const decompressed = zlib.gunzipSync(gzipped);
|
|
fs.writeFileSync('assets/wasm/lib.wasm', decompressed);
|
|
|
|
// Try to determine which implementation it is
|
|
const stats = fs.statSync('assets/wasm/lib.wasm');
|
|
if (stats.size < 500000) {
|
|
implementations.push({ name: 'TinyGo', path: 'assets/wasm/lib.wasm' });
|
|
} else if (stats.size < 5000000) {
|
|
implementations.push({ name: 'Go', path: 'assets/wasm/lib.wasm' });
|
|
} else {
|
|
implementations.push({ name: 'Go+Goja', path: 'assets/wasm/lib.wasm' });
|
|
}
|
|
} catch (error) {
|
|
console.error('Error decompressing Go WASM:', error.message);
|
|
}
|
|
}
|
|
|
|
const allResults = [];
|
|
|
|
for (const impl of implementations) {
|
|
if (fs.existsSync(impl.path)) {
|
|
const result = await benchmarkImplementation(impl.name, impl.path);
|
|
allResults.push(result);
|
|
|
|
// Print summary
|
|
console.log(` Size: ${(result.sizes.raw/1024).toFixed(1)}KB raw, ${(result.sizes.gzipped/1024).toFixed(1)}KB gzipped`);
|
|
if (result.coldStart.length > 0) {
|
|
const avgCold = result.coldStart.reduce((a, b) => a + b, 0) / result.coldStart.length;
|
|
console.log(` Cold start: ${avgCold.toFixed(2)}ms`);
|
|
}
|
|
console.log(` Memory: ${result.memory.heapUsed}MB heap`);
|
|
} else {
|
|
console.log(`\n${impl.name}: Not built (${impl.path} not found)`);
|
|
}
|
|
}
|
|
|
|
// Write CSV results
|
|
console.log('\nWriting results to CSV...');
|
|
|
|
const csvHeader = 'Implementation,Raw Size (KB),Gzipped (KB),Compression %,Cold Start (ms),Memory (MB)';
|
|
const csvRows = [csvHeader];
|
|
|
|
for (const result of allResults) {
|
|
const avgCold = result.coldStart.length > 0
|
|
? (result.coldStart.reduce((a, b) => a + b, 0) / result.coldStart.length).toFixed(2)
|
|
: 'N/A';
|
|
|
|
const compression = result.sizes.raw > 0
|
|
? ((1 - result.sizes.gzipped / result.sizes.raw) * 100).toFixed(1)
|
|
: '0';
|
|
|
|
csvRows.push([
|
|
result.name,
|
|
(result.sizes.raw / 1024).toFixed(1),
|
|
(result.sizes.gzipped / 1024).toFixed(1),
|
|
compression,
|
|
avgCold,
|
|
result.memory.heapUsed
|
|
].join(','));
|
|
}
|
|
|
|
const csvContent = csvRows.join('\n');
|
|
fs.writeFileSync(path.join(__dirname, 'results', 'benchmark-summary.csv'), csvContent);
|
|
|
|
// Create detailed size comparison
|
|
console.log('\n=== SIZE COMPARISON ===');
|
|
console.log('Implementation | Raw WASM | Gzipped | Compression | vs Smallest');
|
|
console.log('---------------|----------|---------|-------------|------------');
|
|
|
|
const sorted = allResults.sort((a, b) => a.sizes.gzipped - b.sizes.gzipped);
|
|
const smallest = sorted[0]?.sizes.gzipped || 1;
|
|
|
|
for (const result of sorted) {
|
|
const ratio = (result.sizes.gzipped / smallest).toFixed(1);
|
|
const compression = ((1 - result.sizes.gzipped / result.sizes.raw) * 100).toFixed(1);
|
|
console.log(
|
|
`${result.name.padEnd(14)} | ${(result.sizes.raw/1024).toFixed(1).padStart(8)}KB | ${(result.sizes.gzipped/1024).toFixed(1).padStart(7)}KB | ${compression.padStart(11)}% | ${ratio}x`
|
|
);
|
|
}
|
|
|
|
console.log('\n✅ Benchmark complete! Results saved to results/benchmark-summary.csv');
|
|
}
|
|
|
|
main().catch(console.error); |