# Streaming / virtualization — measured baseline
Captured **before** any change, on `feat/virtualized-layout` off `main` (`73154a7`), via
`cargo bench --bench virtualization`. These numbers justify every subsequent change; re-run the
same bench after each one and compare.
Fixture: a 65-char log line, laid out no-wrap (one block = one visual line), NotoSans 16px.
## 1. Cost of appending ONE line to an N-line document
| 1 000 | 10.9 ms | 10.4 µs | 1 046× |
| 10 000 | 113.0 ms | 9.7 µs | 11 655× |
| 100 000 | **1.167 s** | 10.0 µs | **117 007×** |
- `full_relayout` = `FlowLayout::layout_blocks(N)` — the path a consumer is forced onto today,
because a block-count change invalidates the layout and there is no tail-append entry point.
Clearly **O(N)**: ~10× cost per 10× lines. At 100 k lines, appending a single line costs
**over a second**.
- `incremental` = `FlowLayout::add_block(1)` onto an already laid-out flow. **Flat ~10 µs
regardless of N** — already O(1); that 10 µs is just the irreducible cost of shaping one line.
Margin collapsing only inspects `flow_order.last()`, and the map/vec inserts are amortized O(1).
**Conclusion:** the O(1) primitive already exists and is fast. What is missing is only the
`DocumentFlow`-level entry point to reach it, plus a consumer that doesn't force a full relayout.
## 2. Cost of keeping N lines resident (scrolled to the tail, 800×600 viewport)
| 1 000 | 154.1 µs | 10.8 MB | 11 374 B |
| 10 000 | 162.6 µs | 68.1 MB | 7 139 B |
| 100 000 | 219.5 µs | **622.9 MB** | 6 531 B |
(`bytes/line` at 1 k is inflated by fixed overhead — font data and the glyph atlas. The *marginal*
cost is ≈ 6.5 KB per resident shaped line.)
- **Render is a non-issue.** 154 µs → 219 µs while N grows 100×: nearly flat. `build_render_frame`
scans all of `flow_order` linearly, but a range comparison per item is trivial next to emitting
the ~30 visible lines' glyphs. At 100 k that is ~1.3 % of a 16.7 ms frame budget.
**`render/frame.rs` therefore needs no change.**
- **Memory is the binding constraint.** ≈ 6.5 KB per resident shaped line means a fully-resident
100 k-line document costs **623 MB**. A bounded resident set is viable only to ~10 k lines
(68 MB); it does **not** reach the 100 k+ target.
**Conclusion:** appending fast is necessary but not sufficient. Keeping every line *shaped* is what
explodes; only laying out a viewport-sized window (memory O(window), not O(N)) reaches the target.
Raw text is cheap by comparison — 65 B/line in the rope, ~6.5 MB at 100 k.
## What this implies for the design
1. `DocumentFlow::add_block` — a thin wrapper over the already-O(1) `FlowLayout::add_block`.
Kills the 1.17 s/line cliff. Low risk.
2. `DocumentFlow::layout_window` (uniform row height, arithmetic `y = idx * row_height`) — required
by the **memory** result, not by render. Keeps only a window shaped. This is the one genuinely
new code path; it is additive and opt-in, and existing `layout_full`/`layout_blocks`/
`relayout_block` are untouched, so `RichTextEditor` keeps its current behaviour exactly.
3. `DocumentFlow::remove_leading` — trims the resident window's front. Absolute `y` values of
survivors are left untouched (the region above simply becomes empty and is scrolled past), so
this is O(n_removed) plus a `Vec` memmove — no reshape, no y rewrite.
---
# After
Same bench, after the change. Both goals met, and both are now **flat in N**.
## Appending one line (`DocumentFlow::add_block`)
| 1 000 | 11.5 ms | 11.3 µs | 1 018× |
| 10 000 | 117.6 ms | 10.4 µs | 11 313× |
| 100 000 | 1.175 s | 10.7 µs | **109 386×** |
Flat at any N — the cost is shaping the one new line and nothing else. The measurement is of
`append_block` (base-overlay capture included), not the raw `add_block`, so the ~0.7 µs the base
clone adds is counted.
## Holding a large document (`DocumentFlow::layout_window`, 100-row window)
| 1 000 | 966 µs | 3.7 MB | 10.8 MB |
| 10 000 | 960 µs | 3.7 MB | 68.1 MB |
| 100 000 | 965 µs | **3.7 MB** | 622.9 MB → **168× less** |
Memory is flat in N: only the window is shaped, and `content_height` still spans the whole document,
so the scrollbar stays honest. Window layout is likewise flat (O(window), not O(N)). The residual
3.7 MB is font data and the glyph atlas — fixed overhead, not per-line.
## Regression
446 → 458 tests passing, 0 failed (12 added). Clippy and rustfmt clean. Every addition is a new
method; `layout_full` / `layout_blocks` / `relayout_block` / `add_block` are untouched, so
`RichTextEditor`'s path is byte-for-byte what it was.