Files
feresaria/compositor/src/resize_metrics.rs
T
Samuel Onoja 28e03a2624 Use analytic springs and instrument resize barriers
Share a stable damped oscillator solver across scalar and rectangle animations while preserving retarget and explicit no-crossing presentation policies. Add deterministic traces, scrolling and zoom continuity coverage, and pause-aware advancement.

Measure resize barrier and handoff timing behind resize-metrics, expose the feature through the release installer, and wait for the intended backend in isolated portal tests.
2026-10-02 00:49:52 +01:00

242 lines
8.6 KiB
Rust

//! Opt-in resize diagnostics. No measurement code exists without resize-metrics.
//! Times are unscaled wall time except handoff_animation, which is the existing
//! handoff clock (affected by animation speed and pauses).
use std::collections::HashMap;
use std::time::Duration;
use ferese_core::WorkspaceId;
use ferese_layout::WindowId;
const BUCKETS_MS: [u64; 16] = [16, 32, 64, 75, 80, 85, 100, 150, 250, 290, 299, 300, 301, 310, 350, 500];
#[derive(Debug, Default)]
struct Histogram {
counts: [u64; 17],
}
impl Histogram {
fn add(&mut self, duration: Duration) {
let index = BUCKETS_MS.partition_point(|ms| Duration::from_millis(*ms) < duration);
self.counts[index] += 1;
}
}
#[derive(Clone, Copy, Debug)]
pub(crate) enum End {
Commit,
Deadline,
Replaced,
Cancelled,
}
#[derive(Default, Debug)]
struct Client {
workspace: Option<WorkspaceId>,
app_id: String,
started: Option<Duration>,
target_changed: Option<Duration>,
last_observation: Duration,
handoff_started: Option<Duration>,
barriers: u64,
deadlines: u64,
commits: u64,
replaced: u64,
cancelled: u64,
viewport_pauses: u64,
other_window_pauses: u64,
viewport_seen: bool,
others_seen: bool,
viewport_overlap: Duration,
other_overlap: Duration,
barrier_duration: Histogram,
viewport_blocked: Histogram,
other_windows_blocked: Histogram,
target_to_handoff: Histogram,
handoff_wall: Histogram,
handoff_animation: Histogram,
}
#[derive(Default)]
pub(crate) struct ResizeMetrics {
clients: HashMap<WindowId, Client>,
workspace_started: HashMap<WorkspaceId, Duration>,
workspace_duration: Histogram,
}
impl ResizeMetrics {
pub(crate) fn begin(&mut self, id: WindowId, workspace: Option<WorkspaceId>, app_id: &str, now: Duration) {
// Keep an overlapping workspace barrier continuous through retargets.
let previous_workspace = self.end_client(id, now, End::Replaced);
if previous_workspace != workspace {
self.finish_workspace(previous_workspace, now);
}
let client = self.clients.entry(id).or_default();
client.workspace = workspace;
client.app_id.clear();
client.app_id.push_str(app_id);
client.started = Some(now);
client.target_changed = Some(now);
client.last_observation = now;
client.handoff_started = None;
client.viewport_seen = false;
client.others_seen = false;
client.viewport_overlap = Duration::ZERO;
client.other_overlap = Duration::ZERO;
client.barriers += 1;
if let Some(workspace) = workspace {
self.workspace_started.entry(workspace).or_insert(now);
}
}
fn end_client(&mut self, id: WindowId, now: Duration, reason: End) -> Option<WorkspaceId> {
let client = self.clients.get_mut(&id)?;
let started = client.started.take()?;
client.barrier_duration.add(now.saturating_sub(started));
client.viewport_blocked.add(client.viewport_overlap);
client.other_windows_blocked.add(client.other_overlap);
match reason {
End::Commit => client.commits += 1,
End::Deadline => client.deadlines += 1,
End::Replaced => client.replaced += 1,
End::Cancelled => client.cancelled += 1,
}
client.workspace
}
pub(crate) fn end(&mut self, id: WindowId, now: Duration, reason: End) {
let Some(workspace) = self.end_client(id, now, reason) else {
return;
};
self.finish_workspace(Some(workspace), now);
}
fn finish_workspace(&mut self, workspace: Option<WorkspaceId>, now: Duration) {
let Some(workspace) = workspace else { return };
if !self
.clients
.values()
.any(|client| client.workspace == Some(workspace) && client.started.is_some())
&& let Some(started) = self.workspace_started.remove(&workspace)
{
self.workspace_duration.add(now.saturating_sub(started));
}
}
pub(crate) fn observe(&mut self, id: WindowId, now: Duration, viewport_active: bool, other_active: bool) {
let Some(client) = self.clients.get_mut(&id) else {
return;
};
if client.started.is_none() {
return;
}
let interval = now.saturating_sub(client.last_observation);
client.last_observation = now;
if viewport_active {
client.viewport_overlap += interval;
if !client.viewport_seen {
client.viewport_seen = true;
client.viewport_pauses += 1;
}
}
if other_active {
client.other_overlap += interval;
if !client.others_seen {
client.others_seen = true;
client.other_window_pauses += 1;
}
}
}
pub(crate) fn pause_clock(&mut self, now: Duration) {
// Activity overlap excludes globally paused presentation time. Barrier
// and target-to-handoff durations remain actual wall-clock durations.
for client in self.clients.values_mut().filter(|client| client.started.is_some()) {
client.last_observation = now;
}
}
pub(crate) fn handoff_tick(&mut self, id: WindowId, now: Duration, blocked: bool) {
let Some(client) = self.clients.get_mut(&id) else {
return;
};
if !blocked && client.handoff_started.is_none() {
client.handoff_started = Some(now);
if let Some(changed) = client.target_changed.take() {
client.target_to_handoff.add(now.saturating_sub(changed));
}
}
}
pub(crate) fn handoff_end(&mut self, id: WindowId, now: Duration, elapsed: Duration) {
let Some(client) = self.clients.get_mut(&id) else {
return;
};
if let Some(started) = client.handoff_started.take() {
client.handoff_wall.add(now.saturating_sub(started));
client.handoff_animation.add(elapsed);
}
}
pub(crate) fn dump(&self) {
// Enabling the feature explicitly opts into the exit report. This does
// not depend on a tracing filter and adds no per-frame log messages.
eprintln!("resize-metrics: histogram upper bounds ms {BUCKETS_MS:?}, final bucket overflow");
eprintln!(
"resize-metrics: workspace barrier durations {:?}",
self.workspace_duration
);
for (id, client) in &self.clients {
eprintln!("resize-metrics: client {id:?} {client:?}");
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn overlapping_clients_deadlines_and_handoff_are_measured_separately() {
let mut metrics = ResizeMetrics::default();
let ws = Some(WorkspaceId(1));
let ms = Duration::from_millis;
metrics.begin(WindowId(1), ws, "terminal", ms(0));
metrics.begin(WindowId(2), ws, "editor", ms(50));
metrics.observe(WindowId(1), ms(100), true, true);
metrics.observe(WindowId(1), ms(300), true, true);
metrics.end(WindowId(1), ms(300), End::Deadline);
assert!(metrics.workspace_started.contains_key(&WorkspaceId(1)));
metrics.end(WindowId(2), ms(310), End::Commit);
assert!(metrics.workspace_started.is_empty());
metrics.handoff_tick(WindowId(1), ms(310), false);
metrics.handoff_end(WindowId(1), ms(390), ms(80));
let client = &metrics.clients[&WindowId(1)];
assert_eq!(client.deadlines, 1);
assert_eq!(client.viewport_pauses, 1);
assert_eq!(client.other_window_pauses, 1);
assert_eq!(client.viewport_overlap, ms(300));
assert_eq!(client.other_overlap, ms(300));
assert_eq!(client.barrier_duration.counts[11], 1);
assert_eq!(client.handoff_animation.counts[4], 1);
assert_eq!(client.handoff_wall.counts[4], 1);
assert_eq!(metrics.workspace_duration.counts[13], 1);
}
#[test]
fn globally_paused_time_is_not_counted_as_animation_blocking_overlap() {
let mut metrics = ResizeMetrics::default();
let ms = Duration::from_millis;
metrics.begin(WindowId(1), Some(WorkspaceId(1)), "terminal", ms(0));
metrics.observe(WindowId(1), ms(40), true, true);
metrics.pause_clock(ms(340));
metrics.observe(WindowId(1), ms(356), true, true);
metrics.end(WindowId(1), ms(356), End::Deadline);
let client = &metrics.clients[&WindowId(1)];
assert_eq!(client.viewport_overlap, ms(56));
assert_eq!(client.other_overlap, ms(56));
assert_eq!(client.barrier_duration.counts[15], 1);
assert_eq!(client.deadlines, 1);
}
}