spike: benchmark dataset growth and fixture-loading strategies - #77
spike: benchmark dataset growth and fixture-loading strategies#77Ogstevyn wants to merge 3 commits into
Conversation
Adds a reproducible harness for measuring how fixture loading and lookup behave as the corpus grows past its current 12 entries. - generate-corpus.mjs builds deterministic synthetic corpora at any size, keeping the real fixtures as the baseline and topping up with entries that mirror the real label mix, risk-pattern vocabulary and notes length - strkey.mjs mints valid synthetic G... account IDs from a seeded PRNG; only the public-key form is implemented, so the harness cannot emit anything resembling a secret seed - strategies.mjs implements five loading strategies behind one build/load contract: raw JSON with object lookup (the current approach), raw JSON with a Map index, a generated ES module, a gzipped archive, and lazily-loaded split packs - measure.mjs measures one (strategy, corpus) pair in an isolated --expose-gc process, covering cold load, a cold 10-lookup session, sustained hit/miss lookup throughput, retained heap and correctness - run-benchmarks.mjs drives the sweep and writes results.json/results.md Output goes to a git-ignored .bench/, so nothing generated is committed. CI runs the two smallest corpora once to keep the harness from bit-rotting; it deliberately does not publish CI timings, since shared runners are too noisy for that.
Writes up the spike in docs/benchmarks/dataset-scale.md: method, corpora, the five strategies compared, measurements at 12 / 100 / 1,000 / 10,000 / 100,000 entries, a comparison matrix, a staged recommendation, and the thresholds at which the current approach stops being sufficient. Headline findings: - Startup, not lookup, is the constraint. Lookup stays under 1 us at every size; cold load spans 1.8 ms to 348 ms across the sweep. - Retained heap is the current approach's worst axis, at ~420 bytes/entry - 3.9 MB at 10,000 entries and 41 MB at 100,000 - because it holds the whole parsed destinations array, roughly half of which is review metadata no consumer reads at lookup time. - Lazy loading only pays once the corpus is much larger than the working set: split packs lose at 10,000 entries and win by 11x at 100,000. - The raw-JSON layout should stay the reviewable source of truth at every size. What changes as the corpus grows is what gets published, not what gets reviewed. The recommendation is to change nothing now; the current approach becomes insufficient between roughly 2,500 and 5,000 entries.
|
please pass failing test |
The secret-check CI job has failed on every branch since #57, so validate (which declares needs: secret-check) has not run either. Two separate problems, both introduced by what looks like an encoding accident when the file was first written. 1. Syntax error. Line 26 had backticks replaced by backslashes and its template placeholders stripped, so the module failed to parse and the job crashed before scanning anything. Restored as a template literal reporting file, line and a redacted match. Two Windows-1252 em-dash bytes elsewhere in the file are replaced with ASCII hyphens so this cannot recur. 2. The detector never fired. Stellar strkeys append the CRC16-XModem checksum little-endian (SEP-23), but isValidEd25519SecretSeed read it big-endian, so every genuine secret seed failed the checksum test and was discarded as a false positive. All 11 real account IDs in destinations.json validate little-endian and none validate big-endian. Verified by planting a spec-correct seed in a tracked file: the guard now reports it and exits 1, where the previous logic reported a clean tree. Fixing only the syntax error would have turned CI green while leaving a security guard that silently never fires, which is worse than a visibly broken one - so the file now self-tests on every run. It derives a well-formed seed at runtime from a fixed payload (no secret material is stored in the file) and exits non-zero if the detector no longer recognises it. Running the self-test against the previous big-endian logic reproduces the failure, so this check would have caught the bug at merge time.
|
@knytcomics-ui thanks for the review - the failing check is fixed, pushed as The failure was 1. The syntax error that was failing CI. Line 26 had its backticks replaced with backslashes and its template placeholders stripped: console.error(\SECRET SEED FOUND: \:\ ? [REDACTED]\);so the module failed to parse and the job crashed before scanning a single file. Restored as a proper template literal reporting file, line and a redacted match. Two stray Windows-1252 em-dash bytes elsewhere in the file are now ASCII hyphens, so the same corruption cannot recur. 2. The detector would never have fired. This one is more serious than the crash. Stellar strkeys append the CRC16-XModem checksum little-endian (SEP-23), but const expected = (bytes[bytes.length - 2] << 8) | bytes[bytes.length - 1];The regex matched a leaked seed, the checksum test then rejected it as a false positive, and the script reported a clean tree. Two independent confirmations:
3. Why I did not stop at the syntax fix. Fixing only the crash would have turned CI green while leaving a security guard that silently never fires, which is worse than a visibly broken one - nobody re-checks a green job. So the script now self-tests on every run: it derives a well-formed seed at runtime from a fixed payload (no secret material is stored in the file) and exits non-zero if the detector stops recognising it. Run against the old big-endian logic that self-test fails, so it would have caught this at merge time. If you would rather keep this PR strictly to the spike, I am happy to move the All three jobs pass locally: The spike itself is unchanged: no fixture files are touched, and the recommendation is still that nothing needs to change today, with the current raw-JSON layout becoming insufficient between roughly 2,500 and 5,000 entries. One thing I cannot do from my side: the workflow run is sitting at |
closes #76
Summary
This spike answers how fixture loading and lookup should behave as the corpus grows from 12 entries to hundreds or thousands. It adds a reproducible benchmark harness under
scripts/bench/and writes the findings up indocs/benchmarks/dataset-scale.md.No fixture files are touched. The recommendation is that nothing needs to change today.
Answer
The current two-file raw-JSON layout is comfortable to roughly 5,000 entries, needs a derived release artifact between 5,000 and 25,000, and only needs lazily-loaded split packs beyond that.
destinations.json+scores.jsonshould stay the reviewable source of truth at every size - the labelling rubric, the changelog discipline and the reviewer checklist inCONTRIBUTING.mdall depend on a fixture change being a readable diff. What should change as the corpus grows is what gets published, not what gets reviewed.What was measured
Five corpus sizes (12 / 100 / 1,000 / 10,000 / 100,000) against five loading strategies:
destinations.json+scores.json, verbatimMapindexfixtures.mjsof packed rowsfixtures.json.gzof packed rowsmanifest.json+ hash-addressedpacks/N.jsonEach cell is the median of 5 independent
node --expose-gcprocesses, so no strategy inherits another's module cache, JIT state or heap. Reported per cell: cold load, warm load, sustained hit/miss lookup, retained heap, artifact size (raw, gzipped, and bytes-before-first-lookup), plus a cold-session figure - load the index and score 10 distinct destinations, which is what an extension session actually pays.The 12-entry corpus is the real
destinations.json/scores.jsoncopied verbatim, so the baseline row is what consumers load today rather than a synthetic stand-in.Headline numbers
Cold load (ms):
Retained heap after load:
The single most important number: at 100,000 entries a cold session (load, then score 10 destinations) costs 220 ms and 41 MB on the current strategy and 20 ms and 389 KB on split packs.
Full tables, including lookup latency and artifact sizes, are in the report.
Findings
notes,addressandtypefields plus the JSON key names are roughly half the bytes and most of the retained heap.constructorreturns a function rather thanundefined. Stellar strkeys andCODE:ISSUERasset IDs cannot collide withObject.prototypekeys, so this is latent robustness rather than a live bug, but it is free to remove by indexing into aMap. The harness asserts this on every run.Thresholds
The current approach becomes insufficient between roughly 2,500 and 5,000 entries - bytes-before-first-lookup goes first for a bundler-inlining consumer, then cold load.
Staged response: (1) no change now; (2) whenever convenient, have
StubOraclebuild aMapand drop the parseddestinationsarray - small change, ~3.5x less retained heap, removes the prototype-key hazard; (3) at ~5,000 entries add a release build step emitting a packed gzipped artifact alongside the JSON; (4) at ~25,000 entries revisit split packs, re-running the harness first.Reproducing
The first is the default sweep; the second is what produced the tables above. Results land in
.bench/results.jsonand.bench/results.md..bench/is git-ignored and the corpora are regenerated deterministically, so nothing generated is committed. Synthetic account IDs are real Stellar strkeys built from a seeded PRNG; only theG...public-key form is implemented, so the harness cannot emit anything that resembles a secret seed.Environment for the numbers above: Node v22.23.0, win32/x64, AMD Ryzen 7 PRO 7730U. The 1,000- and 10,000-entry sweeps were repeated on Node v20.20.2 (the version CI pins): load figures came out 10-30% slower, with the ordering, crossover points and per-entry constants unchanged. Absolute numbers are machine-specific; the ratios and scaling shapes are what the recommendation rests on.
Changes
scripts/bench/- the harness (corpus generator, strkey minter, strategies, per-process measurement, sweep driver)docs/benchmarks/dataset-scale.md- the reportpackage.json-npm run bench,npm run bench:corpus.github/workflows/ci.yml- a smoke job running the two smallest corpora once, so the harness cannot bit-rot. It deliberately does not publish CI timings as results; shared runners are far too noisy for that..gitignore- ignore the benchmark working directoryREADME.md- a pointer to the report and tonpm run benchNo fixture files changed, so no
CHANGELOG.mdentry is required perCONTRIBUTING.md.Caveats
These are Node numbers, not browser numbers. In a browser each cold shard read becomes an async
fetchor IndexedDB round trip, so a browser measurement should precede any split-pack adoption - the adapter's asyncIOracleinterface survives that change, but the timings would shift. The 512-ID working set is also spread evenly across the corpus, whereas real lookups skew toward hot destinations, which favours lazy loading more than these numbers show. Both are recorded in the report.CI fix included
CI on
mainhad been red since #57:scripts/check-secrets.mjsfailed to parse, so thesecret-checkjob crashed andvalidatenever ran (it declaresneeds: secret-check). At the maintainer's request this branch now fixes it, in its own commit kept separate from the spike.Two problems, both from what looks like an encoding accident when the file was first written:
isValidEd25519SecretSeedread it big-endian, so every genuine secret seed failed the checksum test and was discarded as a false positive. All 11 real account IDs indestinations.jsonvalidate little-endian and none validate big-endian. Planting a spec-correct seed in a tracked file confirms it: the guard now reportsSECRET SEED FOUND: <file>:<line> - [REDACTED]and exits 1, where the previous logic reported a clean tree.Fixing only the syntax error would have turned CI green while leaving a security guard that silently never fires - worse than a visibly broken one - so the file now self-tests on every run. It derives a well-formed seed at runtime from a fixed payload (no secret material is stored in the file) and exits non-zero if the detector stops recognising it. Run against the previous big-endian logic, that self-test reproduces the failure, so it would have caught this bug at merge time.
All three CI jobs pass locally:
secret-check,validate(12 destinations, 12 scores) andbench-smoke.