Fix high CPU usage from cursor position request feedback loop - #1310
Merged
Conversation
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.
ArthurSonzogni
force-pushed
the
fix/tmux-cpu-1302
branch
from
July 19, 2026 11:45
ea529b9 to
ad8a81c
Compare
|
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Fixes #1302.
Root cause
In the non-alternate-screen modes (
FitComponent,TerminalOutput), everyDraw()sends a\x1b[6ncursor position request. Two bugs turned this into a permanent ~60fps redraw loop:ThrottledRequest::Send()updatedlast_sent_time_but neverlast_request_time_, so the computed delay was always negative and a new request went out as soon as the previous reply arrived.Any terminal that answers DSR enters this loop; tmux just makes it visible in
top.Fix
last_request_time_inThrottledRequest::Send()so the 500ms throttle works.animation::RequestAnimationFrame(), typically byanimation::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:
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.