Skip to main content

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