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