Reduce polling retries and repeated compositor lookups

This commit is contained in:
Samuel Onoja committed 2026-09-27 06:58:09 +01:00
1 parent dcca5fec88
commit 917379d548
4 files changed
+146 -24

No files matched your search

+32 -6
View File
@@ -1,4 +1,7 @@
use std::time::{Duration, Instant}; use std::{
collections::VecDeque,
time::{Duration, Instant},
};
use smithay::utils::{Physical, Rectangle}; use smithay::utils::{Physical, Rectangle};
@@ -16,7 +19,7 @@ pub(crate) struct RenderMetrics {
damaged_pixels: u64, damaged_pixels: u64,
total_render_time: Duration, total_render_time: Duration,
longest_render_time: Duration, longest_render_time: Duration,
frame_times: Vec<Duration>, frame_times: VecDeque<Duration>,
} }
impl RenderMetrics { impl RenderMetrics {
@@ -37,7 +40,7 @@ impl RenderMetrics {
damaged_pixels: 0, damaged_pixels: 0,
total_render_time: Duration::ZERO, total_render_time: Duration::ZERO,
longest_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.total_render_time += render_time;
self.longest_render_time = self.longest_render_time.max(render_time); self.longest_render_time = self.longest_render_time.max(render_time);
if self.frame_times.len() == 512 { 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 now = Instant::now();
let elapsed = now.saturating_duration_since(self.interval_started); let elapsed = now.saturating_duration_since(self.interval_started);
@@ -72,7 +75,7 @@ impl RenderMetrics {
.as_micros() .as_micros()
.checked_div(u128::from(self.rendered_frames)) .checked_div(u128::from(self.rendered_frames))
.unwrap_or(0); .unwrap_or(0);
self.frame_times.sort_unstable(); self.frame_times.make_contiguous().sort_unstable();
let percentile = |percent: usize| { let percentile = |percent: usize| {
let index = (self.frame_times.len() * percent) let index = (self.frame_times.len() * percent)
.div_ceil(100) .div_ceil(100)
@@ -159,4 +162,27 @@ mod tests {
} }
assert_eq!(metrics.frame_times.len(), 512); 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))
);
}
} }
+48 -6
View File
@@ -1,4 +1,5 @@
use ferese_layout::WindowId; use ferese_layout::WindowId;
use std::collections::HashMap;
pub(crate) fn layer_priority(floating: bool, zooming: bool, fullscreen: bool) -> u8 { pub(crate) fn layer_priority(floating: bool, zooming: bool, fullscreen: bool) -> u8 {
if fullscreen { if fullscreen {
@@ -16,17 +17,24 @@ pub(crate) fn layer_priority(floating: bool, zooming: bool, fullscreen: bool) ->
#[derive(Default)] #[derive(Default)]
pub(crate) struct WindowStack { pub(crate) struct WindowStack {
order: Vec<WindowId>, order: Vec<WindowId>,
ranks: HashMap<WindowId, usize>,
} }
impl WindowStack { impl WindowStack {
pub fn insert(&mut self, id: WindowId) { 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); self.order.push(id);
} }
} }
pub fn remove(&mut self, id: WindowId) { 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) { pub fn raise(&mut self, id: WindowId) {
@@ -35,10 +43,7 @@ impl WindowStack {
} }
pub fn rank(&self, id: WindowId) -> usize { pub fn rank(&self, id: WindowId) -> usize {
self.order self.ranks.get(&id).copied().unwrap_or(usize::MAX)
.iter()
.position(|candidate| *candidate == id)
.unwrap_or(usize::MAX)
} }
} }
@@ -46,6 +51,43 @@ impl WindowStack {
mod tests { mod tests {
use super::*; 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] #[test]
fn floating_windows_stay_above_tiled_and_maximized_windows() { fn floating_windows_stay_above_tiled_and_maximized_windows() {
assert!(layer_priority(true, false, false) > layer_priority(false, true, false)); assert!(layer_priority(true, false, false) > layer_priority(false, true, false));
+9 -6
View File
@@ -1428,6 +1428,13 @@ fn overview_strip_elements(
let cards = state.overview_workspace_cards(output); let cards = state.overview_workspace_cards(output);
let mut elements = Vec::new(); let mut elements = Vec::new();
let programs = rounded_clip_program(state, renderer); 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 { for card in cards {
// Front-to-back: outline and live miniatures above each card, all above // Front-to-back: outline and live miniatures above each card, all above
// the strip. Nothing is painted behind the main overview window grid. // the strip. Nothing is painted behind the main overview window grid.
@@ -1451,14 +1458,10 @@ fn overview_strip_elements(
} }
if let Some(programs) = &programs { if let Some(programs) = &programs {
for (id, rect) in &card.windows { for (id, rect) in &card.windows {
if let Some(window) = state if let Some(window) = windows_by_id.get(id) {
.window_ids
.iter()
.find_map(|(window, candidate)| (*candidate == *id).then(|| window.clone()))
{
elements.extend(rounded_window_elements( elements.extend(rounded_window_elements(
renderer, renderer,
&window, window,
rounded_visual_rect(*rect, output_geometry.loc), rounded_visual_rect(*rect, output_geometry.loc),
physical_rect(*rect, output_geometry.loc, scale), physical_rect(*rect, output_geometry.loc, scale),
scale, scale,
+57 -6
View File
@@ -90,6 +90,16 @@ pub struct Service {
pub generation: u64, pub generation: u64,
} }
type PollState = (Mutex<(u64, bool, Option<String>, bool)>, Condvar);
fn wait_for_poll(shared: &PollState) -> Option<u64> {
let state = shared
.1
.wait_while(shared.0.lock().unwrap(), |state| state.1 && !state.3)
.unwrap();
(!state.3).then_some(state.0)
}
impl Service { impl Service {
pub fn start(settings: Option<Vec<String>>) -> Self { pub fn start(settings: Option<Vec<String>>) -> Self {
let live_settings = Arc::new(Mutex::new(settings)); let live_settings = Arc::new(Mutex::new(settings));
@@ -105,12 +115,11 @@ impl Service {
let polling = shared.clone(); let polling = shared.clone();
thread::spawn(move || { thread::spawn(move || {
loop { loop {
let before = { // A poll overlapping a write is discarded below. Wait for the
let state = polling.0.lock().unwrap(); // write to finish before retrying instead of launching commands
if state.3 { // repeatedly while a slow control operation is still running.
break; let Some(before) = wait_for_poll(&polling) else {
} break;
state.0
}; };
let snapshot = poll(); let snapshot = poll();
let state = polling.0.lock().unwrap(); let state = polling.0.lock().unwrap();
@@ -476,6 +485,48 @@ fn execute(action: &Action, settings: Option<&[String]>) -> Result<(), String> {
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use super::*; 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] #[test]
fn audio_parser_handles_mute_and_rejects_invalid_values() { fn audio_parser_handles_mute_and_rejects_invalid_values() {
assert_eq!(parse_audio("Volume: 0.42 [MUTED]"), Some((42, true))); assert_eq!(parse_audio("Volume: 0.42 [MUTED]"), Some((42, true)));