Skip to content

Render at 10fps so an idle viewer stops burning CPU - #29

Merged
than merged 1 commit into
mainfrom
idle-render-fps
Sep 19, 2026
Merged

than merged 1 commit into
mainfrom
idle-render-fps

Conversation

@than

@than than commented Sep 19, 2026

Copy link
Copy Markdown
Owner

Bubble Tea's renderer wakes every frame and copies the full screen to compare it with the last render, even when nothing changed. The framerate is fixed at startup, so focus or blur events can't throttle it.

Idle CPU per viewer, measured over 40s:

FPS CPU
60 (default) 19cs
20 9cs
10 6cs
5 4cs

At 10fps, a keypress repaints within 100ms at most.

🤖 Generated with Claude Code

Bubble Tea's renderer wakes every frame and copies the whole screen to
compare it, even when nothing changed. At the default 60fps an idle
viewer used 19cs of CPU per 40s; at 10fps it uses 6cs.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@claude

claude Bot commented Sep 19, 2026

Copy link
Copy Markdown

Solid. Three lines, one option, correctly scoped to the stated purpose — and it's the only right lever, since bubbletea v1 fixes the framerate at program construction and gives no focus/blur throttle hook.

Correctness: no impact on any of the load-bearing invariants.

  • Watcher untouched: still w.Add(dir) on the parent, still debounced, still forwards via p.Send. FPS only governs the renderer's flush ticker, not the message loop, so no events are dropped or coalesced — Update runs per message regardless of framerate.
  • Reload/scroll-preservation path untouched. reload still captures m.vp.YOffset and restores it after SetContent (viewport clamps on shrink).
  • Width invariant untouched: renderWidth() still drives every render, so nothing can exceed pane width. The one behavioral wrinkle is transient — during a drag-resize the stale-width frame can now linger up to ~100ms longer before the WindowSizeMsg repaint lands, so the terminal reflows the old frame for an extra beat. It settles at the correct width within a frame; not the wrap bug, just slightly more visible resize noise.
  • Flash timing survives: flashDuration 450ms and lineFlashDuration 500ms are 4–5 frames at 10fps, and per docs/superpowers/specs/2026-07-31-update-pointer-design.md the flash is a single on/off with no fade frames, so there's nothing sub-100ms to swallow. The 1s tickMsg clock is likewise well clear of the frame interval.
  • WithFPS clamps out-of-range values (<1 → 60, >120 → 120), so the literal needs no guarding. build, vet, and go test ./... all pass.

Two notes, neither blocking:

a. The tradeoff you're buying is continuous-scroll smoothness. Held j/k still lands on the right offset — Update sees every keypress — but the visible motion becomes ~100ms steps instead of continuous. If that reads as choppy in the side pane, 20fps is the middle ground your own table prices at 9cs vs 6cs. Your call; 10 is defensible for a pane that's mostly idle.

b. Description nit only, not the code: bubbletea's flush short-circuits on an empty buffer, so the idle cost is the ticker wake plus a mutex acquire, not a full-screen copy per frame. The measured numbers and the conclusion stand either way, and the code comment ("wakes every frame even when idle") is accurate as written.

Leaving the picker's program at the default is right — it's short-lived and interactive, not the thing burning CPU.

@than
than merged commit 3c53d7f into main Sep 19, 2026
1 check passed
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.

1 participant