martensite_devtools/hud.rs
1//! In-app diagnostic HUD overlay.
2//!
3//! Activating F12 toggles a zero-allocation diagnostic overlay that
4//! displays:
5//!
6//! - A rolling 120-frame timing histogram (layout, paint, GPU wait).
7//! - Real-time dirty rect visualization.
8//! - Live `WidgetArena` slot utilization and compaction telemetry.
9//!
10//! # Example
11//!
12//! ```
13//! use martensite_devtools::hud::{DiagnosticHud, FrameTiming, Rect};
14//!
15//! let mut hud = DiagnosticHud::new();
16//! hud.toggle();
17//! assert!(hud.is_enabled());
18//!
19//! hud.record_frame(FrameTiming {
20//! layout_time_ns: 500_000,
21//! paint_time_ns: 300_000,
22//! gpu_wait_time_ns: 100_000,
23//! total_time_ns: 900_000,
24//! });
25//!
26//! hud.add_dirty_rect(Rect::new(0, 0, 100, 100));
27//! ```
28
29/// Number of frames retained in the rolling histogram.
30const HISTOGRAM_SIZE: usize = 120;
31
32/// Frame timing data for a single frame.
33///
34/// All times are in nanoseconds for consistent units.
35///
36/// # Example
37///
38/// ```
39/// use martensite_devtools::hud::FrameTiming;
40///
41/// let timing = FrameTiming {
42/// layout_time_ns: 500_000,
43/// paint_time_ns: 300_000,
44/// gpu_wait_time_ns: 100_000,
45/// total_time_ns: 900_000,
46/// };
47/// assert_eq!(timing.total_time_ns, 900_000);
48/// ```
49#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
50pub struct FrameTiming {
51 /// Time spent in layout passes (nanoseconds).
52 pub layout_time_ns: u64,
53 /// Time spent encoding the paint list (nanoseconds).
54 pub paint_time_ns: u64,
55 /// Time spent waiting for the GPU (nanoseconds).
56 pub gpu_wait_time_ns: u64,
57 /// Total frame time (nanoseconds).
58 pub total_time_ns: u64,
59}
60
61impl FrameTiming {
62 /// Creates a `FrameTiming` from millisecond values.
63 ///
64 /// # Example
65 ///
66 /// ```
67 /// use martensite_devtools::hud::FrameTiming;
68 ///
69 /// let t = FrameTiming::from_ms(0.5, 0.3, 0.1, 0.9);
70 /// assert_eq!(t.layout_time_ns, 500_000);
71 /// ```
72 pub fn from_ms(layout: f64, paint: f64, gpu_wait: f64, total: f64) -> Self {
73 Self {
74 layout_time_ns: (layout * 1_000_000.0) as u64,
75 paint_time_ns: (paint * 1_000_000.0) as u64,
76 gpu_wait_time_ns: (gpu_wait * 1_000_000.0) as u64,
77 total_time_ns: (total * 1_000_000.0) as u64,
78 }
79 }
80
81 /// Adds two `FrameTiming` values component-wise.
82 #[inline]
83 pub fn add(&self, other: &Self) -> Self {
84 Self {
85 layout_time_ns: self.layout_time_ns.saturating_add(other.layout_time_ns),
86 paint_time_ns: self.paint_time_ns.saturating_add(other.paint_time_ns),
87 gpu_wait_time_ns: self.gpu_wait_time_ns.saturating_add(other.gpu_wait_time_ns),
88 total_time_ns: self.total_time_ns.saturating_add(other.total_time_ns),
89 }
90 }
91
92 /// Divides all components by a scalar.
93 #[inline]
94 pub fn div(&self, n: u64) -> Self {
95 if n == 0 {
96 return Self::default();
97 }
98 Self {
99 layout_time_ns: self.layout_time_ns / n,
100 paint_time_ns: self.paint_time_ns / n,
101 gpu_wait_time_ns: self.gpu_wait_time_ns / n,
102 total_time_ns: self.total_time_ns / n,
103 }
104 }
105}
106
107/// Rolling 120-frame timing histogram.
108///
109/// Stores the last `120` frame timings in a fixed-size ring buffer
110/// with zero heap allocation.
111///
112/// # Example
113///
114/// ```
115/// use martensite_devtools::hud::{FrameHistogram, FrameTiming};
116///
117/// let mut hist = FrameHistogram::new();
118/// hist.record(FrameTiming {
119/// layout_time_ns: 500_000,
120/// paint_time_ns: 300_000,
121/// gpu_wait_time_ns: 100_000,
122/// total_time_ns: 900_000,
123/// });
124/// assert_eq!(hist.len(), 1);
125/// let avg = hist.average();
126/// assert_eq!(avg.total_time_ns, 900_000);
127/// ```
128#[derive(Debug, Clone)]
129pub struct FrameHistogram {
130 frames: [FrameTiming; HISTOGRAM_SIZE],
131 index: usize,
132 count: usize,
133}
134
135impl Default for FrameHistogram {
136 fn default() -> Self {
137 Self::new()
138 }
139}
140
141impl FrameHistogram {
142 /// Creates a new empty histogram.
143 ///
144 /// # Example
145 ///
146 /// ```
147 /// use martensite_devtools::hud::FrameHistogram;
148 ///
149 /// let hist = FrameHistogram::new();
150 /// assert!(hist.is_empty());
151 /// ```
152 pub fn new() -> Self {
153 Self {
154 frames: [FrameTiming::default(); HISTOGRAM_SIZE],
155 index: 0,
156 count: 0,
157 }
158 }
159
160 /// Records a frame timing into the ring buffer.
161 ///
162 /// # Example
163 ///
164 /// ```
165 /// use martensite_devtools::hud::{FrameHistogram, FrameTiming};
166 ///
167 /// let mut hist = FrameHistogram::new();
168 /// hist.record(FrameTiming { total_time_ns: 16_000_000, ..Default::default() });
169 /// assert_eq!(hist.len(), 1);
170 /// ```
171 pub fn record(&mut self, timing: FrameTiming) {
172 self.frames[self.index] = timing;
173 self.index = (self.index + 1) % HISTOGRAM_SIZE;
174 if self.count < HISTOGRAM_SIZE {
175 self.count += 1;
176 }
177 }
178
179 /// Returns the average frame timing across all recorded frames.
180 ///
181 /// # Example
182 ///
183 /// ```
184 /// use martensite_devtools::hud::{FrameHistogram, FrameTiming};
185 ///
186 /// let mut hist = FrameHistogram::new();
187 /// hist.record(FrameTiming { total_time_ns: 10_000_000, ..Default::default() });
188 /// hist.record(FrameTiming { total_time_ns: 20_000_000, ..Default::default() });
189 /// assert_eq!(hist.average().total_time_ns, 15_000_000);
190 /// ```
191 pub fn average(&self) -> FrameTiming {
192 if self.count == 0 {
193 return FrameTiming::default();
194 }
195 let mut sum = FrameTiming::default();
196 for i in 0..self.count {
197 sum = sum.add(&self.frames[i]);
198 }
199 sum.div(self.count as u64)
200 }
201
202 /// Returns the timing at the given percentile (0.0–100.0).
203 ///
204 /// `p=50` gives the median, `p=99` gives the 99th percentile.
205 ///
206 /// # Example
207 ///
208 /// ```
209 /// use martensite_devtools::hud::{FrameHistogram, FrameTiming};
210 ///
211 /// let mut hist = FrameHistogram::new();
212 /// for i in 1..=100 {
213 /// hist.record(FrameTiming { total_time_ns: i * 1_000_000, ..Default::default() });
214 /// }
215 /// let p50 = hist.percentile(50.0);
216 /// assert!(p50.total_time_ns >= 49_000_000 && p50.total_time_ns <= 51_000_000);
217 /// ```
218 pub fn percentile(&self, p: f32) -> FrameTiming {
219 if self.count == 0 {
220 return FrameTiming::default();
221 }
222 let mut totals: Vec<u64> = self.frames[..self.count]
223 .iter()
224 .map(|f| f.total_time_ns)
225 .collect();
226 totals.sort_unstable();
227 let idx = ((p.clamp(0.0, 100.0) / 100.0) * (self.count as f32 - 1.0)) as usize;
228 let target = totals[idx];
229 self.frames[..self.count]
230 .iter()
231 .find(|f| f.total_time_ns == target)
232 .copied()
233 .unwrap_or_default()
234 }
235
236 /// Returns the maximum recorded frame timing.
237 ///
238 /// # Example
239 ///
240 /// ```
241 /// use martensite_devtools::hud::{FrameHistogram, FrameTiming};
242 ///
243 /// let mut hist = FrameHistogram::new();
244 /// hist.record(FrameTiming { total_time_ns: 10_000_000, ..Default::default() });
245 /// hist.record(FrameTiming { total_time_ns: 30_000_000, ..Default::default() });
246 /// hist.record(FrameTiming { total_time_ns: 20_000_000, ..Default::default() });
247 /// assert_eq!(hist.max().total_time_ns, 30_000_000);
248 /// ```
249 pub fn max(&self) -> FrameTiming {
250 self.frames[..self.count]
251 .iter()
252 .copied()
253 .max_by_key(|f| f.total_time_ns)
254 .unwrap_or_default()
255 }
256
257 /// Returns a slice of all recorded frame timings.
258 ///
259 /// **Note**: After the ring buffer wraps (more than 120 frames recorded),
260 /// the slice is in physical storage order, not chronological order. The
261 /// newest frame may appear at any position. This is suitable for
262 /// aggregation (average, max, percentile) but not for chronological
263 /// display.
264 ///
265 /// # Example
266 ///
267 /// ```
268 /// use martensite_devtools::hud::{FrameHistogram, FrameTiming};
269 ///
270 /// let mut hist = FrameHistogram::new();
271 /// hist.record(FrameTiming::default());
272 /// assert_eq!(hist.frames().len(), 1);
273 /// ```
274 pub fn frames(&self) -> &[FrameTiming] {
275 &self.frames[..self.count]
276 }
277
278 /// Returns the number of recorded frames.
279 #[inline]
280 pub fn len(&self) -> usize {
281 self.count
282 }
283
284 /// Returns `true` if no frames have been recorded.
285 #[inline]
286 pub fn is_empty(&self) -> bool {
287 self.count == 0
288 }
289
290 /// Clears all recorded frames.
291 pub fn clear(&mut self) {
292 self.index = 0;
293 self.count = 0;
294 }
295}
296
297/// A rectangle for dirty rect tracking.
298///
299/// # Example
300///
301/// ```
302/// use martensite_devtools::hud::Rect;
303///
304/// let r = Rect::new(10, 20, 100, 200);
305/// assert_eq!(r.area(), 20_000);
306/// ```
307#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
308pub struct Rect {
309 /// X coordinate (pixels from left).
310 pub x: i32,
311 /// Y coordinate (pixels from top).
312 pub y: i32,
313 /// Width in pixels.
314 pub width: u32,
315 /// Height in pixels.
316 pub height: u32,
317}
318
319impl Rect {
320 /// Creates a new rectangle.
321 ///
322 /// # Example
323 ///
324 /// ```
325 /// use martensite_devtools::hud::Rect;
326 ///
327 /// let r = Rect::new(0, 0, 100, 100);
328 /// assert_eq!(r.area(), 10_000);
329 /// ```
330 pub fn new(x: i32, y: i32, width: u32, height: u32) -> Self {
331 Self {
332 x,
333 y,
334 width,
335 height,
336 }
337 }
338
339 /// Returns the area of the rectangle in pixels.
340 ///
341 /// # Example
342 ///
343 /// ```
344 /// use martensite_devtools::hud::Rect;
345 ///
346 /// assert_eq!(Rect::new(0, 0, 100, 50).area(), 5_000);
347 /// ```
348 pub fn area(&self) -> u64 {
349 self.width as u64 * self.height as u64
350 }
351
352 /// Returns `true` if this rectangle intersects another.
353 ///
354 /// # Example
355 ///
356 /// ```
357 /// use martensite_devtools::hud::Rect;
358 ///
359 /// let a = Rect::new(0, 0, 100, 100);
360 /// let b = Rect::new(50, 50, 100, 100);
361 /// let c = Rect::new(200, 200, 50, 50);
362 /// assert!(a.intersects(&b));
363 /// assert!(!a.intersects(&c));
364 /// ```
365 pub fn intersects(&self, other: &Rect) -> bool {
366 let self_right = self.x + self.width as i32;
367 let self_bottom = self.y + self.height as i32;
368 let other_right = other.x + other.width as i32;
369 let other_bottom = other.y + other.height as i32;
370 self.x < other_right
371 && self_right > other.x
372 && self.y < other_bottom
373 && self_bottom > other.y
374 }
375
376 /// Returns the bounding union of two rectangles.
377 ///
378 /// # Example
379 ///
380 /// ```
381 /// use martensite_devtools::hud::Rect;
382 ///
383 /// let a = Rect::new(0, 0, 100, 100);
384 /// let b = Rect::new(50, 50, 100, 100);
385 /// let u = a.union(&b);
386 /// assert_eq!(u.x, 0);
387 /// assert_eq!(u.y, 0);
388 /// assert_eq!(u.width, 150);
389 /// assert_eq!(u.height, 150);
390 /// ```
391 pub fn union(&self, other: &Rect) -> Rect {
392 let x = self.x.min(other.x);
393 let y = self.y.min(other.y);
394 let right = (self.x + self.width as i32).max(other.x + other.width as i32);
395 let bottom = (self.y + self.height as i32).max(other.y + other.height as i32);
396 Rect::new(x, y, (right - x) as u32, (bottom - y) as u32)
397 }
398}
399
400/// Dirty rectangle tracking for visualization.
401///
402/// Tracks repainted screen regions for the diagnostic HUD overlay.
403///
404/// # Example
405///
406/// ```
407/// use martensite_devtools::hud::{DirtyRectTracker, Rect};
408///
409/// let mut tracker = DirtyRectTracker::new(64);
410/// tracker.add(Rect::new(0, 0, 100, 100));
411/// tracker.add(Rect::new(50, 50, 100, 100));
412/// assert_eq!(tracker.rects().len(), 2);
413/// assert!(tracker.total_area() > 0);
414/// ```
415#[derive(Debug, Clone)]
416pub struct DirtyRectTracker {
417 rects: Vec<Rect>,
418 max_rects: usize,
419}
420
421impl DirtyRectTracker {
422 /// Creates a new tracker with the given maximum number of rectangles.
423 ///
424 /// # Example
425 ///
426 /// ```
427 /// use martensite_devtools::hud::DirtyRectTracker;
428 ///
429 /// let tracker = DirtyRectTracker::new(32);
430 /// assert!(tracker.is_empty());
431 /// ```
432 pub fn new(max_rects: usize) -> Self {
433 Self {
434 rects: Vec::with_capacity(max_rects),
435 max_rects,
436 }
437 }
438
439 /// Adds a dirty rectangle. If the tracker is full, the oldest
440 /// rectangle is dropped.
441 ///
442 /// # Example
443 ///
444 /// ```
445 /// use martensite_devtools::hud::{DirtyRectTracker, Rect};
446 ///
447 /// let mut tracker = DirtyRectTracker::new(2);
448 /// tracker.add(Rect::new(0, 0, 10, 10));
449 /// tracker.add(Rect::new(10, 10, 10, 10));
450 /// tracker.add(Rect::new(20, 20, 10, 10)); // drops oldest
451 /// assert_eq!(tracker.rects().len(), 2);
452 /// ```
453 pub fn add(&mut self, rect: Rect) {
454 if self.rects.len() >= self.max_rects {
455 self.rects.remove(0);
456 }
457 self.rects.push(rect);
458 }
459
460 /// Returns the tracked rectangles.
461 #[inline]
462 pub fn rects(&self) -> &[Rect] {
463 &self.rects
464 }
465
466 /// Clears all tracked rectangles.
467 pub fn clear(&mut self) {
468 self.rects.clear();
469 }
470
471 /// Returns the total area of all tracked rectangles.
472 ///
473 /// # Example
474 ///
475 /// ```
476 /// use martensite_devtools::hud::{DirtyRectTracker, Rect};
477 ///
478 /// let mut tracker = DirtyRectTracker::new(10);
479 /// tracker.add(Rect::new(0, 0, 100, 100));
480 /// assert_eq!(tracker.total_area(), 10_000);
481 /// ```
482 pub fn total_area(&self) -> u64 {
483 self.rects.iter().map(|r| r.area()).sum()
484 }
485
486 /// Returns `true` if no rectangles are tracked.
487 #[inline]
488 pub fn is_empty(&self) -> bool {
489 self.rects.is_empty()
490 }
491
492 /// Returns the number of tracked rectangles.
493 #[inline]
494 pub fn len(&self) -> usize {
495 self.rects.len()
496 }
497}
498
499/// Arena memory telemetry data.
500///
501/// Tracks `WidgetArena` slot utilization and compaction statistics.
502///
503/// # Example
504///
505/// ```
506/// use martensite_devtools::hud::ArenaTelemetry;
507///
508/// let telemetry = ArenaTelemetry {
509/// total_slots: 1000,
510/// used_slots: 750,
511/// free_slots: 250,
512/// utilization_pct: 75.0,
513/// compaction_count: 3,
514/// };
515/// assert_eq!(telemetry.free_slots, 250);
516/// ```
517#[derive(Debug, Clone, Copy, Default, PartialEq)]
518pub struct ArenaTelemetry {
519 /// Total number of slots in the arena.
520 pub total_slots: usize,
521 /// Number of slots currently in use.
522 pub used_slots: usize,
523 /// Number of free (available) slots.
524 pub free_slots: usize,
525 /// Slot utilization as a percentage (0.0–100.0).
526 pub utilization_pct: f32,
527 /// Number of compaction passes performed.
528 pub compaction_count: u64,
529}
530
531impl ArenaTelemetry {
532 /// Creates telemetry from slot counts, computing derived fields.
533 ///
534 /// # Example
535 ///
536 /// ```
537 /// use martensite_devtools::hud::ArenaTelemetry;
538 ///
539 /// let t = ArenaTelemetry::from_slots(1000, 750, 2);
540 /// assert_eq!(t.free_slots, 250);
541 /// assert_eq!(t.utilization_pct, 75.0);
542 /// ```
543 pub fn from_slots(total: usize, used: usize, compactions: u64) -> Self {
544 let free = total.saturating_sub(used);
545 let utilization_pct = if total > 0 {
546 (used as f32 / total as f32) * 100.0
547 } else {
548 0.0
549 };
550 Self {
551 total_slots: total,
552 used_slots: used,
553 free_slots: free,
554 utilization_pct,
555 compaction_count: compactions,
556 }
557 }
558}
559
560/// The diagnostic HUD state.
561///
562/// Aggregates frame timing, dirty rect, and arena telemetry data
563/// for the F12 diagnostic overlay.
564///
565/// # Example
566///
567/// ```
568/// use martensite_devtools::hud::{DiagnosticHud, FrameTiming, Rect, ArenaTelemetry};
569///
570/// let mut hud = DiagnosticHud::new();
571/// hud.toggle();
572/// assert!(hud.is_enabled());
573///
574/// hud.record_frame(FrameTiming {
575/// layout_time_ns: 500_000,
576/// paint_time_ns: 300_000,
577/// gpu_wait_time_ns: 100_000,
578/// total_time_ns: 900_000,
579/// });
580/// hud.add_dirty_rect(Rect::new(0, 0, 100, 100));
581/// hud.update_arena_telemetry(ArenaTelemetry::from_slots(1000, 500, 0));
582///
583/// assert_eq!(hud.histogram().len(), 1);
584/// assert_eq!(hud.dirty_rects().len(), 1);
585/// ```
586#[derive(Debug, Clone)]
587pub struct DiagnosticHud {
588 enabled: bool,
589 histogram: FrameHistogram,
590 dirty_rects: DirtyRectTracker,
591 arena_telemetry: ArenaTelemetry,
592}
593
594impl Default for DiagnosticHud {
595 fn default() -> Self {
596 Self::new()
597 }
598}
599
600impl DiagnosticHud {
601 /// Creates a new disabled HUD.
602 ///
603 /// # Example
604 ///
605 /// ```
606 /// use martensite_devtools::hud::DiagnosticHud;
607 ///
608 /// let hud = DiagnosticHud::new();
609 /// assert!(!hud.is_enabled());
610 /// ```
611 pub fn new() -> Self {
612 Self {
613 enabled: false,
614 histogram: FrameHistogram::new(),
615 dirty_rects: DirtyRectTracker::new(256),
616 arena_telemetry: ArenaTelemetry::default(),
617 }
618 }
619
620 /// Toggles the HUD on/off (bound to F12 in the application).
621 ///
622 /// # Example
623 ///
624 /// ```
625 /// use martensite_devtools::hud::DiagnosticHud;
626 ///
627 /// let mut hud = DiagnosticHud::new();
628 /// hud.toggle();
629 /// assert!(hud.is_enabled());
630 /// hud.toggle();
631 /// assert!(!hud.is_enabled());
632 /// ```
633 pub fn toggle(&mut self) {
634 self.enabled = !self.enabled;
635 }
636
637 /// Returns whether the HUD is currently visible.
638 #[inline]
639 pub fn is_enabled(&self) -> bool {
640 self.enabled
641 }
642
643 /// Records a frame's timing data.
644 ///
645 /// # Example
646 ///
647 /// ```
648 /// use martensite_devtools::hud::{DiagnosticHud, FrameTiming};
649 ///
650 /// let mut hud = DiagnosticHud::new();
651 /// hud.record_frame(FrameTiming { total_time_ns: 16_000_000, ..Default::default() });
652 /// assert_eq!(hud.histogram().len(), 1);
653 /// ```
654 pub fn record_frame(&mut self, timing: FrameTiming) {
655 self.histogram.record(timing);
656 }
657
658 /// Adds a dirty rectangle for visualization.
659 pub fn add_dirty_rect(&mut self, rect: Rect) {
660 self.dirty_rects.add(rect);
661 }
662
663 /// Updates the arena telemetry data.
664 pub fn update_arena_telemetry(&mut self, telemetry: ArenaTelemetry) {
665 self.arena_telemetry = telemetry;
666 }
667
668 /// Returns the frame timing histogram.
669 #[inline]
670 pub fn histogram(&self) -> &FrameHistogram {
671 &self.histogram
672 }
673
674 /// Returns the dirty rect tracker.
675 #[inline]
676 pub fn dirty_rects(&self) -> &DirtyRectTracker {
677 &self.dirty_rects
678 }
679
680 /// Returns the arena telemetry.
681 #[inline]
682 pub fn arena_telemetry(&self) -> &ArenaTelemetry {
683 &self.arena_telemetry
684 }
685
686 /// Clears all dirty rects (call after each frame).
687 pub fn clear_dirty_rects(&mut self) {
688 self.dirty_rects.clear();
689 }
690}
691
692#[cfg(test)]
693mod tests {
694 use super::*;
695
696 #[test]
697 fn frame_timing_from_ms() {
698 let t = FrameTiming::from_ms(0.5, 0.3, 0.1, 0.9);
699 assert_eq!(t.layout_time_ns, 500_000);
700 assert_eq!(t.paint_time_ns, 300_000);
701 assert_eq!(t.gpu_wait_time_ns, 100_000);
702 assert_eq!(t.total_time_ns, 900_000);
703 }
704
705 #[test]
706 fn frame_timing_add_and_div() {
707 let a = FrameTiming {
708 total_time_ns: 10,
709 ..Default::default()
710 };
711 let b = FrameTiming {
712 total_time_ns: 20,
713 ..Default::default()
714 };
715 let sum = a.add(&b);
716 assert_eq!(sum.total_time_ns, 30);
717 let avg = sum.div(2);
718 assert_eq!(avg.total_time_ns, 15);
719 let div_zero = sum.div(0);
720 assert_eq!(div_zero.total_time_ns, 0);
721 }
722
723 #[test]
724 fn histogram_new_is_empty() {
725 let hist = FrameHistogram::new();
726 assert!(hist.is_empty());
727 assert_eq!(hist.len(), 0);
728 }
729
730 #[test]
731 fn histogram_record_and_average() {
732 let mut hist = FrameHistogram::new();
733 hist.record(FrameTiming {
734 total_time_ns: 10_000_000,
735 ..Default::default()
736 });
737 hist.record(FrameTiming {
738 total_time_ns: 20_000_000,
739 ..Default::default()
740 });
741 assert_eq!(hist.len(), 2);
742 let avg = hist.average();
743 assert_eq!(avg.total_time_ns, 15_000_000);
744 }
745
746 #[test]
747 fn histogram_max() {
748 let mut hist = FrameHistogram::new();
749 hist.record(FrameTiming {
750 total_time_ns: 10,
751 ..Default::default()
752 });
753 hist.record(FrameTiming {
754 total_time_ns: 30,
755 ..Default::default()
756 });
757 hist.record(FrameTiming {
758 total_time_ns: 20,
759 ..Default::default()
760 });
761 assert_eq!(hist.max().total_time_ns, 30);
762 }
763
764 #[test]
765 fn histogram_percentile() {
766 let mut hist = FrameHistogram::new();
767 for i in 1..=100 {
768 hist.record(FrameTiming {
769 total_time_ns: i * 1_000_000,
770 ..Default::default()
771 });
772 }
773 let p50 = hist.percentile(50.0);
774 assert!(p50.total_time_ns >= 49_000_000 && p50.total_time_ns <= 51_000_000);
775 let p99 = hist.percentile(99.0);
776 assert!(p99.total_time_ns >= 98_000_000);
777 }
778
779 #[test]
780 fn histogram_ring_buffer_wrap() {
781 let mut hist = FrameHistogram::new();
782 for i in 0..150 {
783 hist.record(FrameTiming {
784 total_time_ns: i,
785 ..Default::default()
786 });
787 }
788 assert_eq!(hist.len(), 120);
789 }
790
791 #[test]
792 fn histogram_clear() {
793 let mut hist = FrameHistogram::new();
794 hist.record(FrameTiming::default());
795 hist.clear();
796 assert!(hist.is_empty());
797 }
798
799 #[test]
800 fn rect_area() {
801 assert_eq!(Rect::new(0, 0, 100, 50).area(), 5_000);
802 assert_eq!(Rect::new(10, 20, 0, 100).area(), 0);
803 }
804
805 #[test]
806 fn rect_intersects() {
807 let a = Rect::new(0, 0, 100, 100);
808 let b = Rect::new(50, 50, 100, 100);
809 let c = Rect::new(200, 200, 50, 50);
810 assert!(a.intersects(&b));
811 assert!(!a.intersects(&c));
812 }
813
814 #[test]
815 fn rect_union() {
816 let a = Rect::new(0, 0, 100, 100);
817 let b = Rect::new(50, 50, 100, 100);
818 let u = a.union(&b);
819 assert_eq!(u.x, 0);
820 assert_eq!(u.y, 0);
821 assert_eq!(u.width, 150);
822 assert_eq!(u.height, 150);
823 }
824
825 #[test]
826 fn dirty_rect_tracker_basic() {
827 let mut tracker = DirtyRectTracker::new(64);
828 assert!(tracker.is_empty());
829 tracker.add(Rect::new(0, 0, 100, 100));
830 tracker.add(Rect::new(50, 50, 100, 100));
831 assert_eq!(tracker.len(), 2);
832 assert!(!tracker.is_empty());
833 assert!(tracker.total_area() > 0);
834 tracker.clear();
835 assert!(tracker.is_empty());
836 }
837
838 #[test]
839 fn dirty_rect_tracker_eviction() {
840 let mut tracker = DirtyRectTracker::new(2);
841 tracker.add(Rect::new(0, 0, 10, 10));
842 tracker.add(Rect::new(10, 10, 10, 10));
843 tracker.add(Rect::new(20, 20, 10, 10));
844 assert_eq!(tracker.len(), 2);
845 assert_eq!(tracker.rects()[0], Rect::new(10, 10, 10, 10));
846 }
847
848 #[test]
849 fn arena_telemetry_from_slots() {
850 let t = ArenaTelemetry::from_slots(1000, 750, 3);
851 assert_eq!(t.total_slots, 1000);
852 assert_eq!(t.used_slots, 750);
853 assert_eq!(t.free_slots, 250);
854 assert_eq!(t.utilization_pct, 75.0);
855 assert_eq!(t.compaction_count, 3);
856 }
857
858 #[test]
859 fn arena_telemetry_zero_total() {
860 let t = ArenaTelemetry::from_slots(0, 0, 0);
861 assert_eq!(t.utilization_pct, 0.0);
862 }
863
864 #[test]
865 fn hud_toggle() {
866 let mut hud = DiagnosticHud::new();
867 assert!(!hud.is_enabled());
868 hud.toggle();
869 assert!(hud.is_enabled());
870 hud.toggle();
871 assert!(!hud.is_enabled());
872 }
873
874 #[test]
875 fn hud_record_frame_and_dirty_rect() {
876 let mut hud = DiagnosticHud::new();
877 hud.record_frame(FrameTiming {
878 total_time_ns: 16_000_000,
879 ..Default::default()
880 });
881 hud.add_dirty_rect(Rect::new(0, 0, 100, 100));
882 assert_eq!(hud.histogram().len(), 1);
883 assert_eq!(hud.dirty_rects().len(), 1);
884 hud.clear_dirty_rects();
885 assert_eq!(hud.dirty_rects().len(), 0);
886 }
887
888 #[test]
889 fn hud_arena_telemetry_update() {
890 let mut hud = DiagnosticHud::new();
891 hud.update_arena_telemetry(ArenaTelemetry::from_slots(500, 250, 1));
892 assert_eq!(hud.arena_telemetry().used_slots, 250);
893 }
894}