# Inkhaven 1.2.18 — Performance pass + reader-experience

**Status:** active scope (locked 2026-06-01)
**Cycle theme:** turn the 1.2.17 TTS Piper transition
into something a reader can actually consume, after
first making sure the editor stays fast at 10K
paragraphs.

## Motivation

1.2.15–1.2.17 each shipped substantial user surfaces
(stability, intelligence dashboard, TTS Piper).  The
codebase has grown without a baseline perf measurement
since the early 1.2.x cycles; the I.1 performance pass
was deferred from 1.2.16 and 1.2.17 to keep those
cycles release-ready.

1.2.18 closes that debt first, then layers a
reader-experience headline on top: ePub export +
audiobook draft (.m4b) + reading-time chip + reader-
pace preview.  Reader-experience features hit every
paragraph in a project, so they benefit directly from
I.1's hotspot fixes — the perf work pays for itself
in the same release.

## Locked design decisions

These were resolved through brainstorming + question-
and-answer on 2026-06-01.  They constrain the rest of
the plan.

1. **Benchmarks live under `benches/` with criterion.**
   Standard Rust bench framework; auto-handles warmup
   + statistical analysis + regression reporting.
   Adds `criterion` as a dev-dep (acceptable cost for
   the gates).
2. **CI regression threshold = 20%.**  Generous enough
   that timing noise doesn't trip on every PR; tight
   enough that real regressions surface.  GitHub
   Actions runs the bench suite on PRs and posts
   per-scenario deltas in a comment.
3. **The 10K-paragraph fixture is generator-only.**  A
   `tools/gen-fixture` binary produces ~50 MB of
   synthetic project data under any chosen path.  The
   fixture is NOT committed to the repo (~50 MB
   bloat); CI regenerates it before each bench run
   (~30s overhead).
4. **Cycle scope: I.1 + reader-experience.**  I.1 first
   (fixture + baseline + analysis + 3–4 hotspot fixes
   + CI gates), then the reader-experience phases.  T.6.b
   background voice downloads + T.4.b MD5 verification
   absorbed as side deliveries during the reader-
   experience work (audiobook draft needs non-blocking
   synth anyway).

## Architecture

### Bench harness (criterion-based)

```
benches/
  startup.rs          # cold + warm startup → first-frame
  tree_scroll.rs      # 1000-row Page Down on the 10K fixture
  search.rs           # one semantic + one lexical query
  ai_envelope.rs      # RAG retrieval + prompt build (no LLM call)
  paragraph_save.rs   # save round-trip including io_atomic + embeddings
```

Each bench depends on a pre-generated fixture pointed
at via `INKHAVEN_BENCH_FIXTURE=<path>` env var.  A small
helper (`benches/common.rs`) handles setup +
fixture discovery.

```bash
# CI flow
$ cargo run --release --bin gen-fixture -- /tmp/inkhaven-bench-fixture
$ INKHAVEN_BENCH_FIXTURE=/tmp/inkhaven-bench-fixture \
    cargo bench --bench startup --bench tree_scroll \
                --bench search --bench ai_envelope --bench paragraph_save
$ tools/bench-report.rs --baseline main --threshold 0.20
```

### Fixture generator

`tools/gen-fixture/main.rs` produces:

* 5 user books × 20 chapters × 100 paragraphs = **10K
  paragraphs**.  Per-paragraph word counts sampled from
  a realistic distribution (~mean 450, stdev 150).
* 3 system books: Characters (80 entries), Places (30
  entries), Threads (12 threads with 3–8 waypoints
  each).
* 200 inline comments distributed across paragraphs
  (~2% comment-density, matching real projects).
* Tags assigned at 15% coverage (mostly chapter-level).
* Prose: markov-chain output from a public-domain seed
  corpus (Pride and Prejudice + War of the Peace),
  capped per paragraph to the sampled word count.  Not
  semantically meaningful but vocabulary-realistic
  enough to exercise tokenisation + stemming +
  embeddings.

Output: a fully-formed inkhaven project that opens with
the standard CLI.  Metadata.db + blobs.db + vectors
seeded; embeddings generated by the standard flow (the
bench setup phase, run once, can be slow — the fixture
is reusable across many bench runs).

### CI integration

`.github/workflows/bench.yml` triggered on every PR + on
push to main.  Runs:

1. `gen-fixture` (if not cached).
2. `cargo bench` for each scenario.
3. `bench-report` compares against the latest main
   baseline (stored as a workflow artefact on main).
4. Post-comment on the PR with per-scenario percent
   deltas + a ✓/✗ verdict per scenario.
5. Exit non-zero if any scenario regressed >20%.

## Phase plan

Phases sized so each ships a commit with concrete
artefacts.  Total: ~10 phases (I.1.1–I.1.last + R.1–R.4
+ side items).

### I.1.1 — Fixture generator

* `tools/gen-fixture/main.rs` (new bin).
* Public-domain seed corpus bundled in `tools/gen-fixture/seed/`.
* Tests on the size + shape distribution + idempotence
  (re-running on an existing dir cleans + regenerates).
* CLI: `cargo run --release --bin gen-fixture -- <path>`.

Test budget: 6–8 tests covering shape + counts +
deterministic randomness (seed-based RNG so CI runs are
reproducible).

### I.1.2 — Baseline harness (criterion)

* `Cargo.toml` adds `criterion` as a dev-dep.
* `benches/common.rs` — shared setup, fixture
  discovery via env var.
* `benches/startup.rs` — cold + warm startup time
  (project load → first interactive frame).
* `benches/tree_scroll.rs` — 1000-iteration Page Down
  on the tree pane (no rendering; just state mutations
  + the row materialisation logic).
* `benches/search.rs` — one semantic + one lexical
  query.
* `benches/ai_envelope.rs` — RAG retrieval + prompt
  build (mocked LLM client to skip the network call).
* `benches/paragraph_save.rs` — full save round-trip.
* Documentation in `benches/README.md` covering how to
  run + interpret + add new benches.

Test budget: minimal (criterion handles its own
machinery); 4–6 sanity tests on the common helper.

### I.1.3 — Flamegraph analysis + PERF.md

No code commit per se — investigation phase.  Run
`cargo flamegraph --release` against each scenario,
identify the top-N hotspots, write up findings as
`Documentation/PROPOSALS/PERF.md`:

* Per-scenario flamegraph SVG (committed under
  `Documentation/PROPOSALS/perf-flamegraphs/`).
* Per-hotspot section: function, % of CPU time,
  hypothesis for fix, expected delta.
* Prioritised list driving I.1.4 onward.

### I.1.4 — Lazy tree-row materialisation

Hypothesis: `Hierarchy::load` builds every Row at
startup; for 10K paragraphs that's the obvious blocker
on cold-startup time.  Switch to lazy on-demand
materialisation — only build rows for the currently-
visible viewport + a small lookahead.

Carry: cold-startup bench drops; tree-scroll bench
shouldn't regress.

### I.1.5 — Background embedding refresh

Hypothesis: save-path embedding regeneration blocks the
main thread.  Lift to a tokio worker + an internal
queue; save path returns immediately, embeddings
backfill async.

Carry: paragraph-save bench drops; search bench
shouldn't regress beyond the delay tolerance (a freshly-
saved paragraph might not appear in semantic results for
~500ms).

### I.1.6 — DuckDB write batching

Hypothesis: per-save DuckDB INSERTs vs. batched
transactions.  Defer DuckDB commits with a coalescing
window.

Carry: paragraph-save bench drops; cold-startup might
slightly increase (the load path reads outstanding
batched writes); acceptable tradeoff documented.

### I.1.7 — CI benchmark gates

* `.github/workflows/bench.yml` runs the harness on
  every PR + push-to-main.
* `tools/bench-report.rs` parses criterion JSON output,
  compares against the main baseline, posts a PR
  comment.
* Threshold: 20% regression on any scenario fails the
  build.  Per-scenario deltas always reported regardless
  of pass/fail.

### R.1 — ePub export

`inkhaven assemble --epub <book-slug>` builds a
standards-compliant ePub from the project's typst
output.  Cover image + chapter metadata + nav.xhtml +
content.opf assembled following the standard.

Subprocess `pandoc` for the typst → HTML step (already a
common dep for technical writers; document as a
soft-required external if absent).

### R.2 — Audiobook draft (.m4b)

`inkhaven assemble --audiobook <book-slug>`:

1. Loops chapter ordinals, calling
   `tts::engine.speak_to_file_blocking` per paragraph
   into a per-chapter wav.
2. Concatenates per-chapter wavs via ffmpeg.
3. Wraps as m4b with chapter metadata.

Subprocess `ffmpeg` (assume present; document required
version).

T.6.b background voice downloads absorbed here: the
assemble step downloads any missing voices via the same
tokio infrastructure, with a progress chip the
assemble-stdout reports on.

### R.3 — Reading-time chip

* `editor.reading_time_chip: true` HJSON toggle (default
  on if `editor.tts.enabled`).
* Status-bar chip showing total + remaining read time
  at the configured wpm (default 200wpm).
* Doctor scan class `paragraph-too-long` (read time >
  5min) optional.

### R.4 — Reader-pace preview

`Ctrl+B Shift+E` opens a reader-pace preview modal.
Slow-scrolls the open paragraph + adjacent ones at the
configured wpm.  Useful for catching pacing problems
that aren't visible at editor-typing speed.

### Side deliveries

These ride alongside the R.* phases without their own
phase numbers:

* **T.4.b MD5 verification** — when voices are
  downloaded as part of the audiobook assemble, verify
  the MD5 from the catalog.  Subprocess `md5sum` /
  `md5 -q` / `certutil` per platform.
* **T.6.b background voice downloads** — see R.2.

## Test budget total

Roughly 60–80 new tests across the cycle.  The 1.2.17
cycle landed at 955 passing; 1.2.18 should land around
1015–1035.

## Risk register

* **Bench flakiness.**  Timing measurements vary with
  CPU + disk load.  Mitigate: criterion's statistical
  analysis + 20% threshold + averaging over 100
  iterations per scenario.
* **Fixture realism.**  Synthetic prose may not match
  real performance characteristics.  Mitigate: per-
  paragraph word-count distribution sampled from a
  real corpus; comment + tag + thread density tuned
  to real projects' averages.
* **ePub compatibility.**  ePub readers vary in
  strictness.  Mitigate: validate output with
  `epubcheck` in CI; document the readers we tested.
* **m4b assembly cost.**  10K paragraphs × ~3s
  synthesis = ~8 hours.  Mitigate: parallel synthesis
  (limited by CPU); document expected wall-clock time.
* **Background-download cancellation.**  Tokio task
  cancellation needs care to avoid orphan curl
  processes.  Mitigate: explicit `kill_on_drop`
  on the curl child handle.

## Out of scope for 1.2.18

* **Multi-paragraph AI envelope** — tempting but
  belongs in an AI-maturity cycle (1.2.19?).
* **Character voice consistency pass** — same.
* **AI streaming into editor** — needs undo-semantics
  design; defer.
* **Hot-reload prompts.hjson** — small enough to slip
  in if time permits but not committed scope.

## Closing

I.1 first.  Reader experience second.  Side
deliveries for the TTS Piper carryovers.

Honest about the bench-flakiness risk + the m4b
wall-clock cost.  Goal: every metric in the 5-scenario
suite is either flat or down by end of cycle; ePub +
audiobook ship as user-facing headlines that make the
project something a reader (not just a writer) can
consume.
