Skip to main content

ixa/profiling/
display.rs

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