edb_engine/snapshot/
pretty_print.rs

1// EDB - Ethereum Debugger
2// Copyright (C) 2024 Zhuo Zhang and Wuqi Zhang
3//
4// This program is free software: you can redistribute it and/or modify
5// it under the terms of the GNU Affero General Public License as published by
6// the Free Software Foundation, either version 3 of the License, or
7// (at your option) any later version.
8//
9// This program is distributed in the hope that it will be useful,
10// but WITHOUT ANY WARRANTY; without even the implied warranty of
11// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12// GNU Affero General Public License for more details.
13//
14// You should have received a copy of the GNU Affero General Public License
15// along with this program. If not, see <https://www.gnu.org/licenses/>.
16
17//! Pretty printing and statistics for unified snapshot collections.
18//!
19//! This module provides comprehensive display and analysis capabilities for the unified
20//! snapshot collections. It offers detailed statistics, formatted output, and visual
21//! summaries to help developers understand snapshot distribution and debugging coverage.
22//!
23//! # Core Features
24//!
25//! ## Statistics Generation
26//! - **Snapshot Distribution**: Breakdown of hook vs opcode snapshots
27//! - **Frame Coverage**: Analysis of execution frame coverage
28//! - **Type Analysis**: Detailed categorization of snapshot types
29//!
30//! ## Visual Display
31//! - **Summary Output**: Comprehensive formatted summary with colors and icons
32//! - **Frame-by-Frame Details**: Organized display by execution frame
33//! - **Coverage Visualization**: Clear indication of debugging coverage
34//! - **Statistics Dashboard**: Overview of snapshot collection metrics
35//!
36//! # Usage
37//!
38//! The pretty printing functionality is designed for:
39//! - **Development Analysis**: Understanding snapshot collection effectiveness
40//! - **Debug Coverage Assessment**: Identifying gaps in debugging instrumentation
41//! - **Performance Analysis**: Analyzing snapshot overhead and distribution
42//! - **User-Friendly Display**: Providing clear, formatted output for developers
43
44use 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/// Comprehensive statistics about snapshot distribution and coverage.
53///
54/// This structure provides detailed metrics about how snapshots are distributed
55/// across execution frames, enabling analysis of debugging coverage and effectiveness.
56#[derive(Debug, Clone)]
57pub struct SnapshotStats {
58    /// Total number of snapshots
59    pub total_snapshots: usize,
60    /// Number of hook-based snapshots
61    pub hook_snapshots: usize,
62    /// Number of opcode-level snapshots
63    pub opcode_snapshots: usize,
64    /// Total number of unique execution frames
65    pub total_frames: usize,
66    /// Number of frames that have hook snapshots
67    pub frames_with_hooks: usize,
68    /// Number of frames that have opcode snapshots
69    pub frames_with_opcodes: usize,
70}
71
72/// Pretty printing and statistics implementation for unified snapshot collections.
73impl<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    /// Generate comprehensive statistics about snapshot distribution.
80    ///
81    /// This method analyzes the snapshot collection to provide detailed metrics
82    /// about the distribution of hook vs opcode snapshots and frame coverage.
83    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    /// Print comprehensive visual summary of all snapshots with frame aggregation.
113    ///
114    /// This method provides a beautifully formatted, integrated view of both hook and
115    /// opcode snapshots, organized by execution frame for easier debugging analysis.
116    /// The output includes statistics, frame details, and a legend for easy interpretation.
117    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        // Get comprehensive statistics
129        let stats = self.get_snapshot_stats();
130
131        // Overall statistics section
132        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        // Group snapshots by frame ID while preserving order
185        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        // Print frame-by-frame details
196        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        // Print legend
207        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    /// Print detailed information for a single execution frame.
213    ///
214    /// This method displays comprehensive information about all snapshots within
215    /// a specific execution frame, including type analysis and address information.
216    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        // Determine frame type and color
227        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            // Show hook details
255            self.print_hook_details(snapshots, "          ");
256        } else {
257            // Show opcode summary
258            self.print_opcode_summary(snapshots, "          ");
259        }
260
261        // Show address information
262        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    /// Print detailed information for hook snapshots within a frame.
272    ///
273    /// This method displays USID information and other hook-specific details
274    /// for all hook snapshots in the given frame.
275    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        // Show USIDs with smart formatting (similar to hook_snapshot_inspector)
294        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    /// Print summary information for opcode snapshots within a frame.
315    ///
316    /// This method displays program counter ranges, stack depth information,
317    /// and other opcode-specific details for all opcode snapshots in the frame.
318    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}