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 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}