edb_engine/snapshot/
pretty_print.rs1use std::collections::HashMap;
45
46use edb_common::types::ExecutionFrameId;
47use revm::{database::CacheDB, Database, DatabaseCommit, DatabaseRef};
48use tracing::error;
49
50use crate::{Snapshot, SnapshotDetail, Snapshots};
51
52#[derive(Debug, Clone)]
57pub struct SnapshotStats {
58 pub total_snapshots: usize,
60 pub hook_snapshots: usize,
62 pub opcode_snapshots: usize,
64 pub total_frames: usize,
66 pub frames_with_hooks: usize,
68 pub frames_with_opcodes: usize,
70}
71
72impl<DB> Snapshots<DB>
74where
75 DB: Database + DatabaseCommit + DatabaseRef + Clone,
76 <CacheDB<DB> as Database>::Error: Clone,
77 <DB as Database>::Error: Clone,
78{
79 pub fn get_snapshot_stats(&self) -> SnapshotStats {
84 let mut hook_count = 0;
85 let mut opcode_count = 0;
86 let mut frames_with_hooks = std::collections::HashSet::new();
87 let mut frames_with_opcodes = std::collections::HashSet::new();
88
89 for (frame_id, snapshot) in &self.inner {
90 match snapshot.detail() {
91 SnapshotDetail::Hook(_) => {
92 hook_count += 1;
93 frames_with_hooks.insert(*frame_id);
94 }
95 SnapshotDetail::Opcode(_) => {
96 opcode_count += 1;
97 frames_with_opcodes.insert(*frame_id);
98 }
99 }
100 }
101
102 SnapshotStats {
103 total_snapshots: self.inner.len(),
104 hook_snapshots: hook_count,
105 opcode_snapshots: opcode_count,
106 total_frames: self.frame_count(),
107 frames_with_hooks: frames_with_hooks.len(),
108 frames_with_opcodes: frames_with_opcodes.len(),
109 }
110 }
111
112 pub fn print_summary(&self) {
118 println!(
119 "\n\x1b[36m╔══════════════════════════════════════════════════════════════════╗\x1b[0m"
120 );
121 println!(
122 "\x1b[36m║ UNIFIED SNAPSHOTS SUMMARY ║\x1b[0m"
123 );
124 println!(
125 "\x1b[36m╚══════════════════════════════════════════════════════════════════╝\x1b[0m\n"
126 );
127
128 let stats = self.get_snapshot_stats();
130
131 println!("\x1b[33m📊 Overall Statistics:\x1b[0m");
133 println!(" Total snapshots: \x1b[32m{}\x1b[0m", stats.total_snapshots);
134 println!(" Total frames: \x1b[32m{}\x1b[0m", stats.total_frames);
135 println!(
136 " └─ Hook snapshots: \x1b[32m{}\x1b[0m ({:.1}%)",
137 stats.hook_snapshots,
138 if stats.total_snapshots > 0 {
139 stats.hook_snapshots as f64 / stats.total_snapshots as f64 * 100.0
140 } else {
141 0.0
142 }
143 );
144 println!(
145 " └─ Opcode snapshots: \x1b[32m{}\x1b[0m ({:.1}%)",
146 stats.opcode_snapshots,
147 if stats.total_snapshots > 0 {
148 stats.opcode_snapshots as f64 / stats.total_snapshots as f64 * 100.0
149 } else {
150 0.0
151 }
152 );
153
154 println!("\n\x1b[33m🎯 Frame Coverage:\x1b[0m");
155 println!(
156 " Frames with hooks: \x1b[32m{}\x1b[0m ({:.1}%)",
157 stats.frames_with_hooks,
158 if stats.total_frames > 0 {
159 stats.frames_with_hooks as f64 / stats.total_frames as f64 * 100.0
160 } else {
161 0.0
162 }
163 );
164 println!(
165 " Frames with opcodes: \x1b[32m{}\x1b[0m ({:.1}%)",
166 stats.frames_with_opcodes,
167 if stats.total_frames > 0 {
168 stats.frames_with_opcodes as f64 / stats.total_frames as f64 * 100.0
169 } else {
170 0.0
171 }
172 );
173
174 if self.is_empty() {
175 println!("\n\x1b[90m No snapshots were recorded.\x1b[0m");
176 return;
177 }
178
179 println!("\n\x1b[33m📋 Frame Details:\x1b[0m");
180 println!(
181 "\x1b[90m─────────────────────────────────────────────────────────────────\x1b[0m"
182 );
183
184 let mut frame_groups: HashMap<ExecutionFrameId, Vec<&Snapshot<DB>>> = HashMap::new();
186 let mut frame_order = Vec::new();
187
188 for (frame_id, snapshot) in &self.inner {
189 if !frame_groups.contains_key(frame_id) {
190 frame_order.push(*frame_id);
191 }
192 frame_groups.entry(*frame_id).or_default().push(snapshot);
193 }
194
195 for (display_idx, frame_id) in frame_order.iter().enumerate() {
197 let snapshots = frame_groups.get(frame_id).unwrap();
198
199 self.print_frame_summary(display_idx, *frame_id, snapshots);
200 }
201
202 println!(
203 "\n\x1b[90m─────────────────────────────────────────────────────────────────\x1b[0m"
204 );
205
206 println!("\n\x1b[33m📖 Legend:\x1b[0m");
208 println!(" \x1b[92m🎯 Hook\x1b[0m - Strategic instrumentation breakpoint");
209 println!(" \x1b[94m⚙️ Opcode\x1b[0m - Fine-grained instruction-level snapshot");
210 }
211
212 fn print_frame_summary(
217 &self,
218 display_idx: usize,
219 frame_id: ExecutionFrameId,
220 snapshots: &[&Snapshot<DB>],
221 ) {
222 let hook_count = snapshots.iter().filter(|s| s.is_hook()).count();
223 let opcode_count = snapshots.iter().filter(|s| s.is_opcode()).count();
224 let total_count = snapshots.len();
225
226 let (frame_type, color, icon) = if hook_count > 0 && opcode_count > 0 {
228 error!("Frame {} has both hook and opcode snapshots, which is unexpected.", frame_id);
229 ("Mixed", "\x1b[96m", "📍")
230 } else if hook_count > 0 {
231 ("Hook", "\x1b[92m", "🎯")
232 } else {
233 ("Opcode", "\x1b[94m", "⚙️")
234 };
235
236 println!(
237 "\n {}[{:3}] {} Frame {}\x1b[0m (trace.{}, re-entry {})",
238 color,
239 display_idx,
240 icon,
241 frame_id,
242 frame_id.trace_entry_id(),
243 frame_id.re_entry_count()
244 );
245
246 println!(
247 " └─ Type: \x1b[33m{frame_type}\x1b[0m | Snapshots: \x1b[32m{total_count}\x1b[0m"
248 );
249
250 if hook_count > 0 && opcode_count > 0 {
251 println!(" ├─ Hook snapshots: \x1b[32m{hook_count}\x1b[0m");
252 println!(" └─ Opcode snapshots: \x1b[32m{opcode_count}\x1b[0m");
253 } else if hook_count > 0 {
254 self.print_hook_details(snapshots, " ");
256 } else {
257 self.print_opcode_summary(snapshots, " ");
259 }
260
261 let addresses: std::collections::HashSet<_> =
263 snapshots.iter().map(|s| s.bytecode_address()).collect();
264 if addresses.len() == 1 {
265 println!(" └─ Address: \x1b[36m{:?}\x1b[0m", addresses.iter().next().unwrap());
266 } else if !addresses.is_empty() {
267 println!(" └─ Addresses: \x1b[36m{} unique\x1b[0m", addresses.len());
268 }
269 }
270
271 fn print_hook_details(&self, snapshots: &[&Snapshot<DB>], indent: &str) {
276 let hook_snapshots: Vec<_> = snapshots
277 .iter()
278 .filter_map(|s| {
279 if let SnapshotDetail::Hook(ref hook) = s.detail {
280 Some(hook)
281 } else {
282 None
283 }
284 })
285 .collect();
286
287 if hook_snapshots.is_empty() {
288 return;
289 }
290
291 let usids: Vec<_> = hook_snapshots.iter().map(|h| h.usid).collect();
292
293 if usids.len() == 1 {
295 println!("{}└─ USID: \x1b[36m{}\x1b[0m", indent, usids[0]);
296 } else if usids.len() <= 10 {
297 let usid_list: Vec<String> = usids.iter().map(|u| u.to_string()).collect();
298 println!("{}└─ USIDs: \x1b[36m[{}]\x1b[0m", indent, usid_list.join(", "));
299 } else {
300 let first_few: Vec<String> = usids.iter().take(3).map(|u| u.to_string()).collect();
301 let last_few: Vec<String> =
302 usids.iter().rev().take(3).rev().map(|u| u.to_string()).collect();
303
304 println!(
305 "{}└─ USIDs: \x1b[36m[{}, ... {}, {} total]\x1b[0m",
306 indent,
307 first_few.join(", "),
308 last_few.join(", "),
309 usids.len()
310 );
311 }
312 }
313
314 fn print_opcode_summary(&self, snapshots: &[&Snapshot<DB>], indent: &str) {
319 let opcode_snapshots: Vec<_> = snapshots
320 .iter()
321 .filter_map(|s| {
322 if let SnapshotDetail::Opcode(ref opcode) = s.detail {
323 Some(opcode)
324 } else {
325 None
326 }
327 })
328 .collect();
329
330 if opcode_snapshots.is_empty() {
331 return;
332 }
333
334 let pc_range = if opcode_snapshots.len() == 1 {
335 format!("PC {}", opcode_snapshots[0].pc)
336 } else {
337 let min_pc = opcode_snapshots.iter().map(|s| s.pc).min().unwrap_or(0);
338 let max_pc = opcode_snapshots.iter().map(|s| s.pc).max().unwrap_or(0);
339 format!("PC {min_pc}..{max_pc}")
340 };
341
342 let avg_stack: f64 = if !opcode_snapshots.is_empty() {
343 opcode_snapshots.iter().map(|s| s.stack.len()).sum::<usize>() as f64
344 / opcode_snapshots.len() as f64
345 } else {
346 0.0
347 };
348
349 println!("{indent}├─ Range: \x1b[36m{pc_range}\x1b[0m");
350 println!("{indent}└─ Avg stack depth: \x1b[36m{avg_stack:.1}\x1b[0m");
351 }
352}