grafos_profile/
summary.rs

1//! Aggregated statistics computed from a profile recording.
2
3use alloc::collections::BTreeMap;
4use alloc::string::String;
5use alloc::vec::Vec;
6
7use grafos_observe::span::ResourceSpan;
8
9use crate::recording::ProfileRecording;
10
11/// Per-resource-type aggregated statistics.
12#[derive(Debug, Clone, Default)]
13pub struct ResourceTypeSummary {
14    /// Total bytes leased (approximated from lease_cost / duration).
15    pub total_lease_cost_byte_secs: u64,
16    /// Total bytes read across all spans of this type.
17    pub total_bytes_read: u64,
18    /// Total bytes written across all spans of this type.
19    pub total_bytes_written: u64,
20    /// Total operation count.
21    pub total_ops: u64,
22    /// Total lease acquire wait (microseconds).
23    pub total_acquire_wait_us: u64,
24}
25
26/// Aggregated profile statistics.
27#[derive(Debug, Clone)]
28pub struct ProfileSummary {
29    /// Total recording duration in microseconds.
30    pub total_duration_us: u64,
31    /// Number of spans.
32    pub span_count: usize,
33    /// Number of unique lease IDs.
34    pub unique_lease_count: usize,
35    /// Total lease cost across all spans (byte-seconds).
36    pub total_lease_cost_byte_secs: u64,
37    /// Per-resource-type breakdown.
38    pub by_resource_type: BTreeMap<String, ResourceTypeSummary>,
39    /// Top N spans by lease cost (name, cost).
40    pub top_by_cost: Vec<(String, u64)>,
41    /// Top N spans by bytes (name, bytes_read + bytes_written).
42    pub top_by_bytes: Vec<(String, u64)>,
43    /// Top N spans by duration (name, duration_us).
44    pub top_by_duration: Vec<(String, u64)>,
45    /// Top N spans by acquire wait (name, wait_us).
46    pub top_by_acquire_wait: Vec<(String, u64)>,
47}
48
49impl ProfileSummary {
50    /// Compute summary from a recording with default top-N = 10.
51    pub fn from_recording(rec: &ProfileRecording) -> Self {
52        Self::from_recording_top_n(rec, 10)
53    }
54
55    /// Compute summary from a recording with specified top-N.
56    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        // Unique lease IDs
78        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        // Per-resource-type aggregation
88        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        // Attribute bytes and cost to resource types based on ops
98        for span in &rec.spans {
99            if span.ops.is_empty() {
100                continue;
101            }
102            // Distribute cost proportionally by op count
103            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        // Top-N lists
125        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); // 3000 - 1000
201
202        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}