1use alloc::string::String;
15use alloc::vec::Vec;
16
17use grafos_observe::event::ResourceType;
18
19use crate::recording::ProfileRecording;
20use crate::summary::ProfileSummary;
21
22#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
24pub enum SensitivityLevel {
25 Unused,
27 Indifferent,
29 Low,
31 Moderate,
33 Critical,
35}
36
37impl core::fmt::Display for SensitivityLevel {
38 fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
39 match self {
40 SensitivityLevel::Unused => write!(f, "unused"),
41 SensitivityLevel::Indifferent => write!(f, "indifferent"),
42 SensitivityLevel::Low => write!(f, "low"),
43 SensitivityLevel::Moderate => write!(f, "moderate"),
44 SensitivityLevel::Critical => write!(f, "critical"),
45 }
46 }
47}
48
49#[derive(Debug, Clone)]
51pub struct DimensionScore {
52 pub resource_type: ResourceType,
54 pub cost_fraction: f64,
56 pub acquire_wait_fraction: f64,
58 pub ops_per_sec: f64,
60 pub bytes_per_sec: f64,
62 pub level: SensitivityLevel,
64}
65
66#[derive(Debug, Clone)]
68pub struct WorkloadProfile {
69 pub dimensions: Vec<DimensionScore>,
71 pub total_duration_us: u64,
73 pub total_lease_cost: u64,
75}
76
77impl WorkloadProfile {
78 pub fn level_for(&self, rt: ResourceType) -> SensitivityLevel {
80 self.dimensions
81 .iter()
82 .find(|d| d.resource_type == rt)
83 .map(|d| d.level)
84 .unwrap_or(SensitivityLevel::Unused)
85 }
86
87 pub fn render_text(&self) -> String {
89 let mut out = String::new();
90 out.push_str("Workload Sensitivity Profile\n\n");
91
92 for dim in &self.dimensions {
93 out.push_str(&alloc::format!(
94 " {}: {} (cost: {:.1}%, wait: {:.1}%, ops/s: {:.1}, bytes/s: {:.1})\n",
95 dim.resource_type,
96 dim.level,
97 dim.cost_fraction * 100.0,
98 dim.acquire_wait_fraction * 100.0,
99 dim.ops_per_sec,
100 dim.bytes_per_sec,
101 ));
102 }
103
104 let critical: Vec<_> = self
106 .dimensions
107 .iter()
108 .filter(|d| d.level == SensitivityLevel::Critical)
109 .collect();
110 let indifferent: Vec<_> = self
111 .dimensions
112 .iter()
113 .filter(|d| d.level == SensitivityLevel::Indifferent)
114 .collect();
115
116 if !critical.is_empty() {
117 out.push_str(&alloc::format!(
118 "\nThis workload is {}-critical ({:.0}% of lease-cost)",
119 critical[0].resource_type,
120 critical[0].cost_fraction * 100.0,
121 ));
122 for d in &indifferent {
123 out.push_str(&alloc::format!(
124 ", {}-indifferent ({:.0}%)",
125 d.resource_type,
126 d.cost_fraction * 100.0,
127 ));
128 }
129 out.push_str(".\n");
130 }
131
132 out
133 }
134}
135
136pub fn infer_sensitivity(rec: &ProfileRecording) -> WorkloadProfile {
138 if rec.spans.is_empty() {
139 return WorkloadProfile {
140 dimensions: Vec::new(),
141 total_duration_us: 0,
142 total_lease_cost: 0,
143 };
144 }
145
146 let summary = ProfileSummary::from_recording(rec);
147 let total_cost = summary.total_lease_cost_byte_secs;
148 let total_duration_us = summary.total_duration_us;
149 let duration_secs = total_duration_us as f64 / 1_000_000.0;
150
151 let total_acquire_wait: u64 = rec.spans.iter().map(|s| s.lease_acquire_wait_us).sum();
152
153 let resource_types = [
154 ResourceType::Mem,
155 ResourceType::Block,
156 ResourceType::Gpu,
157 ResourceType::Cpu,
158 ResourceType::Net,
159 ];
160
161 let mut dimensions = Vec::new();
162
163 for &rt in &resource_types {
164 let key = alloc::format!("{}", rt);
165 let type_summary = summary.by_resource_type.get(&key);
166
167 let type_cost = type_summary
168 .map(|s| s.total_lease_cost_byte_secs)
169 .unwrap_or(0);
170 let type_ops = type_summary.map(|s| s.total_ops).unwrap_or(0);
171 let type_bytes_read = type_summary.map(|s| s.total_bytes_read).unwrap_or(0);
172 let type_bytes_written = type_summary.map(|s| s.total_bytes_written).unwrap_or(0);
173 let type_wait = type_summary.map(|s| s.total_acquire_wait_us).unwrap_or(0);
174
175 let cost_fraction = if total_cost > 0 {
176 type_cost as f64 / total_cost as f64
177 } else {
178 0.0
179 };
180
181 let acquire_wait_fraction = if total_acquire_wait > 0 {
182 type_wait as f64 / total_acquire_wait as f64
183 } else {
184 0.0
185 };
186
187 let ops_per_sec = if duration_secs > 0.0 {
188 type_ops as f64 / duration_secs
189 } else {
190 0.0
191 };
192
193 let bytes_per_sec = if duration_secs > 0.0 {
194 (type_bytes_read + type_bytes_written) as f64 / duration_secs
195 } else {
196 0.0
197 };
198
199 let level = if cost_fraction > 0.5 || acquire_wait_fraction > 0.3 {
200 SensitivityLevel::Critical
201 } else if cost_fraction > 0.2 {
202 SensitivityLevel::Moderate
203 } else if cost_fraction > 0.05 {
204 SensitivityLevel::Low
205 } else if cost_fraction > 0.0 {
206 SensitivityLevel::Indifferent
207 } else {
208 SensitivityLevel::Unused
209 };
210
211 dimensions.push(DimensionScore {
212 resource_type: rt,
213 cost_fraction,
214 acquire_wait_fraction,
215 ops_per_sec,
216 bytes_per_sec,
217 level,
218 });
219 }
220
221 WorkloadProfile {
222 dimensions,
223 total_duration_us,
224 total_lease_cost: total_cost,
225 }
226}
227
228#[cfg(test)]
229mod tests {
230 use super::*;
231 use grafos_observe::event::OpType;
232 use grafos_observe::span::ResourceSpan;
233 use grafos_observe::trace::TraceContext;
234
235 fn test_ctx() -> TraceContext {
236 let mut bytes = [0u8; 24];
237 for (i, b) in bytes.iter_mut().enumerate() {
238 *b = (i as u8).wrapping_add(0x42);
239 }
240 TraceContext::new_root(&bytes)
241 }
242
243 #[test]
244 fn empty_recording() {
245 let rec = ProfileRecording::from_spans(Vec::new());
246 let profile = infer_sensitivity(&rec);
247 assert!(profile.dimensions.is_empty());
248 assert_eq!(profile.total_lease_cost, 0);
249 }
250
251 #[test]
252 fn memory_critical_block_indifferent() {
253 let ctx = test_ctx();
254 let c1 = ctx.child(&[0xAA; 8]);
255
256 let mut mem_span = ResourceSpan::new("mem_heavy", ctx);
258 mem_span.start_time_unix_us = 0;
259 mem_span.end_time_unix_us = 1_000_000;
260 mem_span.lease_cost_byte_secs = 800;
261 mem_span.bytes_read = 100_000;
262 mem_span.record_op(ResourceType::Mem, OpType::Read, 1000);
263
264 let mut block_span = ResourceSpan::new("block_light", c1);
266 block_span.start_time_unix_us = 0;
267 block_span.end_time_unix_us = 1_000_000;
268 block_span.lease_cost_byte_secs = 20;
269 block_span.bytes_read = 100;
270 block_span.record_op(ResourceType::Block, OpType::ReadBlock, 2);
271
272 let rec = ProfileRecording::from_spans(vec![mem_span, block_span]);
273 let profile = infer_sensitivity(&rec);
274
275 assert_eq!(
276 profile.level_for(ResourceType::Mem),
277 SensitivityLevel::Critical
278 );
279 assert_eq!(
280 profile.level_for(ResourceType::Block),
281 SensitivityLevel::Indifferent
282 );
283 assert_eq!(
284 profile.level_for(ResourceType::Gpu),
285 SensitivityLevel::Unused
286 );
287 }
288
289 #[test]
290 fn moderate_sensitivity() {
291 let ctx = test_ctx();
292 let c1 = ctx.child(&[0xAA; 8]);
293
294 let mut mem_span = ResourceSpan::new("mem_op", ctx);
296 mem_span.start_time_unix_us = 0;
297 mem_span.end_time_unix_us = 1_000_000;
298 mem_span.lease_cost_byte_secs = 400;
299 mem_span.record_op(ResourceType::Mem, OpType::Read, 100);
300
301 let mut block_span = ResourceSpan::new("block_op", c1);
303 block_span.start_time_unix_us = 0;
304 block_span.end_time_unix_us = 1_000_000;
305 block_span.lease_cost_byte_secs = 600;
306 block_span.record_op(ResourceType::Block, OpType::ReadBlock, 100);
307
308 let rec = ProfileRecording::from_spans(vec![mem_span, block_span]);
309 let profile = infer_sensitivity(&rec);
310
311 assert_eq!(
312 profile.level_for(ResourceType::Mem),
313 SensitivityLevel::Moderate
314 );
315 assert_eq!(
316 profile.level_for(ResourceType::Block),
317 SensitivityLevel::Critical
318 );
319 }
320
321 #[test]
322 fn text_summary() {
323 let ctx = test_ctx();
324 let mut span = ResourceSpan::new("mem_op", ctx);
325 span.start_time_unix_us = 0;
326 span.end_time_unix_us = 1_000_000;
327 span.lease_cost_byte_secs = 1000;
328 span.record_op(ResourceType::Mem, OpType::Read, 100);
329
330 let rec = ProfileRecording::from_spans(vec![span]);
331 let profile = infer_sensitivity(&rec);
332 let text = profile.render_text();
333
334 assert!(text.contains("Workload Sensitivity Profile"));
335 assert!(text.contains("mem: critical"));
336 }
337}