Add ETW profile recorder action (#49712)

John Tur created

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.

Change summary

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(-)

Detailed changes

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",

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" }

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",
+] }

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<EtwSessionHandle>);
+
+impl Global for GlobalEtwSession {}
+
+fn has_active_etw_session(cx: &App) -> bool {
+    cx.global::<GlobalEtwSession>().0.is_some()
+}
+
+fn show_etw_notification(cx: &mut App, message: impl Into<gpui::SharedString>) {
+    let message = message.into();
+    show_app_notification(NotificationId::unique::<EtwNotification>(), cx, move |cx| {
+        cx.new(|cx| MessageNotification::new(message.clone(), cx))
+    });
+}
+
+fn show_etw_notification_with_action(
+    cx: &mut App,
+    message: impl Into<gpui::SharedString>,
+    button_label: impl Into<gpui::SharedString>,
+    on_click: impl Fn(&mut gpui::Window, &mut gpui::Context<MessageNotification>)
+    + 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::<EtwNotification>(), 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<StatusMessage>, 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::<GlobalEtwSession>().0 = None;
+                    show_etw_status_notification(cx, status, output_path);
+                });
+            })
+            .detach();
+
+            cx.update(|cx| {
+                cx.global_mut::<GlobalEtwSession>().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::<GlobalEtwSession>().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::<GlobalEtwSession>().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#"<?xml version="1.0" encoding="utf-8"?>
+<WindowsPerformanceRecorder Version="1.0" Author="Zed Industries">
+  <Profiles>
+    <HeapEventProvider Id="ZedHeapProvider">
+      <HeapProcessIds Operation="Set">
+        <HeapProcessId Value="{zed_pid}"/>
+      </HeapProcessIds>
+    </HeapEventProvider>
+
+    <Profile Id="ZedHeap.Verbose.File" Base="Heap.Verbose.File" Name="ZedHeap" DetailLevel="Verbose" LoggingMode="File" Description="Heap tracing for the Zed process">
+      <Collectors Operation="Add">
+        <HeapEventCollectorId Value="HeapCollector_WPRHeapCollector">
+          <HeapEventProviders Operation="Set">
+            <HeapEventProviderId Value="ZedHeapProvider"/>
+          </HeapEventProviders>
+        </HeapEventCollectorId>
+      </Collectors>
+    </Profile>
+  </Profiles>
+
+  <TraceMergeProperties>
+    <TraceMergeProperty Id="TraceMerge_Default" Name="TraceMerge_Default">
+      <FileCompression Value="true"/>
+    </TraceMergeProperty>
+  </TraceMergeProperties>
+</WindowsPerformanceRecorder>"#
+    )
+}
+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::<IParsingErrorInfo>() {
+        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::<IControlErrorInfo>() 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::<windows::Win32::System::Com::IErrorInfo>() {
+        unsafe {
+            if let Ok(desc) = info.GetDescription()
+                && !desc.is_empty()
+            {
+                let _ = write!(out, "\n  IErrorInfo: {desc}");
+            }
+        }
+    }
+
+    out
+}
+
+trait WprContext<T> {
+    fn wpr_context(self, source: &impl Interface) -> Result<T>;
+}
+
+impl<T> WprContext<T> for windows_core::Result<T> {
+    fn wpr_context(self, source: &impl Interface) -> Result<T> {
+        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<T: windows_core::Interface>(clsid: &windows_core::GUID) -> Result<T> {
+    unsafe {
+        WPRCCreateInstanceUnderInstanceName::<_, T>(
+            &BSTR::from(INSTANCE_NAME),
+            clsid,
+            None,
+            CLSCTX_INPROC_SERVER.0,
+        )
+        .context("WPRCCreateInstance failed")
+    }
+}
+
+fn build_profile_collection(zed_pid: u32) -> Result<IProfileCollection> {
+    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<ReceivedCommand> {
+    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::<Command>(&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<net::UnixStream>,
+    listener: net::UnixListener,
+    socket_path: PathBuf,
+}
+
+pub fn launch_etw_recording(zed_pid: u32, output_path: &Path) -> Result<EtwSession> {
+    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<u16> = "runas\0".encode_utf16().collect();
+    let file: Vec<u16> = format!("{}\0", exe_path.to_string_lossy())
+        .encode_utf16()
+        .collect();
+    let parameters: Vec<u16> = 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<T: serde::Serialize>(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<T: serde::de::DeserializeOwned>(reader: &mut impl BufRead) -> Result<T> {
+    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")
+}

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<UnixSocket>);
 
 unsafe impl IoSafe for UnixStream {}
 
 impl UnixStream {
     pub fn new(socket: UnixSocket) -> Self {
-        Self(socket)
+        Self(Arc::new(socket))
     }
 
     pub fn connect<P: AsRef<Path>>(path: P) -> Result<Self> {
@@ -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<UnixSocket>);
+
+impl Read for OwnedReadHalf {
+    fn read(&mut self, buf: &mut [u8]) -> Result<usize> {
+        self.0.recv(buf)
+    }
+}
+
+pub struct OwnedWriteHalf(Arc<UnixSocket>);
+
+impl Write for OwnedWriteHalf {
+    fn write(&mut self, buf: &[u8]) -> Result<usize> {
+        self.0.send(buf)
+    }
+
+    fn flush(&mut self) -> Result<()> {
+        Ok(())
+    }
+}

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]

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::<SettingsStore>({
             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<u32>,
+
+    /// Output path for the ETW trace file.
+    #[cfg(target_os = "windows")]
+    #[arg(long, hide = true)]
+    etw_output: Option<PathBuf>,
+
+    /// Unix socket path for IPC with the parent Zed process.
+    #[cfg(target_os = "windows")]
+    #[arg(long, hide = true)]
+    etw_socket: Option<String>,
 }
 
 #[derive(Clone, Debug)]