testlint_sdk/profiler/
typescript.rs

1#![allow(dead_code)]
2
3use super::{
4    CommonProfileData, FunctionStats, HotFunction, ProfileResult, RuntimeMetrics, StaticMetrics,
5};
6use chrono::Utc;
7use serde_json::Value;
8use std::collections::{HashMap, HashSet};
9use std::fs;
10use std::path::{Path, PathBuf};
11use std::process::{Command, Stdio};
12use std::thread;
13use std::time::Duration;
14
15#[derive(Debug, Clone)]
16struct TsProfileData {
17    execution_count: HashMap<String, u64>,
18    hot_functions: Vec<(String, u64)>,
19    total_samples: u64,
20}
21
22pub struct TypeScriptProfiler {}
23
24impl Default for TypeScriptProfiler {
25    fn default() -> Self {
26        Self::new()
27    }
28}
29
30impl TypeScriptProfiler {
31    pub fn new() -> Self {
32        TypeScriptProfiler {}
33    }
34
35    fn run_node_profiler(&self, ts_file: &str) -> Result<TsProfileData, String> {
36        // Check if Node.js is installed
37        let node_check = Command::new("node")
38            .arg("--version")
39            .stdout(Stdio::null())
40            .stderr(Stdio::null())
41            .status();
42
43        if node_check.is_err() {
44            return Err("Node.js is not installed. Install from: https://nodejs.org/".to_string());
45        }
46
47        // Convert to absolute path
48        let script_path = Path::new(ts_file)
49            .canonicalize()
50            .map_err(|e| format!("Failed to resolve script path: {}", e))?;
51
52        // Create a temporary directory for profiling
53        let temp_dir = std::env::temp_dir();
54
55        // Use unique filename to avoid conflicts when multiple profiling sessions run in parallel
56        let pid = std::process::id();
57        let timestamp = std::time::SystemTime::now()
58            .duration_since(std::time::UNIX_EPOCH)
59            .unwrap()
60            .as_millis();
61        let profile_filename = format!("node_cpu_profile_{}_{}.cpuprofile", pid, timestamp);
62        let profile_file = temp_dir.join(&profile_filename);
63
64        // Record existing .cpuprofile files before starting
65        let existing_profiles: HashSet<PathBuf> = fs::read_dir(&temp_dir)
66            .ok()
67            .map(|entries| {
68                entries
69                    .filter_map(|e| e.ok())
70                    .map(|e| e.path())
71                    .filter(|p| {
72                        p.extension()
73                            .and_then(|ext| ext.to_str())
74                            .map(|ext| ext == "cpuprofile")
75                            .unwrap_or(false)
76                    })
77                    .collect()
78            })
79            .unwrap_or_default();
80
81        println!("Starting Node.js process with --cpu-prof...");
82
83        // Start Node.js with CPU profiling enabled
84        let mut child = Command::new("node")
85            .arg("--cpu-prof")
86            .arg("--cpu-prof-dir")
87            .arg(&temp_dir)
88            .arg("--cpu-prof-name")
89            .arg(&profile_filename)
90            .arg(&script_path)
91            .stdout(Stdio::inherit())
92            .stderr(Stdio::piped())
93            .spawn()
94            .map_err(|e| format!("Failed to start Node.js process: {}", e))?;
95
96        // Wait for the process to complete
97        let status = child
98            .wait()
99            .map_err(|e| format!("Failed to wait for Node.js process: {}", e))?;
100
101        if !status.success() {
102            return Err("Node.js process exited with error".to_string());
103        }
104
105        // Give it a moment to finish writing the profile
106        thread::sleep(Duration::from_millis(500));
107
108        // Find the generated CPU profile file
109        // Node.js may create it with the specified name or its own naming scheme
110        let actual_profile = if profile_file.exists() {
111            Some(profile_file.clone())
112        } else {
113            // Look for NEW .cpuprofile files (not in the existing set)
114            fs::read_dir(&temp_dir).ok().and_then(|entries| {
115                entries.filter_map(|e| e.ok()).map(|e| e.path()).find(|p| {
116                    p.extension()
117                        .and_then(|ext| ext.to_str())
118                        .map(|ext| ext == "cpuprofile")
119                        .unwrap_or(false)
120                        && !existing_profiles.contains(p)
121                })
122            })
123        };
124
125        match actual_profile {
126            Some(profile_path) => {
127                let result = self.parse_cpu_profile(profile_path.to_str().unwrap());
128                // Clean up
129                let _ = fs::remove_file(&profile_path);
130                result
131            }
132            None => {
133                Err("CPU profile was not generated. Make sure your Node.js version supports --cpu-prof (v12+)".to_string())
134            }
135        }
136    }
137
138    fn run_node_profiler_pid(&self, pid: u32) -> Result<TsProfileData, String> {
139        use std::thread;
140        use std::time::Duration;
141
142        println!("🔍 Attaching to Node.js process PID: {}", pid);
143        println!("📊 Starting CPU profiling via V8 Inspector Protocol...");
144        println!();
145
146        // Check if process exists
147        self.check_process_exists(pid)?;
148
149        // Try to find or enable inspector
150        let inspector_port = self.find_or_enable_inspector(pid)?;
151        println!("✓ Found Node.js inspector on port {}", inspector_port);
152
153        // Start profiling
154        println!("⏱️  Profiling for 30 seconds...");
155        self.start_profiler_via_inspector(pid)?;
156
157        // Wait for profile duration
158        thread::sleep(Duration::from_secs(30));
159
160        // Stop profiling and collect results
161        let profile_file = self.stop_profiler_and_collect(pid)?;
162
163        // Parse the profile
164        let profile_data = self.parse_cpu_profile(&profile_file)?;
165
166        // Clean up
167        let _ = fs::remove_file(&profile_file);
168
169        Ok(profile_data)
170    }
171
172    /// Check if process exists
173    fn check_process_exists(&self, pid: u32) -> Result<(), String> {
174        #[cfg(unix)]
175        {
176            use std::process::Command;
177
178            let output = Command::new("ps")
179                .args(["-p", &pid.to_string()])
180                .output()
181                .map_err(|e| format!("Failed to check process: {}", e))?;
182
183            if !output.status.success() {
184                return Err(format!("Process {} not found", pid));
185            }
186
187            // Check if it's a Node.js process
188            let stdout = String::from_utf8_lossy(&output.stdout);
189            if !stdout.contains("node") {
190                println!("⚠️  Warning: Process {} may not be a Node.js process", pid);
191            }
192
193            Ok(())
194        }
195
196        #[cfg(not(unix))]
197        {
198            Ok(())
199        }
200    }
201
202    /// Find or enable inspector for the process
203    fn find_or_enable_inspector(&self, pid: u32) -> Result<u16, String> {
204        // First try to find existing inspector
205        if let Ok(port) = self.find_existing_inspector(pid) {
206            return Ok(port);
207        }
208
209        // Try to enable inspector by sending SIGUSR1
210        println!("📡 Attempting to enable inspector via SIGUSR1...");
211
212        #[cfg(unix)]
213        {
214            use std::process::Command;
215            use std::thread;
216            use std::time::Duration;
217
218            Command::new("kill")
219                .args(["-USR1", &pid.to_string()])
220                .output()
221                .map_err(|e| format!("Failed to send SIGUSR1: {}", e))?;
222
223            // Wait for inspector to start
224            thread::sleep(Duration::from_secs(1));
225
226            // Try again to find the inspector port
227            self.find_existing_inspector(pid)
228        }
229
230        #[cfg(not(unix))]
231        {
232            Err("Inspector detection not supported on this platform.\n\
233                 Start your Node.js process with --inspect flag."
234                .to_string())
235        }
236    }
237
238    /// Find the inspector port for a running Node.js process
239    fn find_existing_inspector(&self, pid: u32) -> Result<u16, String> {
240        // Check common inspector ports first (9229 is default)
241        for port in [9229, 9230, 9231, 9232, 9233] {
242            if self.test_inspector_port(port) {
243                return Ok(port);
244            }
245        }
246
247        // Try to find from process info
248        #[cfg(target_os = "linux")]
249        {
250            let cmdline_path = format!("/proc/{}/cmdline", pid);
251            if let Ok(cmdline) = fs::read_to_string(&cmdline_path) {
252                if let Some(port) = self.extract_inspector_port_from_cmdline(&cmdline) {
253                    if self.test_inspector_port(port) {
254                        return Ok(port);
255                    }
256                }
257            }
258        }
259
260        Err(format!(
261            "Could not find Node.js inspector port for PID {}.\n\
262            Process may not have inspector enabled.\n\
263            Start your Node.js app with --inspect flag or send SIGUSR1 to enable.",
264            pid
265        ))
266    }
267
268    /// Test if an inspector port is active
269    fn test_inspector_port(&self, port: u16) -> bool {
270        use std::net::TcpStream;
271        use std::time::Duration;
272
273        TcpStream::connect_timeout(
274            &format!("127.0.0.1:{}", port).parse().unwrap(),
275            Duration::from_millis(100),
276        )
277        .is_ok()
278    }
279
280    /// Extract inspector port from command line
281    #[allow(dead_code)]
282    fn extract_inspector_port_from_cmdline(&self, cmdline: &str) -> Option<u16> {
283        // Look for --inspect=<port> or --inspect-brk=<port>
284        for arg in cmdline.split('\0') {
285            if let Some(port_str) = arg.strip_prefix("--inspect=") {
286                if let Ok(port) = port_str.parse() {
287                    return Some(port);
288                }
289            }
290            if let Some(port_str) = arg.strip_prefix("--inspect-brk=") {
291                if let Ok(port) = port_str.parse() {
292                    return Some(port);
293                }
294            }
295        }
296        None
297    }
298
299    /// Start profiler via Inspector Protocol
300    fn start_profiler_via_inspector(&self, pid: u32) -> Result<(), String> {
301        let temp_dir = std::env::temp_dir();
302        let script = r#"
303const inspector = require('inspector');
304const session = new inspector.Session();
305
306session.connect();
307
308// Enable Profiler domain
309session.post('Profiler.enable', (err) => {
310  if (err) {
311    console.error('Failed to enable profiler:', err);
312    process.exit(1);
313  }
314
315  // Start profiling
316  session.post('Profiler.start', (err) => {
317    if (err) {
318      console.error('Failed to start profiler:', err);
319      process.exit(1);
320    }
321
322    console.log('[Profiler] Started CPU profiling');
323    process.exit(0);
324  });
325});
326"#;
327
328        let script_path = temp_dir.join(format!("start_profiler_{}.js", pid));
329        fs::write(&script_path, script)
330            .map_err(|e| format!("Failed to write profiler script: {}", e))?;
331
332        let output = std::process::Command::new("node")
333            .arg(&script_path)
334            .output()
335            .map_err(|e| format!("Failed to start profiler: {}", e))?;
336
337        let _ = fs::remove_file(&script_path);
338
339        if !output.status.success() {
340            return Err(format!(
341                "Failed to start profiler: {}",
342                String::from_utf8_lossy(&output.stderr)
343            ));
344        }
345
346        Ok(())
347    }
348
349    /// Stop profiler and collect results
350    fn stop_profiler_and_collect(&self, pid: u32) -> Result<String, String> {
351        let temp_dir = std::env::temp_dir();
352        let profile_file = temp_dir.join(format!("profile_{}.cpuprofile", pid));
353
354        let script = format!(
355            r#"
356const inspector = require('inspector');
357const fs = require('fs');
358const session = new inspector.Session();
359
360session.connect();
361
362// Stop profiling and get results
363session.post('Profiler.stop', (err, {{ profile }}) => {{
364  if (err) {{
365    console.error('Failed to stop profiler:', err);
366    process.exit(1);
367  }}
368
369  // Save profile to file
370  fs.writeFileSync('{}', JSON.stringify(profile, null, 2));
371  console.log('[Profiler] Saved profile to {}');
372
373  // Disable profiler
374  session.post('Profiler.disable');
375
376  process.exit(0);
377}});
378"#,
379            profile_file.display(),
380            profile_file.display()
381        );
382
383        let script_path = temp_dir.join(format!("stop_profiler_{}.js", pid));
384        fs::write(&script_path, script)
385            .map_err(|e| format!("Failed to write stop script: {}", e))?;
386
387        let output = std::process::Command::new("node")
388            .arg(&script_path)
389            .output()
390            .map_err(|e| format!("Failed to stop profiler: {}", e))?;
391
392        let _ = fs::remove_file(&script_path);
393
394        if !output.status.success() {
395            return Err(format!(
396                "Failed to stop profiler: {}",
397                String::from_utf8_lossy(&output.stderr)
398            ));
399        }
400
401        if !profile_file.exists() {
402            return Err(format!("Profile file {} not found", profile_file.display()));
403        }
404
405        Ok(profile_file.to_string_lossy().to_string())
406    }
407
408    fn parse_cpu_profile(&self, profile_path: &str) -> Result<TsProfileData, String> {
409        // Read and parse the V8 CPU profile format (JSON)
410        let profile_content = fs::read_to_string(profile_path)
411            .map_err(|e| format!("Failed to read CPU profile: {}", e))?;
412
413        let profile: Value = serde_json::from_str(&profile_content)
414            .map_err(|e| format!("Failed to parse CPU profile JSON: {}", e))?;
415
416        let mut execution_count: HashMap<String, u64> = HashMap::new();
417        let mut total_samples = 0u64;
418
419        // V8 CPU profile format has nodes array with function info
420        if let Some(nodes) = profile.get("nodes").and_then(|n| n.as_array()) {
421            // Build a map of node IDs to function names
422            let mut node_map: HashMap<i64, String> = HashMap::new();
423
424            for node in nodes {
425                if let (Some(id), Some(call_frame)) = (
426                    node.get("id").and_then(|i| i.as_i64()),
427                    node.get("callFrame"),
428                ) {
429                    let func_name = call_frame
430                        .get("functionName")
431                        .and_then(|f| f.as_str())
432                        .unwrap_or("(anonymous)");
433
434                    let url = call_frame.get("url").and_then(|u| u.as_str()).unwrap_or("");
435
436                    let line = call_frame
437                        .get("lineNumber")
438                        .and_then(|l| l.as_i64())
439                        .unwrap_or(-1);
440
441                    // Create a readable function identifier
442                    let func_identifier = if !url.is_empty() && !url.is_empty() {
443                        let file_name = Path::new(url)
444                            .file_name()
445                            .and_then(|n| n.to_str())
446                            .unwrap_or("unknown");
447                        if line >= 0 {
448                            format!("{}:{}:{}", file_name, line + 1, func_name)
449                        } else {
450                            format!("{}:{}", file_name, func_name)
451                        }
452                    } else {
453                        func_name.to_string()
454                    };
455
456                    node_map.insert(id, func_identifier);
457
458                    // Count hit count if available
459                    if let Some(hit_count) = node.get("hitCount").and_then(|h| h.as_u64()) {
460                        if hit_count > 0 {
461                            *execution_count.entry(node_map[&id].clone()).or_insert(0) += hit_count;
462                            total_samples += hit_count;
463                        }
464                    }
465                }
466            }
467
468            // Also parse samples array if available for more accurate counts
469            if let Some(samples) = profile.get("samples").and_then(|s| s.as_array()) {
470                for sample in samples {
471                    if let Some(node_id) = sample.as_i64() {
472                        if let Some(func_name) = node_map.get(&node_id) {
473                            *execution_count.entry(func_name.clone()).or_insert(0) += 1;
474                            total_samples += 1;
475                        }
476                    }
477                }
478            }
479        }
480
481        // Sort and get hot functions
482        let mut hot_functions: Vec<(String, u64)> = execution_count
483            .iter()
484            .map(|(k, v)| (k.clone(), *v))
485            .collect();
486        hot_functions.sort_by(|a, b| b.1.cmp(&a.1));
487        hot_functions.truncate(10);
488
489        Ok(TsProfileData {
490            execution_count,
491            hot_functions,
492            total_samples,
493        })
494    }
495
496    pub fn profile_continuous(&self, ts_file: &str) -> Result<ProfileResult, String> {
497        println!("Starting TypeScript/Node.js continuous runtime profiling...");
498        println!("File: {}", ts_file);
499        println!("Running until process completes...\n");
500
501        let profile_data = self.run_node_profiler(ts_file)?;
502
503        let mut details = Vec::new();
504        details.push("=== Runtime Profile (Node.js CPU Profiler) ===".to_string());
505        details.push(format!(
506            "Total samples collected: {}",
507            profile_data.total_samples
508        ));
509        details.push(format!(
510            "Unique functions executed: {}",
511            profile_data.execution_count.len()
512        ));
513        details.push("\nTop 10 Hot Functions:".to_string());
514
515        for (idx, (func_name, count)) in profile_data.hot_functions.iter().enumerate() {
516            let percentage = if profile_data.total_samples > 0 {
517                (*count as f64 / profile_data.total_samples as f64) * 100.0
518            } else {
519                0.0
520            };
521            details.push(format!(
522                "  {}. {} - {} samples ({:.2}%)",
523                idx + 1,
524                func_name,
525                count,
526                percentage
527            ));
528        }
529
530        Ok(ProfileResult {
531            language: "TypeScript/Node.js".to_string(),
532            details,
533        })
534    }
535
536    pub fn profile_pid(&self, pid: u32) -> Result<ProfileResult, String> {
537        self.run_node_profiler_pid(pid)?;
538
539        Ok(ProfileResult {
540            language: "TypeScript/Node.js".to_string(),
541            details: vec!["PID attach completed".to_string()],
542        })
543    }
544
545    pub fn profile_to_common_format(&self, ts_file: &str) -> Result<CommonProfileData, String> {
546        println!("Starting TypeScript/Node.js runtime profiling for JSON export...");
547
548        let profile_data = self.run_node_profiler(ts_file)?;
549
550        // Build function stats from runtime data
551        let mut function_stats = HashMap::new();
552
553        for (func_name, count) in &profile_data.execution_count {
554            let percentage = if profile_data.total_samples > 0 {
555                (*count as f64 / profile_data.total_samples as f64) * 100.0
556            } else {
557                0.0
558            };
559
560            function_stats.insert(
561                func_name.clone(),
562                FunctionStats {
563                    name: func_name.clone(),
564                    execution_count: *count,
565                    percentage,
566                    line_number: None,
567                    file_path: None,
568                },
569            );
570        }
571
572        let hot_functions: Vec<HotFunction> = profile_data
573            .hot_functions
574            .iter()
575            .enumerate()
576            .map(|(idx, (name, samples))| {
577                let percentage = if profile_data.total_samples > 0 {
578                    (*samples as f64 / profile_data.total_samples as f64) * 100.0
579                } else {
580                    0.0
581                };
582                HotFunction {
583                    rank: idx + 1,
584                    name: name.clone(),
585                    samples: *samples,
586                    percentage,
587                }
588            })
589            .collect();
590
591        let runtime_metrics = RuntimeMetrics {
592            total_samples: profile_data.total_samples,
593            execution_duration_secs: 0,
594            functions_executed: profile_data.execution_count.len(),
595            function_stats,
596            hot_functions,
597        };
598
599        let static_metrics = StaticMetrics {
600            file_size_bytes: 0,
601            line_count: 0,
602            function_count: 0,
603            class_count: 0,
604            import_count: 0,
605            complexity_score: 0,
606        };
607
608        Ok(CommonProfileData {
609            language: "TypeScript/Node.js".to_string(),
610            source_file: ts_file.to_string(),
611            timestamp: Utc::now().to_rfc3339(),
612            static_analysis: static_metrics,
613            runtime_analysis: Some(runtime_metrics),
614        })
615    }
616}
617
618#[cfg(test)]
619mod tests {
620    use super::*;
621
622    #[test]
623    fn test_profiler_new() {
624        let profiler = TypeScriptProfiler::new();
625        assert_eq!(std::mem::size_of_val(&profiler), 0);
626    }
627
628    #[test]
629    fn test_profiler_default() {
630        let profiler = TypeScriptProfiler::default();
631        assert_eq!(std::mem::size_of_val(&profiler), 0);
632    }
633}