use std::collections::HashMap;
#[derive(Debug, Clone, Default)]
pub struct DependencyGraph {
deps: HashMap<u64, std::collections::HashSet<u64>>,
reverse: HashMap<u64, Vec<u64>>,
}
impl DependencyGraph {
pub fn new() -> Self {
Self {
deps: HashMap::new(),
reverse: HashMap::new(),
}
}
pub fn register(&mut self, component_id: u64, state_key: u64) {
let is_new = self.deps
.entry(state_key)
.or_default()
.insert(component_id);
if is_new {
self.reverse
.entry(component_id)
.or_default()
.push(state_key);
}
}
pub fn unregister(&mut self, component_id: u64) {
if let Some(keys) = self.reverse.remove(&component_id) {
for key in keys {
if let Some(set) = self.deps.get_mut(&key) {
set.remove(&component_id);
}
}
}
}
pub fn affected_components(&self, state_key: u64) -> impl Iterator<Item = u64> + '_ {
self.deps
.get(&state_key)
.into_iter()
.flat_map(|set| set.iter().copied())
}
pub fn has_dependents(&self, state_key: u64) -> bool {
self.deps
.get(&state_key)
.map_or(false, |set| !set.is_empty())
}
pub fn edge_count(&self) -> usize {
self.deps.values().map(|s| s.len()).sum()
}
}
use std::time::{Duration, Instant};
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct SubsystemBudget {
pub time_slice: Duration,
pub skippable: bool,
pub name: &'static str,
}
#[derive(Debug)]
pub struct FrameBudgetTracker {
total: Duration,
allocations: Vec<SubsystemBudget>,
start: Option<Instant>,
elapsed: Vec<Duration>,
}
impl FrameBudgetTracker {
pub fn default_60fps() -> Self {
Self {
total: Duration::from_micros(16_666), allocations: vec![
SubsystemBudget {
time_slice: Duration::from_micros(4_000),
skippable: true,
name: "animation",
},
SubsystemBudget {
time_slice: Duration::from_micros(4_000),
skippable: true,
name: "layout",
},
SubsystemBudget {
time_slice: Duration::from_micros(8_000),
skippable: false, name: "render",
},
],
start: None,
elapsed: vec![Duration::ZERO, Duration::ZERO, Duration::ZERO],
}
}
pub fn default_120fps() -> Self {
Self {
total: Duration::from_micros(8_333), allocations: vec![
SubsystemBudget {
time_slice: Duration::from_micros(2_000),
skippable: true,
name: "animation",
},
SubsystemBudget {
time_slice: Duration::from_micros(2_000),
skippable: true,
name: "layout",
},
SubsystemBudget {
time_slice: Duration::from_micros(4_000),
skippable: false, name: "render",
},
],
start: None,
elapsed: vec![Duration::ZERO, Duration::ZERO, Duration::ZERO],
}
}
pub fn total(&self) -> Duration {
self.total
}
pub fn allocations(&self) -> &[SubsystemBudget] {
&self.allocations
}
pub fn new_frame(&mut self) {
self.start = Some(Instant::now());
for e in self.elapsed.iter_mut() {
*e = Duration::ZERO;
}
}
pub fn subsystem_finish(&mut self, index: usize) {
if let Some(start) = self.start {
if index < self.elapsed.len() {
let now = Instant::now();
self.elapsed[index] = now.duration_since(start);
}
}
}
pub fn is_within_budget(&self, index: usize) -> bool {
if index >= self.allocations.len() {
return false;
}
if index >= self.elapsed.len() {
return false;
}
self.elapsed[index] <= self.allocations[index].time_slice
}
pub fn frame_within_budget(&self) -> bool {
for (i, alloc) in self.allocations.iter().enumerate() {
if i < self.elapsed.len()
&& self.elapsed[i] > alloc.time_slice
&& !alloc.skippable
{
return false;
}
}
true
}
pub fn elapsed(&self, index: usize) -> Duration {
if index < self.elapsed.len() {
self.elapsed[index]
} else {
Duration::ZERO
}
}
pub fn total_elapsed(&self) -> Duration {
match self.start {
Some(start) => start.elapsed(),
None => Duration::ZERO,
}
}
}
#[derive(Debug, Clone)]
pub struct InputLatencyTracker {
window_size: usize,
samples: std::collections::VecDeque<(Instant, Instant)>,
}
impl InputLatencyTracker {
pub fn new(window_size: usize) -> Self {
Self {
window_size,
samples: std::collections::VecDeque::with_capacity(window_size),
}
}
pub fn record_frame(&mut self, event_time: Instant, render_time: Instant) {
if self.window_size == 0 {
return;
}
if self.samples.len() >= self.window_size {
self.samples.pop_front();
}
self.samples.push_back((event_time, render_time));
}
pub fn percentile(&self, p: f64) -> Duration {
if self.samples.is_empty() || p < 0.0 || p > 100.0 {
return Duration::ZERO;
}
let mut latencies: Vec<Duration> = self.samples
.iter()
.map(|&(e, r)| {
if r > e {
r.duration_since(e)
} else {
Duration::ZERO
}
})
.collect();
latencies.sort();
let len = latencies.len();
let rank = p / 100.0;
let index = ((len as f64 * rank).ceil() as usize).saturating_sub(1);
let index = index.min(len - 1);
latencies[index]
}
pub fn clear(&mut self) {
self.samples.clear();
}
pub fn window_size(&self) -> usize {
self.window_size
}
pub fn set_window_size(&mut self, size: usize) {
self.window_size = size;
while self.samples.len() > self.window_size {
self.samples.pop_front();
}
}
pub fn len(&self) -> usize {
self.samples.len()
}
pub fn is_empty(&self) -> bool {
self.samples.is_empty()
}
}
#[cfg(test)]
mod tests {
use super::*;
mod p1_42_dependency_graph_tests {
use super::DependencyGraph;
#[test]
fn register_and_query_single_dep() {
let mut g = DependencyGraph::new();
g.register(42, 100); let affected: Vec<u64> = g.affected_components(100).collect();
assert_eq!(affected, vec![42]);
}
#[test]
fn unregister_removes_component() {
let mut g = DependencyGraph::new();
g.register(1, 10);
g.register(2, 10);
g.unregister(1);
let affected: Vec<u64> = g.affected_components(10).collect();
assert!(!affected.contains(&1), "component 1 must be gone after unregister");
assert!(affected.contains(&2), "component 2 must still be present");
}
#[test]
fn no_deps_returns_empty() {
let g = DependencyGraph::new();
let affected: Vec<u64> = g.affected_components(999).collect();
assert!(affected.is_empty());
assert!(!g.has_dependents(999));
}
#[test]
fn edge_count_reflects_registrations() {
let mut g = DependencyGraph::new();
assert_eq!(g.edge_count(), 0);
g.register(1, 10);
g.register(2, 10);
g.register(1, 20); assert_eq!(g.edge_count(), 3);
}
#[test]
fn re_register_after_unregister_works() {
let mut g = DependencyGraph::new();
g.register(5, 50);
g.unregister(5);
g.register(5, 60);
assert!(!g.has_dependents(50), "old key must be gone");
assert!(g.has_dependents(60), "new key must be present");
}
}
mod p1_43_frame_budget_tests {
use super::FrameBudgetTracker;
#[test]
fn default_60fps_has_16ms_total() {
let fb = FrameBudgetTracker::default_60fps();
assert!(fb.total().as_micros() >= 16_000);
assert!(fb.total().as_micros() <= 17_000);
}
#[test]
fn default_allocations_sum_to_roughly_total() {
let fb = FrameBudgetTracker::default_60fps();
let sum: u128 = fb.allocations().iter()
.map(|a| a.time_slice.as_micros())
.sum();
let total = fb.total().as_micros();
assert!(sum <= total);
assert!(sum >= total - 1_000); }
#[test]
fn render_is_essential_layout_is_skippable() {
let fb = FrameBudgetTracker::default_60fps();
let render = fb.allocations().iter().find(|a| a.name == "render").unwrap();
let layout = fb.allocations().iter().find(|a| a.name == "layout").unwrap();
assert!(!render.skippable, "render must always run");
assert!(layout.skippable, "layout can be skipped if over budget");
}
#[test]
fn new_frame_resets_state() {
let mut fb = FrameBudgetTracker::default_60fps();
fb.new_frame();
for i in 0..fb.allocations().len() {
assert_eq!(fb.elapsed(i).as_nanos(), 0);
}
}
#[test]
fn is_within_budget_initially_true() {
let mut fb = FrameBudgetTracker::default_60fps();
fb.new_frame();
for i in 0..fb.allocations().len() {
assert!(fb.is_within_budget(i));
}
}
#[test]
fn frame_within_budget_initially_true() {
let mut fb = FrameBudgetTracker::default_60fps();
fb.new_frame();
assert!(fb.frame_within_budget());
}
}
mod p2_36_input_latency_tests {
use super::InputLatencyTracker;
use std::time::{Duration, Instant};
#[test]
fn test_empty_tracker() {
let tracker = InputLatencyTracker::new(10);
assert!(tracker.is_empty());
assert_eq!(tracker.len(), 0);
assert_eq!(tracker.percentile(50.0), Duration::ZERO);
}
#[test]
fn test_record_and_sliding_window() {
let mut tracker = InputLatencyTracker::new(3);
let now = Instant::now();
tracker.record_frame(now, now + Duration::from_millis(10));
tracker.record_frame(now, now + Duration::from_millis(20));
tracker.record_frame(now, now + Duration::from_millis(30));
assert_eq!(tracker.len(), 3);
tracker.record_frame(now, now + Duration::from_millis(40));
assert_eq!(tracker.len(), 3);
assert_eq!(tracker.percentile(50.0), Duration::from_millis(30));
assert_eq!(tracker.percentile(0.0), Duration::from_millis(20));
assert_eq!(tracker.percentile(100.0), Duration::from_millis(40));
}
#[test]
fn test_resize_and_clear() {
let mut tracker = InputLatencyTracker::new(5);
let now = Instant::now();
for i in 1..=5 {
tracker.record_frame(now, now + Duration::from_millis(i * 10));
}
assert_eq!(tracker.len(), 5);
tracker.set_window_size(3);
assert_eq!(tracker.window_size(), 3);
assert_eq!(tracker.len(), 3);
assert_eq!(tracker.percentile(0.0), Duration::from_millis(30));
assert_eq!(tracker.percentile(100.0), Duration::from_millis(50));
tracker.clear();
assert!(tracker.is_empty());
assert_eq!(tracker.percentile(50.0), Duration::ZERO);
}
#[test]
fn test_invalid_percentiles() {
let mut tracker = InputLatencyTracker::new(2);
let now = Instant::now();
tracker.record_frame(now, now + Duration::from_millis(10));
assert_eq!(tracker.percentile(-1.0), Duration::ZERO);
assert_eq!(tracker.percentile(101.0), Duration::ZERO);
}
}
}