Skip to content

perf(tui): per-entry display-row-count cache for wrap-mode scroll bounds - #76

Merged
edTheGuy00 merged 5 commits into
mainfrom
feat/75-row-count-cache
Jul 14, 2026
Merged

perf(tui): per-entry display-row-count cache for wrap-mode scroll bounds#76
edTheGuy00 merged 5 commits into
mainfrom
feat/75-row-count-cache

Conversation

@edTheGuy00

Copy link
Copy Markdown
Owner

Closes #75.

What

In wrap mode the scroll-bounds pass (total_lines) grapheme-walked the entire filtered buffer every frame (~20 fps) — correct since #74, but O(buffer) forever in steady state. This adds a per-entry display-row-count cache so unchanged entries cost one HashMap lookup, plus it closes the last deferred #73-class drift (message-line link badges) and takes the small alignments deferred from #74's review.

How

  • Lookup-time keying, zero handler invalidation (design validated by research before implementation): a global key (content width, wrap_mode) clears the map wholesale on mismatch; every hit compares the cached expanded flag against the entry's current collapse state. This is sound because rendered char content depends only on immutable entry text + expanded state + link badges — level and search decoration are style-only, and show_timestamps/show_source are hardcoded (documented as future global-key inputs if ever wired to config).
  • Linked entries bypass the cache and are measured exactly from their formatted lines (message badge included — the deferred drift). Bounded by construction: links are viewport-scoped, ≤35.
  • Pruning is memory hygiene, not correctness: retain(id >= front_id) when the map outgrows 2 × filtered_len + 64; a fully-emptied buffer (clear-logs) clears the cache outright in the empty early-return, which skips the pruning pass.
  • Both render_inner call sites (total_lines loop + render loop) share one entry_display_rows_cached helper; the cache-miss path is byte-identical to fix(tui): wrap-mode scroll bounds count wrapped stack-frame lines exactly #74's math.
  • Ride-alongs: FRAME_INDENT_WIDTH/ASYNC_GAP_TEXT_LEN derived from source instead of bare literals; collapse-visibility rules extracted into one helper shared by all three consumers; docs/REVIEW_FOCUS.md registry entry for the new render-written fields (per the approved-exception process), including the max_collapsed_frames assumed-constant note and the pruning-bound caveat.

Tests (9 new)

Sentinel probe proving the second render reads the cache (mutation-verified: disabling the cache-read branch fails it); coherence tests for width change, wrap toggle, expand/collapse, link-mode enter/exit, and eviction (each asserting estimate == rendered ground truth); load-bearing message-badge-at-wrap-boundary test (fails if the bypass is removed); pruning threshold test; empty-buffer cache-clear regression test (verified red against the pre-fix code). All #73/#74 tests unchanged and green.

Full suites: fdemon-tui 1615, fdemon-app 3370; fmt clean; clippy -D warnings clean on both stable 1.96 and 1.97.

Docs

Plan, verified research, and review record under workflow/plans/features/row-count-cache/ and workflow/reviews/row-count-cache/ (verdict: APPROVED_WITH_CONCERNS, zero confirmed Critical/Major; all actionable minors fixed in-branch).

Out of scope (pre-existing, will file separately)

Link-mode staleness: rescan_links_if_active doesn't run on filter changes, and DetectedLink.entry_index is a raw buffer index that shifts on eviction — formatter and estimate are affected identically, so estimate == render still holds.

- Clear row_cache when the buffer empties (clear-logs): the empty-buffer
  early return skipped the pruning pass, and the unwrap_or(0) fallback
  made retain a no-op — a large pre-clear cache lingered indefinitely.
  Regression test drives the buffer to empty and asserts a cleared cache.
- row_cache_key second element now writes self.wrap_mode instead of a
  hardcoded literal true.
- Reword the three 'registry entry pending (task 02)' doc comments — the
  REVIEW_FOCUS.md entry landed in 671ddb37.
- collapsed_indicator_widths uses FRAME_INDENT_WIDTH instead of a bare 4.
- debug_assert at the u16 rows insert so pathological row counts fail
  loudly in test builds instead of silently saturating.
@edTheGuy00
edTheGuy00 merged commit 98027ea into main Jul 14, 2026
3 checks passed
@edTheGuy00
edTheGuy00 deleted the feat/75-row-count-cache branch July 14, 2026 17:51
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

log_view: per-entry row-count cache + deferred minors from #73 fix (message-line badges, constant derivation)

1 participant