Skip to content

Commit b586fad

Browse files
huacnleeclaude
andauthored
fps: Derive the headline rate from frame cost instead of driving the frame loop (longbridge#2954)
## Summary The HUD asked for a frame after every render, and the doc claimed that being its own view kept the repaint to the HUD's subtree. It does not — a dirty view schedules a **window** draw, and GPUI re-renders every view outside an `Entity::cached` boundary. So each frame the HUD asked for was a full layout and paint of the application, reported one row down as the application's CPU. The rate it was buying is already in the samples: `FRAME` is what a full redraw costs, so its reciprocal is the rate those redraws could sustain, and nothing has to be drawn to find it. | Table page, focused, untouched | Before | After | | --- | --- | --- | | Process CPU | **~62%** | **~0%** | | HUD reads | 60 FPS / CPU 27% | MAX 115 FPS / FRAME 8.7ms / DROP 0.0% | ## Changes **Headline is two rates, right-click to switch, `MAX` says which.** `MAX FPS` is `1 / FRAME`; `FPS` is presents per second. Both come from the same samples, so switching is free — which is the point: the only expensive way to make the first number meaningful is to stop the second from being readable. The marker goes in the box that already balanced the unit, so the figure does not move. Click still collapses. **`MAX` is capped by the display.** Counting presents had that ceiling for free (frames go to the compositor on vsync, see longbridge#2944); a derived figure does not, and a frame drawn in 3ms reads as 333. GPUI does not expose the refresh rate, so the sampler infers it from the gaps between presents: only gaps that could be a refresh (3–50ms), only ones that recur, the mean of the busiest group *and its neighbours* (bucketing truncates the group it measures — that read 149 on a 144Hz panel), preferring a group at least twice as fast when one arrives in bulk (a ProMotion window resting at 60 must still be capped at 120), snapped to a standard rate within 2.5%. Measured on a 143.999Hz panel: estimate 146.6, reads **144**. **The cold start is not measured.** Mounting the HUD drained everything GPUI had recorded since process start — shaders, glyph atlas, icons, every cache cold. One of those frames is 100ms against a budget of 16, which a HUD that has seen eight frames reports as a twelfth of the window's work, in amber, before the reader has done anything. Both the backlog and the first frames after it are now dropped, so a window that just opened reads healthy. **`continuous` is gone.** Its only effect was to manufacture the thing it was measuring. Removed from `gpui-fps` and from the script API — `show_fps_monitor({ continuous })`, `.continuous()`, the typing and the key list. **This is a breaking change for scripts**, but the option can no longer select anything. **A readout clock replaces the resource timer.** Nothing else wakes a HUD that has stopped asking for frames; `show_resources(false)` used to freeze the figures at whatever the application last drew. **story drops `inactive_frame_interval: 500ms`**, which existed only to hold the HUD's self-driven redraws to 2 FPS in the background. **Docs**: `website/docs/fps.md` and its zh-CN mirror, with the reasoning rather than just the behaviour — `MAX FPS` is a rate nothing was clocked at, and without that written down the next reader concludes the framework cannot hold a frame rate. ## Test plan - [ ] HUD on, window focused, untouched: CPU near zero, `MAX` matches `1000/FRAME`, `DROP` and `P95` green - [ ] Right-click switches to `FPS` (single digits while idle) and back - [ ] After scrolling, `MAX` stops at the panel's rate — 60 on a 60Hz display - [ ] macOS ProMotion: caps at 120 while scrolling, and still 120 after it settles - [ ] A heavy page (Table / DataTable) reads a visibly lower `MAX` - [ ] `show_resources(false)` still refreshes rather than freezing - [ ] `fps_monitor().continuous(false)` in a script now errors as an unknown property - [ ] Animated stories no longer step at 2 FPS in a background window ## AI Assistance > 🤖 Code and docs generated by Claude Code. The direction — derive MAX from `FRAME`, drop `continuous`, right-click between the two rates, never exceed the display, open healthy — is the author's. Verified locally on Linux/Wayland with 144Hz and 60Hz panels (debug build, `/proc/<pid>/stat` sampling, screenshots, temporary instrumentation for the refresh estimate); `cargo test -p gpui-fps` (31) and `-p gpui-shell` (695+4+4) pass, clippy `--deny warnings` clean, wasm target builds. **ProMotion is covered by a unit test only — not verified on macOS hardware.** 🤖 Generated with [Claude Code](https://claude.com/claude-code) https://claude.ai/code/session_01USSMRpQ5W58YP3UKUCzrri --------- Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
1 parent a1275a9 commit b586fad

16 files changed

Lines changed: 757 additions & 190 deletions

File tree

‎crates/fps/src/lib.rs‎

Lines changed: 8 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,13 @@
22
//! rolling frame time chart, and this process' GPU, CPU and memory usage.
33
//!
44
//! Frame data comes from GPUI's own frame trace
5-
//! ([`gpui::FrameTimingCollector`]). The rate and the interval count frames
6-
//! *presented*, stamped with their own present time, so they agree with the
7-
//! platform's overlay (Metal's HUD counts the same drawables); the frame cost
8-
//! is what the framework actually spent in `Window::draw`, rather than an
9-
//! approximation measured from the outside.
5+
//! ([`gpui::FrameTimingCollector`]). The interval counts frames *presented*,
6+
//! stamped with their own present time, so it agrees with the platform's
7+
//! overlay (Metal's HUD counts the same drawables); the frame cost is what the
8+
//! framework actually spent in `Window::draw`, rather than an approximation
9+
//! measured from the outside. The headline rate is derived from that cost —
10+
//! the HUD never drives the frame loop, so nothing it reports is something it
11+
//! caused.
1012
//!
1113
//! Render it wherever it should appear, guarded by your own flag:
1214
//!
@@ -25,8 +27,7 @@
2527
//! # }
2628
//! ```
2729
//!
28-
//! The returned overlay can change its corner, frame budget, and whether it
29-
//! continuously drives the window's animation loop. A custom palette or an
30+
//! The returned overlay can change its corner and its frame budget. A custom palette or an
3031
//! embedded rather than overlaid HUD is built by composing [`FpsMonitor`] and
3132
//! [`FpsOverlay`] directly.
3233
//!

0 commit comments

Comments
 (0)