1use serde::{Deserialize, Serialize};
36use std::time::{Duration, Instant};
37
38#[derive(Debug, Clone, Serialize, Deserialize)]
40pub struct ProfileEntry {
41 pub phase: String,
43 pub duration_ms: f64,
45 pub items_processed: usize,
47 pub notes: String,
49}
50
51impl ProfileEntry {
52 pub fn new(phase: impl Into<String>, duration: Duration, items: usize) -> Self {
54 ProfileEntry {
55 phase: phase.into(),
56 duration_ms: duration.as_secs_f64() * 1000.0,
57 items_processed: items,
58 notes: String::new(),
59 }
60 }
61
62 pub fn with_notes(mut self, notes: impl Into<String>) -> Self {
64 self.notes = notes.into();
65 self
66 }
67
68 pub fn throughput(&self) -> f64 {
72 if self.duration_ms < 1e-9 {
73 return f64::INFINITY;
74 }
75 self.items_processed as f64 / (self.duration_ms / 1000.0)
76 }
77}
78
79#[derive(Debug, Clone, Default, Serialize, Deserialize)]
81pub struct ProfileReport {
82 pub entries: Vec<ProfileEntry>,
84 pub total_duration_ms: f64,
86 pub input_complexity: usize,
88 pub output_size: usize,
90}
91
92impl ProfileReport {
93 pub fn new() -> Self {
95 Self::default()
96 }
97
98 pub fn add_entry(&mut self, entry: ProfileEntry) {
100 self.total_duration_ms += entry.duration_ms;
101 self.entries.push(entry);
102 }
103
104 pub fn slowest_phase(&self) -> Option<&ProfileEntry> {
106 self.entries.iter().max_by(|a, b| {
107 a.duration_ms
108 .partial_cmp(&b.duration_ms)
109 .unwrap_or(std::cmp::Ordering::Equal)
110 })
111 }
112
113 pub fn fastest_phase(&self) -> Option<&ProfileEntry> {
115 self.entries.iter().min_by(|a, b| {
116 a.duration_ms
117 .partial_cmp(&b.duration_ms)
118 .unwrap_or(std::cmp::Ordering::Equal)
119 })
120 }
121
122 pub fn phase_percentages(&self) -> Vec<(&str, f64)> {
126 if self.total_duration_ms < 1e-9 {
127 return vec![];
128 }
129 self.entries
130 .iter()
131 .map(|e| {
132 (
133 e.phase.as_str(),
134 e.duration_ms / self.total_duration_ms * 100.0,
135 )
136 })
137 .collect()
138 }
139
140 pub fn phase_count(&self) -> usize {
142 self.entries.len()
143 }
144
145 pub fn summary(&self) -> String {
147 let mut s = format!(
148 "Compilation Profile ({:.2}ms total, {} phases):\n",
149 self.total_duration_ms,
150 self.entries.len()
151 );
152 for entry in &self.entries {
153 let pct = if self.total_duration_ms > 0.0 {
154 entry.duration_ms / self.total_duration_ms * 100.0
155 } else {
156 0.0
157 };
158 s.push_str(&format!(
159 " {:20} {:8.2}ms ({:5.1}%) [{} items]\n",
160 entry.phase, entry.duration_ms, pct, entry.items_processed
161 ));
162 }
163 s.push_str(&format!(
164 "Input complexity: {}, Output size: {}\n",
165 self.input_complexity, self.output_size
166 ));
167 s
168 }
169
170 pub fn compilation_speed(&self) -> f64 {
172 if self.total_duration_ms < 1e-9 {
173 return 0.0;
174 }
175 self.output_size as f64 / self.total_duration_ms
176 }
177}
178
179pub struct CompilationProfiler {
186 report: ProfileReport,
187 current_phase: Option<(String, Instant, usize)>,
188}
189
190impl CompilationProfiler {
191 pub fn new() -> Self {
193 CompilationProfiler {
194 report: ProfileReport::new(),
195 current_phase: None,
196 }
197 }
198
199 pub fn begin_phase(&mut self, phase: impl Into<String>) {
204 self.end_phase();
206 self.current_phase = Some((phase.into(), Instant::now(), 0));
207 }
208
209 pub fn set_items(&mut self, count: usize) {
211 if let Some((_, _, ref mut items)) = self.current_phase {
212 *items = count;
213 }
214 }
215
216 pub fn end_phase(&mut self) {
218 if let Some((name, start, items)) = self.current_phase.take() {
219 let duration = start.elapsed();
220 self.report
221 .add_entry(ProfileEntry::new(name, duration, items));
222 }
223 }
224
225 pub fn set_input_complexity(&mut self, complexity: usize) {
227 self.report.input_complexity = complexity;
228 }
229
230 pub fn set_output_size(&mut self, size: usize) {
232 self.report.output_size = size;
233 }
234
235 pub fn finish(mut self) -> ProfileReport {
239 self.end_phase(); self.report
241 }
242
243 pub fn current_report(&self) -> &ProfileReport {
245 &self.report
246 }
247}
248
249impl Default for CompilationProfiler {
250 fn default() -> Self {
251 Self::new()
252 }
253}
254
255pub fn profile_phase<T, F: FnOnce() -> T>(phase: &str, f: F) -> (T, ProfileEntry) {
273 let start = Instant::now();
274 let result = f();
275 let duration = start.elapsed();
276 (result, ProfileEntry::new(phase, duration, 0))
277}
278
279#[cfg(test)]
280mod tests {
281 use super::*;
282 use std::thread;
283
284 #[test]
285 fn test_profile_entry_new() {
286 let entry = ProfileEntry::new("parse", Duration::from_millis(15), 200);
287 assert_eq!(entry.phase, "parse");
288 assert!((entry.duration_ms - 15.0).abs() < 0.01);
289 assert_eq!(entry.items_processed, 200);
290 assert!(entry.notes.is_empty());
291 }
292
293 #[test]
294 fn test_profile_entry_throughput() {
295 let entry = ProfileEntry::new("phase", Duration::from_millis(10), 100);
297 let tp = entry.throughput();
298 assert!((tp - 10_000.0).abs() < 1.0, "expected ~10000, got {}", tp);
299 }
300
301 #[test]
302 fn test_profile_entry_with_notes() {
303 let entry =
304 ProfileEntry::new("opt", Duration::from_millis(5), 10).with_notes("applied CSE");
305 assert_eq!(entry.notes, "applied CSE");
306 }
307
308 #[test]
309 fn test_profile_entry_zero_duration_throughput() {
310 let entry = ProfileEntry::new("instant", Duration::from_secs(0), 50);
311 assert!(entry.throughput().is_infinite());
312 }
313
314 #[test]
315 fn test_profile_report_new() {
316 let r = ProfileReport::new();
317 assert!(r.entries.is_empty());
318 assert!((r.total_duration_ms).abs() < 1e-9);
319 assert_eq!(r.input_complexity, 0);
320 assert_eq!(r.output_size, 0);
321 }
322
323 #[test]
324 fn test_profile_report_add_entry() {
325 let mut r = ProfileReport::new();
326 r.add_entry(ProfileEntry::new("a", Duration::from_millis(10), 1));
327 r.add_entry(ProfileEntry::new("b", Duration::from_millis(20), 2));
328 assert_eq!(r.entries.len(), 2);
329 assert!((r.total_duration_ms - 30.0).abs() < 0.01);
330 }
331
332 #[test]
333 fn test_profile_report_slowest() {
334 let mut r = ProfileReport::new();
335 r.add_entry(ProfileEntry::new("fast", Duration::from_millis(5), 0));
336 r.add_entry(ProfileEntry::new("slow", Duration::from_millis(50), 0));
337 r.add_entry(ProfileEntry::new("mid", Duration::from_millis(20), 0));
338 let slowest = r.slowest_phase().expect("should have slowest");
339 assert_eq!(slowest.phase, "slow");
340 }
341
342 #[test]
343 fn test_profile_report_fastest() {
344 let mut r = ProfileReport::new();
345 r.add_entry(ProfileEntry::new("fast", Duration::from_millis(5), 0));
346 r.add_entry(ProfileEntry::new("slow", Duration::from_millis(50), 0));
347 let fastest = r.fastest_phase().expect("should have fastest");
348 assert_eq!(fastest.phase, "fast");
349 }
350
351 #[test]
352 fn test_profile_report_percentages() {
353 let mut r = ProfileReport::new();
354 r.add_entry(ProfileEntry::new("a", Duration::from_millis(25), 0));
355 r.add_entry(ProfileEntry::new("b", Duration::from_millis(75), 0));
356 let pcts = r.phase_percentages();
357 assert_eq!(pcts.len(), 2);
358 let sum: f64 = pcts.iter().map(|(_, p)| p).sum();
359 assert!(
360 (sum - 100.0).abs() < 0.01,
361 "percentages should sum to ~100, got {}",
362 sum
363 );
364 assert!((pcts[0].1 - 25.0).abs() < 0.1);
366 assert!((pcts[1].1 - 75.0).abs() < 0.1);
367 }
368
369 #[test]
370 fn test_profile_report_phase_count() {
371 let mut r = ProfileReport::new();
372 assert_eq!(r.phase_count(), 0);
373 r.add_entry(ProfileEntry::new("x", Duration::from_millis(1), 0));
374 r.add_entry(ProfileEntry::new("y", Duration::from_millis(1), 0));
375 r.add_entry(ProfileEntry::new("z", Duration::from_millis(1), 0));
376 assert_eq!(r.phase_count(), 3);
377 }
378
379 #[test]
380 fn test_profile_report_summary() {
381 let mut r = ProfileReport::new();
382 r.input_complexity = 100;
383 r.output_size = 50;
384 r.add_entry(ProfileEntry::new("parse", Duration::from_millis(10), 80));
385 r.add_entry(ProfileEntry::new("codegen", Duration::from_millis(20), 50));
386 let summary = r.summary();
387 assert!(
388 summary.contains("parse"),
389 "should contain phase name 'parse'"
390 );
391 assert!(
392 summary.contains("codegen"),
393 "should contain phase name 'codegen'"
394 );
395 assert!(
396 summary.contains("Input complexity: 100"),
397 "should contain input complexity"
398 );
399 assert!(
400 summary.contains("Output size: 50"),
401 "should contain output size"
402 );
403 }
404
405 #[test]
406 fn test_profile_report_compilation_speed() {
407 let mut r = ProfileReport::new();
408 r.output_size = 200;
409 r.total_duration_ms = 100.0;
411 let speed = r.compilation_speed();
412 assert!((speed - 2.0).abs() < 0.01, "expected 2.0, got {}", speed);
413 }
414
415 #[test]
416 fn test_profiler_begin_end() {
417 let mut profiler = CompilationProfiler::new();
418 profiler.begin_phase("single");
419 thread::sleep(Duration::from_millis(5));
420 profiler.set_items(42);
421 profiler.end_phase();
422
423 let report = profiler.current_report();
424 assert_eq!(report.entries.len(), 1);
425 assert_eq!(report.entries[0].phase, "single");
426 assert_eq!(report.entries[0].items_processed, 42);
427 assert!(report.entries[0].duration_ms > 0.0);
428 }
429
430 #[test]
431 fn test_profiler_multiple_phases() {
432 let mut profiler = CompilationProfiler::new();
433 for name in &["parse", "optimize", "codegen"] {
434 profiler.begin_phase(*name);
435 profiler.set_items(10);
436 profiler.end_phase();
437 }
438 let report = profiler.finish();
439 assert_eq!(report.phase_count(), 3);
440 assert_eq!(report.entries[0].phase, "parse");
441 assert_eq!(report.entries[1].phase, "optimize");
442 assert_eq!(report.entries[2].phase, "codegen");
443 }
444
445 #[test]
446 fn test_profiler_auto_end_on_begin() {
447 let mut profiler = CompilationProfiler::new();
448 profiler.begin_phase("first");
449 profiler.set_items(5);
450 profiler.begin_phase("second");
452 profiler.end_phase();
453
454 let report = profiler.finish();
455 assert_eq!(report.phase_count(), 2);
456 assert_eq!(report.entries[0].phase, "first");
457 assert_eq!(report.entries[0].items_processed, 5);
458 assert_eq!(report.entries[1].phase, "second");
459 }
460
461 #[test]
462 fn test_profiler_finish() {
463 let mut profiler = CompilationProfiler::new();
464 profiler.set_input_complexity(300);
465 profiler.set_output_size(150);
466 profiler.begin_phase("work");
467 profiler.set_items(100);
468 let report = profiler.finish();
470 assert_eq!(report.phase_count(), 1);
471 assert_eq!(report.input_complexity, 300);
472 assert_eq!(report.output_size, 150);
473 assert!(report.total_duration_ms >= 0.0);
474 }
475
476 #[test]
477 fn test_profiler_set_complexity() {
478 let mut profiler = CompilationProfiler::new();
479 profiler.set_input_complexity(999);
480 assert_eq!(profiler.current_report().input_complexity, 999);
481 }
482
483 #[test]
484 fn test_profile_phase_fn() {
485 let (result, entry) = profile_phase("compute", || {
486 let mut sum = 0u64;
487 for i in 0..1000 {
488 sum += i;
489 }
490 sum
491 });
492 assert_eq!(result, 499_500);
493 assert_eq!(entry.phase, "compute");
494 assert_eq!(entry.items_processed, 0);
495 assert!(entry.duration_ms >= 0.0);
496 }
497}