Skip to main content

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")]
168#[must_use]
169pub fn format_with_commas(value: usize) -> String {
170    let s = value.to_string();
171    let mut result = String::new();
172    let bytes = s.as_bytes();
173    let len = bytes.len();
174
175    for (i, &b) in bytes.iter().enumerate() {
176        result.push(b as char);
177        let digits_left = len - i - 1;
178        if digits_left > 0 && digits_left.is_multiple_of(3) {
179            result.push(',');
180        }
181    }
182
183    result
184}
185
186/// Formats a float with thousands separator.
187#[cfg(feature = "profiling")]
188#[must_use]
189pub fn format_with_commas_f64(value: f64) -> String {
190    // Format to two decimal places
191    let formatted = format!("{:.2}", value.abs()); // format positive part only
192    let mut parts = formatted.splitn(2, '.');
193
194    let int_part = parts.next().unwrap_or("");
195    let frac_part = parts.next(); // optional
196
197    // Format integer part with commas
198    let mut result = String::new();
199    let bytes = int_part.as_bytes();
200    let len = bytes.len();
201
202    for (i, &b) in bytes.iter().enumerate() {
203        result.push(b as char);
204        let digits_left = len - i - 1;
205        if digits_left > 0 && digits_left % 3 == 0 {
206            result.push(',');
207        }
208    }
209
210    // Add decimal part
211    if let Some(frac) = frac_part {
212        result.push('.');
213        result.push_str(frac);
214    }
215
216    // Reapply negative sign if needed
217    if value.is_sign_negative() {
218        result.insert(0, '-');
219    }
220
221    result
222}
223
224#[cfg(all(test, feature = "profiling"))]
225mod tests {
226    use std::time::Duration;
227
228    use crate::profiling::display::{
229        format_with_commas, format_with_commas_f64, print_named_counts, print_named_spans,
230    };
231    use crate::profiling::*;
232
233    #[test]
234    fn increments_named_count_correctly() {
235        increment_named_count("display_incr_test_event");
236        increment_named_count("display_incr_test_event");
237        increment_named_count("display_incr_another_event");
238
239        let data = profiling_data();
240        assert_eq!(data.get_named_count("display_incr_test_event"), Some(2));
241        assert_eq!(data.get_named_count("display_incr_another_event"), Some(1));
242    }
243
244    #[test]
245    fn print_named_counts_outputs_expected_format() {
246        // Initialize profiling start_time without mutating it directly
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        increment_named_count("display_event1_print");
252        print_named_counts(); // should print the expected format
253    }
254
255    // region Tests for `format_with_commas()`
256    #[test]
257    fn formats_single_digit() {
258        assert_eq!(format_with_commas(7), "7");
259    }
260
261    #[test]
262    fn formats_two_digits() {
263        assert_eq!(format_with_commas(42), "42");
264    }
265
266    #[test]
267    fn formats_three_digits() {
268        assert_eq!(format_with_commas(999), "999");
269    }
270
271    #[test]
272    fn formats_four_digits() {
273        assert_eq!(format_with_commas(1000), "1,000");
274    }
275
276    #[test]
277    fn formats_five_digits() {
278        assert_eq!(format_with_commas(27_171), "27,171");
279    }
280
281    #[test]
282    fn formats_six_digits() {
283        assert_eq!(format_with_commas(123_456), "123,456");
284    }
285
286    #[test]
287    fn formats_seven_digits() {
288        assert_eq!(format_with_commas(1_000_000), "1,000,000");
289    }
290
291    #[test]
292    fn formats_zero() {
293        assert_eq!(format_with_commas(0), "0");
294    }
295
296    #[test]
297    fn formats_large_number() {
298        assert_eq!(format_with_commas(9_876_543_210), "9,876,543,210");
299    }
300
301    // endregion Tests for `format_with_commas()`
302
303    // region Tests for `format_with_commas_f64()`
304    #[test]
305    fn formats_small_integer() {
306        assert_eq!(format_with_commas_f64(7.0), "7.00");
307        assert_eq!(format_with_commas_f64(42.0), "42.00");
308    }
309
310    #[test]
311    fn formats_small_decimal() {
312        #![allow(clippy::approx_constant)]
313        assert_eq!(format_with_commas_f64(3.14), "3.14");
314        assert_eq!(format_with_commas_f64(0.99), "0.99");
315    }
316
317    #[test]
318    fn formats_zero_f64() {
319        assert_eq!(format_with_commas_f64(0.0), "0.00");
320    }
321
322    #[test]
323    fn formats_exact_thousand() {
324        assert_eq!(format_with_commas_f64(1000.0), "1,000.00");
325    }
326
327    #[test]
328    fn formats_large_number_f64() {
329        assert_eq!(format_with_commas_f64(1234567.89), "1,234,567.89");
330        assert_eq!(format_with_commas_f64(123456789.0), "123,456,789.00");
331    }
332
333    #[test]
334    fn formats_number_with_rounding_up() {
335        assert_eq!(format_with_commas_f64(999.999), "1,000.00");
336        assert_eq!(format_with_commas_f64(999999.999), "1,000,000.00");
337    }
338
339    #[test]
340    fn formats_number_with_rounding_down() {
341        assert_eq!(format_with_commas_f64(1234.444), "1,234.44");
342    }
343
344    #[test]
345    fn formats_negative_number() {
346        assert_eq!(format_with_commas_f64(-1234567.89), "-1,234,567.89");
347    }
348
349    #[test]
350    fn formats_negative_rounding_edge() {
351        assert_eq!(format_with_commas_f64(-999.995), "-1,000.00");
352    }
353
354    // endregion Tests for `format_with_commas_f64()`
355
356    #[test]
357    fn print_named_spans_outputs_expected_format() {
358        // Open a span to initialize start_time without mutating it directly
359        {
360            let _init = open_span("display_init_span");
361            std::thread::sleep(Duration::from_millis(10));
362        }
363        // Add sample spans data
364        {
365            let mut container = profiling_data();
366            container
367                .spans
368                .insert("database_query", (Duration::from_millis(1500), 42));
369            container
370                .spans
371                .insert("api_request", (Duration::from_millis(800), 120));
372            container
373                .spans
374                .insert("data_processing", (Duration::from_secs(5), 15));
375            container
376                .spans
377                .insert("file_io", (Duration::from_millis(350), 78));
378            container
379                .spans
380                .insert("rendering", (Duration::from_secs(2), 30));
381        }
382        print_named_spans();
383    }
384
385    #[test]
386    fn test_print_computed_statistics_integration() {
387        use crate::profiling::{add_computed_statistic, increment_named_count};
388        // Use unique labels; avoid clearing shared profiling data
389        increment_named_count("display_metric_integration");
390        increment_named_count("display_metric_integration");
391
392        add_computed_statistic::<usize>(
393            "display_metric_count_integration",
394            "Total metrics",
395            Box::new(|data| data.get_named_count("display_metric_integration")),
396            Box::new(|value| {
397                println!("Metric count: {}", value);
398            }),
399        );
400
401        print_computed_statistics();
402    }
403}