Rust windows profiling

I am working on writing some CPU-intensive code and trying to better understand how to profile a language like rust on Windows 11. So far, I have used tools such as Superluminal and WPR/WPA (windows performance).

Here are a few questions I have had so far:

  • How do I make sense of some of these? I am getting a bit lost diving through the call graphs, and my goal is to try and understand what is causing performance degradation.
  • Is there other tools available? My research into this showed a lot of the tools are for linux-based systems.
  • Why do so many paths lead to dead ends where no symbol was found? I have debug = true flag enabled for my build profile. Maybe this is common, I am new to systems-level language profiling.

Are there are good resources out there for this sort of thing to help me learn?

Attached image of the sort of UI I have been seeing so far. I generally just dive through these threads' call graphs until I see routines that take up abnormally large portions of the execution time, but can't tell if there is a better way to find/visualize these.

I was able to get useful profiles with Windows' CPU profiling. I opened them in Visual Studio and VS was able to find the source rs files and show perf info just fine.

I also wrote some code to automate collecting the profiles which may or may not be particularly useful to you. I haven't tested it again now but it worked for what I needed a couple months ago ¯\_(ツ)_/¯

Automate collecting perf info
#[derive(Debug)] 
 enum DiagnosticsError { 
     Io(std::io::Error), 
     Exit(ExitStatusError), 
 } 
  
 impl From<std::io::Error> for DiagnosticsError { 
     fn from(from: std::io::Error) -> Self { 
         Self::Io(from) 
     } 
 } 
  
 impl From<ExitStatusError> for DiagnosticsError { 
     fn from(from: ExitStatusError) -> Self { 
         Self::Exit(from) 
     } 
 } 
  
 struct Diagnostics { 
     session: u8, 
     collector_path: Option<PathBuf>, 
 } 
  
 impl Diagnostics { 
     fn start( 
         collector_path: impl AsRef<Path>, 
         session: u8, 
         config: impl AsRef<Path>, 
     ) -> Result<Self, DiagnosticsError> { 
         let collector_path = collector_path.as_ref().to_owned(); 
  
         { 
             let session = session.to_string(); 
             let config = format!( 
                 "/loadConfig:{}", 
                 collector_path.join(&config).as_os_str().to_str().unwrap() 
             ); 
             let attach = format!("/attach:{}", std::process::id()); 
             Command::new(collector_path.join("VSDiagnostics.exe")) 
                 .args(["start", &session, &config, &attach]) 
                 .spawn()? 
                 .wait()? 
                 .exit_ok()? 
         } 
  
         Ok(Self { 
             session, 
             collector_path: Some(collector_path), 
         }) 
     } 
  
     fn stop(mut self, dest: impl AsRef<Path>) -> Result<(), DiagnosticsError> { 
         self.stop_inner(dest) 
     } 
  
     fn stop_inner(&mut self, dest: impl AsRef<Path>) -> Result<(), DiagnosticsError> { 
         { 
             let session = self.session.to_string(); 
             let output = format!("/output:{}", dest.as_ref().as_os_str().to_str().unwrap()); 
             let attach = format!("/attach:{}", std::process::id()); 
             Command::new( 
                 self.collector_path 
                     .take() 
                     .unwrap() 
                     .join("VSDiagnostics.exe"), 
             ) 
             .args(["stop", &session, &output, &attach]) 
             .spawn()? 
             .wait()? 
             .exit_ok()?; 
  
             Ok(()) 
         } 
     } 
 } 
  
 impl Drop for Diagnostics { 
     fn drop(&mut self) { 
         if self.collector_path.is_none() { 
             return; 
         } 
  
         let dir = std::env::current_dir() 
             .unwrap() 
             .join(format!("{}.diagsession", self.session)); 
  
         eprintln!( 
             "WARNING: Stop was not called, stopping session with session name {}", 
             dir.as_os_str().to_string_lossy() 
         ); 
  
         self.stop_inner(dir).unwrap(); 
     } 
 } 
  
 pub fn run_test() { 
     let diagnostics = Diagnostics::start(r"C:\Program Files\Microsoft Visual Studio\2022\Community\Team Tools\DiagnosticsHub\Collector", 1, r"AgentConfigs\CpuUsageHigh.json").unwrap(); 
  
     println!("Starting test"); 

     // Run profiled code here

     let mut dir = std::env::current_dir().unwrap().join(name); 
     dir.set_extension("diagsession"); 
     println!("{:?}", dir); 
  
     println!("{:#?}", rx.recv().unwrap()); 
  
     diagnostics.stop(dir).unwrap(); 
 }
3 Likes

Thanks for the suggestion to use Visual Studio! I have been using primarily vscode so far while writing (mostly because I enjoyed the language support offered), and didn't think to go into Visual Studio. I was able to get more readable results with better visualizations for where my program spent most of its time through its Performance Profiler.

And I appreciate you passing the script along. I will try it out if I hit another wall here.

1 Like

There's another situation where Visual Studio shines, albeit a pretty niche one: It works pretty good with Visual Studio's remote debugging facilities. This comes in handy if you, for instance, need to debug a Rust service during early boot.

Visual Studio is a handy tool for Rust-on-Windows developers to have in their toolbox, imho.