cgp/profilers/
wgpu_profiler.rs1use anyhow::Result;
5use std::path::Path;
6
7pub fn detect_wgpu_backend() -> &'static str {
9 #[cfg(target_os = "linux")]
10 {
11 "Vulkan"
12 }
13 #[cfg(target_os = "macos")]
14 {
15 "Metal"
16 }
17 #[cfg(target_os = "windows")]
18 {
19 "DX12"
20 }
21 #[cfg(not(any(target_os = "linux", target_os = "macos", target_os = "windows")))]
22 {
23 "Unknown"
24 }
25}
26
27pub fn validate_shader(shader_path: &str) -> Result<ShaderInfo> {
29 let path = Path::new(shader_path);
30 if !path.exists() {
31 anyhow::bail!("Shader file not found: {shader_path}");
32 }
33
34 let content = std::fs::read_to_string(path)?;
35 let lines = content.lines().count();
36
37 let workgroup_size = content.lines().find_map(|line| {
39 if line.contains("@workgroup_size") {
40 let start = line.find('(')? + 1;
41 let end = line.find(')')?;
42 Some(line[start..end].to_string())
43 } else {
44 None
45 }
46 });
47
48 let has_compute = content.contains("@compute");
50
51 Ok(ShaderInfo {
52 path: shader_path.to_string(),
53 lines,
54 workgroup_size,
55 has_compute,
56 })
57}
58
59#[derive(Debug)]
61pub struct ShaderInfo {
62 pub path: String,
63 pub lines: usize,
64 pub workgroup_size: Option<String>,
65 pub has_compute: bool,
66}
67
68pub fn parse_dispatch(dispatch: &str) -> Result<[u32; 3]> {
70 let parts: Vec<&str> = dispatch.split(',').collect();
71 if parts.len() != 3 {
72 anyhow::bail!("Dispatch must be X,Y,Z format (got: {dispatch})");
73 }
74 Ok([
75 parts[0].trim().parse()?,
76 parts[1].trim().parse()?,
77 parts[2].trim().parse()?,
78 ])
79}
80
81pub fn profile_wgpu(shader: &str, dispatch: Option<&str>, target: Option<&str>) -> Result<()> {
83 let target_str = target.unwrap_or("native");
84 let backend = detect_wgpu_backend();
85 println!("\n=== CGP wgpu Profile: {shader} (target={target_str}) ===\n");
86 println!(" Shader: {shader}");
87 println!(" Backend: wgpu ({backend})");
88
89 if let Some(d) = dispatch {
90 let dims = parse_dispatch(d)?;
91 println!(" Dispatch: {}x{}x{}", dims[0], dims[1], dims[2]);
92 let total_invocations = dims[0] as u64 * dims[1] as u64 * dims[2] as u64;
93 println!(" Total workgroups: {total_invocations}");
94 }
95
96 println!(" Method: TIMESTAMP_QUERY for GPU-side timing (~1ns resolution)");
97
98 if Path::new(shader).exists() {
100 match validate_shader(shader) {
101 Ok(info) => {
102 println!(" Shader lines: {}", info.lines);
103 if let Some(ws) = &info.workgroup_size {
104 println!(" Workgroup size: {ws}");
105 }
106 if !info.has_compute {
107 println!(" \x1b[33m[WARN]\x1b[0m No @compute entry point found in shader");
108 }
109 }
110 Err(e) => println!(" \x1b[33m[WARN]\x1b[0m Shader validation: {e}"),
111 }
112 }
113
114 if target_str == "web" {
115 let has_chrome = which::which("google-chrome").is_ok()
116 || which::which("chromium").is_ok()
117 || which::which("chromium-browser").is_ok();
118 if !has_chrome {
119 println!(" No browser found -- falling back to wgpu native (Vulkan/Metal)");
120 } else {
121 println!(" Browser: headless Chrome (Chrome DevTools Protocol)");
122 }
123 }
124
125 println!();
126 Ok(())
127}
128
129#[cfg(test)]
130mod tests {
131 use super::*;
132
133 #[test]
135 fn test_wgpu_profile_runs() {
136 let result = profile_wgpu("test.wgsl", Some("256,256,1"), Some("native"));
137 assert!(result.is_ok());
138 }
139
140 #[test]
141 fn test_detect_backend() {
142 let backend = detect_wgpu_backend();
143 assert!(!backend.is_empty());
144 #[cfg(target_os = "linux")]
145 assert_eq!(backend, "Vulkan");
146 }
147
148 #[test]
149 fn test_parse_dispatch() {
150 let dims = parse_dispatch("256,256,1").unwrap();
151 assert_eq!(dims, [256, 256, 1]);
152 }
153
154 #[test]
155 fn test_parse_dispatch_bad() {
156 assert!(parse_dispatch("256,256").is_err());
157 }
158
159 #[test]
160 fn test_validate_shader_missing() {
161 assert!(validate_shader("/tmp/nonexistent_shader.wgsl").is_err());
162 }
163
164 #[test]
166 fn test_web_target_graceful() {
167 let result = profile_wgpu("test.wgsl", None, Some("web"));
168 assert!(result.is_ok());
169 }
170}