Skip to content

fix: bounded frame push prevents copy-mode scroll memory leak#190

Merged
psmux merged 1 commit into
psmux:masterfrom
ohboyftw:fix/bounded-frame-push
Apr 8, 2026
Merged

fix: bounded frame push prevents copy-mode scroll memory leak#190
psmux merged 1 commit into
psmux:masterfrom
ohboyftw:fix/bounded-frame-push

Conversation

@ohboyftw

@ohboyftw ohboyftw commented Apr 8, 2026

Copy link
Copy Markdown

Summary

  • push_frame() used an unbounded mpsc::channel per client, allocating a new String per frame per push
  • During rapid scroll events in copy mode, each scroll sets state_dirty, triggering a full frame rebuild (~500KB with cell content) pushed faster than the writer thread can flush to TCP
  • Measured: 8.5 MB → 1 GB in 1950 scroll events (22.8 MB/s, perfectly linear growth)
  • Replace unbounded channel-of-oneshots with single-slot Arc<Mutex<Option<String>>> per client — push_frame() overwrites any unconsumed frame
  • After fix: 8.5 MB → 16 MB flat across 2000 scroll events (0.2 MB/s)

Changes

  • src/types.rs: Replace FRAME_PUSH_SENDERS (unbounded channel vec) with FRAME_PUSH_SLOTS (single-slot FrameSlot vec). Dead slots pruned via Arc::strong_count. client_id tagging preserved for force-detach.
  • src/server/connection.rs: Writer thread now polls frame slot alongside command response channel (5ms timeout). Command responses still delivered reliably via resp_rx.
  • tests/test_scroll_memory.ps1: Regression test — starts detached session, injects 2000 scroll-up events via TCP, asserts peak memory < 500 MB and growth rate < 10 MB/s.

How to reproduce (before fix)

# Build without this PR, then:
pwsh tests/test_scroll_memory.ps1
# Observe linear memory growth: ~0.5 MB per scroll event

Test plan

  • cargo check — clean
  • cargo test — all tests pass
  • pwsh tests/test_scroll_memory.ps1 — 6 passed, 0 failed (peak 15.9 MB, 0.2 MB/s)
  • Manual: attach to session, scroll rapidly with mouse wheel, verify memory stays bounded

push_frame() used an unbounded mpsc::channel per client, creating a new
String allocation per frame per push.  During rapid scroll events in
copy mode, each scroll set state_dirty, triggering a full frame rebuild
(~500KB with cell content) pushed into the channel faster than the
writer thread could flush to TCP.

Measured: 8.5 MB → 1 GB in 1950 scroll events (22.8 MB/s, linear).

Replace the unbounded channel-of-oneshots with a single-slot
Arc<Mutex<Option<String>>> per client.  push_frame() overwrites any
unconsumed frame — only the latest frame matters for rendering.
Memory is now bounded to O(clients) instead of O(frames).

After fix: 8.5 MB → 16 MB flat across 2000 scroll events (0.2 MB/s).

Includes test_scroll_memory.ps1 regression test that starts a detached
session, injects 2000 scroll-up events via TCP, and asserts peak memory
stays under 500 MB with growth rate under 10 MB/s.
@psmux psmux merged commit 44f061d into psmux:master Apr 8, 2026
@psmux

psmux commented Apr 8, 2026

Copy link
Copy Markdown
Owner

Hey Aravind, really appreciate you taking the time to dig into this. Excellent work!

I reproduced it myself before merging and the numbers matched almost exactly what you reported. On master, 1000 scroll events with backpressure took the server from 8.7 MB to 597 MB (about 602 KB per event). With your single slot fix in place, same scenario, it stayed flat at around 13 MB total. Night and day difference.

The root cause analysis was spot on. The unbounded mpsc channel accumulated frame allocations faster than the writer thread could flush them to TCP, and with no backpressure mechanism the only limit was available RAM. Replacing it with an overwrite slot that keeps only the latest frame is exactly the right approach since intermediate frames are irrelevant for rendering anyway.

Reviewed the full diff carefully:

  • No Cargo.toml changes (just the 3 files you listed)
  • All 726 existing tests pass with zero regressions
  • The regression test script (test_scroll_memory.ps1) is well structured and thorough

Merged and built. Thanks for the clean PR and the detailed write up. This is a solid contribution.

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.

2 participants