perf: disable plot supersampling, add plot render benchmarks (#467)

* dev: add benchmark for drawing plots

* chore: update bench

* perf: disable supersampling
This commit is contained in:
Ilya Zlobintsev 2025-02-10 19:25:15 +02:00 committed by GitHub
parent 8e58d19807
commit 8dde4df4d1
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
10 changed files with 157 additions and 73 deletions

3
Cargo.lock generated
View File

@ -1282,6 +1282,7 @@ name = "lact"
version = "0.7.1" version = "0.7.1"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"divan",
"lact-cli", "lact-cli",
"lact-daemon", "lact-daemon",
"lact-gui", "lact-gui",
@ -1327,7 +1328,6 @@ dependencies = [
"futures", "futures",
"indexmap", "indexmap",
"insta", "insta",
"lact-daemon",
"lact-schema", "lact-schema",
"libdrm_amdgpu_sys", "libdrm_amdgpu_sys",
"libflate", "libflate",
@ -1358,6 +1358,7 @@ dependencies = [
"anyhow", "anyhow",
"cairo-rs", "cairo-rs",
"chrono", "chrono",
"divan",
"gtk4", "gtk4",
"itertools", "itertools",
"lact-client", "lact-client",

View File

@ -26,6 +26,7 @@ chrono = "0.4.31"
indexmap = { version = "2.5.0", features = ["serde"] } indexmap = { version = "2.5.0", features = ["serde"] }
pretty_assertions = "1.4.0" pretty_assertions = "1.4.0"
divan = "0.1" divan = "0.1"
gtk = { version = "0.9", package = "gtk4" }
[profile.release] [profile.release]
strip = "symbols" strip = "symbols"
@ -45,3 +46,5 @@ opt-level = 3
[profile.bench] [profile.bench]
strip = false strip = false
debug = 1 debug = 1
lto = "thin"
codegen-units = 256

View File

@ -48,14 +48,8 @@ copes = { git = "https://gitlab.com/corectrl/copes" }
libloading = "0.8.6" libloading = "0.8.6"
[dev-dependencies] [dev-dependencies]
divan = { workspace = true }
pretty_assertions = { workspace = true } pretty_assertions = { workspace = true }
lact-daemon = { path = ".", features = ["bench"] }
insta = { version = "1.41.1", features = ["json", "yaml"] } insta = { version = "1.41.1", features = ["json", "yaml"] }
[build-dependencies] [build-dependencies]
bindgen = "0.68" bindgen = "0.68"
[[bench]]
name = "daemon"
harness = false

View File

@ -1,5 +0,0 @@
fn main() {
// Include the daemon lib
let _ = lact_daemon::MODULE_CONF_PATH;
divan::main();
}

View File

@ -8,6 +8,7 @@ edition = "2021"
default = ["gtk-tests"] default = ["gtk-tests"]
gtk-tests = [] gtk-tests = []
adw = ["dep:adw", "relm4/libadwaita"] adw = ["dep:adw", "relm4/libadwaita"]
bench = ["dep:divan"]
[dependencies] [dependencies]
lact-client = { path = "../lact-client" } lact-client = { path = "../lact-client" }
@ -19,8 +20,8 @@ tracing = { workspace = true }
anyhow = { workspace = true } anyhow = { workspace = true }
tracing-subscriber = { workspace = true } tracing-subscriber = { workspace = true }
chrono = { workspace = true } chrono = { workspace = true }
gtk = { workspace = true, features = ["v4_6", "blueprint"] }
gtk = { version = "0.9", package = "gtk4", features = ["v4_6", "blueprint"] }
adw = { package = "libadwaita", version = "0.7.1", features = [ adw = { package = "libadwaita", version = "0.7.1", features = [
"v1_4", "v1_4",
], optional = true } ], optional = true }
@ -37,5 +38,7 @@ itertools = "0.13.0"
thread-priority = "1.1.0" thread-priority = "1.1.0"
divan = { workspace = true, optional = true }
[dev-dependencies] [dev-dependencies]
pretty_assertions = "1.4.0" pretty_assertions = "1.4.0"

View File

@ -38,12 +38,13 @@ impl relm4::Component for GraphsWindow {
set_margin_all: 10, set_margin_all: 10,
set_row_spacing: 20, set_row_spacing: 20,
set_column_spacing: 20, set_column_spacing: 20,
set_column_homogeneous: true,
attach[0, 0, 1, 1]: temperature_plot = &Plot { attach[0, 0, 1, 1]: temperature_plot = &Plot {
set_title: "Temperature", set_title: "Temperature",
set_hexpand: true, set_hexpand: true,
set_value_suffix: "°C", set_value_suffix: "°C",
set_y_label_area_relative_size: 0.15, set_y_label_area_relative_size: 0.2,
#[watch] #[watch]
set_time_period_seconds: model.time_period_seconds_adj.value() as i64, set_time_period_seconds: model.time_period_seconds_adj.value() as i64,
}, },
@ -52,7 +53,7 @@ impl relm4::Component for GraphsWindow {
set_title: "Fan speed", set_title: "Fan speed",
set_hexpand: true, set_hexpand: true,
set_value_suffix: "RPM", set_value_suffix: "RPM",
set_y_label_area_relative_size: 0.25, set_y_label_area_relative_size: 0.3,
set_secondary_y_label_area_relative_size: 0.15, set_secondary_y_label_area_relative_size: 0.15,
#[watch] #[watch]
set_time_period_seconds: model.time_period_seconds_adj.value() as i64, set_time_period_seconds: model.time_period_seconds_adj.value() as i64,

View File

@ -9,6 +9,8 @@ use std::collections::BTreeMap;
use super::render_thread::{RenderRequest, RenderThread}; use super::render_thread::{RenderRequest, RenderThread};
const SUPERSAMPLE_FACTOR: u32 = 1;
#[derive(Properties, Default)] #[derive(Properties, Default)]
#[properties(wrapper_type = super::Plot)] #[properties(wrapper_type = super::Plot)]
pub struct Plot { pub struct Plot {
@ -75,7 +77,7 @@ impl WidgetImpl for Plot {
secondary_y_label_relative_area_size: self secondary_y_label_relative_area_size: self
.secondary_y_label_area_relative_size .secondary_y_label_area_relative_size
.get(), .get(),
supersample_factor: 4, supersample_factor: SUPERSAMPLE_FACTOR,
time_period_seconds: self.time_period_seconds.get(), time_period_seconds: self.time_period_seconds.get(),
}); });
} }
@ -105,14 +107,19 @@ impl PlotData {
self.push_secondary_line_series_with_time(name, point, chrono::Local::now().naive_local()); self.push_secondary_line_series_with_time(name, point, chrono::Local::now().naive_local());
} }
fn push_line_series_with_time(&mut self, name: &str, point: f64, time: NaiveDateTime) { pub(super) fn push_line_series_with_time(
&mut self,
name: &str,
point: f64,
time: NaiveDateTime,
) {
self.line_series self.line_series
.entry(name.to_owned()) .entry(name.to_owned())
.or_default() .or_default()
.push((time.and_utc().timestamp_millis(), point)); .push((time.and_utc().timestamp_millis(), point));
} }
pub fn push_secondary_line_series_with_time( pub(super) fn push_secondary_line_series_with_time(
&mut self, &mut self,
name: &str, name: &str,
point: f64, point: f64,
@ -188,3 +195,60 @@ impl PlotData {
self.line_series.is_empty() && self.secondary_line_series.is_empty() self.line_series.is_empty() && self.secondary_line_series.is_empty()
} }
} }
#[cfg(feature = "bench")]
mod benches {
use crate::app::graphs_window::plot::{
render_thread::{process_request, RenderRequest},
PlotData,
};
use chrono::{NaiveDate, NaiveDateTime, NaiveTime};
use divan::{counter::ItemsCount, Bencher};
use std::sync::Mutex;
use super::SUPERSAMPLE_FACTOR;
#[divan::bench]
fn render_plot(bencher: Bencher) {
let last_texture = &Mutex::new(None);
bencher
.with_inputs(sample_plot_data)
.input_counter(|_| ItemsCount::new(1usize))
.bench_values(|data| {
let request = RenderRequest {
title: "bench render".into(),
value_suffix: "%".into(),
secondary_value_suffix: "".into(),
y_label_area_relative_size: 1.0,
secondary_y_label_relative_area_size: 1.0,
data,
width: 1920,
height: 1080,
supersample_factor: SUPERSAMPLE_FACTOR,
time_period_seconds: 60,
};
process_request(request, last_texture)
});
}
fn sample_plot_data() -> PlotData {
let mut data = PlotData::default();
// Simulate 1 minute plot with 4 values per second
for sec in 0..60 {
for milli in [0, 250, 500, 750] {
let datetime = NaiveDateTime::new(
NaiveDate::from_ymd_opt(2025, 1, 1).unwrap(),
NaiveTime::from_hms_milli_opt(0, 0, sec, milli).unwrap(),
);
data.push_line_series_with_time("GPU", 100.0, datetime);
data.push_secondary_line_series_with_time("GPU Secondary", 10.0, datetime);
}
}
data
}
}

View File

@ -94,62 +94,15 @@ impl RenderThread {
match current_request.take() { match current_request.take() {
Some(Request::Render(render_request)) => { Some(Request::Render(render_request)) => {
// Create a new ImageSurface for Cairo rendering. process_request(render_request, last_texture);
let mut surface = ImageSurface::create( }
cairo::Format::ARgb32,
(render_request.width * render_request.supersample_factor) as i32,
(render_request.height * render_request.supersample_factor) as i32,
)
.unwrap();
let cairo_context = CairoContext::new(&surface).unwrap();
// Don't use Cairo's default antialiasing, it makes the lines look too blurry
// Supersampling is our 2D anti-aliasing solution.
if render_request.supersample_factor > 1 {
cairo_context.set_antialias(cairo::Antialias::None);
}
let cairo_backend = CairoBackend::new(
&cairo_context,
// Supersample the rendering
(
render_request.width * render_request.supersample_factor,
render_request.height * render_request.supersample_factor,
),
)
.unwrap();
if let Err(err) = render_request.draw(cairo_backend) {
error!("Failed to plot chart: {err:?}")
}
match (
surface.to_texture(),
last_texture.lock().unwrap().deref_mut(),
) {
// Successfully generated a new texture, but the old texture is also there
(Some(texture), Some(last_texture)) => {
*last_texture = texture;
}
// If texture conversion failed, keep the old texture if it's present.
(None, None) => {
error!("Failed to convert cairo surface to gdk texture, not overwriting old one");
}
// Update the last texture, if The old texture wasn't ever generated (None),
// No matter the result of conversion
(result, last_texture) => {
*last_texture = result;
}
};
}
// Terminate the thread if a Terminate request is received. // Terminate the thread if a Terminate request is received.
Some(Request::Terminate) => break, Some(Request::Terminate) => break,
None => {} None => {}
} }
} }
}) })
.unwrap(); .unwrap();
Self { Self {
state, state,
@ -177,6 +130,60 @@ impl RenderThread {
} }
} }
pub(super) fn process_request(
render_request: RenderRequest,
last_texture: &Mutex<Option<MemoryTexture>>,
) {
// Create a new ImageSurface for Cairo rendering.
let mut surface = ImageSurface::create(
cairo::Format::ARgb32,
(render_request.width * render_request.supersample_factor) as i32,
(render_request.height * render_request.supersample_factor) as i32,
)
.unwrap();
let cairo_context = CairoContext::new(&surface).unwrap();
// Don't use Cairo's default antialiasing, it makes the lines look too blurry
// Supersampling is our 2D anti-aliasing solution.
if render_request.supersample_factor > 1 {
cairo_context.set_antialias(cairo::Antialias::None);
}
let cairo_backend = CairoBackend::new(
&cairo_context,
// Supersample the rendering
(
render_request.width * render_request.supersample_factor,
render_request.height * render_request.supersample_factor,
),
)
.unwrap();
if let Err(err) = render_request.draw(cairo_backend) {
error!("Failed to plot chart: {err:?}")
}
match (
surface.to_texture(),
last_texture.lock().unwrap().deref_mut(),
) {
// Successfully generated a new texture, but the old texture is also there
(Some(texture), Some(last_texture)) => {
*last_texture = texture;
}
// If texture conversion failed, keep the old texture if it's present.
(None, None) => {
error!("Failed to convert cairo surface to gdk texture, not overwriting old one");
}
// Update the last texture, if The old texture wasn't ever generated (None),
// No matter the result of conversion
(result, last_texture) => {
*last_texture = result;
}
};
}
// Implement the default constructor for RenderThread using the `new` method. // Implement the default constructor for RenderThread using the `new` method.
impl Default for RenderThread { impl Default for RenderThread {
fn default() -> Self { fn default() -> Self {
@ -336,12 +343,12 @@ impl RenderRequest {
(current_date, segment.evaluate(current_date)) (current_date, segment.evaluate(current_date))
}) })
}), }),
Palette99::pick(idx).stroke_width(8), Palette99::pick(idx).stroke_width(2),
)) ))
.context("Failed to draw series")? .context("Failed to draw series")?
.label(caption) .label(caption)
.legend(move |(x, y)| { .legend(move |(x, y)| {
let offset = self.relative_size(0.04) as i32; let offset = self.relative_size(0.02) as i32;
Rectangle::new( Rectangle::new(
[(x - offset, y - offset), (x + offset, y + offset)], [(x - offset, y - offset), (x + offset, y + offset)],
Palette99::pick(idx).filled(), Palette99::pick(idx).filled(),
@ -360,12 +367,12 @@ impl RenderRequest {
(current_date, segment.evaluate(current_date)) (current_date, segment.evaluate(current_date))
}) })
}), }),
Palette99::pick(idx + 10).stroke_width(8), Palette99::pick(idx + 10).stroke_width(2),
)) ))
.context("Failed to draw series")? .context("Failed to draw series")?
.label(caption) .label(caption)
.legend(move |(x, y)| { .legend(move |(x, y)| {
let offset = self.relative_size(0.04) as i32; let offset = self.relative_size(0.02) as i32;
Rectangle::new( Rectangle::new(
[(x - offset, y - offset), (x + offset, y + offset)], [(x - offset, y - offset), (x + offset, y + offset)],
Palette99::pick(idx + 10).filled(), Palette99::pick(idx + 10).filled(),

View File

@ -13,3 +13,12 @@ lact-schema = { path = "../lact-schema", features = ["args"] }
lact-cli = { path = "../lact-cli" } lact-cli = { path = "../lact-cli" }
lact-gui = { path = "../lact-gui", optional = true } lact-gui = { path = "../lact-gui", optional = true }
anyhow = { workspace = true } anyhow = { workspace = true }
[dev-dependencies]
divan = { workspace = true }
lact-daemon = { path = "../lact-daemon", features = ["bench"] }
lact-gui = { path = "../lact-gui", features = ["bench"] }
[[bench]]
name = "bench"
harness = false

7
lact/benches/bench.rs Normal file
View File

@ -0,0 +1,7 @@
fn main() {
// Include crates in the binary
let _ = lact_daemon::run;
let _ = lact_gui::run;
divan::main();
}