|
| 1 | +/** |
| 2 | + * Benchmark runner for JSObjectSpace implementations. |
| 3 | + * Run with: npm run bench (builds via rollup.bench.mjs, then node bench/dist/bench.mjs) |
| 4 | + */ |
| 5 | + |
| 6 | +import { JSObjectSpace } from "../src/object-heap.js"; |
| 7 | +import { JSObjectSpaceOriginal } from "./_original.js"; |
| 8 | + |
| 9 | +export interface HeapLike { |
| 10 | + retain(value: unknown): number; |
| 11 | + release(ref: number): void; |
| 12 | + getObject(ref: number): unknown; |
| 13 | +} |
| 14 | + |
| 15 | +const ITERATIONS = 5; |
| 16 | +const HEAVY_OPS = 200_000; |
| 17 | +const FILL_LEVELS = [1_000, 10_000, 50_000] as const; |
| 18 | +const MIXED_OPS_PER_LEVEL = 100_000; |
| 19 | + |
| 20 | +function median(numbers: number[]): number { |
| 21 | + const sorted = [...numbers].sort((a, b) => a - b); |
| 22 | + const mid = Math.floor(sorted.length / 2); |
| 23 | + return sorted.length % 2 !== 0 |
| 24 | + ? sorted[mid]! |
| 25 | + : (sorted[mid - 1]! + sorted[mid]!) / 2; |
| 26 | +} |
| 27 | + |
| 28 | +function runHeavyRetain(Heap: new () => HeapLike): number { |
| 29 | + const times: number[] = []; |
| 30 | + for (let iter = 0; iter < ITERATIONS; iter++) { |
| 31 | + const heap = new Heap(); |
| 32 | + const start = performance.now(); |
| 33 | + for (let i = 0; i < HEAVY_OPS; i++) { |
| 34 | + heap.retain({ __i: i }); |
| 35 | + } |
| 36 | + times.push(performance.now() - start); |
| 37 | + } |
| 38 | + return median(times); |
| 39 | +} |
| 40 | + |
| 41 | +function runHeavyRelease(Heap: new () => HeapLike): number { |
| 42 | + const times: number[] = []; |
| 43 | + for (let iter = 0; iter < ITERATIONS; iter++) { |
| 44 | + const heap = new Heap(); |
| 45 | + const refs: number[] = []; |
| 46 | + for (let i = 0; i < HEAVY_OPS; i++) { |
| 47 | + refs.push(heap.retain({ __i: i })); |
| 48 | + } |
| 49 | + const start = performance.now(); |
| 50 | + for (let i = 0; i < HEAVY_OPS; i++) { |
| 51 | + heap.release(refs[i]!); |
| 52 | + } |
| 53 | + times.push(performance.now() - start); |
| 54 | + } |
| 55 | + return median(times); |
| 56 | +} |
| 57 | + |
| 58 | +function runMixedFillLevel(Heap: new () => HeapLike, fillLevel: number): number { |
| 59 | + const times: number[] = []; |
| 60 | + for (let iter = 0; iter < ITERATIONS; iter++) { |
| 61 | + const heap = new Heap(); |
| 62 | + const refs: number[] = []; |
| 63 | + for (let i = 0; i < fillLevel; i++) { |
| 64 | + refs.push(heap.retain({ __i: i })); |
| 65 | + } |
| 66 | + let nextId = fillLevel; |
| 67 | + const start = performance.now(); |
| 68 | + for (let i = 0; i < MIXED_OPS_PER_LEVEL; i++) { |
| 69 | + const idx = i % fillLevel; |
| 70 | + heap.release(refs[idx]!); |
| 71 | + refs[idx] = heap.retain({ __i: nextId++ }); |
| 72 | + } |
| 73 | + times.push(performance.now() - start); |
| 74 | + } |
| 75 | + return median(times); |
| 76 | +} |
| 77 | + |
| 78 | +function runBenchmark( |
| 79 | + name: string, |
| 80 | + Heap: new () => HeapLike, |
| 81 | +): { name: string; heavyRetain: number; heavyRelease: number; mixed: Record<string, number> } { |
| 82 | + return { |
| 83 | + name, |
| 84 | + heavyRetain: runHeavyRetain(Heap), |
| 85 | + heavyRelease: runHeavyRelease(Heap), |
| 86 | + mixed: { |
| 87 | + "1k": runMixedFillLevel(Heap, 1_000), |
| 88 | + "10k": runMixedFillLevel(Heap, 10_000), |
| 89 | + "50k": runMixedFillLevel(Heap, 50_000), |
| 90 | + }, |
| 91 | + }; |
| 92 | +} |
| 93 | + |
| 94 | +function main() { |
| 95 | + const implementations: Array<{ name: string; Heap: new () => HeapLike }> = [ |
| 96 | + { name: "JSObjectSpaceOriginal", Heap: JSObjectSpaceOriginal }, |
| 97 | + { name: "JSObjectSpace (current)", Heap: JSObjectSpace }, |
| 98 | + ]; |
| 99 | + |
| 100 | + console.log("JSObjectSpace benchmark"); |
| 101 | + console.log("======================\n"); |
| 102 | + console.log( |
| 103 | + `Heavy retain: ${HEAVY_OPS} ops, Heavy release: ${HEAVY_OPS} ops`, |
| 104 | + ); |
| 105 | + console.log( |
| 106 | + `Mixed: ${MIXED_OPS_PER_LEVEL} ops per fill level (${FILL_LEVELS.join(", ")})`, |
| 107 | + ); |
| 108 | + console.log(`Median of ${ITERATIONS} runs per scenario.\n`); |
| 109 | + |
| 110 | + const results: Array<ReturnType<typeof runBenchmark>> = []; |
| 111 | + for (const { name, Heap } of implementations) { |
| 112 | + console.log(`Running ${name}...`); |
| 113 | + runBenchmark(name, Heap); |
| 114 | + results.push(runBenchmark(name, Heap)); |
| 115 | + } |
| 116 | + |
| 117 | + console.log("\nResults (median ms):\n"); |
| 118 | + const pad = Math.max(...results.map((r) => r.name.length)); |
| 119 | + for (const r of results) { |
| 120 | + console.log( |
| 121 | + `${r.name.padEnd(pad)} retain: ${r.heavyRetain.toFixed(2)}ms release: ${r.heavyRelease.toFixed(2)}ms mixed(1k): ${r.mixed["1k"].toFixed(2)}ms mixed(10k): ${r.mixed["10k"].toFixed(2)}ms mixed(50k): ${r.mixed["50k"].toFixed(2)}ms`, |
| 122 | + ); |
| 123 | + } |
| 124 | + |
| 125 | + const total = (r: (typeof results)[0]) => |
| 126 | + r.heavyRetain + r.heavyRelease + r.mixed["1k"] + r.mixed["10k"] + r.mixed["50k"]; |
| 127 | + const best = results.reduce((a, b) => (total(a) <= total(b) ? a : b)); |
| 128 | + console.log(`\nFastest overall (sum of medians): ${best.name}`); |
| 129 | +} |
| 130 | + |
| 131 | +main(); |
0 commit comments