ixa/profiling/
display.rs

1#[cfg(feature = "profiling")]
2use humantime::format_duration;
3
4#[cfg(feature = "profiling")]
5use super::{profiling_data, ProfilingData, NAMED_COUNTS_HEADERS, NAMED_SPANS_HEADERS};
6
7/// Prints all collected profiling data.
8#[cfg(feature = "profiling")]
9pub fn print_profiling_data() {
10    print_named_spans();
11    print_named_counts();
12    print_computed_statistics();
13}
14
15#[cfg(not(feature = "profiling"))]
16pub fn print_profiling_data() {}
17
18/// Prints a table of the named counts, if any.
19#[cfg(feature = "profiling")]
20pub fn print_named_counts() {
21    let container = profiling_data();
22    if container.counts.is_empty() {
23        // nothing to report
24        return;
25    }
26    let rows = container.get_named_counts_table();
27
28    let mut formatted_rows = vec![
29        // The header row
30        NAMED_COUNTS_HEADERS
31            .iter()
32            .map(|s| (*s).to_string())
33            .collect(),
34    ];
35
36    formatted_rows.extend(rows.into_iter().map(|(label, count, rate)| {
37        vec![
38            label,
39            format_with_commas(count),
40            format_with_commas_f64(rate),
41        ]
42    }));
43
44    println!();
45    print_formatted_table(&formatted_rows);
46}
47
48#[cfg(not(feature = "profiling"))]
49pub fn print_named_counts() {}
50
51/// Prints a table of the spans, if any.
52#[cfg(feature = "profiling")]
53pub fn print_named_spans() {
54    let rows = profiling_data().get_named_spans_table();
55    if rows.is_empty() {
56        // nothing to report
57        return;
58    }
59
60    let mut formatted_rows = vec![
61        // Header row
62        NAMED_SPANS_HEADERS
63            .iter()
64            .map(|s| (*s).to_string())
65            .collect(),
66    ];
67
68    formatted_rows.extend(
69        rows.into_iter()
70            .map(|(label, count, duration, percent_runtime)| {
71                vec![
72                    label,
73                    format_with_commas(count),
74                    format_duration(duration).to_string(),
75                    format!("{:.2}%", percent_runtime),
76                ]
77            }),
78    );
79
80    println!();
81    print_formatted_table(&formatted_rows);
82}
83
84#[cfg(not(feature = "profiling"))]
85pub fn print_named_spans() {}
86
87/// Prints the forecast efficiency.
88#[cfg(feature = "profiling")]
89pub fn print_computed_statistics() {
90    let mut container = profiling_data();
91
92    // Compute first to avoid double borrow
93    let stat_count = container.computed_statistics.len();
94    if stat_count == 0 {
95        return;
96    }
97    for idx in 0..stat_count {
98        // Temporarily take the statistic, because we need immutable access to `container`.
99        let mut statistic = container.computed_statistics[idx].take().unwrap();
100        statistic.value = statistic.functions.compute(&container);
101        // Return the statistic
102        container.computed_statistics[idx] = Some(statistic);
103    }
104
105    println!();
106
107    for statistic in &container.computed_statistics {
108        let statistic = statistic.as_ref().unwrap();
109        if statistic.value.is_none() {
110            continue;
111        }
112        statistic.functions.print(statistic.value.unwrap());
113    }
114}
115#[cfg(not(feature = "profiling"))]
116pub fn print_computed_statistics() {}
117
118/// Prints a table with aligned columns, using the first row as a header.
119/// The first column is left-aligned; remaining columns are right-aligned.
120/// Automatically adjusts column widths and inserts a separator line.
121#[cfg(feature = "profiling")]
122pub fn print_formatted_table(rows: &[Vec<String>]) {
123    if rows.len() < 2 {
124        return;
125    }
126
127    let num_cols = rows[0].len();
128    let mut col_widths = vec![0; num_cols];
129
130    // Compute max column widths
131    for row in rows {
132        for (i, cell) in row.iter().enumerate() {
133            col_widths[i] = col_widths[i].max(cell.len());
134        }
135    }
136
137    // Print header row
138    let header = &rows[0];
139    for (i, cell) in header.iter().enumerate() {
140        if i == 0 {
141            print!("{:<width$} ", cell, width = col_widths[i] + 1);
142        } else {
143            print!("{:>width$} ", cell, width = col_widths[i] + 1);
144        }
145    }
146    println!();
147
148    // Print separator
149    let total_width: usize = col_widths.iter().map(|w| *w + 1).sum::<usize>() + 2;
150    println!("{}", "-".repeat(total_width));
151
152    // Print data rows
153    for row in &rows[1..] {
154        // First column left-aligned, rest right-aligned
155        for (i, cell) in row.iter().enumerate() {
156            if i == 0 {
157                print!("{:<width$} ", cell, width = col_widths[i] + 1);
158            } else {
159                print!("{:>width$} ", cell, width = col_widths[i] + 1);
160            }
161        }
162        println!();
163    }
164}
165
166/// Formats an integer with thousands separator.
167#[cfg(feature = "profiling")]
168pub fn format_with_commas(value: usize) -> String {
169    let s = value.to_string();
170    let mut result = String::new();
171    let bytes = s.as_bytes();
172    let len = bytes.len();
173
174    for (i, &b) in bytes.iter().enumerate() {
175        result.push(b as char);
176        let digits_left = len - i - 1;
177        if digits_left > 0 && digits_left.is_multiple_of(3) {
178            result.push(',');
179        }
180    }
181
182    result
183}
184
185/// Formats a float with thousands separator.
186#[cfg(feature = "profiling")]
187pub fn format_with_commas_f64(value: f64) -> String {
188    // Format to two decimal places
189    let formatted = format!("{:.2}", value.abs()); // format positive part only
190    let mut parts = formatted.splitn(2, '.');
191
192    let int_part = parts.next().unwrap_or("");
193    let frac_part = parts.next(); // optional
194
195    // Format integer part with commas
196    let mut result = String::new();
197    let bytes = int_part.as_bytes();
198    let len = bytes.len();
199
200    for (i, &b) in bytes.iter().enumerate() {
201        result.push(b as char);
202        let digits_left = len - i - 1;
203        if digits_left > 0 && digits_left % 3 == 0 {
204            result.push(',');
205        }
206    }
207
208    // Add decimal part
209    if let Some(frac) = frac_part {
210        result.push('.');
211        result.push_str(frac);
212    }
213
214    // Reapply negative sign if needed
215    if value.is_sign_negative() {
216        result.insert(0, '-');
217    }
218
219    result
220}
221
222#[cfg(all(test, feature = "profiling"))]
223mod tests {
224    #![allow(clippy::unreadable_literal)]
225    use std::time::Duration;
226
227    use crate::profiling::display::{
228        format_with_commas, format_with_commas_f64, print_named_counts, print_named_spans,
229    };
230    use crate::profiling::*;
231
232    #[test]
233    fn increments_named_count_correctly() {
234        increment_named_count("display_incr_test_event");
235        increment_named_count("display_incr_test_event");
236        increment_named_count("display_incr_another_event");
237
238        let data = profiling_data();
239        assert_eq!(data.get_named_count("display_incr_test_event"), Some(2));
240        assert_eq!(data.get_named_count("display_incr_another_event"), Some(1));
241    }
242
243    #[test]
244    fn print_named_counts_outputs_expected_format() {
245        // Initialize profiling start_time without mutating it directly
246        increment_named_count("display_event1_print");
247        increment_named_count("display_event1_print");
248        increment_named_count("display_event1_print");
249        increment_named_count("display_event1_print");
250        increment_named_count("display_event1_print");
251        print_named_counts(); // should print the expected format
252    }
253
254    // region Tests for `format_with_commas()`
255    #[test]
256    fn formats_single_digit() {
257        assert_eq!(format_with_commas(7), "7");
258    }
259
260    #[test]
261    fn formats_two_digits() {
262        assert_eq!(format_with_commas(42), "42");
263    }
264
265    #[test]
266    fn formats_three_digits() {
267        assert_eq!(format_with_commas(999), "999");
268    }
269
270    #[test]
271    fn formats_four_digits() {
272        assert_eq!(format_with_commas(1000), "1,000");
273    }
274
275    #[test]
276    fn formats_five_digits() {
277        assert_eq!(format_with_commas(27_171), "27,171");
278    }
279
280    #[test]
281    fn formats_six_digits() {
282        assert_eq!(format_with_commas(123_456), "123,456");
283    }
284
285    #[test]
286    fn formats_seven_digits() {
287        assert_eq!(format_with_commas(1_000_000), "1,000,000");
288    }
289
290    #[test]
291    fn formats_zero() {
292        assert_eq!(format_with_commas(0), "0");
293    }
294
295    #[test]
296    fn formats_large_number() {
297        assert_eq!(format_with_commas(9_876_543_210), "9,876,543,210");
298    }
299
300    // endregion Tests for `format_with_commas()`
301
302    // region Tests for `format_with_commas_f64()`
303    #[test]
304    fn formats_small_integer() {
305        assert_eq!(format_with_commas_f64(7.0), "7.00");
306        assert_eq!(format_with_commas_f64(42.0), "42.00");
307    }
308
309    #[test]
310    fn formats_small_decimal() {
311        #![allow(clippy::approx_constant)]
312        assert_eq!(format_with_commas_f64(3.14), "3.14");
313        assert_eq!(format_with_commas_f64(0.99), "0.99");
314    }
315
316    #[test]
317    fn formats_zero_f64() {
318        assert_eq!(format_with_commas_f64(0.0), "0.00");
319    }
320
321    #[test]
322    fn formats_exact_thousand() {
323        assert_eq!(format_with_commas_f64(1000.0), "1,000.00");
324    }
325
326    #[test]
327    fn formats_large_number_f64() {
328        assert_eq!(format_with_commas_f64(1234567.89), "1,234,567.89");
329        assert_eq!(format_with_commas_f64(123456789.0), "123,456,789.00");
330    }
331
332    #[test]
333    fn formats_number_with_rounding_up() {
334        assert_eq!(format_with_commas_f64(999.999), "1,000.00");
335        assert_eq!(format_with_commas_f64(999999.999), "1,000,000.00");
336    }
337
338    #[test]
339    fn formats_number_with_rounding_down() {
340        assert_eq!(format_with_commas_f64(1234.444), "1,234.44");
341    }
342
343    #[test]
344    fn formats_negative_number() {
345        assert_eq!(format_with_commas_f64(-1234567.89), "-1,234,567.89");
346    }
347
348    #[test]
349    fn formats_negative_rounding_edge() {
350        assert_eq!(format_with_commas_f64(-999.995), "-1,000.00");
351    }
352
353    // endregion Tests for `format_with_commas_f64()`
354
355    #[test]
356    fn print_named_spans_outputs_expected_format() {
357        // Open a span to initialize start_time without mutating it directly
358        {
359            let _init = open_span("display_init_span");
360            std::thread::sleep(Duration::from_millis(10));
361        }
362        // Add sample spans data
363        {
364            let mut container = profiling_data();
365            container
366                .spans
367                .insert("database_query", (Duration::from_millis(1500), 42));
368            container
369                .spans
370                .insert("api_request", (Duration::from_millis(800), 120));
371            container
372                .spans
373                .insert("data_processing", (Duration::from_secs(5), 15));
374            container
375                .spans
376                .insert("file_io", (Duration::from_millis(350), 78));
377            container
378                .spans
379                .insert("rendering", (Duration::from_secs(2), 30));
380        }
381        print_named_spans();
382    }
383
384    #[test]
385    fn test_print_computed_statistics_integration() {
386        use crate::profiling::{add_computed_statistic, increment_named_count};
387        // Use unique labels; avoid clearing shared profiling data
388        increment_named_count("display_metric_integration");
389        increment_named_count("display_metric_integration");
390
391        add_computed_statistic::<usize>(
392            "display_metric_count_integration",
393            "Total metrics",
394            Box::new(|data| data.get_named_count("display_metric_integration")),
395            Box::new(|value| {
396                println!("Metric count: {}", value);
397            }),
398        );
399
400        print_computed_statistics();
401    }
402}