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)]
84#[non_exhaustive]
85pub struct StratumStats {
86 pub stratum_id: usize,
88 pub num_rules: usize,
90 pub is_recursive: bool,
92 pub iterations: usize,
94 pub duration_us: u64,
96 pub ops: Vec<OpStats>,
98}
99
100impl StratumStats {
101 pub fn new(stratum_id: usize, num_rules: usize, is_recursive: bool) -> Self {
103 Self {
104 stratum_id,
105 num_rules,
106 is_recursive,
107 iterations: if is_recursive { 0 } else { 1 },
108 duration_us: 0,
109 ops: Vec::new(),
110 }
111 }
112
113 pub fn op_summary(&self) -> HashMap<String, (usize, u64)> {
115 let mut summary: HashMap<String, (usize, u64)> = HashMap::new();
116 for op in &self.ops {
117 let entry = summary.entry(op.op_name.clone()).or_insert((0, 0));
118 entry.0 += 1;
119 entry.1 += op.duration_us;
120 }
121 summary
122 }
123}
124
125#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
131pub struct WcojFallbackStats {
132 pub chain: u64,
134 pub dedicated_multiway: u64,
136 pub free_join: u64,
138 pub planned_hash: u64,
140 pub factorized_delta: u64,
142 pub groupby_fusion: u64,
144}
145
146impl WcojFallbackStats {
147 pub fn total(self) -> u64 {
149 self.chain
150 .saturating_add(self.dedicated_multiway)
151 .saturating_add(self.free_join)
152 .saturating_add(self.planned_hash)
153 .saturating_add(self.factorized_delta)
154 .saturating_add(self.groupby_fusion)
155 }
156
157 pub fn saturating_add_assign(&mut self, other: Self) {
159 self.chain = self.chain.saturating_add(other.chain);
160 self.dedicated_multiway = self
161 .dedicated_multiway
162 .saturating_add(other.dedicated_multiway);
163 self.free_join = self.free_join.saturating_add(other.free_join);
164 self.planned_hash = self.planned_hash.saturating_add(other.planned_hash);
165 self.factorized_delta = self.factorized_delta.saturating_add(other.factorized_delta);
166 self.groupby_fusion = self.groupby_fusion.saturating_add(other.groupby_fusion);
167 }
168}
169
170#[derive(Debug, Clone, Default)]
172#[non_exhaustive]
173pub struct ExecutionStats {
174 pub total_duration_us: u64,
176 pub strata: Vec<StratumStats>,
178 pub peak_memory_bytes: u64,
180 pub memory_budget_bytes: u64,
182 pub total_output_rows: u64,
184 pub wcoj_triangle_dispatch_count: u64,
188 pub wcoj_4cycle_dispatch_count: u64,
190 pub wcoj_groupby_fusion_dispatch_count: u64,
193 pub free_join_dispatch_count: u64,
195 pub factorized_delta_dispatch_count: u64,
198 pub chain_fallback_scan_equivalents: u64,
202 pub chain_fallback_filter_equivalents: u64,
206 pub wcoj_error_decline_count: u64,
210 pub wcoj_fallback: WcojFallbackStats,
212 pub resident_graph: Option<crate::resident_graph::ResidentGraphExecutionStats>,
215}
216
217impl ExecutionStats {
218 pub fn format_human(&self) -> String {
220 let total_secs = self.total_duration_us as f64 / 1_000_000.0;
221 let mut output = String::new();
222
223 output.push_str(&format!("Execution completed in {:.2}s\n\n", total_secs));
224
225 for stratum in &self.strata {
226 let stratum_secs = stratum.duration_us as f64 / 1_000_000.0;
227 let recursive_info = if stratum.is_recursive {
228 format!(", recursive, {} iterations", stratum.iterations)
229 } else {
230 String::new()
231 };
232
233 output.push_str(&format!(
234 "Stratum {}: {:.2}s ({} rules{})\n",
235 stratum.stratum_id, stratum_secs, stratum.num_rules, recursive_info
236 ));
237
238 let op_summary = stratum.op_summary();
240 let mut ops: Vec<_> = op_summary.into_iter().collect();
241 ops.sort_by_key(|op| std::cmp::Reverse(op.1 .1)); for (op_name, (count, duration_us)) in ops {
244 let op_secs = duration_us as f64 / 1_000_000.0;
245 output.push_str(&format!(
246 " - {}: {:.2}s ({} calls)\n",
247 op_name, op_secs, count
248 ));
249 }
250 }
251
252 let peak_mb = self.peak_memory_bytes as f64 / (1024.0 * 1024.0);
253 let budget_mb = self.memory_budget_bytes as f64 / (1024.0 * 1024.0);
254 output.push_str(&format!(
255 "\nMemory: {:.0} MB peak / {:.0} MB budget\n",
256 peak_mb, budget_mb
257 ));
258 output.push_str(&format!(
259 "Output: {} rows\n",
260 format_rows(self.total_output_rows)
261 ));
262 output.push_str(&format!(
263 "WCOJ dispatch: triangle {}, 4-cycle {}, groupby-fusion {}, free-join {}, factorized-delta {}, fallbacks {}, pipeline-errors {}\n",
264 self.wcoj_triangle_dispatch_count,
265 self.wcoj_4cycle_dispatch_count,
266 self.wcoj_groupby_fusion_dispatch_count,
267 self.free_join_dispatch_count,
268 self.factorized_delta_dispatch_count,
269 self.wcoj_fallback.total(),
270 self.wcoj_error_decline_count,
271 ));
272 output.push_str(&format!(
273 "chain fallback equivalents: scans {}, filters {}\n",
274 self.chain_fallback_scan_equivalents, self.chain_fallback_filter_equivalents
275 ));
276
277 output
278 }
279
280 pub fn format_json(&self) -> String {
282 let strata = self
283 .strata
284 .iter()
285 .map(|stratum| {
286 let mut operations: Vec<_> = stratum.op_summary().into_iter().collect();
287 operations.sort_unstable_by(|left, right| left.0.cmp(&right.0));
288 let operations = operations
289 .into_iter()
290 .map(|(name, (calls, duration_us))| {
291 serde_json::json!({
292 "op": name,
293 "calls": calls,
294 "duration_ms": duration_us / 1000,
295 })
296 })
297 .collect::<Vec<_>>();
298 serde_json::json!({
299 "stratum": stratum.stratum_id,
300 "rules": stratum.num_rules,
301 "recursive": stratum.is_recursive,
302 "iterations": stratum.iterations,
303 "duration_ms": stratum.duration_us / 1000,
304 "ops": operations,
305 })
306 })
307 .collect::<Vec<_>>();
308
309 let mut json = serde_json::json!({
310 "total_ms": self.total_duration_us / 1000,
311 "strata": strata,
312 "peak_memory_mb": self.peak_memory_bytes / (1024 * 1024),
313 "budget_memory_mb": self.memory_budget_bytes / (1024 * 1024),
314 "output_rows": self.total_output_rows,
315 "wcoj": {
316 "triangle_dispatch": self.wcoj_triangle_dispatch_count,
317 "four_cycle_dispatch": self.wcoj_4cycle_dispatch_count,
318 "groupby_fusion_dispatch": self.wcoj_groupby_fusion_dispatch_count,
319 "free_join_dispatch": self.free_join_dispatch_count,
320 "factorized_delta_dispatch": self.factorized_delta_dispatch_count,
321 "chain_fallback_scan_equivalents": self.chain_fallback_scan_equivalents,
322 "chain_fallback_filter_equivalents": self.chain_fallback_filter_equivalents,
323 "error_decline": self.wcoj_error_decline_count,
324 "fallback": {
325 "total": self.wcoj_fallback.total(),
326 "chain": self.wcoj_fallback.chain,
327 "dedicated_multiway": self.wcoj_fallback.dedicated_multiway,
328 "free_join": self.wcoj_fallback.free_join,
329 "planned_hash": self.wcoj_fallback.planned_hash,
330 "factorized_delta": self.wcoj_fallback.factorized_delta,
331 "groupby_fusion": self.wcoj_fallback.groupby_fusion,
332 },
333 },
334 });
335 if let Some(resident) = &self.resident_graph {
336 let selection = match resident.selection {
337 crate::resident_graph::ResidentGraphSelectionKind::ExistingGpu => "existing_gpu",
338 crate::resident_graph::ResidentGraphSelectionKind::ResidentConditionalGraph => {
339 "resident_conditional_graph"
340 }
341 };
342 let mut resident_json = serde_json::json!({
343 "selection": selection,
344 "conditional_graph_launches": resident.conditional_graph_launches,
345 "terminal_synchronizations": resident.terminal_synchronizations,
346 "host_iterations": resident.host_iterations,
347 "host_allocations": resident.host_allocations,
348 "host_status_injections": resident.host_status_injections,
349 "deterministic_d2h_violations": resident.deterministic_d2h_violations,
350 "host_dispatched_scan_ops": resident.host_dispatched_scan_ops,
351 "host_dispatched_filter_ops": resident.host_dispatched_filter_ops,
352 "device_scan_invocations": resident.device_scan_invocations,
353 "device_filter_invocations": resident.device_filter_invocations,
354 "semantic_scan_invocations": resident.semantic_scan_invocations,
355 "semantic_filter_invocations": resident.semantic_filter_invocations,
356 "staged_store_mutations": resident.staged_store_mutations,
357 "deferred_profile": {
358 "timed_scan_filter_invocations": resident.deferred_profile.timed_scan_filter_invocations,
359 "device_elapsed_ns": resident.deferred_profile.device_elapsed_ns,
360 "final_sync_misattributed_ns": resident.deferred_profile.final_sync_misattributed_ns,
361 },
362 "core_transfers": {
363 "tracked_htod_calls": resident.core_transfers.tracked_htod_calls,
364 "tracked_htod_bytes": resident.core_transfers.tracked_htod_bytes,
365 "tracked_dtoh_calls": resident.core_transfers.tracked_dtoh_calls,
366 "tracked_dtoh_bytes": resident.core_transfers.tracked_dtoh_bytes,
367 "provider_dtoh_calls": resident.core_transfers.provider_dtoh_calls,
368 "untracked_metadata_dtoh_calls": resident.core_transfers.untracked_metadata_dtoh_calls,
369 },
370 "final_observation": {
371 "dtoh_calls": resident.final_observation.dtoh_calls,
372 "dtoh_bytes": resident.final_observation.dtoh_bytes,
373 "pinned_receipts": resident.final_observation.pinned_receipts,
374 },
375 });
376 if let Some(reason) = &resident.decline {
377 resident_json["resident_graph_declined"] =
378 serde_json::Value::String(format!("{reason:?}"));
379 }
380 json["resident_graph"] = resident_json;
381 }
382 json.to_string()
383 }
384}
385
386fn format_rows(rows: u64) -> String {
388 let s = rows.to_string();
389 let mut result = String::new();
390 for (i, c) in s.chars().rev().enumerate() {
391 if i > 0 && i % 3 == 0 {
392 result.insert(0, ',');
393 }
394 result.insert(0, c);
395 }
396 result
397}
398
399pub struct Profiler {
429 enabled: bool,
431 stats: Vec<OpStats>,
433 strata: Vec<StratumStats>,
435 current_stratum: Option<usize>,
437 stratum_start: Option<Instant>,
439 peak_memory_bytes: u64,
441 memory_budget_bytes: u64,
443}
444
445impl Profiler {
446 pub fn new(enabled: bool) -> Self {
451 Self {
452 enabled,
453 stats: Vec::new(),
454 strata: Vec::new(),
455 current_stratum: None,
456 stratum_start: None,
457 peak_memory_bytes: 0,
458 memory_budget_bytes: 0,
459 }
460 }
461
462 pub fn set_memory_budget(&mut self, budget_bytes: u64) {
464 self.memory_budget_bytes = budget_bytes;
465 }
466
467 pub fn begin_stratum(&mut self, stratum_id: usize, num_rules: usize, is_recursive: bool) {
474 if !self.enabled {
475 return;
476 }
477 self.current_stratum = Some(stratum_id);
478 self.stratum_start = Some(Instant::now());
479 self.strata
480 .push(StratumStats::new(stratum_id, num_rules, is_recursive));
481 }
482
483 pub fn end_stratum(&mut self) {
485 if !self.enabled {
486 return;
487 }
488 if let (Some(start), Some(_idx)) = (self.stratum_start.take(), self.current_stratum.take())
489 {
490 let duration = start.elapsed();
491 if let Some(stratum) = self.strata.last_mut() {
492 stratum.duration_us = duration.as_micros() as u64;
493 }
494 }
495 }
496
497 pub fn record_iterations(&mut self, iterations: usize) {
499 if !self.enabled {
500 return;
501 }
502 if let Some(stratum) = self.strata.last_mut() {
503 stratum.iterations = iterations;
504 }
505 }
506
507 pub fn record_op(
518 &mut self,
519 op_name: impl Into<String>,
520 input_rows: u64,
521 output_rows: u64,
522 start: Instant,
523 memory_bytes: u64,
524 ) {
525 if !self.enabled {
526 return;
527 }
528 let duration = start.elapsed();
529 self.record(OpStats {
530 op_name: op_name.into(),
531 input_rows,
532 output_rows,
533 duration_us: duration.as_micros() as u64,
534 memory_bytes,
535 });
536 }
537
538 #[inline]
543 pub fn start_op(&self) -> Option<Instant> {
544 if self.enabled {
545 Some(Instant::now())
546 } else {
547 None
548 }
549 }
550
551 pub fn record_peak_memory(&mut self, memory_bytes: u64) {
553 if !self.enabled {
554 return;
555 }
556 if memory_bytes > self.peak_memory_bytes {
557 self.peak_memory_bytes = memory_bytes;
558 }
559 }
560
561 pub fn execution_stats(&self, total_output_rows: u64) -> ExecutionStats {
563 ExecutionStats {
564 total_duration_us: self.strata.iter().map(|s| s.duration_us).sum(),
565 strata: self.strata.clone(),
566 peak_memory_bytes: self.peak_memory_bytes,
567 memory_budget_bytes: self.memory_budget_bytes,
568 total_output_rows,
569 ..Default::default()
572 }
573 }
574
575 pub fn is_enabled(&self) -> bool {
577 self.enabled
578 }
579
580 pub fn record(&mut self, stats: OpStats) {
588 if self.enabled {
589 if self.current_stratum.is_some() {
591 if let Some(stratum) = self.strata.last_mut() {
592 stratum.ops.push(stats.clone());
593 }
594 }
595 self.stats.push(stats);
596 }
597 }
598
599 pub fn stats(&self) -> &[OpStats] {
603 &self.stats
604 }
605
606 pub fn clear(&mut self) {
610 self.stats.clear();
611 self.strata.clear();
612 self.current_stratum = None;
613 self.stratum_start = None;
614 self.peak_memory_bytes = 0;
615 }
616
617 pub fn total_duration_us(&self) -> u64 {
619 self.stats.iter().map(|s| s.duration_us).sum()
620 }
621
622 pub fn total_memory_bytes(&self) -> u64 {
628 self.stats.iter().map(|s| s.memory_bytes).sum()
629 }
630
631 pub fn peak_memory_bytes(&self) -> u64 {
636 self.stats.iter().map(|s| s.memory_bytes).max().unwrap_or(0)
637 }
638
639 pub fn operation_count(&self) -> usize {
641 self.stats.len()
642 }
643
644 pub fn summary(&self) -> String {
652 if self.stats.is_empty() {
653 return "Profiler: No operations recorded".to_string();
654 }
655
656 let total_duration_us = self.total_duration_us();
657 let total_duration_ms = total_duration_us as f64 / 1000.0;
658 let total_memory = self.total_memory_bytes();
659 let peak_memory = self.peak_memory_bytes();
660
661 let mut output = String::new();
662 output.push_str("=== Execution Profile ===\n");
663 output.push_str(&format!("Operations: {}\n", self.stats.len()));
664 output.push_str(&format!(
665 "Total duration: {:.3} ms ({} us)\n",
666 total_duration_ms, total_duration_us
667 ));
668 output.push_str(&format!("Total memory: {} bytes\n", total_memory));
669 output.push_str(&format!("Peak memory: {} bytes\n", peak_memory));
670 output.push_str("\n--- Operations ---\n");
671
672 for (i, stat) in self.stats.iter().enumerate() {
673 let duration_ms = stat.duration_us as f64 / 1000.0;
674 let percentage = if total_duration_us > 0 {
675 (stat.duration_us as f64 / total_duration_us as f64) * 100.0
676 } else {
677 0.0
678 };
679
680 output.push_str(&format!(
681 "{:3}. {:<20} | {:>10} -> {:>10} rows | {:>8.3} ms ({:>5.1}%) | {:>10} bytes\n",
682 i + 1,
683 truncate_name(&stat.op_name, 20),
684 stat.input_rows,
685 stat.output_rows,
686 duration_ms,
687 percentage,
688 stat.memory_bytes
689 ));
690 }
691
692 output
693 }
694
695 pub fn set_enabled(&mut self, enabled: bool) {
699 self.enabled = enabled;
700 }
701}
702
703impl Default for Profiler {
704 fn default() -> Self {
706 Self {
707 enabled: false,
708 stats: Vec::new(),
709 strata: Vec::new(),
710 current_stratum: None,
711 stratum_start: None,
712 peak_memory_bytes: 0,
713 memory_budget_bytes: 0,
714 }
715 }
716}
717
718pub struct MeasureGuard<'a> {
722 profiler: &'a mut Profiler,
723 op_name: String,
724 input_rows: u64,
725 start: Instant,
726 output_rows: Option<u64>,
727}
728
729impl<'a> MeasureGuard<'a> {
730 pub fn new(profiler: &'a mut Profiler, op_name: impl Into<String>, input_rows: u64) -> Self {
732 Self {
733 profiler,
734 op_name: op_name.into(),
735 input_rows,
736 start: Instant::now(),
737 output_rows: None,
738 }
739 }
740
741 pub fn finish(mut self, output_rows: u64) {
743 self.output_rows = Some(output_rows);
744 }
746}
747
748impl<'a> Drop for MeasureGuard<'a> {
749 fn drop(&mut self) {
750 if self.profiler.is_enabled() {
751 let duration = self.start.elapsed();
752 self.profiler.record(OpStats {
753 op_name: std::mem::take(&mut self.op_name),
754 input_rows: self.input_rows,
755 output_rows: self.output_rows.unwrap_or(0),
756 duration_us: duration.as_micros() as u64,
757 memory_bytes: 0,
758 });
759 }
760 }
761}
762
763fn truncate_name(name: &str, max_len: usize) -> String {
765 if name.len() <= max_len {
766 name.to_string()
767 } else {
768 format!("{}...", &name[..max_len.saturating_sub(3)])
769 }
770}
771
772#[cfg(test)]
773mod tests {
774 use super::*;
775
776 #[test]
777 fn explicit_chain_equivalents_and_resident_semantic_counts_are_serialized_separately() {
778 let mut resident = crate::resident_graph::ResidentGraphExecutionStats::declined(
779 crate::resident_graph::ResidentGraphDeclineReason::SourceSetUncertified {
780 relation: "quoted\"relation\\name".to_string(),
781 },
782 );
783 resident.device_scan_invocations = 17;
784 resident.device_filter_invocations = 19;
785 resident.semantic_scan_invocations = 13;
786 resident.semantic_filter_invocations = 11;
787 let stats = ExecutionStats {
788 chain_fallback_scan_equivalents: 2,
789 chain_fallback_filter_equivalents: 3,
790 wcoj_fallback: WcojFallbackStats {
791 chain: 5,
792 dedicated_multiway: 7,
793 free_join: 11,
794 planned_hash: 13,
795 factorized_delta: 17,
796 groupby_fusion: 19,
797 },
798 resident_graph: Some(resident),
799 ..ExecutionStats::default()
800 };
801
802 let human = stats.format_human();
803 assert!(
804 human.contains("chain fallback equivalents: scans 2, filters 3"),
805 "{human}"
806 );
807 let json = stats.format_json();
808 let parsed: serde_json::Value =
809 serde_json::from_str(&json).expect("execution stats must be valid JSON");
810 assert_eq!(parsed["wcoj"]["chain_fallback_scan_equivalents"], 2);
811 assert_eq!(parsed["wcoj"]["chain_fallback_filter_equivalents"], 3);
812 assert_eq!(parsed["wcoj"]["fallback"]["total"], 72);
813 assert_eq!(parsed["wcoj"]["fallback"]["chain"], 5);
814 assert_eq!(parsed["wcoj"]["fallback"]["dedicated_multiway"], 7);
815 assert_eq!(parsed["wcoj"]["fallback"]["free_join"], 11);
816 assert_eq!(parsed["wcoj"]["fallback"]["planned_hash"], 13);
817 assert_eq!(parsed["wcoj"]["fallback"]["factorized_delta"], 17);
818 assert_eq!(parsed["wcoj"]["fallback"]["groupby_fusion"], 19);
819 assert_eq!(parsed["resident_graph"]["device_scan_invocations"], 17);
820 assert_eq!(parsed["resident_graph"]["device_filter_invocations"], 19);
821 assert_eq!(parsed["resident_graph"]["semantic_scan_invocations"], 13);
822 assert_eq!(parsed["resident_graph"]["semantic_filter_invocations"], 11);
823 assert_eq!(
824 parsed["resident_graph"]["resident_graph_declined"],
825 r#"SourceSetUncertified { relation: "quoted\"relation\\name" }"#
826 );
827 assert!(
828 json.contains("\"chain_fallback_scan_equivalents\":2"),
829 "{json}"
830 );
831 assert!(
832 json.contains("\"chain_fallback_filter_equivalents\":3"),
833 "{json}"
834 );
835 assert!(json.contains("\"device_scan_invocations\":17"), "{json}");
836 assert!(json.contains("\"device_filter_invocations\":19"), "{json}");
837 assert!(json.contains("\"semantic_scan_invocations\":13"), "{json}");
838 assert!(
839 json.contains("\"semantic_filter_invocations\":11"),
840 "{json}"
841 );
842 }
843
844 #[test]
847 fn test_opstats_new() {
848 let stats = OpStats::new("hash_join", 1000, 500, 1500, 4096);
849
850 assert_eq!(stats.op_name, "hash_join");
851 assert_eq!(stats.input_rows, 1000);
852 assert_eq!(stats.output_rows, 500);
853 assert_eq!(stats.duration_us, 1500);
854 assert_eq!(stats.memory_bytes, 4096);
855 }
856
857 #[test]
858 fn test_opstats_timed() {
859 let stats = OpStats::timed("filter", 1000, 800, 200);
860
861 assert_eq!(stats.op_name, "filter");
862 assert_eq!(stats.input_rows, 1000);
863 assert_eq!(stats.output_rows, 800);
864 assert_eq!(stats.duration_us, 200);
865 assert_eq!(stats.memory_bytes, 0);
866 }
867
868 #[test]
869 fn test_opstats_default() {
870 let stats = OpStats::default();
871
872 assert_eq!(stats.op_name, "");
873 assert_eq!(stats.input_rows, 0);
874 assert_eq!(stats.output_rows, 0);
875 assert_eq!(stats.duration_us, 0);
876 assert_eq!(stats.memory_bytes, 0);
877 }
878
879 #[test]
880 fn test_opstats_clone() {
881 let stats = OpStats::new("scan", 0, 1000, 100, 2048);
882 let cloned = stats.clone();
883
884 assert_eq!(stats, cloned);
885 }
886
887 #[test]
888 fn test_opstats_debug() {
889 let stats = OpStats::new("test_op", 100, 50, 10, 1024);
890 let debug_str = format!("{:?}", stats);
891
892 assert!(debug_str.contains("test_op"));
893 assert!(debug_str.contains("100"));
894 assert!(debug_str.contains("50"));
895 }
896
897 #[test]
900 fn test_profiler_new_enabled() {
901 let profiler = Profiler::new(true);
902
903 assert!(profiler.is_enabled());
904 assert!(profiler.stats().is_empty());
905 }
906
907 #[test]
908 fn test_profiler_new_disabled() {
909 let profiler = Profiler::new(false);
910
911 assert!(!profiler.is_enabled());
912 assert!(profiler.stats().is_empty());
913 }
914
915 #[test]
916 fn test_profiler_default() {
917 let profiler = Profiler::default();
918
919 assert!(!profiler.is_enabled());
920 assert!(profiler.stats().is_empty());
921 }
922
923 #[test]
926 fn test_profiler_record_when_enabled() {
927 let mut profiler = Profiler::new(true);
928
929 profiler.record(OpStats::new("op1", 100, 50, 10, 1024));
930 profiler.record(OpStats::new("op2", 50, 25, 5, 512));
931
932 assert_eq!(profiler.stats().len(), 2);
933 assert_eq!(profiler.stats()[0].op_name, "op1");
934 assert_eq!(profiler.stats()[1].op_name, "op2");
935 }
936
937 #[test]
938 fn test_profiler_record_when_disabled() {
939 let mut profiler = Profiler::new(false);
940
941 profiler.record(OpStats::new("op1", 100, 50, 10, 1024));
942 profiler.record(OpStats::new("op2", 50, 25, 5, 512));
943
944 assert!(profiler.stats().is_empty());
945 }
946
947 #[test]
948 fn test_profiler_set_enabled() {
949 let mut profiler = Profiler::new(false);
950
951 profiler.record(OpStats::new("op1", 100, 50, 10, 1024));
953 assert!(profiler.stats().is_empty());
954
955 profiler.set_enabled(true);
957 assert!(profiler.is_enabled());
958 profiler.record(OpStats::new("op2", 50, 25, 5, 512));
959 assert_eq!(profiler.stats().len(), 1);
960 assert_eq!(profiler.stats()[0].op_name, "op2");
961
962 profiler.set_enabled(false);
964 assert!(!profiler.is_enabled());
965 profiler.record(OpStats::new("op3", 25, 10, 2, 256));
966 assert_eq!(profiler.stats().len(), 1); }
968
969 #[test]
972 fn test_profiler_clear() {
973 let mut profiler = Profiler::new(true);
974
975 profiler.record(OpStats::new("op1", 100, 50, 10, 1024));
976 profiler.record(OpStats::new("op2", 50, 25, 5, 512));
977 assert_eq!(profiler.stats().len(), 2);
978
979 profiler.clear();
980
981 assert!(profiler.stats().is_empty());
982 assert!(profiler.is_enabled()); }
984
985 #[test]
986 fn test_profiler_clear_preserves_enabled_state() {
987 let mut profiler = Profiler::new(true);
988 profiler.record(OpStats::new("op1", 100, 50, 10, 1024));
989 profiler.clear();
990
991 assert!(profiler.is_enabled());
992
993 profiler.set_enabled(false);
994 profiler.clear();
995
996 assert!(!profiler.is_enabled());
997 }
998
999 #[test]
1002 fn test_total_duration_us() {
1003 let mut profiler = Profiler::new(true);
1004
1005 profiler.record(OpStats::new("op1", 100, 50, 100, 0));
1006 profiler.record(OpStats::new("op2", 50, 25, 200, 0));
1007 profiler.record(OpStats::new("op3", 25, 10, 150, 0));
1008
1009 assert_eq!(profiler.total_duration_us(), 450);
1010 }
1011
1012 #[test]
1013 fn test_total_duration_us_empty() {
1014 let profiler = Profiler::new(true);
1015
1016 assert_eq!(profiler.total_duration_us(), 0);
1017 }
1018
1019 #[test]
1020 fn test_total_memory_bytes() {
1021 let mut profiler = Profiler::new(true);
1022
1023 profiler.record(OpStats::new("op1", 100, 50, 10, 1024));
1024 profiler.record(OpStats::new("op2", 50, 25, 5, 2048));
1025 profiler.record(OpStats::new("op3", 25, 10, 2, 512));
1026
1027 assert_eq!(profiler.total_memory_bytes(), 3584);
1028 }
1029
1030 #[test]
1031 fn test_total_memory_bytes_empty() {
1032 let profiler = Profiler::new(true);
1033
1034 assert_eq!(profiler.total_memory_bytes(), 0);
1035 }
1036
1037 #[test]
1038 fn test_peak_memory_bytes() {
1039 let mut profiler = Profiler::new(true);
1040
1041 profiler.record(OpStats::new("op1", 100, 50, 10, 1024));
1042 profiler.record(OpStats::new("op2", 50, 25, 5, 4096));
1043 profiler.record(OpStats::new("op3", 25, 10, 2, 2048));
1044
1045 assert_eq!(profiler.peak_memory_bytes(), 4096);
1046 }
1047
1048 #[test]
1049 fn test_peak_memory_bytes_empty() {
1050 let profiler = Profiler::new(true);
1051
1052 assert_eq!(profiler.peak_memory_bytes(), 0);
1053 }
1054
1055 #[test]
1056 fn test_operation_count() {
1057 let mut profiler = Profiler::new(true);
1058
1059 assert_eq!(profiler.operation_count(), 0);
1060
1061 profiler.record(OpStats::new("op1", 100, 50, 10, 1024));
1062 assert_eq!(profiler.operation_count(), 1);
1063
1064 profiler.record(OpStats::new("op2", 50, 25, 5, 512));
1065 assert_eq!(profiler.operation_count(), 2);
1066
1067 profiler.clear();
1068 assert_eq!(profiler.operation_count(), 0);
1069 }
1070
1071 #[test]
1074 fn test_summary_empty() {
1075 let profiler = Profiler::new(true);
1076 let summary = profiler.summary();
1077
1078 assert!(summary.contains("No operations recorded"));
1079 }
1080
1081 #[test]
1082 fn test_summary_with_operations() {
1083 let mut profiler = Profiler::new(true);
1084
1085 profiler.record(OpStats::new("scan", 0, 1000, 100, 4096));
1086 profiler.record(OpStats::new("filter", 1000, 500, 200, 2048));
1087 profiler.record(OpStats::new("hash_join", 500, 250, 500, 8192));
1088
1089 let summary = profiler.summary();
1090
1091 assert!(summary.contains("=== Execution Profile ==="));
1093 assert!(summary.contains("Operations: 3"));
1094
1095 assert!(summary.contains("Total duration:"));
1097 assert!(summary.contains("800 us"));
1098
1099 assert!(summary.contains("Total memory: 14336 bytes"));
1101 assert!(summary.contains("Peak memory: 8192 bytes"));
1102
1103 assert!(summary.contains("scan"));
1105 assert!(summary.contains("filter"));
1106 assert!(summary.contains("hash_join"));
1107
1108 assert!(summary.contains("1000"));
1110 assert!(summary.contains("500"));
1111 assert!(summary.contains("250"));
1112 }
1113
1114 #[test]
1115 fn test_summary_percentages() {
1116 let mut profiler = Profiler::new(true);
1117
1118 profiler.record(OpStats::new("fast_op", 100, 50, 250, 0));
1120 profiler.record(OpStats::new("slow_op", 100, 50, 750, 0));
1121
1122 let summary = profiler.summary();
1123
1124 assert!(summary.contains("25.0%") || summary.contains("25."));
1126 assert!(summary.contains("75.0%") || summary.contains("75."));
1127 }
1128
1129 #[test]
1132 fn test_truncate_name_short() {
1133 let result = truncate_name("short", 20);
1134 assert_eq!(result, "short");
1135 }
1136
1137 #[test]
1138 fn test_truncate_name_exact() {
1139 let name = "exactly_twenty_chars"; let result = truncate_name(name, 20);
1141 assert_eq!(result, name);
1142 }
1143
1144 #[test]
1145 fn test_truncate_name_long() {
1146 let name = "this_is_a_very_long_operation_name";
1147 let result = truncate_name(name, 20);
1148 assert_eq!(result.len(), 20);
1149 assert!(result.ends_with("..."));
1150 }
1151
1152 #[test]
1155 fn test_profiler_full_workflow() {
1156 let mut profiler = Profiler::new(true);
1158
1159 profiler.record(OpStats::new("scan_edge", 0, 10000, 500, 40000));
1161 profiler.record(OpStats::new("scan_node", 0, 1000, 100, 4000));
1162 profiler.record(OpStats::new("hash_join", 11000, 5000, 2000, 100000));
1163 profiler.record(OpStats::new("filter", 5000, 2000, 300, 20000));
1164 profiler.record(OpStats::new("project", 2000, 2000, 50, 8000));
1165 profiler.record(OpStats::new("dedup", 2000, 1500, 400, 12000));
1166
1167 assert_eq!(profiler.operation_count(), 6);
1169 assert_eq!(profiler.total_duration_us(), 3350);
1170 assert_eq!(profiler.total_memory_bytes(), 184000);
1171 assert_eq!(profiler.peak_memory_bytes(), 100000);
1172
1173 let summary = profiler.summary();
1175 assert!(summary.contains("6"));
1176 assert!(summary.contains("scan_edge"));
1177 assert!(summary.contains("hash_join"));
1178 assert!(summary.contains("dedup"));
1179
1180 profiler.clear();
1182 assert_eq!(profiler.operation_count(), 0);
1183 assert!(profiler.is_enabled());
1184 }
1185
1186 #[test]
1187 fn test_profiler_disabled_has_zero_overhead() {
1188 let mut profiler = Profiler::new(false);
1190
1191 for i in 0..1000 {
1192 profiler.record(OpStats::new(
1193 format!("op_{}", i),
1194 i as u64,
1195 i as u64,
1196 i as u64,
1197 i as u64,
1198 ));
1199 }
1200
1201 assert_eq!(profiler.operation_count(), 0);
1203 assert_eq!(profiler.total_duration_us(), 0);
1204 assert_eq!(profiler.total_memory_bytes(), 0);
1205 }
1206
1207 #[test]
1208 fn test_profiler_stats_immutable_reference() {
1209 let mut profiler = Profiler::new(true);
1210
1211 profiler.record(OpStats::new("op1", 100, 50, 10, 1024));
1212
1213 let stats = profiler.stats();
1215 assert_eq!(stats.len(), 1);
1216 assert_eq!(stats[0].op_name, "op1");
1217
1218 profiler.record(OpStats::new("op2", 50, 25, 5, 512));
1220 assert_eq!(profiler.stats().len(), 2);
1221 }
1222
1223 #[test]
1224 fn test_opstats_equality() {
1225 let stats1 = OpStats::new("op", 100, 50, 10, 1024);
1226 let stats2 = OpStats::new("op", 100, 50, 10, 1024);
1227 let stats3 = OpStats::new("op", 100, 50, 10, 2048); assert_eq!(stats1, stats2);
1230 assert_ne!(stats1, stats3);
1231 }
1232}