1use serde::{Deserialize, Serialize};
8
9#[derive(Debug, Clone, Serialize, Deserialize)]
14pub struct DashboardEventDTO {
15 pub timestamp: u64,
17 pub event_type: String,
19 pub ptr: String,
21 pub size: usize,
23 pub thread_id: u64,
25 pub task_id: Option<u64>,
27 pub var_name: Option<String>,
29 pub type_name: Option<String>,
31 pub source_file: Option<String>,
33 pub source_line: Option<u32>,
35}
36
37impl From<&crate::event_store::event::MemoryEvent> for DashboardEventDTO {
38 fn from(e: &crate::event_store::event::MemoryEvent) -> Self {
39 Self {
40 timestamp: e.timestamp,
41 event_type: e.event_type.to_string(),
42 ptr: format!("0x{:x}", e.ptr),
43 size: e.size,
44 thread_id: e.thread_id,
45 task_id: e.task_id,
46 var_name: e.var_name.clone(),
47 type_name: e.type_name.clone(),
48 source_file: e.source_file.clone(),
49 source_line: e.source_line,
50 }
51 }
52}
53
54#[derive(Debug, Clone, Serialize, Deserialize, Default)]
60pub struct DataIndex {
61 pub ptr_to_allocation: std::collections::HashMap<String, Vec<usize>>,
63 pub thread_id_to_allocations: std::collections::HashMap<u64, Vec<usize>>,
65 pub thread_id_to_events: std::collections::HashMap<u64, Vec<usize>>,
67 pub type_name_to_allocations: std::collections::HashMap<String, Vec<usize>>,
69 pub var_name_to_allocations: std::collections::HashMap<String, Vec<usize>>,
71 pub allocation_ptr_to_passport: std::collections::HashMap<String, Vec<usize>>,
73 pub allocation_ptr_to_unsafe_reports: std::collections::HashMap<String, Vec<usize>>,
75 pub source_location_to_allocations: std::collections::HashMap<String, Vec<usize>>,
77}
78
79#[derive(Debug, Clone, Serialize, Deserialize)]
81pub struct EventSummary {
82 pub total_event_count: usize,
84 pub exported_event_count: usize,
86 pub is_sampled: bool,
88 pub sampling_strategy: String,
90 pub total_allocations: usize,
92 pub active_allocations: usize,
94 #[serde(default)]
100 pub duplicate_observations: usize,
101 #[serde(default)]
105 pub degraded_releases: usize,
106}
107
108impl EventSummary {
109 pub fn new(
110 total_event_count: usize,
111 exported_event_count: usize,
112 total_allocations: usize,
113 active_allocations: usize,
114 ) -> Self {
115 let is_sampled = exported_event_count < total_event_count;
116 let strategy = if is_sampled {
117 format!("truncated to {} events", exported_event_count)
118 } else {
119 "complete".to_string()
120 };
121 Self {
122 total_event_count,
123 exported_event_count,
124 is_sampled,
125 sampling_strategy: strategy,
126 total_allocations,
127 active_allocations,
128 duplicate_observations: 0,
129 degraded_releases: 0,
130 }
131 }
132
133 #[must_use]
135 pub fn with_degraded_releases(mut self, count: usize) -> Self {
136 self.degraded_releases = count;
137 self
138 }
139
140 #[must_use]
142 pub fn with_duplicate_observations(mut self, count: usize) -> Self {
143 self.duplicate_observations = count;
144 self
145 }
146}
147
148pub fn build_data_index(
153 allocations: &[super::types::AllocationInfo],
154 events: &[DashboardEventDTO],
155 passport_details: &[super::types::PassportDetail],
156 unsafe_reports: &[super::types::UnsafeReport],
157) -> DataIndex {
158 use std::collections::HashMap;
159
160 let mut ptr_to_allocation: HashMap<String, Vec<usize>> = HashMap::new();
161 let mut thread_id_to_allocations: HashMap<u64, Vec<usize>> = HashMap::new();
162 let mut thread_id_to_events: HashMap<u64, Vec<usize>> = HashMap::new();
163 let mut type_name_to_allocations: HashMap<String, Vec<usize>> = HashMap::new();
164 let mut var_name_to_allocations: HashMap<String, Vec<usize>> = HashMap::new();
165 let mut allocation_ptr_to_passport: HashMap<String, Vec<usize>> = HashMap::new();
166 let mut allocation_ptr_to_unsafe_reports: HashMap<String, Vec<usize>> = HashMap::new();
167 let mut source_location_to_allocations: HashMap<String, Vec<usize>> = HashMap::new();
168
169 for (idx, alloc) in allocations.iter().enumerate() {
170 ptr_to_allocation
171 .entry(alloc.address.clone())
172 .or_default()
173 .push(idx);
174
175 if let Some(tid) = parse_thread_id(&alloc.thread_id) {
177 thread_id_to_allocations.entry(tid).or_default().push(idx);
178 }
179
180 let tn = alloc.type_name.to_lowercase();
181 if !tn.is_empty() && tn != "unknown" {
182 type_name_to_allocations
183 .entry(alloc.type_name.clone())
184 .or_default()
185 .push(idx);
186 }
187
188 let vn = alloc.var_name.to_lowercase();
189 if !vn.is_empty() && vn != "unknown" {
190 var_name_to_allocations
191 .entry(alloc.var_name.clone())
192 .or_default()
193 .push(idx);
194 }
195
196 if let (Some(ref file), Some(line)) = (&alloc.source_file, alloc.source_line) {
197 let loc = format!("{}:{}", file, line);
198 source_location_to_allocations
199 .entry(loc)
200 .or_default()
201 .push(idx);
202 } else if let Some(ref file) = &alloc.source_file {
203 let loc = format!("{}:0", file);
204 source_location_to_allocations
205 .entry(loc)
206 .or_default()
207 .push(idx);
208 }
209 }
210
211 for (idx, event) in events.iter().enumerate() {
212 thread_id_to_events
213 .entry(event.thread_id)
214 .or_default()
215 .push(idx);
216 }
217
218 for (idx, passport) in passport_details.iter().enumerate() {
219 allocation_ptr_to_passport
220 .entry(passport.allocation_ptr.clone())
221 .or_default()
222 .push(idx);
223 }
224
225 for (idx, report) in unsafe_reports.iter().enumerate() {
226 allocation_ptr_to_unsafe_reports
227 .entry(report.allocation_ptr.clone())
228 .or_default()
229 .push(idx);
230 }
231
232 DataIndex {
233 ptr_to_allocation,
234 thread_id_to_allocations,
235 thread_id_to_events,
236 type_name_to_allocations,
237 var_name_to_allocations,
238 allocation_ptr_to_passport,
239 allocation_ptr_to_unsafe_reports,
240 source_location_to_allocations,
241 }
242}
243
244fn parse_thread_id(raw: &str) -> Option<u64> {
246 if let Some(stripped) = raw.strip_prefix("Thread-") {
247 stripped.parse::<u64>().ok()
248 } else {
249 raw.parse::<u64>().ok()
250 }
251}
252
253#[cfg(test)]
254mod tests {
255 use super::*;
256
257 #[test]
261 fn test_dto_from_memory_event() {
262 use crate::event_store::event::MemoryEvent;
263 let mut event = MemoryEvent::allocate(0x1000, 64, 1);
264 event.var_name = Some("buf".to_string());
265 event.type_name = Some("[u8; 64]".to_string());
266 event.source_file = Some("src/main.rs".to_string());
267 event.source_line = Some(42);
268 event.task_id = Some(7);
269
270 let dto = DashboardEventDTO::from(&event);
271
272 assert_eq!(dto.ptr, "0x1000");
273 assert_eq!(dto.size, 64);
274 assert_eq!(dto.thread_id, 1);
275 assert_eq!(dto.var_name.as_deref(), Some("buf"));
276 assert_eq!(dto.event_type, "Allocate");
277 assert_eq!(dto.task_id, Some(7));
278 assert_eq!(dto.source_file.as_deref(), Some("src/main.rs"));
279 assert_eq!(dto.source_line, Some(42));
280 }
281
282 #[test]
286 fn test_event_summary_complete() {
287 let s = EventSummary::new(100, 100, 50, 25);
288 assert!(!s.is_sampled);
289 assert_eq!(s.total_event_count, 100);
290 assert_eq!(s.exported_event_count, 100);
291 }
292
293 #[test]
296 fn test_event_summary_sampled() {
297 let s = EventSummary::new(1000, 100, 500, 200);
298 assert!(s.is_sampled);
299 assert_eq!(s.total_event_count, 1000);
300 assert_eq!(s.exported_event_count, 100);
301 }
302
303 #[test]
307 fn test_build_data_index_with_allocations() {
308 use crate::render_engine::dashboard::renderer::types::AllocationInfo;
309
310 let alloc = AllocationInfo {
311 address: "0x1000".to_string(),
312 type_name: "Vec<u8>".to_string(),
313 size: 64,
314 var_name: "buffer".to_string(),
315 timestamp: "0".to_string(),
316 thread_id: "Thread-1".to_string(),
317 immutable_borrows: 0,
318 mutable_borrows: 0,
319 is_clone: false,
320 clone_count: 0,
321 timestamp_alloc: 1000,
322 timestamp_dealloc: 0,
323 lifetime_ms: None,
324 observed_age_ms: None,
325 is_leaked: false,
326 allocation_state: crate::snapshot::AllocationState::live(),
327 leak_status: crate::snapshot::LeakStatus::NotAssessed,
328 allocation_type: "heap".to_string(),
329 is_smart_pointer: false,
330 smart_pointer_type: String::new(),
331 source_file: Some("src/main.rs".to_string()),
332 source_line: Some(42),
333 module_path: None,
334 generation_id: 0,
335 provenance: String::new(),
336 evidence: Default::default(),
337 confidence: Default::default(),
338 layout_snapshot: None,
339 };
340
341 let index = build_data_index(&[alloc], &[], &[], &[]);
342
343 assert!(index.ptr_to_allocation.contains_key("0x1000"));
344 assert!(index.type_name_to_allocations.contains_key("Vec<u8>"));
345 assert!(index.var_name_to_allocations.contains_key("buffer"));
346 assert!(index
347 .source_location_to_allocations
348 .contains_key("src/main.rs:42"));
349 }
350
351 #[test]
354 fn test_build_data_index_empty() {
355 let index = build_data_index(&[], &[], &[], &[]);
356 assert!(index.ptr_to_allocation.is_empty());
357 assert!(index.thread_id_to_allocations.is_empty());
358 assert!(index.type_name_to_allocations.is_empty());
359 }
360
361 #[test]
365 fn test_parse_thread_id() {
366 assert_eq!(parse_thread_id("Thread-1"), Some(1));
367 assert_eq!(parse_thread_id("Thread-42"), Some(42));
368 assert_eq!(parse_thread_id("42"), Some(42));
369 assert_eq!(parse_thread_id("main"), None);
370 }
371
372 #[test]
375 fn test_dto_serialization_roundtrip() {
376 let dto = DashboardEventDTO {
377 timestamp: 1234,
378 event_type: "Allocate".to_string(),
379 ptr: "0x1000".to_string(),
380 size: 64,
381 thread_id: 1,
382 task_id: Some(7),
383 var_name: Some("buf".to_string()),
384 type_name: Some("[u8]".to_string()),
385 source_file: Some("src/lib.rs".to_string()),
386 source_line: Some(10),
387 };
388
389 let json = serde_json::to_string(&dto).unwrap();
390 let deserialized: DashboardEventDTO = serde_json::from_str(&json).unwrap();
391
392 assert_eq!(deserialized.ptr, "0x1000");
393 assert_eq!(deserialized.event_type, "Allocate");
394 assert_eq!(deserialized.task_id, Some(7));
395 assert_eq!(deserialized.source_line, Some(10));
396 }
397}