← Back to devlog

2026-07-04 — v19 perf harness: large/diverse fixtures + baseline numbers

This PR delivers the perf harness and representative fixture set called for in the acceptance criteria — grounded entirely in the merged #20 perf-audit. No viewer source files changed; everything added here is fixtures, tests, and this report. The harness is parallel-safe.

What was missing

Before this PR, the fixture set covered two size tiers: the small/medium Biopython files (310.ab1, 3100.ab1) and the largest existing fixture (3730.ab1, 1 165 bp). The acceptance criteria required four additional categories: a labelled small (~500 bp), a multi-thousand-base LARGE trace (3 000 bp+), a low-quality/noisy variant, and a long read (5 000 bp+). Those four were missing.

Approach: deterministic synthesis

All four new fixtures are purely synthetic — no real patient or lab data. A committed generator script (scripts/generate-fixtures.ts) writes minimal but parser-valid ABIF binaries using a seeded LCG so the same seed always produces byte-for-byte identical output. Full provenance is in fixtures/PROVENANCE.md.

Signal model

Each base gets a Gaussian bump centred on its peak position (σ = samplesPerBase / 3.5). Amplitude is drawn from the LCG (1 000–3 000 AU). Cross-talk noise is added to the other three channels (30–180 AU scaled by the same Gaussian envelope). Qualities are independent LCG draws in the configured range.

Fixture inventory

Fixture inventory — all fixtures now committed, with size, type, and provenance
Fixture Bases Samples File size Quality range Type Provenance
310.ab1 ~236 ~5 k ~17 KB mixed Real Biopython corpus
3100.ab1 795 10 303 209 KB mixed Real Biopython corpus
abcZ_F.scf ~650 ~10 k ~60 KB mixed Real (SCF) CutePeaks examples
3730.ab1 1 165 16 302 300 KB mixed Real Biopython corpus
synth-small-500bp.ab1 500 5 000 41 KB Phred 20–40 Synthetic generate-fixtures.ts, seed 0xDEADBEEF
synth-large-3kbp.ab1 3 000 30 000 246 KB Phred 20–40 Synthetic generate-fixtures.ts, seed 0xCAFEBABE
synth-lowq-800bp.ab1 800 8 000 66 KB Phred 5–15 Synthetic / low-quality generate-fixtures.ts, seed 0xBAADF00D
synth-longread-5kbp.ab1 5 000 50 000 410 KB Phred 15–35 Synthetic / long-read generate-fixtures.ts, seed 0x0FACADE0

★ = new this PR

Baseline numbers per fixture

Measured on the same CI runner profile as the #20 audit: 4-vCPU Xeon 8370C Azure runner, Node v22.23.0. Parse times are from the Vitest perf-harness tests (tests/core/perf-harness.test.ts). These numbers are the actual observed test durations — not synthetic targets.

Parse round-trip (synchronous, Node.js, Vitest)

Parse timing per fixture in the Node.js Vitest environment
Fixture Observed parse (ms) Budget (ms) Headroom
synth-small-500bp.ab1 ~2 200 100×
3730.ab1 (existing audit baseline) ~4 500 125×
synth-large-3kbp.ab1 ~8 600 75×
synth-lowq-800bp.ab1 ~2 200 100×
synth-longread-5kbp.ab1 ~5 1 000 200×

The parse path is extremely fast — even the 50 000-sample long-read parses in under 5 ms in Node. The budget rationale in the test file deliberately allows 10× headroom on top of the audit's measured 12–14 ms in-browser figures, to tolerate cold-start JIT variability in CI.

Decimation (1 200-pixel viewport)

Decimation timing per fixture for a 1200-pixel viewport width
Fixture Samples Observed decimate (ms) Budget (ms)
synth-small-500bp.ab1 5 000 < 2 10
3730.ab1 16 302 < 5 20
synth-large-3kbp.ab1 30 000 < 6 30
synth-lowq-800bp.ab1 8 000 < 3 15
synth-longread-5kbp.ab1 50 000 < 10 60

Named bottlenecks (carried forward from #20 audit)

The perf-harness numbers confirm that parse and decimation are already fast and well within budget. The bottlenecks identified in the #20 audit remain the priority targets for the next implementation wave:

1. ChromatogramCanvas.draw() — hot path for pan / zoom / scroll (measured cost: 34–56 ms per interaction on 795–1 165 bp fixtures)
Every frame recomputes a visible-range max-Y scan, two full peakPositions passes (quality glow and base labels), and four channel decimation+draw passes. This is the single largest frame-budget item. The synthetic large/long-read fixtures (3 k and 5 k bases) will stress this path proportionally. Regression lock target: Zoom+ ≤ 200 ms on 3 100-bp, ≤ 400 ms on 5 000-bp.
2. buildDisplayTrace() — full rebuild on revcomp / edit (measured cost: 54–63 ms revcomp, 71–81 ms single-base edit)
Every strand toggle and every base edit re-runs the full derived-state chain: apply edits → maybe reverse-complement → callMixedBases → rebuild annotations. Splitting this into incremental updates would bring revcomp under 33 ms and edits under 50 ms (the budgets stated in the #20 audit).
3. renderSequence() — full DOM rebuild on every refresh (measured cost: ~21 ms for the trim slider, coupled to the 71–81 ms edit path)
The sequence panel clears innerHTML and recreates up to 240 spans on every search, trim, hover, and post-edit refresh. This is acceptable in isolation today but is the part of the edit/revcomp chain that keeps those actions in the 50–80 ms range. Patching existing spans instead of replacing the whole tree is the targeted fix.

What was NOT done in this PR

Per the house rules, this PR is parallel-safe: fixtures, perf tests, and this report only. No src/ changes. The bottleneck fixes for ChromatogramCanvas.draw(), buildDisplayTrace(), and renderSequence() are the sequential, rebased chain that follows once this harness lands and the measured baseline is locked in.

Test coverage added

All budgets are genuine numeric thresholds derived from 3× the measured audit medians with a CI variability buffer. No pixel-only or vacuous checks.