Skip to content

feat: add WinUI ETW performance recording and analysis - #837

Open
Morten Nielsen (dotMorten) wants to merge 3 commits into
microsoft:mainfrom
dotMorten:dotmorten/perf
Open

Morten Nielsen (dotMorten) wants to merge 3 commits into
microsoft:mainfrom
dotMorten:dotmorten/perf

Conversation

@dotMorten

Copy link
Copy Markdown

Description

Add non-elevated WinUI 3 performance recording and offline analysis to help developers and agents investigate expensive XAML layout, rendering, and scrolling.

  • Add perf start, status, mark, stop, and analyze. Select a running app by PID, process name, or window title, using the same matching rules as winapp ui.
  • Add run --profile across packaged, unpackaged, and execution-alias launch paths. Recording runs in a separate worker, supports detached launches, and finalizes automatically when the target app exits.
  • Keep standard ETL recordings as the source of truth, with disposable NDJSON analysis caches. Queries expose element costs, timed operation trees, slow-frame hotspots, and underlying event evidence.
  • Capture low-volume managed GC events by default and report runtime suspension overlap separately from UI operation timings.
  • Provide readable console output and bounded, pageable JSON for agents, with explicit coverage and partial-data reporting.
  • Include npm wrappers, tests, a performance guide, and a shipped agent skill.

Usage Example

Launch and record a project:

winapp run . --profile .\traces\startup --detach --json

Or record an already-running app and mark the interaction being investigated:

winapp perf start --app MyWinUIApp --output .\traces\scroll --json
winapp perf mark CAPTURE_ID --name scenario-start
# Reproduce the slow scrolling or layout in the app.
winapp perf mark CAPTURE_ID --name scenario-end
winapp perf stop CAPTURE_ID

Replace MyWinUIApp with the app's process name, window title, or PID, and CAPTURE_ID with the id returned by perf start. For run --profile, use the returned Profile.CaptureId. The output directory must be empty.

After recording stops, find expensive work and expand its operation tree:

winapp perf analyze .\traces\scroll --view hotspots --min-frame-ms 16.67
winapp perf analyze .\traces\scroll --view calls --family layout
winapp perf analyze .\traces\scroll --view call --id CALL_ID --depth 3 --json
winapp perf analyze .\traces\scroll --view gc --from-marker scenario-start --to-marker scenario-end --json

Replace CALL_ID with an operation ID returned by the queries. Omit --json for readable console output.

Related Issue

No related issue linked.

Type of Change

  • ✨ New feature
  • 📝 Documentation
  • 🧪 Test update

Checklist

Additional Notes

Recording and interpretation

  • Recording defaults to 30 seconds and 128 MiB. Stopping recording never closes the app. Closing the app finalizes the capture without waiting for the duration limit, although final buffered events or loss counters may be unavailable.
  • run --profile attaches after the PID becomes available; it does not guarantee complete startup coverage.
  • Operation trees describe instrumented ETW scopes, not sampled CPU stacks. GC overlap is contextual evidence, not proof that GC caused a stutter. Missing GC data is reported explicitly.
  • JSON queries default to 10 rows and a 16 KiB response budget. Usable partial results remain on stdout with a nonzero exit status and a structured error on stderr.
  • This does not add graphical charts, CPU/GPU sampling, or privileged system-wide disk/file I/O collection.

Validation

  • Focused C# integration suite: 418 passed, 2 skipped. The skipped cases require disabled Windows long-path support or an explicitly supplied live WinUI test process.
  • npm run/perf forwarding and UI workflow tests: 17 passed.
  • NativeAOT builds and CLI smoke checks completed for x64 and ARM64; npm packaging and generated API/documentation checks completed.
  • MSIX/NuGet packaging and the full repository test suite were not run for this integration pass.

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.

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.
Copilot AI balanced review requested due to automatic review settings September 14, 2026 17:05

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 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 perf lifecycle/analysis commands and run --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.

Comment thread src/winapp-CLI/WinApp.Cli/Services/Performance/PerfAnalysisStore.cs Outdated
Comment thread src/winapp-CLI/WinApp.Cli/Services/Performance/PerfCaptureService.cs Outdated
Comment thread src/winapp-CLI/WinApp.Cli/Services/Performance/PerfCaptureWorker.cs Outdated
Comment thread docs/usage.md

@nmetulev Nikola Metulev (nmetulev) left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🤖 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.

PerfCaptureService.cs:68-70

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.

PerfCaptureService.cs:118-140

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.

PerfQuery.cs:123-126

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.

PerfCommand.cs:131-156

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.

PerfAnalyzer.cs:94-132

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
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants