1use reifydb_core::profiler::ProfilerCategoryId;
5use reifydb_value::value::duration::Duration;
6use serde::{Deserialize, Serialize};
7
8use crate::{
9 category::ProfilerCategory,
10 percentile::{PercentileHistogram, ProfilerPercentiles},
11};
12
13pub type DimIdx = u32;
14pub const DIM_UNSET: DimIdx = 0;
15pub const MAX_DIMENSIONS: usize = 2;
16pub const MAX_EXTRAS: usize = 4;
17
18#[repr(C)]
19#[derive(Clone, Copy, Debug, Serialize, Deserialize)]
20pub struct MinimalSpanRecord {
21 pub category_id: u8,
22 pub callsite_id: u64,
23 pub duration_us: u32,
24 pub self_us: u32,
25 pub dim_indices: [DimIdx; MAX_DIMENSIONS],
26 pub extras: [u64; MAX_EXTRAS],
27}
28
29impl MinimalSpanRecord {
30 pub const fn new(category: ProfilerCategory, callsite_id: u64, duration_us: u32) -> Self {
31 Self {
32 category_id: category as u8,
33 callsite_id,
34 duration_us,
35 self_us: duration_us,
36 dim_indices: [DIM_UNSET; MAX_DIMENSIONS],
37 extras: [0; MAX_EXTRAS],
38 }
39 }
40
41 pub fn with_dimensions(mut self, dim_indices: [DimIdx; MAX_DIMENSIONS]) -> Self {
42 self.dim_indices = dim_indices;
43 self
44 }
45
46 pub fn with_extras(mut self, extras: [u64; MAX_EXTRAS]) -> Self {
47 self.extras = extras;
48 self
49 }
50
51 pub fn category(&self) -> ProfilerCategory {
52 ProfilerCategory::from_id(ProfilerCategoryId(self.category_id))
53 .expect("MinimalSpanRecord must hold a valid ProfilerCategory id")
54 }
55}
56
57#[derive(Clone, Copy, Hash, PartialEq, Eq, Debug)]
58pub struct SpanIdent {
59 pub category: ProfilerCategory,
60 pub callsite_id: u64,
61 pub dim_indices: [DimIdx; MAX_DIMENSIONS],
62}
63
64impl SpanIdent {
65 pub const fn new(category: ProfilerCategory, callsite_id: u64, dim_indices: [DimIdx; MAX_DIMENSIONS]) -> Self {
66 Self {
67 category,
68 callsite_id,
69 dim_indices,
70 }
71 }
72}
73
74#[derive(Clone, Debug, Serialize, Deserialize)]
75pub struct AggregateRecord {
76 pub category: ProfilerCategory,
77 pub span_name: String,
78 pub dimensions: Vec<String>,
79 pub calls: u64,
80 pub total_us: u64,
81 pub self_us: u64,
82 pub histogram: PercentileHistogram,
83 pub extras_sum: [u64; MAX_EXTRAS],
84}
85
86impl AggregateRecord {
87 pub fn fold(&mut self, duration_us: u32, self_us: u32, extras: &[u64; MAX_EXTRAS]) {
88 self.calls = self.calls.saturating_add(1);
89 self.total_us = self.total_us.saturating_add(duration_us as u64);
90 self.self_us = self.self_us.saturating_add(self_us as u64);
91 self.histogram.observe(duration_us);
92 for (sum, &extra) in self.extras_sum.iter_mut().zip(extras.iter()) {
93 *sum = sum.saturating_add(extra);
94 }
95 }
96
97 pub fn total(&self) -> Duration {
98 Duration::from_micros_infallible(self.total_us)
99 }
100
101 pub fn min(&self) -> Duration {
102 Duration::from_micros_infallible(self.histogram.percentile(0.0) as u64)
103 }
104
105 pub fn max(&self) -> Duration {
106 Duration::from_micros_infallible(self.histogram.percentile(1.0) as u64)
107 }
108
109 pub fn percentiles(&self) -> ProfilerPercentiles {
110 self.histogram.percentiles_duration()
111 }
112
113 pub fn extras(&self) -> &[u64; MAX_EXTRAS] {
114 &self.extras_sum
115 }
116}
117
118#[cfg(test)]
119mod tests {
120 use std::mem::size_of;
121
122 use super::*;
123 use crate::category::ALL_CATEGORIES;
124
125 #[test]
126 fn minimal_span_record_size_is_64_bytes() {
127 assert_eq!(size_of::<MinimalSpanRecord>(), 64);
128 }
129
130 #[test]
131 fn aggregate_fold_tracks_calls_and_distribution() {
132 let mut agg = AggregateRecord {
133 category: ProfilerCategory::Flow,
134 span_name: "flow::engine::apply".to_string(),
135 dimensions: vec!["map".to_string(), "n1".to_string()],
136 calls: 0,
137 total_us: 0,
138 self_us: 0,
139 histogram: PercentileHistogram::new(),
140 extras_sum: [0; MAX_EXTRAS],
141 };
142 agg.fold(100, 60, &[10, 20, 0, 0]);
143 agg.fold(50, 50, &[5, 10, 0, 0]);
144 agg.fold(200, 120, &[2, 4, 0, 0]);
145
146 assert_eq!(agg.calls, 3);
147 assert_eq!(agg.total_us, 350);
148 assert_eq!(
149 agg.self_us, 230,
150 "self time accumulates apart from the inclusive total, so nesting cannot inflate it"
151 );
152 assert_eq!(agg.extras_sum, [17, 34, 0, 0]);
153 assert_eq!(agg.histogram.total_count(), 3);
154 let p = agg.histogram.percentiles();
155 assert!(p.p50 <= p.p90, "p50 should not exceed p90");
156 assert!(p.p90 <= p.p99, "p90 should not exceed p99");
157 }
158
159 #[test]
160 fn category_round_trip_through_record() {
161 for cat in ALL_CATEGORIES {
162 let rec = MinimalSpanRecord::new(cat, 42, 99);
163 assert_eq!(rec.category(), cat);
164 }
165 }
166}