1use 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: Duration,
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: Duration,
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,
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(
136 ScopeId(7),
137 "test",
138 0,
139 Duration::from_microseconds(1000).unwrap(),
140 records,
141 None,
142 );
143 assert_eq!(summary.category(ProfilerCategory::Flow).calls, 2);
144 assert_eq!(summary.category(ProfilerCategory::Flow).total_us, 150);
145 assert_eq!(summary.category(ProfilerCategory::Query).calls, 1);
146 assert_eq!(summary.category(ProfilerCategory::Storage).calls, 0);
147 assert_eq!(summary.total_calls(), 3);
148 }
149
150 #[test]
151 fn all_categories_addressable() {
152 let mut per = [CategorySummary::default(); CATEGORY_COUNT];
153 for c in ALL_CATEGORIES {
154 per[c as usize].calls = c as u64;
155 }
156 for c in ALL_CATEGORIES {
157 assert_eq!(per[c as usize].calls, c as u64);
158 }
159 }
160}