use reifydb_core::profiler::ProfilerCategoryId;
use reifydb_value::value::duration::Duration;
use serde::{Deserialize, Serialize};
use crate::{
category::ProfilerCategory,
percentile::{PercentileHistogram, ProfilerPercentiles},
};
pub type DimIdx = u32;
pub const DIM_UNSET: DimIdx = 0;
pub const MAX_DIMENSIONS: usize = 2;
pub const MAX_EXTRAS: usize = 4;
#[repr(C)]
#[derive(Clone, Copy, Debug, Serialize, Deserialize)]
pub struct MinimalSpanRecord {
pub category_id: u8,
pub callsite_id: u64,
pub duration_us: u32,
pub self_us: u32,
pub dim_indices: [DimIdx; MAX_DIMENSIONS],
pub extras: [u64; MAX_EXTRAS],
}
impl MinimalSpanRecord {
pub const fn new(category: ProfilerCategory, callsite_id: u64, duration_us: u32) -> Self {
Self {
category_id: category as u8,
callsite_id,
duration_us,
self_us: duration_us,
dim_indices: [DIM_UNSET; MAX_DIMENSIONS],
extras: [0; MAX_EXTRAS],
}
}
pub fn with_dimensions(mut self, dim_indices: [DimIdx; MAX_DIMENSIONS]) -> Self {
self.dim_indices = dim_indices;
self
}
pub fn with_extras(mut self, extras: [u64; MAX_EXTRAS]) -> Self {
self.extras = extras;
self
}
pub fn category(&self) -> ProfilerCategory {
ProfilerCategory::from_id(ProfilerCategoryId(self.category_id))
.expect("MinimalSpanRecord must hold a valid ProfilerCategory id")
}
}
#[derive(Clone, Copy, Hash, PartialEq, Eq, Debug)]
pub struct SpanIdent {
pub category: ProfilerCategory,
pub callsite_id: u64,
pub dim_indices: [DimIdx; MAX_DIMENSIONS],
}
impl SpanIdent {
pub const fn new(category: ProfilerCategory, callsite_id: u64, dim_indices: [DimIdx; MAX_DIMENSIONS]) -> Self {
Self {
category,
callsite_id,
dim_indices,
}
}
}
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct AggregateRecord {
pub category: ProfilerCategory,
pub span_name: String,
pub dimensions: Vec<String>,
pub calls: u64,
pub total_us: u64,
pub self_us: u64,
pub histogram: PercentileHistogram,
pub extras_sum: [u64; MAX_EXTRAS],
}
impl AggregateRecord {
pub fn fold(&mut self, duration_us: u32, self_us: u32, extras: &[u64; MAX_EXTRAS]) {
self.calls = self.calls.saturating_add(1);
self.total_us = self.total_us.saturating_add(duration_us as u64);
self.self_us = self.self_us.saturating_add(self_us as u64);
self.histogram.observe(duration_us);
for (sum, &extra) in self.extras_sum.iter_mut().zip(extras.iter()) {
*sum = sum.saturating_add(extra);
}
}
pub fn total(&self) -> Duration {
Duration::from_micros_infallible(self.total_us)
}
pub fn min(&self) -> Duration {
Duration::from_micros_infallible(self.histogram.percentile(0.0) as u64)
}
pub fn max(&self) -> Duration {
Duration::from_micros_infallible(self.histogram.percentile(1.0) as u64)
}
pub fn percentiles(&self) -> ProfilerPercentiles {
self.histogram.percentiles_duration()
}
pub fn extras(&self) -> &[u64; MAX_EXTRAS] {
&self.extras_sum
}
}
#[cfg(test)]
mod tests {
use std::mem::size_of;
use super::*;
use crate::category::ALL_CATEGORIES;
#[test]
fn minimal_span_record_size_is_64_bytes() {
assert_eq!(size_of::<MinimalSpanRecord>(), 64);
}
#[test]
fn aggregate_fold_tracks_calls_and_distribution() {
let mut agg = AggregateRecord {
category: ProfilerCategory::Flow,
span_name: "flow::engine::apply".to_string(),
dimensions: vec!["map".to_string(), "n1".to_string()],
calls: 0,
total_us: 0,
self_us: 0,
histogram: PercentileHistogram::new(),
extras_sum: [0; MAX_EXTRAS],
};
agg.fold(100, 60, &[10, 20, 0, 0]);
agg.fold(50, 50, &[5, 10, 0, 0]);
agg.fold(200, 120, &[2, 4, 0, 0]);
assert_eq!(agg.calls, 3);
assert_eq!(agg.total_us, 350);
assert_eq!(
agg.self_us, 230,
"self time accumulates apart from the inclusive total, so nesting cannot inflate it"
);
assert_eq!(agg.extras_sum, [17, 34, 0, 0]);
assert_eq!(agg.histogram.total_count(), 3);
let p = agg.histogram.percentiles();
assert!(p.p50 <= p.p90, "p50 should not exceed p90");
assert!(p.p90 <= p.p99, "p90 should not exceed p99");
}
#[test]
fn category_round_trip_through_record() {
for cat in ALL_CATEGORIES {
let rec = MinimalSpanRecord::new(cat, 42, 99);
assert_eq!(rec.category(), cat);
}
}
}