From 917379d548b43403aed5d3082b5130c8c1592b8a Mon Sep 17 00:00:00 2001 From: Samuel Onoja Date: Sun, 27 Sep 2026 06:58:09 +0100 Subject: [PATCH] Reduce polling retries and repeated compositor lookups --- compositor/src/metrics.rs | 38 ++++++++++++++++--- compositor/src/stacking.rs | 54 ++++++++++++++++++++++++--- compositor/src/winit.rs | 15 +++++--- shell/ferese-shell/src/status.rs | 63 +++++++++++++++++++++++++++++--- 4 files changed, 146 insertions(+), 24 deletions(-) diff --git a/compositor/src/metrics.rs b/compositor/src/metrics.rs index 1b43706..580a0f1 100644 --- a/compositor/src/metrics.rs +++ b/compositor/src/metrics.rs @@ -1,4 +1,7 @@ -use std::time::{Duration, Instant}; +use std::{ + collections::VecDeque, + time::{Duration, Instant}, +}; use smithay::utils::{Physical, Rectangle}; @@ -16,7 +19,7 @@ pub(crate) struct RenderMetrics { damaged_pixels: u64, total_render_time: Duration, longest_render_time: Duration, - frame_times: Vec, + frame_times: VecDeque, } impl RenderMetrics { @@ -37,7 +40,7 @@ impl RenderMetrics { damaged_pixels: 0, total_render_time: Duration::ZERO, longest_render_time: Duration::ZERO, - frame_times: Vec::new(), + frame_times: VecDeque::new(), } } @@ -57,9 +60,9 @@ impl RenderMetrics { self.total_render_time += render_time; self.longest_render_time = self.longest_render_time.max(render_time); if self.frame_times.len() == 512 { - self.frame_times.remove(0); + self.frame_times.pop_front(); } - self.frame_times.push(render_time); + self.frame_times.push_back(render_time); let now = Instant::now(); let elapsed = now.saturating_duration_since(self.interval_started); @@ -72,7 +75,7 @@ impl RenderMetrics { .as_micros() .checked_div(u128::from(self.rendered_frames)) .unwrap_or(0); - self.frame_times.sort_unstable(); + self.frame_times.make_contiguous().sort_unstable(); let percentile = |percent: usize| { let index = (self.frame_times.len() * percent) .div_ceil(100) @@ -159,4 +162,27 @@ mod tests { } assert_eq!(metrics.frame_times.len(), 512); } + + #[test] + fn profiling_retains_the_most_recent_samples_in_order() { + let mut metrics = RenderMetrics::new("test", true, Instant::now()); + for micros in 0..600 { + metrics.record_frame(Duration::from_micros(micros), &[], 0, FrameEffectMetrics); + } + assert_eq!( + metrics.frame_times.front(), + Some(&Duration::from_micros(88)) + ); + assert_eq!( + metrics.frame_times.back(), + Some(&Duration::from_micros(599)) + ); + assert!( + metrics + .frame_times + .iter() + .copied() + .eq((88..600).map(Duration::from_micros)) + ); + } } diff --git a/compositor/src/stacking.rs b/compositor/src/stacking.rs index d5b121d..952e0b8 100644 --- a/compositor/src/stacking.rs +++ b/compositor/src/stacking.rs @@ -1,4 +1,5 @@ use ferese_layout::WindowId; +use std::collections::HashMap; pub(crate) fn layer_priority(floating: bool, zooming: bool, fullscreen: bool) -> u8 { if fullscreen { @@ -16,17 +17,24 @@ pub(crate) fn layer_priority(floating: bool, zooming: bool, fullscreen: bool) -> #[derive(Default)] pub(crate) struct WindowStack { order: Vec, + ranks: HashMap, } impl WindowStack { pub fn insert(&mut self, id: WindowId) { - if !self.order.contains(&id) { + if let std::collections::hash_map::Entry::Vacant(entry) = self.ranks.entry(id) { + entry.insert(self.order.len()); self.order.push(id); } } pub fn remove(&mut self, id: WindowId) { - self.order.retain(|candidate| *candidate != id); + if let Some(rank) = self.ranks.remove(&id) { + self.order.remove(rank); + for (rank, candidate) in self.order.iter().enumerate().skip(rank) { + self.ranks.insert(*candidate, rank); + } + } } pub fn raise(&mut self, id: WindowId) { @@ -35,10 +43,7 @@ impl WindowStack { } pub fn rank(&self, id: WindowId) -> usize { - self.order - .iter() - .position(|candidate| *candidate == id) - .unwrap_or(usize::MAX) + self.ranks.get(&id).copied().unwrap_or(usize::MAX) } } @@ -46,6 +51,43 @@ impl WindowStack { mod tests { use super::*; + #[test] + fn cached_ranks_match_reference_order_after_mixed_operations() { + let mut stack = WindowStack::default(); + let mut reference = Vec::new(); + let mut random = 42_u64; + for _ in 0..2000 { + random = random.wrapping_mul(6364136223846793005).wrapping_add(1); + let id = WindowId((random >> 32) % 32); + match random % 3 { + 0 => { + stack.insert(id); + if !reference.contains(&id) { + reference.push(id); + } + } + 1 => { + stack.remove(id); + reference.retain(|candidate| *candidate != id); + } + _ => { + stack.raise(id); + reference.retain(|candidate| *candidate != id); + reference.push(id); + } + } + for id in (0..33).map(WindowId) { + assert_eq!( + stack.rank(id), + reference + .iter() + .position(|candidate| *candidate == id) + .unwrap_or(usize::MAX) + ); + } + } + } + #[test] fn floating_windows_stay_above_tiled_and_maximized_windows() { assert!(layer_priority(true, false, false) > layer_priority(false, true, false)); diff --git a/compositor/src/winit.rs b/compositor/src/winit.rs index 3782a26..f7d038e 100644 --- a/compositor/src/winit.rs +++ b/compositor/src/winit.rs @@ -1428,6 +1428,13 @@ fn overview_strip_elements( let cards = state.overview_workspace_cards(output); let mut elements = Vec::new(); let programs = rounded_clip_program(state, renderer); + // Resolve IDs once per strip, rather than scanning all managed windows + // separately for every miniature. Own the handles so rendering can mutate state. + let windows_by_id: std::collections::HashMap<_, _> = state + .window_ids + .iter() + .map(|(window, id)| (*id, window.clone())) + .collect(); for card in cards { // Front-to-back: outline and live miniatures above each card, all above // the strip. Nothing is painted behind the main overview window grid. @@ -1451,14 +1458,10 @@ fn overview_strip_elements( } if let Some(programs) = &programs { for (id, rect) in &card.windows { - if let Some(window) = state - .window_ids - .iter() - .find_map(|(window, candidate)| (*candidate == *id).then(|| window.clone())) - { + if let Some(window) = windows_by_id.get(id) { elements.extend(rounded_window_elements( renderer, - &window, + window, rounded_visual_rect(*rect, output_geometry.loc), physical_rect(*rect, output_geometry.loc, scale), scale, diff --git a/shell/ferese-shell/src/status.rs b/shell/ferese-shell/src/status.rs index a1aac33..8bfb37f 100644 --- a/shell/ferese-shell/src/status.rs +++ b/shell/ferese-shell/src/status.rs @@ -90,6 +90,16 @@ pub struct Service { pub generation: u64, } +type PollState = (Mutex<(u64, bool, Option, bool)>, Condvar); + +fn wait_for_poll(shared: &PollState) -> Option { + let state = shared + .1 + .wait_while(shared.0.lock().unwrap(), |state| state.1 && !state.3) + .unwrap(); + (!state.3).then_some(state.0) +} + impl Service { pub fn start(settings: Option>) -> Self { let live_settings = Arc::new(Mutex::new(settings)); @@ -105,12 +115,11 @@ impl Service { let polling = shared.clone(); thread::spawn(move || { loop { - let before = { - let state = polling.0.lock().unwrap(); - if state.3 { - break; - } - state.0 + // A poll overlapping a write is discarded below. Wait for the + // write to finish before retrying instead of launching commands + // repeatedly while a slow control operation is still running. + let Some(before) = wait_for_poll(&polling) else { + break; }; let snapshot = poll(); let state = polling.0.lock().unwrap(); @@ -476,6 +485,48 @@ fn execute(action: &Action, settings: Option<&[String]>) -> Result<(), String> { #[cfg(test)] mod tests { use super::*; + + #[test] + fn polling_waits_for_a_write_and_uses_the_completed_generation() { + let shared = Arc::new((Mutex::new((7, true, None, false)), Condvar::new())); + let worker_state = shared.clone(); + let (sender, receiver) = mpsc::channel(); + let worker = thread::spawn(move || { + sender.send(wait_for_poll(&worker_state)).unwrap(); + }); + assert!(matches!( + receiver.recv_timeout(Duration::from_millis(30)), + Err(mpsc::RecvTimeoutError::Timeout) + )); + { + let mut state = shared.0.lock().unwrap(); + state.0 = 8; + state.1 = false; + shared.1.notify_one(); + } + assert_eq!( + receiver.recv_timeout(Duration::from_secs(2)).unwrap(), + Some(8) + ); + worker.join().unwrap(); + } + + #[test] + fn shutdown_unblocks_polling_even_during_a_write() { + let shared = Arc::new((Mutex::new((7, true, None, false)), Condvar::new())); + let worker_state = shared.clone(); + let (sender, receiver) = mpsc::channel(); + let worker = thread::spawn(move || { + sender.send(wait_for_poll(&worker_state)).unwrap(); + }); + { + let mut state = shared.0.lock().unwrap(); + state.3 = true; + shared.1.notify_one(); + } + assert_eq!(receiver.recv_timeout(Duration::from_secs(2)).unwrap(), None); + worker.join().unwrap(); + } #[test] fn audio_parser_handles_mute_and_rejects_invalid_values() { assert_eq!(parse_audio("Volume: 0.42 [MUTED]"), Some((42, true)));