Skip to main content

reifydb_profiler/
summary.rs

1// SPDX-License-Identifier: Apache-2.0
2// Copyright (c) 2026 ReifyDB
3
4use std::sync::Arc;
5
6use reifydb_value::value::duration::Duration;
7use serde::{Deserialize, Serialize};
8
9use crate::{
10	category::{CATEGORY_COUNT, ProfilerCategory},
11	intern::DimInterner,
12	record::{MAX_EXTRAS, MinimalSpanRecord},
13	scope::ScopeId,
14};
15
16#[derive(Clone, Copy, Default, Debug, Serialize, Deserialize)]
17pub struct CategorySummary {
18	pub calls: u64,
19	pub total_us: u64,
20	pub min_us: u32,
21	pub max_us: u32,
22	pub extras_sum: [u64; MAX_EXTRAS],
23}
24
25impl CategorySummary {
26	pub fn fold(&mut self, duration_us: u32, extras: &[u64; MAX_EXTRAS]) {
27		let was_empty = self.calls == 0;
28		self.calls = self.calls.saturating_add(1);
29		self.total_us = self.total_us.saturating_add(duration_us as u64);
30		if was_empty || duration_us < self.min_us {
31			self.min_us = duration_us;
32		}
33		if duration_us > self.max_us {
34			self.max_us = duration_us;
35		}
36		for (sum, &extra) in self.extras_sum.iter_mut().zip(extras.iter()) {
37			*sum = sum.saturating_add(extra);
38		}
39	}
40
41	pub fn total(&self) -> Duration {
42		Duration::from_micros_infallible(self.total_us)
43	}
44
45	pub fn min(&self) -> Duration {
46		Duration::from_micros_infallible(self.min_us as u64)
47	}
48
49	pub fn max(&self) -> Duration {
50		Duration::from_micros_infallible(self.max_us as u64)
51	}
52
53	pub fn extras(&self) -> &[u64; MAX_EXTRAS] {
54		&self.extras_sum
55	}
56}
57
58#[derive(Clone, Debug, Serialize, Deserialize)]
59pub struct ProfilerSummary {
60	pub scope_id: ScopeId,
61	pub scope_name: &'static str,
62	pub started_at_nanos: u128,
63	pub total_duration_us: u64,
64	pub records: Vec<MinimalSpanRecord>,
65	pub per_category: [CategorySummary; CATEGORY_COUNT],
66	#[serde(skip)]
67	pub interner: Option<Arc<DimInterner>>,
68}
69
70impl ProfilerSummary {
71	pub fn category(&self, c: ProfilerCategory) -> CategorySummary {
72		self.per_category[c as usize]
73	}
74
75	pub fn total_calls(&self) -> u64 {
76		self.per_category.iter().map(|c| c.calls).sum()
77	}
78
79	pub fn from_records(
80		scope_id: ScopeId,
81		scope_name: &'static str,
82		started_at_nanos: u128,
83		total_duration_us: u64,
84		records: Vec<MinimalSpanRecord>,
85		interner: Option<Arc<DimInterner>>,
86	) -> Self {
87		let mut per_category = [CategorySummary::default(); CATEGORY_COUNT];
88		for rec in &records {
89			let idx = rec.category_id as usize;
90			if idx < per_category.len() {
91				per_category[idx].fold(rec.duration_us, &rec.extras);
92			}
93		}
94		Self {
95			scope_id,
96			scope_name,
97			started_at_nanos,
98			total_duration_us,
99			records,
100			per_category,
101			interner,
102		}
103	}
104
105	pub fn flow_category() -> ProfilerCategory {
106		ProfilerCategory::Flow
107	}
108}
109
110#[cfg(test)]
111mod tests {
112	use super::*;
113	use crate::category::ALL_CATEGORIES;
114
115	#[test]
116	fn category_summary_fold_tracks_extremes() {
117		let mut s = CategorySummary::default();
118		s.fold(100, &[1, 2, 3, 4]);
119		s.fold(50, &[1, 1, 1, 1]);
120		s.fold(200, &[0, 0, 0, 0]);
121		assert_eq!(s.calls, 3);
122		assert_eq!(s.total_us, 350);
123		assert_eq!(s.min_us, 50);
124		assert_eq!(s.max_us, 200);
125		assert_eq!(s.extras_sum, [2, 3, 4, 5]);
126	}
127
128	#[test]
129	fn summary_from_records_aggregates_per_category() {
130		let records = vec![
131			MinimalSpanRecord::new(ProfilerCategory::Flow, 1, 100).with_extras([10, 20, 0, 0]),
132			MinimalSpanRecord::new(ProfilerCategory::Flow, 2, 50).with_extras([5, 10, 0, 0]),
133			MinimalSpanRecord::new(ProfilerCategory::Query, 3, 30),
134		];
135		let summary = ProfilerSummary::from_records(ScopeId(7), "test", 0, 1000, records, None);
136		assert_eq!(summary.category(ProfilerCategory::Flow).calls, 2);
137		assert_eq!(summary.category(ProfilerCategory::Flow).total_us, 150);
138		assert_eq!(summary.category(ProfilerCategory::Query).calls, 1);
139		assert_eq!(summary.category(ProfilerCategory::Storage).calls, 0);
140		assert_eq!(summary.total_calls(), 3);
141	}
142
143	#[test]
144	fn all_categories_addressable() {
145		let mut per = [CategorySummary::default(); CATEGORY_COUNT];
146		for c in ALL_CATEGORIES {
147			per[c as usize].calls = c as u64;
148		}
149		for c in ALL_CATEGORIES {
150			assert_eq!(per[c as usize].calls, c as u64);
151		}
152	}
153}