Event Tracing for Windows
Understand how ETW records time-stamped events around a running game and how to read a short performance trace.
Cheatsheet for Lesson 5.10
A one-page summary of “Event Tracing for Windows” you can keep beside your notes. Plain Markdown.
Lesson 5.10 — Event Tracing for Windows: cheatsheet
- Remember: Absence from a trace is meaningful only after checking what was recorded and whether any events were lost.
- Also useful: Think of a recording chain: providers produce observations, a controller starts the session, buffers carry events, and a consumer interprets the saved result.
Key terms
run_wpr:run_wpris the command wrapper from the complete helper above.
Formulas and patterns
providers → ETW session buffers → gha-game.etl → Windows Performance Analyzer
Review
- Q: What makes ETW useful for investigating a short game-performance problem? A: Timestamped events from system and application providers can be lined up on one timeline
- Q: Why might you choose ETW over a breakpoint debugger for a stutter? A: Stopping a thread changes timing, while ETW records events with much less disturbance
Observation without a breakpoint
Section titled “Observation without a breakpoint”A debugger stops or single-steps a thread. That is perfect when you need the exact instruction and registers, but stopping changes timing. A game may stutter, a network timeout may expire, or another thread may continue while the selected thread is paused.
Event Tracing for Windows, shortened to ETW, records structured events with much less disturbance. Windows and applications can report process, thread, image-load, CPU-sampling, disk, networking, and other activity. The exact events depend on which providers and keywords a trace enables.
ETW is for questions such as:
- Which thread used most of the CPU during a slow frame?
- Which DLLs loaded while the game started?
- Did a long pause line up with disk activity?
- Was work happening on one thread or many?
ETW does not tell you the meaning of an arbitrary gold field. Use the tool that matches the question.
ETW is a producer–buffer–consumer system. Providers describe events, a session controls which streams are active, and consumers turn the ordered records into a model:
Buffers separate fast event production from slower analysis, but they are finite. A trace can lose records when production outruns collection, so “missing from this capture” does not automatically mean “never happened.”
1 Records 1, 2 and 3 fill it
Session buffer
Slotsrecord 1record 2record 3The session buffer has three slots, and each event fills one.
2 Event 4 arrives: no free slot
Arrival
Fourth eventevent 4droppedSession buffer
Slotsrecord 1record 2record 3The buffer is full, so the new record is dropped before it is stored.
3 Draining later leaves a gap
Session buffer
SlotsfreefreefreeWhat the log can say
Logrecords 1 to 3 existevent 4: unknownmay have happenedReading frees the slots, but event 4 is gone. The log shows a gap, not proof that the event never happened.
Three slots, four arrivals. The fourth is stopped at the buffer and is not recorded.
Four roles make one trace
Section titled “Four roles make one trace”The names sound formal, but the jobs are simple:
- A provider creates events. Windows, a driver, or an application can be a provider.
- A session receives selected events into buffers and possibly an
.etlfile. - A controller starts and stops the session and selects providers.
- A consumer reads the events live or from the saved file.
Windows Performance Recorder, or WPR, is a controller. Windows Performance Analyzer, or WPA, is a graphical consumer.
providers → ETW session buffers → gha-game.etl → Windows Performance AnalyzerAn event normally includes a timestamp, provider identity, event identity, process/thread context, and provider-defined fields. A keyword is a bitmask used to select groups of related events from a provider.
Keep the lab short and isolated
Section titled “Keep the lab short and isolated”A general performance profile can include activity from the whole machine, not just the game. That may reveal filenames, process names, or timing from unrelated applications.
Use a clean disposable Windows VM, close unrelated programs, record only long enough to reproduce the game action, and do not publish the .etl file without reviewing it. The course helper refuses to overwrite an existing trace.
Keep the controller narrow
Section titled “Keep the controller narrow”The helper launches wpr.exe directly with std::process::Command. It does not build a command string for PowerShell or cmd.exe, so the output path is passed as one argument rather than interpreted as shell code.
Complete lab source: etw_capture.rs
etw_capture.rs
Show the codeHide the code
#[cfg(windows)]
fn main() -> anyhow::Result<()> {
use std::{io, path::PathBuf, process::Command};
use anyhow::{Context, ensure};
fn run_wpr(arguments: &[&std::ffi::OsStr]) -> anyhow::Result<()> {
let status = Command::new("wpr.exe")
.args(arguments)
.status()
.context("could not start wpr.exe")?;
ensure!(status.success(), "wpr.exe returned {status}");
Ok(())
}
let output = std::env::args_os()
.nth(1)
.map(PathBuf::from)
.unwrap_or_else(|| PathBuf::from("gha-game.etl"));
ensure!(
!output.exists(),
"refusing to overwrite {}",
output.display()
);
run_wpr(&[
std::ffi::OsStr::new("-start"),
std::ffi::OsStr::new("GeneralProfile"),
std::ffi::OsStr::new("-filemode"),
])?;
println!("ETW recording started.");
println!("Launch or exercise the game now, then press Enter to stop.");
let mut answer = String::new();
if let Err(error) = io::stdin().read_line(&mut answer) {
let _ = run_wpr(&[std::ffi::OsStr::new("-cancel")]);
return Err(error).context("could not read Enter; the trace was cancelled");
}
let stop_result = run_wpr(&[std::ffi::OsStr::new("-stop"), output.as_os_str()]);
if stop_result.is_err() {
let _ = run_wpr(&[std::ffi::OsStr::new("-cancel")]);
}
stop_result?;
println!("Saved {}", output.display());
println!("Open the ETL file in Windows Performance Analyzer.");
Ok(())
}
#[cfg(not(windows))]
fn main() {
eprintln!("This ETW capture helper must run on Windows.");
}#[cfg(windows)]fn main() -> anyhow::Result<()> { use std::{io, path::PathBuf, process::Command}; use anyhow::{Context, ensure};
fn run_wpr(arguments: &[&std::ffi::OsStr]) -> anyhow::Result<()> { let status = Command::new("wpr.exe") .args(arguments) .status() .context("could not start wpr.exe")?; ensure!(status.success(), "wpr.exe returned {status}"); Ok(()) }
let output = std::env::args_os() .nth(1) .map(PathBuf::from) .unwrap_or_else(|| PathBuf::from("gha-game.etl")); ensure!( !output.exists(), "refusing to overwrite {}", output.display() );
run_wpr(&[ std::ffi::OsStr::new("-start"), std::ffi::OsStr::new("GeneralProfile"), std::ffi::OsStr::new("-filemode"), ])?;
println!("ETW recording started."); println!("Exercise the game, then press Enter to stop."); let mut answer = String::new(); if let Err(error) = io::stdin().read_line(&mut answer) { let _ = run_wpr(&[std::ffi::OsStr::new("-cancel")]); return Err(error) .context("could not read Enter; trace cancellation requested"); }
let stop_result = run_wpr(&[ std::ffi::OsStr::new("-stop"), output.as_os_str(), ]); if stop_result.is_err() { let _ = run_wpr(&[std::ffi::OsStr::new("-cancel")]); } stop_result?; println!("Saved {}", output.display()); println!("Open the ETL file in Windows Performance Analyzer."); Ok(())}
#[cfg(not(windows))]fn main() { eprintln!("This ETW capture helper must run on Windows.");}Why the cleanup path matters
Section titled “Why the cleanup path matters”Starting a trace creates system state that outlives the first wpr.exe process. If reading from the terminal fails or stopping the session fails, the helper asks WPR to cancel.
The original helper starts WPR, waits for Enter, then stops the recording. If the terminal read fails, ? returns early and the recording session remains active. run_wpr is the command wrapper from the complete helper above.
fn record_trace(output: &Path) -> anyhow::Result<()> { run_wpr(&[OsStr::new("-start"), OsStr::new("GeneralProfile")])?; let mut answer = String::new(); io::stdin().read_line(&mut answer)?; // An error skips the stop command. run_wpr(&[OsStr::new("-stop"), output.as_os_str()])?; Ok(())}Handle that early-return boundary explicitly and request best-effort cleanup before returning the original input error. The same cleanup request is made if stopping fails. Cancellation itself can fail, so the returned error says that it was requested rather than claiming it succeeded.
Improved code
fn record_trace(output: &Path) -> anyhow::Result<()> { run_wpr(&[OsStr::new("-start"), OsStr::new("GeneralProfile")])?; let mut answer = String::new(); if let Err(error) = io::stdin().read_line(&mut answer) { let _ = run_wpr(&[OsStr::new("-cancel")]); return Err(error).context("could not read Enter; trace cancellation requested"); } let stop_result = run_wpr(&[OsStr::new("-stop"), output.as_os_str()]); if stop_result.is_err() { let _ = run_wpr(&[OsStr::new("-cancel")]); } stop_result?; Ok(())}Show the diff
fn record_trace(output: &Path) -> anyhow::Result<()> { run_wpr(&[OsStr::new("-start"), OsStr::new("GeneralProfile")])?; let mut answer = String::new(); io::stdin().read_line(&mut answer)?; // An error skips the stop command. if let Err(error) = io::stdin().read_line(&mut answer) { let _ = run_wpr(&[OsStr::new("-cancel")]); return Err(error).context("could not read Enter; trace cancellation requested"); } run_wpr(&[OsStr::new("-stop"), output.as_os_str()])?; let stop_result = run_wpr(&[OsStr::new("-stop"), output.as_os_str()]); if stop_result.is_err() { let _ = run_wpr(&[OsStr::new("-cancel")]); } stop_result?; Ok(())}Takeaway: every operation that starts persistent state needs a cleanup path for later failures.
Why this version?
Section titled “Why this version?”An error should not leave a forgotten recorder running. Cleanup is part of correctness, just like restoring a breakpoint byte or closing a process handle.
The ignored result on -cancel is deliberate: the original error remains the most useful explanation, and cancellation is a best-effort recovery after something already went wrong.
Starting a trace, and cleaning up when something fails. This step-by-step trace needs JavaScript. Here is the code and where the run ends.
run_wpr("-start", "GeneralProfile") # recording begins
print("Press Enter to stop.")
if read_line() fails:
run_wpr("-cancel") # best effort
return Err("trace was cancelled")
stop = run_wpr("-stop", "gha-game.etl")
if stop fails: run_wpr("-cancel")
print("Saved gha-game.etl")The helper reports the saved file. Open it in Windows Performance Analyzer. Every path above ended with the recorder not running, and cleanup is part of correctness.
Final values: recorder = stopped; etl_file = gha-game.etl saved; read_line = ok; stop = ok.
Read every step of the lesson’s example
Step 1, line 1. wpr.exe is asked to start recording with the GeneralProfile. WPR is a separate program that keeps recording after this command returns, so the recorder is now running as system state.
recorder = running; etl_file = none yet.
Step 2, line 2. The helper tells you to exercise the game and press Enter. The recording runs while you do.
recorder = running; etl_file = none yet.
Step 3, line 3. Reading Enter works. You pressed Enter, so the helper moves on to stop the recording.
recorder = running; etl_file = none yet; read_line = ok.
Step 4, line 6. wpr.exe -stop writes everything captured to gha-game.etl and ends the recording.
recorder = stopped; etl_file = gha-game.etl saved; read_line = ok; stop = ok.
Step 5, line 8. The helper reports the saved file. Open it in Windows Performance Analyzer. Every path above ended with the recorder not running, and cleanup is part of correctness.
recorder = stopped; etl_file = gha-game.etl saved; read_line = ok; stop = ok.
Record one repeatable action
Section titled “Record one repeatable action”Build the helper:
cd windows-labscargo build --release --target i686-pc-windows-msvc --bin etw_captureIn a clean VM, start the capture from an administrator terminal if WPR requests it:
.\target\i686-pc-windows-msvc\release\etw_capture.exe ` .\wesnoth-turn.etlThen:
- start Wesnoth 1.14.9;
- load a small local scenario;
- end exactly one turn;
- return to the terminal and press Enter;
- open
wesnoth-turn.etlin WPA.
Use the same short procedure for AssaultCube startup or one Urban Terror map load. Repeatability makes two traces comparable.
Read the result in layers
Section titled “Read the result in layers”Start with broad questions before zooming into thousands of events:
- In Process Lifetimes, confirm the expected executable and PID.
- In CPU Usage (Sampled), filter to that process and find busy threads.
- In Image Load, inspect DLLs loaded during startup.
- In Disk Usage, look for long file reads that overlap a pause.
- In the timeline, select only the few seconds around your action.
A stack may show system DLLs, game functions, or unresolved addresses. Symbols translate addresses into names. Without matching symbols, the timing is still real, but the function label may remain a module plus offset.
Missing events are data too
Section titled “Missing events are data too”ETW writes through buffers. If a provider produces events faster than the session can deliver them, events can be lost. A controller can report buffer and event-loss statistics.
That means “I did not see an event” is weaker than “the event could not have happened.” Check whether the chosen profile included the relevant provider and whether the session lost data.
Keep tracing separate from evasion
Section titled “Keep tracing separate from evasion”This chapter starts, stops, and consumes an ordinary diagnostic session. It does not disable providers, patch tracing APIs, alter another process’s telemetry, or hide module loads. Those actions would corrupt the very trace this lab is trying to read.
The complete helper is etw_capture.rs.
We now have three distinct observations: the scanner locates candidate data, the debugger pauses at exact instructions, and ETW summarizes timed system activity. The optional next lesson asks how to scan several captured regions without letting concurrency change the result. Chapter 6 then uses verified sites while building a small library inside an explicitly supported game process.
References: Microsoft: About Event Tracing, Windows Performance Recorder, and Windows Performance Analyzer.
Correlate events over time
What makes ETW useful for investigating a short game-performance problem?
ETW records structured events with timing and provider identity. A short controlled trace helps connect CPU, file, process, and scheduling activity.
Finished Lesson 5.10?
Mark it done and the lesson list shows a beside it. A chapter whose lessons are all done is marked done too. Progress is saved in this browser only.
Plain Markdown, saved in this browser.
This book grew out of the original Game Hacking Academy by attilathedud and has been rewritten as a beginner-first course. Like the original repository, its source is published under the Apache License 2.0.