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}