Skip to content

feat(history-import): benchmark ConversationView open/scroll/stream against the Mirror baseline (phase 1b-C) - #365

Open
zxch3n wants to merge 2 commits into
feat/conversation-view-rendererfrom
feat/conversation-view-benchmarks
Open

feat(history-import): benchmark ConversationView open/scroll/stream against the Mirror baseline (phase 1b-C)#365
zxch3n wants to merge 2 commits into
feat/conversation-view-rendererfrom
feat/conversation-view-benchmarks

Conversation

@zxch3n

@zxch3n zxch3n commented Sep 3, 2026

Copy link
Copy Markdown
Contributor

Summary

Phase 1b-C: benchmarks that pin the before/after of the ConversationView work (stacked on #364), plus two view fixes the benchmark surfaced.

  • packages/history-import/benchmarks/open-conversation.bench.ts now defaults to the deterministic synthetic replay (--turns, default 120 user turns → 240 turns) at --scale=1,10, keeps --fixture=<file> for a desensitized bench:capture (never committed), and adds view tasks next to the Mirror baseline: view.open (import + view + tail hydrate → one renderable row per turn), view.readAll (what a reader still on the doc.history bridge pays on first access), view.scroll (a 30-turn ensureRange window advancing 20 times, p99), view.stream (100 text deltas into the tail turn with the view attached, p99) and view.append. View tasks run before the baseline so the Mirror's garbage is not charged to them; the summary prints the acceptance checks.
  • View: rebuildIndex / evict resolved positions with ids.indexOf per turn (O(n²) per structural change; an append at 2,400 turns took seconds), now a position map. itemCount is read for assistant turns only and planCount only when a plan exists, which trims two of the five wasm calls per turn at open.

Numbers (M-series laptop, bench:open, mean unless noted; before = full-schema Mirror open, after = view.open)

Fixture Turns / items / containers before: Mirror open after: view.open view.readAll (bridge first read) view.scroll 30 turns p99 view.stream p99
synthetic ×1 240 / 1,080 / 1,921 49.9 ms 6.3 ms (7.9×) 22.9 ms 4.3 ms 0.22 ms
synthetic ×10 2,400 / 10,800 / 19,201 8.2 s (isolated run) – 40 s (in the full run, GC-bound) 46.2 ms (≥178×) 518 ms 17.2 ms 0.18 ms
real, desensitized ×1 57 / 5,672 / 8,157 2,210 ms 90.0 ms (24.6×) 266 ms 148 ms 0.08 ms
real, desensitized ×10 570 / 56,720 / 81,561 (48.5 MiB snapshot) failed — loro-mirror throws unreachable (wasm) while materializing; doc.toJSON() alone is 5.2 s 199 ms (117 ms of it is LoroDoc.import of the snapshot; no baseline to compare) 5.1 s 406 ms 0.08 ms

Acceptance (from the phase 1b task)

  • open ≤ 50 ms at ×10 (~2,400 turns): met on the synthetic ×10 fixture (46.2 ms). On the desensitized real fixture the turns average ~100 items each and view.open is 90 ms at ×1 and 199 ms at ×10 (117 ms of which is decoding the 48.5 MiB snapshot): the cost is the eager hydration of the 20-turn tail (toJSON of ~2,000 items), not the index. Not adjusting the criterion; the item-heavy shape is reported as is.
  • stream p99 ≤ 4 ms: met on every fixture (0.08–0.22 ms).
  • view.scroll (30-turn window) p99 is 4–17 ms on the synthetic fixtures and 148–406 ms on the item-heavy real fixture (~100 items per turn; a window is ~3,000 items of toJSON). The renderer hydrates around the viewport with a 2-screen prefetch, so a scroll normally hits already-hydrated turns; a cold far jump on such a session pays this once.
  • The real ×10 Mirror failure means the before path cannot open that document at all; the view path does, in 199 ms. Root cause isolated and filed upstream as Container-by-container reads on a large imported doc (LoroMap.get / LoroList.get / LoroText.toJSON) retain ~4 KB wasm memory per container, scale superlinearly, and end in a bare RuntimeError: unreachable at 4 GiB loro-dev/loro#1092: container-by-container reads (LoroMap.get / LoroList.get / LoroText.toJSON, which is how loro-mirror builds its initial state) retain ~4 KB of wasm memory per container and scale superlinearly, so a doc of ~1M containers, or a few docs of ~330k containers alive at once, exhausts the 4 GiB wasm32 limit and traps with a bare RuntimeError: unreachable. doc.toJSON() on the same docs needs tens to hundreds of MiB. The standalone repro (synthetic doc, no fixture data) is in the issue.

🤖 Generated with Claude Code

@zxch3n
zxch3n force-pushed the feat/conversation-view-benchmarks branch from e50d078 to e36b775 Compare September 4, 2026 00:02
…gainst the Mirror baseline (phase 1b-C)

`bench:open` now defaults to the deterministic synthetic replay at
`--scale=1,10` (`--fixture` keeps taking a desensitized capture) and measures
the view next to the full-Mirror baseline: `view.open` (import + view + tail
hydrate -> one renderable row per turn), `view.readAll` (the `doc.history`
bridge's first read), `view.scroll` (a 30-turn `ensureRange` window advancing
20 times, p99), `view.stream` (100 text deltas into the tail turn with the
view attached, p99) and `view.append`. View tasks run before the baseline so
the Mirror's garbage is not charged to them, and the summary prints the
phase 1b acceptance checks (open <= 50 ms at x10, stream p99 <= 4 ms).

Two view fixes the benchmark surfaced:
- `rebuildIndex` / `evict` resolved positions with `ids.indexOf` per turn,
  O(n^2) per structural change (an append at 2,400 turns took seconds); they
  use a position map now.
- `itemCount` is read for assistant turns only and `planCount` only when a
  plan exists, which trims two of the five wasm calls per turn at open.

Model: claude-fable-5-1
…rashing the bench summary

The full-schema Mirror throws `unreachable` on the desensitized real fixture at x10
(570 turns, 56k items); the reporter now prints the error per task and in the summary.

Model: claude-fable-5-1
@zxch3n
zxch3n force-pushed the feat/conversation-view-benchmarks branch from e36b775 to be3c121 Compare September 4, 2026 00:03
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant