testlint_sdk/profiler/
csharp.rs1#![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 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 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 self.profile_csharp_source(csharp_file_or_dll)
38 } else {
39 self.profile_csharp_binary(csharp_file_or_dll)
41 }
42 }
43
44 fn profile_csharp_source(&self, _cs_file: &str) -> Result<ProfileResult, String> {
46 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 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 self.profile_running_process(pid)
69 }
70
71 fn profile_csharp_binary(&self, binary_path: &str) -> Result<ProfileResult, String> {
73 println!("🚀 Profiling C# binary: {}", binary_path);
74
75 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 self.profile_running_process(pid)
88 }
89
90 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 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 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 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 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 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}