1use std::collections::{HashMap, VecDeque};
16use std::time::{Duration, Instant};
17
18#[derive(Debug, Clone)]
22pub struct Span {
23 pub name: String,
24 pub depth: u32,
25 pub start_ns: u64,
26 pub end_ns: u64,
27 pub thread_id: u64,
28}
29
30impl Span {
31 pub fn duration_us(&self) -> f64 {
32 (self.end_ns.saturating_sub(self.start_ns)) as f64 / 1_000.0
33 }
34 pub fn duration_ms(&self) -> f64 {
35 self.duration_us() / 1_000.0
36 }
37}
38
39#[derive(Debug, Clone)]
43pub struct FrameRecord {
44 pub frame_index: u64,
45 pub spans: Vec<Span>,
46 pub counters: HashMap<String, f64>,
47 pub frame_ms: f64,
48}
49
50impl FrameRecord {
51 pub fn total_span_ms(&self) -> f64 {
52 self.spans.iter()
53 .filter(|s| s.depth == 0)
54 .map(|s| s.duration_ms())
55 .sum()
56 }
57
58 pub fn top_spans(&self, n: usize) -> Vec<&Span> {
60 let mut sorted: Vec<&Span> = self.spans.iter().collect();
61 sorted.sort_by(|a, b| b.end_ns.saturating_sub(b.start_ns).cmp(&(a.end_ns.saturating_sub(a.start_ns))));
62 sorted.truncate(n);
63 sorted
64 }
65
66 pub fn aggregate(&self) -> HashMap<String, (f64, u32)> {
68 let mut map: HashMap<String, (f64, u32)> = HashMap::new();
69 for s in &self.spans {
70 let entry = map.entry(s.name.clone()).or_insert((0.0, 0));
71 entry.0 += s.duration_ms();
72 entry.1 += 1;
73 }
74 map
75 }
76}
77
78#[derive(Debug, Clone)]
82pub struct SpanStats {
83 pub name: String,
84 pub samples: VecDeque<f64>,
85 pub max_samples: usize,
86}
87
88impl SpanStats {
89 pub fn new(name: impl Into<String>) -> Self {
90 Self { name: name.into(), samples: VecDeque::new(), max_samples: 128 }
91 }
92
93 pub fn push(&mut self, ms: f64) {
94 if self.samples.len() >= self.max_samples {
95 self.samples.pop_front();
96 }
97 self.samples.push_back(ms);
98 }
99
100 pub fn avg_ms(&self) -> f64 {
101 if self.samples.is_empty() { return 0.0; }
102 self.samples.iter().sum::<f64>() / self.samples.len() as f64
103 }
104
105 pub fn min_ms(&self) -> f64 {
106 self.samples.iter().cloned().fold(f64::MAX, f64::min)
107 }
108
109 pub fn max_ms(&self) -> f64 {
110 self.samples.iter().cloned().fold(0.0_f64, f64::max)
111 }
112
113 pub fn percentile(&self, p: f64) -> f64 {
114 if self.samples.is_empty() { return 0.0; }
115 let mut sorted: Vec<f64> = self.samples.iter().cloned().collect();
116 sorted.sort_by(|a, b| a.partial_cmp(b).unwrap());
117 let idx = ((p / 100.0) * (sorted.len() - 1) as f64) as usize;
118 sorted[idx.min(sorted.len() - 1)]
119 }
120
121 pub fn p50(&self) -> f64 { self.percentile(50.0) }
122 pub fn p95(&self) -> f64 { self.percentile(95.0) }
123 pub fn p99(&self) -> f64 { self.percentile(99.0) }
124
125 pub fn variance(&self) -> f64 {
126 let avg = self.avg_ms();
127 if self.samples.len() < 2 { return 0.0; }
128 let sum_sq: f64 = self.samples.iter().map(|x| (x - avg).powi(2)).sum();
129 sum_sq / (self.samples.len() - 1) as f64
130 }
131
132 pub fn std_dev(&self) -> f64 {
133 self.variance().sqrt()
134 }
135}
136
137#[derive(Debug, Clone)]
141pub struct Counter {
142 pub name: String,
143 pub value: f64,
144 pub history: VecDeque<f64>,
145 pub max_history: usize,
146}
147
148impl Counter {
149 pub fn new(name: impl Into<String>) -> Self {
150 Self { name: name.into(), value: 0.0, history: VecDeque::new(), max_history: 128 }
151 }
152
153 pub fn add(&mut self, v: f64) { self.value += v; }
154 pub fn set(&mut self, v: f64) { self.value = v; }
155 pub fn reset(&mut self) { self.value = 0.0; }
156
157 pub fn flush(&mut self) {
158 if self.history.len() >= self.max_history { self.history.pop_front(); }
159 self.history.push_back(self.value);
160 self.value = 0.0;
161 }
162
163 pub fn avg(&self) -> f64 {
164 if self.history.is_empty() { return 0.0; }
165 self.history.iter().sum::<f64>() / self.history.len() as f64
166 }
167
168 pub fn peak(&self) -> f64 {
169 self.history.iter().cloned().fold(0.0_f64, f64::max)
170 }
171}
172
173#[derive(Debug, Clone, Default)]
177pub struct MemoryStats {
178 pub heap_used_bytes: usize,
179 pub heap_reserved_bytes: usize,
180 pub gpu_used_bytes: usize,
181 pub gpu_reserved_bytes: usize,
182 pub texture_bytes: usize,
183 pub mesh_bytes: usize,
184 pub audio_bytes: usize,
185 pub script_bytes: usize,
186}
187
188impl MemoryStats {
189 pub fn total_used_mb(&self) -> f64 {
190 (self.heap_used_bytes + self.gpu_used_bytes) as f64 / 1_048_576.0
191 }
192
193 pub fn gpu_used_mb(&self) -> f64 {
194 self.gpu_used_bytes as f64 / 1_048_576.0
195 }
196
197 pub fn heap_used_mb(&self) -> f64 {
198 self.heap_used_bytes as f64 / 1_048_576.0
199 }
200}
201
202#[derive(Debug, Clone)]
206pub struct GpuSpan {
207 pub name: String,
208 pub gpu_us: f64,
209 pub pass: GpuPass,
210}
211
212#[derive(Debug, Clone, Copy, PartialEq, Eq)]
214pub enum GpuPass {
215 ShadowMap,
216 GBuffer,
217 Lighting,
218 Transparent,
219 PostProcess,
220 UI,
221 Compute,
222 Other,
223}
224
225#[derive(Debug, Clone, Default)]
227pub struct GpuFrameStats {
228 pub spans: Vec<GpuSpan>,
229 pub total_us: f64,
230 pub frame_idx: u64,
231}
232
233impl GpuFrameStats {
234 pub fn total_ms(&self) -> f64 { self.total_us / 1000.0 }
235
236 pub fn pass_total(&self, pass: GpuPass) -> f64 {
237 self.spans.iter()
238 .filter(|s| s.pass == pass)
239 .map(|s| s.gpu_us)
240 .sum::<f64>() / 1000.0
241 }
242}
243
244#[derive(Debug, Clone)]
248pub struct FlameNode {
249 pub name: String,
250 pub total_ms: f64,
251 pub self_ms: f64, pub call_count: u32,
253 pub children: Vec<FlameNode>,
254}
255
256impl FlameNode {
257 pub fn new(name: impl Into<String>) -> Self {
258 Self { name: name.into(), total_ms: 0.0, self_ms: 0.0, call_count: 0, children: Vec::new() }
259 }
260
261 pub fn find_child_mut(&mut self, name: &str) -> Option<&mut FlameNode> {
262 self.children.iter_mut().find(|c| c.name == name)
263 }
264
265 pub fn get_or_insert_child(&mut self, name: &str) -> &mut FlameNode {
266 if let Some(idx) = self.children.iter().position(|c| c.name == name) {
267 return &mut self.children[idx];
268 }
269 self.children.push(FlameNode::new(name));
270 self.children.last_mut().unwrap()
271 }
272
273 pub fn children_total_ms(&self) -> f64 {
274 self.children.iter().map(|c| c.total_ms).sum()
275 }
276
277 pub fn recompute_self_ms(&mut self) {
278 self.self_ms = (self.total_ms - self.children_total_ms()).max(0.0);
279 for child in &mut self.children {
280 child.recompute_self_ms();
281 }
282 }
283
284 pub fn flatten(&self) -> Vec<(String, f64, u32)> {
286 let mut out = vec![(self.name.clone(), self.total_ms, self.call_count)];
287 for c in &self.children {
288 out.extend(c.flatten());
289 }
290 out
291 }
292}
293
294struct OpenSpan {
297 name: String,
298 start_ns: u64,
299 depth: u32,
300}
301
302pub struct Profiler {
306 pub enabled: bool,
307 epoch: Instant,
308 open_stack: Vec<OpenSpan>,
309 current_spans: Vec<Span>,
310 pub counters: HashMap<String, Counter>,
311 pub span_stats: HashMap<String, SpanStats>,
312 frame_history: VecDeque<FrameRecord>,
313 pub max_history: usize,
314 pub frame_index: u64,
315 frame_start_ns: u64,
316 pub memory: MemoryStats,
317 pub gpu: GpuFrameStats,
318 pub fps_history: VecDeque<f64>,
319 pub fps: f64,
320}
321
322impl Profiler {
323 pub fn new() -> Self {
324 Self {
325 enabled: true,
326 epoch: Instant::now(),
327 open_stack: Vec::new(),
328 current_spans: Vec::new(),
329 counters: HashMap::new(),
330 span_stats: HashMap::new(),
331 frame_history: VecDeque::new(),
332 max_history: 120,
333 frame_index: 0,
334 frame_start_ns: 0,
335 memory: MemoryStats::default(),
336 gpu: GpuFrameStats::default(),
337 fps_history: VecDeque::new(),
338 fps: 0.0,
339 }
340 }
341
342 fn now_ns(&self) -> u64 {
343 self.epoch.elapsed().as_nanos() as u64
344 }
345
346 pub fn begin(&mut self, name: &str) {
348 if !self.enabled { return; }
349 let depth = self.open_stack.len() as u32;
350 let start = self.now_ns();
351 self.open_stack.push(OpenSpan { name: name.to_string(), start_ns: start, depth });
352 }
353
354 pub fn end(&mut self, name: &str) {
356 if !self.enabled { return; }
357 let end = self.now_ns();
358 if let Some(pos) = self.open_stack.iter().rposition(|s| s.name == name) {
359 let open = self.open_stack.remove(pos);
360 let span = Span {
361 name: open.name.clone(),
362 depth: open.depth,
363 start_ns: open.start_ns,
364 end_ns: end,
365 thread_id: 0,
366 };
367 let ms = span.duration_ms();
369 self.span_stats.entry(open.name.clone())
370 .or_insert_with(|| SpanStats::new(&open.name))
371 .push(ms);
372 self.current_spans.push(span);
373 }
374 }
375
376 pub fn count(&mut self, name: &str, delta: f64) {
378 self.counters.entry(name.to_string())
379 .or_insert_with(|| Counter::new(name))
380 .add(delta);
381 }
382
383 pub fn set_counter(&mut self, name: &str, value: f64) {
385 self.counters.entry(name.to_string())
386 .or_insert_with(|| Counter::new(name))
387 .set(value);
388 }
389
390 pub fn frame_begin(&mut self) {
392 if !self.enabled { return; }
393 self.frame_start_ns = self.now_ns();
394 }
395
396 pub fn frame_end(&mut self) -> FrameRecord {
398 let frame_end_ns = self.now_ns();
399 let frame_ms = (frame_end_ns.saturating_sub(self.frame_start_ns)) as f64 / 1_000_000.0;
400
401 if frame_ms > 0.0 {
403 let fps = 1000.0 / frame_ms;
404 if self.fps_history.len() >= 128 { self.fps_history.pop_front(); }
405 self.fps_history.push_back(fps);
406 self.fps = self.fps_history.iter().sum::<f64>() / self.fps_history.len() as f64;
407 }
408
409 let counter_snapshot: HashMap<String, f64> = self.counters.iter()
411 .map(|(k, v)| (k.clone(), v.value))
412 .collect();
413 for c in self.counters.values_mut() {
414 c.flush();
415 }
416
417 while let Some(open) = self.open_stack.pop() {
419 let span = Span {
420 name: open.name.clone(),
421 depth: open.depth,
422 start_ns: open.start_ns,
423 end_ns: frame_end_ns,
424 thread_id: 0,
425 };
426 let ms = span.duration_ms();
427 self.span_stats.entry(open.name.clone())
428 .or_insert_with(|| SpanStats::new(&open.name))
429 .push(ms);
430 self.current_spans.push(span);
431 }
432
433 let record = FrameRecord {
434 frame_index: self.frame_index,
435 spans: std::mem::take(&mut self.current_spans),
436 counters: counter_snapshot,
437 frame_ms,
438 };
439
440 if self.frame_history.len() >= self.max_history {
441 self.frame_history.pop_front();
442 }
443 self.frame_history.push_back(record.clone());
444 self.frame_index += 1;
445
446 record
447 }
448
449 pub fn stats(&self, name: &str) -> Option<&SpanStats> {
451 self.span_stats.get(name)
452 }
453
454 pub fn build_flame_graph(&self, last_n_frames: usize) -> FlameNode {
456 let mut root = FlameNode::new("root");
457 let start = self.frame_history.len().saturating_sub(last_n_frames);
458
459 for frame in self.frame_history.iter().skip(start) {
460 let mut path_stack: Vec<String> = Vec::new();
462 for span in &frame.spans {
463 while path_stack.len() > span.depth as usize {
464 path_stack.pop();
465 }
466 path_stack.push(span.name.clone());
467
468 let mut node = &mut root;
470 for seg in &path_stack {
471 node = node.get_or_insert_child(seg);
472 }
473 node.total_ms += span.duration_ms();
474 node.call_count += 1;
475 }
476 }
477
478 root.recompute_self_ms();
479 root
480 }
481
482 pub fn recent_frames(&self, n: usize) -> &[FrameRecord] {
484 let start = self.frame_history.len().saturating_sub(n);
485 let _ = start; &[] }
491
492 pub fn last_frame(&self) -> Option<&FrameRecord> {
494 self.frame_history.back()
495 }
496
497 pub fn avg_fps(&self) -> f64 { self.fps }
499
500 pub fn avg_frame_ms(&self) -> f64 {
502 if self.fps > 0.0 { 1000.0 / self.fps } else { 0.0 }
503 }
504
505 pub fn summary(&self) -> String {
507 let mut lines = Vec::new();
508 lines.push(format!("=== Profiler Frame {} ===", self.frame_index));
509 lines.push(format!("FPS: {:.1} Frame: {:.2}ms", self.avg_fps(), self.avg_frame_ms()));
510
511 let mut sorted_stats: Vec<(&String, &SpanStats)> = self.span_stats.iter().collect();
512 sorted_stats.sort_by(|a, b| b.1.avg_ms().partial_cmp(&a.1.avg_ms()).unwrap());
513
514 for (name, stats) in sorted_stats.iter().take(15) {
515 lines.push(format!(
516 " {:30} avg={:.3}ms p95={:.3}ms p99={:.3}ms",
517 name, stats.avg_ms(), stats.p95(), stats.p99()
518 ));
519 }
520
521 if !self.counters.is_empty() {
522 lines.push(" --- Counters ---".to_string());
523 for (name, counter) in &self.counters {
524 lines.push(format!(" {:30} avg={:.1} peak={:.1}", name, counter.avg(), counter.peak()));
525 }
526 }
527
528 lines.join("\n")
529 }
530
531 pub fn reset(&mut self) {
533 self.span_stats.clear();
534 self.counters.clear();
535 self.frame_history.clear();
536 self.current_spans.clear();
537 self.open_stack.clear();
538 self.fps_history.clear();
539 self.frame_index = 0;
540 }
541}
542
543impl Default for Profiler {
544 fn default() -> Self { Self::new() }
545}
546
547pub struct ScopedSpan<'a> {
557 profiler: &'a mut Profiler,
558 name: String,
559}
560
561impl<'a> ScopedSpan<'a> {
562 pub fn new(profiler: &'a mut Profiler, name: &str) -> Self {
563 profiler.begin(name);
564 Self { profiler, name: name.to_string() }
565 }
566}
567
568impl<'a> Drop for ScopedSpan<'a> {
569 fn drop(&mut self) {
570 self.profiler.end(&self.name);
571 }
572}
573
574#[derive(Debug, Clone)]
578pub struct FrameBudget {
579 pub target_fps: f64,
580 pub physics_ms: f64,
581 pub render_ms: f64,
582 pub ai_ms: f64,
583 pub audio_ms: f64,
584 pub scripting_ms: f64,
585}
586
587impl FrameBudget {
588 pub fn from_target_fps(fps: f64) -> Self {
589 let total = 1000.0 / fps;
590 Self {
591 target_fps: fps,
592 physics_ms: total * 0.20,
593 render_ms: total * 0.45,
594 ai_ms: total * 0.15,
595 audio_ms: total * 0.05,
596 scripting_ms: total * 0.10,
597 }
598 }
599
600 pub fn total_ms(&self) -> f64 { 1000.0 / self.target_fps }
601
602 pub fn check_violations(&self, frame: &FrameRecord) -> Vec<BudgetViolation> {
603 let agg = frame.aggregate();
604 let mut violations = Vec::new();
605
606 let check = |cat: &str, budget: f64| -> Option<BudgetViolation> {
607 let total: f64 = agg.iter()
608 .filter(|(k, _)| k.starts_with(cat))
609 .map(|(_, (ms, _))| ms)
610 .sum();
611 if total > budget {
612 Some(BudgetViolation { category: cat.to_string(), actual_ms: total, budget_ms: budget })
613 } else { None }
614 };
615
616 if let Some(v) = check("physics", self.physics_ms) { violations.push(v); }
617 if let Some(v) = check("render", self.render_ms) { violations.push(v); }
618 if let Some(v) = check("ai", self.ai_ms) { violations.push(v); }
619 if let Some(v) = check("audio", self.audio_ms) { violations.push(v); }
620 if let Some(v) = check("script", self.scripting_ms) { violations.push(v); }
621
622 violations
623 }
624}
625
626#[derive(Debug, Clone)]
628pub struct BudgetViolation {
629 pub category: String,
630 pub actual_ms: f64,
631 pub budget_ms: f64,
632}
633
634impl BudgetViolation {
635 pub fn overage_ms(&self) -> f64 { (self.actual_ms - self.budget_ms).max(0.0) }
636 pub fn overage_pct(&self) -> f64 { self.overage_ms() / self.budget_ms * 100.0 }
637}
638
639#[derive(Debug, Clone)]
643pub struct StutterDetector {
644 pub threshold_multiplier: f64, pub window: usize,
646 history: VecDeque<f64>,
647 pub stutter_count: u32,
648 pub last_stutter_frame: u64,
649}
650
651impl StutterDetector {
652 pub fn new(threshold: f64) -> Self {
653 Self {
654 threshold_multiplier: threshold,
655 window: 60,
656 history: VecDeque::new(),
657 stutter_count: 0,
658 last_stutter_frame: 0,
659 }
660 }
661
662 pub fn update(&mut self, frame_ms: f64, frame_index: u64) -> bool {
663 if self.history.len() >= self.window { self.history.pop_front(); }
664 self.history.push_back(frame_ms);
665
666 if self.history.len() < 10 { return false; }
667
668 let avg: f64 = self.history.iter().sum::<f64>() / self.history.len() as f64;
669 let is_stutter = frame_ms > avg * self.threshold_multiplier;
670
671 if is_stutter {
672 self.stutter_count += 1;
673 self.last_stutter_frame = frame_index;
674 }
675 is_stutter
676 }
677
678 pub fn rolling_avg_ms(&self) -> f64 {
679 if self.history.is_empty() { return 0.0; }
680 self.history.iter().sum::<f64>() / self.history.len() as f64
681 }
682
683 pub fn reset_stutter_count(&mut self) { self.stutter_count = 0; }
684}
685
686#[derive(Debug, Clone)]
690pub struct ProfileOverlay {
691 pub frame_ms_graph: Vec<f64>,
693 pub system_bars: Vec<(String, f64)>,
695 pub avg_fps: f64,
696 pub avg_frame_ms: f64,
697 pub p99_frame_ms: f64,
698 pub stutter_count: u32,
699 pub memory_mb: f64,
700}
701
702impl ProfileOverlay {
703 pub fn from_profiler(p: &Profiler, sd: &StutterDetector) -> Self {
704 let frame_ms_graph: Vec<f64> = p.frame_history.iter().map(|f| f.frame_ms).collect();
705
706 let mut system_bars: Vec<(String, f64)> = p.span_stats.iter()
708 .filter(|(_, s)| s.avg_ms() > 0.01)
709 .map(|(k, s)| (k.clone(), s.avg_ms()))
710 .collect();
711 system_bars.sort_by(|a, b| b.1.partial_cmp(&a.1).unwrap());
712 system_bars.truncate(20);
713
714 let p99 = if frame_ms_graph.len() >= 2 {
715 let mut sorted = frame_ms_graph.clone();
716 sorted.sort_by(|a, b| a.partial_cmp(b).unwrap());
717 sorted[(sorted.len() as f64 * 0.99) as usize]
718 } else { 0.0 };
719
720 Self {
721 frame_ms_graph,
722 system_bars,
723 avg_fps: p.avg_fps(),
724 avg_frame_ms: p.avg_frame_ms(),
725 p99_frame_ms: p99,
726 stutter_count: sd.stutter_count,
727 memory_mb: p.memory.total_used_mb(),
728 }
729 }
730}
731
732#[derive(Debug, Clone)]
736pub struct ThreadProfile {
737 pub thread_name: String,
738 pub thread_id: u64,
739 pub spans: Vec<Span>,
740}
741
742pub fn merge_thread_profiles(main: FrameRecord, threads: Vec<ThreadProfile>) -> FrameRecord {
744 let mut merged = main;
745 for tp in threads {
746 let mut tagged: Vec<Span> = tp.spans.into_iter()
747 .map(|mut s| { s.thread_id = tp.thread_id; s.name = format!("[{}] {}", tp.thread_name, s.name); s })
748 .collect();
749 merged.spans.append(&mut tagged);
750 }
751 merged.spans.sort_by_key(|s| s.start_ns);
753 merged
754}
755
756pub fn export_csv(profiler: &Profiler) -> String {
760 let mut lines = vec!["frame,span_name,depth,start_ns,end_ns,duration_us".to_string()];
761 for frame in &profiler.frame_history {
762 for span in &frame.spans {
763 lines.push(format!(
764 "{},{},{},{},{},{}",
765 frame.frame_index, span.name, span.depth,
766 span.start_ns, span.end_ns,
767 (span.end_ns.saturating_sub(span.start_ns)) / 1_000,
768 ));
769 }
770 }
771 lines.join("\n")
772}
773
774#[derive(Debug, Clone)]
778pub struct ProfileMarker {
779 pub name: String,
780 pub time_ns: u64,
781 pub frame: u64,
782 pub color: [f32; 4],
783 pub extra: String,
784}
785
786#[derive(Debug, Clone, Default)]
788pub struct MarkerLog {
789 pub markers: Vec<ProfileMarker>,
790}
791
792impl MarkerLog {
793 pub fn mark(&mut self, name: &str, time_ns: u64, frame: u64, extra: &str) {
794 self.markers.push(ProfileMarker {
795 name: name.to_string(),
796 time_ns,
797 frame,
798 color: [1.0, 1.0, 0.0, 1.0],
799 extra: extra.to_string(),
800 });
801 }
802
803 pub fn mark_colored(&mut self, name: &str, time_ns: u64, frame: u64, color: [f32;4]) {
804 self.markers.push(ProfileMarker {
805 name: name.to_string(), time_ns, frame, color, extra: String::new(),
806 });
807 }
808
809 pub fn since_frame(&self, frame: u64) -> impl Iterator<Item = &ProfileMarker> {
810 self.markers.iter().filter(move |m| m.frame >= frame)
811 }
812
813 pub fn clear_before(&mut self, frame: u64) {
814 self.markers.retain(|m| m.frame >= frame);
815 }
816}