Skip to content

fix(tui): cap session tab pulse frame rate - #39681

Closed
kitlangton wants to merge 1 commit into
v2from
fix-tab-fps
Closed

kitlangton wants to merge 1 commit into
v2from
fix-tab-fps

Conversation

@kitlangton

Copy link
Copy Markdown
Contributor

What

Cap animated session tabs at one shared 60 Hz clock instead of marking every TabPulseRenderable as OpenTUI live.

The visual animation is unchanged, but a busy tab no longer drives the renderer thousands of times per second.

Before / After

Before: the first busy tab set live: true. Although OpenCode creates one global renderer with targetFps: 60 and OpenTUI reports maxFps: 60, a pulse-only benchmark measured 1,000–3,500 renderer-reported FPS (typically 1,600–1,900). The request stayed busy for 20 seconds without emitting text or tool deltas, so this isolates the animation loop from session traffic.

After: active pulses join one module-global 60 Hz clock. The clock advances every active pulse, requests rendering through the existing global renderer, and stops when the last pulse finishes. The same benchmark measures 51–55 frames/sec with OpenTUI reporting a stable 55–56 FPS.

How

packages/tui/src/component/tab-pulse.tsx:

  • Keep TabPulseRenderable.live disabled, avoiding the uncapped OpenTUI live loop.
  • Maintain one Set of active pulse renderables and one 60 Hz timer for the process.
  • Advance all active envelopes from the same measured delta.
  • Remove stopped/destroyed pulses and clear the timer when none remain.
sequenceDiagram
    participant S as Session status
    participant P as Active pulses
    participant C as Shared 60 Hz clock
    participant R as Global renderer

    S->>P: tab becomes busy
    P->>C: join clock
    loop while any pulse is active
        C->>P: advance(delta)
        P->>R: requestRender()
    end
    S->>P: pulse/fade completes
    P->>C: leave clock
Loading

Scope

  • This is a localized workaround for the OpenTUI live scheduling path. It does not change the global renderer configuration.
  • No animation timing, easing, color, glow, or completion behavior changes.
  • CPU improves only modestly because the excess frames were cheap: pulse-only steady-state samples moved from roughly 20–32% to 18–22%. The important invariant is that renderer work is now bounded.

Testing

  • bun typecheck in packages/tui
  • bun run test in packages/tui: 566 pass, 5 skip, 0 fail
  • bun run test test/component/tab-pulse.test.ts: 5 pass, 0 fail
  • Repo-pinned Prettier and oxlint clean on the changed file
  • Isolated opencode-drive pulse benchmark, 160×45 viewport, busy background session with zero streamed deltas:
    • before: typically 1,600–1,900 FPS, renderer-reported peaks above 3,000
    • after: 51–55 measured FPS, renderer-reported 55–56 FPS

@kitlangton
kitlangton marked this pull request as draft July 30, 2026 15:28
@kitlangton

Copy link
Copy Markdown
Contributor Author

Holding this as a draft while tracing the root scheduler interaction. A generic OpenTUI live renderable honors the global 60 FPS cap; the runaway requires the full combination of a live tab pulse and high-frequency streaming updates. If the core scheduler can coalesce that path correctly, this localized clock workaround will be replaced or the PR closed.

@kitlangton

Copy link
Copy Markdown
Contributor Author

Closing in favor of the architectural fix in OpenTUI core. The root cause is a leaked scheduled frame timer: CliRenderer.loop() nulled renderTimeout before trying to clear it, so an intermediate frame during a live loop orphaned the pending timer and streaming multiplied concurrent timer chains. The core fix cancels the pending timer before clearing its handle; the full OpenCode repro drops from ~1,625 FPS to ~52 FPS without a TabPulse-specific clock.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant