Skip to main content

tensorlogic_compiler/
profiling.rs

1//! Compilation profiling: track time and resource usage across phases.
2//!
3//! Instruments the compilation pipeline to provide per-phase timing breakdowns,
4//! helping identify bottlenecks and optimize compilation performance.
5//!
6//! # Overview
7//!
8//! The profiling module provides three main components:
9//!
10//! - [`ProfileEntry`]: A single profiling entry for a compilation phase
11//! - [`ProfileReport`]: A complete profiling report for a compilation run
12//! - [`CompilationProfiler`]: Real-time phase tracker for compilation
13//!
14//! # Examples
15//!
16//! ```rust
17//! use tensorlogic_compiler::profiling::{CompilationProfiler, ProfileReport, ProfileEntry};
18//!
19//! let mut profiler = CompilationProfiler::new();
20//! profiler.set_input_complexity(500);
21//!
22//! profiler.begin_phase("parse");
23//! profiler.set_items(120);
24//! profiler.end_phase();
25//!
26//! profiler.begin_phase("optimize");
27//! profiler.set_items(80);
28//! profiler.end_phase();
29//!
30//! profiler.set_output_size(60);
31//! let report = profiler.finish();
32//! println!("{}", report.summary());
33//! ```
34
35use serde::{Deserialize, Serialize};
36use std::time::{Duration, Instant};
37
38/// A single profiling entry for a compilation phase.
39#[derive(Debug, Clone, Serialize, Deserialize)]
40pub struct ProfileEntry {
41    /// Phase name (e.g., "parse", "type_check", "optimize", "codegen")
42    pub phase: String,
43    /// Duration of this phase in milliseconds
44    pub duration_ms: f64,
45    /// Number of nodes/expressions processed in this phase
46    pub items_processed: usize,
47    /// Optional notes about what happened
48    pub notes: String,
49}
50
51impl ProfileEntry {
52    /// Create a new profile entry from a phase name, duration, and item count.
53    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    /// Attach notes to this entry (builder pattern).
63    pub fn with_notes(mut self, notes: impl Into<String>) -> Self {
64        self.notes = notes.into();
65        self
66    }
67
68    /// Throughput: items per second.
69    ///
70    /// Returns `f64::INFINITY` when duration is effectively zero.
71    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/// A complete profiling report for a compilation run.
80#[derive(Debug, Clone, Default, Serialize, Deserialize)]
81pub struct ProfileReport {
82    /// Individual phase entries in execution order
83    pub entries: Vec<ProfileEntry>,
84    /// Total wall-clock duration in milliseconds
85    pub total_duration_ms: f64,
86    /// Expression complexity (number of nodes in input)
87    pub input_complexity: usize,
88    /// Output graph size (number of nodes in compiled graph)
89    pub output_size: usize,
90}
91
92impl ProfileReport {
93    /// Create a new empty report.
94    pub fn new() -> Self {
95        Self::default()
96    }
97
98    /// Add an entry and accumulate its duration into the total.
99    pub fn add_entry(&mut self, entry: ProfileEntry) {
100        self.total_duration_ms += entry.duration_ms;
101        self.entries.push(entry);
102    }
103
104    /// Get the slowest phase by duration.
105    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    /// Get the fastest phase by duration.
114    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    /// Percentage of total time spent in each phase.
123    ///
124    /// Returns an empty vector when total duration is effectively zero.
125    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    /// Number of phases recorded.
141    pub fn phase_count(&self) -> usize {
142        self.entries.len()
143    }
144
145    /// Human-readable summary of the compilation profile.
146    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    /// Compilation speed: output nodes per millisecond.
171    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
179/// A profiler that tracks compilation phases in real-time.
180///
181/// Use [`begin_phase`](CompilationProfiler::begin_phase) /
182/// [`end_phase`](CompilationProfiler::end_phase) to bracket each compilation
183/// stage, then call [`finish`](CompilationProfiler::finish) to collect the
184/// completed [`ProfileReport`].
185pub struct CompilationProfiler {
186    report: ProfileReport,
187    current_phase: Option<(String, Instant, usize)>,
188}
189
190impl CompilationProfiler {
191    /// Create a new profiler.
192    pub fn new() -> Self {
193        CompilationProfiler {
194            report: ProfileReport::new(),
195            current_phase: None,
196        }
197    }
198
199    /// Start timing a new phase.
200    ///
201    /// If there is an active phase it is ended automatically before the new
202    /// one begins.
203    pub fn begin_phase(&mut self, phase: impl Into<String>) {
204        // If there's an active phase, end it first
205        self.end_phase();
206        self.current_phase = Some((phase.into(), Instant::now(), 0));
207    }
208
209    /// Set the items processed count for the current phase.
210    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    /// End the current phase and record it.
217    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    /// Set input complexity on the report.
226    pub fn set_input_complexity(&mut self, complexity: usize) {
227        self.report.input_complexity = complexity;
228    }
229
230    /// Set output graph size on the report.
231    pub fn set_output_size(&mut self, size: usize) {
232        self.report.output_size = size;
233    }
234
235    /// Finish profiling and return the report.
236    ///
237    /// Any open phase is ended automatically.
238    pub fn finish(mut self) -> ProfileReport {
239        self.end_phase(); // end any open phase
240        self.report
241    }
242
243    /// Get a reference to the current (in-progress) report.
244    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
255/// Profile a closure as a single phase and return (result, entry).
256///
257/// The returned [`ProfileEntry`] has `items_processed` set to 0; callers can
258/// adjust it or attach notes afterwards.
259///
260/// # Examples
261///
262/// ```rust
263/// use tensorlogic_compiler::profiling::profile_phase;
264///
265/// let (result, entry) = profile_phase("my_phase", || {
266///     // expensive work
267///     42
268/// });
269/// assert_eq!(result, 42);
270/// assert_eq!(entry.phase, "my_phase");
271/// ```
272pub 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        // 100 items in 10ms = 10_000 items/s
296        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        // "a" should be ~25%, "b" should be ~75%
365        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        // Manually set total to avoid floating-point drift from add_entry
410        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        // Starting a new phase should auto-end "first"
451        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        // finish should auto-end the open phase
469        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}