From 989887ca0b9f751cc68f4239aa52d817e6d4119d Mon Sep 17 00:00:00 2001 From: John Tur Date: Fri, 20 Feb 2026 08:36:04 -0500 Subject: [PATCH] Add ETW profile recorder action (#49712) https://github.com/user-attachments/assets/8b0be641-625e-410f-b7c1-abe549504c11 Before you mark this PR as ready for review, make sure that you have: - [X] Added a solid test coverage and/or screenshots from doing manual testing - [X] Done a self-review taking into account security and performance aspects - [X] Aligned any UI changes with the [UI checklist](https://github.com/zed-industries/zed/blob/main/CONTRIBUTING.md#uiux-checklist) Release Notes: - Added a `zed: record etw profile` action that can be used to collect performance profiles on Windows. --- Cargo.lock | 46 ++- Cargo.toml | 2 + crates/etw_tracing/Cargo.toml | 30 ++ crates/etw_tracing/LICENSE-GPL | 1 + crates/etw_tracing/etw_tracing.rs | 654 ++++++++++++++++++++++++++++++ crates/net/src/stream.rs | 32 +- crates/zed/Cargo.toml | 1 + crates/zed/src/main.rs | 44 ++ 8 files changed, 804 insertions(+), 6 deletions(-) create mode 100644 crates/etw_tracing/Cargo.toml create mode 120000 crates/etw_tracing/LICENSE-GPL create mode 100644 crates/etw_tracing/etw_tracing.rs diff --git a/Cargo.lock b/Cargo.lock index 1f770efed1d929909cfa3a0d94d82849d6f1a883..90aa2b09ace5b9b99c3adda5d6989efe776d629d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3895,7 +3895,7 @@ dependencies = [ "wasm-bindgen", "wasm-bindgen-futures", "web-sys", - "windows 0.61.3", + "windows 0.62.2", ] [[package]] @@ -5753,6 +5753,23 @@ dependencies = [ "windows-sys 0.48.0", ] +[[package]] +name = "etw_tracing" +version = "0.1.0" +dependencies = [ + "anyhow", + "gpui", + "log", + "net", + "serde", + "serde_json", + "util", + "windows 0.61.3", + "windows-core 0.61.2", + "workspace", + "wprcontrol", +] + [[package]] name = "euclid" version = "0.22.11" @@ -7329,7 +7346,7 @@ dependencies = [ "log", "presser", "thiserror 2.0.17", - "windows 0.61.3", + "windows 0.62.2", ] [[package]] @@ -8430,7 +8447,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4b0f83760fb341a774ed326568e19f5a863af4a952def8c39f9ab92fd95b88e5" dependencies = [ "equivalent", - "hashbrown 0.15.5", + "hashbrown 0.16.1", "serde", "serde_core", ] @@ -19836,6 +19853,17 @@ dependencies = [ "windows-numerics 0.3.1", ] +[[package]] +name = "windows-bindgen" +version = "0.61.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b4e97b01190d32f268a2dfbd3f006f77840633746707fbe40bcee588108a231" +dependencies = [ + "serde", + "serde_json", + "windows-threading 0.1.0", +] + [[package]] name = "windows-capture" version = "1.4.3" @@ -20863,6 +20891,17 @@ dependencies = [ "worktree", ] +[[package]] +name = "wprcontrol" +version = "0.1.0" +source = "git+https://github.com/zed-industries/wprcontrol?rev=cd811f7#cd811f7d744f65291e13131b1d907fda63ed91a1" +dependencies = [ + "windows 0.61.3", + "windows-bindgen", + "windows-core 0.61.2", + "windows-link 0.2.1", +] + [[package]] name = "writeable" version = "0.6.1" @@ -21260,6 +21299,7 @@ dependencies = [ "editor", "encoding_selector", "env_logger 0.11.8", + "etw_tracing", "extension", "extension_host", "extensions_ui", diff --git a/Cargo.toml b/Cargo.toml index 2eae224ffb4377c13d38674437ce011ac5d03d20..6e726622b7f7ae245152f4c3803dbcb43db4d6e7 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -61,6 +61,7 @@ members = [ "crates/edit_prediction_context", "crates/editor", "crates/encoding_selector", + "crates/etw_tracing", "crates/eval", "crates/eval_utils", "crates/explorer_command_injector", @@ -309,6 +310,7 @@ dev_container = { path = "crates/dev_container" } diagnostics = { path = "crates/diagnostics" } editor = { path = "crates/editor" } encoding_selector = { path = "crates/encoding_selector" } +etw_tracing = { path = "crates/etw_tracing" } eval_utils = { path = "crates/eval_utils" } extension = { path = "crates/extension" } extension_host = { path = "crates/extension_host" } diff --git a/crates/etw_tracing/Cargo.toml b/crates/etw_tracing/Cargo.toml new file mode 100644 index 0000000000000000000000000000000000000000..7f287307bc90e4462257fbeae8d5716dc5056ee7 --- /dev/null +++ b/crates/etw_tracing/Cargo.toml @@ -0,0 +1,30 @@ +[package] +name = "etw_tracing" +version = "0.1.0" +edition.workspace = true +publish.workspace = true +license = "GPL-3.0-or-later" + +[lib] +path = "etw_tracing.rs" + +[dependencies] +anyhow.workspace = true +gpui.workspace = true +log.workspace = true +net.workspace = true +serde.workspace = true +serde_json.workspace = true +util.workspace = true +workspace.workspace = true + +[target.'cfg(target_os = "windows")'.dependencies] +wprcontrol = { git = "https://github.com/zed-industries/wprcontrol", rev = "cd811f7" } +windows-core = "0.61" +windows = { workspace = true, features = [ + "Win32_Foundation", + "Win32_System_Com", + "Win32_System_Ole", + "Win32_System_Variant", + "Win32_UI_Shell", +] } diff --git a/crates/etw_tracing/LICENSE-GPL b/crates/etw_tracing/LICENSE-GPL new file mode 120000 index 0000000000000000000000000000000000000000..89e542f750cd3860a0598eff0dc34b56d7336dc4 --- /dev/null +++ b/crates/etw_tracing/LICENSE-GPL @@ -0,0 +1 @@ +../../LICENSE-GPL \ No newline at end of file diff --git a/crates/etw_tracing/etw_tracing.rs b/crates/etw_tracing/etw_tracing.rs new file mode 100644 index 0000000000000000000000000000000000000000..350c32510d75a10fa1d8436da6c5ffb98a1b14b0 --- /dev/null +++ b/crates/etw_tracing/etw_tracing.rs @@ -0,0 +1,654 @@ +#![cfg(target_os = "windows")] + +use anyhow::{Context as _, Result, bail}; +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}; +use windows_core::{BSTR, Interface}; +use workspace::notifications::simple_message_notification::MessageNotification; +use workspace::notifications::{NotificationId, show_app_notification}; +use wprcontrol::*; + +actions!( + zed, + [ + /// Starts recording an ETW (Event Tracing for Windows) trace. + RecordEtwTrace, + /// Stops an in-progress ETW trace and saves it. + StopEtwTrace, + /// Cancels an in-progress ETW trace without saving. + CancelEtwTrace, + ] +); + +struct EtwNotification; + +struct EtwSessionHandle { + writer: net::OwnedWriteHalf, + _listener: net::UnixListener, + socket_path: PathBuf, +} + +impl Drop for EtwSessionHandle { + fn drop(&mut self) { + let _ = std::fs::remove_file(&self.socket_path); + } +} + +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| { + cx.new(|cx| MessageNotification::new(message.clone(), cx)) + }); +} + +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) { + 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::Cancelled) => { + show_etw_notification(cx, "ETW recording cancelled"); + } + Ok(_) => { + show_etw_notification(cx, "ETW recording ended unexpectedly"); + } + Err(error) => { + show_etw_notification(cx, format!("Failed to complete ETW recording: {error:#}")); + } + } +} + +pub fn init(cx: &mut App) { + cx.set_global(GlobalEtwSession(None)); + + cx.on_action(|_: &RecordEtwTrace, cx: &mut App| { + if has_active_etw_session(cx) { + show_etw_notification(cx, "ETW recording is already in progress"); + return; + } + let zed_pid = std::process::id(); + let save_dialog = cx.prompt_for_new_path(&std::env::temp_dir(), 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(zed_pid, &output_path) }) + .await; + + let EtwSession { + output_path, + stream, + listener, + socket_path, + } = match result { + Ok(session) => session, + Err(error) => { + cx.update(|cx| { + show_etw_notification( + cx, + format!("Failed to start ETW recording: {error:#}"), + ); + }); + return; + } + }; + + 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, + }); + show_etw_notification(cx, "ETW recording started"); + }); + }) + .detach(); + }); + + cx.on_action(|_: &StopEtwTrace, 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::Stop) { + Ok(()) => { + show_etw_notification(cx, "Stopping ETW recording..."); + } + Err(error) => { + show_etw_notification(cx, format!("Failed to stop ETW recording: {error:#}")); + } + } + }); + + 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"); + return; + }; + match send_json(&mut session.writer, &Command::Cancel) { + Ok(()) => { + show_etw_notification(cx, "Cancelling ETW recording..."); + } + Err(error) => { + show_etw_notification(cx, format!("Failed to cancel ETW recording: {error:#}")); + } + } + }); +} + +const RECORDING_TIMEOUT: Duration = Duration::from_secs(60); + +const INSTANCE_NAME: &str = "Zed"; + +const BUILTIN_PROFILES: &[&str] = &[ + "CPU.Verbose.File", + "GPU.Verbose.File", + "DiskIO.Light.File", + "FileIO.Light.File", +]; + +fn heap_tracing_profile(zed_pid: u32) -> String { + format!( + r#" + + + + + + + + + + + + + + + + + + + + + + + + +"# + ) +} +fn wpr_error_context(hresult: windows_core::HRESULT, source: &windows_core::IUnknown) -> String { + let mut out = format!("HRESULT: {hresult}"); + + unsafe { + let mut message = BSTR::new(); + let mut description = BSTR::new(); + let mut detail = BSTR::new(); + if WPRCFormatError( + hresult, + Some(source), + &mut message, + Some(&mut description), + Some(&mut detail), + ) + .is_ok() + { + for (label, value) in [ + ("Message", &message), + ("Description", &description), + ("Detail", &detail), + ] { + if !value.is_empty() { + let _ = write!(out, "\n {label}: {value}"); + } + } + } + } + + if let Ok(info) = source.cast::() { + unsafe { + if let Ok(line) = info.GetLineNumber() { + let _ = write!(out, "\n Parse error at line: {line}"); + if let Ok(col) = info.GetColumnNumber() { + let _ = write!(out, ", column: {col}"); + } + } + for (label, getter) in [ + ("Element type", info.GetElementType()), + ("Element ID", info.GetElementId()), + ("Description", info.GetDescription()), + ] { + if let Ok(value) = getter + && !value.is_empty() + { + let _ = write!(out, "\n {label}: {value}"); + } + } + } + } + + fn append_control_chain(out: &mut String, source: &windows_core::IUnknown) { + let Ok(info) = source.cast::() else { + return; + }; + unsafe { + if let Ok(object_type) = info.GetObjectType() { + let name = match object_type { + wprcontrol::ObjectType_Profile => "Profile", + wprcontrol::ObjectType_Collector => "Collector", + wprcontrol::ObjectType_Provider => "Provider", + _ => "Unknown", + }; + let _ = write!(out, "\n Object type: {name}"); + } + if let Ok(hr) = info.GetHResult() { + let _ = write!(out, "\n Inner HRESULT: {hr}"); + } + if let Ok(desc) = info.GetDescription() + && !desc.is_empty() + { + let _ = write!(out, "\n Description: {desc}"); + } + let mut inner = None; + if info.GetInnerErrorInfo(&mut inner).is_ok() + && let Some(inner) = inner + { + let _ = write!(out, "\n Caused by:"); + append_control_chain(out, &inner); + } + } + } + append_control_chain(&mut out, source); + + if let Ok(info) = source.cast::() { + unsafe { + if let Ok(desc) = info.GetDescription() + && !desc.is_empty() + { + let _ = write!(out, "\n IErrorInfo: {desc}"); + } + } + } + + out +} + +trait WprContext { + fn wpr_context(self, source: &impl Interface) -> Result; +} + +impl WprContext for windows_core::Result { + fn wpr_context(self, source: &impl Interface) -> Result { + self.map_err(|e| { + let unknown: windows_core::IUnknown = source.cast().expect("cast to IUnknown"); + let context = wpr_error_context(e.code(), &unknown); + anyhow::anyhow!("{context}") + }) + } +} + +fn create_wpr(clsid: &windows_core::GUID) -> Result { + unsafe { + WPRCCreateInstanceUnderInstanceName::<_, T>( + &BSTR::from(INSTANCE_NAME), + clsid, + None, + CLSCTX_INPROC_SERVER.0, + ) + .context("WPRCCreateInstance failed") + } +} + +fn build_profile_collection(zed_pid: u32) -> Result { + let collection: IProfileCollection = create_wpr(&CProfileCollection)?; + + for profile_name in BUILTIN_PROFILES { + let profile: IProfile = create_wpr(&CProfile)?; + unsafe { + profile + .LoadFromFile(&BSTR::from(*profile_name), &BSTR::new()) + .wpr_context(&profile) + .with_context(|| format!("Load built-in profile '{profile_name}'"))?; + collection + .Add(&profile, VARIANT_FALSE) + .wpr_context(&collection) + .with_context(|| format!("Add profile '{profile_name}' to collection"))?; + } + } + + let heap_xml = heap_tracing_profile(zed_pid); + let heap_profile: IProfile = create_wpr(&CProfile)?; + unsafe { + heap_profile + .LoadFromString(&BSTR::from(heap_xml)) + .wpr_context(&heap_profile) + .context("Load profile from XML string")?; + collection + .Add(&heap_profile, VARIANT_BOOL(0)) + .wpr_context(&collection) + .context("Add ZedHeap profile to collection")?; + } + + Ok(collection) +} + +pub fn record_etw_trace(zed_pid: u32, output_path: &Path, socket_path: &str) -> 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(zed_pid, output_path, &mut stream) { + Ok(()) => Ok(()), + Err(e) => { + send_json( + &mut stream, + &StatusMessage::Error { + message: format!("{e:#}"), + }, + ) + .log_err(); + Err(e) + } + } +} + +fn record_etw_trace_inner( + zed_pid: u32, + output_path: &Path, + stream: &mut net::UnixStream, +) -> Result<()> { + let collection = build_profile_collection(zed_pid)?; + let control_manager: IControlManager = create_wpr(&CControlManager)?; + + // Cancel any leftover sessions with the same name that might exist + unsafe { + _ = control_manager.Cancel(None); + } + + unsafe { + control_manager + .Start(&collection) + .wpr_context(&control_manager) + .context("Start WPR recording")?; + } + + // We must call Stop or Cancel before returning, or the ETW session will record unbounded data to disk. + let cancel_guard = defer({ + let control_manager = control_manager.clone(); + move || unsafe { + let _ = control_manager.Cancel(None); + } + }); + + send_json(stream, &StatusMessage::Started)?; + + let command = receive_command(stream)?; + + match command { + ReceivedCommand::Cancel => { + unsafe { + control_manager + .Cancel(None) + .wpr_context(&control_manager) + .context("Cancel WPR recording")?; + } + cancel_guard.abort(); + + send_json(stream, &StatusMessage::Cancelled).log_err(); + } + ReceivedCommand::Stop { timed_out } => { + unsafe { + control_manager + .Stop( + &BSTR::from(output_path.to_string_lossy().as_ref()), + &collection, + None, + ) + .wpr_context(&control_manager) + .context("Stop WPR recording")?; + } + cancel_guard.abort(); + + if timed_out { + send_json(stream, &StatusMessage::TimedOut).log_err(); + } else { + send_json(stream, &StatusMessage::Stopped).log_err(); + } + } + } + + Ok(()) +} + +enum ReceivedCommand { + Cancel, + Stop { timed_out: bool }, +} + +fn receive_command(stream: &mut net::UnixStream) -> Result { + 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::Cancel) => Ok(ReceivedCommand::Cancel), + Ok(Command::Stop) => Ok(ReceivedCommand::Stop { timed_out: false }), + Err(error) => { + log::warn!("Failed to receive ETW command, treating as timed-out Stop: {error:#}"); + Ok(ReceivedCommand::Stop { timed_out: true }) + } + } +} + +pub struct EtwSession { + output_path: PathBuf, + stream: BufReader, + listener: net::UnixListener, + socket_path: PathBuf, +} + +pub fn launch_etw_recording(zed_pid: u32, output_path: &Path) -> Result { + let sock_path = std::env::temp_dir().join(format!("zed-etw-{zed_pid}.sock")); + + _ = 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 args = format!( + "--record-etw-trace --etw-zed-pid {} --etw-output \"{}\" --etw-socket \"{}\"", + zed_pid, + output_path.display(), + sock_path.display(), + ); + + use windows::Win32::UI::Shell::ShellExecuteW; + use windows_core::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 result = unsafe { + ShellExecuteW( + None, + PCWSTR(operation.as_ptr()), + PCWSTR(file.as_ptr()), + PCWSTR(parameters.as_ptr()), + PCWSTR::null(), + windows::Win32::UI::WindowsAndMessaging::SW_HIDE, + ) + }; + + let result_code = result.0 as usize; + if result_code <= 32 { + bail!("ShellExecuteW failed to launch elevated process (code: {result_code})"); + } + + let (stream, _) = listener.accept().context("Accept subprocess connection")?; + + 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 { + StatusMessage::Started => {} + StatusMessage::Error { message } => { + bail!("Subprocess reported error during start: {message}"); + } + other => { + bail!("Unexpected status from subprocess: {other:?}"); + } + } + + Ok(session) +} + +#[derive(Debug, serde::Serialize, serde::Deserialize)] +#[serde(tag = "type")] +pub enum StatusMessage { + Started, + Stopped, + TimedOut, + Cancelled, + Error { message: String }, +} + +#[derive(Debug, serde::Serialize, serde::Deserialize)] +#[serde(tag = "type")] +pub enum Command { + Stop, + Cancel, +} + +fn send_json(writer: &mut impl Write, value: &T) -> Result<()> { + let json = serde_json::to_string(value).context("Serialize message")?; + writeln!(writer, "{json}").context("Write to socket")?; + writer.flush().context("Flush socket")?; + Ok(()) +} + +fn recv_json(reader: &mut impl BufRead) -> Result { + let mut line = String::new(); + reader.read_line(&mut line).context("Read from socket")?; + if line.is_empty() { + bail!("Socket closed before a message was received"); + } + serde_json::from_str(line.trim()).context("Parse message") +} diff --git a/crates/net/src/stream.rs b/crates/net/src/stream.rs index d8b6852fcf3ecada2eab4711be12861f3d92fd74..986aa35a8efcd3b47b86340e08a3170ae99527af 100644 --- a/crates/net/src/stream.rs +++ b/crates/net/src/stream.rs @@ -2,6 +2,7 @@ use std::{ io::{Read, Result, Write}, os::windows::io::{AsSocket, BorrowedSocket}, path::Path, + sync::Arc, }; use async_io::IoSafe; @@ -12,13 +13,13 @@ use crate::{ util::{init, map_ret, sockaddr_un}, }; -pub struct UnixStream(UnixSocket); +pub struct UnixStream(Arc); unsafe impl IoSafe for UnixStream {} impl UnixStream { pub fn new(socket: UnixSocket) -> Self { - Self(socket) + Self(Arc::new(socket)) } pub fn connect>(path: P) -> Result { @@ -32,9 +33,14 @@ impl UnixStream { &addr as *const _ as *const _, len as i32, ))?; - Ok(Self(inner)) + Ok(Self(Arc::new(inner))) } } + + pub fn into_split(self) -> (OwnedReadHalf, OwnedWriteHalf) { + let inner = self.0; + (OwnedReadHalf(inner.clone()), OwnedWriteHalf(inner)) + } } impl Read for UnixStream { @@ -58,3 +64,23 @@ impl AsSocket for UnixStream { unsafe { BorrowedSocket::borrow_raw(self.0.as_raw().0 as _) } } } + +pub struct OwnedReadHalf(Arc); + +impl Read for OwnedReadHalf { + fn read(&mut self, buf: &mut [u8]) -> Result { + self.0.recv(buf) + } +} + +pub struct OwnedWriteHalf(Arc); + +impl Write for OwnedWriteHalf { + fn write(&mut self, buf: &[u8]) -> Result { + self.0.send(buf) + } + + fn flush(&mut self) -> Result<()> { + Ok(()) + } +} diff --git a/crates/zed/Cargo.toml b/crates/zed/Cargo.toml index 33c95ec742f96179d24f30a913699337e614b000..856c9b9dab4884773ec7d53dd210e81bbc4bedbf 100644 --- a/crates/zed/Cargo.toml +++ b/crates/zed/Cargo.toml @@ -230,6 +230,7 @@ zlog.workspace = true zlog_settings.workspace = true [target.'cfg(target_os = "windows")'.dependencies] +etw_tracing.workspace = true windows.workspace = true [target.'cfg(target_os = "windows")'.build-dependencies] diff --git a/crates/zed/src/main.rs b/crates/zed/src/main.rs index 54db5ba88f13e52fde818d174444d441c1727c73..27fe5e6e881b2117d2fd4dd412dcbf4a1e259225 100644 --- a/crates/zed/src/main.rs +++ b/crates/zed/src/main.rs @@ -197,6 +197,28 @@ fn main() { return; } + #[cfg(target_os = "windows")] + if args.record_etw_trace { + let zed_pid = args.etw_zed_pid.unwrap_or(0); + 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()) + { + eprintln!("ETW trace recording failed: {error:#}"); + process::exit(1); + } + return; + } + // `zed --nc` Makes zed operate in nc/netcat mode for use with MCP if let Some(socket) = &args.nc { match nc::main(socket) { @@ -699,6 +721,8 @@ fn main() { json_schema_store::init(cx); miniprofiler_ui::init(*STARTUP_TIME.get().unwrap(), cx); which_key::init(cx); + #[cfg(target_os = "windows")] + etw_tracing::init(cx); cx.observe_global::({ let http = app_state.client.http_client(); @@ -1597,6 +1621,26 @@ struct Args { /// Output current environment variables as JSON to stdout #[arg(long, hide = true)] printenv: bool, + + /// Record an ETW trace. Must be run as administrator. + #[cfg(target_os = "windows")] + #[arg(long, hide = true)] + record_etw_trace: bool, + + /// The PID of the Zed process to trace for heap analysis. + #[cfg(target_os = "windows")] + #[arg(long, hide = true)] + etw_zed_pid: Option, + + /// Output path for the ETW trace file. + #[cfg(target_os = "windows")] + #[arg(long, hide = true)] + etw_output: Option, + + /// Unix socket path for IPC with the parent Zed process. + #[cfg(target_os = "windows")] + #[arg(long, hide = true)] + etw_socket: Option, } #[derive(Clone, Debug)]