diff options
author | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-28 14:29:10 +0000 |
---|---|---|
committer | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-28 14:29:10 +0000 |
commit | 2aa4a82499d4becd2284cdb482213d541b8804dd (patch) | |
tree | b80bf8bf13c3766139fbacc530efd0dd9d54394c /toolkit/components/glean/api/src | |
parent | Initial commit. (diff) | |
download | firefox-upstream.tar.xz firefox-upstream.zip |
Adding upstream version 86.0.1.upstream/86.0.1upstream
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'toolkit/components/glean/api/src')
21 files changed, 3176 insertions, 0 deletions
diff --git a/toolkit/components/glean/api/src/common_test.rs b/toolkit/components/glean/api/src/common_test.rs new file mode 100644 index 0000000000..c88ae88cb4 --- /dev/null +++ b/toolkit/components/glean/api/src/common_test.rs @@ -0,0 +1,51 @@ +// 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 std::sync::{Mutex, MutexGuard}; + +use once_cell::sync::Lazy; + +const GLOBAL_APPLICATION_ID: &str = "org.mozilla.firefox.test"; + +/// UGLY HACK. +/// We use a global lock to force synchronization of all tests, even if run multi-threaded. +/// This allows us to run without `--test-threads 1`.` +pub fn lock_test() -> (MutexGuard<'static, ()>, tempfile::TempDir) { + static GLOBAL_LOCK: Lazy<Mutex<()>> = Lazy::new(|| Mutex::new(())); + + let lock = GLOBAL_LOCK.lock().unwrap(); + + let dir = setup_glean(None); + (lock, dir) +} + +// Create a new instance of Glean with a temporary directory. +// We need to keep the `TempDir` alive, so that it's not deleted before we stop using it. +fn setup_glean(tempdir: Option<tempfile::TempDir>) -> tempfile::TempDir { + let dir = match tempdir { + Some(tempdir) => tempdir, + None => tempfile::tempdir().unwrap(), + }; + let tmpname = dir.path().display().to_string(); + + let cfg = glean::Configuration { + upload_enabled: true, + data_path: tmpname, + application_id: GLOBAL_APPLICATION_ID.into(), + max_events: None, + delay_ping_lifetime_io: false, + channel: None, + server_endpoint: None, + uploader: None, + }; + + let client_info = glean::ClientInfoMetrics { + app_build: "test-build".into(), + app_display_version: "1.2.3".into(), + }; + + glean::test_reset_glean(cfg, client_info, true); + + dir +} diff --git a/toolkit/components/glean/api/src/ffi/event.rs b/toolkit/components/glean/api/src/ffi/event.rs new file mode 100644 index 0000000000..6a433eda54 --- /dev/null +++ b/toolkit/components/glean/api/src/ffi/event.rs @@ -0,0 +1,90 @@ +// 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/. + +#![cfg(feature = "with_gecko")] + +use nsstring::{nsACString, nsCString}; +use thin_vec::ThinVec; + +use crate::metrics::__glean_metric_maps as metric_maps; +use crate::private::EventRecordingError; + +#[no_mangle] +pub extern "C" fn fog_event_record( + id: u32, + extra_keys: &ThinVec<i32>, + extra_values: &ThinVec<nsCString>, +) { + // If no extra keys are passed, we can shortcut here. + if extra_keys.is_empty() { + if metric_maps::event_record_wrapper(id, Default::default()).is_err() { + panic!("No event for id {}", id); + } + + return; + } + + assert_eq!( + extra_keys.len(), + extra_values.len(), + "Extra keys and values differ in length. ID: {}", + id + ); + + // Otherwise we need to decode them and pass them along. + let extra = extra_keys + .iter() + .zip(extra_values.iter()) + .map(|(&k, v)| (k, v.to_string())) + .collect(); + match metric_maps::event_record_wrapper(id, extra) { + Ok(()) => {} + Err(EventRecordingError::InvalidId) => panic!("No event for id {}", id), + Err(EventRecordingError::InvalidExtraKey) => { + panic!("Invalid extra keys in map for id {}", id) + } + } +} + +#[no_mangle] +pub extern "C" fn fog_event_record_str( + id: u32, + extra_keys: &ThinVec<nsCString>, + extra_values: &ThinVec<nsCString>, +) { + // If no extra keys are passed, we can shortcut here. + if extra_keys.is_empty() { + if metric_maps::event_record_wrapper_str(id, Default::default()).is_err() { + panic!("No event for id {}", id); + } + + return; + } + + assert_eq!( + extra_keys.len(), + extra_values.len(), + "Extra keys and values differ in length. ID: {}", + id + ); + + // Otherwise we need to decode them and pass them along. + let extra = extra_keys + .iter() + .zip(extra_values.iter()) + .map(|(k, v)| (k.to_string(), v.to_string())) + .collect(); + match metric_maps::event_record_wrapper_str(id, extra) { + Ok(()) => {} + Err(EventRecordingError::InvalidId) => panic!("No event for id {}", id), + Err(EventRecordingError::InvalidExtraKey) => { + panic!("Invalid extra keys in map for id {}", id) + } + } +} + +#[no_mangle] +pub unsafe extern "C" fn fog_event_test_has_value(id: u32, storage_name: &nsACString) -> bool { + metric_maps::event_test_get_value_wrapper(id, &storage_name.to_utf8()).is_some() +} diff --git a/toolkit/components/glean/api/src/ffi/macros.rs b/toolkit/components/glean/api/src/ffi/macros.rs new file mode 100644 index 0000000000..c6776e96af --- /dev/null +++ b/toolkit/components/glean/api/src/ffi/macros.rs @@ -0,0 +1,61 @@ +// 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/. + +//! Helper macros for implementing the FFI API for metric types. + +/// Get a metric object by ID from the corresponding map. +/// +/// # Arguments +/// +/// * `$map` - The name of the hash map within `metrics::__glean_metric_maps` +/// as generated by glean_parser. +/// * `$id` - The ID of the metric to get. +macro_rules! metric_get { + ($map:ident, $id:ident) => { + match $crate::metrics::__glean_metric_maps::$map.get(&$id.into()) { + Some(metric) => metric, + None => panic!("No metric for id {}", $id), + } + }; +} + +/// Test whether a value is stored for the metric identified by its ID. +/// +/// # Arguments +/// +/// * `$map` - The name of the hash map within `metrics::__glean_metric_maps` +/// as generated by glean_parser. +/// * `$id` - The ID of the metric to get. +/// * `$storage` - the storage name to look into. +macro_rules! test_has { + ($map:ident, $id:ident, $storage:ident) => {{ + let metric = metric_get!($map, $id); + let storage = if $storage.is_empty() { + None + } else { + Some($storage.to_utf8()) + }; + metric.test_get_value(storage.as_deref()).is_some() + }}; +} + +/// Get the currently stored value for the metric identified by its ID. +/// +/// # Arguments +/// +/// * `$map` - The name of the hash map within `metrics::__glean_metric_maps` +/// as generated by glean_parser. +/// * `$id` - The ID of the metric to get. +/// * `$storage` - the storage name to look into. +macro_rules! test_get { + ($map:ident, $id:ident, $storage:ident) => {{ + let metric = metric_get!($map, $id); + let storage = if $storage.is_empty() { + None + } else { + Some($storage.to_utf8()) + }; + metric.test_get_value(storage.as_deref()).unwrap() + }}; +} diff --git a/toolkit/components/glean/api/src/ffi/mod.rs b/toolkit/components/glean/api/src/ffi/mod.rs new file mode 100644 index 0000000000..b5ba32b430 --- /dev/null +++ b/toolkit/components/glean/api/src/ffi/mod.rs @@ -0,0 +1,268 @@ +// 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/. + +#![cfg(feature = "with_gecko")] + +use crate::pings; +use nsstring::{nsACString, nsCString}; +use thin_vec::ThinVec; +use uuid::Uuid; + +#[macro_use] +mod macros; +mod event; + +#[no_mangle] +pub unsafe extern "C" fn fog_counter_add(id: u32, amount: i32) { + let metric = metric_get!(COUNTER_MAP, id); + metric.add(amount); +} + +#[no_mangle] +pub unsafe extern "C" fn fog_counter_test_has_value(id: u32, storage_name: &nsACString) -> bool { + test_has!(COUNTER_MAP, id, storage_name) +} + +#[no_mangle] +pub unsafe extern "C" fn fog_counter_test_get_value(id: u32, storage_name: &nsACString) -> i32 { + test_get!(COUNTER_MAP, id, storage_name) +} + +#[no_mangle] +pub unsafe extern "C" fn fog_timespan_start(id: u32) { + let metric = metric_get!(TIMESPAN_MAP, id); + metric.start(); +} + +#[no_mangle] +pub unsafe extern "C" fn fog_timespan_stop(id: u32) { + let metric = metric_get!(TIMESPAN_MAP, id); + metric.stop(); +} + +#[no_mangle] +pub unsafe extern "C" fn fog_timespan_test_has_value(id: u32, storage_name: &nsACString) -> bool { + test_has!(TIMESPAN_MAP, id, storage_name) +} + +#[no_mangle] +pub unsafe extern "C" fn fog_timespan_test_get_value(id: u32, storage_name: &nsACString) -> u64 { + test_get!(TIMESPAN_MAP, id, storage_name) +} + +#[no_mangle] +pub unsafe extern "C" fn fog_boolean_test_has_value(id: u32, storage_name: &nsACString) -> bool { + test_has!(BOOLEAN_MAP, id, storage_name) +} + +#[no_mangle] +pub unsafe extern "C" fn fog_boolean_test_get_value(id: u32, storage_name: &nsACString) -> bool { + test_get!(BOOLEAN_MAP, id, storage_name) +} + +#[no_mangle] +pub extern "C" fn fog_boolean_set(id: u32, value: bool) { + let metric = metric_get!(BOOLEAN_MAP, id); + metric.set(value); +} + +// The String functions are custom because test_get needs to use an outparam. +// If we can make test_get optional, we can go back to using the macro to +// generate the rest of the functions, or something. + +#[no_mangle] +pub extern "C" fn fog_string_test_has_value(id: u32, storage_name: &nsACString) -> bool { + test_has!(STRING_MAP, id, storage_name) +} + +#[no_mangle] +pub extern "C" fn fog_string_test_get_value( + id: u32, + storage_name: &nsACString, + value: &mut nsACString, +) { + let val = test_get!(STRING_MAP, id, storage_name); + value.assign(&val); +} + +#[no_mangle] +pub extern "C" fn fog_string_set(id: u32, value: &nsACString) { + let metric = metric_get!(STRING_MAP, id); + metric.set(value.to_utf8()); +} + +// String List Functions: + +#[no_mangle] +pub extern "C" fn fog_string_list_test_has_value(id: u32, storage_name: &nsACString) -> bool { + test_has!(STRING_LIST_MAP, id, storage_name) +} + +#[no_mangle] +pub extern "C" fn fog_string_list_test_get_value( + id: u32, + storage_name: &nsACString, + value: &mut ThinVec<nsCString>, +) { + let val = test_get!(STRING_LIST_MAP, id, storage_name); + for v in val { + value.push(v.into()); + } +} + +#[no_mangle] +pub extern "C" fn fog_string_list_add(id: u32, value: &nsACString) { + let metric = metric_get!(STRING_LIST_MAP, id); + metric.add(value.to_utf8()); +} + +#[no_mangle] +pub extern "C" fn fog_string_list_set(id: u32, value: &ThinVec<nsCString>) { + let metric = metric_get!(STRING_LIST_MAP, id); + let value = value.iter().map(|s| s.to_utf8().into()).collect(); + metric.set(value); +} + +// The Uuid functions are custom because test_get needs to use an outparam. +// If we can make test_get optional, we can go back to using the macro to +// generate the rest of the functions, or something. + +#[no_mangle] +pub extern "C" fn fog_uuid_test_has_value(id: u32, storage_name: &nsACString) -> bool { + test_has!(UUID_MAP, id, storage_name) +} + +#[no_mangle] +pub extern "C" fn fog_uuid_test_get_value( + id: u32, + storage_name: &nsACString, + value: &mut nsACString, +) { + let uuid = test_get!(UUID_MAP, id, storage_name).to_string(); + value.assign(&uuid); +} + +#[no_mangle] +pub extern "C" fn fog_uuid_set(id: u32, value: &nsACString) { + if let Ok(uuid) = Uuid::parse_str(&value.to_utf8()) { + let metric = metric_get!(UUID_MAP, id); + metric.set(uuid); + } +} + +#[no_mangle] +pub extern "C" fn fog_uuid_generate_and_set(id: u32) { + let metric = metric_get!(UUID_MAP, id); + metric.generate_and_set(); +} + +#[no_mangle] +pub extern "C" fn fog_datetime_test_has_value(id: u32, storage_name: &nsACString) -> bool { + test_has!(DATETIME_MAP, id, storage_name) +} + +#[no_mangle] +pub extern "C" fn fog_datetime_test_get_value( + id: u32, + storage_name: &nsACString, + value: &mut nsACString, +) { + let val = test_get!(DATETIME_MAP, id, storage_name); + value.assign(&val.to_rfc3339()); +} + +#[no_mangle] +pub extern "C" fn fog_datetime_set( + id: u32, + year: i32, + month: u32, + day: u32, + hour: u32, + minute: u32, + second: u32, + nano: u32, + offset_seconds: i32, +) { + let metric = metric_get!(DATETIME_MAP, id); + metric.set_with_details(year, month, day, hour, minute, second, nano, offset_seconds); +} + +#[no_mangle] +pub extern "C" fn fog_memory_distribution_test_has_value( + id: u32, + storage_name: &nsACString, +) -> bool { + test_has!(MEMORY_DISTRIBUTION_MAP, id, storage_name) +} + +#[no_mangle] +pub extern "C" fn fog_memory_distribution_test_get_value( + id: u32, + storage_name: &nsACString, + sum: &mut u64, + buckets: &mut ThinVec<u64>, + counts: &mut ThinVec<u64>, +) { + let val = test_get!(MEMORY_DISTRIBUTION_MAP, id, storage_name); + *sum = val.sum; + for (&bucket, &count) in val.values.iter() { + buckets.push(bucket); + counts.push(count); + } +} + +#[no_mangle] +pub extern "C" fn fog_memory_distribution_accumulate(id: u32, sample: u64) { + let metric = metric_get!(MEMORY_DISTRIBUTION_MAP, id); + metric.accumulate(sample); +} + +#[no_mangle] +pub extern "C" fn fog_submit_ping_by_id(id: u32, reason: &nsACString) { + let reason = if reason.is_empty() { + None + } else { + Some(reason.to_utf8()) + }; + pings::submit_ping_by_id(id, reason.as_deref()); +} + +#[no_mangle] +pub extern "C" fn fog_timing_distribution_start(id: u32) -> u64 { + let metric = metric_get!(TIMING_DISTRIBUTION_MAP, id); + metric.start() +} + +#[no_mangle] +pub extern "C" fn fog_timing_distribution_stop_and_accumulate(id: u32, timing_id: u64) { + let metric = metric_get!(TIMING_DISTRIBUTION_MAP, id); + metric.stop_and_accumulate(timing_id); +} + +#[no_mangle] +pub extern "C" fn fog_timing_distribution_cancel(id: u32, timing_id: u64) { + let metric = metric_get!(TIMING_DISTRIBUTION_MAP, id); + metric.cancel(timing_id); +} + +#[no_mangle] +pub extern "C" fn fog_timing_distribution_test_has_value(id: u32, ping_name: &nsACString) -> bool { + test_has!(TIMING_DISTRIBUTION_MAP, id, ping_name) +} + +#[no_mangle] +pub extern "C" fn fog_timing_distribution_test_get_value( + id: u32, + ping_name: &nsACString, + sum: &mut u64, + buckets: &mut ThinVec<u64>, + counts: &mut ThinVec<u64>, +) { + let val = test_get!(TIMING_DISTRIBUTION_MAP, id, ping_name); + *sum = val.sum; + for (&bucket, &count) in val.values.iter() { + buckets.push(bucket); + counts.push(count); + } +} diff --git a/toolkit/components/glean/api/src/ipc.rs b/toolkit/components/glean/api/src/ipc.rs new file mode 100644 index 0000000000..1b6992d7b5 --- /dev/null +++ b/toolkit/components/glean/api/src/ipc.rs @@ -0,0 +1,127 @@ +// 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/. + +//! IPC Implementation, Rust part + +use crate::private::{Instant, MetricId}; +use once_cell::sync::Lazy; +use serde::{Deserialize, Serialize}; +use std::collections::HashMap; +#[cfg(not(feature = "with_gecko"))] +use std::sync::atomic::{AtomicBool, Ordering}; +use std::sync::Mutex; +#[cfg(feature = "with_gecko")] +use { + std::sync::atomic::{AtomicU32, Ordering}, + xpcom::interfaces::nsIXULRuntime, +}; + +use super::metrics::__glean_metric_maps; + +type EventRecord = (Instant, Option<HashMap<i32, String>>); + +/// Contains all the information necessary to update the metrics on the main +/// process. +#[derive(Debug, Default, Deserialize, Serialize)] +pub struct IPCPayload { + pub counters: HashMap<MetricId, i32>, + pub events: HashMap<MetricId, Vec<EventRecord>>, + pub memory_samples: HashMap<MetricId, Vec<u64>>, + pub string_lists: HashMap<MetricId, Vec<String>>, + pub timing_samples: HashMap<MetricId, Vec<u128>>, +} + +/// Global singleton: pending IPC payload. +static PAYLOAD: Lazy<Mutex<IPCPayload>> = Lazy::new(|| Mutex::new(IPCPayload::default())); + +pub fn with_ipc_payload<F, R>(f: F) -> R +where + F: FnOnce(&mut IPCPayload) -> R, +{ + let mut payload = PAYLOAD.lock().unwrap(); + f(&mut payload) +} + +/// Do we need IPC? +/// +/// Thread-safe. +#[cfg(feature = "with_gecko")] +static PROCESS_TYPE: Lazy<AtomicU32> = Lazy::new(|| { + if let Some(appinfo) = xpcom::services::get_XULRuntime() { + let mut process_type = nsIXULRuntime::PROCESS_TYPE_DEFAULT as u32; + let rv = unsafe { appinfo.GetProcessType(&mut process_type) }; + if rv.succeeded() { + return AtomicU32::new(process_type); + } + } + AtomicU32::new(nsIXULRuntime::PROCESS_TYPE_DEFAULT as u32) +}); + +#[cfg(feature = "with_gecko")] +pub fn need_ipc() -> bool { + PROCESS_TYPE.load(Ordering::Relaxed) != nsIXULRuntime::PROCESS_TYPE_DEFAULT as u32 +} + +/// An RAII that, on drop, restores the value used to determine whether FOG +/// needs IPC. Used in tests. +/// ```rust,ignore +/// #[test] +/// fn test_need_ipc_raii() { +/// assert!(false == ipc::need_ipc()); +/// { +/// let _raii = ipc::test_set_need_ipc(true); +/// assert!(ipc::need_ipc()); +/// } +/// assert!(false == ipc::need_ipc()); +/// } +/// ``` +#[cfg(not(feature = "with_gecko"))] +pub struct TestNeedIpcRAII { + prev_value: bool, +} + +#[cfg(not(feature = "with_gecko"))] +impl Drop for TestNeedIpcRAII { + fn drop(&mut self) { + TEST_NEED_IPC.store(self.prev_value, Ordering::Relaxed); + } +} + +#[cfg(not(feature = "with_gecko"))] +static TEST_NEED_IPC: AtomicBool = AtomicBool::new(false); + +/// Test-only API for telling FOG to use IPC mechanisms even if the test has +/// only the one process. See TestNeedIpcRAII for an example. +#[cfg(not(feature = "with_gecko"))] +pub fn test_set_need_ipc(need_ipc: bool) -> TestNeedIpcRAII { + TestNeedIpcRAII { + prev_value: TEST_NEED_IPC.swap(need_ipc, Ordering::Relaxed), + } +} + +#[cfg(not(feature = "with_gecko"))] +pub fn need_ipc() -> bool { + TEST_NEED_IPC.load(Ordering::Relaxed) +} + +pub fn take_buf() -> Option<Vec<u8>> { + with_ipc_payload(move |payload| { + let buf = bincode::serialize(&payload).ok(); + *payload = IPCPayload { + ..Default::default() + }; + buf + }) +} + +pub fn replay_from_buf(buf: &[u8]) -> Result<(), ()> { + let ipc_payload: IPCPayload = bincode::deserialize(buf).map_err(|_| ())?; + for (id, value) in ipc_payload.counters.into_iter() { + log::info!("Asked to replay {:?}, {:?}", id, value); + if let Some(metric) = __glean_metric_maps::COUNTER_MAP.get(&id) { + metric.add(value); + } + } + Ok(()) +} diff --git a/toolkit/components/glean/api/src/lib.rs b/toolkit/components/glean/api/src/lib.rs new file mode 100644 index 0000000000..e2cd29c7af --- /dev/null +++ b/toolkit/components/glean/api/src/lib.rs @@ -0,0 +1,23 @@ +// 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/. + +//! The public FOG APIs, for Rust consumers. + +// Re-exporting for later use in generated code. +pub extern crate chrono; +pub extern crate once_cell; +pub extern crate uuid; + +// Re-exporting for use in user tests. +pub use private::{DistributionData, ErrorType, RecordedEvent}; + +pub mod metrics; +pub mod pings; +pub mod private; + +pub mod ipc; + +#[cfg(test)] +mod common_test; +mod ffi; diff --git a/toolkit/components/glean/api/src/metrics.rs b/toolkit/components/glean/api/src/metrics.rs new file mode 100644 index 0000000000..9c2860bc29 --- /dev/null +++ b/toolkit/components/glean/api/src/metrics.rs @@ -0,0 +1,70 @@ +// 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/. + +//! This file contains the Generated Glean Metrics API +//! +//! The contents of this module are generated by +//! `toolkit/components/glean/build_scripts/glean_parser_ext/run_glean_parser.py`, from +//! 'toolkit/components/glean/metrics.yaml`. + +#[cfg(not(feature = "cargo-clippy"))] +include!(concat!( + env!("MOZ_TOPOBJDIR"), + "/toolkit/components/glean/api/src/metrics.rs" +)); + +// When running clippy the linter, `MOZ_TOPOBJDIR` is not set +// (and the `metrics.rs` file might not even be generated yet), +// so we need to manually define some things we expect from it so the rest of the build can assume +// it's there. +// See https://bugzilla.mozilla.org/show_bug.cgi?id=1674726. +#[cfg(feature = "cargo-clippy")] +#[allow(dead_code)] +pub(crate) mod __glean_metric_maps { + use std::collections::HashMap; + + use crate::private::*; + use once_cell::sync::Lazy; + + pub static TIMESPAN_MAP: Lazy<HashMap<MetricId, &Lazy<TimespanMetric>>> = + Lazy::new(HashMap::new); + + pub static COUNTER_MAP: Lazy<HashMap<MetricId, &Lazy<CounterMetric>>> = Lazy::new(HashMap::new); + + pub static BOOLEAN_MAP: Lazy<HashMap<MetricId, &Lazy<BooleanMetric>>> = Lazy::new(HashMap::new); + + pub static DATETIME_MAP: Lazy<HashMap<MetricId, &Lazy<DatetimeMetric>>> = + Lazy::new(HashMap::new); + + pub static STRING_MAP: Lazy<HashMap<MetricId, &Lazy<StringMetric>>> = Lazy::new(HashMap::new); + + pub static MEMORY_DISTRIBUTION_MAP: Lazy<HashMap<MetricId, &Lazy<MemoryDistributionMetric>>> = + Lazy::new(HashMap::new); + + pub static STRING_LIST_MAP: Lazy<HashMap<MetricId, &Lazy<StringListMetric>>> = + Lazy::new(HashMap::new); + + pub static UUID_MAP: Lazy<HashMap<MetricId, &Lazy<UuidMetric>>> = Lazy::new(HashMap::new); + + pub(crate) fn event_record_wrapper( + _metric_id: u32, + _extra: Option<HashMap<i32, String>>, + ) -> Result<(), EventRecordingError> { + Err(EventRecordingError::InvalidId) + } + + pub(crate) fn event_record_wrapper_str( + _metric_id: u32, + _extra: Option<HashMap<String, String>>, + ) -> Result<(), EventRecordingError> { + Err(EventRecordingError::InvalidId) + } + + pub(crate) fn event_test_get_value_wrapper( + _metric_id: u32, + _storage_name: &str, + ) -> Option<Vec<RecordedEvent>> { + None + } +} diff --git a/toolkit/components/glean/api/src/pings.rs b/toolkit/components/glean/api/src/pings.rs new file mode 100644 index 0000000000..5837278566 --- /dev/null +++ b/toolkit/components/glean/api/src/pings.rs @@ -0,0 +1,15 @@ +// 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/. + +//! This file contains the Generated Glean Metrics API (Ping portion) +//! +//! The contents of this module are generated by +//! `toolkit/components/glean/build_scripts/glean_parser_ext/run_glean_parser.py`, from +//! 'toolkit/components/glean/pings.yaml`. + +#[cfg(not(feature = "cargo-clippy"))] +include!(concat!( + env!("MOZ_TOPOBJDIR"), + "/toolkit/components/glean/api/src/pings.rs" +)); diff --git a/toolkit/components/glean/api/src/private/boolean.rs b/toolkit/components/glean/api/src/private/boolean.rs new file mode 100644 index 0000000000..8fd6dbbd0f --- /dev/null +++ b/toolkit/components/glean/api/src/private/boolean.rs @@ -0,0 +1,128 @@ +// 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 glean_core::traits::Boolean; + +use super::CommonMetricData; + +use crate::ipc::need_ipc; +use crate::private::MetricId; + +/// A boolean metric. +/// +/// Records a simple true or false value. +#[derive(Clone)] +pub enum BooleanMetric { + Parent(glean::private::BooleanMetric), + Child(BooleanMetricIpc), +} +#[derive(Clone, Debug)] +pub struct BooleanMetricIpc; + +impl BooleanMetric { + /// Create a new boolean metric. + pub fn new(_id: MetricId, meta: CommonMetricData) -> Self { + if need_ipc() { + BooleanMetric::Child(BooleanMetricIpc) + } else { + BooleanMetric::Parent(glean::private::BooleanMetric::new(meta)) + } + } + + #[cfg(test)] + pub(crate) fn child_metric(&self) -> Self { + match self { + BooleanMetric::Parent(_) => BooleanMetric::Child(BooleanMetricIpc), + BooleanMetric::Child(_) => panic!("Can't get a child metric from a child metric"), + } + } +} + +#[inherent(pub)] +impl Boolean for BooleanMetric { + /// Set to the specified boolean value. + /// + /// ## Arguments + /// + /// * `value` - the value to set. + fn set(&self, value: bool) { + match self { + BooleanMetric::Parent(p) => { + Boolean::set(&*p, value); + } + BooleanMetric::Child(_) => { + log::error!("Unable to set boolean metric in non-parent process. Ignoring."); + // TODO: Record an error. + } + } + } + + /// **Test-only API.** + /// + /// Get the currently stored value as a boolean. + /// This doesn't clear the stored value. + /// + /// ## Arguments + /// + /// * `ping_name` - the storage name to look into. + /// + /// ## Return value + /// + /// Returns the stored value or `None` if nothing stored. + fn test_get_value<'a, S: Into<Option<&'a str>>>(&self, ping_name: S) -> Option<bool> { + match self { + BooleanMetric::Parent(p) => p.test_get_value(ping_name), + BooleanMetric::Child(_) => { + panic!("Cannot get test value for boolean metric in non-parent process!",) + } + } + } +} + +#[cfg(test)] +mod test { + use crate::{common_test::*, ipc, metrics}; + + #[test] + fn sets_boolean_value() { + let _lock = lock_test(); + + let metric = &metrics::test_only_ipc::a_bool; + metric.set(true); + + assert!(metric.test_get_value("store1").unwrap()); + } + + #[test] + fn boolean_ipc() { + // BooleanMetric doesn't support IPC. + let _lock = lock_test(); + + let parent_metric = &metrics::test_only_ipc::a_bool; + + parent_metric.set(false); + + { + let child_metric = parent_metric.child_metric(); + + // scope for need_ipc RAII + let _raii = ipc::test_set_need_ipc(true); + + // Instrumentation calls do not panic. + child_metric.set(true); + + // (They also shouldn't do anything, + // but that's not something we can inspect in this test) + } + + assert!(ipc::replay_from_buf(&ipc::take_buf().unwrap()).is_ok()); + + assert!( + false == parent_metric.test_get_value("store1").unwrap(), + "Boolean metrics should only work in the parent process" + ); + } +} diff --git a/toolkit/components/glean/api/src/private/counter.rs b/toolkit/components/glean/api/src/private/counter.rs new file mode 100644 index 0000000000..207589adaf --- /dev/null +++ b/toolkit/components/glean/api/src/private/counter.rs @@ -0,0 +1,188 @@ +// 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 glean_core::traits::Counter; + +use super::CommonMetricData; + +use crate::ipc::{need_ipc, with_ipc_payload}; +use crate::private::MetricId; + +/// A counter metric. +/// +/// Used to count things. +/// The value can only be incremented, not decremented. +#[derive(Clone)] +pub enum CounterMetric { + Parent { + /// The metric's ID. + /// + /// **TEST-ONLY** - Do not use unless gated with `#[cfg(test)]`. + id: MetricId, + inner: glean::private::CounterMetric, + }, + Child(CounterMetricIpc), +} +#[derive(Clone, Debug)] +pub struct CounterMetricIpc(MetricId); + +impl CounterMetric { + /// Create a new counter metric. + pub fn new(id: MetricId, meta: CommonMetricData) -> Self { + if need_ipc() { + CounterMetric::Child(CounterMetricIpc(id)) + } else { + let inner = glean::private::CounterMetric::new(meta); + CounterMetric::Parent { id, inner } + } + } + + #[cfg(test)] + pub(crate) fn metric_id(&self) -> MetricId { + match self { + CounterMetric::Parent { id, .. } => *id, + CounterMetric::Child(c) => c.0, + } + } + + #[cfg(test)] + pub(crate) fn child_metric(&self) -> Self { + match self { + CounterMetric::Parent { id, .. } => CounterMetric::Child(CounterMetricIpc(*id)), + CounterMetric::Child(_) => panic!("Can't get a child metric from a child metric"), + } + } +} + +#[inherent(pub)] +impl Counter for CounterMetric { + /// Increase the counter by `amount`. + /// + /// ## Arguments + /// + /// * `amount` - The amount to increase by. Should be positive. + /// + /// ## Notes + /// + /// Logs an error if the `amount` is 0 or negative. + fn add(&self, amount: i32) { + match self { + CounterMetric::Parent { inner, .. } => { + Counter::add(&*inner, amount); + } + CounterMetric::Child(c) => { + with_ipc_payload(move |payload| { + if let Some(v) = payload.counters.get_mut(&c.0) { + *v += amount; + } else { + payload.counters.insert(c.0, amount); + } + }); + } + } + } + + /// **Test-only API.** + /// + /// Get the currently stored value as an integer. + /// This doesn't clear the stored value. + /// + /// ## Arguments + /// + /// * `ping_name` - the storage name to look into. + /// + /// ## Return value + /// + /// Returns the stored value or `None` if nothing stored. + fn test_get_value<'a, S: Into<Option<&'a str>>>(&self, ping_name: S) -> Option<i32> { + match self { + CounterMetric::Parent { inner, .. } => inner.test_get_value(ping_name), + CounterMetric::Child(c) => { + panic!("Cannot get test value for {:?} in non-parent process!", c.0) + } + } + } + + /// **Test-only API.** + /// + /// Gets the number of recorded errors for the given metric and 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 reported. + fn test_get_num_recorded_errors<'a, S: Into<Option<&'a str>>>( + &self, + error: glean::ErrorType, + ping_name: S, + ) -> i32 { + match self { + CounterMetric::Parent { inner, .. } => { + inner.test_get_num_recorded_errors(error, ping_name) + } + CounterMetric::Child(c) => panic!( + "Cannot get the number of recorded errors for {:?} in non-parent process!", + c.0 + ), + } + } +} + +#[cfg(test)] +mod test { + use crate::{common_test::*, ipc, metrics}; + + #[test] + fn sets_counter_value_parent() { + let _lock = lock_test(); + + let metric = &metrics::test_only_ipc::a_counter; + metric.add(1); + + assert_eq!(1, metric.test_get_value("store1").unwrap()); + } + + #[test] + fn sets_counter_value_child() { + let _lock = lock_test(); + + let parent_metric = &metrics::test_only_ipc::a_counter; + parent_metric.add(3); + + { + let child_metric = parent_metric.child_metric(); + + // scope for need_ipc RAII + let _raii = ipc::test_set_need_ipc(true); + let metric_id = child_metric.metric_id(); + + child_metric.add(42); + + ipc::with_ipc_payload(move |payload| { + assert!( + 42 == *payload.counters.get(&metric_id).unwrap(), + "Stored the correct value in the ipc payload" + ); + }); + } + + assert!( + false == ipc::need_ipc(), + "RAII dropped, should not need ipc any more" + ); + assert!(ipc::replay_from_buf(&ipc::take_buf().unwrap()).is_ok()); + + assert!( + 45 == parent_metric.test_get_value("store1").unwrap(), + "Values from the 'processes' should be summed" + ); + } +} diff --git a/toolkit/components/glean/api/src/private/datetime.rs b/toolkit/components/glean/api/src/private/datetime.rs new file mode 100644 index 0000000000..acaafdedd9 --- /dev/null +++ b/toolkit/components/glean/api/src/private/datetime.rs @@ -0,0 +1,241 @@ +// 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 super::{CommonMetricData, MetricId}; + +use super::TimeUnit; +use crate::ipc::need_ipc; +use chrono::{FixedOffset, TimeZone}; +use glean_core::traits::Datetime; + +/// A datetime metric of a certain resolution. +/// +/// Datetimes are used to make record when something happened according to the +/// client's clock. +#[derive(Clone)] +pub enum DatetimeMetric { + Parent(glean::private::DatetimeMetric), + Child(DatetimeMetricIpc), +} +#[derive(Debug, Clone)] +pub struct DatetimeMetricIpc; + +impl DatetimeMetric { + /// Create a new datetime metric. + pub fn new(_id: MetricId, meta: CommonMetricData, time_unit: TimeUnit) -> Self { + if need_ipc() { + DatetimeMetric::Child(DatetimeMetricIpc) + } else { + DatetimeMetric::Parent(glean::private::DatetimeMetric::new(meta, time_unit)) + } + } + + #[cfg(test)] + pub(crate) fn child_metric(&self) -> Self { + match self { + DatetimeMetric::Parent { .. } => DatetimeMetric::Child(DatetimeMetricIpc), + DatetimeMetric::Child(_) => panic!("Can't get a child metric from a child metric"), + } + } + + /// Sets the metric to a date/time including the timezone offset. + /// + /// # Arguments + /// + /// * `year` - the year to set the metric to. + /// * `month` - the month to set the metric to (1-12). + /// * `day` - the day to set the metric to (1-based). + /// * `hour` - the hour to set the metric to (0-23). + /// * `minute` - the minute to set the metric to. + /// * `second` - the second to set the metric to. + /// * `nano` - the nanosecond fraction to the last whole second. + /// * `offset_seconds` - the timezone difference, in seconds, for the Eastern + /// Hemisphere. Negative seconds mean Western Hemisphere. + #[cfg_attr(not(feature = "with-gecko"), allow(dead_code))] + #[allow(clippy::too_many_arguments)] + pub(crate) fn set_with_details( + &self, + year: i32, + month: u32, + day: u32, + hour: u32, + minute: u32, + second: u32, + nano: u32, + offset_seconds: i32, + ) { + match self { + DatetimeMetric::Parent(p) => { + let tz = FixedOffset::east_opt(offset_seconds); + if tz.is_none() { + log::error!( + "Unable to set datetime metric with invalid offset seconds {}", + offset_seconds + ); + // TODO: Record an error + return; + } + + let value = FixedOffset::east(offset_seconds) + .ymd_opt(year, month, day) + .and_hms_nano_opt(hour, minute, second, nano); + match value.single() { + Some(d) => p.set(Some(d)), + _ => { + log::error!("Unable to construct datetime") + // TODO: Record an error + } + } + } + DatetimeMetric::Child(_) => { + log::error!("Unable to set datetime metric in non-parent process. Ignoring."); + // TODO: Record an error. + } + } + } +} + +#[inherent(pub)] +impl Datetime for DatetimeMetric { + /// Sets the metric to a date/time which including the timezone offset. + /// + /// ## Arguments + /// + /// - `value` - The date and time and timezone value to set. + /// If None we use the current local time. + fn set(&self, value: Option<glean_core::metrics::Datetime>) { + match self { + DatetimeMetric::Parent(p) => { + Datetime::set(&*p, value); + } + DatetimeMetric::Child(_) => { + log::error!( + "Unable to set datetime metric DatetimeMetric in non-parent process. Ignoring." + ); + // TODO: Record an error. + } + } + } + + /// **Exported for test purposes.** + /// + /// Gets the currently stored value as a Datetime. + /// + /// The precision of this value is truncated to the `time_unit` precision. + /// + /// 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`. + fn test_get_value<'a, S: Into<Option<&'a str>>>( + &self, + ping_name: S, + ) -> Option<glean_core::metrics::Datetime> { + match self { + DatetimeMetric::Parent(p) => p.test_get_value(ping_name), + DatetimeMetric::Child(_) => { + panic!("Cannot get test value for DatetimeMetric in non-parent process!") + } + } + } + + /// **Exported for test purposes.** + /// + /// Gets the number of recorded errors for the given metric and 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 reported. + fn test_get_num_recorded_errors<'a, S: Into<Option<&'a str>>>( + &self, + error: glean::ErrorType, + ping_name: S, + ) -> i32 { + match self { + DatetimeMetric::Parent(p) => p.test_get_num_recorded_errors(error, ping_name), + DatetimeMetric::Child(_) => panic!("Cannot get the number of recorded errors for DatetimeMetric in non-parent process!"), + } + } +} + +#[cfg(test)] +mod test { + use chrono::{DateTime, FixedOffset, TimeZone}; + + use crate::{common_test::*, ipc, metrics}; + + #[test] + fn sets_datetime_value() { + let _lock = lock_test(); + + let metric = &metrics::test_only_ipc::a_date; + + let a_datetime = FixedOffset::east(5 * 3600) + .ymd(2020, 05, 07) + .and_hms(11, 58, 00); + metric.set(Some(a_datetime)); + + assert_eq!( + DateTime::parse_from_rfc3339("2020-05-07T11:58:00+05:00").unwrap(), + metric.test_get_value("store1").unwrap() + ); + } + + #[test] + fn sets_datetime_value_with_details() { + let _lock = lock_test(); + + let metric = &metrics::test_only_ipc::a_date; + + metric.set_with_details(2020, 05, 07, 11, 58, 0, 0, 5 * 3600); + + assert_eq!( + DateTime::parse_from_rfc3339("2020-05-07T11:58:00+05:00").unwrap(), + metric.test_get_value("store1").unwrap() + ); + } + + #[test] + fn datetime_ipc() { + // DatetimeMetric doesn't support IPC. + let _lock = lock_test(); + + let parent_metric = &metrics::test_only_ipc::a_date; + + // Instrumentation calls do not panic. + let a_datetime = FixedOffset::east(5 * 3600) + .ymd(2020, 10, 13) + .and_hms(16, 41, 00); + parent_metric.set(Some(a_datetime)); + + { + let child_metric = parent_metric.child_metric(); + + let _raii = ipc::test_set_need_ipc(true); + + let a_datetime = FixedOffset::east(0).ymd(2018, 4, 7).and_hms(12, 01, 00); + child_metric.set(Some(a_datetime)); + + // (They also shouldn't do anything, + // but that's not something we can inspect in this test) + } + + assert!(ipc::replay_from_buf(&ipc::take_buf().unwrap()).is_ok()); + + assert_eq!( + DateTime::parse_from_rfc3339("2020-10-13T16:41:00+05:00").unwrap(), + parent_metric.test_get_value("store1").unwrap() + ); + } +} diff --git a/toolkit/components/glean/api/src/private/event.rs b/toolkit/components/glean/api/src/private/event.rs new file mode 100644 index 0000000000..306376c269 --- /dev/null +++ b/toolkit/components/glean/api/src/private/event.rs @@ -0,0 +1,237 @@ +// 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 std::collections::HashMap; + +use inherent::inherent; + +use super::{CommonMetricData, Instant, MetricId, RecordedEvent}; + +use crate::ipc::{need_ipc, with_ipc_payload}; + +use glean_core::traits::Event; +pub use glean_core::traits::{EventRecordingError, ExtraKeys, NoExtraKeys}; + +/// An event metric. +/// +/// Events allow recording of e.g. individual occurences of user actions, say +/// every time a view was open and from where. Each time you record an event, it +/// records a timestamp, the event's name and a set of custom values. +pub enum EventMetric<K> { + Parent { + /// The metric's ID. + /// + /// **TEST-ONLY** - Do not use unless gated with `#[cfg(test)]`. + id: MetricId, + inner: glean::private::EventMetric<K>, + }, + Child(EventMetricIpc), +} + +#[derive(Debug)] +pub struct EventMetricIpc(MetricId); + +impl<K: 'static + ExtraKeys + Send + Sync> EventMetric<K> { + /// Create a new event metric. + pub fn new(id: MetricId, meta: CommonMetricData) -> Self { + if need_ipc() { + EventMetric::Child(EventMetricIpc(id)) + } else { + let inner = glean::private::EventMetric::new(meta); + EventMetric::Parent { id, inner } + } + } + + #[cfg(test)] + pub(crate) fn child_metric(&self) -> Self { + match self { + EventMetric::Parent { id, .. } => EventMetric::Child(EventMetricIpc(*id)), + EventMetric::Child(_) => panic!("Can't get a child metric from a child metric"), + } + } +} + +#[inherent(pub)] +impl<K: 'static + ExtraKeys + Send + Sync> Event for EventMetric<K> { + type Extra = K; + + fn record<M: Into<Option<HashMap<K, String>>>>(&self, extra: M) { + match self { + EventMetric::Parent { inner, .. } => { + inner.record(extra); + } + EventMetric::Child(c) => { + let extra = extra.into().map(|hash_map| { + hash_map + .iter() + .map(|(k, v)| (k.index(), v.clone())) + .collect() + }); + let now = Instant::now(); + with_ipc_payload(move |payload| { + if let Some(v) = payload.events.get_mut(&c.0) { + v.push((now, extra)); + } else { + let mut v = vec![]; + v.push((now, extra)); + payload.events.insert(c.0, v); + } + }); + } + } + } + + fn test_get_value<'a, S: Into<Option<&'a str>>>( + &self, + ping_name: S, + ) -> Option<Vec<RecordedEvent>> { + match self { + EventMetric::Parent { inner, .. } => inner.test_get_value(ping_name), + EventMetric::Child(_) => { + panic!("Cannot get test value for event metric in non-parent process!",) + } + } + } + + fn test_get_num_recorded_errors<'a, S: Into<Option<&'a str>>>( + &self, + error: glean::ErrorType, + ping_name: S, + ) -> i32 { + match self { + EventMetric::Parent { inner, .. } => { + inner.test_get_num_recorded_errors(error, ping_name) + } + EventMetric::Child(c) => panic!( + "Cannot get the number of recorded errors for {:?} in non-parent process!", + c.0 + ), + } + } +} + +#[cfg(test)] +mod test { + use super::*; + use crate::{common_test::*, ipc, metrics}; + + #[test] + #[ignore] // TODO: Enable them back when bug 1673668 lands. + fn smoke_test_event() { + let _lock = lock_test(); + + let metric = EventMetric::<NoExtraKeys>::new( + 0.into(), + CommonMetricData { + name: "event_metric".into(), + category: "telemetry".into(), + send_in_pings: vec!["store1".into()], + disabled: false, + ..Default::default() + }, + ); + + // No extra keys + metric.record(None); + + let recorded = metric.test_get_value("store1").unwrap(); + + assert!(recorded.iter().any(|e| e.name == "event_metric")); + } + + #[test] + #[ignore] // TODO: Enable them back when bug 1673668 lands. + fn smoke_test_event_with_extra() { + let _lock = lock_test(); + + #[derive(Clone, Copy, Debug, Hash, Eq, PartialEq)] + enum TestKeys { + Extra1, + } + + impl ExtraKeys for TestKeys { + const ALLOWED_KEYS: &'static [&'static str] = &["extra1"]; + + fn index(self) -> i32 { + self as i32 + } + } + + let metric = EventMetric::<TestKeys>::new( + 0.into(), + CommonMetricData { + name: "event_metric_with_extra".into(), + category: "telemetry".into(), + send_in_pings: vec!["store1".into()], + disabled: false, + ..Default::default() + }, + ); + + // No extra keys + metric.record(None); + + // A valid extra key + let mut map = HashMap::new(); + map.insert(TestKeys::Extra1, "a-valid-value".into()); + metric.record(map); + + let recorded = metric.test_get_value("store1").unwrap(); + + let events: Vec<&RecordedEvent> = recorded + .iter() + .filter(|&e| e.category == "telemetry" && e.name == "event_metric_with_extra") + .collect(); + assert_eq!(events.len(), 2); + assert_eq!(events[0].extra, None); + assert!(events[1].extra.as_ref().unwrap().get("extra1").unwrap() == "a-valid-value"); + } + + #[test] + #[ignore] // TODO: Enable them back when bug 1673668 lands. + fn event_ipc() { + use metrics::test_only_ipc::AnEventKeys; + + let _lock = lock_test(); + + let parent_metric = &metrics::test_only_ipc::an_event; + + // No extra keys + parent_metric.record(None); + + { + let child_metric = parent_metric.child_metric(); + + // scope for need_ipc RAII. + let _raii = ipc::test_set_need_ipc(true); + + child_metric.record(None); + + let mut map = HashMap::new(); + map.insert(AnEventKeys::Extra1, "a-child-value".into()); + child_metric.record(map); + } + + // Record in the parent after the child. + let mut map = HashMap::new(); + map.insert(AnEventKeys::Extra1, "a-valid-value".into()); + parent_metric.record(map); + + let recorded = parent_metric.test_get_value("store1").unwrap(); + + let events: Vec<&RecordedEvent> = recorded + .iter() + .filter(|&e| e.category == "test_only.ipc" && e.name == "an_event") + .collect(); + assert_eq!(events.len(), 2); + assert_eq!(events[0].extra, None); + + assert!(events[1].extra.as_ref().unwrap().get("extra1").unwrap() == "a-valid-value"); + // TODO: implement replay. See bug 1646165. Then change and add asserts for a-child-value. + // Ensure the replay values apply without error, at least. + let buf = ipc::take_buf().unwrap(); + assert!(buf.len() > 0); + assert!(ipc::replay_from_buf(&ipc::take_buf().unwrap()).is_ok()); + } +} diff --git a/toolkit/components/glean/api/src/private/labeled.rs b/toolkit/components/glean/api/src/private/labeled.rs new file mode 100644 index 0000000000..e7e4de61d2 --- /dev/null +++ b/toolkit/components/glean/api/src/private/labeled.rs @@ -0,0 +1,333 @@ +// 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 super::{BooleanMetric, CommonMetricData, CounterMetric, ErrorType, MetricId, StringMetric}; +use crate::ipc::need_ipc; + +/// Sealed traits protect against downstream implementations. +/// +/// We wrap it in a private module that is inaccessible outside of this module. +mod private { + use super::{BooleanMetric, CounterMetric, StringMetric}; + + /// The sealed trait. + /// + /// This allows us to define which FOG metrics can be used + /// as labeled types. + pub trait Sealed { + type GleanMetric: glean::private::AllowLabeled + Clone; + } + + // `LabeledMetric<BooleanMetric>` is possible. + // + // See [Labeled Booleans](https://mozilla.github.io/glean/book/user/metrics/labeled_booleans.html). + impl Sealed for BooleanMetric { + type GleanMetric = glean::private::BooleanMetric; + } + + // `LabeledMetric<StringMetric>` is possible. + // + // See [Labeled Strings](https://mozilla.github.io/glean/book/user/metrics/labeled_strings.html). + impl Sealed for StringMetric { + type GleanMetric = glean::private::StringMetric; + } + + // `LabeledMetric<CounterMetric>` is possible. + // + // See [Labeled Counters](https://mozilla.github.io/glean/book/user/metrics/labeled_counters.html). + impl Sealed for CounterMetric { + type GleanMetric = glean::private::CounterMetric; + } +} + +/// Marker trait for metrics that can be nested inside a labeled metric. +/// +/// This trait is sealed and cannot be implemented for types outside this crate. +pub trait AllowLabeled: private::Sealed {} + +// Implement the trait for everything we marked as allowed. +impl<T> AllowLabeled for T where T: private::Sealed {} + +/// A labeled metric. +/// +/// Labeled metrics allow to record multiple sub-metrics of the same type under different string labels. +/// +/// ## Example +/// +/// The following piece of code will be generated by `glean_parser`: +/// +/// ```rust,ignore +/// use glean::metrics::{LabeledMetric, BooleanMetric, CommonMetricData, Lifetime}; +/// use once_cell::sync::Lazy; +/// +/// mod error { +/// pub static seen_one: Lazy<LabeledMetric<BooleanMetric>> = Lazy::new(|| LabeledMetric::new(CommonMetricData { +/// name: "seen_one".into(), +/// category: "error".into(), +/// send_in_pings: vec!["ping".into()], +/// disabled: false, +/// lifetime: Lifetime::Ping, +/// ..Default::default() +/// }, None)); +/// } +/// ``` +/// +/// It can then be used with: +/// +/// ```rust,ignore +/// errro::seen_one.get("upload").set(true); +/// ``` +//#[derive(Debug)] +pub struct LabeledMetric<T: AllowLabeled> { + // TODO: the `_id` is currently not needed, hence the + // prefix, but it will be needed when adding IPC support + // to this type. + /// The metric ID of the underlying metric. + _id: MetricId, + + /// Wrapping the underlying core metric. + /// + /// We delegate all functionality to this and wrap it up again in our own metric type. + core: glean::private::LabeledMetric<T::GleanMetric>, +} + +impl<T> LabeledMetric<T> +where + T: AllowLabeled, +{ + /// Create a new labeled metric from the given metric instance and optional list of labels. + /// + /// See [`get`](#method.get) for information on how static or dynamic labels are handled. + pub fn new( + id: MetricId, + meta: CommonMetricData, + labels: Option<Vec<String>>, + ) -> LabeledMetric<T> { + let core = glean::private::LabeledMetric::new(meta, labels); + LabeledMetric { _id: id, core } + } +} + +#[inherent(pub)] +impl<U> glean_core::traits::Labeled<U::GleanMetric> for LabeledMetric<U> +where + U: AllowLabeled + Clone, +{ + /// Gets a specific metric for a given label. + /// + /// If a set of acceptable labels were specified in the `metrics.yaml` file, + /// and the given label is not in the set, it will be recorded under the special `OTHER_LABEL` label. + /// + /// If a set of acceptable labels was not specified in the `metrics.yaml` file, + /// only the first 16 unique labels will be used. + /// After that, any additional labels will be recorded under the special `OTHER_LABEL` label. + /// + /// Labels must be `snake_case` and less than 30 characters. + /// If an invalid label is used, the metric will be recorded in the special `OTHER_LABEL` label. + fn get(&self, label: &str) -> U::GleanMetric { + if need_ipc() { + panic!("Use of labeled metrics in IPC land not yet implemented!"); + } else { + self.core.get(label) + } + } + + /// **Exported for test purposes.** + /// + /// Gets the number of recorded errors for the given metric and 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 reported. + fn test_get_num_recorded_errors<'a, S: Into<Option<&'a str>>>( + &self, + error: ErrorType, + ping_name: S, + ) -> i32 { + if need_ipc() { + panic!("Use of labeled metrics in IPC land not yet implemented!"); + } else { + self.core.test_get_num_recorded_errors(error, ping_name) + } + } +} + +#[cfg(test)] +mod test { + use once_cell::sync::Lazy; + + use super::*; + use crate::common_test::*; + + // Smoke test for what should be the generated code. + static GLOBAL_METRIC: Lazy<LabeledMetric<BooleanMetric>> = Lazy::new(|| { + LabeledMetric::new( + 0.into(), + CommonMetricData { + name: "global".into(), + category: "metric".into(), + send_in_pings: vec!["ping".into()], + disabled: false, + ..Default::default() + }, + None, + ) + }); + + #[test] + fn smoke_test_global_metric() { + let _lock = lock_test(); + + GLOBAL_METRIC.get("a_value").set(true); + assert_eq!( + true, + GLOBAL_METRIC.get("a_value").test_get_value("ping").unwrap() + ); + } + + #[test] + fn sets_labeled_bool_metrics() { + let _lock = lock_test(); + let store_names: Vec<String> = vec!["store1".into()]; + + let metric: LabeledMetric<BooleanMetric> = LabeledMetric::new( + 0.into(), + CommonMetricData { + name: "bool".into(), + category: "labeled".into(), + send_in_pings: store_names, + disabled: false, + ..Default::default() + }, + None, + ); + + metric.get("upload").set(true); + + assert!(metric.get("upload").test_get_value("store1").unwrap()); + assert_eq!(None, metric.get("download").test_get_value("store1")); + } + + #[test] + fn sets_labeled_string_metrics() { + let _lock = lock_test(); + let store_names: Vec<String> = vec!["store1".into()]; + + let metric: LabeledMetric<StringMetric> = LabeledMetric::new( + 0.into(), + CommonMetricData { + name: "string".into(), + category: "labeled".into(), + send_in_pings: store_names, + disabled: false, + ..Default::default() + }, + None, + ); + + metric.get("upload").set("Glean"); + + assert_eq!( + "Glean", + metric.get("upload").test_get_value("store1").unwrap() + ); + assert_eq!(None, metric.get("download").test_get_value("store1")); + } + + #[test] + fn sets_labeled_counter_metrics() { + let _lock = lock_test(); + let store_names: Vec<String> = vec!["store1".into()]; + + let metric: LabeledMetric<CounterMetric> = LabeledMetric::new( + 0.into(), + CommonMetricData { + name: "counter".into(), + category: "labeled".into(), + send_in_pings: store_names, + disabled: false, + ..Default::default() + }, + None, + ); + + metric.get("upload").add(10); + + assert_eq!(10, metric.get("upload").test_get_value("store1").unwrap()); + assert_eq!(None, metric.get("download").test_get_value("store1")); + } + + #[test] + fn records_errors() { + let _lock = lock_test(); + let store_names: Vec<String> = vec!["store1".into()]; + + let metric: LabeledMetric<BooleanMetric> = LabeledMetric::new( + 0.into(), + CommonMetricData { + name: "bool".into(), + category: "labeled".into(), + send_in_pings: store_names, + disabled: false, + ..Default::default() + }, + None, + ); + + metric + .get("this_string_has_more_than_thirty_characters") + .set(true); + + assert_eq!( + 1, + metric.test_get_num_recorded_errors(ErrorType::InvalidLabel, None) + ); + } + + #[test] + fn predefined_labels() { + let _lock = lock_test(); + let store_names: Vec<String> = vec!["store1".into()]; + + let metric: LabeledMetric<BooleanMetric> = LabeledMetric::new( + 0.into(), + CommonMetricData { + name: "bool".into(), + category: "labeled".into(), + send_in_pings: store_names, + disabled: false, + ..Default::default() + }, + Some(vec!["label1".into(), "label2".into()]), + ); + + metric.get("label1").set(true); + metric.get("label2").set(false); + metric.get("not_a_label").set(true); + + assert_eq!(true, metric.get("label1").test_get_value("store1").unwrap()); + assert_eq!( + false, + metric.get("label2").test_get_value("store1").unwrap() + ); + // The label not in the predefined set is recorded to the `other` bucket. + assert_eq!( + true, + metric.get("__other__").test_get_value("store1").unwrap() + ); + + assert_eq!( + 0, + metric.test_get_num_recorded_errors(ErrorType::InvalidLabel, None) + ); + } +} diff --git a/toolkit/components/glean/api/src/private/memory_distribution.rs b/toolkit/components/glean/api/src/private/memory_distribution.rs new file mode 100644 index 0000000000..4db7f73a2b --- /dev/null +++ b/toolkit/components/glean/api/src/private/memory_distribution.rs @@ -0,0 +1,193 @@ +// 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 super::{CommonMetricData, DistributionData, MemoryUnit, MetricId}; + +use glean_core::traits::MemoryDistribution; + +use crate::ipc::{need_ipc, with_ipc_payload}; + +/// A memory distribution metric. +/// +/// Memory distributions are used to accumulate and store memory measurements for analyzing distributions of the memory data. +#[derive(Clone)] +pub enum MemoryDistributionMetric { + Parent { + /// The metric's ID. + /// + /// **TEST-ONLY** - Do not use unless gated with `#[cfg(test)]`. + id: MetricId, + inner: glean::private::MemoryDistributionMetric, + }, + Child(MemoryDistributionMetricIpc), +} +#[derive(Clone, Debug)] +pub struct MemoryDistributionMetricIpc(MetricId); + +impl MemoryDistributionMetric { + /// Create a new memory distribution metric. + pub fn new(id: MetricId, meta: CommonMetricData, memory_unit: MemoryUnit) -> Self { + if need_ipc() { + MemoryDistributionMetric::Child(MemoryDistributionMetricIpc(id)) + } else { + let inner = glean::private::MemoryDistributionMetric::new(meta, memory_unit); + MemoryDistributionMetric::Parent { id, inner } + } + } + + #[cfg(test)] + pub(crate) fn child_metric(&self) -> Self { + match self { + MemoryDistributionMetric::Parent { id, .. } => { + MemoryDistributionMetric::Child(MemoryDistributionMetricIpc(*id)) + } + MemoryDistributionMetric::Child(_) => { + panic!("Can't get a child metric from a child metric") + } + } + } +} + +#[inherent(pub)] +impl MemoryDistribution for MemoryDistributionMetric { + /// Accumulates the provided sample in the metric. + /// + /// ## Arguments + /// + /// * `sample` - The sample to be recorded by the metric. The sample is assumed to be in the + /// configured memory unit of the metric. + /// + /// ## Notes + /// + /// Values bigger than 1 Terabyte (2<sup>40</sup> bytes) are truncated + /// and an `ErrorType::InvalidValue` error is recorded. + fn accumulate(&self, sample: u64) { + match self { + MemoryDistributionMetric::Parent { inner, .. } => { + MemoryDistribution::accumulate(&*inner, sample); + } + MemoryDistributionMetric::Child(c) => { + with_ipc_payload(move |payload| { + if let Some(v) = payload.memory_samples.get_mut(&c.0) { + v.push(sample); + } else { + payload.memory_samples.insert(c.0, vec![sample]); + } + }); + } + } + } + + /// **Test-only API.** + /// + /// Get the currently-stored histogram as a DistributionData of the serialized value. + /// This doesn't clear the stored value. + /// + /// ## Arguments + /// + /// * `ping_name` - the storage name to look into. + /// + /// ## Return value + /// + /// Returns the stored value or `None` if nothing stored. + fn test_get_value<'a, S: Into<Option<&'a str>>>( + &self, + ping_name: S, + ) -> Option<DistributionData> { + match self { + MemoryDistributionMetric::Parent { inner, .. } => inner.test_get_value(ping_name), + MemoryDistributionMetric::Child(c) => { + panic!("Cannot get test value for {:?} in non-parent process!", c.0) + } + } + } + + /// **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. + fn test_get_num_recorded_errors<'a, S: Into<Option<&'a str>>>( + &self, + error: glean::ErrorType, + ping_name: S, + ) -> i32 { + match self { + MemoryDistributionMetric::Parent { inner, .. } => { + inner.test_get_num_recorded_errors(error, ping_name) + } + MemoryDistributionMetric::Child(c) => panic!( + "Cannot get the number of recorded errors for {:?} in non-parent process!", + c.0 + ), + } + } +} + +#[cfg(test)] +mod test { + use super::*; + use crate::{common_test::*, ipc, metrics}; + + #[test] + fn smoke_test_memory_distribution() { + let _lock = lock_test(); + + let metric = MemoryDistributionMetric::new( + 0.into(), + CommonMetricData { + name: "memory_distribution_metric".into(), + category: "telemetry".into(), + send_in_pings: vec!["store1".into()], + disabled: false, + ..Default::default() + }, + MemoryUnit::Kilobyte, + ); + + metric.accumulate(42); + + let metric_data = metric.test_get_value("store1").unwrap(); + assert_eq!(1, metric_data.values[&42494]); + assert_eq!(0, metric_data.values[&44376]); + assert_eq!(43008, metric_data.sum); + } + + #[test] + fn memory_distribution_child() { + let _lock = lock_test(); + + let parent_metric = &metrics::test_only_ipc::a_memory_dist; + parent_metric.accumulate(42); + + { + let child_metric = parent_metric.child_metric(); + + // scope for need_ipc RAII + let _raii = ipc::test_set_need_ipc(true); + child_metric.accumulate(13 * 9); + } + + let metric_data = parent_metric.test_get_value("store1").unwrap(); + assert_eq!(1, metric_data.values[&42494]); + assert_eq!(0, metric_data.values[&44376]); + assert_eq!(43008, metric_data.sum); + + // TODO: implement replay. See bug 1646165. + // For now, let's ensure there's something in the buffer and replay doesn't error. + let buf = ipc::take_buf().unwrap(); + assert!(buf.len() > 0); + assert!(ipc::replay_from_buf(&buf).is_ok()); + } +} diff --git a/toolkit/components/glean/api/src/private/mod.rs b/toolkit/components/glean/api/src/private/mod.rs new file mode 100644 index 0000000000..1e77e4b49e --- /dev/null +++ b/toolkit/components/glean/api/src/private/mod.rs @@ -0,0 +1,90 @@ +// 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/. + +//! The different metric types supported by the Glean SDK to handle data. + +use std::convert::TryFrom; +use std::time::{SystemTime, UNIX_EPOCH}; + +use serde::{Deserialize, Serialize}; + +// Re-export of `glean_core` types we can re-use. +// That way a user only needs to depend on this crate, not on glean_core (and there can't be a +// version mismatch). +pub use glean_core::{ + metrics::DistributionData, metrics::MemoryUnit, metrics::RecordedEvent, metrics::TimeUnit, + CommonMetricData, ErrorType, Lifetime, +}; + +mod boolean; +mod counter; +mod datetime; +mod event; +mod labeled; +mod memory_distribution; +mod ping; +pub(crate) mod string; +mod string_list; +mod timespan; +mod timing_distribution; +mod uuid; + +pub use self::boolean::BooleanMetric; +pub use self::counter::CounterMetric; +pub use self::datetime::DatetimeMetric; +pub use self::event::{EventMetric, EventRecordingError, ExtraKeys, NoExtraKeys}; +pub use self::labeled::LabeledMetric; +pub use self::memory_distribution::MemoryDistributionMetric; +pub use self::ping::Ping; +pub use self::string::StringMetric; +pub use self::string_list::StringListMetric; +pub use self::timespan::TimespanMetric; +pub use self::timing_distribution::TimingDistributionMetric; +pub use self::uuid::UuidMetric; + +/// An instant in time. +/// +/// Similar to [`std::time::Instant`](https://doc.rust-lang.org/std/time/struct.Instant.html), +/// but much simpler in that we explicitly expose that it's just an integer. +/// +/// This is needed, as the current `glean-core` API expects timestamps as integers. +/// We probably should move this API into `glean-core` directly. +/// See [Bug 1619253](https://bugzilla.mozilla.org/show_bug.cgi?id=1619253). +#[derive(Clone, Debug, Deserialize, Serialize)] +pub struct Instant(u64); + +impl Instant { + /// Returns an instant corresponding to "now". + fn now() -> Instant { + let now = SystemTime::now() + .duration_since(UNIX_EPOCH) + .expect("SystemTime before UNIX epoch!"); + let now = now.as_nanos(); + + match u64::try_from(now) { + Ok(now) => Instant(now), + Err(_) => { + // Greetings to 2554 from 2020! + panic!("timestamp exceeds value range") + } + } + } +} + +/// Uniquely identifies a single metric within its metric type. +#[derive(Debug, PartialEq, Eq, Hash, Copy, Clone, Deserialize, Serialize)] +#[repr(transparent)] +pub struct MetricId(u32); + +impl MetricId { + pub fn new(id: u32) -> Self { + Self(id) + } +} + +impl From<u32> for MetricId { + fn from(id: u32) -> Self { + Self(id) + } +} diff --git a/toolkit/components/glean/api/src/private/ping.rs b/toolkit/components/glean/api/src/private/ping.rs new file mode 100644 index 0000000000..2b11f1ad5b --- /dev/null +++ b/toolkit/components/glean/api/src/private/ping.rs @@ -0,0 +1,89 @@ +// 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 crate::ipc::need_ipc; + +/// A Glean ping. +/// +/// See [Glean Pings](https://mozilla.github.io/glean/book/user/pings/index.html). +#[derive(Clone, Debug)] +pub enum Ping { + Parent(glean::private::PingType), + Child, +} + +impl Ping { + /// Create a new ping type for the given name, whether to include the client ID and whether to + /// send this ping empty. + /// + /// ## Arguments + /// + /// * `name` - The name of the ping. + /// * `include_client_id` - Whether to include the client ID in the assembled ping when submitting. + /// * `send_if_empty` - Whether the ping should be sent empty or not. + /// * `reason_codes` - The valid reason codes for this ping. + pub fn new<S: Into<String>>( + name: S, + include_client_id: bool, + send_if_empty: bool, + reason_codes: Vec<String>, + ) -> Self { + if need_ipc() { + Ping::Child + } else { + Ping::Parent(glean::private::PingType::new( + name, + include_client_id, + send_if_empty, + reason_codes, + )) + } + } +} + +#[inherent(pub)] +impl glean_core::traits::Ping for Ping { + /// Submits the ping for eventual uploading + /// + /// # Arguments + /// + /// * `reason` - the reason the ping was triggered. Included in the + /// `ping_info.reason` part of the payload. + fn submit(&self, reason: Option<&str>) { + match self { + Ping::Parent(p) => { + glean_core::traits::Ping::submit(p, reason); + } + Ping::Child => { + log::error!( + "Unable to submit ping {:?} in non-main process. Ignoring.", + self + ); + // TODO: Record an error. + } + }; + } +} + +#[cfg(test)] +mod test { + use once_cell::sync::Lazy; + + use super::*; + use crate::common_test::*; + + // Smoke test for what should be the generated code. + static PROTOTYPE_PING: Lazy<Ping> = Lazy::new(|| Ping::new("prototype", false, true, vec![])); + + #[test] + fn smoke_test_custom_ping() { + let _lock = lock_test(); + + // We can only check that nothing explodes. + // More comprehensive tests are blocked on bug 1673660. + PROTOTYPE_PING.submit(None); + } +} diff --git a/toolkit/components/glean/api/src/private/string.rs b/toolkit/components/glean/api/src/private/string.rs new file mode 100644 index 0000000000..9beb11f3f1 --- /dev/null +++ b/toolkit/components/glean/api/src/private/string.rs @@ -0,0 +1,185 @@ +// 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 super::{CommonMetricData, MetricId}; + +use crate::ipc::need_ipc; + +/// A string metric. +/// +/// Record an Unicode string value with arbitrary content. +/// Strings are length-limited to `MAX_LENGTH_VALUE` bytes. +/// +/// # Example +/// +/// The following piece of code will be generated by `glean_parser`: +/// +/// ```rust,ignore +/// use glean::metrics::{StringMetric, CommonMetricData, Lifetime}; +/// use once_cell::sync::Lazy; +/// +/// mod browser { +/// pub static search_engine: Lazy<StringMetric> = Lazy::new(|| StringMetric::new(CommonMetricData { +/// name: "search_engine".into(), +/// category: "browser".into(), +/// lifetime: Lifetime::Ping, +/// disabled: false, +/// dynamic_label: None +/// })); +/// } +/// ``` +/// +/// It can then be used with: +/// +/// ```rust,ignore +/// browser::search_engine.set("websearch"); +/// ``` +#[derive(Clone)] +pub enum StringMetric { + Parent(glean::private::StringMetric), + Child(StringMetricIpc), +} +#[derive(Clone, Debug)] +pub struct StringMetricIpc; + +impl StringMetric { + /// Create a new string metric. + pub fn new(_id: MetricId, meta: CommonMetricData) -> Self { + if need_ipc() { + StringMetric::Child(StringMetricIpc) + } else { + StringMetric::Parent(glean::private::StringMetric::new(meta)) + } + } + + #[cfg(test)] + pub(crate) fn child_metric(&self) -> Self { + match self { + StringMetric::Parent(_) => StringMetric::Child(StringMetricIpc), + StringMetric::Child(_) => panic!("Can't get a child metric from a child metric"), + } + } +} + +#[inherent(pub)] +impl glean_core::traits::String for StringMetric { + /// Sets to the specified value. + /// + /// # Arguments + /// + /// * `value` - The string to set the metric to. + /// + /// ## Notes + /// + /// Truncates the value if it is longer than `MAX_STRING_LENGTH` bytes and logs an error. + fn set<S: Into<std::string::String>>(&self, value: S) { + match self { + StringMetric::Parent(p) => { + glean_core::traits::String::set(&*p, value); + } + StringMetric::Child(_) => { + log::error!("Unable to set string metric in non-main process. Ignoring."); + // TODO: Record an error. + } + }; + } + + /// **Exported for test purposes.** + /// + /// Gets the currently stored value as a string. + /// + /// 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`. + fn test_get_value<'a, S: Into<Option<&'a str>>>( + &self, + ping_name: S, + ) -> Option<std::string::String> { + match self { + StringMetric::Parent(p) => p.test_get_value(ping_name), + StringMetric::Child(_) => { + panic!("Cannot get test value for string metric in non-parent process!") + } + } + } + + /// **Exported for test purposes.** + /// + /// Gets the number of recorded errors for the given metric and 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 reported. + fn test_get_num_recorded_errors<'a, S: Into<Option<&'a str>>>( + &self, + error: glean::ErrorType, + ping_name: S, + ) -> i32 { + match self { + StringMetric::Parent(p) => p.test_get_num_recorded_errors(error, ping_name), + StringMetric::Child(_) => panic!( + "Cannot get the number of recorded errors for string metric in non-parent process!" + ), + } + } +} + +#[cfg(test)] +mod test { + use crate::{common_test::*, ipc, metrics}; + + #[test] + fn sets_string_value() { + let _lock = lock_test(); + + let metric = &metrics::test_only_ipc::a_string; + + metric.set("test_string_value"); + + assert_eq!( + "test_string_value", + metric.test_get_value("store1").unwrap() + ); + } + + #[test] + fn string_ipc() { + // StringMetric doesn't support IPC. + let _lock = lock_test(); + + let parent_metric = &metrics::test_only_ipc::a_string; + + parent_metric.set("test_parent_value"); + + { + let child_metric = parent_metric.child_metric(); + + let _raii = ipc::test_set_need_ipc(true); + + // Instrumentation calls do not panic. + child_metric.set("test_string_value"); + + // (They also shouldn't do anything, + // but that's not something we can inspect in this test) + } + + assert!(ipc::replay_from_buf(&ipc::take_buf().unwrap()).is_ok()); + + assert!( + "test_parent_value" == parent_metric.test_get_value("store1").unwrap(), + "String metrics should only work in the parent process" + ); + } +} diff --git a/toolkit/components/glean/api/src/private/string_list.rs b/toolkit/components/glean/api/src/private/string_list.rs new file mode 100644 index 0000000000..a583b1fced --- /dev/null +++ b/toolkit/components/glean/api/src/private/string_list.rs @@ -0,0 +1,212 @@ +// 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 super::{CommonMetricData, MetricId}; + +use glean_core::traits::StringList; + +use crate::ipc::{need_ipc, with_ipc_payload}; + +/// A string list metric. +/// +/// This allows appending a string value with arbitrary content to a list. +#[derive(Clone)] +pub enum StringListMetric { + Parent { + /// The metric's ID. + /// + /// **TEST-ONLY** - Do not use unless gated with `#[cfg(test)]`. + id: MetricId, + inner: glean::private::StringListMetric, + }, + Child(StringListMetricIpc), +} +#[derive(Clone, Debug)] +pub struct StringListMetricIpc(MetricId); + +impl StringListMetric { + /// Create a new string list metric. + pub fn new(id: MetricId, meta: CommonMetricData) -> Self { + if need_ipc() { + StringListMetric::Child(StringListMetricIpc(id)) + } else { + let inner = glean::private::StringListMetric::new(meta); + StringListMetric::Parent { id, inner } + } + } + + #[cfg(test)] + pub(crate) fn child_metric(&self) -> Self { + match self { + StringListMetric::Parent { id, .. } => { + StringListMetric::Child(StringListMetricIpc(*id)) + } + StringListMetric::Child(_) => panic!("Can't get a child metric from a child metric"), + } + } +} + +#[inherent(pub)] +impl StringList for StringListMetric { + /// Add a new string to the list. + /// + /// ## Arguments + /// + /// * `value` - The string to add. + /// + /// ## Notes + /// + /// Truncates the value if it is longer than `MAX_STRING_LENGTH` bytes and logs an error. + /// See [String list metric limits](https://mozilla.github.io/glean/book/user/metrics/string_list.html#limits). + fn add<S: Into<String>>(&self, value: S) { + match self { + StringListMetric::Parent { inner, .. } => { + StringList::add(&*inner, value); + } + StringListMetric::Child(c) => { + with_ipc_payload(move |payload| { + if let Some(v) = payload.string_lists.get_mut(&c.0) { + v.push(value.into()); + } else { + let mut v = vec![]; + v.push(value.into()); + payload.string_lists.insert(c.0, v); + } + }); + } + } + } + + /// Set to a specific list of strings. + /// + /// ## Arguments + /// + /// * `value` - The list of string to set the metric to. + /// + /// ## Notes + /// + /// If passed an empty list, records an error and returns. + /// Truncates the list if it is longer than `MAX_LIST_LENGTH` and logs an error. + /// Truncates any value in the list if it is longer than `MAX_STRING_LENGTH` and logs an error. + fn set(&self, value: Vec<String>) { + match self { + StringListMetric::Parent { inner, .. } => { + StringList::set(&*inner, value); + } + StringListMetric::Child(c) => { + log::error!( + "Unable to set string list metric {:?} in non-main process. Ignoring.", + c.0 + ); + // TODO: Record an error. + } + } + } + + /// **Test-only API.** + /// + /// Get the currently stored values. + /// This doesn't clear the stored value. + /// + /// ## Arguments + /// + /// * `storage_name` - the storage name to look into. + /// + /// ## Return value + /// + /// Returns the stored value or `None` if nothing stored. + fn test_get_value<'a, S: Into<Option<&'a str>>>(&self, ping_name: S) -> Option<Vec<String>> { + match self { + StringListMetric::Parent { inner, .. } => inner.test_get_value(ping_name), + StringListMetric::Child(c) => { + panic!("Cannot get test value for {:?} in non-parent process!", c.0) + } + } + } + + /// **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. + fn test_get_num_recorded_errors<'a, S: Into<Option<&'a str>>>( + &self, + error: glean::ErrorType, + ping_name: S, + ) -> i32 { + match self { + StringListMetric::Parent { inner, .. } => { + inner.test_get_num_recorded_errors(error, ping_name) + } + StringListMetric::Child(c) => panic!( + "Cannot get the number of recorded errors for {:?} in non-parent process!", + c.0 + ), + } + } +} + +#[cfg(test)] +mod test { + use crate::{common_test::*, ipc, metrics}; + + #[test] + #[ignore] // TODO: Enable them back when bug 1677454 lands. + fn sets_string_list_value() { + let _lock = lock_test(); + + let metric = &metrics::test_only_ipc::a_string_list; + + metric.set(vec!["test_string_value".to_string()]); + metric.add("another test value"); + + assert_eq!( + vec!["test_string_value", "another test value"], + metric.test_get_value("store1").unwrap() + ); + } + + #[test] + #[ignore] // TODO: Enable them back when bug 1677454 lands. + fn string_list_ipc() { + // StringListMetric supports IPC only for `add`, not `set`. + let _lock = lock_test(); + + let parent_metric = &metrics::test_only_ipc::a_string_list; + + parent_metric.set(vec!["test_string_value".to_string()]); + parent_metric.add("another test value"); + + { + let child_metric = parent_metric.child_metric(); + + // scope for need_ipc RAII + let _raii = ipc::test_set_need_ipc(true); + + // Recording APIs do not panic, even when they don't work. + child_metric.set(vec!["not gonna be set".to_string()]); + + child_metric.add("child_value"); + assert!(ipc::take_buf().unwrap().len() > 0); + } + + // TODO: implement replay. See bug 1646165. + // Then perform the replay and assert we have the values from both "processes". + assert!(ipc::replay_from_buf(&ipc::take_buf().unwrap()).is_ok()); + assert_eq!( + vec!["test_string_value", "another test value"], + parent_metric.test_get_value("store1").unwrap() + ); + } +} diff --git a/toolkit/components/glean/api/src/private/timespan.rs b/toolkit/components/glean/api/src/private/timespan.rs new file mode 100644 index 0000000000..af740d5a41 --- /dev/null +++ b/toolkit/components/glean/api/src/private/timespan.rs @@ -0,0 +1,134 @@ +// 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 super::{CommonMetricData, MetricId, TimeUnit}; + +use glean_core::traits::Timespan; + +use crate::ipc::need_ipc; + +/// A timespan metric. +/// +/// Timespans are used to make a measurement of how much time is spent in a particular task. +pub enum TimespanMetric { + Parent(glean::private::TimespanMetric), + Child, +} + +impl TimespanMetric { + /// Create a new timespan metric. + pub fn new(_id: MetricId, meta: CommonMetricData, time_unit: TimeUnit) -> Self { + if need_ipc() { + TimespanMetric::Child + } else { + TimespanMetric::Parent(glean::private::TimespanMetric::new(meta, time_unit)) + } + } +} + +#[inherent(pub)] +impl Timespan for TimespanMetric { + fn start(&self) { + match self { + TimespanMetric::Parent(p) => Timespan::start(p), + TimespanMetric::Child => { + log::error!("Unable to start timespan metric in non-main process. Ignoring."); + // TODO: Record an error. + } + } + } + + fn stop(&self) { + match self { + TimespanMetric::Parent(p) => Timespan::stop(p), + TimespanMetric::Child => { + log::error!("Unable to stop timespan metric in non-main process. Ignoring."); + // TODO: Record an error. + } + } + } + + fn cancel(&self) { + match self { + TimespanMetric::Parent(p) => Timespan::cancel(p), + TimespanMetric::Child => { + log::error!("Unable to cancel timespan metric in non-main process. Ignoring."); + // TODO: Record an error. + } + } + } + + fn test_get_value<'a, S: Into<Option<&'a str>>>(&self, ping_name: S) -> Option<u64> { + match self { + TimespanMetric::Parent(p) => p.test_get_value(ping_name), + TimespanMetric::Child => { + panic!("Cannot get test value for in non-parent process!"); + } + } + } + + fn test_get_num_recorded_errors<'a, S: Into<Option<&'a str>>>( + &self, + error: glean::ErrorType, + ping_name: S, + ) -> i32 { + match self { + TimespanMetric::Parent(p) => p.test_get_num_recorded_errors(error, ping_name), + TimespanMetric::Child => { + panic!("Cannot get the number of recorded errors for timespan metric in non-parent process!"); + } + } + } +} + +#[cfg(test)] +mod test { + use super::*; + use crate::{common_test::*, ipc, metrics}; + + #[test] + fn smoke_test_timespan() { + let _lock = lock_test(); + + let metric = TimespanMetric::new( + 0.into(), + CommonMetricData { + name: "timespan_metric".into(), + category: "telemetry".into(), + send_in_pings: vec!["store1".into()], + disabled: false, + ..Default::default() + }, + TimeUnit::Nanosecond, + ); + + 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(); + + assert_eq!(None, metric.test_get_value("store1")); + } + + #[test] + fn timespan_ipc() { + let _lock = lock_test(); + let _raii = ipc::test_set_need_ipc(true); + + let child_metric = &metrics::test_only::can_we_time_it; + + // Instrumentation calls do not panic. + child_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. + child_metric.cancel(); + + // (They also shouldn't do anything, + // but that's not something we can inspect in this test) + } +} diff --git a/toolkit/components/glean/api/src/private/timing_distribution.rs b/toolkit/components/glean/api/src/private/timing_distribution.rs new file mode 100644 index 0000000000..bfab77cc4e --- /dev/null +++ b/toolkit/components/glean/api/src/private/timing_distribution.rs @@ -0,0 +1,273 @@ +// 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}, + RwLock, +}; +use std::time::Instant; + +use super::{CommonMetricData, MetricId, TimeUnit}; +use glean::{DistributionData, ErrorType}; +use glean_core::metrics::TimerId; + +use crate::ipc::{need_ipc, with_ipc_payload}; +use glean_core::traits::TimingDistribution; + +/// 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. + /// + /// **TEST-ONLY** - Do not use unless gated with `#[cfg(test)]`. + id: MetricId, + inner: glean::private::TimingDistributionMetric, + }, + Child(TimingDistributionMetricIpc), +} +#[derive(Debug)] +pub struct TimingDistributionMetricIpc { + metric_id: MetricId, + next_timer_id: AtomicUsize, + instants: RwLock<HashMap<u64, Instant>>, +} + +impl TimingDistributionMetric { + /// Create a new timing distribution metric. + pub fn new(id: MetricId, meta: CommonMetricData, time_unit: TimeUnit) -> Self { + if need_ipc() { + TimingDistributionMetric::Child(TimingDistributionMetricIpc { + metric_id: id, + next_timer_id: AtomicUsize::new(0), + instants: RwLock::new(HashMap::new()), + }) + } else { + let inner = glean::private::TimingDistributionMetric::new(meta, time_unit); + TimingDistributionMetric::Parent { id, inner } + } + } + + #[cfg(test)] + pub(crate) fn child_metric(&self) -> Self { + match self { + TimingDistributionMetric::Parent { id, .. } => { + TimingDistributionMetric::Child(TimingDistributionMetricIpc { + metric_id: *id, + next_timer_id: AtomicUsize::new(0), + instants: RwLock::new(HashMap::new()), + }) + } + TimingDistributionMetric::Child(_) => { + panic!("Can't get a child metric from a child metric") + } + } + } +} + +#[inherent(pub)] +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. + fn 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 + } + } + } + + /// 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. + fn stop_and_accumulate(&self, id: TimerId) { + match self { + TimingDistributionMetric::Parent { inner, .. } => { + inner.stop_and_accumulate(id); + } + TimingDistributionMetric::Child(c) => { + let mut map = c + .instants + .write() + .expect("Write lock must've been poisoned."); + if let Some(start) = map.remove(&id) { + let sample = start.elapsed().as_nanos(); + with_ipc_payload(move |payload| { + if let Some(v) = payload.timing_samples.get_mut(&c.metric_id) { + v.push(sample); + } else { + payload.timing_samples.insert(c.metric_id, vec![sample]); + } + }); + } else { + // TODO: report an error (timer id for stop wasn't started). + } + } + } + } + + /// 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. + fn 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).is_none() { + // TODO: report an error (cancelled a non-started id). + } + } + } + } + + /// **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`. + fn test_get_value<'a, S: Into<Option<&'a str>>>( + &self, + ping_name: S, + ) -> Option<DistributionData> { + 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) + } + } + } + + /// **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. + fn test_get_num_recorded_errors<'a, S: Into<Option<&'a str>>>( + &self, + error: ErrorType, + ping_name: S, + ) -> i32 { + match self { + TimingDistributionMetric::Parent { inner, .. } => { + inner.test_get_num_recorded_errors(error, ping_name) + } + TimingDistributionMetric::Child(c) => panic!( + "Cannot get number of recorded errors for {:?} in non-parent process!", + c + ), + } + } +} + +#[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("store1").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); + } + + // TODO: implement replay. See bug 1646165. + // For now let's ensure there's something in the buffer and replay doesn't error. + let buf = ipc::take_buf().unwrap(); + assert!(buf.len() > 0); + assert!(ipc::replay_from_buf(&buf).is_ok()); + } +} diff --git a/toolkit/components/glean/api/src/private/uuid.rs b/toolkit/components/glean/api/src/private/uuid.rs new file mode 100644 index 0000000000..fcffc640fc --- /dev/null +++ b/toolkit/components/glean/api/src/private/uuid.rs @@ -0,0 +1,168 @@ +// 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 uuid::Uuid; + +use super::{CommonMetricData, MetricId}; + +use crate::ipc::need_ipc; + +/// A UUID metric. +/// +/// Stores UUID values. +pub enum UuidMetric { + Parent(glean::private::UuidMetric), + Child(UuidMetricIpc), +} + +#[derive(Debug)] +pub struct UuidMetricIpc; + +impl UuidMetric { + /// Create a new UUID metric. + pub fn new(_id: MetricId, meta: CommonMetricData) -> Self { + if need_ipc() { + UuidMetric::Child(UuidMetricIpc) + } else { + UuidMetric::Parent(glean::private::UuidMetric::new(meta)) + } + } + + #[cfg(test)] + pub(crate) fn child_metric(&self) -> Self { + match self { + UuidMetric::Parent(_) => UuidMetric::Child(UuidMetricIpc), + UuidMetric::Child(_) => panic!("Can't get a child metric from a child metric"), + } + } +} + +#[inherent(pub)] +impl glean_core::traits::Uuid for UuidMetric { + /// Set to the specified value. + /// + /// ## Arguments + /// + /// * `value` - The UUID to set the metric to. + fn set(&self, value: Uuid) { + match self { + UuidMetric::Parent(p) => { + glean_core::traits::Uuid::set(&*p, value); + } + UuidMetric::Child(_c) => { + log::error!("Unable to set the uuid metric in non-main process. Ignoring."); + // TODO: Record an error. + } + }; + } + + /// Generate a new random UUID and set the metric to it. + /// + /// ## Return value + /// + /// Returns the stored UUID value or `Uuid::nil` if called from + /// a non-parent process. + fn generate_and_set(&self) -> Uuid { + match self { + UuidMetric::Parent(p) => glean_core::traits::Uuid::generate_and_set(&*p), + UuidMetric::Child(_c) => { + log::error!("Unable to set the uuid metric in non-main process. Ignoring."); + // TODO: Record an error. + Uuid::nil() + } + } + } + + /// **Test-only API.** + /// + /// Get the stored UUID value. + /// This doesn't clear the stored value. + /// + /// ## Arguments + /// + /// * `storage_name` - the storage name to look into. + /// + /// ## Return value + /// + /// Returns the stored value or `None` if nothing stored. + fn test_get_value<'a, S: Into<Option<&'a str>>>(&self, storage_name: S) -> Option<Uuid> { + match self { + UuidMetric::Parent(p) => p.test_get_value(storage_name), + UuidMetric::Child(_c) => panic!("Cannot get test value for in non-parent process!"), + } + } + + /// **Exported for test purposes.** + /// + /// Gets the number of recorded errors for the given metric and 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 reported. + fn test_get_num_recorded_errors<'a, S: Into<Option<&'a str>>>( + &self, + error: glean::ErrorType, + ping_name: S, + ) -> i32 { + match self { + UuidMetric::Parent(p) => p.test_get_num_recorded_errors(error, ping_name), + UuidMetric::Child(_c) => { + panic!("Cannot get test value for UuidMetric in non-parent process!") + } + } + } +} + +#[cfg(test)] +mod test { + use super::*; + use crate::{common_test::*, ipc, metrics}; + + #[test] + fn sets_uuid_value() { + let _lock = lock_test(); + + let metric = &metrics::test_only_ipc::a_uuid; + let expected = Uuid::new_v4(); + metric.set(expected.clone()); + + assert_eq!(expected, metric.test_get_value("store1").unwrap()); + } + + #[test] + fn uuid_ipc() { + // UuidMetric doesn't support IPC. + let _lock = lock_test(); + + let parent_metric = &metrics::test_only_ipc::a_uuid; + let expected = Uuid::new_v4(); + parent_metric.set(expected.clone()); + + { + let child_metric = parent_metric.child_metric(); + + // Instrumentation calls do not panic. + child_metric.set(Uuid::new_v4()); + + // (They also shouldn't do anything, + // but that's not something we can inspect in this test) + } + + assert!(ipc::replay_from_buf(&ipc::take_buf().unwrap()).is_ok()); + + assert_eq!( + expected, + parent_metric.test_get_value("store1").unwrap(), + "UUID metrics should only work in the parent process" + ); + } +} |