Skip to content

Fix high CPU usage from cursor position request feedback loop - #1310

Merged
ArthurSonzogni merged 3 commits into
mainfrom
fix/tmux-cpu-1302
Jul 19, 2026
Merged

Fix high CPU usage from cursor position request feedback loop#1310
ArthurSonzogni merged 3 commits into
mainfrom
fix/tmux-cpu-1302

Conversation

@ArthurSonzogni

@ArthurSonzogni ArthurSonzogni commented Jul 19, 2026

Copy link
Copy Markdown
Owner

Fixes #1302.

Root cause

In the non-alternate-screen modes (FitComponent, TerminalOutput), every Draw() sends a \x1b[6n cursor position request. Two bugs turned this into a permanent ~60fps redraw loop:

  1. The 500ms request throttle never engaged. ThrottledRequest::Send() updated last_sent_time_ but never last_request_time_, so the computed delay was always negative and a new request went out as soon as the previous reply arrived.
  2. The terminal's reply triggered a redraw. Since the v7 event loop rework, every received event armed an animation frame — including the terminal's replies to our own requests. Each reply armed a frame, the next 15ms tick redrew, and the redraw sent another request.

Any terminal that answers DSR enters this loop; tmux just makes it visible in top.

Fix

  • Record last_request_time_ in ThrottledRequest::Send() so the 500ms throttle works.
  • Restore the FTXUI 6 animation contract: animation frames are produced only when requested via animation::RequestAnimationFrame(), typically by animation::Animator. Receiving an event no longer implicitly arms an animation frame — that behavior was an accident of the v7 rework, and it is what let internal terminal replies drive redraws.

Verification

Gallery example idling inside tmux, measured over 15s:

Process Before After
gallery ~24% CPU ~0.4% CPU
tmux server (detached) ~8% CPU ~0.0% CPU

The UI renders identically and keyboard navigation still works (tmux send-keys + capture-pane). A new test, AnimationTest.TicksOnlyWhenRequested, pins the animation contract: no tick without a request, no tick for a bare event, exactly one tick per request. All 349 tests pass.

In non-alternate-screen modes, every Draw() sends a \x1b[6n cursor
position request. Two compounding bugs turned this into a permanent
~60fps redraw loop, pegging both the app and the terminal emulator
(most visibly tmux):

1. ThrottledRequest::Send() never updated last_request_time_, so the
   intended 500ms throttle never engaged: a new request was sent as
   soon as the previous reply arrived.

2. The terminal input parser sink requested an animation frame for
   every parsed event, including cursor position reports, which are
   internal replies to our own request. The armed animation frame
   invalidated the frame, triggering a full redraw that sent the next
   request, whose reply armed the next frame, and so on.

Fix: record last_request_time_ in Send(), and stop requesting
animation frames for cursor position reports (they are still delivered
to the event buffer and consumed as before).

Measured with the gallery example idling inside tmux: the app drops
from ~24% CPU to ~0.4%, the tmux server from ~8% (detached; ~100% of
a core with an attached client) to ~0%.

Fixes #1302
Move RequestAnimationFrame() from the two event producers (the
TerminalInputParser callback and App::PostEvent) to the Event branch of
App::Internal::HandleTask, below the internal-reply early-returns.

A single place now decides which events grant an animation frame, next
to the code discriminating event kinds. As a result, every internal
terminal reply (cursor shape, terminal capabilities, name/version,
emulator) stops arming animation frames: previously only cursor
position reports were exempted, and only on the parser path. This also
moves the animation_requested_ write onto the main loop thread,
removing an unsynchronized write when PostEvent is called from another
thread.

Add a test covering the event -> animation tick pipeline. See #1302.
Restore the FTXUI 6 behavior: animation frames are produced only when
requested via animation::RequestAnimationFrame(), typically by an
animation::Animator which arms it on construction and re-arms it from
OnAnimation until the transition completes.

Handling an event no longer implicitly grants an animation tick. That
behavior was introduced accidentally by the v7 event loop rework
(33a40ee) and is what made the #1302 feedback loop possible in the
first place: internal terminal replies are events too.
@github-actions

Copy link
Copy Markdown

⚠️ ABI Compatibility Report

Comparing main (base) with HEAD (PR)

ABI fingerprints changed from d0578d99d54909d813dd510a85d45a4a1c1f0d7e8f179b0eac88c644218de938 to c41901d6a4da49c7eec689a174eb1f30c2994f8c54799cc1f60e60a24985001f.

Detailed ABI Analysis
Checking ABI changes between origin/main and HEAD...
Building origin/main...
Building HEAD...
--------------------------------------------------------------------------------
Comparing libftxui-screen.so...
No ABI changes detected for libftxui-screen.so.
--------------------------------------------------------------------------------
Comparing libftxui-dom.so...
No ABI changes detected for libftxui-dom.so.
--------------------------------------------------------------------------------
Comparing libftxui-component.so...
No ABI changes detected for libftxui-component.so.
--------------------------------------------------------------------------------
No ABI changes detected.

@ArthurSonzogni
ArthurSonzogni merged commit 3b5d351 into main Jul 19, 2026
21 checks passed
@ArthurSonzogni
ArthurSonzogni deleted the fix/tmux-cpu-1302 branch July 19, 2026 12:03
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.

High CPU usage on tmux.

1 participant