winapp run . --profile .\traces\startup --detachThis launches your project and starts a bounded native WinUI 3 ETW recording.
Use the returned capture ID with winapp perf status or winapp perf stop.
The default recording lasts 30 seconds and allows 512 MiB of raw ETL. Override
these with --profile-duration-sec (1-300) and --profile-max-size-mib (1-1024).
UI-heavy scenarios such as repeated page navigation can record several MiB per
second. If a recording stops with stop reason size-limit before your scenario
ends, raise the size limit or shorten the scenario.
Without --detach, run continues waiting for the app after recording finishes.
Stopping a recording never closes the app.
Profiling runs only on this machine. run --profile --on sandbox fails before
building or launching; remove --on sandbox to profile locally, or remove
--profile to run in Windows Sandbox without recording.
Closing the app ends and finalizes its recording immediately, including with
--detach; it does not wait for the duration limit. Status reports completed
with stop reason target-exited. Retained events remain available for analysis.
The final buffered events or loss counters may be unavailable after process exit,
so coverage can still be partial. Stop explicitly before closing the app when
preserving the final moments is important.
For startup coverage, see Record startup layout.
--no-launch cannot be combined with --profile.
Existing run restrictions still apply; for example,
--debug-output cannot be combined with --detach or --json. Debugger pauses
distort elapsed timings, so avoid attaching a debugger while profiling.
winapp perf start --app MyWinUIApp --output .\traces\scroll
winapp perf mark CAPTURE_ID --name scenario-start
# Navigate and scroll in the app, manually or with verified winapp ui selectors.
# Wait for the intended visible scenario to finish before placing the end marker.
winapp perf mark CAPTURE_ID --name scenario-end
winapp perf stop CAPTURE_IDReplace CAPTURE_ID with the returned id, and MyWinUIApp with your app.
start prints the capture ID and stop command. mark confirms the added marker
name, and stop confirms that recording stopped. Warnings remain visible.
Add --json when a script or agent needs structured capture state and IDs.
start returns after the worker's control channel and provider enablement are
ready, not after proving that useful events have been recorded. The recorder
does not take the foreground or prevent a separate agent from driving the UI.
Use winapp ui inspect --app MyWinUIApp --interactive to discover selectors;
see UI automation for navigation and scrolling commands.
For attach captures, --app (or -a) accepts a PID, process name (exact or partial),
or window title, using the same matching rules as winapp ui:
winapp perf start --app MyWinUIApp --output .\traces\layout
winapp perf start --app "My App Window" --output .\traces\interactionNames and titles resolve once to a process. Recording covers that process, not only the selected window. If a process name is ambiguous, use a PID or a more specific window title.
The corresponding limit options are --duration-sec and
--max-size-mib. A recording also stops when the target exits or its size limit
is reached. Use winapp perf status CAPTURE_ID to check whether a background
recording is still running or has ended, including its stop reason and any final
errors, without stopping it. stop is repeatable.
If a worker disappears, status reports failure rather than success. A later
stop attempts cleanup only when the recorded process and ETW session identities
still match. Unknown loss remains unknown after recovery.
The output directory must be empty. Keep it until you no longer need the evidence, and move captures only after stopping them.
winapp run . --profile .\traces\startup --profile-mode elevated --detach --jsonBy default, run --profile starts recording just after the app launches, so
early WinUI initialization, XAML parsing, and first layout can be missed.
Profile.StartupCoverage reports post-launch attachment; early startup may be missing. That is fine for scenarios that happen after launch, such as
navigation or scrolling.
To record startup, add --profile-mode elevated. Windows shows one
administrator approval (UAC) prompt for the recorder; your app still runs
unelevated under your account. The recording starts before launch and
Profile.StartupCoverage reports recorded from launch (elevated session).
Do not add startup delays or replace a generated Main to capture startup.
Choose an empty output directory on a local drive, without symbolic links or
junctions in its path. Elevated recording also requires a link-free local path
for winapp's control directory. The recorder prevents these directories and their
parents from being renamed or redirected until it exits. If a path cannot be
secured, recording fails; use the direct directory path or record after launch
with --profile-mode attach.
Elevated recording is limited to the launched app's package (packaged apps) or
executable name (unpackaged apps), and analysis keeps only the launched process.
Other instances of the same app that run during the recording also count
toward --profile-max-size-mib. If the UAC prompt is declined, run fails
before launching; rerun without --profile-mode elevated to record after launch.
If activation reuses an existing process, coverage is labeled
attached-to-existing; startup not recorded. Close that instance before
launching when you need its startup.
If an elevated recorder is terminated unexpectedly, its recording keeps running
until its size limit, the next elevated recording, or until you stop it from an
administrator terminal with logman stop WinApp-Perf-<capture-id> -ets.
winapp perf analyze .\traces\scroll --from-marker scenario-start --to-marker scenario-endThe default report starts with process resources,
then observed activity on the primary UI thread. It
lists the 10 XAML resources with the most parsing time, followed by the detailed
operation ranking. The text report skips the parsing-resource section when it is
empty. Each activity interval belongs to only one category, so nested
layout or parsing is not counted again as framework time.
Percentages use the selected range's wall-clock duration, not sampled CPU time.
The text summary omits per-operation evidence IDs and count/mean/max/p95
statistics for readability. Add --json to include them, or use a drill-down
view for more detail.
Its footer shows the returned row count and, when more rows exist, a next-page
command with the same range and filters. Coverage details remain in JSON;
incomplete evidence still produces a nonzero exit status.
All text views omit the repeated interpretation disclaimers; use --json to
include the structured limitations field.
| Category | What it includes |
|---|---|
| Parsing | Timed XAML parsing and component-loading scopes |
| Layout | Template application, measure, arrange, and related timed layout scopes |
| Render | Concrete UI-thread render-walk and frame-submission scopes; the enclosing frame is not counted as rendering |
| Composition submission | Main-device commit and DComp surface-update calls on the selected UI thread; not GPU execution or display latency |
| Other observed XAML | Other timed XAML framework, input, scrolling, virtualization, and initialization work |
| Image decode/load | Timed image work on the selected UI thread; off-thread decode does not contribute |
| Unclassified | The remaining selected-range time, including idle, waits, and uninstrumented work |
Without --thread, analysis chooses the thread with the most complete frames in
the selected range, breaking ties by observed frame time and then thread ID. If
no frame is available, it chooses the thread with the most non-image WinUI
activity. Use --thread <id> to override that choice.
The parsing preview reports selected-range time, so a parse that crosses a marker or timestamp boundary is clipped. To page every observed resource, run:
winapp perf analyze .\traces\scroll --view parsing --from-marker scenario-start --to-marker scenario-endUse narrower views to investigate the overview:
winapp perf analyze .\traces\scroll --view elements --type ItemsStackPanel --sort self
winapp perf analyze .\traces\scroll --view element --id e70 --depth 2
winapp perf analyze .\traces\scroll --view frames --offset 10 --limit 10
winapp perf analyze .\traces\scroll --view hotspots --min-frame-ms 16.67
winapp perf analyze .\traces\scroll --view events --event v123Replace element and event IDs with IDs from your results.
| View | What it returns |
|---|---|
summary |
Process resources, primary-UI-thread activity, the 10 hottest parsed resources, then recorded phases and elements ranked by exclusive or self time |
parsing |
Complete pageable XAML resource ranking by observed parsing time |
elements |
Element rankings; --type filters observed type names, and --sort accepts self, inclusive, or count |
element |
One trace-local element and, with --depth, elements observed beneath it during the selected range |
frames |
UI-side frame, render-walk and submission intervals, longest first |
hotspots |
Complete Frame intervals at or above --min-frame-ms, longest first, with dominant direct operations and GC overlap |
events |
Original event names, descriptors and selected payload fields, in time order |
calls |
Instrumented operations, longest first; --family layout restricts the operation family |
call |
One operation and its execution subtree, selected with --id; default depth 2, maximum 4 |
gc |
CLR collection lifetimes and runtime suspension episodes, in time order |
resources |
Process CPU counters, private bytes, working set, thread count, and handle count, in time order |
All views accept --from-ms and --to-ms. --thread selects the UI thread for
summary and parsing, and restricts UI operations or raw events in other
applicable views. It does not apply to gc, whose boundaries can cross threads,
or process-wide resources.
Milliseconds are relative
to provider readiness; negative times can occur while the provider set is being
enabled. A marker and a millisecond boundary cannot specify the same range end.
The events view additionally accepts --provider <guid>, --element <id>, and
--event <exact-name-or-evidence-id>.
hotspots defaults --min-frame-ms to 16.67. Thresholding uses each complete
frame's full elapsed duration, even when the selected range clips the frame;
clippedOverlapMs reports only the selected-range overlap. Each row embeds up to
four longest complete direct child operations with their call IDs, families,
elapsed/exclusive durations, and evidence IDs. Use a child ID with
--view call --id <call-id> for deeper expansion. These are instrumented direct
children, not CPU stacks, GPU work, or visual-tree descendants.
JSON queries default to 10 rows and at most 16,384 UTF-8 bytes for the whole
response. Use nextOffset with the same immutable query to retrieve the next page.
--limit allows 1-100 rows; --max-bytes allows 4,096-1,048,576 bytes.
byteBudgetLimited, row projected, and event omittedFields indicate bounded
projections. Narrow the query or increase the budget to retrieve more detail.
For summary, activity is fixed-size and parsingResources is a top-10
preview; parsingResourcesTotal and parsingResourcesOmitted describe that
preview independently of the detailed summary page.
Usable partial results are written to stdout with a nonzero exit status. Text
output explains why the results may be incomplete on stderr, such as lost
events, undecodable timing payloads, or a time range extending beyond the recording.
With --json, stderr contains a structured partial_data error and the reasons
are in stdout's coverage.reasons. Do not discard stdout solely because
the exit code is nonzero. No usable performance evidence is an error, not a
report that the app is fast.
winapp perf analyze .\traces\scroll --view calls --family layout
winapp perf analyze .\traces\scroll --view call --id c42 --depth 3
winapp perf analyze .\traces\scroll --view call --id c42 --depth 3 --jsonReplace c42 with a call ID from your results. Console output indents child
operations and shows elapsed/exclusive timings, source information where recorded,
boundary status, and evidence IDs. This is an execution tree of instrumented
operations, not a sampled CPU stack or the visual tree.
Without explicit time boundaries, call selects the operation's observed start
and end. clippedOverlapMs is its overlap with the query range; the full elapsed
duration is kept separately. When expansion stops at the depth limit, query the
indicated child call to continue.
JSON rows include parent IDs, depth, ancestor IDs, child counts and depthLimited.
Follow nextOffset to page the same tree without losing its context. A frame
containing a long layout can be expanded into the element operations inside it,
rather than treating the entire frame duration as rendering work.
winapp perf analyze .\traces\scroll --view gc --from-ms 500 --to-ms 1500
winapp perf analyze .\traces\scroll --view gc --from-marker scenario-start --to-marker scenario-end
winapp perf analyze .\traces\scroll --view gc --sort duration
winapp perf analyze .\traces\scroll --view gc --id gc7Use a returned GC interval ID in place of gc7 to inspect its boundaries and evidence.
New captures request informational CLR GC and suspension events automatically.
No extra flag is needed for either perf start or run --profile. Allocation
sampling, heap dumps, forced collections, and runtime rundown are not requested.
Collection rows show generation, reason, collection type and start-to-end lifetime.
A background collection's lifetime is not a pause. Suspension rows keep the
request, fully-suspended, restart-begin and restart-complete boundaries separately.
The outer duration includes suspension and resumption transitions;
fullySuspendedMs covers suspend-complete through restart-begin.
Non-GC reasons, such as debugger suspension, are not attributed to GC.
GC rows are chronological unless --sort duration is specified. Duration sorting
ranks complete collection and suspension rows by outer elapsed durationMs,
longest first; incomplete intervals with unavailable duration are returned last.
fullySuspendedMs remains separately reported and is not the sort key.
Call, frame, and hotspot rows include gcOverlap when CLR events were observed. Its duration
is the union of complete GC-related suspension intervals overlapping that
operation and the selected range. It is not additional child work, not CPU time,
and not proof that the native UI thread was blocked throughout that interval.
If GC coverage is partial, the observed overlap is a lower bound, not an exact
total. Use the referenced intervals and their event evidence to investigate.
gcCoverage distinguishes observed, unsupported, unavailable, not-observed, and
not-recorded evidence. A failed optional GC provider does not discard usable XAML
data; inspect providerStates and its error. A GC query with no recorded CLR events
fails explicitly instead of reporting zero pauses. Native apps may emit no CLR
events, and older captures cannot gain GC events that were never recorded.
Disk/file I/O, managed lock contention, JIT/loading, CPU scheduling and GPU activity are not included. Physical disk/file tracing requires a separate system-tracing workflow and its permissions; it is not another event source this private logger can enable. Logical network or file-request duration, where separately recorded, also does not by itself prove UI blocking.
winapp perf analyze .\traces\startup --view resources
winapp perf analyze .\traces\scroll --view resources --from-marker scenario-start --to-marker scenario-end --jsonProcess counters are collected automatically in both attach and elevated captures, including native C++ apps, without requiring extra elevation. Sampling starts when the recorder binds to the target process, repeats approximately once per second, and takes a final sample when stopping if the process is still alive.
The default analysis includes a processResources summary. Use --view resources
for the paged timeline, with the same marker/millisecond bounds, --limit,
--offset, and --max-bytes. This view reads capture.json directly, so it also
works when a finalized capture has no usable XAML events or ETL files.
The summary shows first, last, and maximum observed private bytes, working set, thread count, and handle count within the selected range. Private bytes measure private committed memory; working set measures resident memory and can include shared pages. These are process costs, not allocation counts or memory attributed to XAML, compositor, or driver components.
Summary user/kernel/total CPU milliseconds are differences between the first and
last samples in the selected range, across all target-process threads. The
timeline's user/kernel CPU values are cumulative since process creation.
firstSampleMs and lastSampleMs identify the actual observed span, relative to
recording readiness. CPU can exceed elapsed time when multiple cores work
concurrently. No CPU percentage or per-thread/module attribution is inferred.
Counters exclude child processes. Even an elevated startup capture cannot recover counter snapshots from before binding. Sampling can miss brief memory/thread spikes, and process exit can prevent a final sample; observed maxima are not exact lifetime peaks. Range boundaries are not interpolated. With fewer than two samples, interval CPU is unavailable, not zero.
Older captures report not-recorded in the summary and cannot gain counters
retroactively. A range without samples reports not-observed. Failed sampling
attempts appear as failedSamples and lastError, without discarding the ETW
recording. A resource query returns retained samples with a nonzero exit status
when sampling failures occurred; keep stdout and inspect coverage.reasons.
winapp perf analyze .\traces\scroll --view calls --family composition --from-marker scenario-start --to-marker scenario-end --json
winapp perf analyze .\traces\scroll --view events --event DCompDeviceCommit --from-marker scenario-start --to-marker scenario-end --jsonCommitMainDevice measures elapsed time inside WinUI's main composition-device
commit call. Compare it with layout and render-walk timings; the default report
separates Composition submission from Render on the selected UI thread.
These scopes can nest, so their inclusive totals must not be added together.
New captures also request optional DirectComposition events:
| Evidence | What it tells you |
|---|---|
DCompBeginDraw |
Elapsed time entering a surface update; start-event fields identify the resource and update rectangle |
DCompEndDraw |
Elapsed time finishing a surface update |
DCompUpdateToken |
Elapsed time updating the surface token |
DCompDeviceCommit |
A notification containing device/channel IDs and the last committed/confirmed batch IDs; it has no inferred duration |
The calls view includes all target-process threads unless you add --thread.
Use --view call --id <call-id> to inspect a timed operation and --view events --event <event-id> to inspect its referenced start/end evidence. Resource/device
IDs are trace-local identifiers, not XAML elements or UI Automation selectors.
Both attach and elevated captures request the same app-side composition evidence.
The query's directCompositionProvider reports whether that optional provider
was enabled, unavailable, unconfirmed, or not recorded; text summaries also show
its status. If unavailable, inspect its error and retain the XAML results.
Missing events are not zero composition cost. Older recordings can reveal
CommitMainDevice when the XAML events are already present, but cannot gain
DirectComposition events that were never recorded.
These timings include any waits inside the calls, not just CPU execution. Neither a returned commit call nor a confirmed batch establishes when pixels reached the display. This does not record DXGI presents, DWM's cross-process composition work, GPU execution, displayed FPS, or input-to-display latency.
- Inclusive time includes nested work. Exclusive time subtracts the union of direct child operations. Element self time subtracts nested element work without subtracting the same-object override wrapper twice. Both are elapsed time, including waits and interruptions, not CPU execution time.
- Mean, maximum and nearest-rank p95 use complete calls wholly inside the selected range. Boundary overlaps are clipped and counted separately.
layoutBusyMsByThreadis the interval union of observed layout work on each thread. It is not CPU utilization; summing threads is not global wall time.- Frame phases are not presented frames or FPS. Input and scrolling events show recorded framework work and state, not proven input-to-display latency. Temporal proximity alone does not prove causation.
- Independent compositor scrolling may produce little layout activity. Missing layout or realization events do not prove smooth scrolling or broken virtualization. CPU sampling, kernel waits and GPU attribution are not captured.
- Element IDs and pointers are valid only within this capture. Source, names and parent relationships exist only where emitted; they are not a complete visual tree or a mapping to UI Automation IDs. Attaching after creation often leaves anonymous elements. Re-navigating during capture can improve metadata coverage.
coveragedescribes the selected range/thread and capture-wide collection constraints. Capture-wide decode counts remain visible separately; an error elsewhere is not automatically an error in the selected interval. Optional GC coverage is reported independently of XAML.
capture.json records identity, runtime provenance, limits, markers, process counter samples, actual ETL
filenames, and stop/loss information. The standard trace.etl files are the raw
evidence. Windows tools such as WPA or PerfView may need manifests matching the
recorded WinUI runtime to interpret legacy events.
Offline analysis requires a winapp capture directory, not an arbitrary ETL file.
It creates a disposable analysis cache with manifest.json, events.ndjson,
calls.ndjson, elements.ndjson, and gc.ndjson. Unchanged captures reuse the cache without
decoding ETL again; content fingerprints detect changed inputs. If cache integrity
checks fail, remove only that capture's analysis directory and analyze again.
Recording uses the target PID and native XAML, XAML diagnostics, XAML operational,
and Controls.Perf providers, plus optional CLR GC and DirectComposition events. Controls.Debug, kernel
tracing, CPU sampling, and EventPipe are not enabled. Where Windows rejects a
narrower event-ID filter, PID filtering stays enabled and the corresponding
providerStates entry has eventIdFilterApplied: false.
That fallback can record additional informational GC, composition, or XAML diagnostics data
and increase overhead.
Raw ETL and metadata can contain names, source paths, URIs, resource keys, and messages. Keep captures local and review them before sharing. A filtered query does not sanitize the raw files.
| Problem | Action |
|---|---|
| No usable events | Verify the real WinUI app PID, record again, and exercise navigation or scrolling while recording |
| Runtime not observed | Confirm the target loads Microsoft.UI.Xaml.dll; a launcher, non-WinUI app, or already-exited process is not a supported target |
| Native access error | Use an app running under your own account and inspect the native error; winapp elevates only the startup recorder, and only with run --profile-mode elevated |
| Unsupported descriptors or payloads | Keep the ETL and runtime version for investigation; unsupported events are not decoded using an unrelated installed manifest |
| Partial tail after app exit | Recording is finalized normally; analyze the retained events. For more reliable final-moment coverage, stop recording before closing the app |
| Loss or file cap | Use a shorter focused scenario; target-resident private buffers may be lost at exit |
| Analysis resource limit | Capture a smaller scenario; the report marks incomplete evidence rather than truncating the raw ETL |
| GC unavailable or not observed | Inspect providerStates, managedRuntimes and gcCoverage; preserve XAML results, and do not conclude that no GC occurred |
| Composition unavailable or not observed | Inspect directCompositionProvider and its error; retain XAML evidence and do not infer zero composition or presentation cost |