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}
95
96impl EventSummary {
97 pub fn new(
98 total_event_count: usize,
99 exported_event_count: usize,
100 total_allocations: usize,
101 active_allocations: usize,
102 ) -> Self {
103 let is_sampled = exported_event_count < total_event_count;
104 let strategy = if is_sampled {
105 format!("truncated to {} events", exported_event_count)
106 } else {
107 "complete".to_string()
108 };
109 Self {
110 total_event_count,
111 exported_event_count,
112 is_sampled,
113 sampling_strategy: strategy,
114 total_allocations,
115 active_allocations,
116 }
117 }
118}
119
120pub fn build_data_index(
125 allocations: &[super::types::AllocationInfo],
126 events: &[DashboardEventDTO],
127 passport_details: &[super::types::PassportDetail],
128 unsafe_reports: &[super::types::UnsafeReport],
129) -> DataIndex {
130 use std::collections::HashMap;
131
132 let mut ptr_to_allocation: HashMap<String, Vec<usize>> = HashMap::new();
133 let mut thread_id_to_allocations: HashMap<u64, Vec<usize>> = HashMap::new();
134 let mut thread_id_to_events: HashMap<u64, Vec<usize>> = HashMap::new();
135 let mut type_name_to_allocations: HashMap<String, Vec<usize>> = HashMap::new();
136 let mut var_name_to_allocations: HashMap<String, Vec<usize>> = HashMap::new();
137 let mut allocation_ptr_to_passport: HashMap<String, Vec<usize>> = HashMap::new();
138 let mut allocation_ptr_to_unsafe_reports: HashMap<String, Vec<usize>> = HashMap::new();
139 let mut source_location_to_allocations: HashMap<String, Vec<usize>> = HashMap::new();
140
141 for (idx, alloc) in allocations.iter().enumerate() {
142 ptr_to_allocation
143 .entry(alloc.address.clone())
144 .or_default()
145 .push(idx);
146
147 if let Some(tid) = parse_thread_id(&alloc.thread_id) {
149 thread_id_to_allocations.entry(tid).or_default().push(idx);
150 }
151
152 let tn = alloc.type_name.to_lowercase();
153 if !tn.is_empty() && tn != "unknown" {
154 type_name_to_allocations
155 .entry(alloc.type_name.clone())
156 .or_default()
157 .push(idx);
158 }
159
160 let vn = alloc.var_name.to_lowercase();
161 if !vn.is_empty() && vn != "unknown" {
162 var_name_to_allocations
163 .entry(alloc.var_name.clone())
164 .or_default()
165 .push(idx);
166 }
167
168 if let (Some(ref file), Some(line)) = (&alloc.source_file, alloc.source_line) {
169 let loc = format!("{}:{}", file, line);
170 source_location_to_allocations
171 .entry(loc)
172 .or_default()
173 .push(idx);
174 } else if let Some(ref file) = &alloc.source_file {
175 let loc = format!("{}:0", file);
176 source_location_to_allocations
177 .entry(loc)
178 .or_default()
179 .push(idx);
180 }
181 }
182
183 for (idx, event) in events.iter().enumerate() {
184 thread_id_to_events
185 .entry(event.thread_id)
186 .or_default()
187 .push(idx);
188 }
189
190 for (idx, passport) in passport_details.iter().enumerate() {
191 allocation_ptr_to_passport
192 .entry(passport.allocation_ptr.clone())
193 .or_default()
194 .push(idx);
195 }
196
197 for (idx, report) in unsafe_reports.iter().enumerate() {
198 allocation_ptr_to_unsafe_reports
199 .entry(report.allocation_ptr.clone())
200 .or_default()
201 .push(idx);
202 }
203
204 DataIndex {
205 ptr_to_allocation,
206 thread_id_to_allocations,
207 thread_id_to_events,
208 type_name_to_allocations,
209 var_name_to_allocations,
210 allocation_ptr_to_passport,
211 allocation_ptr_to_unsafe_reports,
212 source_location_to_allocations,
213 }
214}
215
216fn parse_thread_id(raw: &str) -> Option<u64> {
218 if let Some(stripped) = raw.strip_prefix("Thread-") {
219 stripped.parse::<u64>().ok()
220 } else {
221 raw.parse::<u64>().ok()
222 }
223}
224
225#[cfg(test)]
226mod tests {
227 use super::*;
228
229 #[test]
233 fn test_dto_from_memory_event() {
234 use crate::event_store::event::MemoryEvent;
235 let mut event = MemoryEvent::allocate(0x1000, 64, 1);
236 event.var_name = Some("buf".to_string());
237 event.type_name = Some("[u8; 64]".to_string());
238 event.source_file = Some("src/main.rs".to_string());
239 event.source_line = Some(42);
240 event.task_id = Some(7);
241
242 let dto = DashboardEventDTO::from(&event);
243
244 assert_eq!(dto.ptr, "0x1000");
245 assert_eq!(dto.size, 64);
246 assert_eq!(dto.thread_id, 1);
247 assert_eq!(dto.var_name.as_deref(), Some("buf"));
248 assert_eq!(dto.event_type, "Allocate");
249 assert_eq!(dto.task_id, Some(7));
250 assert_eq!(dto.source_file.as_deref(), Some("src/main.rs"));
251 assert_eq!(dto.source_line, Some(42));
252 }
253
254 #[test]
258 fn test_event_summary_complete() {
259 let s = EventSummary::new(100, 100, 50, 25);
260 assert!(!s.is_sampled);
261 assert_eq!(s.total_event_count, 100);
262 assert_eq!(s.exported_event_count, 100);
263 }
264
265 #[test]
268 fn test_event_summary_sampled() {
269 let s = EventSummary::new(1000, 100, 500, 200);
270 assert!(s.is_sampled);
271 assert_eq!(s.total_event_count, 1000);
272 assert_eq!(s.exported_event_count, 100);
273 }
274
275 #[test]
279 fn test_build_data_index_with_allocations() {
280 use crate::render_engine::dashboard::renderer::types::AllocationInfo;
281
282 let alloc = AllocationInfo {
283 address: "0x1000".to_string(),
284 type_name: "Vec<u8>".to_string(),
285 size: 64,
286 var_name: "buffer".to_string(),
287 timestamp: "0".to_string(),
288 thread_id: "Thread-1".to_string(),
289 immutable_borrows: 0,
290 mutable_borrows: 0,
291 is_clone: false,
292 clone_count: 0,
293 timestamp_alloc: 1000,
294 timestamp_dealloc: 0,
295 lifetime_ms: 0.0,
296 is_leaked: false,
297 allocation_type: "heap".to_string(),
298 is_smart_pointer: false,
299 smart_pointer_type: String::new(),
300 source_file: Some("src/main.rs".to_string()),
301 source_line: Some(42),
302 module_path: None,
303 generation_id: 0,
304 provenance: String::new(),
305 evidence: Default::default(),
306 confidence: Default::default(),
307 layout_snapshot: None,
308 };
309
310 let index = build_data_index(&[alloc], &[], &[], &[]);
311
312 assert!(index.ptr_to_allocation.contains_key("0x1000"));
313 assert!(index.type_name_to_allocations.contains_key("Vec<u8>"));
314 assert!(index.var_name_to_allocations.contains_key("buffer"));
315 assert!(index
316 .source_location_to_allocations
317 .contains_key("src/main.rs:42"));
318 }
319
320 #[test]
323 fn test_build_data_index_empty() {
324 let index = build_data_index(&[], &[], &[], &[]);
325 assert!(index.ptr_to_allocation.is_empty());
326 assert!(index.thread_id_to_allocations.is_empty());
327 assert!(index.type_name_to_allocations.is_empty());
328 }
329
330 #[test]
334 fn test_parse_thread_id() {
335 assert_eq!(parse_thread_id("Thread-1"), Some(1));
336 assert_eq!(parse_thread_id("Thread-42"), Some(42));
337 assert_eq!(parse_thread_id("42"), Some(42));
338 assert_eq!(parse_thread_id("main"), None);
339 }
340
341 #[test]
344 fn test_dto_serialization_roundtrip() {
345 let dto = DashboardEventDTO {
346 timestamp: 1234,
347 event_type: "Allocate".to_string(),
348 ptr: "0x1000".to_string(),
349 size: 64,
350 thread_id: 1,
351 task_id: Some(7),
352 var_name: Some("buf".to_string()),
353 type_name: Some("[u8]".to_string()),
354 source_file: Some("src/lib.rs".to_string()),
355 source_line: Some(10),
356 };
357
358 let json = serde_json::to_string(&dto).unwrap();
359 let deserialized: DashboardEventDTO = serde_json::from_str(&json).unwrap();
360
361 assert_eq!(deserialized.ptr, "0x1000");
362 assert_eq!(deserialized.event_type, "Allocate");
363 assert_eq!(deserialized.task_id, Some(7));
364 assert_eq!(deserialized.source_line, Some(10));
365 }
366}