Skip to main content

ixa/profiling/
data.rs

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/// Acquires an exclusive lock on the profiling data, blocking until it's available.
28#[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    // We store span counts with the span duration, because they are updated when
42    // the spans are and displayed with the spans rather than with the other counts.
43    pub spans: HashMap<&'static str, (Duration, usize)>,
44    // The number of spans that are currently open. We use this and the `total_measured` span to
45    // compute the amount of time accounted for by all the spans. This together with total
46    // runtime can tell you if there is significant runtime not accounted for by the existing
47    // spans. When `open_span_count` transitions from `0`, the `total_measured` span is opened.
48    // When `open_span_count` transitions back to `0`, `total_measured` is closed and duration
49    // is recorded.
50    #[cfg(feature = "profiling")]
51    pub(super) open_span_count: usize,
52    #[cfg(feature = "profiling")]
53    pub(super) coverage: Option<ProfilingInstant>,
54    // Custom computed statistics. They are wrapped in an `Option` to allow for temporarily
55    // removing them to avoid a double borrow.
56    #[cfg(feature = "profiling")]
57    pub(super) computed_statistics: Vec<Option<ComputedStatistic>>,
58}
59
60/// Aggregate profiling data for all committed executions of one query shape.
61///
62/// [`QueryProfiler`] stores one value per query identity and updates it
63/// whenever a completed query execution is recorded.
64#[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/// Context-owned storage for aggregate query-profiling data.
93///
94/// Recording handles submit completed query identities and elapsed durations to
95/// this collection. Interior mutability permits profiling through query APIs
96/// that borrow the owning context immutably.
97#[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    /// Returns `(query label, aggregate profiling data)` pairs sorted by
116    /// descending total duration and then ascending query label.
117    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/// Copyable recording destination for one query shape in one context.
143///
144/// Carrying this handle does not record an execution. Contiguous timing scopes
145/// and discontinuous execution profiles use it to commit completed elapsed
146/// durations to the owning [`QueryProfiler`].
147#[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/// RAII guard that records one continuously measured query execution.
188///
189/// The guard starts timing when constructed and commits one elapsed duration
190/// through its [`QueryProfileHandle`] when dropped.
191#[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/// Accumulates discontinuous query work for one lazy iterator execution.
205///
206/// Each [`QueryExecutionScope`] adds one elapsed work slice. [`Self::finish`]
207/// or `Drop` commits the accumulated duration once; an execution that never
208/// opened a work scope records nothing.
209#[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/// RAII guard for one timed work slice in a discontinuous query execution.
249///
250/// Dropping the guard adds the slice's elapsed duration to its
251/// [`QueryExecutionProfile`]. It does not commit an aggregate observation.
252#[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    /// Initialize a new `ProfilingData`.
268    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            // Start recording coverage time.
291            self.coverage = Some(ProfilingInstant::now());
292        }
293        self.open_span_count += 1;
294        Span::new(label)
295    }
296
297    /// Do not call directly. This method is called from `Span::drop`.
298    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                // Stop recording coverage time. The `total_measured` must be `Some(..)` if
303                // `open_span_count` was nonzero, so unwrap always succeeds.
304                let coverage = self.coverage.take().unwrap();
305                self.close_span_without_coverage(TOTAL_MEASURED, coverage.elapsed());
306            }
307        }
308        // Always record the span itself.
309        self.close_span_without_coverage(span.label, span.start_time.elapsed());
310    }
311
312    /// Closes the span without checking the coverage span.
313    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    /// Constructs a table of ("Event Label", "Count", "Rate (per sec)"). Used to print
324    /// stats to the console and write the stats to a file.
325    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        // Collect data rows
333        for (key, count) in &self.counts {
334            let rate = (*count as f64) / elapsed; // Just allow this to be `inf`/`nan` if `elapsed == 0.0`.
335
336            rows.push(((*key).to_string(), *count, rate));
337        }
338
339        rows
340    }
341
342    /// Constructs a table of "Span Label", "Count", "Duration", "% runtime". Used to print
343    /// stats to the console and write the stats to a file.
344    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        // Add all regular span rows
353        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        // Add the "Total measured" row at the end
364        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
415/// Call this if you want to explicitly close a span before the end of the scope in which the
416/// span was defined. Equivalent to `span.drop()`.
417pub fn close_span(_span: Span) {
418    // The `span` is dropped here, and `ProfilingData::close_span` is called
419    // from `Span::drop`. Incidentally, this is the same implementation as `span.drop()`!
420}
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        // Just verify our specific spans exist
508        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        // Capture container start_time before adding counts
521        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        // Sleep to ensure measurable time has passed
530        std::thread::sleep(Duration::from_millis(100));
531
532        // Use the same origin as container rate calculation; if None, fall back to local start
533        let elapsed = if let Some(start_time) = container_start {
534            start_time.elapsed().as_secs_f64()
535        } else {
536            // If profiling hasn't started yet, rate will be based on init at first increment,
537            // so approximate the elapsed time from the first increment call.
538            // In practice, this path should rarely trigger.
539            0.1
540        };
541
542        let data = get_profiling_data();
543        let table = data.get_named_counts_table();
544
545        // Find our specific events instead of checking total table length
546        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        // Rate should be approximately 2/elapsed (2 events / ~0.1 second = ~20/sec)
553        let expected_rate = 2.0 / elapsed;
554
555        // Allow 10% margin for timing variations
556        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        // Capture container start time without mutating it
569        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        // Compute expected percent from container start time
596        let elapsed = if let Some(start_time) = container_start {
597            start_time.elapsed().as_secs_f64()
598        } else {
599            // If profiling hasn't started yet, approximate with 0.2s total elapsed (100ms span + 100ms idle)
600            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        // Allow reasonable tolerance for timing variations
605        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}