From 75ae08fb35a24bf4f0658097af5bb36ff6ab6469 Mon Sep 17 00:00:00 2001 From: John Tur Date: Fri, 14 Aug 2026 14:24:11 -0400 Subject: [PATCH 1/3] Delay-load ETW profiling DLLs --- crates/etw_tracing/etw_tracing.rs | 362 +++++++++++++----------------- crates/zed/build.rs | 2 + crates/zed/src/main.rs | 21 +- docs/src/development.md | 4 +- 4 files changed, 167 insertions(+), 222 deletions(-) diff --git a/crates/etw_tracing/etw_tracing.rs b/crates/etw_tracing/etw_tracing.rs index c58a44745cb086..9728b1912d3afe 100644 --- a/crates/etw_tracing/etw_tracing.rs +++ b/crates/etw_tracing/etw_tracing.rs @@ -5,7 +5,7 @@ use gpui::{App, AppContext as _, DismissEvent, Global, actions}; use std::fmt::Write as _; use std::io::{BufRead, BufReader, Write}; use std::path::{Path, PathBuf}; -use std::time::Duration; + use util::{ResultExt as _, defer}; use windows::Win32::Foundation::{VARIANT_BOOL, VARIANT_FALSE}; use windows::Win32::System::Com::{CLSCTX_INPROC_SERVER, COINIT_MULTITHREADED, CoInitializeEx}; @@ -30,10 +30,17 @@ actions!( struct EtwNotification; +enum EtwSessionState { + Recording, + ChoosingOutputPath, + Stopping, +} + struct EtwSessionHandle { writer: net::OwnedWriteHalf, _listener: net::UnixListener, socket_path: PathBuf, + state: EtwSessionState, } impl Drop for EtwSessionHandle { @@ -46,10 +53,6 @@ struct GlobalEtwSession(Option); impl Global for GlobalEtwSession {} -fn has_active_etw_session(cx: &App) -> bool { - cx.global::().0.is_some() -} - fn show_etw_notification(cx: &mut App, message: impl Into) { let message = message.into(); show_app_notification(NotificationId::unique::(), cx, move |cx| { @@ -57,59 +60,30 @@ fn show_etw_notification(cx: &mut App, message: impl Into) { }); } -fn show_etw_notification_with_action( - cx: &mut App, - message: impl Into, - button_label: impl Into, - on_click: impl Fn(&mut gpui::Window, &mut gpui::Context) - + Send - + Sync - + 'static, -) { - let message = message.into(); - let button_label = button_label.into(); - let on_click = std::sync::Arc::new(on_click); - show_app_notification(NotificationId::unique::(), cx, move |cx| { - let message = message.clone(); - let button_label = button_label.clone(); - cx.new(|cx| { - MessageNotification::new(message, cx) - .primary_message(button_label) - .primary_on_click_arc(on_click.clone()) - }) - }); -} - -fn show_etw_status_notification(cx: &mut App, status: Result, output_path: PathBuf) { +fn show_etw_status_notification(cx: &mut App, status: Result) { match status { - Ok(StatusMessage::Stopped) => { - let display_path = output_path.display().to_string(); - show_etw_notification_with_action( - cx, - format!("ETW trace saved to {display_path}"), - "Show in File Manager", - move |_window, cx| { - cx.reveal_path(&output_path); - cx.emit(DismissEvent); - }, - ); - } - Ok(StatusMessage::TimedOut) => { - let display_path = output_path.display().to_string(); - show_etw_notification_with_action( - cx, - format!("ETW recording timed out. Trace saved to {display_path}"), - "Show in File Manager", - move |_window, cx| { - cx.reveal_path(&output_path); - cx.emit(DismissEvent); - }, - ); + Ok(StatusMessage::Stopped { output_path }) => { + let message = format!("ETW trace saved to {}", output_path.display()); + show_app_notification(NotificationId::unique::(), cx, move |cx| { + let message = message.clone(); + let output_path = output_path.clone(); + cx.new(|cx| { + MessageNotification::new(message, cx) + .primary_message("Show in File Manager") + .primary_on_click(move |_window, cx| { + cx.reveal_path(&output_path); + cx.emit(DismissEvent); + }) + }) + }); } Ok(StatusMessage::Cancelled) => { show_etw_notification(cx, "ETW recording cancelled"); } - Ok(_) => { + Ok(StatusMessage::Error { message }) => { + show_etw_notification(cx, format!("ETW recording failed: {message}")); + } + Ok(StatusMessage::Started) => { show_etw_notification(cx, "ETW recording ended unexpectedly"); } Err(error) => { @@ -130,67 +104,113 @@ pub fn init(cx: &mut App) { }); cx.on_action(|_: &SaveEtwTrace, cx: &mut App| { - let session = cx.global_mut::().0.as_mut(); - let Some(session) = session else { - show_etw_notification(cx, "No active ETW recording to stop"); - return; - }; - match send_json(&mut session.writer, &Command::Save) { - Ok(()) => { - show_etw_notification(cx, "Stopping ETW recording..."); - } - Err(error) => { - show_etw_notification(cx, format!("Failed to stop ETW recording: {error:#}")); - } - } + prompt_for_etw_output_path(cx); }); cx.on_action(|_: &CancelEtwTrace, cx: &mut App| { - let session = cx.global_mut::().0.as_mut(); - let Some(session) = session else { - show_etw_notification(cx, "No active ETW recording to cancel"); + cancel_etw_recording(cx); + }); +} + +fn prompt_for_etw_output_path(cx: &mut App) { + let Some(session) = cx.global_mut::().0.as_mut() else { + show_etw_notification(cx, "No active ETW recording to stop"); + return; + }; + match &session.state { + EtwSessionState::Recording => { + session.state = EtwSessionState::ChoosingOutputPath; + } + EtwSessionState::ChoosingOutputPath => { + show_etw_notification(cx, "ETW recording is already waiting for a save location"); return; - }; - match send_json(&mut session.writer, &Command::Cancel) { - Ok(()) => { - show_etw_notification(cx, "Cancelling ETW recording..."); - } + } + EtwSessionState::Stopping => { + show_etw_notification(cx, "ETW recording is already stopping"); + return; + } + } + + let save_dialog = cx.prompt_for_new_path(&PathBuf::default(), Some("zed-trace.etl")); + cx.spawn(async move |cx| { + let picked = save_dialog.await.unwrap_or(Ok(None)); + cx.update(|cx| match picked { + Ok(Some(output_path)) => save_etw_recording(output_path, cx), + Ok(None) => resume_etw_recording(cx), Err(error) => { - show_etw_notification(cx, format!("Failed to cancel ETW recording: {error:#}")); + resume_etw_recording(cx); + show_etw_notification(cx, format!("Failed to pick save location: {error:#}")); } + }); + }) + .detach(); +} + +fn save_etw_recording(output_path: PathBuf, cx: &mut App) { + let Some(session) = cx.global_mut::().0.as_mut() else { + return; + }; + if !matches!(&session.state, EtwSessionState::ChoosingOutputPath) { + return; + } + + let command = Command::Save { + output_path: output_path.clone(), + }; + match send_json(&mut session.writer, &command) { + Ok(()) => { + session.state = EtwSessionState::Stopping; + show_etw_notification(cx, "Stopping ETW recording..."); } - }); + Err(error) => { + session.state = EtwSessionState::Recording; + show_etw_notification(cx, format!("Failed to stop ETW recording: {error:#}")); + } + } +} + +fn resume_etw_recording(cx: &mut App) { + let Some(session) = cx.global_mut::().0.as_mut() else { + return; + }; + if matches!(&session.state, EtwSessionState::ChoosingOutputPath) { + session.state = EtwSessionState::Recording; + } +} + +fn cancel_etw_recording(cx: &mut App) { + let Some(session) = cx.global_mut::().0.as_mut() else { + show_etw_notification(cx, "No active ETW recording to cancel"); + return; + }; + if matches!(&session.state, EtwSessionState::Stopping) { + show_etw_notification(cx, "ETW recording is already stopping"); + return; + } + + match send_json(&mut session.writer, &Command::Cancel) { + Ok(()) => { + session.state = EtwSessionState::Stopping; + show_etw_notification(cx, "Cancelling ETW recording..."); + } + Err(error) => { + session.state = EtwSessionState::Recording; + show_etw_notification(cx, format!("Failed to cancel ETW recording: {error:#}")); + } + } } fn start_etw_recording(cx: &mut App, heap_pid: Option) { - if has_active_etw_session(cx) { + if cx.global::().0.is_some() { show_etw_notification(cx, "ETW recording is already in progress"); return; } - let save_dialog = cx.prompt_for_new_path(&PathBuf::default(), Some("zed-trace.etl")); cx.spawn(async move |cx| { - let output_path = match save_dialog.await { - Ok(Ok(Some(path))) => path, - Ok(Ok(None)) => return, - Ok(Err(error)) => { - cx.update(|cx| { - show_etw_notification(cx, format!("Failed to pick save location: {error:#}")); - }); - return; - } - Err(_) => return, - }; - let result = cx - .background_spawn(async move { launch_etw_recording(heap_pid, &output_path) }) + .background_spawn(async move { launch_etw_recording(heap_pid) }) .await; - let EtwSession { - output_path, - stream, - listener, - socket_path, - } = match result { + let EtwSession { mut reader, handle } = match result { Ok(session) => session, Err(error) => { cx.update(|cx| { @@ -200,36 +220,24 @@ fn start_etw_recording(cx: &mut App, heap_pid: Option) { } }; - let (read_half, write_half) = stream.into_inner().into_split(); - - cx.spawn(async |cx| { - let status = cx - .background_spawn(async move { - recv_json(&mut BufReader::new(read_half)) - .context("Receive status from subprocess") - }) - .await; - cx.update(|cx| { - cx.global_mut::().0 = None; - show_etw_status_notification(cx, status, output_path); - }); - }) - .detach(); - cx.update(|cx| { - cx.global_mut::().0 = Some(EtwSessionHandle { - writer: write_half, - _listener: listener, - socket_path, - }); + cx.global_mut::().0 = Some(handle); show_etw_notification(cx, "ETW recording started"); }); + + let status = cx + .background_spawn(async move { + recv_json(&mut reader).context("Receive status from subprocess") + }) + .await; + cx.update(|cx| { + cx.global_mut::().0 = None; + show_etw_status_notification(cx, status); + }); }) .detach(); } -const RECORDING_TIMEOUT: Duration = Duration::from_secs(60); - const INSTANCE_NAME: &str = "Zed"; const BUILTIN_PROFILES: &[&str] = &[ @@ -438,21 +446,16 @@ fn build_profile_collection(heap_pid: Option) -> Result Ok(collection) } -pub fn record_etw_trace( - heap_pid: Option, - output_path: &Path, - socket_path: &str, -) -> Result<()> { +pub fn record_etw_trace(heap_pid: Option, socket_path: &Path) -> Result<()> { unsafe { CoInitializeEx(None, COINIT_MULTITHREADED) .ok() .context("COM initialization failed")?; } - let socket_path = Path::new(socket_path); let mut stream = net::UnixStream::connect(socket_path).context("Connect to parent socket")?; - match record_etw_trace_inner(heap_pid, output_path, &mut stream) { + match record_etw_trace_inner(heap_pid, &mut stream) { Ok(()) => Ok(()), Err(e) => { send_json( @@ -467,11 +470,7 @@ pub fn record_etw_trace( } } -fn record_etw_trace_inner( - heap_pid: Option, - output_path: &Path, - stream: &mut net::UnixStream, -) -> Result<()> { +fn record_etw_trace_inner(heap_pid: Option, stream: &mut net::UnixStream) -> Result<()> { let collection = build_profile_collection(heap_pid)?; let control_manager: IControlManager = create_wpr(&CControlManager)?; @@ -497,7 +496,8 @@ fn record_etw_trace_inner( send_json(stream, &StatusMessage::Started)?; - let (command, timed_out) = receive_command(stream)?; + let command: Command = + recv_json(&mut BufReader::new(&mut *stream)).context("Receive command from Zed")?; match command { Command::Cancel => { @@ -511,7 +511,7 @@ fn record_etw_trace_inner( send_json(stream, &StatusMessage::Cancelled).log_err(); } - Command::Save => { + Command::Save { output_path } => { unsafe { control_manager .Save( @@ -524,76 +524,37 @@ fn record_etw_trace_inner( } cancel_guard.abort(); - if timed_out { - send_json(stream, &StatusMessage::TimedOut).log_err(); - } else { - send_json(stream, &StatusMessage::Stopped).log_err(); - } + send_json(stream, &StatusMessage::Stopped { output_path }).log_err(); } } Ok(()) } -fn receive_command(stream: &mut net::UnixStream) -> Result<(Command, bool)> { - use std::os::windows::io::{AsRawSocket, AsSocket}; - use windows::Win32::Networking::WinSock::{SO_RCVTIMEO, SOL_SOCKET, setsockopt}; - - // Set a receive timeout so read_line returns an error after `timeout`. - let millis = RECORDING_TIMEOUT.as_millis() as u32; - let socket = stream.as_socket(); - let ret = unsafe { - setsockopt( - windows::Win32::Networking::WinSock::SOCKET(socket.as_raw_socket() as _), - SOL_SOCKET, - SO_RCVTIMEO, - Some(&millis.to_ne_bytes()), - ) - }; - if ret != 0 { - bail!("Failed to set socket receive timeout: setsockopt returned {ret}"); - } - - let mut reader = BufReader::new(&mut *stream); - match recv_json::(&mut reader) { - Ok(command) => Ok((command, false)), - Err(error) => { - log::warn!("Failed to receive ETW command, treating as timed-out Save: {error:#}"); - Ok((Command::Save, true)) - } - } -} - -pub struct EtwSession { - output_path: PathBuf, - stream: BufReader, - listener: net::UnixListener, - socket_path: PathBuf, +struct EtwSession { + reader: BufReader, + handle: EtwSessionHandle, } -pub fn launch_etw_recording(heap_pid: Option, output_path: &Path) -> Result { +fn launch_etw_recording(heap_pid: Option) -> Result { let sock_path = std::env::temp_dir().join(format!("zed-etw-{}.sock", std::process::id())); _ = std::fs::remove_file(&sock_path); let listener = net::UnixListener::bind(&sock_path).context("Bind Unix socket for ETW IPC")?; let exe_path = std::env::current_exe().context("Failed to get current exe path")?; - let pid_arg = heap_pid.map_or(-1i64, |pid| pid as i64); + let heap_arg = heap_pid.map_or(String::new(), |pid| format!(" --etw-zed-pid {pid}")); let args = format!( - "--record-etw-trace --etw-zed-pid {} --etw-output \"{}\" --etw-socket \"{}\"", - pid_arg, - output_path.display(), + "--record-etw-trace{heap_arg} --etw-socket \"{}\"", sock_path.display(), ); use windows::Win32::UI::Shell::ShellExecuteW; - use windows_core::PCWSTR; + use windows_core::{HSTRING, PCWSTR}; - let operation: Vec = "runas\0".encode_utf16().collect(); - let file: Vec = format!("{}\0", exe_path.to_string_lossy()) - .encode_utf16() - .collect(); - let parameters: Vec = format!("{args}\0").encode_utf16().collect(); + let operation = HSTRING::from("runas"); + let file = HSTRING::from(exe_path.to_string_lossy().as_ref()); + let parameters = HSTRING::from(args); let result = unsafe { ShellExecuteW( @@ -612,18 +573,10 @@ pub fn launch_etw_recording(heap_pid: Option, output_path: &Path) -> Result } let (stream, _) = listener.accept().context("Accept subprocess connection")?; + let (read_half, write_half) = stream.into_split(); + let mut reader = BufReader::new(read_half); - let mut session = EtwSession { - output_path: output_path.to_path_buf(), - stream: BufReader::new(stream), - listener, - socket_path: sock_path, - }; - - let status: StatusMessage = - recv_json(&mut session.stream).context("Wait for Started status")?; - - match status { + match recv_json(&mut reader).context("Wait for Started status")? { StatusMessage::Started => {} StatusMessage::Error { message } => { bail!("Subprocess reported error during start: {message}"); @@ -633,23 +586,30 @@ pub fn launch_etw_recording(heap_pid: Option, output_path: &Path) -> Result } } - Ok(session) + Ok(EtwSession { + reader, + handle: EtwSessionHandle { + writer: write_half, + _listener: listener, + socket_path: sock_path, + state: EtwSessionState::Recording, + }, + }) } #[derive(Debug, serde::Serialize, serde::Deserialize)] #[serde(tag = "type")] -pub enum StatusMessage { +enum StatusMessage { Started, - Stopped, - TimedOut, + Stopped { output_path: PathBuf }, Cancelled, Error { message: String }, } #[derive(Debug, serde::Serialize, serde::Deserialize)] #[serde(tag = "type")] -pub enum Command { - Save, +enum Command { + Save { output_path: PathBuf }, Cancel, } diff --git a/crates/zed/build.rs b/crates/zed/build.rs index b27eba36a8a1dc..f49396a5a2c092 100644 --- a/crates/zed/build.rs +++ b/crates/zed/build.rs @@ -86,6 +86,8 @@ fn main() { if cfg!(target_env = "msvc") { // todo(windows): This is to avoid stack overflow. Remove it when solved. println!("cargo:rustc-link-arg=/stack:{}", 8 * 1024 * 1024); + println!("cargo:rustc-link-arg=/DELAYLOAD:windowsperformancerecordercontrol"); + println!("cargo:rustc-link-lib=delayimp"); } if cfg!(target_arch = "x86_64") || cfg!(target_arch = "aarch64") { diff --git a/crates/zed/src/main.rs b/crates/zed/src/main.rs index 971599e2acf052..73f3639b6b331c 100644 --- a/crates/zed/src/main.rs +++ b/crates/zed/src/main.rs @@ -226,22 +226,12 @@ fn main() { #[cfg(target_os = "windows")] if args.record_etw_trace { - let zed_pid = args - .etw_zed_pid - .and_then(|pid| if pid >= 0 { Some(pid as u32) } else { None }); - let Some(output_path) = args.etw_output else { - eprintln!("--etw-output is required for --record-etw-trace"); - process::exit(1); - }; - let Some(etw_socket) = args.etw_socket else { eprintln!("--etw-socket is required for --record-etw-trace"); process::exit(1); }; - if let Err(error) = - etw_tracing::record_etw_trace(zed_pid, &output_path, etw_socket.as_str()) - { + if let Err(error) = etw_tracing::record_etw_trace(args.etw_zed_pid, &etw_socket) { eprintln!("ETW trace recording failed: {error:#}"); process::exit(1); } @@ -1778,18 +1768,13 @@ struct Args { /// The PID of the Zed process to trace for heap analysis. #[cfg(target_os = "windows")] - #[arg(long, hide = true, allow_hyphen_values = true)] - etw_zed_pid: Option, - - /// Output path for the ETW trace file. - #[cfg(target_os = "windows")] #[arg(long, hide = true)] - etw_output: Option, + etw_zed_pid: Option, /// Unix socket path for IPC with the parent Zed process. #[cfg(target_os = "windows")] #[arg(long, hide = true)] - etw_socket: Option, + etw_socket: Option, } #[derive(Clone, Debug)] diff --git a/docs/src/development.md b/docs/src/development.md index b4c9ea387da020..73e77f2a9f8136 100644 --- a/docs/src/development.md +++ b/docs/src/development.md @@ -99,7 +99,7 @@ Open the command palette and run one of the following: - `zed: record etw trace`: records CPU, GPU, memory, and I/O activity - `zed: record etw trace with heap tracing`: includes heap allocation data for the Zed process -Zed will prompt you to choose a save location for the `.etl` file, then request administrator permission. Once granted, recording will begin. +Zed will request administrator permission. Once granted, recording will begin. ### Saving or canceling @@ -108,8 +108,6 @@ While a trace is recording, open the command palette and run one of the followin - `zed: save etw trace`: stops recording and saves the trace to disk - `zed: cancel etw trace`: stops recording without saving -Recordings automatically save after 60 seconds if not stopped manually. - ## Contributor links - [CONTRIBUTING.md](https://github.com/zed-industries/zed/blob/main/CONTRIBUTING.md) From 3f649f526cb63656bbf69265378f9f3544e83e74 Mon Sep 17 00:00:00 2001 From: John Tur Date: Fri, 14 Aug 2026 14:28:36 -0400 Subject: [PATCH 2/3] Fix machete --- crates/etw_tracing/Cargo.toml | 1 - crates/etw_tracing/etw_tracing.rs | 1 - 2 files changed, 2 deletions(-) diff --git a/crates/etw_tracing/Cargo.toml b/crates/etw_tracing/Cargo.toml index c46e3b820a950f..ce57b9e4679a73 100644 --- a/crates/etw_tracing/Cargo.toml +++ b/crates/etw_tracing/Cargo.toml @@ -11,7 +11,6 @@ path = "etw_tracing.rs" [dependencies] anyhow.workspace = true gpui.workspace = true -log.workspace = true net.workspace = true serde.workspace = true serde_json.workspace = true diff --git a/crates/etw_tracing/etw_tracing.rs b/crates/etw_tracing/etw_tracing.rs index 9728b1912d3afe..f77cf2a778881b 100644 --- a/crates/etw_tracing/etw_tracing.rs +++ b/crates/etw_tracing/etw_tracing.rs @@ -5,7 +5,6 @@ use gpui::{App, AppContext as _, DismissEvent, Global, actions}; use std::fmt::Write as _; use std::io::{BufRead, BufReader, Write}; use std::path::{Path, PathBuf}; - use util::{ResultExt as _, defer}; use windows::Win32::Foundation::{VARIANT_BOOL, VARIANT_FALSE}; use windows::Win32::System::Com::{CLSCTX_INPROC_SERVER, COINIT_MULTITHREADED, CoInitializeEx}; From ddfd5ceb8f7cfe179ada176b44abc0038fa2ff1c Mon Sep 17 00:00:00 2001 From: John Tur Date: Fri, 14 Aug 2026 14:34:43 -0400 Subject: [PATCH 3/3] Oops --- Cargo.lock | 1 - 1 file changed, 1 deletion(-) diff --git a/Cargo.lock b/Cargo.lock index 06575e383339a8..7a0f9a11871512 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -6167,7 +6167,6 @@ version = "0.1.0" dependencies = [ "anyhow", "gpui", - "log", "net", "serde", "serde_json",