Skip to content

native: fewer idle frames: a slow ring without focus, a rarer service beat, a cursor row left alone - #1013

Merged
omartelo merged 2 commits into
mainfrom
perf/native-idle-frames
Nov 2, 2026
Merged

omartelo merged 2 commits into
mainfrom
perf/native-idle-frames

Conversation

@omartelo

@omartelo omartelo commented Nov 2, 2026

Copy link
Copy Markdown
Collaborator

Why

After #1011 and #1012 a busy session draws at the ring's 24fps and nothing else (measured on the real window: 41 → 23.5 frames a second, 13.6% → 7.5% of the GPU). Every frame costs the window the same whatever it shows, about 0.3% of a laptop GPU, so what is left is the frames nobody asked for.

What

  • Without focus the ring slows instead of stopping. perf(native): cap looping animations at 30fps and hold them while unfocused #1006 held every looping animation still while the window had no focus; from another monitor the ring looked dead. ui.StillFrame (125ms) is Theme.Loop's frame while Still: the ring turns at 8fps, a fifth of the frames.
  • A running service's dot beats every 6s instead of 2.4s. Each beat is 800ms of frames at LoopFrame; the old pace was 8 frames a second for as long as a service ran, all day for a 14dp dot. Its rest now always schedules the next beat, since a still window only slows.
  • The terminal's cursor row is recomposed only when the cursor moves. rowRenderer marked the cursor's row dirty on every draw, recomposing it on the CPU and uploading its 1116x19 texture every frame while the cursor stayed put; now it keeps the row's image until the cursor moves (then the row it left comes with it) or the row's text changes.

Tests changed for a changed contract: TestStillThemeHoldsTheFrame became TestStillThemeSlowsTheLoop, and TestServiceBeatPacing pins the 6s period and the slow frame while still.

Test plan

  • TestStillThemeSlowsTheLoop, TestLoopRecordsTheFrameItAskedFor (the slow frame is recorded too)
  • TestServiceBeatPacing
  • TestRowsRecomposeForTheCursorOnlyWhenItMoves: nothing changed redraws no row; the cursor moving along its row redraws that row; moving to the next row redraws both
  • native/: gofmt, go vet ./..., go test ./...

… beat, a cursor row left alone

Every frame costs the window the same whatever it shows, about 0.3% of a
laptop GPU, so what is left after #1011 is the frames nobody asked for:

- Without focus the ring stopped dead; now it turns at 8fps, so a session
  is still seen working from another monitor, at a fifth of the frames.
- A running service's dot beat every 2.4s, a burst of frames each time:
  8 frames a second all day for a 14dp dot. It beats every 6s.
- The terminal recomposed and re-uploaded the cursor's row on every frame
  while the cursor stayed put. The row is recomposed when the cursor moves
  or the row's own text changes.
TestBrowserFrameSurplusDecodesOnlyTheNewest captured the process log and
wanted it empty, but a layout save of an earlier test, failing after that
test's backend closed, logs there too: on the macOS runner it did. The
test now looks for the decode's own line, logged under the event's name.
@omartelo
omartelo merged commit 712b70e into main Nov 2, 2026
7 checks passed
@omartelo
omartelo deleted the perf/native-idle-frames branch November 2, 2026 02:40
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