1use alloc::{
7 string::{String, ToString},
8 vec::Vec,
9};
10
11#[derive(Debug, Clone)]
13pub(crate) struct FlameFrame {
14 pub(crate) name: String,
15 pub(crate) samples: u64,
16 pub(crate) children: Vec<FlameFrame>,
17}
18
19impl FlameFrame {
20 pub(crate) fn new(name: &str) -> Self {
21 Self {
22 name: name.to_string(),
23 samples: 0,
24 children: Vec::new(),
25 }
26 }
27
28 pub(crate) fn add_child(&mut self, child: FlameFrame) {
29 self.children.push(child);
30 }
31
32 pub(crate) fn total_samples(&self) -> u64 {
34 self.samples + self.children.iter().map(|c| c.total_samples()).sum::<u64>()
35 }
36
37 pub(crate) fn get_or_create_child(&mut self, name: &str) -> &mut FlameFrame {
39 let idx = self.children.iter().position(|c| c.name == name);
40 if let Some(i) = idx {
41 &mut self.children[i]
42 } else {
43 self.children.push(FlameFrame::new(name));
44 self.children.last_mut().unwrap()
45 }
46 }
47}
48
49#[derive(Debug, Clone)]
51pub(crate) struct StackSample {
52 pub(crate) timestamp: u64,
53 pub(crate) frames: Vec<String>,
54 pub(crate) cpu: u32,
55}
56
57#[derive(Debug, Clone, Copy)]
59pub(crate) struct CpuDataPoint {
60 pub(crate) timestamp: u64,
61 pub(crate) usage_percent: u8,
62 pub(crate) cpu_id: u32,
63}
64
65#[derive(Debug, Clone, Copy)]
67pub(crate) struct MemDataPoint {
68 pub(crate) timestamp: u64,
69 pub(crate) used_bytes: u64,
70 pub(crate) total_bytes: u64,
71}
72
73pub(crate) struct ProfilerSession {
75 pub(crate) name: String,
76 pub(crate) samples: Vec<StackSample>,
77 pub(crate) cpu_timeline: Vec<CpuDataPoint>,
78 pub(crate) mem_timeline: Vec<MemDataPoint>,
79 pub(crate) start_time: u64,
80 pub(crate) end_time: u64,
81}
82
83impl ProfilerSession {
84 pub(crate) fn new(name: &str) -> Self {
85 Self {
86 name: name.to_string(),
87 samples: Vec::new(),
88 cpu_timeline: Vec::new(),
89 mem_timeline: Vec::new(),
90 start_time: 0,
91 end_time: 0,
92 }
93 }
94
95 pub(crate) fn add_sample(&mut self, sample: StackSample) {
96 if self.samples.is_empty() {
97 self.start_time = sample.timestamp;
98 }
99 self.end_time = sample.timestamp;
100 self.samples.push(sample);
101 }
102
103 pub(crate) fn add_cpu_point(&mut self, point: CpuDataPoint) {
104 self.cpu_timeline.push(point);
105 }
106
107 pub(crate) fn add_mem_point(&mut self, point: MemDataPoint) {
108 self.mem_timeline.push(point);
109 }
110
111 pub(crate) fn build_flame_graph(&self) -> FlameFrame {
113 let mut root = FlameFrame::new("all");
114
115 for sample in &self.samples {
116 let mut current = &mut root;
117 for frame in &sample.frames {
118 current = current.get_or_create_child(frame);
119 current.samples += 1;
120 }
121 }
122
123 root
124 }
125
126 pub(crate) fn duration(&self) -> u64 {
128 self.end_time.saturating_sub(self.start_time)
129 }
130
131 pub(crate) fn sample_count(&self) -> usize {
132 self.samples.len()
133 }
134
135 pub(crate) fn avg_cpu_usage(&self) -> u8 {
137 if self.cpu_timeline.is_empty() {
138 return 0;
139 }
140 let sum: u64 = self
141 .cpu_timeline
142 .iter()
143 .map(|p| p.usage_percent as u64)
144 .sum();
145 (sum / self.cpu_timeline.len() as u64) as u8
146 }
147
148 pub(crate) fn peak_memory(&self) -> u64 {
150 self.mem_timeline
151 .iter()
152 .map(|p| p.used_bytes)
153 .max()
154 .unwrap_or(0)
155 }
156}
157
158pub(crate) struct ProfilerGui {
160 pub(crate) width: u32,
161 pub(crate) height: u32,
162 pub(crate) view: ProfilerView,
163}
164
165#[derive(Debug, Clone, Copy, PartialEq, Eq)]
166pub(crate) enum ProfilerView {
167 FlameGraph,
168 CpuTimeline,
169 MemoryTimeline,
170 CallTree,
171}
172
173impl ProfilerGui {
174 pub(crate) fn new(width: u32, height: u32) -> Self {
175 Self {
176 width,
177 height,
178 view: ProfilerView::FlameGraph,
179 }
180 }
181
182 pub(crate) fn switch_view(&mut self, view: ProfilerView) {
183 self.view = view;
184 }
185
186 pub(crate) fn render_flame_graph(&self, root: &FlameFrame, buf: &mut [u32]) {
188 let total = root.total_samples();
189 if total == 0 || buf.len() < (self.width * self.height) as usize {
190 return;
191 }
192
193 for pixel in buf.iter_mut() {
195 *pixel = 0xFF1A1A2E; }
197
198 self.render_flame_frame(root, buf, 0, 0, self.width, total, 0);
200 }
201
202 fn render_flame_frame(
203 &self,
204 frame: &FlameFrame,
205 buf: &mut [u32],
206 x: u32,
207 _y: u32,
208 width: u32,
209 total_samples: u64,
210 depth: u32,
211 ) {
212 if width < 2 || depth > 50 {
213 return;
214 }
215
216 let bar_height = 20u32;
217 let bar_y = self.height.saturating_sub((depth + 1) * bar_height);
218
219 let color = match depth % 6 {
221 0 => 0xFFE74C3C, 1 => 0xFFE67E22, 2 => 0xFFF39C12, 3 => 0xFF2ECC71, 4 => 0xFF3498DB, _ => 0xFF9B59B6, };
228
229 for dy in 0..bar_height.saturating_sub(1) {
231 let py = bar_y + dy;
232 if py >= self.height {
233 continue;
234 }
235 for dx in 1..width.saturating_sub(1) {
236 let px = x + dx;
237 if px < self.width {
238 buf[(py * self.width + px) as usize] = color;
239 }
240 }
241 }
242
243 let mut child_x = x;
245 for child in &frame.children {
246 let child_total = child.total_samples();
247 let child_width = ((child_total * width as u64) / total_samples.max(1)) as u32;
248 if child_width > 0 {
249 self.render_flame_frame(
250 child,
251 buf,
252 child_x,
253 _y,
254 child_width,
255 total_samples,
256 depth + 1,
257 );
258 child_x += child_width;
259 }
260 }
261 }
262}
263
264#[derive(Debug, Clone)]
266pub(crate) struct CallTreeNode {
267 pub(crate) function_name: String,
268 pub(crate) self_time: u64,
270 pub(crate) total_time: u64,
272 pub(crate) children: Vec<CallTreeNode>,
273 pub(crate) call_count: u64,
274}
275
276impl CallTreeNode {
277 pub(crate) fn new(name: &str) -> Self {
278 Self {
279 function_name: name.to_string(),
280 self_time: 0,
281 total_time: 0,
282 children: Vec::new(),
283 call_count: 0,
284 }
285 }
286
287 fn find_child(&self, name: &str) -> Option<usize> {
289 self.children.iter().position(|c| c.function_name == name)
290 }
291
292 fn get_or_create_child(&mut self, name: &str) -> &mut CallTreeNode {
294 let idx = self.find_child(name);
295 if let Some(i) = idx {
296 &mut self.children[i]
297 } else {
298 self.children.push(CallTreeNode::new(name));
299 self.children.last_mut().unwrap()
300 }
301 }
302}
303
304#[derive(Debug, Clone)]
306pub(crate) struct FlatCallEntry {
307 pub(crate) function_name: String,
308 pub(crate) self_time: u64,
309 pub(crate) total_time: u64,
310 pub(crate) call_count: u64,
311 pub(crate) depth: u32,
312}
313
314pub(crate) struct CallTree {
316 pub(crate) roots: Vec<CallTreeNode>,
317 pub(crate) total_samples: u64,
318}
319
320impl Default for CallTree {
321 fn default() -> Self {
322 Self::new()
323 }
324}
325
326impl CallTree {
327 pub(crate) fn new() -> Self {
328 Self {
329 roots: Vec::new(),
330 total_samples: 0,
331 }
332 }
333
334 pub(crate) fn build_from_stacks(samples: &[StackSample]) -> Self {
339 let mut tree = CallTree::new();
340 tree.total_samples = samples.len() as u64;
341
342 for sample in samples {
343 if sample.frames.is_empty() {
344 continue;
345 }
346
347 let bottom_name = &sample.frames[0];
349
350 let root_idx = tree
352 .roots
353 .iter()
354 .position(|r| r.function_name == *bottom_name);
355 let root = if let Some(i) = root_idx {
356 &mut tree.roots[i]
357 } else {
358 tree.roots.push(CallTreeNode::new(bottom_name));
359 tree.roots.last_mut().unwrap()
360 };
361
362 root.call_count += 1;
363 root.total_time += 1;
364
365 let mut current = root as *mut CallTreeNode;
367 for (i, frame_name) in sample.frames.iter().enumerate().skip(1) {
368 let node = unsafe { &mut *current };
371 let child = node.get_or_create_child(frame_name);
372 child.call_count += 1;
373 child.total_time += 1;
374
375 if i == sample.frames.len() - 1 {
377 child.self_time += 1;
378 }
379
380 current = child as *mut CallTreeNode;
381 }
382
383 if sample.frames.len() == 1 {
385 root.self_time += 1;
386 }
387 }
388
389 tree
390 }
391
392 pub(crate) fn flatten(&self) -> Vec<FlatCallEntry> {
394 let mut result = Vec::new();
395 for root in &self.roots {
396 Self::flatten_node(root, 0, &mut result);
397 }
398 result
399 }
400
401 fn flatten_node(node: &CallTreeNode, depth: u32, result: &mut Vec<FlatCallEntry>) {
402 result.push(FlatCallEntry {
403 function_name: node.function_name.clone(),
404 self_time: node.self_time,
405 total_time: node.total_time,
406 call_count: node.call_count,
407 depth,
408 });
409 for child in &node.children {
410 Self::flatten_node(child, depth + 1, result);
411 }
412 }
413
414 pub(crate) fn hottest_paths(&self, top_n: usize) -> Vec<FlatCallEntry> {
416 let mut sorted = self.flatten();
417 sorted.sort_by(|a, b| b.total_time.cmp(&a.total_time));
419 sorted.truncate(top_n);
420 sorted
421 }
422}
423
424pub(crate) struct ProfilerExport;
426
427impl ProfilerExport {
428 pub(crate) fn export_flamegraph_svg(session: &ProfilerSession) -> String {
433 let mut out = String::new();
434 out.push_str("<!-- VeridianOS Profiler Flame Graph -->\n");
436 out.push_str("<!-- Format: stack;path sample_count -->\n");
437
438 for sample in &session.samples {
439 if sample.frames.is_empty() {
440 continue;
441 }
442 for (i, frame) in sample.frames.iter().enumerate() {
444 if i > 0 {
445 out.push(';');
446 }
447 out.push_str(frame);
448 }
449 out.push_str(" 1\n");
450 }
451 out
452 }
453
454 pub(crate) fn export_summary(session: &ProfilerSession) -> String {
456 let mut out = String::from("=== Profiler Summary ===\n");
457 out.push_str("Session: ");
458 out.push_str(&session.name);
459 out.push('\n');
460 out.push_str("Samples: ");
461 push_u64_str(&mut out, session.sample_count() as u64);
462 out.push('\n');
463 out.push_str("Duration: ");
464 push_u64_str(&mut out, session.duration());
465 out.push_str(" ticks\n");
466 out.push_str("Avg CPU: ");
467 push_u64_str(&mut out, session.avg_cpu_usage() as u64);
468 out.push_str("%\n");
469 out.push_str("Peak Mem: ");
470 push_u64_str(&mut out, session.peak_memory());
471 out.push_str(" bytes\n");
472
473 let tree = CallTree::build_from_stacks(&session.samples);
475 let hot = tree.hottest_paths(5);
476 if !hot.is_empty() {
477 out.push_str("\nTop functions by total time:\n");
478 for entry in &hot {
479 out.push_str(" ");
480 out.push_str(&entry.function_name);
481 out.push_str(" (total=");
482 push_u64_str(&mut out, entry.total_time);
483 out.push_str(", self=");
484 push_u64_str(&mut out, entry.self_time);
485 out.push_str(")\n");
486 }
487 }
488
489 out
490 }
491}
492
493impl ProfilerGui {
494 pub(crate) fn render_call_tree(&self, tree: &CallTree) -> String {
496 let mut out = String::from("Call Tree View\n");
497 out.push_str("==============\n");
498 let total = tree.total_samples.max(1);
499 let flat = tree.flatten();
500 for entry in &flat {
501 for _ in 0..entry.depth {
503 out.push_str(" ");
504 }
505 out.push_str(&entry.function_name);
506 out.push_str(" [");
507 let pct = (entry.total_time * 100) / total;
509 push_u64_str(&mut out, pct);
510 out.push_str("%, self=");
511 let self_pct = (entry.self_time * 100) / total;
512 push_u64_str(&mut out, self_pct);
513 out.push_str("%, calls=");
514 push_u64_str(&mut out, entry.call_count);
515 out.push_str("]\n");
516 }
517 out
518 }
519
520 pub(crate) fn render_hotspots(&self, tree: &CallTree, top_n: usize) -> String {
522 let mut out = String::from("Hotspot Analysis\n");
523 out.push_str("================\n");
524 let total = tree.total_samples.max(1);
525
526 let flat = tree.flatten();
528 let mut by_self: Vec<&FlatCallEntry> = flat.iter().filter(|e| e.self_time > 0).collect();
529 by_self.sort_by(|a, b| b.self_time.cmp(&a.self_time));
530 by_self.truncate(top_n);
531
532 for (i, entry) in by_self.iter().enumerate() {
533 push_u64_str(&mut out, (i + 1) as u64);
534 out.push_str(". ");
535 out.push_str(&entry.function_name);
536 out.push_str(" self=");
537 let pct = (entry.self_time * 100) / total;
538 push_u64_str(&mut out, pct);
539 out.push_str("% (");
540 push_u64_str(&mut out, entry.self_time);
541 out.push_str(" samples)\n");
542 }
543 out
544 }
545}
546
547fn push_u64_str(out: &mut String, mut val: u64) {
549 if val == 0 {
550 out.push('0');
551 return;
552 }
553 let start = out.len();
554 while val > 0 {
555 let digit = (val % 10) as u8 + b'0';
556 out.push(digit as char);
557 val /= 10;
558 }
559 let bytes = unsafe { out.as_bytes_mut() };
560 bytes[start..].reverse();
561}
562
563#[cfg(test)]
568mod tests {
569 #[allow(unused_imports)]
570 use alloc::vec;
571
572 use super::*;
573
574 #[test]
575 fn test_flame_frame_new() {
576 let frame = FlameFrame::new("main");
577 assert_eq!(frame.name, "main");
578 assert_eq!(frame.samples, 0);
579 assert!(frame.children.is_empty());
580 }
581
582 #[test]
583 fn test_flame_frame_total_samples() {
584 let mut root = FlameFrame::new("root");
585 root.samples = 5;
586 let mut child = FlameFrame::new("child");
587 child.samples = 3;
588 root.add_child(child);
589 assert_eq!(root.total_samples(), 8);
590 }
591
592 #[test]
593 fn test_profiler_session() {
594 let mut session = ProfilerSession::new("test");
595 session.add_sample(StackSample {
596 timestamp: 100,
597 frames: vec!["main".to_string(), "foo".to_string()],
598 cpu: 0,
599 });
600 session.add_sample(StackSample {
601 timestamp: 200,
602 frames: vec!["main".to_string(), "bar".to_string()],
603 cpu: 0,
604 });
605
606 assert_eq!(session.sample_count(), 2);
607 assert_eq!(session.duration(), 100);
608 }
609
610 #[test]
611 fn test_build_flame_graph() {
612 let mut session = ProfilerSession::new("test");
613 session.add_sample(StackSample {
614 timestamp: 0,
615 frames: vec!["main".to_string(), "foo".to_string()],
616 cpu: 0,
617 });
618 session.add_sample(StackSample {
619 timestamp: 1,
620 frames: vec!["main".to_string(), "foo".to_string()],
621 cpu: 0,
622 });
623 session.add_sample(StackSample {
624 timestamp: 2,
625 frames: vec!["main".to_string(), "bar".to_string()],
626 cpu: 0,
627 });
628
629 let flame = session.build_flame_graph();
630 assert_eq!(flame.children.len(), 1); let main_frame = &flame.children[0];
632 assert_eq!(main_frame.name, "main");
633 assert_eq!(main_frame.children.len(), 2); }
635
636 #[test]
637 fn test_cpu_timeline() {
638 let mut session = ProfilerSession::new("test");
639 session.add_cpu_point(CpuDataPoint {
640 timestamp: 0,
641 usage_percent: 50,
642 cpu_id: 0,
643 });
644 session.add_cpu_point(CpuDataPoint {
645 timestamp: 1,
646 usage_percent: 80,
647 cpu_id: 0,
648 });
649 assert_eq!(session.avg_cpu_usage(), 65);
650 }
651
652 #[test]
653 fn test_mem_timeline() {
654 let mut session = ProfilerSession::new("test");
655 session.add_mem_point(MemDataPoint {
656 timestamp: 0,
657 used_bytes: 1000,
658 total_bytes: 4096,
659 });
660 session.add_mem_point(MemDataPoint {
661 timestamp: 1,
662 used_bytes: 3000,
663 total_bytes: 4096,
664 });
665 assert_eq!(session.peak_memory(), 3000);
666 }
667
668 #[test]
669 fn test_profiler_gui_new() {
670 let gui = ProfilerGui::new(800, 600);
671 assert_eq!(gui.view, ProfilerView::FlameGraph);
672 }
673
674 #[test]
675 fn test_profiler_gui_switch_view() {
676 let mut gui = ProfilerGui::new(800, 600);
677 gui.switch_view(ProfilerView::CpuTimeline);
678 assert_eq!(gui.view, ProfilerView::CpuTimeline);
679 }
680
681 #[test]
682 fn test_profiler_view_eq() {
683 assert_eq!(ProfilerView::FlameGraph, ProfilerView::FlameGraph);
684 assert_ne!(ProfilerView::FlameGraph, ProfilerView::CallTree);
685 }
686
687 #[test]
688 fn test_empty_session_stats() {
689 let session = ProfilerSession::new("empty");
690 assert_eq!(session.avg_cpu_usage(), 0);
691 assert_eq!(session.peak_memory(), 0);
692 assert_eq!(session.duration(), 0);
693 }
694
695 #[test]
696 fn test_call_tree_build() {
697 let samples = vec![
698 StackSample {
699 timestamp: 0,
700 frames: vec!["main".to_string(), "foo".to_string(), "bar".to_string()],
701 cpu: 0,
702 },
703 StackSample {
704 timestamp: 1,
705 frames: vec!["main".to_string(), "foo".to_string()],
706 cpu: 0,
707 },
708 ];
709 let tree = CallTree::build_from_stacks(&samples);
710 assert_eq!(tree.total_samples, 2);
711 assert_eq!(tree.roots.len(), 1);
712 assert_eq!(tree.roots[0].function_name, "main");
713 assert_eq!(tree.roots[0].call_count, 2);
714 }
715
716 #[test]
717 fn test_call_tree_flatten() {
718 let samples = vec![StackSample {
719 timestamp: 0,
720 frames: vec!["main".to_string(), "compute".to_string()],
721 cpu: 0,
722 }];
723 let tree = CallTree::build_from_stacks(&samples);
724 let flat = tree.flatten();
725 assert_eq!(flat.len(), 2);
726 assert_eq!(flat[0].depth, 0);
727 assert_eq!(flat[1].depth, 1);
728 assert_eq!(flat[1].function_name, "compute");
729 }
730
731 #[test]
732 fn test_call_tree_hottest_paths() {
733 let samples = vec![
734 StackSample {
735 timestamp: 0,
736 frames: vec!["main".to_string(), "hot".to_string()],
737 cpu: 0,
738 },
739 StackSample {
740 timestamp: 1,
741 frames: vec!["main".to_string(), "hot".to_string()],
742 cpu: 0,
743 },
744 StackSample {
745 timestamp: 2,
746 frames: vec!["main".to_string(), "cold".to_string()],
747 cpu: 0,
748 },
749 ];
750 let tree = CallTree::build_from_stacks(&samples);
751 let hot = tree.hottest_paths(2);
752 assert!(!hot.is_empty());
753 assert_eq!(hot[0].function_name, "main");
755 }
756
757 #[test]
758 fn test_export_flamegraph_svg() {
759 let mut session = ProfilerSession::new("test");
760 session.add_sample(StackSample {
761 timestamp: 0,
762 frames: vec!["main".to_string(), "foo".to_string()],
763 cpu: 0,
764 });
765 let svg = ProfilerExport::export_flamegraph_svg(&session);
766 assert!(svg.contains("main;foo 1"));
767 assert!(svg.contains("VeridianOS"));
768 }
769
770 #[test]
771 fn test_export_summary() {
772 let mut session = ProfilerSession::new("bench");
773 session.add_sample(StackSample {
774 timestamp: 100,
775 frames: vec!["main".to_string()],
776 cpu: 0,
777 });
778 session.add_sample(StackSample {
779 timestamp: 200,
780 frames: vec!["main".to_string()],
781 cpu: 0,
782 });
783 let summary = ProfilerExport::export_summary(&session);
784 assert!(summary.contains("Session: bench"));
785 assert!(summary.contains("Samples: 2"));
786 assert!(summary.contains("Duration: 100"));
787 }
788
789 #[test]
790 fn test_render_call_tree_view() {
791 let samples = vec![
792 StackSample {
793 timestamp: 0,
794 frames: vec!["main".to_string(), "work".to_string()],
795 cpu: 0,
796 },
797 StackSample {
798 timestamp: 1,
799 frames: vec!["main".to_string(), "work".to_string()],
800 cpu: 0,
801 },
802 ];
803 let tree = CallTree::build_from_stacks(&samples);
804 let gui = ProfilerGui::new(800, 600);
805 let text = gui.render_call_tree(&tree);
806 assert!(text.contains("main"));
807 assert!(text.contains("work"));
808 assert!(text.contains("100%")); }
810}