mirror: A fast canonicalisation utility for stable object equality
0

Configure Feed

Select the types of activity you want to include in your feed.

docs: display benchmarks much nicer

Marais Rossouw (Jun 24, 2026, 3:59 PM +1000) 6d77d864 25c0e4de

+331 -169
+2 -1
deno.json
··· 3 3 "name": "@mr/object-identity", 4 4 "exports": "./lib/mod.ts", 5 5 "tasks": { 6 - "build": "deno run -A scripts/build.ts" 6 + "build": "deno run -A scripts/build.ts", 7 + "bench": "deno run -A scripts/bench.ts" 7 8 }, 8 9 "imports": { 9 10 "@std/assert": "jsr:@std/assert@^1",
+110 -127
lib/mod.bench.ts
··· 1 1 // deno-lint-ignore-file no-import-prefix 2 2 3 - import objectHash from 'npm:object-hash@^3.0'; 4 - import { serialize as ohashSerialize } from 'npm:ohash@^2.0'; 5 - import { objectStringify as liqdObjectStringify } from 'npm:@liqd-js/fast-object-hash@^2.0'; 6 - import { hash as hashIt } from 'npm:hash-it@^7.0'; 7 - import jsonStableStringify from 'npm:json-stable-stringify@^1.3'; 8 - import fastJsonStableStringify from 'npm:fast-json-stable-stringify@^2.1'; 9 - import { stringify as tinyStableStringify } from 'npm:tiny-stable-stringify@^0.1'; 10 - import safeStableStringify from 'npm:safe-stable-stringify@^2.5'; 11 - import jsonStringifyDeterministic from 'npm:json-stringify-deterministic@^1.0'; 12 - import jsonStableStringifyWithoutJsonify from 'npm:json-stable-stringify-without-jsonify@^1.0'; 13 - import emeraldJsonStableStringify from 'npm:@emeraldsquad/json-stable-stringify@^1.0'; 14 - import fasterStableStringify from 'npm:faster-stable-stringify@^1.0'; 15 - import canonicalize from 'npm:canonicalize@^3.0'; 16 - import { canonicalize as jsonCanonicalize } from 'npm:json-canonicalize@^2.0'; 17 - import canonicalJson from 'npm:canonical-json@^0.4'; 18 - import { canonicalize as tufCanonicalize } from 'npm:@tufjs/canonical-json@^2.0'; 3 + const scenarios: Record<string, () => unknown> = { 4 + simple() { 5 + return { 6 + id: 1234, 7 + name: 'object-identity', 8 + active: true, 9 + score: 98.6, 10 + tags: ['canonical', 'stable', 'fast'], 11 + meta: { 12 + created: 1_700_000_000_000, 13 + version: '0.1.2', 14 + nested: { a: 1, b: 2, c: 3 }, 15 + }, 16 + items: [ 17 + { k: 'one', v: 1 }, 18 + { k: 'two', v: 2 }, 19 + { k: 'three', v: 3 }, 20 + ], 21 + }; 22 + }, 19 23 20 - import { identify } from './mod.ts'; 24 + deep() { 25 + let node: any = { leaf: true, value: 0 }; 26 + for (let i = 0; i < 32; i++) { 27 + node = { depth: i, label: 'node-' + i, sibling: [i, i + 1], child: node }; 28 + } 29 + return node; 30 + }, 21 31 22 - function get_object() { 23 - const c = [1]; 24 - // @ts-expect-error circular 25 - c.push(c); 26 - return { 27 - a: { b: ['c', new Set(['d', new Map([['e', 'f']]), c, 'g'])] }, 28 - d: new Date(), 29 - r: /a/, 30 - }; 31 - } 32 + 'deep circular'() { 33 + const root: any = { id: 'root' }; 34 + let node: any = root; 35 + for (let i = 0; i < 32; i++) { 36 + node.next = { depth: i, root }; 37 + node = node.next; 38 + } 39 + node.next = root; 40 + return root; 41 + }, 32 42 33 - Deno.bench({ 34 - name: 'object-identity', 35 - baseline: true, 36 - fn() { 37 - let _ = identify(get_object()); 43 + big() { 44 + const out: Record<string, unknown> = {}; 45 + for (let i = 0; i < 256; i++) { 46 + out['key_' + i] = { i, label: 'item-' + i, on: i % 2 === 0, tags: ['a', 'b', 'c'] }; 47 + } 48 + return out; 38 49 }, 39 - }); 40 50 41 - Deno.bench({ 42 - name: 'object-hash', 43 - fn() { 44 - let _ = objectHash(get_object(), { algorithm: 'passthrough' }); 51 + leafy() { 52 + const out: unknown[] = []; 53 + for (let i = 0; i < 512; i++) out.push(i, 'str-' + i, i % 3 === 0, null); 54 + return out; 45 55 }, 46 - }); 56 + }; 47 57 48 - Deno.bench({ 49 - name: 'ohash', 50 - fn() { 51 - let _ = ohashSerialize(get_object()); 58 + const candidates: Record<string, { 59 + lib: () => Promise<any>; 60 + run: (mod: any, value: any) => unknown; 61 + }> = { 62 + 'object-identity': { 63 + lib: () => import('./mod.ts'), 64 + run: (m, o) => m.identify(o), 65 + }, 66 + 'safe-stable-stringify': { 67 + lib: () => import('npm:safe-stable-stringify@^2.5'), 68 + run: (m, o) => m.default(o), 52 69 }, 53 - }); 54 - 55 - Deno.bench({ 56 - name: '@liqd-js/fast-object-hash', 57 - fn() { 58 - let _ = liqdObjectStringify(get_object()); 70 + ohash: { 71 + lib: () => import('npm:ohash@^2.0'), 72 + run: (m, o) => m.serialize(o), 59 73 }, 60 - }); 61 - 62 - Deno.bench({ 63 - name: 'hash-it †', 64 - fn() { 65 - let _ = hashIt(get_object()); 74 + 'object-hash': { 75 + lib: () => import('npm:object-hash@^3.0'), 76 + run: (m, o) => m.default(o, { algorithm: 'passthrough' }), 66 77 }, 67 - }); 68 - 69 - Deno.bench({ 70 - name: 'json-stable-stringify', 71 - fn() { 72 - let _ = jsonStableStringify(get_object()); 78 + 'hash-it †': { 79 + lib: () => import('npm:hash-it@^7.0'), 80 + run: (m, o) => m.hash(o), 73 81 }, 74 - }); 75 - 76 - Deno.bench({ 77 - name: 'fast-json-stable-stringify', 78 - fn() { 79 - let _ = fastJsonStableStringify(get_object()); 82 + 'json-stable-stringify': { 83 + lib: () => import('npm:json-stable-stringify@^1.3'), 84 + run: (m, o) => m.default(o), 80 85 }, 81 - }); 82 - 83 - Deno.bench({ 84 - name: 'faster-stable-stringify', 85 - fn() { 86 - let _ = fasterStableStringify(get_object()); 86 + 'fast-json-stable-stringify': { 87 + lib: () => import('npm:fast-json-stable-stringify@^2.1'), 88 + run: (m, o) => m.default(o), 87 89 }, 88 - }); 89 - 90 - Deno.bench({ 91 - name: 'tiny-stable-stringify', 92 - fn() { 93 - let _ = tinyStableStringify(get_object()); 90 + 'tiny-stable-stringify': { 91 + lib: () => import('npm:tiny-stable-stringify@^0.1'), 92 + run: (m, o) => m.stringify(o), 94 93 }, 95 - }); 96 - 97 - Deno.bench({ 98 - name: 'safe-stable-stringify', 99 - fn() { 100 - let _ = safeStableStringify(get_object()); 94 + 'json-stringify-deterministic': { 95 + lib: () => import('npm:json-stringify-deterministic@^1.0'), 96 + run: (m, o) => m.default(o), 101 97 }, 102 - }); 103 - 104 - Deno.bench({ 105 - name: 'json-stringify-deterministic', 106 - fn() { 107 - let _ = jsonStringifyDeterministic(get_object()); 98 + 'json-stable-stringify-without-jsonify': { 99 + lib: () => import('npm:json-stable-stringify-without-jsonify@^1.0'), 100 + run: (m, o) => m.default(o), 108 101 }, 109 - }); 110 - 111 - Deno.bench({ 112 - name: 'json-stable-stringify-without-jsonify', 113 - fn() { 114 - let _ = jsonStableStringifyWithoutJsonify(get_object()); 102 + canonicalize: { 103 + lib: () => import('npm:canonicalize@^3.0'), 104 + run: (m, o) => m.default(o), 115 105 }, 116 - }); 117 - 118 - Deno.bench({ 119 - name: '@emeraldsquad/json-stable-stringify', 120 - fn() { 121 - let _ = emeraldJsonStableStringify(get_object()); 106 + '@tufjs/canonical-json': { 107 + lib: () => import('npm:@tufjs/canonical-json@^2.0'), 108 + run: (m, o) => m.canonicalize(o), 122 109 }, 123 - }); 110 + }; 124 111 125 - Deno.bench({ 126 - name: 'canonicalize', 127 - fn() { 128 - let _ = canonicalize(get_object()); 129 - }, 130 - }); 112 + const mods: Record<string, any> = {}; 113 + for (const [name, c] of Object.entries(candidates)) mods[name] = await c.lib(); 131 114 132 - Deno.bench({ 133 - name: 'json-canonicalize', 134 - fn() { 135 - let _ = jsonCanonicalize(get_object()); 136 - }, 137 - }); 115 + const fixtures = Object.entries(scenarios).map(([name, make]) => [name, make()] as const); 138 116 139 - Deno.bench({ 140 - name: 'canonical-json', 141 - fn() { 142 - let _ = canonicalJson(get_object()); 143 - }, 144 - }); 117 + for (const [scenario, value] of fixtures) { 118 + for (const [name, c] of Object.entries(candidates)) { 119 + // Validate up front: a throw (can't handle the shape) or non-deterministic 120 + // output marks the bench ignored, so it's skipped instead of measured. 121 + let valid = false; 122 + try { 123 + valid = c.run(mods[name], value) === c.run(mods[name], value); 124 + } catch { /* leaves valid false -> ignored */ } 145 125 146 - Deno.bench({ 147 - name: '@tufjs/canonical-json', 148 - fn() { 149 - let _ = tufCanonicalize(get_object()); 150 - }, 151 - }); 126 + Deno.bench({ 127 + name, 128 + group: scenario, 129 + baseline: name === 'object-identity', 130 + ignore: !valid, 131 + fn: () => void c.run(mods[name], value), 132 + }); 133 + } 134 + }
+102 -41
readme.md
··· 53 53 54 54 ## 💨 Benchmark 55 55 56 - | benchmark | time/iter (avg) | iter/s | (min … max) | p75 | p99 | p995 | 57 - | ------------------------------------- | --------------- | --------- | --------------------- | -------- | -------- | -------- | 58 - | object-identity | 582.4 ns | 1,717,000 | (540.4 ns … 807.5 ns) | 588.1 ns | 807.5 ns | 807.5 ns | 59 - | object-hash | 8.3 µs | 120,500 | ( 7.4 µs … 211.7 µs) | 8.0 µs | 15.4 µs | 46.0 µs | 60 - | ohash | 3.1 µs | 320,500 | ( 2.8 µs … 208.7 µs) | 3.1 µs | 3.5 µs | 4.7 µs | 61 - | @liqd-js/fast-object-hash | 2.5 µs | 403,400 | ( 2.4 µs … 2.9 µs) | 2.5 µs | 2.9 µs | 2.9 µs | 62 - | hash-it † | 2.5 µs | 406,700 | ( 2.4 µs … 3.0 µs) | 2.5 µs | 3.0 µs | 3.0 µs | 63 - | json-stable-stringify | 1.7 µs | 600,300 | ( 1.6 µs … 1.8 µs) | 1.7 µs | 1.8 µs | 1.8 µs | 64 - | fast-json-stable-stringify | 1.3 µs | 749,800 | ( 1.3 µs … 1.5 µs) | 1.3 µs | 1.5 µs | 1.5 µs | 65 - | faster-stable-stringify | 1.6 µs | 630,500 | ( 1.5 µs … 1.8 µs) | 1.6 µs | 1.8 µs | 1.8 µs | 66 - | tiny-stable-stringify | 1.6 µs | 644,200 | ( 1.5 µs … 1.6 µs) | 1.6 µs | 1.6 µs | 1.6 µs | 67 - | safe-stable-stringify | 1.2 µs | 850,700 | ( 1.2 µs … 1.2 µs) | 1.2 µs | 1.2 µs | 1.2 µs | 68 - | json-stringify-deterministic | 2.8 µs | 359,400 | ( 2.7 µs … 2.9 µs) | 2.8 µs | 2.9 µs | 2.9 µs | 69 - | json-stable-stringify-without-jsonify | 1.6 µs | 639,400 | ( 1.5 µs … 1.6 µs) | 1.6 µs | 1.6 µs | 1.6 µs | 70 - | @emeraldsquad/json-stable-stringify | 1.6 µs | 638,000 | ( 1.5 µs … 1.6 µs) | 1.6 µs | 1.6 µs | 1.6 µs | 71 - | canonicalize | 1.8 µs | 570,100 | ( 1.7 µs … 1.8 µs) | 1.8 µs | 1.8 µs | 1.8 µs | 72 - | json-canonicalize | 1.6 µs | 612,100 | ( 1.6 µs … 1.7 µs) | 1.7 µs | 1.7 µs | 1.7 µs | 73 - | canonical-json | 1.7 µs | 587,100 | ( 1.7 µs … 1.8 µs) | 1.7 µs | 1.8 µs | 1.8 µs | 74 - | @tufjs/canonical-json | 1.2 µs | 805,100 | ( 1.2 µs … 1.3 µs) | 1.2 µs | 1.3 µs | 1.3 µs | 56 + - **simple** a complete nested object, with an arrays and other mixed primitives. 57 + - **deep** a long, narrow nesting chain. 58 + - **deep circular** the deep chain, but every node points back and the tail closes the loop. 59 + - **big** a wide object: hundreds of keys, but with small values. 60 + - **leafy** a large flat array of mixed primitives. 61 + 62 + <!-- BEGIN BENCHMARK --> 63 + 64 + ``` 65 + ✔ simple ~ 1.1µs @ 910,961 ops/sec ± 0.30% 66 + ✔ deep ~ 8.7µs @ 115,162 ops/sec ± 1.80% 67 + ✔ deep circular ~ 5.7µs @ 175,990 ops/sec ± 0.22% 68 + ✔ big ~ 140.9µs @ 7,095 ops/sec ± 0.08% 69 + ✔ leafy ~ 23.0µs @ 43,393 ops/sec ± 1.10% 70 + ``` 71 + 72 + <details><summary>All candidates</summary> 75 73 76 74 ``` 77 - summary 78 - object-identity 79 - 2.02x faster than safe-stable-stringify 80 - 2.13x faster than @tufjs/canonical-json 81 - 2.29x faster than fast-json-stable-stringify 82 - 2.67x faster than tiny-stable-stringify 83 - 2.69x faster than json-stable-stringify-without-jsonify 84 - 2.69x faster than @emeraldsquad/json-stable-stringify 85 - 2.72x faster than faster-stable-stringify 86 - 2.81x faster than json-canonicalize 87 - 2.86x faster than json-stable-stringify 88 - 2.92x faster than canonical-json 89 - 3.01x faster than canonicalize 90 - 4.22x faster than hash-it † 91 - 4.26x faster than @liqd-js/fast-object-hash 92 - 4.78x faster than json-stringify-deterministic 93 - 5.36x faster than ohash 94 - 14.25x faster than object-hash 75 + simple 76 + ✔ object-identity ~ 1.1µs @ 910,961 ops/sec ± 0.30% 77 + ✔ safe-stable-stringify ~ 1.7µs @ 593,432 ops/sec ± 0.15% 78 + ✔ ohash ~ 3.5µs @ 286,954 ops/sec ± 0.00% 79 + ✔ object-hash ~ 9.5µs @ 105,175 ops/sec ± 1.84% 80 + ✔ hash-it † ~ 2.7µs @ 364,963 ops/sec ± 2.26% 81 + ✔ json-stable-stringify ~ 3.2µs @ 315,125 ops/sec ± 0.64% 82 + ✔ fast-json-stable-stringify ~ 2.0µs @ 495,655 ops/sec ± 0.52% 83 + ✔ tiny-stable-stringify ~ 2.3µs @ 429,496 ops/sec ± 0.00% 84 + ✔ json-stringify-deterministic ~ 3.3µs @ 303,470 ops/sec ± 0.08% 85 + ✔ json-stable-stringify-without-jsonify ~ 3.1µs @ 323,737 ops/sec ± 1.89% 86 + ✔ canonicalize ~ 3.3µs @ 303,716 ops/sec ± 0.10% 87 + ✘ @tufjs/canonical-json 88 + ───────────────────────────────────────────────────────────────────────── 89 + ⭐︎ object-identity (53.5% faster than safe-stable-stringify) 90 + 91 + deep 92 + ✔ object-identity ~ 8.7µs @ 115,162 ops/sec ± 1.80% 93 + ✔ safe-stable-stringify ~ 13.6µs @ 73,438 ops/sec ± 0.24% 94 + ✔ ohash ~ 26.1µs @ 38,257 ops/sec ± 0.05% 95 + ✔ object-hash ~ 53.8µs @ 18,572 ops/sec ± 0.72% 96 + ✔ hash-it † ~ 21.7µs @ 46,100 ops/sec ± 1.80% 97 + ✔ json-stable-stringify ~ 22.5µs @ 44,539 ops/sec ± 1.46% 98 + ✔ fast-json-stable-stringify ~ 13.4µs @ 74,806 ops/sec ± 0.88% 99 + ✔ tiny-stable-stringify ~ 15.5µs @ 64,717 ops/sec ± 0.50% 100 + ✔ json-stringify-deterministic ~ 22.7µs @ 44,121 ops/sec ± 1.10% 101 + ✔ json-stable-stringify-without-jsonify ~ 22.1µs @ 45,243 ops/sec ± 1.22% 102 + ✔ canonicalize ~ 25.0µs @ 39,935 ops/sec ± 1.16% 103 + ✔ @tufjs/canonical-json ~ 33.5µs @ 29,867 ops/sec ± 1.31% 104 + ────────────────────────────────────────────────────────────────────────── 105 + ⭐︎ object-identity (53.9% faster than fast-json-stable-stringify) 106 + 107 + deep circular 108 + ✔ object-identity ~ 5.7µs @ 175,990 ops/sec ± 0.22% 109 + ✔ safe-stable-stringify ~ 7.2µs @ 138,163 ops/sec ± 1.36% 110 + ✔ ohash ~ 16.9µs @ 59,172 ops/sec ± 2.12% 111 + ✔ object-hash ~ 44.8µs @ 22,345 ops/sec ± 1.22% 112 + ✔ hash-it † ~ 15.4µs @ 64,834 ops/sec ± 2.48% 113 + ✘ json-stable-stringify 114 + ✘ fast-json-stable-stringify 115 + ✘ tiny-stable-stringify 116 + ✘ json-stringify-deterministic 117 + ✘ json-stable-stringify-without-jsonify 118 + ✘ canonicalize 119 + ✘ @tufjs/canonical-json 120 + ────────────────────────────────────────────────────────────────────────── 121 + ⭐︎ object-identity (27.4% faster than safe-stable-stringify) 122 + 123 + big 124 + ✔ object-identity ~ 140.9µs @ 7,095 ops/sec ± 0.08% 125 + ✔ safe-stable-stringify ~ 135.6µs @ 7,377 ops/sec ± 1.24% 126 + ✔ ohash ~ 292.0µs @ 3,424 ops/sec ± 0.02% 127 + ✔ object-hash ~ 469.1µs @ 2,132 ops/sec ± 1.36% 128 + ✔ hash-it † ~ 258.4µs @ 3,871 ops/sec ± 0.22% 129 + ✔ json-stable-stringify ~ 209.4µs @ 4,776 ops/sec ± 0.50% 130 + ✔ fast-json-stable-stringify ~ 150.4µs @ 6,647 ops/sec ± 0.68% 131 + ✔ tiny-stable-stringify ~ 164.6µs @ 6,076 ops/sec ± 0.06% 132 + ✔ json-stringify-deterministic ~ 268.9µs @ 3,719 ops/sec ± 0.90% 133 + ✔ json-stable-stringify-without-jsonify ~ 220.8µs @ 4,529 ops/sec ± 0.10% 134 + ✔ canonicalize ~ 240.3µs @ 4,162 ops/sec ± 0.61% 135 + ✔ @tufjs/canonical-json ~ 369.3µs @ 2,708 ops/sec ± 0.38% 136 + ───────────────────────────────────────────────────────────────────────── 137 + ⭐︎ safe-stable-stringify (4.0% faster than object-identity) 138 + 139 + leafy 140 + ✔ object-identity ~ 23.0µs @ 43,393 ops/sec ± 1.10% 141 + ✔ safe-stable-stringify ~ 48.8µs @ 20,510 ops/sec ± 0.61% 142 + ✔ ohash ~ 31.7µs @ 31,549 ops/sec ± 1.41% 143 + ✔ object-hash ~ 84.5µs @ 11,837 ops/sec ± 1.50% 144 + ✔ hash-it † ~ 79.5µs @ 12,585 ops/sec ± 0.26% 145 + ✔ json-stable-stringify ~ 105.9µs @ 9,439 ops/sec ± 0.26% 146 + ✔ fast-json-stable-stringify ~ 54.8µs @ 18,234 ops/sec ± 1.77% 147 + ✔ tiny-stable-stringify ~ 101.9µs @ 9,810 ops/sec ± 0.51% 148 + ✔ json-stringify-deterministic ~ 123.0µs @ 8,133 ops/sec ± 1.66% 149 + ✔ json-stable-stringify-without-jsonify ~ 114.9µs @ 8,702 ops/sec ± 0.25% 150 + ✔ canonicalize ~ 91.0µs @ 10,986 ops/sec ± 0.06% 151 + ✔ @tufjs/canonical-json ~ 215.3µs @ 4,646 ops/sec ± 0.31% 152 + ────────────────────────────────────────────────────────────────────────── 153 + ⭐︎ object-identity (37.5% faster than ohash) 95 154 ``` 96 155 97 - > ^ `object-identity` only handles JSON-like data by design. It won't fingerprint functions or every 98 - > Node builtin the way some alternatives do, so these numbers only reflect the payloads it targets. 99 - > 100 - > `†` no canonicalization-only step, so its time includes hashing. 156 + </details> 157 + <!-- END BENCHMARK --> 158 + 159 + > `object-identity` only handles mainly JSON-like data by design. It won't fingerprint functions or 160 + > every Node builtin the way some alternatives do, so these numbers only reflect the payloads it 161 + > targets. 101 162 102 163 ## License 103 164
+117
scripts/bench.ts
··· 1 + const BASELINE = 'object-identity'; 2 + 3 + const { stdout } = await new Deno.Command('deno', { 4 + args: ['bench', '-A', '--json', 'lib/mod.bench.ts'], 5 + stdout: 'piped', 6 + stderr: 'null', 7 + }).output(); 8 + 9 + const report = JSON.parse(new TextDecoder().decode(stdout)); 10 + 11 + type Row = { name: string; ok: boolean; avg?: number; ops?: number; jitter?: number }; 12 + 13 + const groups: string[] = []; 14 + const order: string[] = []; // candidate union, in first-seen (registration) order 15 + const seen = new Set<string>(); 16 + const metric: Record<string, Record<string, Row>> = {}; 17 + 18 + for (const b of report.benches as Array<Record<string, any>>) { 19 + if (!metric[b.group]) ((metric[b.group] = {}), groups.push(b.group)); 20 + if (!seen.has(b.name)) (seen.add(b.name), order.push(b.name)); 21 + 22 + const ok = b.results?.[0]?.ok; 23 + if (!ok) continue; 24 + 25 + metric[b.group][b.name] = { 26 + name: b.name, 27 + ok: true, 28 + avg: ok.avg, // nanoseconds 29 + ops: Math.round(1e9 / ok.avg), 30 + // no stddev, so approximate jitter from p75 vs avg. 31 + jitter: (Math.abs(ok.p75 - ok.avg) / ok.avg) * 100, 32 + }; 33 + } 34 + 35 + const rows: Record<string, Row[]> = {}; 36 + 37 + for (const g of groups) rows[g] = order.map((name) => metric[g][name] ?? { name, ok: false }); 38 + 39 + const nf = new Intl.NumberFormat('en-AU'); 40 + 41 + function dur(ns: number): string { 42 + if (ns < 1e3) return `${ns.toFixed(1)}ns`; 43 + if (ns < 1e6) return `${(ns / 1e3).toFixed(1)}µs`; 44 + return `${(ns / 1e6).toFixed(1)}ms`; 45 + } 46 + 47 + function line(r: Row, label: string, nameW: number, avgW: number, opsW: number): string { 48 + const mark = r.ok ? '✔' : '✘'; 49 + if (r.ops == null) return `${mark} ${label}`; 50 + return `${mark} ${label.padEnd(nameW)} ~ ${dur(r.avg!).padStart(avgW)} @ ${ 51 + nf 52 + .format(r.ops) 53 + .padStart(opsW) 54 + } ops/sec ± ${r.jitter!.toFixed(2)}%`; 55 + } 56 + 57 + const avgW = (rs: Row[]) => Math.max(0, ...rs.map((r) => (r.avg != null ? dur(r.avg).length : 0))); 58 + const opsW = (rs: Row[]) => 59 + Math.max(0, ...rs.map((r) => (r.ops != null ? nf.format(r.ops).length : 0))); 60 + 61 + // Top: just this library, one row per shape (the readme set). 62 + const baseline = groups.map((g) => rows[g].find((x) => x.name === BASELINE)!); 63 + const sumNameW = Math.max(...groups.map((g) => g.length)); 64 + const sumAvgW = avgW(baseline); 65 + const sumOpsW = opsW(baseline); 66 + const summary = groups.map((g, k) => line(baseline[k], g, sumNameW, sumAvgW, sumOpsW)).join('\n'); 67 + 68 + // Details: every candidate, grouped by shape, with the winner called out. 69 + const details = groups 70 + .map((g) => { 71 + const nameW = Math.max(...rows[g].map((r) => r.name.length)); 72 + const w = [nameW, avgW(rows[g]), opsW(rows[g])] as const; 73 + const lines = rows[g].map((r) => line(r, r.name, ...w)); 74 + 75 + // Fastest candidate wins; note how far ahead of the next best it is. 76 + const ranked = rows[g].filter((r) => r.ops != null).sort((a, b) => b.ops! - a.ops!); 77 + const [win, alt] = ranked; 78 + if (win) { 79 + const ahead = alt 80 + ? ` (${((win.ops! / alt.ops! - 1) * 100).toFixed(1)}% faster than ${alt.name})` 81 + : ''; 82 + lines.push('─'.repeat(Math.max(...lines.map((l) => l.length))), `⭐︎ ${win.name}${ahead}`); 83 + } 84 + 85 + return `${g}\n${lines.join('\n')}`; 86 + }) 87 + .join('\n\n'); 88 + 89 + const block = [ 90 + '', 91 + '```', 92 + summary, 93 + '```', 94 + '', 95 + '<details><summary>All candidates</summary>', 96 + '', 97 + '```', 98 + details, 99 + '```', 100 + '', 101 + '</details>', 102 + '', 103 + ].join('\n'); 104 + 105 + const BEGIN = '<!-- BEGIN BENCHMARK -->'; 106 + const END = '<!-- END BENCHMARK -->'; 107 + 108 + let readme = await Deno.readTextFile('readme.md'); 109 + const i = readme.indexOf(BEGIN); 110 + const j = readme.indexOf(END); 111 + readme = readme.slice(0, i + BEGIN.length) + block + readme.slice(j); 112 + await Deno.writeTextFile('readme.md', readme); 113 + 114 + await new Deno.Command('deno', { args: ['fmt', 'readme.md'], stdout: 'null', stderr: 'null' }) 115 + .output(); 116 + 117 + console.log('patched (%d groups, %d candidates)', groups.length, rows[groups[0]].length);