1use std::collections::HashMap;
28use std::time::Instant;
29
30#[derive(Debug, Clone, Default, PartialEq, Eq)]
34pub struct OpStats {
35 pub op_name: String,
37 pub input_rows: u64,
39 pub output_rows: u64,
41 pub duration_us: u64,
43 pub memory_bytes: u64,
45}
46
47impl OpStats {
48 pub fn new(
50 op_name: impl Into<String>,
51 input_rows: u64,
52 output_rows: u64,
53 duration_us: u64,
54 memory_bytes: u64,
55 ) -> Self {
56 Self {
57 op_name: op_name.into(),
58 input_rows,
59 output_rows,
60 duration_us,
61 memory_bytes,
62 }
63 }
64
65 pub fn timed(
67 op_name: impl Into<String>,
68 input_rows: u64,
69 output_rows: u64,
70 duration_us: u64,
71 ) -> Self {
72 Self {
73 op_name: op_name.into(),
74 input_rows,
75 output_rows,
76 duration_us,
77 memory_bytes: 0,
78 }
79 }
80}
81
82#[derive(Debug, Clone, Default)]
84pub struct StratumStats {
85 pub stratum_id: usize,
87 pub num_rules: usize,
89 pub is_recursive: bool,
91 pub iterations: usize,
93 pub duration_us: u64,
95 pub ops: Vec<OpStats>,
97}
98
99impl StratumStats {
100 pub fn new(stratum_id: usize, num_rules: usize, is_recursive: bool) -> Self {
102 Self {
103 stratum_id,
104 num_rules,
105 is_recursive,
106 iterations: if is_recursive { 0 } else { 1 },
107 duration_us: 0,
108 ops: Vec::new(),
109 }
110 }
111
112 pub fn op_summary(&self) -> HashMap<String, (usize, u64)> {
114 let mut summary: HashMap<String, (usize, u64)> = HashMap::new();
115 for op in &self.ops {
116 let entry = summary.entry(op.op_name.clone()).or_insert((0, 0));
117 entry.0 += 1;
118 entry.1 += op.duration_us;
119 }
120 summary
121 }
122}
123
124#[derive(Debug, Clone, Default)]
126pub struct ExecutionStats {
127 pub total_duration_us: u64,
129 pub strata: Vec<StratumStats>,
131 pub peak_memory_bytes: u64,
133 pub memory_budget_bytes: u64,
135 pub total_output_rows: u64,
137 pub wcoj_triangle_dispatch_count: u64,
141 pub wcoj_4cycle_dispatch_count: u64,
143 pub wcoj_groupby_fusion_dispatch_count: u64,
146 pub free_join_dispatch_count: u64,
148 pub factorized_delta_dispatch_count: u64,
151 pub wcoj_error_decline_count: u64,
155}
156
157impl ExecutionStats {
158 pub fn format_human(&self) -> String {
160 let total_secs = self.total_duration_us as f64 / 1_000_000.0;
161 let mut output = String::new();
162
163 output.push_str(&format!("Execution completed in {:.2}s\n\n", total_secs));
164
165 for stratum in &self.strata {
166 let stratum_secs = stratum.duration_us as f64 / 1_000_000.0;
167 let recursive_info = if stratum.is_recursive {
168 format!(", recursive, {} iterations", stratum.iterations)
169 } else {
170 String::new()
171 };
172
173 output.push_str(&format!(
174 "Stratum {}: {:.2}s ({} rules{})\n",
175 stratum.stratum_id, stratum_secs, stratum.num_rules, recursive_info
176 ));
177
178 let op_summary = stratum.op_summary();
180 let mut ops: Vec<_> = op_summary.into_iter().collect();
181 ops.sort_by_key(|op| std::cmp::Reverse(op.1 .1)); for (op_name, (count, duration_us)) in ops {
184 let op_secs = duration_us as f64 / 1_000_000.0;
185 output.push_str(&format!(
186 " - {}: {:.2}s ({} calls)\n",
187 op_name, op_secs, count
188 ));
189 }
190 }
191
192 let peak_mb = self.peak_memory_bytes as f64 / (1024.0 * 1024.0);
193 let budget_mb = self.memory_budget_bytes as f64 / (1024.0 * 1024.0);
194 output.push_str(&format!(
195 "\nMemory: {:.0} MB peak / {:.0} MB budget\n",
196 peak_mb, budget_mb
197 ));
198 output.push_str(&format!(
199 "Output: {} rows\n",
200 format_rows(self.total_output_rows)
201 ));
202 output.push_str(&format!(
203 "WCOJ dispatch: triangle {}, 4-cycle {}, groupby-fusion {}, free-join {}, factorized-delta {}, declines {}\n",
204 self.wcoj_triangle_dispatch_count,
205 self.wcoj_4cycle_dispatch_count,
206 self.wcoj_groupby_fusion_dispatch_count,
207 self.free_join_dispatch_count,
208 self.factorized_delta_dispatch_count,
209 self.wcoj_error_decline_count,
210 ));
211
212 output
213 }
214
215 pub fn format_json(&self) -> String {
217 let total_ms = self.total_duration_us / 1000;
218 let strata_json: Vec<String> = self.strata.iter().map(|s| {
219 let ops_json: Vec<String> = s.op_summary().iter().map(|(name, (count, duration))| {
220 format!(
221 r#"{{"op":"{}","calls":{},"duration_ms":{}}}"#,
222 name, count, duration / 1000
223 )
224 }).collect();
225 format!(
226 r#"{{"stratum":{},"rules":{},"recursive":{},"iterations":{},"duration_ms":{},"ops":[{}]}}"#,
227 s.stratum_id, s.num_rules, s.is_recursive, s.iterations, s.duration_us / 1000,
228 ops_json.join(",")
229 )
230 }).collect();
231
232 format!(
233 r#"{{"total_ms":{},"strata":[{}],"peak_memory_mb":{},"budget_memory_mb":{},"output_rows":{},"wcoj":{{"triangle_dispatch":{},"four_cycle_dispatch":{},"groupby_fusion_dispatch":{},"free_join_dispatch":{},"factorized_delta_dispatch":{},"error_decline":{}}}}}"#,
234 total_ms,
235 strata_json.join(","),
236 self.peak_memory_bytes / (1024 * 1024),
237 self.memory_budget_bytes / (1024 * 1024),
238 self.total_output_rows,
239 self.wcoj_triangle_dispatch_count,
240 self.wcoj_4cycle_dispatch_count,
241 self.wcoj_groupby_fusion_dispatch_count,
242 self.free_join_dispatch_count,
243 self.factorized_delta_dispatch_count,
244 self.wcoj_error_decline_count,
245 )
246 }
247}
248
249fn format_rows(rows: u64) -> String {
251 let s = rows.to_string();
252 let mut result = String::new();
253 for (i, c) in s.chars().rev().enumerate() {
254 if i > 0 && i % 3 == 0 {
255 result.insert(0, ',');
256 }
257 result.insert(0, c);
258 }
259 result
260}
261
262pub struct Profiler {
292 enabled: bool,
294 stats: Vec<OpStats>,
296 strata: Vec<StratumStats>,
298 current_stratum: Option<usize>,
300 stratum_start: Option<Instant>,
302 peak_memory_bytes: u64,
304 memory_budget_bytes: u64,
306}
307
308impl Profiler {
309 pub fn new(enabled: bool) -> Self {
314 Self {
315 enabled,
316 stats: Vec::new(),
317 strata: Vec::new(),
318 current_stratum: None,
319 stratum_start: None,
320 peak_memory_bytes: 0,
321 memory_budget_bytes: 0,
322 }
323 }
324
325 pub fn set_memory_budget(&mut self, budget_bytes: u64) {
327 self.memory_budget_bytes = budget_bytes;
328 }
329
330 pub fn begin_stratum(&mut self, stratum_id: usize, num_rules: usize, is_recursive: bool) {
337 if !self.enabled {
338 return;
339 }
340 self.current_stratum = Some(stratum_id);
341 self.stratum_start = Some(Instant::now());
342 self.strata
343 .push(StratumStats::new(stratum_id, num_rules, is_recursive));
344 }
345
346 pub fn end_stratum(&mut self) {
348 if !self.enabled {
349 return;
350 }
351 if let (Some(start), Some(_idx)) = (self.stratum_start.take(), self.current_stratum.take())
352 {
353 let duration = start.elapsed();
354 if let Some(stratum) = self.strata.last_mut() {
355 stratum.duration_us = duration.as_micros() as u64;
356 }
357 }
358 }
359
360 pub fn record_iterations(&mut self, iterations: usize) {
362 if !self.enabled {
363 return;
364 }
365 if let Some(stratum) = self.strata.last_mut() {
366 stratum.iterations = iterations;
367 }
368 }
369
370 pub fn record_op(
381 &mut self,
382 op_name: impl Into<String>,
383 input_rows: u64,
384 output_rows: u64,
385 start: Instant,
386 memory_bytes: u64,
387 ) {
388 if !self.enabled {
389 return;
390 }
391 let duration = start.elapsed();
392 self.record(OpStats {
393 op_name: op_name.into(),
394 input_rows,
395 output_rows,
396 duration_us: duration.as_micros() as u64,
397 memory_bytes,
398 });
399 }
400
401 #[inline]
406 pub fn start_op(&self) -> Option<Instant> {
407 if self.enabled {
408 Some(Instant::now())
409 } else {
410 None
411 }
412 }
413
414 pub fn record_peak_memory(&mut self, memory_bytes: u64) {
416 if !self.enabled {
417 return;
418 }
419 if memory_bytes > self.peak_memory_bytes {
420 self.peak_memory_bytes = memory_bytes;
421 }
422 }
423
424 pub fn execution_stats(&self, total_output_rows: u64) -> ExecutionStats {
426 ExecutionStats {
427 total_duration_us: self.strata.iter().map(|s| s.duration_us).sum(),
428 strata: self.strata.clone(),
429 peak_memory_bytes: self.peak_memory_bytes,
430 memory_budget_bytes: self.memory_budget_bytes,
431 total_output_rows,
432 ..Default::default()
435 }
436 }
437
438 pub fn is_enabled(&self) -> bool {
440 self.enabled
441 }
442
443 pub fn record(&mut self, stats: OpStats) {
451 if self.enabled {
452 if self.current_stratum.is_some() {
454 if let Some(stratum) = self.strata.last_mut() {
455 stratum.ops.push(stats.clone());
456 }
457 }
458 self.stats.push(stats);
459 }
460 }
461
462 pub fn stats(&self) -> &[OpStats] {
466 &self.stats
467 }
468
469 pub fn clear(&mut self) {
473 self.stats.clear();
474 self.strata.clear();
475 self.current_stratum = None;
476 self.stratum_start = None;
477 self.peak_memory_bytes = 0;
478 }
479
480 pub fn total_duration_us(&self) -> u64 {
482 self.stats.iter().map(|s| s.duration_us).sum()
483 }
484
485 pub fn total_memory_bytes(&self) -> u64 {
491 self.stats.iter().map(|s| s.memory_bytes).sum()
492 }
493
494 pub fn peak_memory_bytes(&self) -> u64 {
499 self.stats.iter().map(|s| s.memory_bytes).max().unwrap_or(0)
500 }
501
502 pub fn operation_count(&self) -> usize {
504 self.stats.len()
505 }
506
507 pub fn summary(&self) -> String {
515 if self.stats.is_empty() {
516 return "Profiler: No operations recorded".to_string();
517 }
518
519 let total_duration_us = self.total_duration_us();
520 let total_duration_ms = total_duration_us as f64 / 1000.0;
521 let total_memory = self.total_memory_bytes();
522 let peak_memory = self.peak_memory_bytes();
523
524 let mut output = String::new();
525 output.push_str("=== Execution Profile ===\n");
526 output.push_str(&format!("Operations: {}\n", self.stats.len()));
527 output.push_str(&format!(
528 "Total duration: {:.3} ms ({} us)\n",
529 total_duration_ms, total_duration_us
530 ));
531 output.push_str(&format!("Total memory: {} bytes\n", total_memory));
532 output.push_str(&format!("Peak memory: {} bytes\n", peak_memory));
533 output.push_str("\n--- Operations ---\n");
534
535 for (i, stat) in self.stats.iter().enumerate() {
536 let duration_ms = stat.duration_us as f64 / 1000.0;
537 let percentage = if total_duration_us > 0 {
538 (stat.duration_us as f64 / total_duration_us as f64) * 100.0
539 } else {
540 0.0
541 };
542
543 output.push_str(&format!(
544 "{:3}. {:<20} | {:>10} -> {:>10} rows | {:>8.3} ms ({:>5.1}%) | {:>10} bytes\n",
545 i + 1,
546 truncate_name(&stat.op_name, 20),
547 stat.input_rows,
548 stat.output_rows,
549 duration_ms,
550 percentage,
551 stat.memory_bytes
552 ));
553 }
554
555 output
556 }
557
558 pub fn set_enabled(&mut self, enabled: bool) {
562 self.enabled = enabled;
563 }
564}
565
566impl Default for Profiler {
567 fn default() -> Self {
569 Self {
570 enabled: false,
571 stats: Vec::new(),
572 strata: Vec::new(),
573 current_stratum: None,
574 stratum_start: None,
575 peak_memory_bytes: 0,
576 memory_budget_bytes: 0,
577 }
578 }
579}
580
581pub struct MeasureGuard<'a> {
585 profiler: &'a mut Profiler,
586 op_name: String,
587 input_rows: u64,
588 start: Instant,
589 output_rows: Option<u64>,
590}
591
592impl<'a> MeasureGuard<'a> {
593 pub fn new(profiler: &'a mut Profiler, op_name: impl Into<String>, input_rows: u64) -> Self {
595 Self {
596 profiler,
597 op_name: op_name.into(),
598 input_rows,
599 start: Instant::now(),
600 output_rows: None,
601 }
602 }
603
604 pub fn finish(mut self, output_rows: u64) {
606 self.output_rows = Some(output_rows);
607 }
609}
610
611impl<'a> Drop for MeasureGuard<'a> {
612 fn drop(&mut self) {
613 if self.profiler.is_enabled() {
614 let duration = self.start.elapsed();
615 self.profiler.record(OpStats {
616 op_name: std::mem::take(&mut self.op_name),
617 input_rows: self.input_rows,
618 output_rows: self.output_rows.unwrap_or(0),
619 duration_us: duration.as_micros() as u64,
620 memory_bytes: 0,
621 });
622 }
623 }
624}
625
626fn truncate_name(name: &str, max_len: usize) -> String {
628 if name.len() <= max_len {
629 name.to_string()
630 } else {
631 format!("{}...", &name[..max_len.saturating_sub(3)])
632 }
633}
634
635#[cfg(test)]
636mod tests {
637 use super::*;
638
639 #[test]
642 fn test_opstats_new() {
643 let stats = OpStats::new("hash_join", 1000, 500, 1500, 4096);
644
645 assert_eq!(stats.op_name, "hash_join");
646 assert_eq!(stats.input_rows, 1000);
647 assert_eq!(stats.output_rows, 500);
648 assert_eq!(stats.duration_us, 1500);
649 assert_eq!(stats.memory_bytes, 4096);
650 }
651
652 #[test]
653 fn test_opstats_timed() {
654 let stats = OpStats::timed("filter", 1000, 800, 200);
655
656 assert_eq!(stats.op_name, "filter");
657 assert_eq!(stats.input_rows, 1000);
658 assert_eq!(stats.output_rows, 800);
659 assert_eq!(stats.duration_us, 200);
660 assert_eq!(stats.memory_bytes, 0);
661 }
662
663 #[test]
664 fn test_opstats_default() {
665 let stats = OpStats::default();
666
667 assert_eq!(stats.op_name, "");
668 assert_eq!(stats.input_rows, 0);
669 assert_eq!(stats.output_rows, 0);
670 assert_eq!(stats.duration_us, 0);
671 assert_eq!(stats.memory_bytes, 0);
672 }
673
674 #[test]
675 fn test_opstats_clone() {
676 let stats = OpStats::new("scan", 0, 1000, 100, 2048);
677 let cloned = stats.clone();
678
679 assert_eq!(stats, cloned);
680 }
681
682 #[test]
683 fn test_opstats_debug() {
684 let stats = OpStats::new("test_op", 100, 50, 10, 1024);
685 let debug_str = format!("{:?}", stats);
686
687 assert!(debug_str.contains("test_op"));
688 assert!(debug_str.contains("100"));
689 assert!(debug_str.contains("50"));
690 }
691
692 #[test]
695 fn test_profiler_new_enabled() {
696 let profiler = Profiler::new(true);
697
698 assert!(profiler.is_enabled());
699 assert!(profiler.stats().is_empty());
700 }
701
702 #[test]
703 fn test_profiler_new_disabled() {
704 let profiler = Profiler::new(false);
705
706 assert!(!profiler.is_enabled());
707 assert!(profiler.stats().is_empty());
708 }
709
710 #[test]
711 fn test_profiler_default() {
712 let profiler = Profiler::default();
713
714 assert!(!profiler.is_enabled());
715 assert!(profiler.stats().is_empty());
716 }
717
718 #[test]
721 fn test_profiler_record_when_enabled() {
722 let mut profiler = Profiler::new(true);
723
724 profiler.record(OpStats::new("op1", 100, 50, 10, 1024));
725 profiler.record(OpStats::new("op2", 50, 25, 5, 512));
726
727 assert_eq!(profiler.stats().len(), 2);
728 assert_eq!(profiler.stats()[0].op_name, "op1");
729 assert_eq!(profiler.stats()[1].op_name, "op2");
730 }
731
732 #[test]
733 fn test_profiler_record_when_disabled() {
734 let mut profiler = Profiler::new(false);
735
736 profiler.record(OpStats::new("op1", 100, 50, 10, 1024));
737 profiler.record(OpStats::new("op2", 50, 25, 5, 512));
738
739 assert!(profiler.stats().is_empty());
740 }
741
742 #[test]
743 fn test_profiler_set_enabled() {
744 let mut profiler = Profiler::new(false);
745
746 profiler.record(OpStats::new("op1", 100, 50, 10, 1024));
748 assert!(profiler.stats().is_empty());
749
750 profiler.set_enabled(true);
752 assert!(profiler.is_enabled());
753 profiler.record(OpStats::new("op2", 50, 25, 5, 512));
754 assert_eq!(profiler.stats().len(), 1);
755 assert_eq!(profiler.stats()[0].op_name, "op2");
756
757 profiler.set_enabled(false);
759 assert!(!profiler.is_enabled());
760 profiler.record(OpStats::new("op3", 25, 10, 2, 256));
761 assert_eq!(profiler.stats().len(), 1); }
763
764 #[test]
767 fn test_profiler_clear() {
768 let mut profiler = Profiler::new(true);
769
770 profiler.record(OpStats::new("op1", 100, 50, 10, 1024));
771 profiler.record(OpStats::new("op2", 50, 25, 5, 512));
772 assert_eq!(profiler.stats().len(), 2);
773
774 profiler.clear();
775
776 assert!(profiler.stats().is_empty());
777 assert!(profiler.is_enabled()); }
779
780 #[test]
781 fn test_profiler_clear_preserves_enabled_state() {
782 let mut profiler = Profiler::new(true);
783 profiler.record(OpStats::new("op1", 100, 50, 10, 1024));
784 profiler.clear();
785
786 assert!(profiler.is_enabled());
787
788 profiler.set_enabled(false);
789 profiler.clear();
790
791 assert!(!profiler.is_enabled());
792 }
793
794 #[test]
797 fn test_total_duration_us() {
798 let mut profiler = Profiler::new(true);
799
800 profiler.record(OpStats::new("op1", 100, 50, 100, 0));
801 profiler.record(OpStats::new("op2", 50, 25, 200, 0));
802 profiler.record(OpStats::new("op3", 25, 10, 150, 0));
803
804 assert_eq!(profiler.total_duration_us(), 450);
805 }
806
807 #[test]
808 fn test_total_duration_us_empty() {
809 let profiler = Profiler::new(true);
810
811 assert_eq!(profiler.total_duration_us(), 0);
812 }
813
814 #[test]
815 fn test_total_memory_bytes() {
816 let mut profiler = Profiler::new(true);
817
818 profiler.record(OpStats::new("op1", 100, 50, 10, 1024));
819 profiler.record(OpStats::new("op2", 50, 25, 5, 2048));
820 profiler.record(OpStats::new("op3", 25, 10, 2, 512));
821
822 assert_eq!(profiler.total_memory_bytes(), 3584);
823 }
824
825 #[test]
826 fn test_total_memory_bytes_empty() {
827 let profiler = Profiler::new(true);
828
829 assert_eq!(profiler.total_memory_bytes(), 0);
830 }
831
832 #[test]
833 fn test_peak_memory_bytes() {
834 let mut profiler = Profiler::new(true);
835
836 profiler.record(OpStats::new("op1", 100, 50, 10, 1024));
837 profiler.record(OpStats::new("op2", 50, 25, 5, 4096));
838 profiler.record(OpStats::new("op3", 25, 10, 2, 2048));
839
840 assert_eq!(profiler.peak_memory_bytes(), 4096);
841 }
842
843 #[test]
844 fn test_peak_memory_bytes_empty() {
845 let profiler = Profiler::new(true);
846
847 assert_eq!(profiler.peak_memory_bytes(), 0);
848 }
849
850 #[test]
851 fn test_operation_count() {
852 let mut profiler = Profiler::new(true);
853
854 assert_eq!(profiler.operation_count(), 0);
855
856 profiler.record(OpStats::new("op1", 100, 50, 10, 1024));
857 assert_eq!(profiler.operation_count(), 1);
858
859 profiler.record(OpStats::new("op2", 50, 25, 5, 512));
860 assert_eq!(profiler.operation_count(), 2);
861
862 profiler.clear();
863 assert_eq!(profiler.operation_count(), 0);
864 }
865
866 #[test]
869 fn test_summary_empty() {
870 let profiler = Profiler::new(true);
871 let summary = profiler.summary();
872
873 assert!(summary.contains("No operations recorded"));
874 }
875
876 #[test]
877 fn test_summary_with_operations() {
878 let mut profiler = Profiler::new(true);
879
880 profiler.record(OpStats::new("scan", 0, 1000, 100, 4096));
881 profiler.record(OpStats::new("filter", 1000, 500, 200, 2048));
882 profiler.record(OpStats::new("hash_join", 500, 250, 500, 8192));
883
884 let summary = profiler.summary();
885
886 assert!(summary.contains("=== Execution Profile ==="));
888 assert!(summary.contains("Operations: 3"));
889
890 assert!(summary.contains("Total duration:"));
892 assert!(summary.contains("800 us"));
893
894 assert!(summary.contains("Total memory: 14336 bytes"));
896 assert!(summary.contains("Peak memory: 8192 bytes"));
897
898 assert!(summary.contains("scan"));
900 assert!(summary.contains("filter"));
901 assert!(summary.contains("hash_join"));
902
903 assert!(summary.contains("1000"));
905 assert!(summary.contains("500"));
906 assert!(summary.contains("250"));
907 }
908
909 #[test]
910 fn test_summary_percentages() {
911 let mut profiler = Profiler::new(true);
912
913 profiler.record(OpStats::new("fast_op", 100, 50, 250, 0));
915 profiler.record(OpStats::new("slow_op", 100, 50, 750, 0));
916
917 let summary = profiler.summary();
918
919 assert!(summary.contains("25.0%") || summary.contains("25."));
921 assert!(summary.contains("75.0%") || summary.contains("75."));
922 }
923
924 #[test]
927 fn test_truncate_name_short() {
928 let result = truncate_name("short", 20);
929 assert_eq!(result, "short");
930 }
931
932 #[test]
933 fn test_truncate_name_exact() {
934 let name = "exactly_twenty_chars"; let result = truncate_name(name, 20);
936 assert_eq!(result, name);
937 }
938
939 #[test]
940 fn test_truncate_name_long() {
941 let name = "this_is_a_very_long_operation_name";
942 let result = truncate_name(name, 20);
943 assert_eq!(result.len(), 20);
944 assert!(result.ends_with("..."));
945 }
946
947 #[test]
950 fn test_profiler_full_workflow() {
951 let mut profiler = Profiler::new(true);
953
954 profiler.record(OpStats::new("scan_edge", 0, 10000, 500, 40000));
956 profiler.record(OpStats::new("scan_node", 0, 1000, 100, 4000));
957 profiler.record(OpStats::new("hash_join", 11000, 5000, 2000, 100000));
958 profiler.record(OpStats::new("filter", 5000, 2000, 300, 20000));
959 profiler.record(OpStats::new("project", 2000, 2000, 50, 8000));
960 profiler.record(OpStats::new("dedup", 2000, 1500, 400, 12000));
961
962 assert_eq!(profiler.operation_count(), 6);
964 assert_eq!(profiler.total_duration_us(), 3350);
965 assert_eq!(profiler.total_memory_bytes(), 184000);
966 assert_eq!(profiler.peak_memory_bytes(), 100000);
967
968 let summary = profiler.summary();
970 assert!(summary.contains("6"));
971 assert!(summary.contains("scan_edge"));
972 assert!(summary.contains("hash_join"));
973 assert!(summary.contains("dedup"));
974
975 profiler.clear();
977 assert_eq!(profiler.operation_count(), 0);
978 assert!(profiler.is_enabled());
979 }
980
981 #[test]
982 fn test_profiler_disabled_has_zero_overhead() {
983 let mut profiler = Profiler::new(false);
985
986 for i in 0..1000 {
987 profiler.record(OpStats::new(
988 format!("op_{}", i),
989 i as u64,
990 i as u64,
991 i as u64,
992 i as u64,
993 ));
994 }
995
996 assert_eq!(profiler.operation_count(), 0);
998 assert_eq!(profiler.total_duration_us(), 0);
999 assert_eq!(profiler.total_memory_bytes(), 0);
1000 }
1001
1002 #[test]
1003 fn test_profiler_stats_immutable_reference() {
1004 let mut profiler = Profiler::new(true);
1005
1006 profiler.record(OpStats::new("op1", 100, 50, 10, 1024));
1007
1008 let stats = profiler.stats();
1010 assert_eq!(stats.len(), 1);
1011 assert_eq!(stats[0].op_name, "op1");
1012
1013 profiler.record(OpStats::new("op2", 50, 25, 5, 512));
1015 assert_eq!(profiler.stats().len(), 2);
1016 }
1017
1018 #[test]
1019 fn test_opstats_equality() {
1020 let stats1 = OpStats::new("op", 100, 50, 10, 1024);
1021 let stats2 = OpStats::new("op", 100, 50, 10, 1024);
1022 let stats3 = OpStats::new("op", 100, 50, 10, 2048); assert_eq!(stats1, stats2);
1025 assert_ne!(stats1, stats3);
1026 }
1027}