testlint_sdk/profiler/
csharp.rs

1#![allow(dead_code)]
2
3use crate::profiler::ProfileResult;
4use std::fs;
5use std::path::Path;
6use std::process::{Command, Stdio};
7
8pub struct CSharpProfiler;
9
10impl Default for CSharpProfiler {
11    fn default() -> Self {
12        Self::new()
13    }
14}
15
16impl CSharpProfiler {
17    pub fn new() -> Self {
18        CSharpProfiler
19    }
20
21    /// Continuous profiling of C# applications using dotnet-trace
22    pub fn profile_continuous(&self, csharp_file_or_dll: &str) -> Result<ProfileResult, String> {
23        println!("🔷 Starting C# runtime profiling...");
24        println!("📝 Note: This uses dotnet-trace for CPU profiling");
25
26        // Check if dotnet-trace is installed
27        if !self.is_dotnet_trace_installed() {
28            println!("⚠️  dotnet-trace not found. Attempting to install...");
29            self.install_dotnet_trace()?;
30        }
31
32        let path = Path::new(csharp_file_or_dll);
33        let is_source = path.extension().is_some_and(|ext| ext == "cs");
34
35        if is_source {
36            // For .cs files, we need to run them with dotnet run
37            self.profile_csharp_source(csharp_file_or_dll)
38        } else {
39            // For .dll or .exe files, profile directly
40            self.profile_csharp_binary(csharp_file_or_dll)
41        }
42    }
43
44    /// Profile a C# source file (compile and run with profiling)
45    fn profile_csharp_source(&self, _cs_file: &str) -> Result<ProfileResult, String> {
46        // Check if it's a .NET project
47        if !Path::new("*.csproj").exists() && self.find_csproj().is_none() {
48            return Err(
49                "No .csproj file found. Please run from a .NET project directory.".to_string(),
50            );
51        }
52
53        println!("🔨 Running .NET project with profiling...");
54
55        // Start dotnet run in background and get its PID
56        let mut run_cmd = Command::new("dotnet");
57        run_cmd.arg("run");
58        run_cmd.arg("--");
59
60        let child = run_cmd
61            .spawn()
62            .map_err(|e| format!("Failed to start dotnet run: {}", e))?;
63
64        let pid = child.id();
65        println!("Started .NET application with PID: {}", pid);
66
67        // Start profiling
68        self.profile_running_process(pid)
69    }
70
71    /// Profile a C# binary (DLL or EXE)
72    fn profile_csharp_binary(&self, binary_path: &str) -> Result<ProfileResult, String> {
73        println!("🚀 Profiling C# binary: {}", binary_path);
74
75        // Start the binary in background
76        let mut run_cmd = Command::new("dotnet");
77        run_cmd.arg(binary_path);
78
79        let child = run_cmd
80            .spawn()
81            .map_err(|e| format!("Failed to start {}: {}", binary_path, e))?;
82
83        let pid = child.id();
84        println!("Started application with PID: {}", pid);
85
86        // Start profiling
87        self.profile_running_process(pid)
88    }
89
90    /// Profile a running .NET process
91    fn profile_running_process(&self, pid: u32) -> Result<ProfileResult, String> {
92        println!("📊 Starting dotnet-trace on PID {}...", pid);
93        println!("Press Ctrl+C to stop profiling and generate trace file...");
94
95        let trace_file = format!("trace_{}.nettrace", pid);
96
97        let mut cmd = Command::new("dotnet-trace");
98        cmd.args([
99            "collect",
100            "--process-id",
101            &pid.to_string(),
102            "--providers",
103            "Microsoft-DotNETCore-SampleProfiler",
104            "--output",
105            &trace_file,
106        ]);
107
108        let output = cmd
109            .output()
110            .map_err(|e| format!("Failed to run dotnet-trace: {}", e))?;
111
112        if !output.status.success() {
113            return Err(format!(
114                "dotnet-trace failed: {}",
115                String::from_utf8_lossy(&output.stderr)
116            ));
117        }
118
119        self.parse_trace_output(&trace_file)
120    }
121
122    /// Check if dotnet-trace is installed
123    fn is_dotnet_trace_installed(&self) -> bool {
124        Command::new("dotnet-trace")
125            .arg("--version")
126            .stdout(Stdio::null())
127            .stderr(Stdio::null())
128            .status()
129            .is_ok()
130    }
131
132    /// Install dotnet-trace
133    fn install_dotnet_trace(&self) -> Result<(), String> {
134        println!("Installing dotnet-trace...");
135
136        let output = Command::new("dotnet")
137            .args(["tool", "install", "-g", "dotnet-trace"])
138            .output()
139            .map_err(|e| format!("Failed to install dotnet-trace: {}", e))?;
140
141        if !output.status.success() {
142            return Err(format!(
143                "Failed to install dotnet-trace: {}",
144                String::from_utf8_lossy(&output.stderr)
145            ));
146        }
147
148        println!("✓ dotnet-trace installed successfully");
149        Ok(())
150    }
151
152    /// Find a .csproj file in the current directory
153    fn find_csproj(&self) -> Option<String> {
154        if let Ok(entries) = fs::read_dir(".") {
155            for entry in entries.flatten() {
156                if let Some(name) = entry.file_name().to_str() {
157                    if name.ends_with(".csproj") {
158                        return Some(name.to_string());
159                    }
160                }
161            }
162        }
163        None
164    }
165
166    /// Parse trace output and create ProfileResult
167    fn parse_trace_output(&self, trace_file: &str) -> Result<ProfileResult, String> {
168        if !Path::new(trace_file).exists() {
169            return Err("Trace file not generated. Profiling may have failed.".to_string());
170        }
171
172        let file_size = fs::metadata(trace_file).map(|m| m.len()).unwrap_or(0);
173
174        let details = vec![
175            "✓ Profiling completed successfully".to_string(),
176            format!("📊 Trace file generated: {}", trace_file),
177            format!("   Size: {} bytes", file_size),
178            "".to_string(),
179            "To analyze the trace:".to_string(),
180            "".to_string(),
181            "1. Convert to speedscope format:".to_string(),
182            format!("   dotnet-trace convert {} --format speedscope", trace_file),
183            "".to_string(),
184            "2. View with PerfView (Windows):".to_string(),
185            "   Download from: https://github.com/microsoft/perfview".to_string(),
186            format!("   Open {} in PerfView", trace_file),
187            "".to_string(),
188            "3. View with speedscope (web-based):".to_string(),
189            "   Upload to: https://www.speedscope.app/".to_string(),
190            "".to_string(),
191            "Trace contains:".to_string(),
192            "  - CPU sampling data".to_string(),
193            "  - Method call stacks".to_string(),
194            "  - Hot paths and bottlenecks".to_string(),
195        ];
196
197        Ok(ProfileResult {
198            language: "C#".to_string(),
199            details,
200        })
201    }
202
203    /// Profile by attaching to PID
204    pub fn profile_pid(&self, pid: u32) -> Result<ProfileResult, String> {
205        println!("🔍 Attaching to C# process PID: {}", pid);
206
207        if !self.is_dotnet_trace_installed() {
208            println!("⚠️  dotnet-trace not found. Attempting to install...");
209            self.install_dotnet_trace()?;
210        }
211
212        self.profile_running_process(pid)
213    }
214}
215
216#[cfg(test)]
217mod tests {
218    use super::*;
219
220    #[test]
221    fn test_profiler_new() {
222        let profiler = CSharpProfiler::new();
223        assert_eq!(std::mem::size_of_val(&profiler), 0);
224    }
225
226    #[test]
227    fn test_profiler_default() {
228        let profiler = CSharpProfiler;
229        assert_eq!(std::mem::size_of_val(&profiler), 0);
230    }
231}