suggest/
metrics.rs

1/* This Source Code Form is subject to the terms of the Mozilla Public
2 * License, v. 2.0. If a copy of the MPL was not distributed with this
3 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
4
5use std::time::Instant;
6
7/// Single sample for a Glean labeled_timing_distribution
8#[derive(uniffi::Record)]
9pub struct LabeledTimingSample {
10    pub label: String,
11    /// Time in microseconds
12    pub value: u64,
13}
14
15impl LabeledTimingSample {
16    fn new(label: String, value: u64) -> Self {
17        Self { label, value }
18    }
19}
20
21/// Ingestion metrics
22///
23/// These are recorded during [crate::Store::ingest] and returned to the consumer to record.
24#[derive(Default, uniffi::Record)]
25pub struct SuggestIngestionMetrics {
26    /// Samples for the `suggest.ingestion_time` metric
27    pub ingestion_times: Vec<LabeledTimingSample>,
28    /// Samples for the `suggest.ingestion_download_time` metric
29    pub download_times: Vec<LabeledTimingSample>,
30}
31
32impl SuggestIngestionMetrics {
33    /// Wraps each iteration in `ingest` and records the time for it.
34    ///
35    /// Passes the closure a `&mut MetricsContext`.
36    pub fn measure_ingest<F, T>(&mut self, record_type: impl Into<String>, operation: F) -> T
37    where
38        F: FnOnce(&mut MetricsContext) -> T,
39    {
40        let timer = Instant::now();
41        let record_type = record_type.into();
42        let mut context = MetricsContext::default();
43        let result = operation(&mut context);
44        let elapsed = timer.elapsed().as_micros() as u64;
45        match context {
46            MetricsContext::Uninstrumented => (),
47            MetricsContext::Instrumented { download_time } => {
48                self.ingestion_times.push(LabeledTimingSample::new(
49                    record_type.clone(),
50                    elapsed - download_time,
51                ));
52                self.download_times
53                    .push(LabeledTimingSample::new(record_type, download_time));
54            }
55        }
56        result
57    }
58}
59
60/// Context for a ingestion measurement
61#[derive(Default)]
62pub enum MetricsContext {
63    /// Default state, if this is the state at the end of `measure_ingest`, then it means no work
64    /// was done and we should not record anything
65    #[default]
66    Uninstrumented,
67    /// State after `measure_download()` is called.  We currently always download an attachment
68    /// whenever we do any ingestion work, so we can use this a test for if we should record
69    /// anything.
70    Instrumented { download_time: u64 },
71}
72
73impl MetricsContext {
74    /// Tracks download times during the ingestion
75    ///
76    /// We report download times as a separate metric [Self::measure_download] can be called
77    /// multiple times.  [DownloadTimer] will track the total time for all calls.
78    pub fn measure_download<F, T>(&mut self, operation: F) -> T
79    where
80        F: FnOnce() -> T,
81    {
82        let timer = Instant::now();
83        let result = operation();
84        let elasped = timer.elapsed().as_micros() as u64;
85        match self {
86            Self::Uninstrumented => {
87                *self = Self::Instrumented {
88                    download_time: elasped,
89                }
90            }
91            Self::Instrumented { download_time } => *download_time += elasped,
92        }
93        result
94    }
95}
96
97/// Query metrics
98///
99/// These are recorded during [crate::Store::query] and returned to the consumer to record.
100#[derive(Default)]
101pub struct SuggestQueryMetrics {
102    pub times: Vec<LabeledTimingSample>,
103}
104
105impl SuggestQueryMetrics {
106    pub fn measure_query<F, T>(&mut self, provider: impl Into<String>, operation: F) -> T
107    where
108        F: FnOnce() -> T,
109    {
110        let provider = provider.into();
111        let timer = Instant::now();
112        // Make sure the compiler doesn't reorder/inline in a way that invalidates this
113        // measurement.
114        let result = std::hint::black_box(operation());
115        let elapsed = timer.elapsed().as_micros() as u64;
116        self.times.push(LabeledTimingSample::new(provider, elapsed));
117        result
118    }
119}