use std::collections::HashMap;
use std::time::{Duration, Instant};
use crate::ontology::*;
#[derive(Debug, Clone)]
pub struct FrameProfile {
pub frame_number: u64,
pub total: Duration,
pub update: Duration,
pub layout: Duration,
pub render: Duration,
pub widget_count: usize,
}
pub struct Profiler {
frame_number: u64,
timers: HashMap<String, Instant>,
durations: HashMap<String, Duration>,
widget_count: usize,
frame_start: Instant,
history: Vec<FrameProfile>,
max_history: usize,
}
impl Profiler {
pub fn new(max_history: usize) -> Self {
Self {
frame_number: 0,
timers: HashMap::new(),
durations: HashMap::new(),
widget_count: 0,
frame_start: Instant::now(),
history: Vec::new(),
max_history,
}
}
pub fn begin_frame(&mut self) {
self.timers.clear();
self.durations.clear();
self.widget_count = 0;
self.frame_start = Instant::now();
}
pub fn start(&mut self, label: &str) {
self.timers.insert(label.to_string(), Instant::now());
}
pub fn stop(&mut self, label: &str) {
if let Some(start) = self.timers.remove(label) {
self.durations.insert(label.to_string(), start.elapsed());
}
}
pub fn count_widget(&mut self) {
self.widget_count += 1;
}
pub fn count_widgets(&mut self, n: usize) {
self.widget_count += n;
}
pub fn end_frame(&mut self) {
let total = self.frame_start.elapsed();
let profile = FrameProfile {
frame_number: self.frame_number,
total,
update: self.get_duration("update"),
layout: self.get_duration("layout"),
render: self.get_duration("render"),
widget_count: self.widget_count,
};
self.history.push(profile);
if self.history.len() > self.max_history {
self.history.remove(0);
}
self.frame_number += 1;
}
fn get_duration(&self, label: &str) -> Duration {
self.durations.get(label).copied().unwrap_or(Duration::ZERO)
}
pub fn last_frame(&self) -> Option<&FrameProfile> {
self.history.last()
}
pub fn history(&self) -> &[FrameProfile] {
&self.history
}
pub fn avg_frame_time(&self) -> Duration {
if self.history.is_empty() {
return Duration::ZERO;
}
let total: Duration = self.history.iter().map(|f| f.total).sum();
total / self.history.len() as u32
}
pub fn avg_fps(&self) -> f64 {
let avg = self.avg_frame_time();
if avg.is_zero() {
return 0.0;
}
1.0 / avg.as_secs_f64()
}
pub fn max_frame_time(&self) -> Duration {
self.history
.iter()
.map(|f| f.total)
.max()
.unwrap_or(Duration::ZERO)
}
pub fn duration(&self, label: &str) -> Duration {
self.get_duration(label)
}
pub fn frame_number(&self) -> u64 {
self.frame_number
}
}
impl Default for Profiler {
fn default() -> Self {
Self::new(120) }
}
impl Discoverable for Profiler {
fn schema(&self) -> WidgetSchema {
let mut schema = WidgetSchema::new(
"Profiler",
"Frame profiling and performance metrics tracker",
SemanticRole::Diagnostic,
);
schema.usage_hint = Some("profiler.begin_frame(); ... profiler.end_frame();".into());
schema.tags = vec![
"profiling".into(),
"performance".into(),
"fps".into(),
"timing".into(),
"metrics".into(),
];
schema
}
fn capabilities(&self) -> Vec<AgentCapability> {
vec![]
}
fn actions(&self) -> Vec<AgentAction> {
vec![
AgentAction::simple("reset", "Clear profiling history", true),
AgentAction::simple("snapshot", "Get current performance snapshot", false),
]
}
fn semantic_role(&self) -> SemanticRole {
SemanticRole::Diagnostic
}
fn agent_state(&self) -> serde_json::Value {
let last = self.last_frame().map(|f| {
serde_json::json!({
"frame_number": f.frame_number,
"total_ms": f.total.as_secs_f64() * 1000.0,
"update_ms": f.update.as_secs_f64() * 1000.0,
"layout_ms": f.layout.as_secs_f64() * 1000.0,
"render_ms": f.render.as_secs_f64() * 1000.0,
"widget_count": f.widget_count,
})
});
serde_json::json!({
"frame_number": self.frame_number,
"avg_fps": self.avg_fps(),
"avg_frame_time_ms": self.avg_frame_time().as_secs_f64() * 1000.0,
"max_frame_time_ms": self.max_frame_time().as_secs_f64() * 1000.0,
"history_length": self.history.len(),
"max_history": self.max_history,
"last_frame": last,
})
}
fn execute_action(
&mut self,
action: &str,
_params: &serde_json::Value,
) -> Result<serde_json::Value, String> {
match action {
"reset" => {
self.history.clear();
self.frame_number = 0;
Ok(serde_json::json!({ "reset": true }))
}
"snapshot" => Ok(self.agent_state()),
_ => Err(format!("Unknown action: {action}")),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn profiler_basics() {
let mut p = Profiler::new(10);
p.begin_frame();
p.start("update");
std::thread::sleep(Duration::from_millis(1));
p.stop("update");
p.count_widgets(5);
p.end_frame();
let frame = p.last_frame().unwrap();
assert_eq!(frame.frame_number, 0);
assert_eq!(frame.widget_count, 5);
assert!(frame.update > Duration::ZERO);
assert!(p.avg_fps() > 0.0);
}
#[test]
fn max_history_cap() {
let mut p = Profiler::new(3);
for _ in 0..5 {
p.begin_frame();
p.end_frame();
}
assert_eq!(p.history().len(), 3);
assert_eq!(p.frame_number(), 5);
}
}