Skip to content

fix: keep PTY output flow control bounded - #66

Open
htazq wants to merge 1 commit into
GOODBOY008:mainfrom
htazq:fix/pty-output-flow-control
Open

fix: keep PTY output flow control bounded#66
htazq wants to merge 1 commit into
GOODBOY008:mainfrom
htazq:fix/pty-output-flow-control

Conversation

@htazq

@htazq htazq commented Jul 27, 2026

Copy link
Copy Markdown
Contributor

Summary

  • return exactly one PTY output credit for each WebSocket frame after xterm processes its RAF batch
  • immediately return credits for frames buffered by the streaming UTF-8 decoder without producing text
  • remove the cumulative 2 MiB reset() / clear() path and rely on the existing bounded xterm scrollback
  • add regression coverage for initial credits, single and batched frames, split UTF-8, sustained output, and tab activation

Root cause

The backend consumes one semaphore permit per output frame, but the frontend replenished a fixed four permits after processing any low-water batch. creditsGranted was never decremented when frames arrived and was reset immediately after granting, so a low-rate stream could grow the backend credit window by roughly three permits per frame. After a long-running session, bursts were no longer bounded by the intended initial window of two frames.

The separate 2 MiB guard counted all decoded output since the last clear, not retained terminal memory. Normal top, watch, iostat, build, or fio output eventually called both term.reset() and term.clear() even though xterm scrollback is already bounded by its configured line limit.

After this change, the invariant is: available credits plus frames awaiting xterm completion remain equal to the initial window. An incomplete UTF-8 sequence retains at most the decoder's few pending bytes and returns that frame credit immediately, avoiding deadlock.

Related work

Validation

Regression command was red on current main before the fix:

  • one processed frame: 6 Resume messages instead of 3 total
  • three batched frames: 6 instead of 5 total
  • split UTF-8 across two frames: 6 instead of 4 total
  • sustained output over 2 MiB: term.reset() called once

Passing after the fix:

  • pnpm install --frozen-lockfile
  • pnpm test -- src/__tests__/pty-terminal-activation.test.tsx src/__tests__/terminal-scrollback.test.ts src/__tests__/terminal-tab-portals.test.tsx — 12 passed
  • pnpm test — 512 passed
  • pnpm build
  • pnpm exec tsc --noEmit
  • pnpm i18n:check — 945 keys in both locales
  • pnpm exec eslint src/components/pty-terminal.tsx --quiet — 0 errors
  • git diff --check

Repository-wide pnpm lint remains blocked by the pre-existing src/components/settings-modal.tsx:308 no-unnecessary-type-assertion error; this PR does not touch that file. Local Rust/Tauri commands are blocked by this Windows host's application-control policy (os error 4551) when Cargo executes newly generated build scripts. The PR CI runs cargo test on Windows, macOS, and Ubuntu.

Performance boundary

The affected long-running app process was no longer active when the patch was built, and there was no historical PerfMon/WPR series, so this PR does not claim a measured renderer CPU or memory reduction. It fixes the deterministic credit-window expansion and destructive clear. Background terminals intentionally remain mounted and continue parsing output; their bounded per-terminal xterm/WebGL cost is unchanged.

Risk / rollback

The change reduces the maximum in-flight output to the already documented two-frame window. If xterm stops completing writes, output now backpressures instead of accumulating permits. Rollback is the single commit in this PR.

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.

Bug: terminal clears all scrollback after only 2 MiB of cumulative output

1 participant