testlint_sdk/profiler/
php.rs

1#![allow(dead_code)]
2
3use crate::profiler::ProfileResult;
4use std::fs;
5use std::process::Command;
6
7pub struct PhpProfiler;
8
9impl Default for PhpProfiler {
10    fn default() -> Self {
11        Self::new()
12    }
13}
14
15impl PhpProfiler {
16    pub fn new() -> Self {
17        PhpProfiler
18    }
19
20    /// Continuous profiling of PHP applications using Xdebug
21    pub fn profile_continuous(&self, php_file: &str) -> Result<ProfileResult, String> {
22        println!("🐘 Starting PHP runtime profiling...");
23        println!("📝 Note: This uses Xdebug profiler mode");
24
25        // Check if Xdebug is installed
26        if !self.is_xdebug_installed() {
27            return Err("Xdebug not found. Please install it:\n\
28                 - PECL: pecl install xdebug\n\
29                 - Or see: https://xdebug.org/docs/install"
30                .to_string());
31        }
32
33        self.profile_with_xdebug(php_file)
34    }
35
36    /// Profile PHP script using Xdebug
37    fn profile_with_xdebug(&self, php_file: &str) -> Result<ProfileResult, String> {
38        println!("🚀 Profiling PHP script: {}", php_file);
39
40        // Create output directory for cachegrind files
41        let output_dir = "xdebug_profile";
42        fs::create_dir_all(output_dir)
43            .map_err(|e| format!("Failed to create output directory: {}", e))?;
44
45        // Run PHP with Xdebug profiling enabled
46        let mut cmd = Command::new("php");
47        cmd.args([
48            "-d",
49            "xdebug.mode=profile",
50            "-d",
51            &format!("xdebug.output_dir={}", output_dir),
52            "-d",
53            "xdebug.profiler_output_name=cachegrind.out.%p",
54            php_file,
55        ]);
56
57        println!("Running: php -d xdebug.mode=profile {}", php_file);
58        println!("Profiling until script exits...");
59
60        let output = cmd
61            .output()
62            .map_err(|e| format!("Failed to run PHP: {}", e))?;
63
64        if !output.status.success() {
65            return Err(format!(
66                "PHP script failed: {}",
67                String::from_utf8_lossy(&output.stderr)
68            ));
69        }
70
71        // Show output from the script
72        let stdout = String::from_utf8_lossy(&output.stdout);
73        if !stdout.is_empty() {
74            println!("\n--- Script Output ---");
75            println!("{}", stdout);
76            println!("--- End Output ---\n");
77        }
78
79        self.parse_xdebug_output(output_dir)
80    }
81
82    /// Check if Xdebug is installed
83    fn is_xdebug_installed(&self) -> bool {
84        let output = Command::new("php").args(["-m"]).output();
85
86        if let Ok(output) = output {
87            let modules = String::from_utf8_lossy(&output.stdout);
88            return modules.contains("Xdebug") || modules.contains("xdebug");
89        }
90
91        false
92    }
93
94    /// Parse Xdebug output and create ProfileResult
95    fn parse_xdebug_output(&self, output_dir: &str) -> Result<ProfileResult, String> {
96        // Find the cachegrind file
97        let cachegrind_file = self.find_cachegrind_file(output_dir)?;
98
99        let file_size = fs::metadata(&cachegrind_file).map(|m| m.len()).unwrap_or(0);
100
101        // Try to extract some basic stats from cachegrind file
102        let stats = self.analyze_cachegrind(&cachegrind_file)?;
103
104        let mut details = vec![
105            "✓ Profiling completed successfully".to_string(),
106            format!("📊 Cachegrind file generated: {}", cachegrind_file),
107            format!("   Size: {} bytes", file_size),
108            "".to_string(),
109        ];
110
111        details.extend(stats);
112
113        details.extend(vec![
114            "".to_string(),
115            "To analyze the profile:".to_string(),
116            "".to_string(),
117            "1. Using KCacheGrind (Linux/macOS):".to_string(),
118            format!("   kcachegrind {}", cachegrind_file),
119            "".to_string(),
120            "2. Using QCacheGrind (macOS):".to_string(),
121            "   brew install qcachegrind".to_string(),
122            format!("   qcachegrind {}", cachegrind_file),
123            "".to_string(),
124            "3. Using WinCacheGrind (Windows):".to_string(),
125            "   Download from: https://sourceforge.net/projects/wincachegrind/".to_string(),
126            "".to_string(),
127            "Cachegrind file contains:".to_string(),
128            "  - Function call counts".to_string(),
129            "  - Time spent in each function".to_string(),
130            "  - Call graph relationships".to_string(),
131            "  - Memory usage (if enabled)".to_string(),
132        ]);
133
134        Ok(ProfileResult {
135            language: "PHP".to_string(),
136            details,
137        })
138    }
139
140    /// Find the cachegrind file in output directory
141    fn find_cachegrind_file(&self, output_dir: &str) -> Result<String, String> {
142        let entries = fs::read_dir(output_dir)
143            .map_err(|e| format!("Failed to read output directory: {}", e))?;
144
145        for entry in entries.flatten() {
146            if let Some(name) = entry.file_name().to_str() {
147                if name.starts_with("cachegrind.out") {
148                    return Ok(format!("{}/{}", output_dir, name));
149                }
150            }
151        }
152
153        Err("No cachegrind file found. Profiling may have failed.".to_string())
154    }
155
156    /// Analyze cachegrind file for basic stats
157    fn analyze_cachegrind(&self, cachegrind_file: &str) -> Result<Vec<String>, String> {
158        let content = fs::read_to_string(cachegrind_file)
159            .map_err(|e| format!("Failed to read cachegrind file: {}", e))?;
160
161        let mut function_count = 0;
162        let mut total_calls = 0;
163
164        for line in content.lines() {
165            if line.starts_with("fn=") {
166                function_count += 1;
167            } else if line.starts_with("calls=") {
168                if let Some(calls_str) = line.split('=').nth(1) {
169                    if let Ok(calls) = calls_str
170                        .split_whitespace()
171                        .next()
172                        .unwrap_or("0")
173                        .parse::<u64>()
174                    {
175                        total_calls += calls;
176                    }
177                }
178            }
179        }
180
181        Ok(vec![
182            "Profile Statistics:".to_string(),
183            format!("  - Functions profiled: {}", function_count),
184            format!("  - Total function calls: {}", total_calls),
185        ])
186    }
187
188    /// Profile by attaching to PID (not supported for PHP)
189    pub fn profile_pid(&self, _pid: u32) -> Result<ProfileResult, String> {
190        Err("PID profiling is not supported for PHP.\n\
191             PHP profiling requires running the script with Xdebug enabled from the start."
192            .to_string())
193    }
194}
195
196#[cfg(test)]
197mod tests {
198    use super::*;
199
200    #[test]
201    fn test_profiler_new() {
202        let profiler = PhpProfiler::new();
203        assert_eq!(std::mem::size_of_val(&profiler), 0);
204    }
205
206    #[test]
207    fn test_profiler_default() {
208        let profiler = PhpProfiler;
209        assert_eq!(std::mem::size_of_val(&profiler), 0);
210    }
211}