Skip to main content

profile_inspect/ir/
frame.rs

1use serde::Serialize;
2
3/// Unique identifier for a frame
4#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize)]
5pub struct FrameId(pub u32);
6
7/// Classification of what kind of code a frame represents
8#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize)]
9#[serde(rename_all = "snake_case")]
10pub enum FrameKind {
11    /// Regular JavaScript/TypeScript function
12    Function,
13    /// Native C++ code
14    Native,
15    /// Garbage collection
16    GC,
17    /// eval() code
18    Eval,
19    /// WebAssembly code
20    Wasm,
21    /// V8 builtin function
22    Builtin,
23    /// Regular expression execution
24    RegExp,
25    /// CPU idle time
26    Idle,
27    /// Program root
28    Program,
29    /// Unknown frame type
30    Unknown,
31}
32
33/// Category for filtering and grouping frames
34#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize)]
35#[serde(rename_all = "snake_case")]
36pub enum FrameCategory {
37    /// Application code (user's code)
38    App,
39    /// Third-party dependencies (node_modules)
40    Deps,
41    /// Node.js internal modules
42    NodeInternal,
43    /// V8 engine internals
44    V8Internal,
45    /// Native/builtin code
46    Native,
47}
48
49impl FrameCategory {
50    /// Check if this category represents "internal" code (Node/V8/Native)
51    pub fn is_internal(&self) -> bool {
52        matches!(self, Self::NodeInternal | Self::V8Internal | Self::Native)
53    }
54}
55
56/// Normalized frame representation used across all analyses
57#[derive(Debug, Clone, Serialize)]
58pub struct Frame {
59    /// Unique identifier for this frame
60    pub id: FrameId,
61
62    /// Function name (resolved via sourcemap if available)
63    pub name: String,
64
65    /// Source file path
66    pub file: Option<String>,
67
68    /// Line number (1-based)
69    pub line: Option<u32>,
70
71    /// Column number (1-based)
72    pub col: Option<u32>,
73
74    /// What kind of frame this is
75    pub kind: FrameKind,
76
77    /// Category for filtering
78    pub category: FrameCategory,
79
80    /// Original minified name if resolved via sourcemap
81    pub minified_name: Option<String>,
82
83    /// Original minified location if resolved via sourcemap
84    pub minified_location: Option<String>,
85}
86
87impl Frame {
88    /// Create a new frame with the given properties
89    pub fn new(
90        id: FrameId,
91        name: String,
92        file: Option<String>,
93        line: Option<u32>,
94        col: Option<u32>,
95        kind: FrameKind,
96        category: FrameCategory,
97    ) -> Self {
98        Self {
99            id,
100            name,
101            file,
102            line,
103            col,
104            kind,
105            category,
106            minified_name: None,
107            minified_location: None,
108        }
109    }
110
111    /// Get a display-friendly location string
112    pub fn location(&self) -> String {
113        match (&self.file, self.line, self.col) {
114            (Some(file), Some(line), Some(col)) => {
115                format!("{}:{line}:{col}", Self::clean_file_path(file))
116            }
117            (Some(file), Some(line), None) => {
118                format!("{}:{line}", Self::clean_file_path(file))
119            }
120            (Some(file), None, None) => Self::clean_file_path(file),
121            _ => "(unknown)".to_string(),
122        }
123    }
124
125    /// Clean a file path for display (strip file:// prefix, etc.)
126    fn clean_file_path(path: &str) -> String {
127        path.strip_prefix("file://").unwrap_or(path).to_string()
128    }
129
130    /// Get the cleaned file path (without file:// prefix)
131    pub fn clean_file(&self) -> Option<String> {
132        self.file.as_ref().map(|f| Self::clean_file_path(f))
133    }
134
135    /// Get a short display name (function name or fallback)
136    ///
137    /// For anonymous functions, uses a location-based name if available.
138    pub fn display_name(&self) -> String {
139        if !self.name.is_empty() && self.name != "(anonymous)" {
140            return self.name.clone();
141        }
142
143        // For anonymous functions with known location, create a descriptive name
144        if let Some(file) = &self.file {
145            // Clean the path and extract just the filename
146            let clean_path = Self::clean_file_path(file);
147            let filename = std::path::Path::new(&clean_path)
148                .file_name()
149                .map(|s| s.to_string_lossy())
150                .unwrap_or_else(|| clean_path.as_str().into());
151
152            if let Some(line) = self.line {
153                return format!("(anonymous @ {filename}:{line})");
154            }
155            return format!("(anonymous @ {filename})");
156        }
157
158        "(anonymous)".to_string()
159    }
160}
161
162impl std::fmt::Display for FrameKind {
163    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
164        match self {
165            Self::Function => write!(f, "function"),
166            Self::Native => write!(f, "native"),
167            Self::GC => write!(f, "gc"),
168            Self::Eval => write!(f, "eval"),
169            Self::Wasm => write!(f, "wasm"),
170            Self::Builtin => write!(f, "builtin"),
171            Self::RegExp => write!(f, "regexp"),
172            Self::Idle => write!(f, "idle"),
173            Self::Program => write!(f, "program"),
174            Self::Unknown => write!(f, "unknown"),
175        }
176    }
177}
178
179impl std::fmt::Display for FrameCategory {
180    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
181        match self {
182            Self::App => write!(f, "App"),
183            Self::Deps => write!(f, "Dependencies"),
184            Self::NodeInternal => write!(f, "Node.js Internal"),
185            Self::V8Internal => write!(f, "V8 Internal"),
186            Self::Native => write!(f, "Native"),
187        }
188    }
189}