Phase
Phase 3 — Botcoin Core decoder package. Tagged `v0.phase-3` with this perf concern explicitly tracked.
Measured vs target
| Metric |
Measured (dev box, pure-JS) |
§9 Phase 3 target |
| eval p50 |
~327 ms |
< 10 ms |
| eval p99 |
~660 ms |
< 50 ms |
Phase 3 PR (#7) gates the throw on the perf budget behind a `distAvailable` flag — i.e. only enforces it when running against compiled `packages/cortex/dist/`. That's not a real fix; `tsc` strip-types doesn't make JS faster. CI will likely fail the perf gate when it runs against compiled dist.
Root cause
`merkleizeState` recomputes the full 1024-leaf binary Merkle tree on every patch eval: ~2047 keccak256 calls per eval. Each pure-JS keccak256 over a 32-byte input is ~150–300 µs in V8, so total wall time is ~300–600 ms per eval. The pure-JS keccak is the bottleneck.
Recommended fix: incremental Merkle update
Most patches change 1–4 words. We only need to recompute the affected leaves and the O(log n × patchSize) ≈ 40 internal nodes from those leaves to the root, not the full 2047. Expected speedup: ~50× (within p50 budget).
Proposed approach
- Cache the leaf hashes and internal-node hashes for the parent state on the worker thread.
- `applyPatch + merkleizeState` inside the worker becomes `applyPatchIncremental(state, patch, parentTree) -> { state, root, tree }`.
- The worker keeps a small LRU of recent (parentStateRoot → tree) entries (<= 16 entries; ~1 MB each).
- Cache miss path falls back to the current full recomputation.
Alternative (cheaper) fix: faster keccak
If incremental Merkle is too invasive for a follow-up, a pinned native or WASM keccak (`@noble/hashes` with explicit Keccak vs SHA3 selection) would give ~3–5× speedup. `@noble/hashes` is a single-file pure-JS / WASM dual that's reasonable to vendor.
Definition of done
- p50 < 10 ms, p99 < 50 ms on the GitHub Actions `ubuntu-latest` runner spec, on the same 10k-sample fuzz used in `test/e2e/phase-3/run.mjs`.
- The `distAvailable` short-circuit in run.mjs is removed; the gate is unconditional.
- The fix is a Phase-3 follow-up PR; does not change the Core decoder version hash.
Cross-references
- `packages/cortex/src/eval/index.ts` — eval entry point.
- `packages/cortex/src/state/merkle.ts` — current full-recompute Merkle.
- `test/e2e/phase-3/run.mjs` lines 273–319 — perf gate.
- ORGANISM_CORTEX_STATE_PLAN.md §9 Phase 3 — perf budget is a hard CI gate; merging Phase 3 with the perf budget unmet is documented here as a known-issue follow-up.
cc @botcoinmoney
Phase
Phase 3 — Botcoin Core decoder package. Tagged `v0.phase-3` with this perf concern explicitly tracked.
Measured vs target
Phase 3 PR (#7) gates the throw on the perf budget behind a `distAvailable` flag — i.e. only enforces it when running against compiled `packages/cortex/dist/`. That's not a real fix; `tsc` strip-types doesn't make JS faster. CI will likely fail the perf gate when it runs against compiled dist.
Root cause
`merkleizeState` recomputes the full 1024-leaf binary Merkle tree on every patch eval: ~2047 keccak256 calls per eval. Each pure-JS keccak256 over a 32-byte input is ~150–300 µs in V8, so total wall time is ~300–600 ms per eval. The pure-JS keccak is the bottleneck.
Recommended fix: incremental Merkle update
Most patches change 1–4 words. We only need to recompute the affected leaves and the O(log n × patchSize) ≈ 40 internal nodes from those leaves to the root, not the full 2047. Expected speedup: ~50× (within p50 budget).
Proposed approach
Alternative (cheaper) fix: faster keccak
If incremental Merkle is too invasive for a follow-up, a pinned native or WASM keccak (`@noble/hashes` with explicit Keccak vs SHA3 selection) would give ~3–5× speedup. `@noble/hashes` is a single-file pure-JS / WASM dual that's reasonable to vendor.
Definition of done
Cross-references
cc @botcoinmoney