Skip to main content

ixa/profiling/
reporting.rs

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