glean_core/metrics/
timespan.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 https://mozilla.org/MPL/2.0/.
4
5use std::sync::{Arc, RwLock};
6use std::time::Duration;
7
8use crate::common_metric_data::CommonMetricDataInternal;
9use crate::error_recording::{record_error, test_get_num_recorded_errors, ErrorType};
10use crate::metrics::time_unit::TimeUnit;
11use crate::metrics::Metric;
12use crate::metrics::MetricType;
13use crate::storage::StorageManager;
14use crate::Glean;
15use crate::{CommonMetricData, TestGetValue};
16
17/// A timespan metric.
18///
19/// Timespans are used to make a measurement of how much time is spent in a particular task.
20///
21// Implementation note:
22// Because we dispatch this, we handle this with interior mutability.
23// The whole struct is clonable, but that's comparable cheap, as it does not clone the data.
24// Cloning `CommonMetricData` is not free, as it contains strings, so we also wrap that in an Arc.
25#[derive(Clone, Debug)]
26pub struct TimespanMetric {
27    meta: Arc<CommonMetricDataInternal>,
28    time_unit: TimeUnit,
29    start_time: Arc<RwLock<Option<u64>>>,
30}
31
32impl MetricType for TimespanMetric {
33    fn meta(&self) -> &CommonMetricDataInternal {
34        &self.meta
35    }
36}
37
38// IMPORTANT:
39//
40// When changing this implementation, make sure all the operations are
41// also declared in the related trait in `../traits/`.
42impl TimespanMetric {
43    /// Creates a new timespan metric.
44    pub fn new(meta: CommonMetricData, time_unit: TimeUnit) -> Self {
45        Self {
46            meta: Arc::new(meta.into()),
47            time_unit,
48            start_time: Arc::new(RwLock::new(None)),
49        }
50    }
51
52    /// Starts tracking time for the provided metric.
53    ///
54    /// This records an error if it's already tracking time (i.e. start was
55    /// already called with no corresponding
56    /// [`set_stop`](TimespanMetric::set_stop)): in that case the original start
57    /// time will be preserved.
58    pub fn start(&self) {
59        let start_time = zeitstempel::now();
60
61        let metric = self.clone();
62        crate::launch_with_glean(move |glean| metric.set_start(glean, start_time));
63    }
64
65    /// Set start time synchronously.
66    #[doc(hidden)]
67    pub fn set_start(&self, glean: &Glean, start_time: u64) {
68        let mut lock = self
69            .start_time
70            .write()
71            .expect("Lock poisoned for timespan metric on start.");
72
73        if lock.is_some() {
74            record_error(
75                glean,
76                &self.meta,
77                ErrorType::InvalidState,
78                "Timespan already started",
79                None,
80            );
81            return;
82        }
83
84        *lock = Some(start_time);
85    }
86
87    /// Stops tracking time for the provided metric. Sets the metric to the elapsed time.
88    ///
89    /// This will record an error if no [`set_start`](TimespanMetric::set_start) was called.
90    pub fn stop(&self) {
91        let stop_time = zeitstempel::now();
92
93        let metric = self.clone();
94        crate::launch_with_glean(move |glean| metric.set_stop(glean, stop_time));
95    }
96
97    /// Set stop time synchronously.
98    #[doc(hidden)]
99    pub fn set_stop(&self, glean: &Glean, stop_time: u64) {
100        // Need to write in either case, so get the lock first.
101        let mut lock = self
102            .start_time
103            .write()
104            .expect("Lock poisoned for timespan metric on stop.");
105
106        if !self.should_record(glean) {
107            // Reset timer when disabled, so that we don't record timespans across
108            // disabled/enabled toggling.
109            *lock = None;
110            return;
111        }
112
113        if lock.is_none() {
114            record_error(
115                glean,
116                &self.meta,
117                ErrorType::InvalidState,
118                "Timespan not running",
119                None,
120            );
121            return;
122        }
123
124        let start_time = lock.take().unwrap();
125        let duration = match stop_time.checked_sub(start_time) {
126            Some(duration) => duration,
127            None => {
128                record_error(
129                    glean,
130                    &self.meta,
131                    ErrorType::InvalidValue,
132                    "Timespan was negative",
133                    None,
134                );
135                return;
136            }
137        };
138        let duration = Duration::from_nanos(duration);
139        self.set_raw_inner(glean, duration);
140    }
141
142    /// Aborts a previous [`set_start`](TimespanMetric::set_start) call. No
143    /// error is recorded if no [`set_start`](TimespanMetric::set_start) was
144    /// called.
145    pub fn cancel(&self) {
146        let metric = self.clone();
147        crate::dispatcher::launch(move || {
148            let mut lock = metric
149                .start_time
150                .write()
151                .expect("Lock poisoned for timespan metric on cancel.");
152            *lock = None;
153        });
154    }
155
156    /// Explicitly sets the timespan value.
157    ///
158    /// This API should only be used if your library or application requires
159    /// recording times in a way that can not make use of
160    /// [`set_start`](TimespanMetric::set_start)/[`set_stop`](TimespanMetric::set_stop)/[`cancel`](TimespanMetric::cancel).
161    ///
162    /// Care should be taken using this if the ping lifetime might contain more
163    /// than one timespan measurement. To be safe,
164    /// [`set_raw`](TimespanMetric::set_raw) should generally be followed by
165    /// sending a custom ping containing the timespan.
166    ///
167    /// # Arguments
168    ///
169    /// * `elapsed` - The elapsed time to record.
170    pub fn set_raw(&self, elapsed: Duration) {
171        let metric = self.clone();
172        crate::launch_with_glean(move |glean| metric.set_raw_sync(glean, elapsed));
173    }
174
175    /// Explicitly sets the timespan value in nanoseconds.
176    ///
177    /// This API should only be used if your library or application requires
178    /// recording times in a way that can not make use of
179    /// [`set_start`](TimespanMetric::set_start)/[`set_stop`](TimespanMetric::set_stop)/[`cancel`](TimespanMetric::cancel).
180    ///
181    /// Care should be taken using this if the ping lifetime might contain more
182    /// than one timespan measurement. To be safe,
183    /// [`set_raw`](TimespanMetric::set_raw) should generally be followed by
184    /// sending a custom ping containing the timespan.
185    ///
186    /// # Arguments
187    ///
188    /// * `elapsed_nanos` - The elapsed time to record, in nanoseconds.
189    pub fn set_raw_nanos(&self, elapsed_nanos: i64) {
190        let elapsed = Duration::from_nanos(elapsed_nanos.try_into().unwrap_or(0));
191        self.set_raw(elapsed)
192    }
193
194    /// Explicitly sets the timespan value synchronously.
195    #[doc(hidden)]
196    pub fn set_raw_sync(&self, glean: &Glean, elapsed: Duration) {
197        if !self.should_record(glean) {
198            return;
199        }
200
201        let lock = self
202            .start_time
203            .read()
204            .expect("Lock poisoned for timespan metric on set_raw.");
205
206        if lock.is_some() {
207            record_error(
208                glean,
209                &self.meta,
210                ErrorType::InvalidState,
211                "Timespan already running. Raw value not recorded.",
212                None,
213            );
214            return;
215        }
216
217        self.set_raw_inner(glean, elapsed);
218    }
219
220    fn set_raw_inner(&self, glean: &Glean, elapsed: Duration) {
221        let mut report_value_exists: bool = false;
222        glean.storage().record_with(glean, &self.meta, |old_value| {
223            match old_value {
224                Some(old @ Metric::Timespan(..)) => {
225                    // If some value already exists, report an error.
226                    // We do this out of the storage since recording an
227                    // error accesses the storage as well.
228                    report_value_exists = true;
229                    old
230                }
231                _ => Metric::Timespan(elapsed, self.time_unit),
232            }
233        });
234
235        if report_value_exists {
236            record_error(
237                glean,
238                &self.meta,
239                ErrorType::InvalidState,
240                "Timespan value already recorded. New value discarded.",
241                None,
242            );
243        };
244    }
245
246    /// Get the current value
247    #[doc(hidden)]
248    pub fn get_value<'a, S: Into<Option<&'a str>>>(
249        &self,
250        glean: &Glean,
251        ping_name: S,
252    ) -> Option<u64> {
253        let queried_ping_name = ping_name
254            .into()
255            .unwrap_or_else(|| &self.meta().inner.send_in_pings[0]);
256
257        match StorageManager.snapshot_metric_for_test(
258            glean.storage(),
259            queried_ping_name,
260            &self.meta.identifier(glean),
261            self.meta.inner.lifetime,
262        ) {
263            Some(Metric::Timespan(time, time_unit)) => Some(time_unit.duration_convert(time)),
264            _ => None,
265        }
266    }
267
268    /// **Exported for test purposes.**
269    ///
270    /// Gets the number of recorded errors for the given metric and error type.
271    ///
272    /// # Arguments
273    ///
274    /// * `error` - The type of error
275    ///
276    /// # Returns
277    ///
278    /// The number of errors reported.
279    pub fn test_get_num_recorded_errors(&self, error: ErrorType) -> i32 {
280        crate::block_on_dispatcher();
281
282        crate::core::with_glean(|glean| {
283            test_get_num_recorded_errors(glean, self.meta(), error).unwrap_or(0)
284        })
285    }
286}
287
288impl TestGetValue<i64> for TimespanMetric {
289    /// **Test-only API (exported for FFI purposes).**
290    ///
291    /// Gets the currently stored value as an integer.
292    ///
293    /// This doesn't clear the stored value.
294    ///
295    /// # Arguments
296    ///
297    /// * `ping_name` - the optional name of the ping to retrieve the metric
298    ///                 for. Defaults to the first value in `send_in_pings`.
299    ///
300    /// # Returns
301    ///
302    /// The stored value or `None` if nothing stored.
303    fn test_get_value(&self, ping_name: Option<String>) -> Option<i64> {
304        crate::block_on_dispatcher();
305        crate::core::with_glean(|glean| {
306            self.get_value(glean, ping_name.as_deref()).map(|val| {
307                val.try_into()
308                    .expect("Timespan can't be represented as i64")
309            })
310        })
311    }
312}