const HISTOGRAM_SIZE: usize = 120;
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
pub struct FrameTiming {
pub layout_time_ns: u64,
pub paint_time_ns: u64,
pub gpu_wait_time_ns: u64,
pub total_time_ns: u64,
}
impl FrameTiming {
pub fn from_ms(layout: f64, paint: f64, gpu_wait: f64, total: f64) -> Self {
Self {
layout_time_ns: (layout * 1_000_000.0) as u64,
paint_time_ns: (paint * 1_000_000.0) as u64,
gpu_wait_time_ns: (gpu_wait * 1_000_000.0) as u64,
total_time_ns: (total * 1_000_000.0) as u64,
}
}
#[inline]
pub fn add(&self, other: &Self) -> Self {
Self {
layout_time_ns: self.layout_time_ns.saturating_add(other.layout_time_ns),
paint_time_ns: self.paint_time_ns.saturating_add(other.paint_time_ns),
gpu_wait_time_ns: self.gpu_wait_time_ns.saturating_add(other.gpu_wait_time_ns),
total_time_ns: self.total_time_ns.saturating_add(other.total_time_ns),
}
}
#[inline]
pub fn div(&self, n: u64) -> Self {
if n == 0 {
return Self::default();
}
Self {
layout_time_ns: self.layout_time_ns / n,
paint_time_ns: self.paint_time_ns / n,
gpu_wait_time_ns: self.gpu_wait_time_ns / n,
total_time_ns: self.total_time_ns / n,
}
}
}
#[derive(Debug, Clone)]
pub struct FrameHistogram {
frames: [FrameTiming; HISTOGRAM_SIZE],
index: usize,
count: usize,
}
impl Default for FrameHistogram {
fn default() -> Self {
Self::new()
}
}
impl FrameHistogram {
pub fn new() -> Self {
Self {
frames: [FrameTiming::default(); HISTOGRAM_SIZE],
index: 0,
count: 0,
}
}
pub fn record(&mut self, timing: FrameTiming) {
self.frames[self.index] = timing;
self.index = (self.index + 1) % HISTOGRAM_SIZE;
if self.count < HISTOGRAM_SIZE {
self.count += 1;
}
}
pub fn average(&self) -> FrameTiming {
if self.count == 0 {
return FrameTiming::default();
}
let mut sum = FrameTiming::default();
for i in 0..self.count {
sum = sum.add(&self.frames[i]);
}
sum.div(self.count as u64)
}
pub fn percentile(&self, p: f32) -> FrameTiming {
if self.count == 0 {
return FrameTiming::default();
}
let mut totals: Vec<u64> = self.frames[..self.count]
.iter()
.map(|f| f.total_time_ns)
.collect();
totals.sort_unstable();
let idx = ((p.clamp(0.0, 100.0) / 100.0) * (self.count as f32 - 1.0)) as usize;
let target = totals[idx];
self.frames[..self.count]
.iter()
.find(|f| f.total_time_ns == target)
.copied()
.unwrap_or_default()
}
pub fn max(&self) -> FrameTiming {
self.frames[..self.count]
.iter()
.copied()
.max_by_key(|f| f.total_time_ns)
.unwrap_or_default()
}
pub fn frames(&self) -> &[FrameTiming] {
&self.frames[..self.count]
}
#[inline]
pub fn len(&self) -> usize {
self.count
}
#[inline]
pub fn is_empty(&self) -> bool {
self.count == 0
}
pub fn clear(&mut self) {
self.index = 0;
self.count = 0;
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub struct Rect {
pub x: i32,
pub y: i32,
pub width: u32,
pub height: u32,
}
impl Rect {
pub fn new(x: i32, y: i32, width: u32, height: u32) -> Self {
Self {
x,
y,
width,
height,
}
}
pub fn area(&self) -> u64 {
self.width as u64 * self.height as u64
}
pub fn intersects(&self, other: &Rect) -> bool {
let self_right = self.x + self.width as i32;
let self_bottom = self.y + self.height as i32;
let other_right = other.x + other.width as i32;
let other_bottom = other.y + other.height as i32;
self.x < other_right
&& self_right > other.x
&& self.y < other_bottom
&& self_bottom > other.y
}
pub fn union(&self, other: &Rect) -> Rect {
let x = self.x.min(other.x);
let y = self.y.min(other.y);
let right = (self.x + self.width as i32).max(other.x + other.width as i32);
let bottom = (self.y + self.height as i32).max(other.y + other.height as i32);
Rect::new(x, y, (right - x) as u32, (bottom - y) as u32)
}
}
#[derive(Debug, Clone)]
pub struct DirtyRectTracker {
rects: Vec<Rect>,
max_rects: usize,
}
impl DirtyRectTracker {
pub fn new(max_rects: usize) -> Self {
Self {
rects: Vec::with_capacity(max_rects),
max_rects,
}
}
pub fn add(&mut self, rect: Rect) {
if self.rects.len() >= self.max_rects {
self.rects.remove(0);
}
self.rects.push(rect);
}
#[inline]
pub fn rects(&self) -> &[Rect] {
&self.rects
}
pub fn clear(&mut self) {
self.rects.clear();
}
pub fn total_area(&self) -> u64 {
self.rects.iter().map(|r| r.area()).sum()
}
#[inline]
pub fn is_empty(&self) -> bool {
self.rects.is_empty()
}
#[inline]
pub fn len(&self) -> usize {
self.rects.len()
}
}
#[derive(Debug, Clone, Copy, Default, PartialEq)]
pub struct ArenaTelemetry {
pub total_slots: usize,
pub used_slots: usize,
pub free_slots: usize,
pub utilization_pct: f32,
pub compaction_count: u64,
}
impl ArenaTelemetry {
pub fn from_slots(total: usize, used: usize, compactions: u64) -> Self {
let free = total.saturating_sub(used);
let utilization_pct = if total > 0 {
(used as f32 / total as f32) * 100.0
} else {
0.0
};
Self {
total_slots: total,
used_slots: used,
free_slots: free,
utilization_pct,
compaction_count: compactions,
}
}
}
#[derive(Debug, Clone)]
pub struct DiagnosticHud {
enabled: bool,
histogram: FrameHistogram,
dirty_rects: DirtyRectTracker,
arena_telemetry: ArenaTelemetry,
}
impl Default for DiagnosticHud {
fn default() -> Self {
Self::new()
}
}
impl DiagnosticHud {
pub fn new() -> Self {
Self {
enabled: false,
histogram: FrameHistogram::new(),
dirty_rects: DirtyRectTracker::new(256),
arena_telemetry: ArenaTelemetry::default(),
}
}
pub fn toggle(&mut self) {
self.enabled = !self.enabled;
}
#[inline]
pub fn is_enabled(&self) -> bool {
self.enabled
}
pub fn record_frame(&mut self, timing: FrameTiming) {
self.histogram.record(timing);
}
pub fn add_dirty_rect(&mut self, rect: Rect) {
self.dirty_rects.add(rect);
}
pub fn update_arena_telemetry(&mut self, telemetry: ArenaTelemetry) {
self.arena_telemetry = telemetry;
}
#[inline]
pub fn histogram(&self) -> &FrameHistogram {
&self.histogram
}
#[inline]
pub fn dirty_rects(&self) -> &DirtyRectTracker {
&self.dirty_rects
}
#[inline]
pub fn arena_telemetry(&self) -> &ArenaTelemetry {
&self.arena_telemetry
}
pub fn clear_dirty_rects(&mut self) {
self.dirty_rects.clear();
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn frame_timing_from_ms() {
let t = FrameTiming::from_ms(0.5, 0.3, 0.1, 0.9);
assert_eq!(t.layout_time_ns, 500_000);
assert_eq!(t.paint_time_ns, 300_000);
assert_eq!(t.gpu_wait_time_ns, 100_000);
assert_eq!(t.total_time_ns, 900_000);
}
#[test]
fn frame_timing_add_and_div() {
let a = FrameTiming {
total_time_ns: 10,
..Default::default()
};
let b = FrameTiming {
total_time_ns: 20,
..Default::default()
};
let sum = a.add(&b);
assert_eq!(sum.total_time_ns, 30);
let avg = sum.div(2);
assert_eq!(avg.total_time_ns, 15);
let div_zero = sum.div(0);
assert_eq!(div_zero.total_time_ns, 0);
}
#[test]
fn histogram_new_is_empty() {
let hist = FrameHistogram::new();
assert!(hist.is_empty());
assert_eq!(hist.len(), 0);
}
#[test]
fn histogram_record_and_average() {
let mut hist = FrameHistogram::new();
hist.record(FrameTiming {
total_time_ns: 10_000_000,
..Default::default()
});
hist.record(FrameTiming {
total_time_ns: 20_000_000,
..Default::default()
});
assert_eq!(hist.len(), 2);
let avg = hist.average();
assert_eq!(avg.total_time_ns, 15_000_000);
}
#[test]
fn histogram_max() {
let mut hist = FrameHistogram::new();
hist.record(FrameTiming {
total_time_ns: 10,
..Default::default()
});
hist.record(FrameTiming {
total_time_ns: 30,
..Default::default()
});
hist.record(FrameTiming {
total_time_ns: 20,
..Default::default()
});
assert_eq!(hist.max().total_time_ns, 30);
}
#[test]
fn histogram_percentile() {
let mut hist = FrameHistogram::new();
for i in 1..=100 {
hist.record(FrameTiming {
total_time_ns: i * 1_000_000,
..Default::default()
});
}
let p50 = hist.percentile(50.0);
assert!(p50.total_time_ns >= 49_000_000 && p50.total_time_ns <= 51_000_000);
let p99 = hist.percentile(99.0);
assert!(p99.total_time_ns >= 98_000_000);
}
#[test]
fn histogram_ring_buffer_wrap() {
let mut hist = FrameHistogram::new();
for i in 0..150 {
hist.record(FrameTiming {
total_time_ns: i,
..Default::default()
});
}
assert_eq!(hist.len(), 120);
}
#[test]
fn histogram_clear() {
let mut hist = FrameHistogram::new();
hist.record(FrameTiming::default());
hist.clear();
assert!(hist.is_empty());
}
#[test]
fn rect_area() {
assert_eq!(Rect::new(0, 0, 100, 50).area(), 5_000);
assert_eq!(Rect::new(10, 20, 0, 100).area(), 0);
}
#[test]
fn rect_intersects() {
let a = Rect::new(0, 0, 100, 100);
let b = Rect::new(50, 50, 100, 100);
let c = Rect::new(200, 200, 50, 50);
assert!(a.intersects(&b));
assert!(!a.intersects(&c));
}
#[test]
fn rect_union() {
let a = Rect::new(0, 0, 100, 100);
let b = Rect::new(50, 50, 100, 100);
let u = a.union(&b);
assert_eq!(u.x, 0);
assert_eq!(u.y, 0);
assert_eq!(u.width, 150);
assert_eq!(u.height, 150);
}
#[test]
fn dirty_rect_tracker_basic() {
let mut tracker = DirtyRectTracker::new(64);
assert!(tracker.is_empty());
tracker.add(Rect::new(0, 0, 100, 100));
tracker.add(Rect::new(50, 50, 100, 100));
assert_eq!(tracker.len(), 2);
assert!(!tracker.is_empty());
assert!(tracker.total_area() > 0);
tracker.clear();
assert!(tracker.is_empty());
}
#[test]
fn dirty_rect_tracker_eviction() {
let mut tracker = DirtyRectTracker::new(2);
tracker.add(Rect::new(0, 0, 10, 10));
tracker.add(Rect::new(10, 10, 10, 10));
tracker.add(Rect::new(20, 20, 10, 10));
assert_eq!(tracker.len(), 2);
assert_eq!(tracker.rects()[0], Rect::new(10, 10, 10, 10));
}
#[test]
fn arena_telemetry_from_slots() {
let t = ArenaTelemetry::from_slots(1000, 750, 3);
assert_eq!(t.total_slots, 1000);
assert_eq!(t.used_slots, 750);
assert_eq!(t.free_slots, 250);
assert_eq!(t.utilization_pct, 75.0);
assert_eq!(t.compaction_count, 3);
}
#[test]
fn arena_telemetry_zero_total() {
let t = ArenaTelemetry::from_slots(0, 0, 0);
assert_eq!(t.utilization_pct, 0.0);
}
#[test]
fn hud_toggle() {
let mut hud = DiagnosticHud::new();
assert!(!hud.is_enabled());
hud.toggle();
assert!(hud.is_enabled());
hud.toggle();
assert!(!hud.is_enabled());
}
#[test]
fn hud_record_frame_and_dirty_rect() {
let mut hud = DiagnosticHud::new();
hud.record_frame(FrameTiming {
total_time_ns: 16_000_000,
..Default::default()
});
hud.add_dirty_rect(Rect::new(0, 0, 100, 100));
assert_eq!(hud.histogram().len(), 1);
assert_eq!(hud.dirty_rects().len(), 1);
hud.clear_dirty_rects();
assert_eq!(hud.dirty_rects().len(), 0);
}
#[test]
fn hud_arena_telemetry_update() {
let mut hud = DiagnosticHud::new();
hud.update_arena_telemetry(ArenaTelemetry::from_slots(500, 250, 1));
assert_eq!(hud.arena_telemetry().used_slots, 250);
}
}