1use alloc::collections::BTreeMap;
4use alloc::string::String;
5use alloc::vec::Vec;
6
7use grafos_observe::span::ResourceSpan;
8
9use crate::recording::ProfileRecording;
10
11#[derive(Debug, Clone, Default)]
13pub struct ResourceTypeSummary {
14 pub total_lease_cost_byte_secs: u64,
16 pub total_bytes_read: u64,
18 pub total_bytes_written: u64,
20 pub total_ops: u64,
22 pub total_acquire_wait_us: u64,
24}
25
26#[derive(Debug, Clone)]
28pub struct ProfileSummary {
29 pub total_duration_us: u64,
31 pub span_count: usize,
33 pub unique_lease_count: usize,
35 pub total_lease_cost_byte_secs: u64,
37 pub by_resource_type: BTreeMap<String, ResourceTypeSummary>,
39 pub top_by_cost: Vec<(String, u64)>,
41 pub top_by_bytes: Vec<(String, u64)>,
43 pub top_by_duration: Vec<(String, u64)>,
45 pub top_by_acquire_wait: Vec<(String, u64)>,
47}
48
49impl ProfileSummary {
50 pub fn from_recording(rec: &ProfileRecording) -> Self {
52 Self::from_recording_top_n(rec, 10)
53 }
54
55 pub fn from_recording_top_n(rec: &ProfileRecording, top_n: usize) -> Self {
57 let total_duration_us = rec.header.duration_us.unwrap_or_else(|| {
58 if rec.spans.is_empty() {
59 0
60 } else {
61 let min_start = rec
62 .spans
63 .iter()
64 .map(|s| s.start_time_unix_us)
65 .min()
66 .unwrap_or(0);
67 let max_end = rec
68 .spans
69 .iter()
70 .map(|s| s.end_time_unix_us)
71 .max()
72 .unwrap_or(0);
73 max_end.saturating_sub(min_start)
74 }
75 });
76
77 let mut lease_ids: Vec<u128> = Vec::new();
79 for span in &rec.spans {
80 for &lid in &span.lease_ids {
81 if !lease_ids.contains(&lid) {
82 lease_ids.push(lid);
83 }
84 }
85 }
86
87 let mut by_type: BTreeMap<String, ResourceTypeSummary> = BTreeMap::new();
89 for span in &rec.spans {
90 for &(ref op_key, count) in &span.ops {
91 let key = alloc::format!("{}", op_key.resource_type);
92 let entry = by_type.entry(key).or_default();
93 entry.total_ops += count;
94 }
95 }
96
97 for span in &rec.spans {
99 if span.ops.is_empty() {
100 continue;
101 }
102 let total_ops: u64 = span.ops.iter().map(|(_, c)| c).sum();
104 if total_ops == 0 {
105 continue;
106 }
107 for &(ref op_key, count) in &span.ops {
108 let key = alloc::format!("{}", op_key.resource_type);
109 let entry = by_type.entry(key).or_default();
110 let fraction_num = count;
111 let fraction_denom = total_ops;
112 entry.total_lease_cost_byte_secs +=
113 span.lease_cost_byte_secs * fraction_num / fraction_denom;
114 entry.total_bytes_read += span.bytes_read * fraction_num / fraction_denom;
115 entry.total_bytes_written += span.bytes_written * fraction_num / fraction_denom;
116 entry.total_acquire_wait_us +=
117 span.lease_acquire_wait_us * fraction_num / fraction_denom;
118 }
119 }
120
121 let total_lease_cost_byte_secs: u64 =
122 rec.spans.iter().map(|s| s.lease_cost_byte_secs).sum();
123
124 let top_by_cost = top_n_by(&rec.spans, top_n, |s| s.lease_cost_byte_secs);
126 let top_by_bytes = top_n_by(&rec.spans, top_n, |s| s.bytes_read + s.bytes_written);
127 let top_by_duration = top_n_by(&rec.spans, top_n, |s| s.duration_us());
128 let top_by_acquire_wait = top_n_by(&rec.spans, top_n, |s| s.lease_acquire_wait_us);
129
130 ProfileSummary {
131 total_duration_us,
132 span_count: rec.spans.len(),
133 unique_lease_count: lease_ids.len(),
134 total_lease_cost_byte_secs,
135 by_resource_type: by_type,
136 top_by_cost,
137 top_by_bytes,
138 top_by_duration,
139 top_by_acquire_wait,
140 }
141 }
142}
143
144fn top_n_by(
145 spans: &[ResourceSpan],
146 n: usize,
147 key_fn: fn(&ResourceSpan) -> u64,
148) -> Vec<(String, u64)> {
149 let mut items: Vec<(String, u64)> = spans.iter().map(|s| (s.name.clone(), key_fn(s))).collect();
150 items.sort_by(|a, b| b.1.cmp(&a.1));
151 items.truncate(n);
152 items
153}
154
155#[cfg(test)]
156mod tests {
157 use super::*;
158 use grafos_observe::event::{OpType, ResourceType};
159 use grafos_observe::trace::TraceContext;
160
161 fn test_ctx() -> TraceContext {
162 let mut bytes = [0u8; 24];
163 for (i, b) in bytes.iter_mut().enumerate() {
164 *b = (i as u8).wrapping_add(0x42);
165 }
166 TraceContext::new_root(&bytes)
167 }
168
169 fn make_span(name: &str, start: u64, end: u64, cost: u64) -> ResourceSpan {
170 let mut child_bytes = [0xAA_u8; 8];
171 let src = name.as_bytes();
172 let len = src.len().min(8);
173 child_bytes[..len].copy_from_slice(&src[..len]);
174 let ctx = test_ctx().child(&child_bytes);
175 let mut span = ResourceSpan::new(name, ctx);
176 span.start_time_unix_us = start;
177 span.end_time_unix_us = end;
178 span.lease_cost_byte_secs = cost;
179 span
180 }
181
182 #[test]
183 fn summary_basic() {
184 let mut s1 = make_span("read_op", 1000, 2000, 100);
185 s1.bytes_read = 4096;
186 s1.record_op(ResourceType::Mem, OpType::Read, 10);
187 s1.add_lease_id(1);
188
189 let mut s2 = make_span("write_op", 1500, 3000, 200);
190 s2.bytes_written = 8192;
191 s2.record_op(ResourceType::Block, OpType::WriteBlock, 5);
192 s2.add_lease_id(2);
193
194 let rec = ProfileRecording::from_spans(vec![s1, s2]);
195 let summary = ProfileSummary::from_recording(&rec);
196
197 assert_eq!(summary.span_count, 2);
198 assert_eq!(summary.unique_lease_count, 2);
199 assert_eq!(summary.total_lease_cost_byte_secs, 300);
200 assert_eq!(summary.total_duration_us, 2000); assert!(summary.by_resource_type.contains_key("mem"));
203 assert!(summary.by_resource_type.contains_key("block"));
204 assert_eq!(summary.by_resource_type["mem"].total_ops, 10);
205 assert_eq!(summary.by_resource_type["block"].total_ops, 5);
206 }
207
208 #[test]
209 fn top_n_ordering() {
210 let s1 = make_span("cheap", 0, 100, 10);
211 let s2 = make_span("expensive", 0, 100, 1000);
212 let s3 = make_span("medium", 0, 100, 500);
213
214 let rec = ProfileRecording::from_spans(vec![s1, s2, s3]);
215 let summary = ProfileSummary::from_recording_top_n(&rec, 2);
216
217 assert_eq!(summary.top_by_cost.len(), 2);
218 assert_eq!(summary.top_by_cost[0].0, "expensive");
219 assert_eq!(summary.top_by_cost[1].0, "medium");
220 }
221}