use std::time::{Duration, Instant};
use crate::dirty::DirtyFlags;
use crate::tree::NodeId;
#[derive(Debug, Clone, Default)]
pub struct UiMetrics {
pub layout_time: Duration,
pub text_shape_time: Duration,
pub build_batches_time: Duration,
pub gpu_upload_time: Duration,
pub total_time: Duration,
pub nodes_layout_dirty: usize,
pub nodes_text_dirty: usize,
pub nodes_paint_dirty: usize,
pub nodes_geometry_dirty: usize,
pub total_nodes: usize,
pub layout_skips: usize,
pub text_cache_hits: usize,
pub text_cache_misses: usize,
pub atlas_evictions: usize,
pub instance_updates: usize,
}
impl UiMetrics {
pub fn new() -> Self {
Self::default()
}
pub fn text_cache_hit_rate(&self) -> f32 {
let total = self.text_cache_hits + self.text_cache_misses;
if total == 0 {
0.0
} else {
self.text_cache_hits as f32 / total as f32
}
}
pub fn layout_dirty_percentage(&self) -> f32 {
if self.total_nodes == 0 {
0.0
} else {
(self.nodes_layout_dirty as f32 / self.total_nodes as f32) * 100.0
}
}
pub fn paint_only_percentage(&self) -> f32 {
if self.total_nodes == 0 {
0.0
} else {
(self.nodes_paint_dirty as f32 / self.total_nodes as f32) * 100.0
}
}
pub fn is_paint_only_frame(&self) -> bool {
self.nodes_layout_dirty == 0 && self.nodes_text_dirty == 0
}
pub fn format_summary(&self) -> String {
format!(
"UI Frame: {:.2}ms | Layout: {:.2}ms ({} nodes) | Text: {:.2}ms ({} cache hits) | Paint: {} nodes",
self.total_time.as_secs_f64() * 1000.0,
self.layout_time.as_secs_f64() * 1000.0,
self.nodes_layout_dirty,
self.text_shape_time.as_secs_f64() * 1000.0,
self.text_cache_hits,
self.nodes_paint_dirty,
)
}
pub fn format_detailed(&self) -> String {
format!(
r#"UI Performance Metrics:
Total Time: {:.3}ms
Layout Time: {:.3}ms ({}% of total)
Text Shaping: {:.3}ms ({}% of total)
Batch Building: {:.3}ms ({}% of total)
GPU Upload: {:.3}ms ({}% of total)
Dirty Nodes:
Layout: {} / {} ({:.1}%)
Text: {}
Paint Only: {}
Geometry: {}
Text Cache:
Hits: {}
Misses: {}
Hit Rate: {:.1}%
Optimizations:
Layout Skips: {}
Instance Updates: {}
Atlas Evictions: {}
"#,
self.total_time.as_secs_f64() * 1000.0,
self.layout_time.as_secs_f64() * 1000.0,
self.percentage_of_total(self.layout_time),
self.text_shape_time.as_secs_f64() * 1000.0,
self.percentage_of_total(self.text_shape_time),
self.build_batches_time.as_secs_f64() * 1000.0,
self.percentage_of_total(self.build_batches_time),
self.gpu_upload_time.as_secs_f64() * 1000.0,
self.percentage_of_total(self.gpu_upload_time),
self.nodes_layout_dirty,
self.total_nodes,
self.layout_dirty_percentage(),
self.nodes_text_dirty,
self.nodes_paint_dirty,
self.nodes_geometry_dirty,
self.text_cache_hits,
self.text_cache_misses,
self.text_cache_hit_rate() * 100.0,
self.layout_skips,
self.instance_updates,
self.atlas_evictions,
)
}
fn percentage_of_total(&self, duration: Duration) -> u32 {
if self.total_time.as_nanos() == 0 {
0
} else {
((duration.as_nanos() as f64 / self.total_time.as_nanos() as f64) * 100.0) as u32
}
}
}
pub struct MetricsTimer {
start: Instant,
}
impl MetricsTimer {
pub fn start() -> Self {
Self {
start: Instant::now(),
}
}
pub fn stop(self) -> Duration {
self.start.elapsed()
}
}
#[derive(Debug, Clone, Default)]
pub struct DirtyStats {
pub layout_count: usize,
pub text_count: usize,
pub paint_count: usize,
pub geometry_count: usize,
}
impl DirtyStats {
pub fn new() -> Self {
Self::default()
}
pub fn add_node(&mut self, flags: DirtyFlags) {
if flags.needs_layout() {
self.layout_count += 1;
}
if flags.needs_text_shaping() {
self.text_count += 1;
}
if flags.is_paint_only() {
self.paint_count += 1;
}
if flags.needs_geometry_rebuild() {
self.geometry_count += 1;
}
}
}
#[derive(Debug, Clone)]
pub struct NodeMetrics {
pub node_id: NodeId,
pub dirty_flags: DirtyFlags,
pub layout_time: Duration,
pub text_time: Duration,
pub paint_time: Duration,
}
impl NodeMetrics {
pub fn new(node_id: NodeId, dirty_flags: DirtyFlags) -> Self {
Self {
node_id,
dirty_flags,
layout_time: Duration::ZERO,
text_time: Duration::ZERO,
paint_time: Duration::ZERO,
}
}
pub fn total_time(&self) -> Duration {
self.layout_time + self.text_time + self.paint_time
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_metrics_default() {
let metrics = UiMetrics::new();
assert_eq!(metrics.total_nodes, 0);
assert_eq!(metrics.nodes_layout_dirty, 0);
assert_eq!(metrics.text_cache_hit_rate(), 0.0);
}
#[test]
fn test_cache_hit_rate() {
let mut metrics = UiMetrics::new();
metrics.text_cache_hits = 8;
metrics.text_cache_misses = 2;
assert_eq!(metrics.text_cache_hit_rate(), 0.8);
}
#[test]
fn test_paint_only_frame() {
let mut metrics = UiMetrics::new();
metrics.nodes_paint_dirty = 5;
assert!(metrics.is_paint_only_frame());
metrics.nodes_layout_dirty = 1;
assert!(!metrics.is_paint_only_frame());
}
#[test]
fn test_dirty_stats() {
let mut stats = DirtyStats::new();
stats.add_node(DirtyFlags::LAYOUT);
stats.add_node(DirtyFlags::COLOR);
stats.add_node(DirtyFlags::TEXT_SHAPING);
assert_eq!(stats.layout_count, 2); assert_eq!(stats.text_count, 1);
assert_eq!(stats.paint_count, 1);
}
#[test]
fn test_timer() {
let timer = MetricsTimer::start();
std::thread::sleep(Duration::from_millis(10));
let elapsed = timer.stop();
assert!(elapsed >= Duration::from_millis(10));
}
}