// This Source Code Form is subject to the terms of the Mozilla Public // License, v. 2.0. If a copy of the MPL was not distributed with this // file, You can obtain one at https://mozilla.org/MPL/2.0/. use inherent::inherent; use std::collections::HashMap; use std::convert::TryInto; use std::sync::{ atomic::{AtomicUsize, Ordering}, Arc, RwLock, }; use std::time::{Duration, Instant}; #[cfg(feature = "with_gecko")] use thin_vec::ThinVec; use super::{BaseMetricId, ChildMetricMeta, CommonMetricData, MetricId, MetricNamer, TimeUnit}; use glean::{DistributionData, ErrorType, MetricIdentifier, TimerId}; use crate::ipc::{need_ipc, with_ipc_payload}; use glean::traits::TimingDistribution; #[cfg(feature = "with_gecko")] use super::MetricMetadataGetter; #[cfg(feature = "with_gecko")] use super::profiler_utils::{truncate_vector_for_marker, TelemetryProfilerCategory}; #[cfg(feature = "with_gecko")] use std::marker::PhantomData; #[cfg(feature = "with_gecko")] #[derive(serde::Serialize, serde::Deserialize, Debug)] pub(crate) enum TDMPayload { Duration(std::time::Duration), Sample(i64), Samples(Vec), SamplesNS(Vec), } #[cfg(feature = "with_gecko")] impl TDMPayload { pub fn from_samples_signed(samples: &Vec) -> TDMPayload { TDMPayload::Samples(truncate_vector_for_marker(samples)) } pub fn from_samples_unsigned(samples: &Vec) -> TDMPayload { TDMPayload::SamplesNS(truncate_vector_for_marker(samples)) } } #[cfg(feature = "with_gecko")] #[derive(serde::Serialize, serde::Deserialize, Debug)] pub(crate) struct TimingDistributionMetricMarker { id: MetricId, label: Option, timer_id: Option, value: Option, _phantom: PhantomData, } #[cfg(feature = "with_gecko")] impl TimingDistributionMetricMarker { pub fn new( id: MetricId, label: Option, timer_id: Option, value: Option, ) -> TimingDistributionMetricMarker { TimingDistributionMetricMarker { id, label, timer_id, value, _phantom: PhantomData, } } } #[cfg(feature = "with_gecko")] impl gecko_profiler::ProfilerMarker for TimingDistributionMetricMarker { fn marker_type_name() -> &'static str { "TimingDist" } fn marker_type_display() -> gecko_profiler::MarkerSchema { use gecko_profiler::schema::*; let mut schema = MarkerSchema::new(&[Location::MarkerChart, Location::MarkerTable]); schema.set_tooltip_label( "{marker.data.cat}.{marker.data.id} {marker.data.label} {marker.data.duration}{marker.data.sample}", ); schema.set_table_label("{marker.data.cat}.{marker.data.id} {marker.data.label}: {marker.data.duration}{marker.data.sample}{marker.data.samples}"); schema.set_chart_label("{marker.data.cat}.{marker.data.id} {marker.data.label}"); schema.add_key_label_format_with_flags( "cat", "Category", Format::UniqueString, PayloadFlags::Searchable, ); schema.add_key_label_format_with_flags( "id", "Metric", Format::UniqueString, PayloadFlags::Searchable, ); schema.add_key_label_format_with_flags( "label", "Label", Format::UniqueString, PayloadFlags::Searchable, ); schema.add_key_label_format_with_flags( "timer_id", "TimerId", Format::Integer, PayloadFlags::Searchable, ); schema.add_key_label_format("duration", "Duration", Format::String); schema.add_key_label_format("sample", "Sample", Format::String); schema.add_key_label_format("samples", "Samples", Format::String); schema } fn stream_json_marker_data(&self, json_writer: &mut gecko_profiler::JSONWriter) { crate::private::profiler_utils::stream_identifiers_by_id::( &self.id.into(), json_writer, ); if let Some(id) = &self.timer_id { // We don't care about exactly what the timer id is - so just // perform a bitwise cast, as that provides a 1:1 mapping. json_writer.int_property("timer_id", *id as i64); }; match &self.value { Some(p) => { match p { TDMPayload::Duration(d) => { // Durations do not have a `Display` implementation, // however for the profiler, the debug formatting // should be more than sufficient. let s = format!("{:?}", d); json_writer.string_property("duration", s.as_str()); } TDMPayload::Sample(s) => { let s = format!("{}", s); json_writer.string_property("sample", s.as_str()); } TDMPayload::Samples(s) => { let s = format!( "[{}]", s.iter() .map(|v| v.to_string()) .collect::>() .join(",") ); json_writer.string_property("samples", s.as_str()); } TDMPayload::SamplesNS(s) => { let s = format!( "(ns) [{}]", s.iter() .map(|v| v.to_string()) .collect::>() .join(",") ); json_writer.string_property("samples", s.as_str()); } }; } None => {} }; } } /// A timing distribution metric. /// /// Timing distributions are used to accumulate and store time measurements for analyzing distributions of the timing data. pub enum TimingDistributionMetric { Parent { /// The metric's ID. Used for testing, GIFFT, and profiler markers. /// Timing distribution metrics can be labeled, so we may have either /// a metric ID or sub-metric ID. id: MetricId, gifft_time_unit: TimeUnit, inner: Arc, }, Child(TimingDistributionMetricIpc), } #[derive(Debug)] pub struct TimingDistributionMetricIpc { meta: ChildMetricMeta, #[allow(unused)] gifft_time_unit: TimeUnit, next_timer_id: AtomicUsize, instants: RwLock>, } impl TimingDistributionMetric { /// Create a new timing distribution metric, _child process only_. pub(crate) fn new_child(meta: ChildMetricMeta, time_unit: TimeUnit) -> Self { debug_assert!(need_ipc()); TimingDistributionMetric::Child(TimingDistributionMetricIpc { meta, gifft_time_unit: time_unit, next_timer_id: AtomicUsize::new(0), instants: RwLock::new(HashMap::new()), }) } /// Create a new timing distribution metric. pub fn new(id: BaseMetricId, meta: CommonMetricData, time_unit: TimeUnit) -> Self { if need_ipc() { Self::new_child( ChildMetricMeta::from_common_metric_data(id, meta), time_unit, ) } else { let inner = glean::private::TimingDistributionMetric::new(meta, time_unit); TimingDistributionMetric::Parent { id: id.into(), gifft_time_unit: time_unit, inner: Arc::new(inner), } } } #[cfg(test)] pub(crate) fn child_metric(&self) -> Self { match self { TimingDistributionMetric::Parent { id, gifft_time_unit, inner, } => TimingDistributionMetric::Child(TimingDistributionMetricIpc { // SAFETY: We can unwrap here, as this code is only run in // the context of a test. If this code is used elsewhere, // the `unwrap` should be replaced with proper error // handling of the `None` case. meta: ChildMetricMeta::from_metric_identifier( id.base_metric_id().unwrap(), inner.as_ref(), ), gifft_time_unit: *gifft_time_unit, next_timer_id: AtomicUsize::new(0), instants: RwLock::new(HashMap::new()), }), TimingDistributionMetric::Child(_) => { panic!("Can't get a child metric from a child metric") } } } /// Performs the core portions of a start() call, but no frippery like GIFFT. pub(crate) fn inner_start(&self) -> TimerId { match self { TimingDistributionMetric::Parent { inner, .. } => inner.start(), TimingDistributionMetric::Child(c) => { // There is no glean-core on this process to give us a TimerId, // so we'll have to make our own and do our own bookkeeping. let id = c .next_timer_id .fetch_add(1, Ordering::SeqCst) .try_into() .unwrap(); let mut map = c .instants .write() .expect("lock of instants map was poisoned"); if let Some(_v) = map.insert(id, Instant::now()) { // TODO: report an error and find a different TimerId. } id.into() } } } /// Performs the core portions of a stop_and_accumulate() call, but no frippery like GIFFT. pub(crate) fn inner_stop_and_accumulate(&self, id: TimerId) { match self { TimingDistributionMetric::Parent { inner, .. } => inner.stop_and_accumulate(id), TimingDistributionMetric::Child(c) => { if let Some(sample) = self.child_stop(id) { with_ipc_payload(move |payload| { if let Some(v) = payload.timing_samples.get_mut(&c.meta.id) { v.push(sample); } else { payload.timing_samples.insert(c.meta.id, vec![sample]); } }); } else { // TODO: report an error (timer id for stop wasn't started). } } } } /// Stops the provided TimerId, but instead of accumulating the sample, returns it. No GIFFT neither. pub(crate) fn child_stop(&self, id: TimerId) -> Option { match self { TimingDistributionMetric::Parent { .. } => { panic!("Can't child_stop a parent-process timing_distribution") } TimingDistributionMetric::Child(c) => { let mut map = c .instants .write() .expect("Write lock must've been poisoned."); if let Some(start) = map.remove(&id.id) { let now = Instant::now(); let sample = now .checked_duration_since(start) .map(|s| s.as_nanos().try_into()); match sample { Some(Ok(sample)) => Some(sample), Some(Err(_)) => { log::warn!("Elapsed time larger than fits into 64-bytes. Saturating at u64::MAX."); Some(u64::MAX) } None => { log::warn!("Time went backwards. Not recording."); // TODO: report an error (timer id for stop was started, but time went backwards). None } } } else { // TODO: report an error (timer id for stop was never started). None } } } } /// Cancels the provided TimerId without notifying GIFFT. pub(crate) fn inner_cancel(&self, id: TimerId) { match self { TimingDistributionMetric::Parent { inner, .. } => inner.cancel(id), TimingDistributionMetric::Child(c) => { let mut map = c .instants .write() .expect("Write lock must've been poisoned."); if map.remove(&id.id).is_none() { // TODO: report an error (cancelled a non-started id). } } } } /// Accumulates the raw duration without notifying GIFFT. pub(crate) fn inner_accumulate_raw_duration(&self, duration: Duration) { let sample = duration.as_nanos().try_into().unwrap_or_else(|_| { // TODO: Instrument this error log::warn!( "Elapsed nanoseconds larger than fits into 64-bytes. Saturating at u64::MAX." ); u64::MAX }); match self { TimingDistributionMetric::Parent { inner, .. } => { inner.accumulate_raw_duration(duration) } TimingDistributionMetric::Child(c) => { with_ipc_payload(move |payload| { if let Some(v) = payload.timing_samples.get_mut(&c.meta.id) { v.push(sample); } else { payload.timing_samples.insert(c.meta.id, vec![sample]); } }); } } } /// Accumulates the samples without notifying GIFFT. pub(crate) fn inner_accumulate_samples(&self, samples: Vec) { match self { #[allow(unused)] TimingDistributionMetric::Parent { id: id @ MetricId::Id(_), inner, .. } => { // Only record a marker if we are accumulating samples for an // actual metric. Sub-metrics will record their own markers // in their own variant of `accumulate_samples`, so if we // also record a marker, there will be duplicates. #[cfg(feature = "with_gecko")] gecko_profiler::lazy_add_marker!( "TimingDistribution::accumulate", TelemetryProfilerCategory, TimingDistributionMetricMarker::::new( *id, None, None, Some(TDMPayload::from_samples_signed(&samples)), ) ); inner.accumulate_samples(samples) } TimingDistributionMetric::Parent { inner, .. } => inner.accumulate_samples(samples), TimingDistributionMetric::Child(_c) => { // TODO: Instrument this error log::error!("Can't record samples for a timing distribution from a child metric"); } } } /// Accumulates the single sample without notifying GIFFT. pub(crate) fn inner_accumulate_single_sample(&self, sample: i64) { match self { #[allow(unused)] TimingDistributionMetric::Parent { id: id @ MetricId::Id(_), inner, .. } => { // Only record metric markers, not sub-metric markers. See // `inner_accumulate_samples` for details of why. #[cfg(feature = "with_gecko")] gecko_profiler::lazy_add_marker!( "TimingDistribution::accumulate", TelemetryProfilerCategory, TimingDistributionMetricMarker::::new( *id, None, None, Some(TDMPayload::Sample(sample.clone())), ) ); inner.accumulate_single_sample(sample) } TimingDistributionMetric::Parent { inner, .. } => { inner.accumulate_single_sample(sample) } TimingDistributionMetric::Child(_c) => { // TODO: Instrument this error log::error!("Can't record samples for a timing distribution from a child metric"); } } } } #[inherent] impl TimingDistribution for TimingDistributionMetric { /// Starts tracking time for the provided metric. /// /// This records an error if it’s already tracking time (i.e. /// [`start`](TimingDistribution::start) was already called with no corresponding /// [`stop_and_accumulate`](TimingDistribution::stop_and_accumulate)): in that case the /// original start time will be preserved. /// /// # Returns /// /// A unique [`TimerId`] for the new timer. pub fn start(&self) -> TimerId { let timer_id = self.inner_start(); #[cfg(feature = "with_gecko")] { let metric_id: BaseMetricId = match self { TimingDistributionMetric::Parent { id, .. } => id .base_metric_id() .expect("Cannot perform GIFFT calls without a metric id."), TimingDistributionMetric::Child(c) => c.meta.id, }; extern "C" { fn GIFFT_TimingDistributionStart(metric_id: u32, timer_id: u64); } // SAFETY: using only primitives, no return value. unsafe { GIFFT_TimingDistributionStart(metric_id.0, timer_id.id); } // NOTE: we would like to record interval markers, either separate // markers with start/end, or a single marker with both start/end. // This is currently not possible, as the profiler incorrectly // matches separate start/end markers in the frontend, and we do // not have sufficient information to emit one marker when we stop // or cancel a timer. // This is being tracked in the following two bugs: // - Profiler, Bug 1929070, // - Glean, Bug 1931369, // While these bugs are being solved, we record instant markers so // that we still have *some* information. gecko_profiler::lazy_add_marker!( "TimingDistribution::start", TelemetryProfilerCategory, gecko_profiler::MarkerOptions::default() .with_timing(gecko_profiler::MarkerTiming::instant_now()), TimingDistributionMetricMarker::::new( metric_id.into(), None, Some(timer_id.id), None ) ); } timer_id.into() } /// Stops tracking time for the provided metric and associated timer id. /// /// Adds a count to the corresponding bucket in the timing distribution. /// This will record an error if no [`start`](TimingDistribution::start) was /// called. /// /// # Arguments /// /// * `id` - The [`TimerId`] to associate with this timing. This allows /// for concurrent timing of events associated with different ids to the /// same timespan metric. pub fn stop_and_accumulate(&self, id: TimerId) { self.inner_stop_and_accumulate(id); #[cfg(feature = "with_gecko")] { let (metric_id, gifft_time_unit) = match self { TimingDistributionMetric::Parent { id, gifft_time_unit, .. } => ( id.base_metric_id() .expect("Cannot perform GIFFT calls without a metric id."), gifft_time_unit, ), TimingDistributionMetric::Child(c) => (c.meta.id, &c.gifft_time_unit), }; extern "C" { fn GIFFT_TimingDistributionStopAndAccumulate( metric_id: u32, timer_id: u64, unit: i32, ); } // SAFETY: using only primitives, no return value. unsafe { GIFFT_TimingDistributionStopAndAccumulate( metric_id.0, id.id, *gifft_time_unit as i32, ); } // See note on TimingDistribution::start gecko_profiler::lazy_add_marker!( "TimingDistribution::stop", TelemetryProfilerCategory, gecko_profiler::MarkerOptions::default() .with_timing(gecko_profiler::MarkerTiming::instant_now()), TimingDistributionMetricMarker::::new( metric_id.into(), None, Some(id.id), None ) ); } } /// Aborts a previous [`start`](TimingDistribution::start) call. No /// error is recorded if no [`start`](TimingDistribution::start) was /// called. /// /// # Arguments /// /// * `id` - The [`TimerId`] to associate with this timing. This allows /// for concurrent timing of events associated with different ids to the /// same timing distribution metric. pub fn cancel(&self, id: TimerId) { self.inner_cancel(id); #[cfg(feature = "with_gecko")] { let metric_id: BaseMetricId = match self { TimingDistributionMetric::Parent { id, .. } => id .base_metric_id() .expect("Cannot perform GIFFT calls without a metric id."), TimingDistributionMetric::Child(c) => c.meta.id, }; extern "C" { fn GIFFT_TimingDistributionCancel(metric_id: u32, timer_id: u64); } // SAFETY: using only primitives, no return value. unsafe { GIFFT_TimingDistributionCancel(metric_id.0, id.id); } // See note on TimingDistribution::start gecko_profiler::lazy_add_marker!( "TimingDistribution::cancel", TelemetryProfilerCategory, gecko_profiler::MarkerOptions::default() .with_timing(gecko_profiler::MarkerTiming::instant_now()), TimingDistributionMetricMarker::::new( metric_id.into(), None, Some(id.id), None ) ); } } /// Accumulates the provided signed samples in the metric. /// /// This is required so that the platform-specific code can provide us with /// 64 bit signed integers if no `u64` comparable type is available. This /// will take care of filtering and reporting errors for any provided negative /// sample. /// /// Please note that this assumes that the provided samples are already in /// the "unit" declared by the instance of the metric type (e.g. if the /// instance this method was called on is using [`crate::TimeUnit::Second`], then /// `samples` are assumed to be in that unit). /// /// # Arguments /// /// * `samples` - The vector holding the samples to be recorded by the metric. /// /// ## Notes /// /// Discards any negative value in `samples` and report an [`ErrorType::InvalidValue`] /// for each of them. Reports an [`ErrorType::InvalidOverflow`] error for samples that /// are longer than `MAX_SAMPLE_TIME`. pub fn accumulate_samples(&self, samples: Vec) { #[cfg(feature = "with_gecko")] { let gifft_samples = samples .iter() .filter(|&&i| i >= 0) .map(|&i| { i.try_into().unwrap_or_else(|_| { // TODO: Instrument this error log::warn!( "Samples larger than fits into 32-bytes. Saturating at u32::MAX." ); u32::MAX }) }) .collect(); let metric_id = match self { TimingDistributionMetric::Parent { id, .. } => id .base_metric_id() .expect("Cannot perform GIFFT calls without a metric id."), TimingDistributionMetric::Child(c) => c.meta.id, }; extern "C" { fn GIFFT_TimingDistributionAccumulateRawSamples( metric_id: u32, samples: &ThinVec, ); } // SAFETY: No one uses the ThinVec after lending it to C++, no return value. unsafe { GIFFT_TimingDistributionAccumulateRawSamples(metric_id.0, &gifft_samples); } } self.inner_accumulate_samples(samples); } /// Accumulates the provided samples in the metric. /// /// # Arguments /// /// * `samples` - A list of samples recorded by the metric. /// Samples must be in nanoseconds. /// ## Notes /// /// Reports an [`ErrorType::InvalidOverflow`] error for samples that /// are longer than `MAX_SAMPLE_TIME`. pub(crate) fn accumulate_raw_samples_nanos(&self, samples: Vec) { match self { #[allow(unused)] TimingDistributionMetric::Parent { id: id @ MetricId::Id(_), inner, .. } => { // Only record metric markers, not sub-metric markers. See // `inner_accumulate_samples` for details of why. #[cfg(feature = "with_gecko")] gecko_profiler::lazy_add_marker!( "TimingDistribution::accumulate", TelemetryProfilerCategory, TimingDistributionMetricMarker::::new( *id, None, None, Some(TDMPayload::from_samples_unsigned(&samples)), ) ); inner.accumulate_raw_samples_nanos(samples) } TimingDistributionMetric::Parent { inner, .. } => { inner.accumulate_raw_samples_nanos(samples) } TimingDistributionMetric::Child(_c) => { // TODO: Instrument this error log::error!("Can't record samples for a timing distribution from a child metric"); } } } pub fn accumulate_single_sample(&self, sample: i64) { self.inner_accumulate_single_sample(sample); #[cfg(feature = "with_gecko")] { let metric_id = match self { TimingDistributionMetric::Parent { id, .. } => id .base_metric_id() .expect("Cannot perform GIFFT calls without a metric id."), TimingDistributionMetric::Child(c) => c.meta.id, }; let sample = sample.try_into().unwrap_or_else(|_| { // TODO: Instrument this error log::warn!("Sample larger than fits into 32-bytes. Saturating at u32::MAX."); u32::MAX }); extern "C" { fn GIFFT_TimingDistributionAccumulateRawSample(metric_id: u32, sample: u32); } // SAFETY: using only primitives, no return value. unsafe { GIFFT_TimingDistributionAccumulateRawSample(metric_id.0, sample); } } } /// Accumulates a time duration sample for the provided metric. /// /// Adds a count to the corresponding bucket in the timing distribution. /// Saturates at u64::MAX nanoseconds. /// /// Prefer start() and stop_and_accumulate() where possible. /// /// Users of this API are responsible for ensuring the timing source used /// to calculate the duration is monotonic and consistent across platforms. /// /// # Arguments /// /// * `duration` - The [`Duration`] of the accumulated sample. pub fn accumulate_raw_duration(&self, duration: Duration) { self.inner_accumulate_raw_duration(duration); #[cfg(feature = "with_gecko")] { let (metric_id, gifft_time_unit) = match self { TimingDistributionMetric::Parent { id, gifft_time_unit, .. } => ( id.base_metric_id() .expect("Cannot perform GIFFT calls without a metric id."), gifft_time_unit, ), TimingDistributionMetric::Child(c) => (c.meta.id, &c.gifft_time_unit), }; let sample = gifft_time_unit.duration_convert(duration); let sample = sample.try_into().unwrap_or_else(|_| { // TODO: Instrument this error log::warn!( "Elapsed duration larger than fits into 32-bytes. Saturating at u32::MAX." ); u32::MAX }); extern "C" { fn GIFFT_TimingDistributionAccumulateRawSample(metric_id: u32, sample: u32); } // SAFETY: using only primitives, no return value. unsafe { GIFFT_TimingDistributionAccumulateRawSample(metric_id.0, sample); } gecko_profiler::lazy_add_marker!( "TimingDistribution::accumulate", TelemetryProfilerCategory, TimingDistributionMetricMarker::::new( metric_id.into(), None, None, Some(TDMPayload::Duration(duration.clone())), ) ); } } /// **Exported for test purposes.** /// /// Gets the number of recorded errors for the given error type. /// /// # Arguments /// /// * `error` - The type of error /// * `ping_name` - represents the optional name of the ping to retrieve the /// metric for. Defaults to the first value in `send_in_pings`. /// /// # Returns /// /// The number of errors recorded. pub fn test_get_num_recorded_errors(&self, error: ErrorType) -> i32 { match self { TimingDistributionMetric::Parent { inner, .. } => { inner.test_get_num_recorded_errors(error) } TimingDistributionMetric::Child(c) => panic!( "Cannot get number of recorded errors for {:?} in non-parent process!", c ), } } } #[inherent] impl glean::TestGetValue for TimingDistributionMetric { /// **Exported for test purposes.** /// /// Gets the currently stored value of the metric. /// /// This doesn't clear the stored value. /// /// # Arguments /// /// * `ping_name` - represents the optional name of the ping to retrieve the /// metric for. Defaults to the first value in `send_in_pings`. pub fn test_get_value(&self, ping_name: Option) -> Option { match self { TimingDistributionMetric::Parent { inner, .. } => inner.test_get_value(ping_name), TimingDistributionMetric::Child(c) => { panic!("Cannot get test value for {:?} in non-parent process!", c) } } } } impl MetricNamer for TimingDistributionMetric { fn get_metadata(&self) -> super::MetricMetadata { crate::private::MetricMetadata::from_triple(match self { TimingDistributionMetric::Parent { inner, .. } => inner.get_identifiers(), TimingDistributionMetric::Child(tdmi) => tdmi.meta.get_identifiers(), }) } } #[cfg(test)] mod test { use crate::{common_test::*, ipc, metrics}; #[test] fn smoke_test_timing_distribution() { let _lock = lock_test(); let metric = &metrics::test_only_ipc::a_timing_dist; let id = metric.start(); // Stopping right away might not give us data, if the underlying clock source is not precise // enough. // So let's cancel and make sure nothing blows up. metric.cancel(id); // We can't inspect the values yet. assert!(metric .test_get_value(Some("test-ping".to_string())) .is_none()); } #[test] fn timing_distribution_child() { let _lock = lock_test(); let parent_metric = &metrics::test_only_ipc::a_timing_dist; let id = parent_metric.start(); std::thread::sleep(std::time::Duration::from_millis(10)); parent_metric.stop_and_accumulate(id); { let child_metric = parent_metric.child_metric(); // scope for need_ipc RAII let _raii = ipc::test_set_need_ipc(true); let id = child_metric.start(); let id2 = child_metric.start(); assert_ne!(id, id2); std::thread::sleep(std::time::Duration::from_millis(10)); child_metric.stop_and_accumulate(id); child_metric.cancel(id2); } let buf = ipc::take_buf().unwrap(); assert!(buf.len() > 0); assert!(ipc::replay_from_buf(&buf).is_ok()); let data = parent_metric .test_get_value(Some("test-ping".to_string())) .expect("should have some data"); // No guarantees from timers means no guarantees on buckets. // But we can guarantee it's only two samples. assert_eq!( 2, data.values.values().fold(0, |acc, count| acc + count), "record 2 values, one parent, one child measurement" ); assert!(0 < data.sum, "record some time"); } }