Skip to content

perf(native): cap looping animations at 30fps and hold them while unfocused - #1006

Merged
omartelo merged 1 commit into
mainfrom
perf/native-busy-ring-repaint
Nov 1, 2026
Merged

omartelo merged 1 commit into
mainfrom
perf/native-busy-ring-repaint

Conversation

@omartelo

@omartelo omartelo commented Nov 1, 2026

Copy link
Copy Markdown
Collaborator

Why

With a session busy, lich-native held 44 to 78% of an RTX 3050 Laptop (nvidia-smi pmon), and the compositor recomposited every frame. Two causes, both measured:

  • A project tab's spinner (ui.Spinner) and the Pulls busy ring (ui.Ring) asked for the next frame at once (op.InvalidateCmd{}), so a session busy outside the active project repainted the whole window at the display's refresh rate.
  • Every frame costs the whole window: Gio's path cache key carries the ops version, which changes on every Reset, so each frame re-strokes and re-uploads every path on screen and redraws the full surface. A pprof of the live window with three spinners: 26% of a core, 60% of it in gpu.Frame; the ring itself was 0.5%.

What

  • Theme.Loop(gtx) asks for a looping animation's next frame ui.LoopFrame (33ms) on. Spinner, BusyRing (renamed from Ring, which clashed with the palette's Ring color token, and now a Theme method like Spinner), the sidebar card's busy ring and a running service's beat all go through it.
  • Theme.Still is set while the window has no focus (app.ConfigEvent): looping animations hold their frame and ask for none, and the beat skips its wake for the next pulse. Regaining focus asks for a frame.

Measured

Nested headless umbriel on the same GPU, 1400x1420 window at 100Hz, one session busy in a project that is not the active tab:

fps GPU sm CPU
before 98 9.7% ~30% of a core
after, focused 30 4.2% under 10%
after, unfocused 0.7 0%

The rig's window holds little content; the live window's per-frame cost is several times higher.

Not in this PR

Making a frame cost proportional to the ring's area needs partial redraw in the Gio fork's renderer (scissor, preserved back buffer, swap with damage, per backend) and stable op keys. Left for later.

Test plan

  • ui: TestLoopingAnimationsRedrawAt30fps (fails on the old immediate invalidate) and TestStillThemeHoldsTheFrame
  • cmd/lich-native: TestServiceBeatPacing, mid-pulse, at rest and still
  • native/: gofmt, go vet ./..., go test ./...
  • Frame rate and GPU before and after in a rig window, numbers above

…ocused

A project tab's spinner and the Pulls busy ring asked for a frame at once,
so a session busy outside the active project repainted the whole window at
the display's rate. Spinner, BusyRing, the sidebar's busy ring and a running
service's beat now share Theme.Loop: the next frame 33ms on, none while the
window has no focus.
@omartelo
omartelo merged commit d1f8db0 into main Nov 1, 2026
7 checks passed
@omartelo
omartelo deleted the perf/native-busy-ring-repaint branch November 1, 2026 18:59
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