Skip to main content

reifydb_profiler/
record.rs

1// SPDX-License-Identifier: Apache-2.0
2// Copyright (c) 2026 ReifyDB
3
4use 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}