1#[cfg(feature = "profiling")]
2use std::sync::{Mutex, MutexGuard, OnceLock};
3use std::time::{Duration, Instant};
4
5use super::computed_statistic::ComputableType;
6use super::Span;
7#[cfg(feature = "profiling")]
8use super::{
9 ComputedStatistic, ComputedValue, CustomStatisticComputer, CustomStatisticPrinter,
10 TOTAL_MEASURED,
11};
12use crate::HashMap;
13
14#[cfg(feature = "profiling")]
15static PROFILING_DATA: OnceLock<Mutex<ProfilingData>> = OnceLock::new();
16
17#[cfg(feature = "profiling")]
19pub(super) fn profiling_data() -> MutexGuard<'static, ProfilingData> {
20 PROFILING_DATA
21 .get_or_init(|| Mutex::new(ProfilingData::new()))
22 .lock()
23 .unwrap()
24}
25
26#[derive(Default)]
27pub struct ProfilingData {
28 pub start_time: Option<Instant>,
29 pub counts: HashMap<&'static str, usize>,
30 pub spans: HashMap<&'static str, (Duration, usize)>,
33 #[cfg(feature = "profiling")]
40 pub(super) open_span_count: usize,
41 #[cfg(feature = "profiling")]
42 pub(super) coverage: Option<Instant>,
43 #[cfg(feature = "profiling")]
46 pub(super) computed_statistics: Vec<Option<ComputedStatistic>>,
47}
48
49#[cfg(feature = "profiling")]
50impl ProfilingData {
51 fn new() -> Self {
53 Self::default()
54 }
55
56 pub(super) fn increment_named_count(&mut self, key: &'static str) {
57 self.init_start_time();
58 self.counts.entry(key).and_modify(|v| *v += 1).or_insert(1);
59 }
60
61 pub(super) fn get_named_count(&self, key: &'static str) -> Option<usize> {
62 self.counts.get(&key).copied()
63 }
64
65 fn init_start_time(&mut self) {
66 if self.start_time.is_none() {
67 self.start_time = Some(Instant::now());
68 }
69 }
70
71 fn open_span(&mut self, label: &'static str) -> Span {
72 self.init_start_time();
73 if self.open_span_count == 0 {
74 self.coverage = Some(Instant::now());
76 }
77 self.open_span_count += 1;
78 Span::new(label)
79 }
80
81 pub(super) fn close_span(&mut self, span: &Span) {
83 if self.open_span_count > 0 {
84 self.open_span_count -= 1;
85 if self.open_span_count == 0 {
86 let coverage = self.coverage.take().unwrap();
89 self.close_span_without_coverage(TOTAL_MEASURED, coverage.elapsed());
90 }
91 }
92 self.close_span_without_coverage(span.label, span.start_time.elapsed());
94 }
95
96 fn close_span_without_coverage(&mut self, label: &'static str, elapsed: Duration) {
98 self.spans
99 .entry(label)
100 .and_modify(|(time, count)| {
101 *time += elapsed;
102 *count += 1;
103 })
104 .or_insert((elapsed, 1));
105 }
106
107 pub(super) fn get_named_counts_table(&self) -> Vec<(String, usize, f64)> {
110 let elapsed = match self.start_time {
111 Some(start_time) => start_time.elapsed().as_secs_f64(),
112 None => 0.0,
113 };
114 let mut rows = vec![];
115
116 for (key, count) in &self.counts {
118 let rate = (*count as f64) / elapsed; rows.push(((*key).to_string(), *count, rate));
121 }
122
123 rows
124 }
125
126 pub(super) fn get_named_spans_table(&self) -> Vec<(String, usize, Duration, f64)> {
129 let elapsed = match self.start_time {
130 Some(start_time) => start_time.elapsed().as_secs_f64(),
131 None => 0.0,
132 };
133
134 let mut rows = vec![];
135
136 for (&label, &(duration, count)) in self.spans.iter().filter(|(k, _)| *k != &TOTAL_MEASURED)
138 {
139 rows.push((
140 label.to_string(),
141 count,
142 duration,
143 duration.as_secs_f64() / elapsed * 100.0,
144 ));
145 }
146
147 if let Some(&(duration, count)) = self.spans.get(&TOTAL_MEASURED) {
149 rows.push((
150 TOTAL_MEASURED.to_string(),
151 count,
152 duration,
153 duration.as_secs_f64() / elapsed * 100.0,
154 ));
155 }
156
157 rows
158 }
159
160 pub(super) fn add_computed_statistic<T: ComputableType>(
161 &mut self,
162 label: &'static str,
163 description: &'static str,
164 computer: CustomStatisticComputer<T>,
165 printer: CustomStatisticPrinter<T>,
166 ) {
167 let computed_stat = ComputedStatistic {
168 label,
169 description,
170 value: None,
171 functions: T::new_functions(computer, printer),
172 };
173 self.computed_statistics.push(Some(computed_stat));
174 }
175}
176
177#[cfg(feature = "profiling")]
178pub fn increment_named_count(key: &'static str) {
179 let mut container = profiling_data();
180 container.increment_named_count(key);
181}
182
183#[cfg(not(feature = "profiling"))]
184pub fn increment_named_count(_key: &'static str) {}
185
186#[cfg(feature = "profiling")]
187#[must_use]
188pub fn open_span(label: &'static str) -> Span {
189 let mut container = profiling_data();
190 container.open_span(label)
191}
192
193#[cfg(not(feature = "profiling"))]
194#[must_use]
195pub fn open_span(label: &'static str) -> Span {
196 Span::new(label)
197}
198
199pub fn close_span(_span: Span) {
202 }
205
206#[cfg(all(test, feature = "profiling"))]
207mod tests {
208 use std::time::Duration;
209
210 use super::*;
211 use crate::profiling::{get_profiling_data, increment_named_count};
212
213 #[test]
214 fn test_span_basic() {
215 {
216 let _span = open_span("test_operation_basic");
217 std::thread::sleep(Duration::from_millis(10));
218 }
219
220 let data = get_profiling_data();
221 assert!(data.spans.contains_key("test_operation_basic"));
222 let (duration, count) = data.spans.get("test_operation_basic").unwrap();
223 assert_eq!(*count, 1);
224 assert!(*duration >= Duration::from_millis(10));
225 }
226
227 #[test]
228 fn test_span_multiple_calls() {
229 for _ in 0..5 {
230 let _span = open_span("repeated_operation_multi_test");
231 std::thread::sleep(Duration::from_millis(5));
232 }
233
234 let data = get_profiling_data();
235 let (duration, count) = data.spans.get("repeated_operation_multi_test").unwrap();
236 assert!(*count >= 4, "expected at least 4 drops, got {}", count);
237 assert!(*duration >= Duration::from_millis(15));
238 }
239
240 #[test]
241 fn test_span_explicit_close() {
242 let span = open_span("explicit_close_test");
243 std::thread::sleep(Duration::from_millis(10));
244 close_span(span);
245
246 let data = get_profiling_data();
247 assert!(data.spans.contains_key("explicit_close_test"));
248 }
249
250 #[test]
251 fn test_span_nesting() {
252 {
253 let _outer = open_span("outer_nesting_test");
254 std::thread::sleep(Duration::from_millis(5));
255 {
256 let _inner = open_span("inner_nesting_test");
257 std::thread::sleep(Duration::from_millis(5));
258 }
259 std::thread::sleep(Duration::from_millis(5));
260 }
261
262 let data = get_profiling_data();
263 assert!(data.spans.contains_key("outer_nesting_test"));
264 assert!(data.spans.contains_key("inner_nesting_test"));
265
266 let (outer_duration, _) = data.spans.get("outer_nesting_test").unwrap();
267 let (inner_duration, _) = data.spans.get("inner_nesting_test").unwrap();
268
269 assert!(*outer_duration > *inner_duration);
270 assert!(*outer_duration >= Duration::from_millis(15));
271 assert!(*inner_duration >= Duration::from_millis(5));
272 }
273
274 #[test]
275 fn test_total_measured_span() {
276 {
277 let _span1 = open_span("operation1_total_measured");
278 std::thread::sleep(Duration::from_millis(10));
279 }
280
281 std::thread::sleep(Duration::from_millis(5));
282
283 {
284 let _span2 = open_span("operation2_total_measured");
285 std::thread::sleep(Duration::from_millis(10));
286 }
287
288 let data = get_profiling_data();
289
290 assert!(data.spans.contains_key("operation1_total_measured"));
292 assert!(data.spans.contains_key("operation2_total_measured"));
293
294 let (duration1, _) = data.spans.get("operation1_total_measured").unwrap();
295 let (duration2, _) = data.spans.get("operation2_total_measured").unwrap();
296
297 assert!(*duration1 >= Duration::from_millis(10));
298 assert!(*duration2 >= Duration::from_millis(10));
299 }
300
301 #[test]
302 fn test_get_named_counts_table() {
303 let container_start = {
305 let data = get_profiling_data();
306 data.start_time
307 };
308 increment_named_count("event_a_counts_table_test");
309 increment_named_count("event_a_counts_table_test");
310 increment_named_count("event_b_counts_table_test");
311
312 std::thread::sleep(Duration::from_millis(100));
314
315 let elapsed = if let Some(start_time) = container_start {
317 start_time.elapsed().as_secs_f64()
318 } else {
319 0.1
323 };
324
325 let data = get_profiling_data();
326 let table = data.get_named_counts_table();
327
328 let event_a = table
330 .iter()
331 .find(|(label, _, _)| label == "event_a_counts_table_test");
332 assert!(event_a.is_some());
333 let (_, count, rate) = event_a.unwrap();
334 assert_eq!(*count, 2);
335 let expected_rate = 2.0 / elapsed;
337
338 assert!(*rate > expected_rate * 0.9 && *rate < expected_rate * 1.1);
340
341 let event_b = table
342 .iter()
343 .find(|(label, _, _)| label == "event_b_counts_table_test");
344 assert!(event_b.is_some());
345 let (_, count, _) = event_b.unwrap();
346 assert_eq!(*count, 1);
347 }
348
349 #[test]
350 fn test_get_named_spans_table() {
351 let container_start = {
353 let data = get_profiling_data();
354 data.start_time
355 };
356
357 {
358 let _span = open_span("test_span_table");
359 std::thread::sleep(Duration::from_millis(100));
360 }
361
362 std::thread::sleep(Duration::from_millis(100));
363
364 let data = get_profiling_data();
365 let table = data.get_named_spans_table();
366
367 assert!(table.len() >= 2);
368
369 let test_span = table
370 .iter()
371 .find(|(label, _, _, _)| label == "test_span_table");
372 assert!(test_span.is_some());
373
374 let last = table.last().unwrap();
375 assert_eq!(last.0, "Total Measured");
376
377 let (_, _, _, percent) = test_span.unwrap();
378 let elapsed = if let Some(start_time) = container_start {
380 start_time.elapsed().as_secs_f64()
381 } else {
382 0.2
384 };
385 let (duration, _) = data.spans.get("test_span_table").unwrap();
386 let expected_percent = duration.as_secs_f64() / elapsed * 100.0;
387 assert!((*percent - expected_percent).abs() < 5.0);
389 }
390}