fix: invalidate merge_cache on scrollback-capacity drain (#405) - #458
Merged
Merged
Conversation
Once primary-screen scrollback is at capacity, every line feed pushes a new row at the bottom and then `enforce_scrollback_limit` drains one row from the front in the same logical step, netting `self.rows.len()` unchanged. Because `visible_window_bounds` (and therefore the `MergeWindowFp` keying `Buffer::merge_cache`) derives purely from `rows.len()` and `height`, the fingerprint the next flatten computes is numerically identical to the one the stale `merge_cache` was built against -- even though every surviving row's identity just shifted down by `overflow`. The incremental merge fast path then served a stale, off-by-`overflow` prefix: an old top line frozen at the top of the window while genuinely-new content below it was dropped. This is the same confined in-place row-rotation bug class already guarded at the three `scroll.rs` sites (`scroll_slice_up`, `scroll_slice_down`, `scroll_up`), each of which sets `self.merge_cache = None;` at its rotation point. `enforce_scrollback_limit` was the missed fourth site. The precondition is a full scrollback (e.g. `cat` a large file, then run a program that rewrites lines while emitting new ones such as `pre-commit run --all-files`); below capacity `rows.len()` grows each line feed so the fingerprint changes and a correct full merge is forced. The divergence is silent in release builds -- the `debug_verify_against_oracle` cross-check that catches it is `#[cfg(debug_assertions)]` only. Fix: null `merge_cache` in `enforce_scrollback_limit`'s `overflow > 0` branch (never in the `rows.len() <= max_rows` early return, which drains nothing and must not pay this cost per line feed). Correct the `merge_cache` field doc, which previously (and incorrectly) claimed this site was covered by fingerprint mismatch; `erase_scrollback` remains genuinely fp-covered (it always collapses `visible_start` to 0, so the bounds always change) and is documented as such. This is the correctness fix. It forces a full-window re-merge on every line feed at steady-state capacity, giving up the incremental fast path in that workload; the structural shift-in-place follow-up is tracked in #457. New bench `bench_lf_flatten_at_capacity` establishes the before/after baseline (~36 us at height 24, ~66 us at height 100). Regression test: `incremental_merge_matches_oracle_after_scrollback_capacity_rotation` drives many line feeds past the scrollback cap, re-flattening after each and cross-checking every result against the independent full-merge oracle. It panics on current code without this fix.
|
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configurationConfiguration used: Organization UI Review profile: CHILL Plan: Pro Plus Run ID: 📒 Files selected for processing (4)
📝 WalkthroughWalkthroughThe change invalidates ChangesScrollback merge-cache correctness
Estimated code review effort: 3 (Moderate) | ~20 minutes Possibly related issues
Possibly related PRs
🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
Comment |
Codecov Report✅ All modified and coverable lines are covered by tests. 📢 Thoughts on this report? Let us know! |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What
Fixes a visual buffer desync where, during long-running output at full
scrollback (e.g.
pre-commit run --all-filesaftercat-ing a large file),the terminal window would freeze an old line at the top and drop
genuinely-new content, becoming unusable until the pane was killed.
Root cause
Buffer::merge_cache(the incremental flatten-merge cache from #405 Part C)is keyed by
MergeWindowFp { visible_start, visible_end, auto_detect }, whosebounds derive purely from
rows.len()andheight.Once scrollback is at capacity, each line feed pushes a row at the bottom and
then
enforce_scrollback_limitdrains a row from the front in the same logicalstep — netting
rows.len()unchanged. The fingerprint therefore staysidentical across the rotation while every row's identity shifts down by
overflow, so the incremental fast path serves a stale, off-by-overflowprefix.
This is the same "confined in-place row rotation" bug class already guarded at
the three
scroll.rssites (scroll_slice_up/scroll_slice_down/scroll_up), each of which nullsmerge_cache.enforce_scrollback_limitwasthe missed fourth site.
The divergence is silent in release — the
debug_verify_against_oraclecross-check that catches it is
#[cfg(debug_assertions)]only.Fix
merge_cacheinenforce_scrollback_limit'soverflow > 0branch(never in the
rows.len() <= max_rowsearly return, which drains nothing).merge_cachefield doc, which previously (wrongly) claimed thissite was covered by fingerprint mismatch.
erase_scrollbackremainsgenuinely fp-covered (always collapses
visible_startto 0) and is documentedas such.
Tradeoff (tracked follow-up: #457)
This is the correctness fix. At steady-state capacity it forces a
full-window re-merge every line feed, giving up the incremental fast path in
that workload. The structural shift-in-place fix (rotate the cached merge
rather than discard it) that buys the performance back is tracked in #457 — it
is a correctness minefield (tag-splitting across the dropped boundary,
url_tag_indicesrebasing,overflow > 1) and deliberately deferred so it canbe done carefully. There is no user-facing correctness bug left open.
Tests & benchmarks
incremental_merge_matches_oracle_after_scrollback_capacity_rotation(drives many line feeds past the cap, cross-checks every flatten against the
independent full-merge oracle). Panics on
mainwithout this fix; green withit.
bench_lf_flatten_at_capacityestablishes the before/afterbaseline for perf: structural shift of merge_cache on scrollback drain (replace null-on-drain) #457: ~36 µs at window height 24, ~66 µs at height 100 (the cost
scales with height, confirming it is the full-window re-merge).
Verification
cargo test --all— green (625 freminal-buffer lib tests + all integration)cargo clippy --all-targets --all-features -- -D warnings— cleancargo machete— cleancargo fmt --all -- --check— cleancargo xtask check-windows— cleanProvenance
Diagnosed and fixed via an orchestrated multi-agent pass: read-only exploration
of the flatten/merge-cache and snapshot-windowing paths, a proven headless
reproduction, and an adversarial review of both the diagnosis and the patch
(which caught a
iter_batchedvsiter_batched_refbenchmark methodology bug,since fixed).
Summary by CodeRabbit
Bug Fixes
Tests
Performance