ixa/profiling/
reporting.rs

1use std::path::Path;
2
3use super::file::write_profiling_data_to_file;
4use crate::{error, Context, ContextReportExt};
5
6/// Trait extension for [`Context`] providing profiling capabilities.
7pub trait ProfilingContextExt: ContextReportExt {
8    /// Prints the execution statistics for this context to the console.
9    ///
10    /// If `include_profiling_data` is true, also prints the global profiling data
11    /// (spans, counts, and computed statistics).
12    fn print_execution_statistics(&mut self, include_profiling_data: bool) {
13        let stats = self.get_execution_statistics();
14        crate::execution_stats::print_execution_statistics(&stats);
15
16        if include_profiling_data {
17            super::print_profiling_data();
18        }
19    }
20
21    /// Writes the execution statistics for the context and all profiling data
22    /// to a JSON file.
23    fn write_profiling_data(&mut self) {
24        let (mut prefix, directory, overwrite) = {
25            let report_options = self.report_options();
26            (
27                report_options.file_prefix.clone(),
28                report_options.output_dir.clone(),
29                report_options.overwrite,
30            )
31        };
32
33        let execution_statistics = self.get_execution_statistics();
34        // Default filename when not provided via parameters: write under report options
35        // using the current file prefix.
36        prefix.push_str("profiling.json");
37        let profiling_data_path = directory.join(prefix);
38        let profiling_data_path = Path::new(&profiling_data_path);
39
40        if !overwrite && profiling_data_path.exists() {
41            error!(
42                "profiling output file already exists: {}",
43                profiling_data_path.display()
44            );
45            return;
46        }
47
48        write_profiling_data_to_file(profiling_data_path, execution_statistics)
49            .expect("could not write profiling data to file");
50    }
51}
52impl ProfilingContextExt for Context {}
53
54#[cfg(test)]
55mod tests {
56    use std::fs;
57    use std::time::Duration;
58
59    use tempfile::tempdir;
60
61    use super::ProfilingContextExt;
62    use crate::context::Context;
63    use crate::profiling::{add_computed_statistic, increment_named_count, open_span};
64    use crate::report::ContextReportExt as _; // bring trait with methods like report_options into scope
65
66    #[test]
67    fn print_execution_statistics_without_profiling_data() {
68        let mut context = Context::new();
69        context.print_execution_statistics(false);
70    }
71
72    #[test]
73    fn print_execution_statistics_with_profiling_data() {
74        // Create some profiling activity so data exists
75        increment_named_count("reporting_print_event");
76        increment_named_count("reporting_print_event");
77        {
78            let _span = open_span("reporting_print_span");
79            std::thread::sleep(Duration::from_millis(5));
80        }
81        add_computed_statistic::<usize>(
82            "reporting_print_stat",
83            "Count of reporting_print_event",
84            Box::new(|data| data.get_named_count("reporting_print_event")),
85            Box::new(|_v| {}),
86        );
87
88        let mut context = Context::new();
89        context.print_execution_statistics(true);
90    }
91
92    #[test]
93    fn write_profiling_data_creates_json() {
94        let temp_dir = tempdir().unwrap();
95        let out_dir = temp_dir.path().to_path_buf();
96
97        // Prepare some profiling data
98        increment_named_count("reporting_write_event");
99        {
100            let _span = open_span("reporting_write_span");
101            std::thread::sleep(Duration::from_millis(3));
102        }
103
104        let mut context = Context::new();
105        let config = context.report_options();
106        config
107            .file_prefix("test_")
108            .directory(out_dir.clone())
109            .overwrite(true);
110
111        context.write_profiling_data();
112
113        let file_path = out_dir.join("test_profiling.json");
114        assert!(file_path.exists(), "JSON file should be created");
115
116        let content = fs::read_to_string(&file_path).expect("Failed to read JSON");
117        let json: serde_json::Value = serde_json::from_str(&content).expect("Invalid JSON");
118        assert!(json["execution_statistics"].is_object());
119        assert!(json["named_counts"].is_array());
120        assert!(json["named_spans"].is_array());
121        assert!(json["computed_statistics"].is_object());
122    }
123
124    #[test]
125    fn write_profiling_data_respects_overwrite_false() {
126        let temp_dir = tempdir().unwrap();
127        let out_dir = temp_dir.path().to_path_buf();
128
129        // Pre-create the target file with distinct content
130        let file_path = out_dir.join("prefix_profiling.json");
131        fs::write(&file_path, "PREEXISTING").unwrap();
132
133        let mut context = Context::new();
134        let config = context.report_options();
135        config
136            .file_prefix("prefix_")
137            .directory(out_dir.clone())
138            .overwrite(false);
139
140        // Attempt to write; should return early and not modify the file
141        context.write_profiling_data();
142
143        let after = fs::read_to_string(&file_path).unwrap();
144        assert_eq!(
145            after, "PREEXISTING",
146            "File should remain unchanged when overwrite=false"
147        );
148    }
149
150    #[test]
151    fn write_profiling_data_overwrites_when_true() {
152        let temp_dir = tempdir().unwrap();
153        let out_dir = temp_dir.path().to_path_buf();
154
155        let file_path = out_dir
156            .join("ow_")
157            .join("..") // ensure path handling remains simple
158            .canonicalize()
159            .unwrap_or(out_dir.clone())
160            .join("ow_profiling.json");
161
162        // Ensure directory exists and pre-create file
163        let _ = fs::create_dir_all(file_path.parent().unwrap());
164        fs::write(&file_path, "OLD").unwrap();
165
166        let mut context = Context::new();
167        let config = context.report_options();
168        config
169            .file_prefix("ow_")
170            .directory(file_path.parent().unwrap().to_path_buf())
171            .overwrite(true);
172
173        context.write_profiling_data();
174
175        let content = fs::read_to_string(&file_path).unwrap();
176        assert!(
177            content.starts_with("{"),
178            "File should contain JSON after overwrite"
179        );
180        assert_ne!(
181            content, "OLD",
182            "File content should be updated when overwrite=true"
183        );
184    }
185}