Skip to content

Keep long sessions responsive: block-grouped scrollback, longer inbound slices, O(1) hasFocus, buffered VFS appends - #463

Merged
Delwing merged 3 commits into
masterfrom
claude/fix-issue-457
Oct 8, 2026
Merged

Delwing merged 3 commits into
masterfrom
claude/fix-issue-457

Conversation

@Delwing

@Delwing Delwing commented Oct 8, 2026

Copy link
Copy Markdown
Collaborator

Fixes #457. In long sessions, output and trigger replies fell seconds behind once scrollback passed about 10k lines, and hasFocus() and io appends got slower over time.

Changes

  • Scrollback in blocks (§1): main-console rows are grouped into blocks of 100 (src/ui/output/outputRows.ts). content-visibility: auto now sits on each finished block instead of on every row, so the browser tracks about a hundredth as many elements per frame. All code that walks the rows goes through the new helpers: cursor ops, eviction, the sticky area, caret mode, user windows and miniconsoles. This is the issue's fallback suggestion, not full virtualisation.
  • Longer inbound slices (§2): MudClient now handles incoming data in slices of up to 50 ms, or 12 ms when input is waiting (isInputPending), instead of always 12 ms. Large bursts trigger far fewer layout and paint frames, and the order of the stream is unchanged.
  • Cheap hasFocus() (§3): it checks document.activeElement.closest('.command-input') instead of searching the whole document, so its cost no longer grows with scrollback.
  • Buffered appends (§4): ProfileVFS holds appends to existing files in memory, so chat logging (as EMCO does) no longer rewrites the whole file on every line.
    • The held data is written out after 500 ms, at 256 KB, and before any read, stat, rewrite, move or removal of the file.
    • It is also written when the page is hidden, frozen or unloaded.
    • The freeze listener was added in review: a hidden tab can be discarded without a pagehide event.
  • Tests: new scrollbackBlocks457, hasFocusCost457 and vfsAppendTail457, plus updates to inboundSlices and scrollbackRenderCost443.

Overlap

#456's PR also edits src/ui/output/OutputRenderer.ts, but not the same functions, and the two merge cleanly. Whichever lands second should rerun splitViewEchoBurst456.

Testing

  • yarn typecheck passes.
  • yarn test: 2 failures, neither caused by this branch:
    • serverEncoding.test.ts (GBK euro sign) also fails on master.
    • mapperParity295 "stays fast on a large grid" is a timing test that fails only under load and passes when run alone.

🤖 Generated with Claude Code

https://claude.ai/code/session_01Um3ZSHRbGj1Rbf55rAQzdM


Generated by Claude Code

claude added 3 commits October 8, 2026 14:23
…(1) hasFocus, buffered appends (#457)

- Scrollback rows now go into blocks of 100 (src/ui/output/outputRows.ts),
  and content-visibility sits on the full blocks instead of on every row.
  Blink tracked every row with its internal intersection observer, which
  became the largest cost per frame once scrollback passed ~10k lines. The
  trigger cursor, eviction, the split view, caret mode, scripted scrolling
  and OSC 8 line deletion all walk rows through the new helpers. Eviction
  drops whole blocks or one Range per partial block, and never leaves an
  empty block behind. The open tail block is never skipped, so its height
  stays exact while the reader is scrolled up.
- Inbound slices now run up to 50 ms, or 12 ms when isInputPending reports
  waiting input, instead of yielding every 12 ms. Each yield could let a
  render frame through, so a large burst paid for a frame on nearly every
  slice.
- hasFocus() now asks activeElement.closest('.command-input') instead of
  running a document-wide querySelector, which walked every scrollback row.
- ProfileVFS keeps bytes appended to an existing file in memory and writes
  them in one go: after 500 ms, at 256 KB, before any read, stat, rewrite,
  rename or removal of the file, on flush/unmount, and when the page is
  hidden or closed. ZenFS rewrote the whole file on every append. stat()
  and exists() count the held tail, so io.open(f, "a") never forces a
  flush.

Fixes #457

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014UBzjfaJoyFC6yixLRLJ7z
A hidden tab can be frozen and then discarded without a pagehide, and a
frozen page runs no timer, so a tail appended after the tab was hidden
was never written out.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Um3ZSHRbGj1Rbf55rAQzdM
@Delwing
Delwing merged commit 244871d into master Oct 8, 2026
10 checks passed
@Delwing
Delwing deleted the claude/fix-issue-457 branch October 8, 2026 23:26
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

2 participants