1use super::super::helpers::draw_text_overlay;
4use super::super::DevToolsConfig;
5use super::types::{ComponentStats, Frame, ProfilerView, RenderEvent};
6use crate::layout::Rect;
7use crate::render::Buffer;
8use crate::style::Color;
9use std::collections::HashMap;
10use std::time::{Duration, Instant};
11
12pub struct Profiler {
14 recording: bool,
16 frames: Vec<Frame>,
18 current_frame: Option<Frame>,
20 stats: HashMap<String, ComponentStats>,
22 frame_counter: u64,
24 recording_start: Option<Instant>,
26 view: ProfilerView,
28 selected_frame: Option<usize>,
30 scroll_offset: usize,
32}
33
34impl Profiler {
35 pub fn new() -> Self {
37 Self {
38 recording: false,
39 frames: Vec::new(),
40 current_frame: None,
41 stats: HashMap::new(),
42 frame_counter: 0,
43 recording_start: None,
44 view: ProfilerView::default(),
45 selected_frame: None,
46 scroll_offset: 0,
47 }
48 }
49
50 pub fn start_recording(&mut self) {
52 self.recording = true;
53 self.recording_start = Some(Instant::now());
54 self.frames.clear();
55 self.stats.clear();
56 self.frame_counter = 0;
57 self.start_frame();
58 }
59
60 pub fn stop_recording(&mut self) {
62 self.end_frame();
63 self.recording = false;
64 self.recording_start = None;
65 }
66
67 pub fn is_recording(&self) -> bool {
69 self.recording
70 }
71
72 pub fn toggle_recording(&mut self) {
74 if self.recording {
75 self.stop_recording();
76 } else {
77 self.start_recording();
78 }
79 }
80
81 pub fn start_frame(&mut self) {
83 if self.recording {
84 self.frame_counter += 1;
85 self.current_frame = Some(Frame::new(self.frame_counter));
86 }
87 }
88
89 pub fn end_frame(&mut self) {
91 if let Some(mut frame) = self.current_frame.take() {
92 frame.end();
93 self.frames.push(frame);
94 }
95 }
96
97 pub fn record_render(&mut self, event: RenderEvent) {
99 let stats = self
101 .stats
102 .entry(event.component.clone())
103 .or_insert_with(|| ComponentStats::new(&event.component));
104 stats.record(event.duration, event.reason);
105
106 if let Some(frame) = &mut self.current_frame {
108 frame.add_event(event);
109 }
110 }
111
112 pub fn frame_count(&self) -> usize {
114 self.frames.len()
115 }
116
117 pub fn recording_duration(&self) -> Duration {
119 self.recording_start
120 .map(|start| start.elapsed())
121 .unwrap_or(Duration::ZERO)
122 }
123
124 pub fn avg_frame_time(&self) -> Duration {
126 if self.frames.is_empty() {
127 return Duration::ZERO;
128 }
129 let total: Duration = self.frames.iter().map(|f| f.duration).sum();
130 total / self.frames.len() as u32
131 }
132
133 pub fn stats_by_time(&self) -> Vec<&ComponentStats> {
135 let mut stats: Vec<_> = self.stats.values().collect();
136 stats.sort_by(|a, b| b.total_time.cmp(&a.total_time));
137 stats
138 }
139
140 pub fn stats_by_count(&self) -> Vec<&ComponentStats> {
142 let mut stats: Vec<_> = self.stats.values().collect();
143 stats.sort_by(|a, b| b.render_count.cmp(&a.render_count));
144 stats
145 }
146
147 pub fn view(&self) -> ProfilerView {
149 self.view
150 }
151
152 pub fn set_view(&mut self, view: ProfilerView) {
154 self.view = view;
155 self.scroll_offset = 0;
156 }
157
158 pub fn next_view(&mut self) {
160 self.view = self.view.next();
161 self.scroll_offset = 0;
162 }
163
164 pub fn select_frame(&mut self, index: Option<usize>) {
166 self.selected_frame = index;
167 }
168
169 pub fn scroll_up(&mut self) {
171 self.scroll_offset = self.scroll_offset.saturating_sub(1);
172 }
173
174 pub fn scroll_down(&mut self) {
176 self.scroll_offset += 1;
177 }
178
179 pub fn clear(&mut self) {
181 self.frames.clear();
182 self.stats.clear();
183 self.current_frame = None;
184 self.frame_counter = 0;
185 self.selected_frame = None;
186 self.scroll_offset = 0;
187 }
188
189 pub fn render_content(&self, buffer: &mut Buffer, area: Rect, config: &DevToolsConfig) {
191 match self.view {
192 ProfilerView::Flamegraph => self.render_flamegraph(buffer, area, config),
193 ProfilerView::Timeline => self.render_timeline(buffer, area, config),
194 ProfilerView::Ranked => self.render_ranked(buffer, area, config),
195 ProfilerView::Counts => self.render_counts(buffer, area, config),
196 }
197 }
198
199 fn render_flamegraph(&self, buffer: &mut Buffer, area: Rect, config: &DevToolsConfig) {
200 if self.frames.is_empty() {
201 self.render_empty(buffer, area, config, "No data. Press R to start recording.");
202 return;
203 }
204
205 let header = format!(
207 "Flamegraph - {} frames, avg {:.2}ms/frame",
208 self.frames.len(),
209 self.avg_frame_time().as_secs_f64() * 1000.0
210 );
211 self.render_text(buffer, area.x, area.y, &header, config.fg_color);
212
213 let frame = self
215 .selected_frame
216 .and_then(|i| self.frames.get(i))
217 .or_else(|| self.frames.last());
218
219 if let Some(frame) = frame {
220 let content_y = area.y + 2;
221 let content_height = area.height.saturating_sub(3);
222
223 let max_depth = frame.events.iter().map(|e| e.depth).max().unwrap_or(0);
225 let row_height = if max_depth > 0 {
226 (content_height as usize / (max_depth + 1)).max(1)
227 } else {
228 content_height as usize
229 };
230
231 for event in &frame.events {
232 let y = content_y + (event.depth * row_height) as u16;
233 if y >= area.y + area.height {
234 continue;
235 }
236
237 let total_time = frame.total_render_time().as_nanos() as f64;
239 let event_time = event.duration.as_nanos() as f64;
240 let width = if total_time > 0.0 {
241 ((event_time / total_time) * area.width as f64) as u16
242 } else {
243 1
244 };
245 let width = width.max(1).min(area.width);
246
247 let color = event.reason.color();
249 for x in area.x..area.x + width {
250 if let Some(cell) = buffer.get_mut(x, y) {
251 cell.bg = Some(color);
252 }
253 }
254
255 let label = format!(
257 "{} ({:.2}ms)",
258 event.component,
259 event.duration.as_secs_f64() * 1000.0
260 );
261 self.render_text(buffer, area.x, y, &label, config.bg_color);
262 }
263 }
264 }
265
266 fn render_timeline(&self, buffer: &mut Buffer, area: Rect, config: &DevToolsConfig) {
267 if self.frames.is_empty() {
268 self.render_empty(buffer, area, config, "No data. Press R to start recording.");
269 return;
270 }
271
272 let header = format!("Timeline - {} frames", self.frames.len());
274 self.render_text(buffer, area.x, area.y, &header, config.fg_color);
275
276 let content_y = area.y + 2;
277 let content_height = area.height.saturating_sub(3) as usize;
278
279 let max_time = self
281 .frames
282 .iter()
283 .map(|f| f.duration)
284 .max()
285 .unwrap_or(Duration::from_millis(16));
286
287 let visible_frames = area.width as usize;
289 let start_frame = self
290 .scroll_offset
291 .min(self.frames.len().saturating_sub(visible_frames));
292
293 for (i, frame) in self
294 .frames
295 .iter()
296 .skip(start_frame)
297 .take(visible_frames)
298 .enumerate()
299 {
300 let x = area.x + i as u16;
301 let height = ((frame.duration.as_nanos() as f64 / max_time.as_nanos() as f64)
302 * content_height as f64) as u16;
303 let height = height.max(1);
304
305 let bar_y = content_y + (content_height as u16).saturating_sub(height);
306
307 let color = if frame.duration < Duration::from_millis(8) {
309 Color::rgb(100, 200, 100) } else if frame.duration < Duration::from_millis(16) {
311 Color::rgb(200, 200, 100) } else if frame.duration < Duration::from_millis(33) {
313 Color::rgb(220, 150, 100) } else {
315 Color::rgb(220, 100, 100) };
317
318 for y in bar_y..content_y + content_height as u16 {
320 if let Some(cell) = buffer.get_mut(x, y) {
321 cell.bg = Some(color);
322 cell.symbol = ' ';
323 }
324 }
325
326 if self.selected_frame == Some(start_frame + i) {
328 if let Some(cell) = buffer.get_mut(x, bar_y) {
329 cell.symbol = '▼';
330 cell.fg = Some(config.accent_color);
331 }
332 }
333 }
334
335 if let Some(idx) = self.selected_frame {
337 if let Some(frame) = self.frames.get(idx) {
338 let info = format!(
339 "Frame {}: {:.2}ms, {} renders",
340 frame.number,
341 frame.duration.as_secs_f64() * 1000.0,
342 frame.event_count()
343 );
344 self.render_text(
345 buffer,
346 area.x,
347 area.y + area.height - 1,
348 &info,
349 config.accent_color,
350 );
351 }
352 }
353 }
354
355 fn render_ranked(&self, buffer: &mut Buffer, area: Rect, config: &DevToolsConfig) {
356 if self.stats.is_empty() {
357 self.render_empty(buffer, area, config, "No data. Press R to start recording.");
358 return;
359 }
360
361 let header = "Ranked by Total Time";
363 self.render_text(buffer, area.x, area.y, header, config.fg_color);
364
365 let content_y = area.y + 2;
366 let content_height = area.height.saturating_sub(3) as usize;
367
368 let stats = self.stats_by_time();
369
370 for (i, stat) in stats
371 .iter()
372 .skip(self.scroll_offset)
373 .take(content_height)
374 .enumerate()
375 {
376 let y = content_y + i as u16;
377
378 let name = if stat.name.len() > 20 {
380 format!("{}...", &stat.name[..17])
381 } else {
382 stat.name.clone()
383 };
384
385 let line = format!(
387 "{:<20} {:>6.2}ms total {:>6.2}ms avg {:>4} renders",
388 name,
389 stat.total_time.as_secs_f64() * 1000.0,
390 stat.avg_time.as_secs_f64() * 1000.0,
391 stat.render_count
392 );
393
394 self.render_text(buffer, area.x, y, &line, config.fg_color);
395
396 if let Some(cell) = buffer.get_mut(area.x + area.width - 2, y) {
398 cell.bg = Some(stat.last_reason.color());
399 }
400 }
401 }
402
403 fn render_counts(&self, buffer: &mut Buffer, area: Rect, config: &DevToolsConfig) {
404 if self.stats.is_empty() {
405 self.render_empty(buffer, area, config, "No data. Press R to start recording.");
406 return;
407 }
408
409 let header = "Ranked by Render Count";
411 self.render_text(buffer, area.x, area.y, header, config.fg_color);
412
413 let content_y = area.y + 2;
414 let content_height = area.height.saturating_sub(3) as usize;
415
416 let stats = self.stats_by_count();
417 let max_count = stats.first().map(|s| s.render_count).unwrap_or(1);
418
419 for (i, stat) in stats
420 .iter()
421 .skip(self.scroll_offset)
422 .take(content_height)
423 .enumerate()
424 {
425 let y = content_y + i as u16;
426
427 let name = if stat.name.len() > 20 {
429 format!("{}...", &stat.name[..17])
430 } else {
431 stat.name.clone()
432 };
433
434 let bar_width =
436 ((stat.render_count as f64 / max_count as f64) * (area.width as f64 / 2.0)) as u16;
437 let bar_width = bar_width.max(1);
438
439 let count_str = format!("{:<20} {:>6}", name, stat.render_count);
441 self.render_text(buffer, area.x, y, &count_str, config.fg_color);
442
443 let bar_start = area.x + 28;
445 for x in bar_start..bar_start + bar_width {
446 if x < area.x + area.width {
447 if let Some(cell) = buffer.get_mut(x, y) {
448 cell.bg = Some(config.accent_color);
449 cell.symbol = ' ';
450 }
451 }
452 }
453 }
454 }
455
456 fn render_empty(&self, buffer: &mut Buffer, area: Rect, config: &DevToolsConfig, msg: &str) {
457 let x = area.x + (area.width.saturating_sub(msg.len() as u16)) / 2;
458 let y = area.y + area.height / 2;
459 self.render_text(buffer, x, y, msg, config.fg_color);
460 }
461
462 fn render_text(&self, buffer: &mut Buffer, x: u16, y: u16, text: &str, color: Color) {
463 draw_text_overlay(buffer, x, y, text, color);
464 }
465}
466
467impl Default for Profiler {
468 fn default() -> Self {
469 Self::new()
470 }
471}
472
473#[cfg(test)]
474mod tests {
475 use super::*;
476 use crate::devtools::profiler::types::RenderReason;
477
478 #[test]
479 fn test_profiler_creation() {
480 let profiler = Profiler::new();
481 assert!(!profiler.is_recording());
482 assert_eq!(profiler.frame_count(), 0);
483 }
484
485 #[test]
486 fn test_profiler_recording() {
487 let mut profiler = Profiler::new();
488
489 profiler.start_recording();
490 assert!(profiler.is_recording());
491
492 profiler.record_render(RenderEvent::new("Button", Duration::from_micros(100)));
493 profiler.end_frame();
494 profiler.start_frame();
495 profiler.record_render(RenderEvent::new("Input", Duration::from_micros(200)));
496 profiler.end_frame();
497
498 profiler.stop_recording();
499 assert!(!profiler.is_recording());
500 assert_eq!(profiler.frame_count(), 2);
501 }
502
503 #[test]
504 fn test_profiler_toggle() {
505 let mut profiler = Profiler::new();
506
507 profiler.toggle_recording();
508 assert!(profiler.is_recording());
509
510 profiler.toggle_recording();
511 assert!(!profiler.is_recording());
512 }
513
514 #[test]
515 fn test_render_event() {
516 let event = RenderEvent::new("MyComponent", Duration::from_millis(5))
517 .parent("ParentComponent")
518 .reason(RenderReason::StateChange)
519 .depth(2);
520
521 assert_eq!(event.component, "MyComponent");
522 assert_eq!(event.parent, Some("ParentComponent".to_string()));
523 assert_eq!(event.reason, RenderReason::StateChange);
524 assert_eq!(event.depth, 2);
525 }
526
527 #[test]
528 fn test_frame() {
529 let mut frame = Frame::new(1);
530 frame.add_event(RenderEvent::new("A", Duration::from_micros(100)));
531 frame.add_event(RenderEvent::new("B", Duration::from_micros(200)));
532 frame.end();
533
534 assert_eq!(frame.number, 1);
535 assert_eq!(frame.event_count(), 2);
536 assert_eq!(frame.total_render_time(), Duration::from_micros(300));
537 }
538
539 #[test]
540 fn test_component_stats() {
541 let mut stats = ComponentStats::new("Button");
542
543 stats.record(Duration::from_micros(100), RenderReason::Initial);
544 stats.record(Duration::from_micros(200), RenderReason::StateChange);
545 stats.record(Duration::from_micros(150), RenderReason::PropsChange);
546
547 assert_eq!(stats.render_count, 3);
548 assert_eq!(stats.total_time, Duration::from_micros(450));
549 assert_eq!(stats.min_time, Duration::from_micros(100));
550 assert_eq!(stats.max_time, Duration::from_micros(200));
551 assert_eq!(stats.last_reason, RenderReason::PropsChange);
552 }
553
554 #[test]
555 fn test_stats_sorting() {
556 let mut profiler = Profiler::new();
557 profiler.start_recording();
558
559 profiler.record_render(RenderEvent::new("Fast", Duration::from_micros(50)));
561 profiler.record_render(RenderEvent::new("Slow", Duration::from_micros(500)));
562 profiler.record_render(RenderEvent::new("Medium", Duration::from_micros(200)));
563
564 let by_time = profiler.stats_by_time();
565 assert_eq!(by_time[0].name, "Slow");
566 assert_eq!(by_time[1].name, "Medium");
567 assert_eq!(by_time[2].name, "Fast");
568 }
569
570 #[test]
571 fn test_profiler_view_cycle() {
572 let mut profiler = Profiler::new();
573 assert_eq!(profiler.view(), ProfilerView::Flamegraph);
574
575 profiler.next_view();
576 assert_eq!(profiler.view(), ProfilerView::Timeline);
577
578 profiler.next_view();
579 assert_eq!(profiler.view(), ProfilerView::Ranked);
580
581 profiler.next_view();
582 assert_eq!(profiler.view(), ProfilerView::Counts);
583
584 profiler.next_view();
585 assert_eq!(profiler.view(), ProfilerView::Flamegraph);
586 }
587
588 #[test]
589 fn test_profiler_clear() {
590 let mut profiler = Profiler::new();
591 profiler.start_recording();
592 profiler.record_render(RenderEvent::new("Test", Duration::from_micros(100)));
593 profiler.end_frame();
594 profiler.stop_recording();
595
596 assert!(!profiler.stats.is_empty());
597 assert!(!profiler.frames.is_empty());
598
599 profiler.clear();
600
601 assert!(profiler.stats.is_empty());
602 assert!(profiler.frames.is_empty());
603 }
604
605 #[test]
606 fn test_render_reason_colors() {
607 let reasons = [
609 RenderReason::Initial,
610 RenderReason::StateChange,
611 RenderReason::PropsChange,
612 RenderReason::ContextChange,
613 RenderReason::ParentRender,
614 RenderReason::ForceUpdate,
615 ];
616
617 for reason in &reasons {
618 let _ = reason.color();
620 let _ = reason.label();
621 }
622 }
623
624 #[test]
625 fn test_profiler_scroll() {
626 let mut profiler = Profiler::new();
627
628 profiler.scroll_down();
629 assert_eq!(profiler.scroll_offset, 1);
630
631 profiler.scroll_down();
632 assert_eq!(profiler.scroll_offset, 2);
633
634 profiler.scroll_up();
635 assert_eq!(profiler.scroll_offset, 1);
636
637 profiler.scroll_up();
638 profiler.scroll_up(); assert_eq!(profiler.scroll_offset, 0);
640 }
641
642 #[test]
643 fn test_profiler_select_frame() {
644 let mut profiler = Profiler::new();
645
646 profiler.select_frame(Some(5));
647 assert_eq!(profiler.selected_frame, Some(5));
648
649 profiler.select_frame(None);
650 assert_eq!(profiler.selected_frame, None);
651 }
652
653 #[test]
654 fn test_stats_by_count() {
655 let mut profiler = Profiler::new();
656 profiler.start_recording();
657
658 profiler.record_render(RenderEvent::new("Once", Duration::from_micros(100)));
660 profiler.record_render(RenderEvent::new("Twice", Duration::from_micros(50)));
661 profiler.record_render(RenderEvent::new("Twice", Duration::from_micros(50)));
662
663 let by_count = profiler.stats_by_count();
664 assert_eq!(by_count[0].name, "Twice");
665 assert_eq!(by_count[1].name, "Once");
666 }
667
668 #[test]
669 fn test_set_view() {
670 let mut profiler = Profiler::new();
671 assert_eq!(profiler.view(), ProfilerView::Flamegraph);
672 assert_eq!(profiler.scroll_offset, 0);
673
674 profiler.set_view(ProfilerView::Timeline);
675 assert_eq!(profiler.view(), ProfilerView::Timeline);
676 assert_eq!(profiler.scroll_offset, 0);
678
679 profiler.scroll_down();
680 assert_eq!(profiler.scroll_offset, 1);
681
682 profiler.set_view(ProfilerView::Ranked);
683 assert_eq!(profiler.view(), ProfilerView::Ranked);
684 assert_eq!(profiler.scroll_offset, 0);
685 }
686
687 #[test]
688 fn test_recording_duration() {
689 let mut profiler = Profiler::new();
690
691 assert_eq!(profiler.recording_duration(), Duration::ZERO);
693
694 profiler.start_recording();
695 std::thread::sleep(std::time::Duration::from_millis(2));
697 let duration_while_recording = profiler.recording_duration();
698 assert!(duration_while_recording >= Duration::from_millis(2));
699
700 profiler.stop_recording();
701
702 assert_eq!(profiler.recording_duration(), Duration::ZERO);
704 }
705
706 #[test]
707 fn test_avg_frame_time_empty() {
708 let profiler = Profiler::new();
709 assert_eq!(profiler.avg_frame_time(), Duration::ZERO);
710 }
711
712 #[test]
713 fn test_avg_frame_time() {
714 let mut profiler = Profiler::new();
715 profiler.start_recording();
716
717 profiler.record_render(RenderEvent::new("Test", Duration::from_micros(100)));
718 std::thread::sleep(std::time::Duration::from_millis(1));
719 profiler.end_frame();
720
721 profiler.record_render(RenderEvent::new("Test", Duration::from_micros(200)));
722 std::thread::sleep(std::time::Duration::from_millis(1));
723 profiler.end_frame();
724
725 profiler.stop_recording();
726
727 let avg = profiler.avg_frame_time();
730 assert!(avg >= Duration::from_millis(1));
731 }
732}