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