use std::collections::HashMap;
use serde::Serialize;
use crate::error_recording::{record_error, ErrorType};
use crate::histogram::{Functional, Histogram};
use crate::metrics::time_unit::TimeUnit;
use crate::metrics::Metric;
use crate::metrics::MetricType;
use crate::storage::StorageManager;
use crate::CommonMetricData;
use crate::Glean;
const LOG_BASE: f64 = 2.0;
const BUCKETS_PER_MAGNITUDE: f64 = 8.0;
const MAX_SAMPLE_TIME: u64 = 1000 * 1000 * 1000 * 60 * 10;
pub type TimerId = u64;
#[derive(Debug, Clone)]
struct Timings {
next_id: TimerId,
start_times: HashMap<TimerId, u64>,
}
impl Timings {
fn new() -> Self {
Self {
next_id: 0,
start_times: HashMap::new(),
}
}
fn set_start(&mut self, start_time: u64) -> TimerId {
let id = self.next_id;
self.next_id += 1;
self.start_times.insert(id, start_time);
id
}
fn set_stop(&mut self, id: TimerId, stop_time: u64) -> Result<u64, &str> {
let start_time = match self.start_times.remove(&id) {
Some(start_time) => start_time,
None => return Err("Timing not running"),
};
let duration = match stop_time.checked_sub(start_time) {
Some(duration) => duration,
None => return Err("Timer stopped with negative duration"),
};
Ok(duration)
}
fn cancel(&mut self, id: TimerId) {
self.start_times.remove(&id);
}
}
#[derive(Debug)]
pub struct TimingDistributionMetric {
meta: CommonMetricData,
time_unit: TimeUnit,
timings: Timings,
}
#[derive(Debug, Serialize)]
pub struct Snapshot {
values: HashMap<u64, u64>,
sum: u64,
}
pub(crate) fn snapshot(hist: &Histogram<Functional>) -> Snapshot {
Snapshot {
values: hist.snapshot(),
sum: hist.sum(),
}
}
impl MetricType for TimingDistributionMetric {
fn meta(&self) -> &CommonMetricData {
&self.meta
}
fn meta_mut(&mut self) -> &mut CommonMetricData {
&mut self.meta
}
}
impl TimingDistributionMetric {
pub fn new(meta: CommonMetricData, time_unit: TimeUnit) -> Self {
Self {
meta,
time_unit,
timings: Timings::new(),
}
}
pub fn set_start(&mut self, start_time: u64) -> TimerId {
self.timings.set_start(start_time)
}
pub fn set_stop_and_accumulate(&mut self, glean: &Glean, id: TimerId, stop_time: u64) {
let mut duration = match self.timings.set_stop(id, stop_time) {
Err(error) => {
record_error(glean, &self.meta, ErrorType::InvalidValue, error, None);
return;
}
Ok(duration) => duration,
};
if duration > MAX_SAMPLE_TIME {
let msg = "Sample is longer than 10 minutes";
record_error(glean, &self.meta, ErrorType::InvalidOverflow, msg, None);
duration = MAX_SAMPLE_TIME;
}
if !self.should_record(glean) {
return;
}
glean
.storage()
.record_with(glean, &self.meta, |old_value| match old_value {
Some(Metric::TimingDistribution(mut hist)) => {
hist.accumulate(duration);
Metric::TimingDistribution(hist)
}
_ => {
let mut hist = Histogram::functional(LOG_BASE, BUCKETS_PER_MAGNITUDE);
hist.accumulate(duration);
Metric::TimingDistribution(hist)
}
});
}
pub fn cancel(&mut self, id: TimerId) {
self.timings.cancel(id);
}
pub fn accumulate_samples_signed(&mut self, glean: &Glean, samples: Vec<i64>) {
let mut num_negative_samples = 0;
let mut num_too_long_samples = 0;
glean.storage().record_with(glean, &self.meta, |old_value| {
let mut hist = match old_value {
Some(Metric::TimingDistribution(hist)) => hist,
_ => Histogram::functional(LOG_BASE, BUCKETS_PER_MAGNITUDE),
};
for &sample in samples.iter() {
if sample < 0 {
num_negative_samples += 1;
} else {
let sample = sample as u64;
let mut sample = self.time_unit.as_nanos(sample);
if sample > MAX_SAMPLE_TIME {
num_too_long_samples += 1;
sample = MAX_SAMPLE_TIME;
}
hist.accumulate(sample);
}
}
Metric::TimingDistribution(hist)
});
if num_negative_samples > 0 {
let msg = format!("Accumulated {} negative samples", num_negative_samples);
record_error(
glean,
&self.meta,
ErrorType::InvalidValue,
msg,
num_negative_samples,
);
}
if num_too_long_samples > 0 {
let msg = format!(
"Accumulated {} samples longer than 10 minutes",
num_too_long_samples
);
record_error(
glean,
&self.meta,
ErrorType::InvalidOverflow,
msg,
num_too_long_samples,
);
}
}
pub fn test_get_value(
&self,
glean: &Glean,
storage_name: &str,
) -> Option<Histogram<Functional>> {
match StorageManager.snapshot_metric(
glean.storage(),
storage_name,
&self.meta.identifier(glean),
) {
Some(Metric::TimingDistribution(hist)) => Some(hist),
_ => None,
}
}
pub fn test_get_value_as_json_string(
&self,
glean: &Glean,
storage_name: &str,
) -> Option<String> {
self.test_get_value(glean, storage_name)
.map(|hist| serde_json::to_string(&snapshot(&hist)).unwrap())
}
}
#[cfg(test)]
mod test {
use super::*;
#[test]
fn can_snapshot() {
use serde_json::json;
let mut hist = Histogram::functional(2.0, 8.0);
for i in 1..=10 {
hist.accumulate(i);
}
let snap = snapshot(&hist);
let expected_json = json!({
"sum": 55,
"values": {
"1": 1,
"2": 1,
"3": 1,
"4": 1,
"5": 1,
"6": 1,
"7": 1,
"8": 1,
"9": 1,
"10": 1,
"11": 0,
},
});
assert_eq!(expected_json, json!(snap));
}
#[test]
fn can_snapshot_sparse() {
use serde_json::json;
let mut hist = Histogram::functional(2.0, 8.0);
hist.accumulate(1024);
hist.accumulate(1024);
hist.accumulate(1116);
hist.accumulate(1448);
let snap = snapshot(&hist);
let expected_json = json!({
"sum": 4612,
"values": {
"1024": 2,
"1116": 1,
"1217": 0,
"1327": 0,
"1448": 1,
"1579": 0,
},
});
assert_eq!(expected_json, json!(snap));
}
}