feat: add WinUI ETW performance recording and analysis - #837
Morten Nielsen (dotMorten) wants to merge 3 commits into
Conversation
Add non-elevated performance diagnostics for investigating expensive XAML layout, rendering, and scrolling, with readable console output and bounded JSON queries for agents. - Add perf start, status, mark, stop, and analyze commands, with app selection by PID, process name, or window title. - Integrate recording with run --profile across packaged, unpackaged, and execution-alias launch paths. - Support detached, duration- and size-limited captures that finalize automatically when the target app exits. - Preserve standard ETL recordings and generate disposable NDJSON caches for querying operation trees, element costs, slow-frame hotspots, event evidence, and managed GC suspension overlap. - Report incomplete coverage and unavailable GC data explicitly, without presenting elapsed timings as CPU usage or overlap as proof of causation. - Add native capture and analysis tests, npm wrappers, and performance documentation and agent guidance.
There was a problem hiding this comment.
🟡 Changes recommended
Malformed IPC can kill captures, orphan recovery can remain stale, and cache cleanup can delete through a junction.
Get a fresh assessment by requesting another Copilot review.
Pull request overview
Adds end-to-end WinUI ETW performance capture, profiling, offline analysis, npm bindings, tests, and guidance.
Changes:
- Adds
perflifecycle/analysis commands andrun --profile. - Implements bounded ETW, WinUI, and GC processing.
- Adds comprehensive tests and user/agent documentation.
File summaries
| File | Description |
|---|---|
src/winapp-npm/test/winapp-commands-run.test.ts |
Tests npm perf/profile forwarding. |
src/winapp-npm/src/winapp-commands.ts |
Adds generated npm APIs. |
src/winapp-npm/scripts/generate-commands.mjs |
Generates required option types. |
src/winapp-CLI/WinApp.Cli/Services/Performance/WinUiEventDecoder.cs |
Decodes WinUI ETW events. |
src/winapp-CLI/WinApp.Cli/Services/Performance/PrivateEtwSession.cs |
Manages private ETW sessions. |
src/winapp-CLI/WinApp.Cli/Services/Performance/PerfGcAnalyzer.cs |
Builds GC intervals. |
src/winapp-CLI/WinApp.Cli/Services/Performance/PerfEtwReader.cs |
Reads native ETL records. |
src/winapp-CLI/WinApp.Cli/Services/Performance/PerfControlChannel.cs |
Implements worker IPC. |
src/winapp-CLI/WinApp.Cli/Services/Performance/PerfCaptureWorker.cs |
Runs capture lifecycle. |
src/winapp-CLI/WinApp.Cli/Services/Performance/PerfCaptureService.cs |
Prepares and controls captures. |
src/winapp-CLI/WinApp.Cli/Services/Performance/PerfCaptureDocument.cs |
Defines capture metadata. |
src/winapp-CLI/WinApp.Cli/Services/Performance/PerfAnalyzer.cs |
Builds elements and operation trees. |
src/winapp-CLI/WinApp.Cli/Services/Performance/PerfAnalysisStore.cs |
Creates verified analysis caches. |
src/winapp-CLI/WinApp.Cli/Services/Performance/ClrGcEventDecoder.cs |
Decodes CLR GC events. |
src/winapp-CLI/WinApp.Cli/Program.cs |
Dispatches workers and JSON errors. |
src/winapp-CLI/WinApp.Cli/NativeMethods.txt |
Adds required ETW APIs. |
src/winapp-CLI/WinApp.Cli/Helpers/HostBuilderExtensions.cs |
Registers performance services/commands. |
src/winapp-CLI/WinApp.Cli/Commands/WinAppRootCommand.cs |
Exposes the perf command. |
src/winapp-CLI/WinApp.Cli/Commands/RunCommand.ProjectMode.cs |
Profiles unpackaged launches. |
src/winapp-CLI/WinApp.Cli/Commands/RunCommand.Performance.cs |
Implements run --profile. |
src/winapp-CLI/WinApp.Cli/Commands/RunCommand.cs |
Integrates profiling across launch paths. |
src/winapp-CLI/WinApp.Cli/Commands/PerfCommand.cs |
Defines perf CLI operations. |
src/winapp-CLI/WinApp.Cli.Tests/TestApps/PerfNativeProbe/Program.cs |
Adds native ETW probe. |
src/winapp-CLI/WinApp.Cli.Tests/TestApps/PerfNativeProbe/PerfNativeProbe.csproj |
Configures NativeAOT probe. |
src/winapp-CLI/WinApp.Cli.Tests/TestApps/PerfGcFixture/Program.cs |
Generates GC activity. |
src/winapp-CLI/WinApp.Cli.Tests/TestApps/PerfGcFixture/PerfGcFixture.csproj |
Configures GC fixture. |
src/winapp-CLI/WinApp.Cli.Tests/RunCommandTests.cs |
Hardens fake launch PIDs. |
src/winapp-CLI/WinApp.Cli.Tests/RunCommandProjectModeTests.cs |
Tests profile launch ordering. |
src/winapp-CLI/WinApp.Cli.Tests/PerfScopeTests.cs |
Tests scope accounting. |
src/winapp-CLI/WinApp.Cli.Tests/PerfGcTests.cs |
Tests GC decoding/intervals. |
src/winapp-CLI/WinApp.Cli.Tests/PerfEtwTests.cs |
Tests native ETW behavior. |
src/winapp-CLI/WinApp.Cli.Tests/PerfCommandTests.cs |
Tests perf CLI contracts. |
src/winapp-CLI/WinApp.Cli.Tests/PerfCaptureCompletionTests.cs |
Tests finalization behavior. |
src/winapp-CLI/WinApp.Cli.Tests/PerfCallQueryTests.cs |
Tests queries, paging, and bounds. |
src/winapp-CLI/WinApp.Cli.Tests/PerfAnalysisTests.cs |
Tests analysis safety/correctness. |
README.md |
Links the performance workflow. |
plugins/winapp/skills/winapp-ui-automation/SKILL.md |
Connects UI automation to profiling. |
plugins/winapp/skills/winapp-troubleshoot/SKILL.md |
Routes performance troubleshooting. |
plugins/winapp/skills/winapp-performance/SKILL.md |
Adds the performance agent workflow. |
docs/usage.md |
Documents perf and profiling entry points. |
docs/guides/winui-performance.md |
Adds the full performance guide. |
docs/debugging.md |
Links performance diagnostics. |
Review details
- Files reviewed: 45/45 changed files
- Comments generated: 4
- Review effort level: Balanced
💡 Configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
Nikola Metulev (nmetulev)
left a comment
There was a problem hiding this comment.
🤖 AI-generated review (winappcli pr-review skill) — verify before acting.
Requesting changes. The feature's scope makes sense, but I would fix the five issues below before merging. They affect whether recordings survive and whether the results can be trusted. None requires expanding this into a general-purpose profiler.
Fix before merging
Cancelling another command also kills the recording helper
Example: Run winapp perf start --app MyApp --output .\capture --duration-sec 120. It returns your terminal prompt while recording continues. Run another command in that terminal, then press Ctrl+C to cancel it. The recording helper also exits, and perf status <id> reports failure.
Why this matters: You intended to cancel the other command, not the recording. Redirecting the helper's input/output makes it quiet but leaves it attached to the same Windows console. Killing the helper is not a clean stop; tracing in the target can remain active without its supervisor.
Proposed fix: Launch the helper without inheriting the terminal's console lifetime. Keep the existing stop, duration, size, and target-exit handling, and verify that the helper still exits through those paths.
Recovery fails if the helper dies and the app then closes
Example: Start recording, terminate its helper, then close the app. perf stop <id> fails because the app's process no longer exists. In the reproduction, the recording file remained on disk, but capture.json still said recording with an empty traceFiles list. perf analyze refused to open it.
Why this matters: A useful recording becomes inaccessible through the CLI. Retrying stop hits the same failure.
Proposed fix: When the original target is gone, still find the saved trace files and finalize the metadata as incomplete, with unknown event loss. Only attempt native session cleanup when the original process identity still matches; do not touch a replacement process that reused its PID.
Missing UI evidence can look like a successful performance report
Example: A capture contains garbage-collection events but no usable UI timing events. perf analyze .\capture --view hotspots --json returns exit code 0, coverage.complete: true, and no rows.
Why this matters: "No UI evidence was recorded" is not the same as "we found no slow frames." The current check accepts GC evidence for a UI query, then the query excludes it. The same check also blocks --view events when retained events have unsupported formats, hiding the diagnostic details that explain the problem.
Proposed fix: Check for evidence appropriate to the requested view. GC alone must not establish usable UI coverage. Let the events view return retained unsupported-event details as partial evidence instead of telling the user to repeat an unhelpful recording.
Dropped events can make two short GC pauses look like one long pause
Example: Suppose the app pauses for garbage collection at 0-3 ms and again at 97-100 ms: 6 ms total. If the first pause's ending and the second pause's earlier boundaries are lost, the remaining events can be paired into one apparently complete 0-100 ms pause.
Why this matters: The report can substantially overestimate GC's overlap with a slow frame. It still describes partial overlap as a "lower bound," even though this reconstruction can be larger than the actual pause time. A general event-loss warning does not make that numerical claim correct.
Proposed fix: Carry known event-loss uncertainty into suspension analysis. Preserve uncertain boundary evidence, but do not use an uncertifiable pairing as a complete pause in guaranteed lower-bound overlap calculations.
PerfGcAnalyzer.cs:106-113 and PerfAnalysisStore.cs:239-251
The console report hides information needed to understand the result
Example: A layout operation spans the selected 5-35 ms range. --view elements --from-ms 5 --to-ms 35 prints count 0 and elapsed/self time 0. The JSON correctly contains one boundary-crossing operation and 30 ms of overlap, but the console does not show those fields.
Following an evidence ID with --view events --event v1 also prints the event name and unknown timings, but omits its timestamp, start/end phase, and recorded values.
Why this matters: A busy interval can look idle, and inspecting an event does not reveal its evidence unless the user switches to JSON.
Proposed fix: Print the existing boundary-overlap count and clipped duration alongside the wholly-contained totals. Render individual events with their timestamp, phase, and available payload fields rather than inapplicable duration columns.
Non-blocking improvements
Element-filtered events lose the template operation's end
Example: An ApplyTemplate call belongs to element e1 and references start/end events v1,v2, but --view events --element e1 returns only v1. The stop event has no element in its payload, although the analyzer already knows which operation it closes.
Why this matters: Element-focused investigation loses closing evidence that the tool has already associated successfully.
Proposed fix: Give a matched stop event the scope's element ID when the stop has no element identity of its own.
The agent skill creates scenario markers but does not use them
Example: Record eleven slow preparation frames, then one shorter frame between the scenario markers. The skill's unfiltered hotspot query returns ten preparation frames and excludes the marked scenario from its first page.
Why this matters: An agent can spend its initial queries and context investigating setup instead of the interaction the user asked about.
Proposed fix: Put --from-marker scenario-start --to-marker scenario-end in the initial query, using the marker names just created, and retain that range for subsequent rankings and GC comparison.
winapp-performance/SKILL.md:16-27
Reproduction scope
The behaviors above were reproduced with published CLI binaries and controlled ETW recordings. The GC-loss case used a deliberately incomplete event sequence and simulated loss metadata; it was not a measurement of a real app's GC pause. Ctrl+C was sent in an isolated console.
NativeAOT publishes succeeded for x64 and ARM64. The targeted C# suite had 302 passes and 2 skips; npm wrapper tests had 11 passes. A live WinUI application's provider coverage and packaged/alias launches were not exercised end-to-end.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: eec84ca0-c432-4ed5-9aaf-ef6c5ce3175f
Description
Add non-elevated WinUI 3 performance recording and offline analysis to help developers and agents investigate expensive XAML layout, rendering, and scrolling.
perf start,status,mark,stop, andanalyze. Select a running app by PID, process name, or window title, using the same matching rules aswinapp ui.run --profileacross packaged, unpackaged, and execution-alias launch paths. Recording runs in a separate worker, supports detached launches, and finalizes automatically when the target app exits.Usage Example
Launch and record a project:
Or record an already-running app and mark the interaction being investigated:
Replace
MyWinUIAppwith the app's process name, window title, or PID, andCAPTURE_IDwith theidreturned byperf start. Forrun --profile, use the returnedProfile.CaptureId. The output directory must be empty.After recording stops, find expensive work and expand its operation tree:
Replace
CALL_IDwith an operation ID returned by the queries. Omit--jsonfor readable console output.Related Issue
No related issue linked.
Type of Change
Checklist
plugins/winapp/skills/Additional Notes
Recording and interpretation
run --profileattaches after the PID becomes available; it does not guarantee complete startup coverage.Validation
AI Description
This section is auto-generated by AI when the PR is opened or updated. To opt out, delete this entire section including the marker comments.