Skip to main content

profile_inspect/classify/
frame_classifier.rs

1use crate::ir::{FrameCategory, FrameKind};
2
3/// Classifies frames based on their URL and function name
4pub struct FrameClassifier {
5    /// Base path for the application (to distinguish app from deps)
6    app_base_path: Option<String>,
7}
8
9impl FrameClassifier {
10    /// Create a new classifier with an optional app base path
11    pub fn new(app_base_path: Option<String>) -> Self {
12        Self { app_base_path }
13    }
14
15    /// Classify the kind of frame based on function name
16    pub fn classify_kind(&self, name: &str, url: &str) -> FrameKind {
17        // V8 internal markers
18        if name.starts_with('(') && name.ends_with(')') {
19            return match name {
20                "(garbage collector)" => FrameKind::GC,
21                "(idle)" => FrameKind::Idle,
22                "(program)" => FrameKind::Program,
23                _ => FrameKind::Native,
24            };
25        }
26
27        // Native builtins
28        if name.contains("Builtin:") || name == "(native)" {
29            return FrameKind::Builtin;
30        }
31
32        // Eval code
33        if url.contains("eval at") || name.contains("eval") {
34            return FrameKind::Eval;
35        }
36
37        // WebAssembly
38        if url.starts_with("wasm://") || name.starts_with("wasm-") {
39            return FrameKind::Wasm;
40        }
41
42        // Regular expressions
43        if name.starts_with("RegExp:") {
44            return FrameKind::RegExp;
45        }
46
47        // Native code (no URL)
48        if url.is_empty() && !name.is_empty() {
49            return FrameKind::Native;
50        }
51
52        // Default to regular function
53        FrameKind::Function
54    }
55
56    /// Classify the category of a frame for filtering
57    pub fn classify_category(&self, url: &str, name: &str) -> FrameCategory {
58        // Node.js internals
59        if url.starts_with("node:") || url.contains("internal/") {
60            return FrameCategory::NodeInternal;
61        }
62
63        // V8 internals (special names)
64        if name.starts_with('(') && name.ends_with(')') {
65            match name {
66                "(garbage collector)" | "(idle)" | "(program)" | "(root)" => {
67                    return FrameCategory::V8Internal;
68                }
69                _ => {}
70            }
71        }
72
73        // Native/Builtin code
74        if url.is_empty() || name.contains("Builtin:") || name == "(native)" {
75            return FrameCategory::Native;
76        }
77
78        // Dependencies (node_modules)
79        if url.contains("node_modules") {
80            return FrameCategory::Deps;
81        }
82
83        // Check if it's outside the app base path (likely a dep)
84        if let Some(base) = &self.app_base_path
85            && !url.is_empty()
86            && !url.starts_with(base)
87            && !url.starts_with("file://")
88            // Could be a bundled dependency
89            && (url.contains("/dist/") || url.contains("/build/"))
90        {
91            return FrameCategory::App; // Likely bundled app code
92        }
93
94        // Default to application code
95        FrameCategory::App
96    }
97
98    /// Classify both kind and category at once
99    pub fn classify(&self, url: &str, name: &str) -> (FrameKind, FrameCategory) {
100        (
101            self.classify_kind(name, url),
102            self.classify_category(url, name),
103        )
104    }
105}
106
107impl Default for FrameClassifier {
108    fn default() -> Self {
109        Self::new(None)
110    }
111}
112
113#[cfg(test)]
114mod tests {
115    use super::*;
116
117    // ========================================================================
118    // FrameKind Classification Tests
119    // ========================================================================
120
121    #[test]
122    fn test_gc_classification() {
123        let classifier = FrameClassifier::default();
124        assert_eq!(
125            classifier.classify_kind("(garbage collector)", ""),
126            FrameKind::GC
127        );
128        assert_eq!(
129            classifier.classify_category("", "(garbage collector)"),
130            FrameCategory::V8Internal
131        );
132    }
133
134    #[test]
135    fn test_idle_classification() {
136        let classifier = FrameClassifier::default();
137        assert_eq!(classifier.classify_kind("(idle)", ""), FrameKind::Idle);
138        assert_eq!(
139            classifier.classify_category("", "(idle)"),
140            FrameCategory::V8Internal
141        );
142    }
143
144    #[test]
145    fn test_program_classification() {
146        let classifier = FrameClassifier::default();
147        assert_eq!(
148            classifier.classify_kind("(program)", ""),
149            FrameKind::Program
150        );
151        assert_eq!(
152            classifier.classify_category("", "(program)"),
153            FrameCategory::V8Internal
154        );
155    }
156
157    #[test]
158    fn test_root_classification() {
159        let classifier = FrameClassifier::default();
160        assert_eq!(
161            classifier.classify_category("", "(root)"),
162            FrameCategory::V8Internal
163        );
164    }
165
166    #[test]
167    fn test_builtin_classification() {
168        let classifier = FrameClassifier::default();
169
170        assert_eq!(
171            classifier.classify_kind("Builtin:ArrayPush", ""),
172            FrameKind::Builtin
173        );
174        // Note: (native) matches the parenthesized pattern first, so it's Native
175        // Only names containing "Builtin:" are classified as Builtin
176        assert_eq!(classifier.classify_kind("(native)", ""), FrameKind::Native);
177    }
178
179    #[test]
180    fn test_eval_classification() {
181        let classifier = FrameClassifier::default();
182
183        assert_eq!(
184            classifier.classify_kind("anonymous", "eval at <anonymous>"),
185            FrameKind::Eval
186        );
187        assert_eq!(
188            classifier.classify_kind("eval", "/src/main.js"),
189            FrameKind::Eval
190        );
191    }
192
193    #[test]
194    fn test_wasm_classification() {
195        let classifier = FrameClassifier::default();
196
197        assert_eq!(
198            classifier.classify_kind("funcName", "wasm://wasm/123456"),
199            FrameKind::Wasm
200        );
201        assert_eq!(
202            classifier.classify_kind("wasm-function[42]", ""),
203            FrameKind::Wasm
204        );
205    }
206
207    #[test]
208    fn test_regexp_classification() {
209        let classifier = FrameClassifier::default();
210        assert_eq!(
211            classifier.classify_kind("RegExp: /\\d+/", ""),
212            FrameKind::RegExp
213        );
214    }
215
216    #[test]
217    fn test_native_no_url() {
218        let classifier = FrameClassifier::default();
219        // Function with name but no URL is native
220        assert_eq!(
221            classifier.classify_kind("nativeFunction", ""),
222            FrameKind::Native
223        );
224    }
225
226    #[test]
227    fn test_regular_function() {
228        let classifier = FrameClassifier::default();
229        assert_eq!(
230            classifier.classify_kind("myFunction", "/src/main.js"),
231            FrameKind::Function
232        );
233    }
234
235    // ========================================================================
236    // FrameCategory Classification Tests
237    // ========================================================================
238
239    #[test]
240    fn test_node_internal() {
241        let classifier = FrameClassifier::default();
242        assert_eq!(
243            classifier.classify_category("node:fs", "readFile"),
244            FrameCategory::NodeInternal
245        );
246        assert_eq!(
247            classifier.classify_category("node:internal/modules/cjs/loader", "load"),
248            FrameCategory::NodeInternal
249        );
250        assert_eq!(
251            classifier.classify_category("/internal/bootstrap.js", "startup"),
252            FrameCategory::NodeInternal
253        );
254    }
255
256    #[test]
257    fn test_deps_classification() {
258        let classifier = FrameClassifier::default();
259        assert_eq!(
260            classifier.classify_category("/project/node_modules/lodash/index.js", "map"),
261            FrameCategory::Deps
262        );
263        assert_eq!(
264            classifier.classify_category("/node_modules/@babel/core/lib/index.js", "transform"),
265            FrameCategory::Deps
266        );
267        assert_eq!(
268            classifier.classify_category(
269                "file:///Users/test/project/node_modules/vitest/dist/index.js",
270                "run"
271            ),
272            FrameCategory::Deps
273        );
274    }
275
276    #[test]
277    fn test_app_code() {
278        let classifier = FrameClassifier::default();
279        assert_eq!(
280            classifier.classify_category("/project/src/main.ts", "processData"),
281            FrameCategory::App
282        );
283        assert_eq!(
284            classifier.classify_category("file:///Users/test/project/src/utils.js", "helper"),
285            FrameCategory::App
286        );
287    }
288
289    #[test]
290    fn test_native_category() {
291        let classifier = FrameClassifier::default();
292
293        // Empty URL defaults to Native
294        assert_eq!(
295            classifier.classify_category("", "someFunction"),
296            FrameCategory::Native
297        );
298
299        // Builtins are Native
300        assert_eq!(
301            classifier.classify_category("", "Builtin:ArrayPush"),
302            FrameCategory::Native
303        );
304    }
305
306    // ========================================================================
307    // Combined classify() Tests
308    // ========================================================================
309
310    #[test]
311    fn test_classify_combined() {
312        let classifier = FrameClassifier::default();
313
314        let (kind, category) = classifier.classify("", "(garbage collector)");
315        assert_eq!(kind, FrameKind::GC);
316        assert_eq!(category, FrameCategory::V8Internal);
317
318        let (kind, category) = classifier.classify("/src/main.js", "processData");
319        assert_eq!(kind, FrameKind::Function);
320        assert_eq!(category, FrameCategory::App);
321
322        let (kind, category) = classifier.classify("node:fs", "readFile");
323        assert_eq!(kind, FrameKind::Function);
324        assert_eq!(category, FrameCategory::NodeInternal);
325    }
326
327    // ========================================================================
328    // Custom App Base Path Tests
329    // ========================================================================
330
331    #[test]
332    fn test_custom_app_base_path() {
333        let classifier = FrameClassifier::new(Some("/home/user/myproject".to_string()));
334
335        // Within base path is App
336        assert_eq!(
337            classifier.classify_category("/home/user/myproject/src/main.ts", "func"),
338            FrameCategory::App
339        );
340
341        // node_modules is still Deps
342        assert_eq!(
343            classifier
344                .classify_category("/home/user/myproject/node_modules/lodash/index.js", "map"),
345            FrameCategory::Deps
346        );
347    }
348}