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.
This commit is contained in:
John Tur 2026-02-20 08:36:04 -05:00 committed by GitHub
parent f22f4db6c0
commit 989887ca0b
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
8 changed files with 804 additions and 6 deletions

46
Cargo.lock generated
View file

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

View file

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

View file

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

View file

@ -0,0 +1 @@
../../LICENSE-GPL

View file

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

View file

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

View file

@ -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]

View file

@ -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)]