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#[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#[cfg(feature = "profiling")]
26pub fn print_named_counts() {
27 let container = profiling_data();
28 if container.counts.is_empty() {
29 return;
31 }
32 let rows = container.get_named_counts_table();
33
34 let mut formatted_rows = vec![
35 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#[cfg(feature = "profiling")]
59pub fn print_named_spans() {
60 let rows = profiling_data().get_named_spans_table();
61 if rows.is_empty() {
62 return;
64 }
65
66 let mut formatted_rows = vec![
67 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 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#[cfg(feature = "profiling")]
123pub fn print_computed_statistics() {
124 let mut container = profiling_data();
125
126 let stat_count = container.computed_statistics.len();
128 if stat_count == 0 {
129 return;
130 }
131 for idx in 0..stat_count {
132 let mut statistic = container.computed_statistics[idx].take().unwrap();
134 statistic.value = statistic.functions.compute(&container);
135 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#[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 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 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 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#[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#[cfg(feature = "profiling")]
225#[must_use]
226pub fn format_with_commas_f64(value: f64) -> String {
227 let formatted = format!("{:.2}", value.abs()); let mut parts = formatted.splitn(2, '.');
230
231 let int_part = parts.next().unwrap_or("");
232 let frac_part = parts.next(); 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 if let Some(frac) = frac_part {
249 result.push('.');
250 result.push_str(frac);
251 }
252
253 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 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(); }
292
293 #[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 #[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 #[test]
395 fn print_named_spans_outputs_expected_format() {
396 {
398 let _init = open_span("display_init_span");
399 std::thread::sleep(Duration::from_millis(10));
400 }
401 {
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 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}