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}