Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

6 changes: 6 additions & 0 deletions crates/fps/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ sysinfo = "0.37"
# counter the platform's own activity monitor attributes per process with, so no
# vendor SDK or elevated privilege is involved.
[target.'cfg(target_os = "macos")'.dependencies]
core-graphics = "0.24"
libc = "0.2"
objc2-core-foundation = { version = "0.3", default-features = false, features = [
"std",
Expand All @@ -42,8 +43,13 @@ objc2-io-kit = { version = "0.3", default-features = false, features = [
"libc",
] }

[target.'cfg(target_os = "linux")'.dependencies]
uuid = { version = "1", features = ["v5"] }
wayland-client = "0.31"

[target.'cfg(target_os = "windows")'.dependencies]
windows = { workspace = true, features = [
"Win32_Graphics_Gdi",
"Win32_System_Performance",
"Win32_System_ProcessStatus",
"Win32_System_Threading",
Expand Down
1 change: 1 addition & 0 deletions crates/fps/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ mod gpu;
mod memory;
mod monitor;
mod overlay;
mod refresh;
mod sampler;
mod style;

Expand Down
74 changes: 48 additions & 26 deletions crates/fps/src/monitor.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,15 +3,16 @@ use std::time::Duration;
use web_time::Instant;

use gpui::{
Bounds, Context, Div, Hsla, InteractiveElement as _, IntoElement, MouseButton, ParentElement,
PathBuilder, Pixels, Point, Render, StatefulInteractiveElement as _, Styled, Window, canvas,
div, point, prelude::FluentBuilder as _, px, relative,
App, Bounds, Context, DisplayId, Div, Hsla, InteractiveElement as _, IntoElement, MouseButton,
ParentElement, PathBuilder, Pixels, Point, Render, StatefulInteractiveElement as _, Styled,
Window, canvas, div, point, prelude::FluentBuilder as _, px, relative,
};

use gpui::Task;

use crate::{
FrameTraceGuard,
refresh::display_refresh_rate,
sampler::{FrameSampler, ResourceSample, minimum_resource_interval},
style::FpsStyle,
};
Expand Down Expand Up @@ -121,9 +122,9 @@ struct Readout {
/// resource row right underneath. The frame cost answers the same question
/// without being paid for.
///
/// Held to the display's refresh rate once that is known. A frame drawn in
/// 3ms is not 333 frames the reader could ever see, and printing it that
/// way turns the headline back into a benchmark score rather than a rate.
/// A ceiling the frame cost can prove, not one the display can show: a
/// window whose frames cost 3ms could redraw 333 times a second, on a
/// panel that would scan out sixty of them.
max_fps: f32,
/// Frames presented per second: the rate the window is actually drawing
/// at, which an idle application drives to zero. The reciprocal of
Expand All @@ -142,24 +143,23 @@ struct Readout {
}

/// The rate a full redraw could sustain: what a frame's cost implies, held to
/// what the display can present.
/// what the panel can scan out.
///
/// The cap is the half the derivation loses. Counting presents could never
/// exceed the refresh rate — frames go to the compositor on vsync, so the
/// bound came for free — and a figure derived from frame cost has no such
/// ceiling: a frame drawn in 3ms reads as 333, a number nobody could ever
/// see. `display` is `None` until the window has presented two frames a
/// plausible refresh apart, and an uncapped reading is better than one capped
/// by a guess.
fn sustainable_rate(mean_draw: Duration, display: Option<f32>) -> f32 {
/// bound came for free — while a frame drawn in 3ms reads as 333, a rate
/// nobody could ever see. `display` is `None` where the platform would not say
/// what the panel runs at, and an uncapped reading is better than one held to
/// a guess: see [`crate::refresh`] for why guessing was tried and abandoned.
fn sustainable_rate(mean_draw: Duration, display: Option<Duration>) -> f32 {
let mean_draw = mean_draw.as_secs_f32();
if mean_draw <= 0. {
return 0.;
}
let rate = 1. / mean_draw;
match display {
Some(display) => rate.min(display),
None => rate,
match display.map(|period| period.as_secs_f32()) {
Some(period) if period > 0. => rate.min(1. / period),
_ => rate,
}
}

Expand All @@ -184,6 +184,10 @@ pub struct FpsMonitor {
style: FpsStyle,
frame_budget: Duration,
headline: Headline,
/// The panel's refresh period, and which display it was asked about, so
/// that moving the window to another monitor re-asks and staying on one
/// does not ask again every frame.
display: Option<(DisplayId, Option<Duration>)>,
show_resources: bool,
resource_interval: Duration,
resources: Option<ResourceSample>,
Expand All @@ -204,6 +208,7 @@ impl FpsMonitor {
style: FpsStyle::default(),
frame_budget,
headline: Headline::Max,
display: None,
show_resources: true,
resource_interval: DEFAULT_RESOURCE_INTERVAL,
resources: None,
Expand Down Expand Up @@ -336,6 +341,19 @@ impl FpsMonitor {
}));
}

/// Re-asks the platform for the refresh rate when the window has moved to
/// another display, and not otherwise: the answer is a property of the
/// panel, and on some platforms asking is a round trip.
fn update_display(&mut self, window: &Window, cx: &App) {
let Some(display) = window.display(cx) else {
return;
};
let id = display.id();
if self.display.map(|(asked, _)| asked) != Some(id) {
self.display = Some((id, display_refresh_rate(display.as_ref())));
}
}

/// Republishes the readings if [`READOUT_INTERVAL`] has passed.
fn update_readout(&mut self) {
let now = Instant::now();
Expand All @@ -347,7 +365,10 @@ impl FpsMonitor {
}

self.readout = Readout {
max_fps: sustainable_rate(self.sampler.mean_draw(), self.sampler.peak_present_rate()),
max_fps: sustainable_rate(
self.sampler.mean_draw(),
self.display.and_then(|(_, refresh_rate)| refresh_rate),
),
fps: self.sampler.fps(),
interval_millis: self.sampler.present_interval().as_secs_f32() * 1000.,
// The mean over the interval rather than the latest frame, which
Expand Down Expand Up @@ -497,8 +518,9 @@ impl FpsMonitor {
}

impl Render for FpsMonitor {
fn render(&mut self, _: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
self.sampler.tick();
self.update_display(window, cx);
self.update_readout();
self.update_axis();
self.start_clock(cx);
Expand Down Expand Up @@ -739,19 +761,19 @@ mod tests {
use super::*;

#[test]
fn the_headline_rate_never_exceeds_what_the_display_can_present() {
fn the_headline_rate_is_what_a_frame_costs_and_the_panel_allows() {
let sixty = Duration::from_micros(16_667);
// A cheap frame on a 60Hz panel is not 333 frames anyone could see.
assert!((sustainable_rate(Duration::from_millis(3), Some(sixty)) - 60.).abs() < 0.01);
// A frame that costs more than a refresh sets the rate itself.
assert_eq!(
sustainable_rate(Duration::from_millis(3), Some(60.)),
60.,
"a frame cheaper than a refresh is capped by the refresh"
sustainable_rate(Duration::from_millis(20), Some(sixty)),
50.
);
// Until the display has shown its cadence, capping would be a guess.
// Where the platform will not say, an uncapped reading beats a guess.
assert!((sustainable_rate(Duration::from_millis(3), None) - 333.33).abs() < 0.1);
// A frame that costs more than a refresh sets the rate itself.
assert_eq!(sustainable_rate(Duration::from_millis(20), Some(60.)), 50.);
// No frames drawn yet is no rate, not an infinite one.
assert_eq!(sustainable_rate(Duration::ZERO, Some(60.)), 0.);
assert_eq!(sustainable_rate(Duration::ZERO, Some(sixty)), 0.);
}

#[gpui::test]
Expand Down
Loading
Loading