1#[cfg(feature = "profiling")]
2use std::cell::RefCell;
3#[cfg(feature = "profiling")]
4use std::collections::hash_map::Entry;
5#[cfg(feature = "profiling")]
6use std::ptr::eq;
7#[cfg(feature = "profiling")]
8use std::sync::{Mutex, MutexGuard, OnceLock};
9use std::time::Duration;
10
11use super::computed_statistic::ComputableType;
12#[cfg(feature = "profiling")]
13use super::ProfilingInstant;
14use super::Span;
15#[cfg(feature = "profiling")]
16use super::{
17 ComputedStatistic, ComputedValue, CustomStatisticComputer, CustomStatisticPrinter,
18 TOTAL_MEASURED,
19};
20#[cfg(feature = "profiling")]
21use crate::entity::multi_property::{query_identity_label, QueryIdentityId};
22use crate::HashMap;
23
24#[cfg(feature = "profiling")]
25static PROFILING_DATA: OnceLock<Mutex<ProfilingData>> = OnceLock::new();
26
27#[cfg(feature = "profiling")]
29pub(super) fn profiling_data() -> MutexGuard<'static, ProfilingData> {
30 PROFILING_DATA
31 .get_or_init(|| Mutex::new(ProfilingData::new()))
32 .lock()
33 .unwrap()
34}
35
36#[derive(Default)]
37pub struct ProfilingData {
38 #[cfg(feature = "profiling")]
39 pub start_time: Option<ProfilingInstant>,
40 pub counts: HashMap<&'static str, usize>,
41 pub spans: HashMap<&'static str, (Duration, usize)>,
44 #[cfg(feature = "profiling")]
51 pub(super) open_span_count: usize,
52 #[cfg(feature = "profiling")]
53 pub(super) coverage: Option<ProfilingInstant>,
54 #[cfg(feature = "profiling")]
57 pub(super) computed_statistics: Vec<Option<ComputedStatistic>>,
58}
59
60#[cfg(feature = "profiling")]
65#[derive(Clone, Copy, Debug)]
66pub(crate) struct QueryProfilingData {
67 pub(crate) count: usize,
68 pub(crate) total: Duration,
69 pub(crate) min: Duration,
70 pub(crate) max: Duration,
71}
72
73#[cfg(feature = "profiling")]
74impl QueryProfilingData {
75 fn new(elapsed: Duration) -> Self {
76 Self {
77 count: 1,
78 total: elapsed,
79 min: elapsed,
80 max: elapsed,
81 }
82 }
83
84 fn record(&mut self, elapsed: Duration) {
85 self.count += 1;
86 self.total += elapsed;
87 self.min = self.min.min(elapsed);
88 self.max = self.max.max(elapsed);
89 }
90}
91
92#[cfg(feature = "profiling")]
98#[derive(Default)]
99pub(crate) struct QueryProfiler {
100 timings: RefCell<HashMap<QueryIdentityId, QueryProfilingData>>,
101}
102
103#[cfg(feature = "profiling")]
104impl QueryProfiler {
105 fn record(&self, identity: QueryIdentityId, elapsed: Duration) {
106 let mut timings = self.timings.borrow_mut();
107 match timings.entry(identity) {
108 Entry::Occupied(mut entry) => entry.get_mut().record(elapsed),
109 Entry::Vacant(entry) => {
110 entry.insert(QueryProfilingData::new(elapsed));
111 }
112 }
113 }
114
115 pub(crate) fn snapshot(&self) -> Vec<(&'static str, QueryProfilingData)> {
118 let timings = self.timings.borrow();
119 let mut rows = timings
120 .iter()
121 .map(|(&identity, &data)| (query_identity_label(identity), data))
122 .collect::<Vec<_>>();
123
124 rows.sort_by(|(left_query, left_data), (right_query, right_data)| {
125 right_data
126 .total
127 .cmp(&left_data.total)
128 .then_with(|| left_query.cmp(right_query))
129 });
130 rows
131 }
132
133 #[cfg(test)]
134 pub(crate) fn query_profiling_data(
135 &self,
136 identity: QueryIdentityId,
137 ) -> Option<QueryProfilingData> {
138 self.timings.borrow().get(&identity).copied()
139 }
140}
141
142#[cfg(feature = "profiling")]
148#[derive(Clone, Copy)]
149pub(crate) struct QueryProfileHandle<'a> {
150 profiler: &'a QueryProfiler,
151 identity: QueryIdentityId,
152}
153
154#[cfg(feature = "profiling")]
155impl<'a> QueryProfileHandle<'a> {
156 pub(crate) fn new(profiler: &'a QueryProfiler, identity: QueryIdentityId) -> Self {
157 Self { profiler, identity }
158 }
159
160 fn record(self, elapsed: Duration) {
161 self.profiler.record(self.identity, elapsed);
162 }
163
164 #[must_use]
165 pub(crate) fn scope(self) -> QueryProfileScope<'a> {
166 QueryProfileScope {
167 handle: self,
168 start_time: ProfilingInstant::now(),
169 }
170 }
171
172 pub(crate) fn execution(self) -> QueryExecutionProfile<'a> {
173 QueryExecutionProfile::new(self)
174 }
175}
176
177#[cfg(feature = "profiling")]
178impl PartialEq for QueryProfileHandle<'_> {
179 fn eq(&self, other: &Self) -> bool {
180 eq(self.profiler, other.profiler) && self.identity == other.identity
181 }
182}
183
184#[cfg(feature = "profiling")]
185impl Eq for QueryProfileHandle<'_> {}
186
187#[cfg(feature = "profiling")]
192pub(crate) struct QueryProfileScope<'a> {
193 handle: QueryProfileHandle<'a>,
194 start_time: ProfilingInstant,
195}
196
197#[cfg(feature = "profiling")]
198impl Drop for QueryProfileScope<'_> {
199 fn drop(&mut self) {
200 self.handle.record(self.start_time.elapsed());
201 }
202}
203
204#[cfg(feature = "profiling")]
210pub(crate) struct QueryExecutionProfile<'a> {
211 handle: Option<QueryProfileHandle<'a>>,
212 elapsed: Option<Duration>,
213}
214
215#[cfg(feature = "profiling")]
216impl<'a> QueryExecutionProfile<'a> {
217 fn new(handle: QueryProfileHandle<'a>) -> Self {
218 Self {
219 handle: Some(handle),
220 elapsed: None,
221 }
222 }
223
224 #[must_use]
225 pub(crate) fn scope(&mut self) -> Option<QueryExecutionScope<'_, 'a>> {
226 self.handle.as_ref()?;
227 self.elapsed.get_or_insert(Duration::ZERO);
228 Some(QueryExecutionScope {
229 execution: self,
230 start_time: ProfilingInstant::now(),
231 })
232 }
233
234 pub(crate) fn finish(&mut self) {
235 if let (Some(handle), Some(elapsed)) = (self.handle.take(), self.elapsed.take()) {
236 handle.record(elapsed);
237 }
238 }
239}
240
241#[cfg(feature = "profiling")]
242impl Drop for QueryExecutionProfile<'_> {
243 fn drop(&mut self) {
244 self.finish();
245 }
246}
247
248#[cfg(feature = "profiling")]
253pub(crate) struct QueryExecutionScope<'execution, 'context> {
254 execution: &'execution mut QueryExecutionProfile<'context>,
255 start_time: ProfilingInstant,
256}
257
258#[cfg(feature = "profiling")]
259impl Drop for QueryExecutionScope<'_, '_> {
260 fn drop(&mut self) {
261 *self.execution.elapsed.get_or_insert(Duration::ZERO) += self.start_time.elapsed();
262 }
263}
264
265#[cfg(feature = "profiling")]
266impl ProfilingData {
267 fn new() -> Self {
269 Self::default()
270 }
271
272 pub(super) fn increment_named_count(&mut self, key: &'static str) {
273 self.init_start_time();
274 self.counts.entry(key).and_modify(|v| *v += 1).or_insert(1);
275 }
276
277 pub(super) fn get_named_count(&self, key: &'static str) -> Option<usize> {
278 self.counts.get(&key).copied()
279 }
280
281 fn init_start_time(&mut self) {
282 if self.start_time.is_none() {
283 self.start_time = Some(ProfilingInstant::now());
284 }
285 }
286
287 fn open_span(&mut self, label: &'static str) -> Span {
288 self.init_start_time();
289 if self.open_span_count == 0 {
290 self.coverage = Some(ProfilingInstant::now());
292 }
293 self.open_span_count += 1;
294 Span::new(label)
295 }
296
297 pub(super) fn close_span(&mut self, span: &Span) {
299 if self.open_span_count > 0 {
300 self.open_span_count -= 1;
301 if self.open_span_count == 0 {
302 let coverage = self.coverage.take().unwrap();
305 self.close_span_without_coverage(TOTAL_MEASURED, coverage.elapsed());
306 }
307 }
308 self.close_span_without_coverage(span.label, span.start_time.elapsed());
310 }
311
312 fn close_span_without_coverage(&mut self, label: &'static str, elapsed: Duration) {
314 self.spans
315 .entry(label)
316 .and_modify(|(time, count)| {
317 *time += elapsed;
318 *count += 1;
319 })
320 .or_insert((elapsed, 1));
321 }
322
323 pub(super) fn get_named_counts_table(&self) -> Vec<(String, usize, f64)> {
326 let elapsed = match self.start_time {
327 Some(start_time) => start_time.elapsed().as_secs_f64(),
328 None => 0.0,
329 };
330 let mut rows = vec![];
331
332 for (key, count) in &self.counts {
334 let rate = (*count as f64) / elapsed; rows.push(((*key).to_string(), *count, rate));
337 }
338
339 rows
340 }
341
342 pub(super) fn get_named_spans_table(&self) -> Vec<(String, usize, Duration, f64)> {
345 let elapsed = match self.start_time {
346 Some(start_time) => start_time.elapsed().as_secs_f64(),
347 None => 0.0,
348 };
349
350 let mut rows = vec![];
351
352 for (&label, &(duration, count)) in self.spans.iter().filter(|(k, _)| *k != &TOTAL_MEASURED)
354 {
355 rows.push((
356 label.to_string(),
357 count,
358 duration,
359 duration.as_secs_f64() / elapsed * 100.0,
360 ));
361 }
362
363 if let Some(&(duration, count)) = self.spans.get(&TOTAL_MEASURED) {
365 rows.push((
366 TOTAL_MEASURED.to_string(),
367 count,
368 duration,
369 duration.as_secs_f64() / elapsed * 100.0,
370 ));
371 }
372
373 rows
374 }
375
376 pub(super) fn add_computed_statistic<T: ComputableType>(
377 &mut self,
378 label: &'static str,
379 description: &'static str,
380 computer: CustomStatisticComputer<T>,
381 printer: CustomStatisticPrinter<T>,
382 ) {
383 let computed_stat = ComputedStatistic {
384 label,
385 description,
386 value: None,
387 functions: T::new_functions(computer, printer),
388 };
389 self.computed_statistics.push(Some(computed_stat));
390 }
391}
392
393#[cfg(feature = "profiling")]
394pub fn increment_named_count(key: &'static str) {
395 let mut container = profiling_data();
396 container.increment_named_count(key);
397}
398
399#[cfg(not(feature = "profiling"))]
400pub fn increment_named_count(_key: &'static str) {}
401
402#[cfg(feature = "profiling")]
403#[must_use]
404pub fn open_span(label: &'static str) -> Span {
405 let mut container = profiling_data();
406 container.open_span(label)
407}
408
409#[cfg(not(feature = "profiling"))]
410#[must_use]
411pub fn open_span(label: &'static str) -> Span {
412 Span::new(label)
413}
414
415pub fn close_span(_span: Span) {
418 }
421
422#[cfg(all(test, feature = "profiling"))]
423mod tests {
424 use std::time::Duration;
425
426 use super::*;
427 use crate::entity::multi_property::test_query_identity;
428 use crate::profiling::{get_profiling_data, increment_named_count};
429
430 #[test]
431 fn test_span_basic() {
432 {
433 let _span = open_span("test_operation_basic");
434 std::thread::sleep(Duration::from_millis(10));
435 }
436
437 let data = get_profiling_data();
438 assert!(data.spans.contains_key("test_operation_basic"));
439 let (duration, count) = data.spans.get("test_operation_basic").unwrap();
440 assert_eq!(*count, 1);
441 assert!(*duration >= Duration::from_millis(10));
442 }
443
444 #[test]
445 fn test_span_multiple_calls() {
446 for _ in 0..5 {
447 let _span = open_span("repeated_operation_multi_test");
448 std::thread::sleep(Duration::from_millis(5));
449 }
450
451 let data = get_profiling_data();
452 let (duration, count) = data.spans.get("repeated_operation_multi_test").unwrap();
453 assert!(*count >= 4, "expected at least 4 drops, got {}", count);
454 assert!(*duration >= Duration::from_millis(15));
455 }
456
457 #[test]
458 fn test_span_explicit_close() {
459 let span = open_span("explicit_close_test");
460 std::thread::sleep(Duration::from_millis(10));
461 close_span(span);
462
463 let data = get_profiling_data();
464 assert!(data.spans.contains_key("explicit_close_test"));
465 }
466
467 #[test]
468 fn test_span_nesting() {
469 {
470 let _outer = open_span("outer_nesting_test");
471 std::thread::sleep(Duration::from_millis(5));
472 {
473 let _inner = open_span("inner_nesting_test");
474 std::thread::sleep(Duration::from_millis(5));
475 }
476 std::thread::sleep(Duration::from_millis(5));
477 }
478
479 let data = get_profiling_data();
480 assert!(data.spans.contains_key("outer_nesting_test"));
481 assert!(data.spans.contains_key("inner_nesting_test"));
482
483 let (outer_duration, _) = data.spans.get("outer_nesting_test").unwrap();
484 let (inner_duration, _) = data.spans.get("inner_nesting_test").unwrap();
485
486 assert!(*outer_duration > *inner_duration);
487 assert!(*outer_duration >= Duration::from_millis(15));
488 assert!(*inner_duration >= Duration::from_millis(5));
489 }
490
491 #[test]
492 fn test_total_measured_span() {
493 {
494 let _span1 = open_span("operation1_total_measured");
495 std::thread::sleep(Duration::from_millis(10));
496 }
497
498 std::thread::sleep(Duration::from_millis(5));
499
500 {
501 let _span2 = open_span("operation2_total_measured");
502 std::thread::sleep(Duration::from_millis(10));
503 }
504
505 let data = get_profiling_data();
506
507 assert!(data.spans.contains_key("operation1_total_measured"));
509 assert!(data.spans.contains_key("operation2_total_measured"));
510
511 let (duration1, _) = data.spans.get("operation1_total_measured").unwrap();
512 let (duration2, _) = data.spans.get("operation2_total_measured").unwrap();
513
514 assert!(*duration1 >= Duration::from_millis(10));
515 assert!(*duration2 >= Duration::from_millis(10));
516 }
517
518 #[test]
519 fn test_get_named_counts_table() {
520 let container_start = {
522 let data = get_profiling_data();
523 data.start_time
524 };
525 increment_named_count("event_a_counts_table_test");
526 increment_named_count("event_a_counts_table_test");
527 increment_named_count("event_b_counts_table_test");
528
529 std::thread::sleep(Duration::from_millis(100));
531
532 let elapsed = if let Some(start_time) = container_start {
534 start_time.elapsed().as_secs_f64()
535 } else {
536 0.1
540 };
541
542 let data = get_profiling_data();
543 let table = data.get_named_counts_table();
544
545 let event_a = table
547 .iter()
548 .find(|(label, _, _)| label == "event_a_counts_table_test");
549 assert!(event_a.is_some());
550 let (_, count, rate) = event_a.unwrap();
551 assert_eq!(*count, 2);
552 let expected_rate = 2.0 / elapsed;
554
555 assert!(*rate > expected_rate * 0.9 && *rate < expected_rate * 1.1);
557
558 let event_b = table
559 .iter()
560 .find(|(label, _, _)| label == "event_b_counts_table_test");
561 assert!(event_b.is_some());
562 let (_, count, _) = event_b.unwrap();
563 assert_eq!(*count, 1);
564 }
565
566 #[test]
567 fn test_get_named_spans_table() {
568 let container_start = {
570 let data = get_profiling_data();
571 data.start_time
572 };
573
574 {
575 let _span = open_span("test_span_table");
576 std::thread::sleep(Duration::from_millis(100));
577 }
578
579 std::thread::sleep(Duration::from_millis(100));
580
581 let data = get_profiling_data();
582 let table = data.get_named_spans_table();
583
584 assert!(table.len() >= 2);
585
586 let test_span = table
587 .iter()
588 .find(|(label, _, _, _)| label == "test_span_table");
589 assert!(test_span.is_some());
590
591 let last = table.last().unwrap();
592 assert_eq!(last.0, "Total Measured");
593
594 let (_, _, _, percent) = test_span.unwrap();
595 let elapsed = if let Some(start_time) = container_start {
597 start_time.elapsed().as_secs_f64()
598 } else {
599 0.2
601 };
602 let (duration, _) = data.spans.get("test_span_table").unwrap();
603 let expected_percent = duration.as_secs_f64() / elapsed * 100.0;
604 assert!((*percent - expected_percent).abs() < 5.0);
606 }
607
608 #[test]
609 fn query_profiler_first_observation_initializes_record() {
610 let profiler = QueryProfiler::default();
611 let identity = test_query_identity("QueryTimingInit: (Age)");
612 profiler.record(identity, Duration::from_micros(10));
613
614 let data = profiler.query_profiling_data(identity).unwrap();
615 assert_eq!(data.count, 1);
616 assert_eq!(data.total, Duration::from_micros(10));
617 assert_eq!(data.min, Duration::from_micros(10));
618 assert_eq!(data.max, Duration::from_micros(10));
619 }
620
621 #[test]
622 fn query_profiler_later_observations_update_aggregate() {
623 let profiler = QueryProfiler::default();
624 let identity = test_query_identity("QueryTimingUpdate: (Age)");
625 profiler.record(identity, Duration::from_micros(10));
626 profiler.record(identity, Duration::from_micros(30));
627 profiler.record(identity, Duration::from_micros(5));
628
629 let data = profiler.query_profiling_data(identity).unwrap();
630 assert_eq!(data.count, 3);
631 assert_eq!(data.total, Duration::from_micros(45));
632 assert_eq!(data.min, Duration::from_micros(5));
633 assert_eq!(data.max, Duration::from_micros(30));
634 }
635
636 #[test]
637 fn query_profiler_snapshot_includes_count_total_min_and_max() {
638 let profiler = QueryProfiler::default();
639 let identity = test_query_identity("QueryTimingTable: (Age)");
640 profiler.record(identity, Duration::from_millis(10));
641 profiler.record(identity, Duration::from_millis(30));
642
643 let snapshot = profiler.snapshot();
644 let (_, data) = snapshot
645 .iter()
646 .find(|(query, _)| *query == "QueryTimingTable: (Age)")
647 .unwrap();
648
649 assert_eq!(data.count, 2);
650 assert_eq!(data.total, Duration::from_millis(40));
651 assert_eq!(data.min, Duration::from_millis(10));
652 assert_eq!(data.max, Duration::from_millis(30));
653 }
654
655 #[test]
656 fn query_profiler_same_identity_aggregates_into_one_row() {
657 let profiler = QueryProfiler::default();
658 let identity = test_query_identity("QueryTimingSame: (Age)");
659 profiler.record(identity, Duration::from_micros(10));
660 profiler.record(identity, Duration::from_micros(30));
661 profiler.record(identity, Duration::from_micros(5));
662
663 let data = profiler.query_profiling_data(identity).unwrap();
664 assert_eq!(data.count, 3);
665 assert_eq!(data.total, Duration::from_micros(45));
666 }
667
668 #[test]
669 fn query_profiler_snapshot_sorts_by_total_then_label() {
670 let profiler = QueryProfiler::default();
671 let b = test_query_identity("QueryTimingSort: B");
672 let c = test_query_identity("QueryTimingSort: C");
673 let a = test_query_identity("QueryTimingSort: A");
674 profiler.record(b, Duration::from_micros(20));
675 profiler.record(c, Duration::from_micros(10));
676 profiler.record(a, Duration::from_micros(20));
677
678 let snapshot = profiler.snapshot();
679 let labels = snapshot.iter().map(|(query, _)| *query).collect::<Vec<_>>();
680
681 assert_eq!(
682 labels,
683 vec![
684 "QueryTimingSort: A",
685 "QueryTimingSort: B",
686 "QueryTimingSort: C"
687 ]
688 );
689 }
690
691 #[test]
692 fn query_profile_scope_records_on_drop() {
693 let profiler = QueryProfiler::default();
694 let identity = test_query_identity("QueryProfileScope: (Age)");
695 {
696 let _scope = QueryProfileHandle::new(&profiler, identity).scope();
697 }
698
699 let data = profiler.query_profiling_data(identity).unwrap();
700 assert_eq!(data.count, 1);
701 }
702
703 #[test]
704 fn query_profile_handle_equality_uses_profiler_and_identity() {
705 fn requires_eq<T: Eq>(_value: T) {}
706
707 let first_profiler = QueryProfiler::default();
708 let second_profiler = QueryProfiler::default();
709 let first_identity = test_query_identity("QueryHandleEquality: A");
710 let second_identity = test_query_identity("QueryHandleEquality: B");
711
712 let first = QueryProfileHandle::new(&first_profiler, first_identity);
713 let same = QueryProfileHandle::new(&first_profiler, first_identity);
714 let different_identity = QueryProfileHandle::new(&first_profiler, second_identity);
715 let different_profiler = QueryProfileHandle::new(&second_profiler, first_identity);
716
717 assert!(first == same);
718 assert!(first != different_identity);
719 assert!(first != different_profiler);
720 requires_eq(first);
721 }
722
723 #[test]
724 fn query_execution_scopes_accumulate_into_one_observation() {
725 let profiler = QueryProfiler::default();
726 let identity = test_query_identity("QueryExecutionScopes: (Age)");
727 {
728 let mut execution = QueryProfileHandle::new(&profiler, identity).execution();
729 drop(execution.scope());
730 drop(execution.scope());
731 }
732
733 let data = profiler.query_profiling_data(identity).unwrap();
734 assert_eq!(data.count, 1);
735 }
736
737 #[test]
738 fn unused_query_execution_records_nothing() {
739 let profiler = QueryProfiler::default();
740 let identity = test_query_identity("UnusedQueryExecution: (Age)");
741 drop(QueryProfileHandle::new(&profiler, identity).execution());
742
743 assert!(profiler.query_profiling_data(identity).is_none());
744 }
745
746 #[test]
747 fn explicitly_finished_query_execution_records_only_once() {
748 let profiler = QueryProfiler::default();
749 let identity = test_query_identity("FinishedQueryExecution: (Age)");
750 {
751 let mut execution = QueryProfileHandle::new(&profiler, identity).execution();
752 drop(execution.scope());
753 execution.finish();
754 }
755
756 let data = profiler.query_profiling_data(identity).unwrap();
757 assert_eq!(data.count, 1);
758 }
759
760 #[test]
761 fn finished_query_execution_cannot_open_another_scope() {
762 let profiler = QueryProfiler::default();
763 let identity = test_query_identity("NoScopeAfterFinish: (Age)");
764 let mut execution = QueryProfileHandle::new(&profiler, identity).execution();
765 drop(execution.scope());
766 execution.finish();
767
768 assert!(execution.scope().is_none());
769 drop(execution);
770 let data = profiler.query_profiling_data(identity).unwrap();
771 assert_eq!(data.count, 1);
772 }
773
774 #[test]
775 fn panicking_query_execution_scope_records_elapsed_work() {
776 use std::panic::{catch_unwind, AssertUnwindSafe};
777
778 let profiler = QueryProfiler::default();
779 let identity = test_query_identity("PanickingQueryExecution: (Age)");
780 let result = catch_unwind(AssertUnwindSafe(|| {
781 let mut execution = QueryProfileHandle::new(&profiler, identity).execution();
782 let _scope = execution.scope();
783 panic!("end the measured work slice");
784 }));
785
786 assert!(result.is_err());
787 let data = profiler.query_profiling_data(identity).unwrap();
788 assert_eq!(data.count, 1);
789 }
790}