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 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 let script_path = Path::new(ts_file)
49 .canonicalize()
50 .map_err(|e| format!("Failed to resolve script path: {}", e))?;
51
52 let temp_dir = std::env::temp_dir();
54
55 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 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 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 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 thread::sleep(Duration::from_millis(500));
107
108 let actual_profile = if profile_file.exists() {
111 Some(profile_file.clone())
112 } else {
113 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 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 self.check_process_exists(pid)?;
148
149 let inspector_port = self.find_or_enable_inspector(pid)?;
151 println!("✓ Found Node.js inspector on port {}", inspector_port);
152
153 println!("⏱️ Profiling for 30 seconds...");
155 self.start_profiler_via_inspector(pid)?;
156
157 thread::sleep(Duration::from_secs(30));
159
160 let profile_file = self.stop_profiler_and_collect(pid)?;
162
163 let profile_data = self.parse_cpu_profile(&profile_file)?;
165
166 let _ = fs::remove_file(&profile_file);
168
169 Ok(profile_data)
170 }
171
172 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 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 fn find_or_enable_inspector(&self, pid: u32) -> Result<u16, String> {
204 if let Ok(port) = self.find_existing_inspector(pid) {
206 return Ok(port);
207 }
208
209 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 thread::sleep(Duration::from_secs(1));
225
226 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 fn find_existing_inspector(&self, pid: u32) -> Result<u16, String> {
240 for port in [9229, 9230, 9231, 9232, 9233] {
242 if self.test_inspector_port(port) {
243 return Ok(port);
244 }
245 }
246
247 #[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 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 #[allow(dead_code)]
282 fn extract_inspector_port_from_cmdline(&self, cmdline: &str) -> Option<u16> {
283 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 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 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 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 if let Some(nodes) = profile.get("nodes").and_then(|n| n.as_array()) {
421 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 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 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 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 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 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}