Skip to content

harness: O(1) positional freshCases relocation (was a per-case name() scan) - #83

Merged
lann merged 1 commit into
mainfrom
fresh-cases-positional
Aug 11, 2026
Merged

harness: O(1) positional freshCases relocation (was a per-case name() scan)#83
lann merged 1 commit into
mainfrom
fresh-cases-positional

Conversation

@lann

@lann lann commented Aug 11, 2026

Copy link
Copy Markdown
Collaborator

The freshCases path re-found each case in its fresh instance by a linear name() scan — O(census) boundary calls per case, which at conformance scale dominates everything else in the per-case-fresh-instance loop. Measured under deltic on #82's synthetic 10k-case suite: ~3.4µs per name() call hot → ~17ms/case average (33ms worst), ~170s of pure scanning across a full 10k run. O(N²) overall.

all() order is contractually deterministic across instances of one artifact (the enumeration rule in wit/tests.wit; lockfiles already depend on it), so the census index relocates in O(1); one name() call verifies.

Semantics note: this is stricter than the scan, which silently tolerated cross-instance reordering — a contract violation. Order disagreement now throws the same unsound-run drift error a vanished case always did (message says what was found at the index).

Measured effect (deltic, 10k cases, relocating the last case):

fresh-enumerate relocate
scan 10.9ms 33.4ms
positional 6.9ms 0.0ms

Verification

  • just verify-deltic — includes the real trap → fresh-instance → relocation sequence (fixture/trap/after green after boom), tag scheduling, striping partition equality.
  • just verify-viewer — viewer selftest's synthetic freshCases cases (same-order template; empty re-enumeration must throw) pass unchanged.

Not addressed here: upstream deltic's runSuite mirrors the old harness semantics and keeps its own scan — worth an upstream issue; and the ct-runner wasmtime path already relocates positionally (session.cases.get(index)), so no change needed there.

The freshCases path re-found each case in its fresh instance by
scanning name() linearly — O(census) boundary calls per case, the
dominant cost of per-case-fresh-instance execution at conformance
scale: measured under deltic on a synthetic 10k-case suite (PR #82's
bench), the scan costs ~3.4us per name() call hot, ~17ms per case on
average (33ms worst), ~170s across a full run — an order more than
the all() re-enumeration it follows. O(N^2) overall.

Enumeration order is contractually deterministic across instances of
one artifact (wit/tests.wit enumeration rule; lockfiles already depend
on it), so the census index relocates the case in O(1); one name()
call verifies. This is stricter than the scan, which silently
tolerated cross-instance reordering — a contract violation; order
disagreement now throws the same unsound-run drift error a vanished
case always did.

Verified: just verify-deltic (incl. trap -> fresh-instance -> relocate
sequence and striping legs), just verify-viewer; the viewer selftest's
synthetic freshCases cases (same-order template, empty re-enumeration)
cover both the happy and vanished paths unchanged. Relocation of the
last case in the 10k suite: 33.4ms -> 0.0ms.

Upstream deltic's runSuite mirrors the old harness semantics and keeps
its own scan; tracked separately.
lann added a commit that referenced this pull request Aug 11, 2026
…ld vanishes

The mint benchmark's follow-up (issue #25): with the registry-build
half measured as ~3x the lift at 10k cases, try wizening the suite so
fresh instances are born with the case table built.

It works, but only by driving wasmtime-wizer 47 as a library
(Wizer::run_component takes a caller-supplied instantiate closure, so
our linker satisfies test-context — a host resource init never calls —
plus full WASI). The CLI path is blocked three ways, recorded as
finding 22: the invoke grammar rejects versioned interface qualifiers,
unknown-import stubbing cannot synthesize resource types, and composed
bundles hit 'nested components with modules not currently supported'.
The init entry is therefore a bare-named wizer-initialize export:
bench-suite's new wizer-init feature adds it as a second inline-WIT
world, merged by wasm-component-ld. keep_init_func(false) — the
default — emits an invalid component (dangling core-instance export
reference); the driver keeps it. Custom sections survive the rewrite:
scheduling and drift checks work on the wizened artifact.

Measured (findings 23-24), 10k-case suite, medians:

- wasmtime: all#1 3.15ms -> 663us (= all#2: born initialized);
  instantiate unchanged at ~19us (CoW absorbs the 122KB -> 1.29MB
  snapshot); store drop 80us -> 12us. End-to-end K=1 full-isolation
  run: 30.8s -> 7.1s sequential, 1.14s at jobs=8 — per-case isolation
  on a wizened suite now undercuts #22's shared-instance numbers.
- deltic: net ~1.5x only — all#1 6.9ms -> 2.9ms but instantiate
  0.78ms -> 2.17ms (no CoW; the active data segment is copied per
  instantiation). K>1 remains the JS-leg lever.

Also corrects finding 21's scan-cost constant: hot-loop name() is
~3.4us (a cold single call measures ~26us), so the freshCases scan
averaged ~17ms/case at 10k (33ms worst, measured), not 130ms; fixed
by positional relocation in PR #83 (33.4ms -> 0.0ms measured).

New surfaces, all out of the gates: bench-suite feature wizer-init
(default build unchanged — a pure suite world), runner feature wizer
with the required-features bin wizer-preinit (optional dep
wasmtime-wizer, absent from default builds; clippy clean under the
feature).
@lann
lann merged commit 23f0921 into main Aug 11, 2026
3 checks passed
@lann
lann deleted the fresh-cases-positional branch August 11, 2026 02:03
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant