Skip to content

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

Merged
Nikola Metulev (nmetulev) merged 39 commits into
microsoft:mainfrom
dotMorten:dotmorten/perf
Oct 13, 2026
Merged

Nikola Metulev (nmetulev) merged 39 commits into
microsoft:mainfrom
dotMorten:dotmorten/perf

Conversation

@dotMorten

Copy link
Copy Markdown
Contributor

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
- isolate suppressed background workers from the invoking console
- validate evidence by query view and preserve unsupported event diagnostics
- avoid certifying GC overlap when ETW event loss is known or unknown
- include boundary overlap and event details in console output
- propagate element identity from matched scopes to stop events
- bound performance analysis workflows with scenario markers
- add regression coverage for each reported issue

@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). Each finding below was checked by running the CLI or reading the code, but it may still be wrong or miss context. Morten Nielsen (@dotMorten), please verify each one yourself before changing anything, and push back where you disagree.

All five issues from the previous review are fixed and covered by tests. Recording works well on real apps: run --profile, perf start/mark/stop, repeated stop, and finalization when the app exits all behaved correctly.

Below are the issues found in this pass, roughly in order of user impact.

1. On real apps, page loads lose their durations and almost every analysis is reported as partial

What is wrong: There are two independent causes. Both come from how WinUI writes these events, and the analyzer's begin/end pairing doesn't handle either one.

  • Component loads are logged twice, and the copies interleave. WinUI writes each component load as a legacy event (ApplicationLoadComponent) and as a newer event (Application_LoadComponent), in this order: legacy begin → new begin → legacy end → new end. The analyzer pairs begin and end events with one stack per thread. When the legacy end arrives, it pops the newer begin as missing-end and marks the outer load corrupt-boundaries. The newer end then becomes missing-begin. Corrupt calls have no duration, and the corruption spreads to ancestors (MeasureElement, MeasureOverride, ParseXaml).
  • Image decodes only have an end event. WinUI defines ImageCacheDecodeBegin (event 145) but never emits it; only TraceImageCacheDecodeEnd is called (dxaml/xcp/components/imaging/ImageCache.cpp). Every decoded image therefore becomes a missing-begin incomplete call.

Show me: Run winapp run AIDevGallery.csproj --arch arm64 --profile .\cap --detach, navigate Samples → Models → AI APIs → Settings → Home between perf mark calls, then perf analyze .\cap --view summary.

  • Event order on the UI thread: v117 ApplicationLoadComponent begin → v118 Application_LoadComponent begin (both at 400.57 ms) → v1429 ApplicationLoadComponent end → v1430 Application_LoadComponent end (both at 581.72 ms).
  • Actual: call c2 (400.57–581.72 ms) is corrupt-boundaries with no duration. Across the capture, 25 component loads covering 573 ms of time have no duration. There are also 21 missing-begin and 21 missing-end new-format loads, 14 MeasureElement and 13 MeasureOverride marked corrupt, and 52 ImageCacheDecode with missing-begin (52 end events, 0 begin events recorded). The analysis exits 1 with "Some UI operations have missing or inconsistent start/end events".
  • The image cause alone is enough. A capture of the one-window samples/winui-unpackaged-app has no interleaved loads, but its single ImageCacheDecode missing-begin still makes every view exit 1.
  • Expected: page loads paired with durations counted toward Parsing, end-only events not treated as broken calls, and exit 0 when nothing is actually missing.

Why it matters: The Parsing figure leaves out page-load time, which is usually what someone investigating slow navigation wants to see. Because nearly every real capture exits nonzero, a partial result can't be told apart from a clean one. The npm perfAnalyze() wrapper also throws (see below).

Smallest fix: In PerfAnalyzer.Accept, when an end arrives, close only the matching begin. Leave unrelated open scopes on the stack instead of popping them as missing-end and marking ancestors corrupt; the two load events then pair independently. Treat ImageCacheDecode end as a point event, or drop it from the scope table. Add a test with the interleaved order above and one with an end-only decode.

PerfAnalyzer.cs:79-110, WinUiEventDecoder.cs:54-55

2. run --profile --on sandbox reports success but records nothing

What is wrong: Both sandbox launch paths return before the recording is prepared or bound, so no recording worker starts. --profile then sees no error and returns the launch's exit code.

Show me:

  • Unpackaged: winapp run . --on sandbox --profile .\cap --detach --json → exit 0, JSON contains "Sandbox": true and no Profile block, .\cap is never created, and the app is running in the sandbox.
  • Packaged (samples/winui-app): the same command without --json prints "Started the application in Windows Sandbox (PID: 4184)" and exits 0, with no warning.
  • Expected: an error that --profile only records on this machine.

Why it matters: A user or agent believes the run was profiled when nothing was recorded. winapp perf start --on sandbox already refuses with "always runs on this machine", so only run --profile slips through.

Fix (author's choice):

  • Support it: record inside the sandbox and bring the capture back to the host, or
  • Fail clearly: in RunCommand.Performance.InvokeAsync, reject --profile when ExecutionTargetSelection.Resolve(parseResult) isn't local, the same way perf start already does, and note in the guide that profiling runs on this machine only.

Either way, add a test.

RunCommand.Performance.cs:54, RunCommand.cs:742, RunCommand.ProjectMode.cs:382

3. "App callbacks" always reports 0 ms

What is wrong: This category counts only the EventCallback event (IDs 15/16). The WinUI manifest marks EventCallbackBegin as notLogged="true", and nothing in the WinUI source emits it.

Show me: In both captures above, the first table shows App callbacks 0.000 0.0%, and events.ndjson has 0 EventCallback events. The AI Dev Gallery capture ran navigation handlers on every page change. Expected: either real callback time or no row.

Why it matters: The report's first table tells users their event handlers cost nothing, while that time actually lands in "Unclassified".

Smallest fix: Remove the "App callbacks" category and the EventCallback decoder entries.

PerfQuery.cs:588-608, WinUiEventDecoder.cs:38-39

4. Two documented commands fail when copied

What is wrong: The guide gives the wrong duration flag for run --profile, and the usage example reads a JSON property that doesn't exist.

Show me:

  • docs/guides/winui-performance.md:10: winapp run . --profile .\cap --duration-sec 60 → Unrecognized arguments: '--duration-sec', '60'. The option is --profile-duration-sec. Line 62, about perf start, is correct.
  • docs/usage.md:2161: winapp perf stop $capture.captureId → Required argument missing for command: 'stop', because perf start --json returns id, not captureId. The recording keeps running until its deadline.

Why it matters: These are the first commands readers copy.

Smallest fix: Use --profile-duration-sec on guide line 10, and $capture.id in usage.md.

5. npm perfAnalyze() throws on partial results

The CLI uses a nonzero exit to mean "partial, stdout still usable", but callWinappCliCapture rejects on any nonzero exit, and npm-usage.md says non-zero throws. The usable result is only reachable through the undocumented error.stdout. Fixing #1 makes this rarer but doesn't remove it. Either resolve for the partial-data exit or document error.stdout. (winapp-cli-utils.ts:242)

6. Truncated resource names lose the part that tells them apart

The parsing table cuts long URIs at the end, so ms-resource:///Files/Controls/HomePage/... appears on two rows with different times, and the sample app shows two identical ms-resource:///Files/Microsoft.UI.Xaml;... rows. Truncate in the middle and keep the file name.

7. Two test fixtures are never built or run

TestApps/PerfNativeProbe and TestApps/PerfGcFixture (about 290 lines) are excluded from the test project and referenced by no test, script, or workflow, so they can break without anyone noticing. Please wire them into a test that builds and runs them against a real capture, so they stay current and catch regressions in native and GC recording.

8. Capture startup by launching the app paused, instead of asking users to edit their app

The guide's "Recording startup layout" section tells users to add a 10-second Thread.Sleep to Main, sometimes by disabling the generated entry point. The new skill says "never modify app code to make capture work", so an agent following both gets conflicting instructions. run --profile can close the startup gap itself: create the process suspended, start the private session for its PID, then resume. The repo already does this in GuestProcessHost (CREATE_SUSPENDED + ResumeThread). For packaged activation, IPackageDebugSettings.EnableDebugging starts the app suspended and passes its PID to a helper command; winapp already uses that interface for termination. This would work for any framework without app changes, and the sleep recipe could then be removed. Please check that the private session captures a process that starts suspended before relying on it.

What was exercised

  • NativeAOT dotnet publish -r win-x64 succeeded. Targeted tests (Perf, RunCommand, AppLauncherService) passed: 460 passed, 2 skipped (the skips need long-path support disabled or a live WinUI PID).
  • Real apps: samples/winui-unpackaged-app (run --profile, perf start/stop) and AI Dev Gallery arm64 (run --profile --detach, perf mark, navigation via winapp ui invoke, perf stop, analyze over the marker range).
  • Sandbox: unpackaged and packaged run --on sandbox --profile.
  • Not exercised: packaged execution-alias launch with --profile, the npm wrapper at runtime, MSIX/NuGet packaging, and the full repository test suite.

Use only modern Application_LoadComponent events for load timings.
Treat end-only ImageCacheDecode events as informational.
Add regression coverage.
Fail run --profile --on sandbox explicitly instead of reporting
success without recording. Add rejection tests and document that
profiling is local-only. Will revisit sandbox support in a future PR.
…ies 15/16.

The guide now clarifies that uninstrumented handler time remains in **Unclassified**, rather than implying callbacks cost zero.

Schema, decoder, and analyzer versions are unchanged. Changes are uncommitted; pausing for your review before moving to item microsoft#4.
- The guide now uses `--profile-duration-sec` for `run --profile`.
- The usage example now stops recording with `$capture.id`.
Preserve evidence, diagnostics, and exit code 1 for partial analysis.
Keep genuine failures rejecting and document coverage handling.
Middle-truncate resource names in summary and parsing tables.
Keep full URIs in JSON output and cover truncation boundaries.
Add real-capture coverage for native payloads and timed GC intervals.
Fix the native probe's EventRegister signature and clean up processes.
Replace post-launch profiling attachment with a debugger-gated startup
sequence that enables ETW providers before application code begins.

- Add architecture-aware entry-point breakpoints and restore instructions
  and thread context before detaching the debugger and resuming execution.
- Support unpackaged launches, packaged activation through a private
  startup helper, and execution aliases.
- Scope package debugging to activation and validate helper ownership,
  package identity, process creation time, and activation thread identity.
- Use credentialed worker status messages for readiness instead of polling
  capture files, avoiding concurrent manifest-read/write failures.
- Terminate owned paused processes on setup failure or cancellation.
- Preserve attach-only coverage when activation reuses an existing process.
- Add native first-event capture, ARM64/x64/WOW64 execution, readiness,
  cancellation, failure cleanup, and control-channel regression coverage.
- Update CLI, npm, guide, and plugin documentation to describe startup
  coverage and the exclusion of earlier DLL initialization and TLS callbacks
Ignore unclassified diagnostic decode errors when tracking operation
boundaries, while retaining conservative handling of malformed known scopes.

Bump the analyzer cache version to rebuild affected captures and add
regression coverage for nested timings and cache invalidation.
Reload the private control registration after the capture worker
disconnects so finalized startup recordings retain their bound target.

Add regression coverage for completed recordings, genuine capture
failures, and mismatched targets in editable capture manifests.
Disable debug-heap initialization for profiled executable and packaged
launches. Reject startup profiling from an emulated x64 CLI on ARM64
Windows before building or launching, with guidance to use the ARM64 CLI.

Add regression coverage and document the architecture requirement.
Per-user ETW sessions accept only process and executable-name filters, so
Windows ignores the event-ID filter for XAML-Diagnostics and the .NET
runtime. During UI-heavy work such as repeated page navigation, events the
analyzer never uses can fill the old 128 MiB default within seconds.

Raise the default size limit for `perf start --max-size-mib` and
`run --profile-max-size-mib` from 128 to 512 MiB. When Windows skips event
filtering, record a warning in capture.json that names the affected
providers and tells users how to recover if recording stops with
`size-limit`.

Update the WinUI performance guide and the regenerated CLI schema, and add
a test for the warning.
Morten Nielsen added 2 commits October 8, 2026 17:13
- Finish a capture as "target-exited" when the ETW session can't be stopped
  (error 87) because the app crashed and Windows Error Reporting is still
  holding it. Previously the recording ended as "failed". Stopping now waits
  up to 30 seconds for the app to exit, in attach mode only.
- Free the private ETW session even when stopping it fails during Dispose.
- Scope elevated captures of packaged apps to the package. The package full
  name now comes from a per-user lookup that works without admin. The shared
  lookup in AppLauncherService only works with admin, so elevated captures
  ran unfiltered and could not identify the app.
- Show a clean error when the user declines the UAC prompt. It no longer has
  a double period or the unrelated note about the worker shutdown deadline.
- Stop repeating capture warnings on every `perf mark`.
- Skill: if the user declines elevation, don't retry; fall back to attach
  mode and say that early startup may be missing.
- The native fixture test expects the wrong cache version
- Failed app identification widens elevated recording to other processes
- Elevated writes use a replaceable output-directory path
@nmetulev

Copy link
Copy Markdown
Member

Tried this on a packaged native C++/WinRT app (ARM64) and it worked well. run --profile was easy to set up, and the parse/layout/image breakdown pointed right at a couple of startup items worth deferring (an off-screen flyout loading its images eagerly, and per-file .xbf parse times).

The question I was chasing was a bit different, though: why the app was heavier (more memory, more threads, more total CPU), not why it was slower. A few things I ended up needing other tools for. Sharing in case any of them are useful; totally your call whether they fit here, in a follow-up, or not at all:

  • CPU outside the UI thread. UI-thread time was basically flat between the two builds I compared, but total process CPU was ~20% higher, spread across the in-process compositor (dwmcorei), thread pool, and GPU driver threads. A per-thread or per-module CPU summary (ETW CPU sampling, or even thread cycle times grouped by start-address module) would have shown that directly.
  • Who created threads and D3D devices. The fixed overhead turned out to be an extra D3D11 device created by the in-process compositor, plus a few short-lived devices from adapter capability probing (CheckInterfaceSupport). On the driver I was using, each device brought ~20 threads and ~12 MB. Thread-create stacks, or D3D11/DXGI provider events for device creation, would make that kind of thing visible.
  • Memory / thread / handle counts. A simple timeline or end-of-run summary of private bytes, thread count and handle count would help when the question is about resource cost rather than latency.
  • Present / frame events. DXGI and DComp present events would help separate XAML work from composition and presentation time.
  • Comparing two captures. A diff of two recordings of the same scenario would make before/after or A/B checks easier. My baseline was on a different UI stack that the recorder doesn't capture, so I fell back to debugger traces and thread cycle counts there. Not sure that's in scope.

Happy to share more details on any of these if helpful.

Morten Nielsen and others added 3 commits October 9, 2026 11:45
user/kernel/total CPU, private bytes, working set, thread count, and handle count. Captures sample approximately once per second; the default analysis shows a first/last/observed-maximum summary.
…dence

Extend `winapp perf` to capture early startup and help distinguish expensive XAML work from process-wide resource costs and app-side composition submission.


**Elevated startup recording**

- Add `--profile-mode elevated` to start recording before app launch, while keeping the app unelevated.

- Restrict collection to the identified app package or executable. Failed app identification stops recording setup rather than widening collection to other processes.

- Protect output and control directories, including their ancestors, against replacement or redirection while the elevated recorder is active.

- Report when activation reuses an existing process, so attachment is not mistaken for startup coverage.



**Process-resource evidence**

- Automatically sample user/kernel CPU, private bytes, working set, thread count, and handle count.

- Include resource summaries in the default report and expose a paged `--view resources` timeline with marker and timestamp filtering.

- Report CPU across all target-process threads, helping identify increased resource cost even when UI-thread timings remain unchanged.

- Keep sampling failures explicit and allow resource queries without usable XAML events.

**Composition evidence**

- Decode WinUI `CommitMainDevice` timing and optionally collect DirectComposition surface-update calls: `DCompBeginDraw`, `DCompEndDraw`, and `DCompUpdateToken`.

- Expose `DCompDeviceCommit` notifications with device/channel and batch identifiers.

- Add the `composition` call family and a separate **Composition submission** category in the default UI-thread activity summary.

- Report optional-provider availability without discarding usable XAML evidence.


Update CLI documentation, the performance guide, shipped performance skill, and native/npm regression coverage. Schema, decoder, and analyzer versions remain **1**.

## Usage Example

PowerShell


```
# Record startup before launch; Windows prompts to elevate only the recorder.
winapp run . --profile .\traces\startup --profile-mode elevated --detach

# Inspect the recording after it finishes.
winapp perf analyze .\traces\startup

# Compare process-wide CPU, memory, thread, and handle counters.
winapp perf analyze .\traces\startup --view resources --json

# Inspect app-side composition timings and commit notifications.
winapp perf analyze .\traces\startup --view calls --family composition --json
winapp perf analyze .\traces\startup --view events --event DCompDeviceCommit --json

```



## Related Issue


No linked issue.


## Type of Change

- 🐛 Bug fix
- ✨ New feature
- 📝 Documentation
- 🧪 Test update

## Checklist

- New tests added for new functionality (if applicable)
- Tested locally on Windows
- Main README.md updated (if applicable) — not applicable
- docs/usage.md updated (if CLI commands changed)
- Language-specific guides updated (if applicable)
- Sample projects updated to reflect changes (if applicable) — not applicable
- Shipped skills updated in `plugins/winapp/skills/` (if CLI commands/workflows changed)



## Screenshots / Demo


A real WinUI capture using the published x64 CLI recorded **855 timed composition calls**, including **45 main-device commits**, plus **120 DirectComposition commit notifications**, with no decode errors or incomplete timed calls.


This verifies event collection and decoding; it is not a performance-improvement benchmark.


## Additional Notes


### Validation



- Focused performance suite: **210 passed, 1 optional live-provider test skipped**.

- npm suite: **324 passed**.

- After restoring decoder version 1, the NativeAOT fixture/cache regression was rerun: **1 passed**.

- Native x64 and ARM64 builds passed, including a fresh `.\scripts\build-cli.ps1 -SkipAll` after the version correction.

- `npm run build` passed; `npm run build-copy-only` refreshed npm binaries after the final native build.

- Schema-aware plugin validation passed.

- Runtime composition evidence was exercised on x64; ARM64 was built but not tested on ARM64 hardware.



**Full-suite validation remains incomplete:** an earlier complete CLI run hung in `PackagedSandboxMutationLockTests.ConcurrentPackagedSandboxRuns_SameDeployment_SerializeRegistrationNotLaunch`. The focused results above do not establish that every repository test passes.


### Interpretation limits


Process counters cover the target process, not child processes or individual threads/modules. Sampling begins after process binding, runs approximately once per second, and can miss brief spikes. CPU summaries use actual retained samples without interpolating range boundaries; fewer than two samples means interval CPU is unavailable.


Composition timings measure app-side elapsed API time, including possible waits. They do **not** measure GPU execution, DWM processing, displayed frames, FPS, or display latency. Commit notifications are not treated as timed calls or presented frames. This PR does not enable DXGI Present collection or add frame-presentation correlation.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

Copilot-Session: 3e28624e-44d2-4095-8dca-04fb49e8bc93

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

Reviewed at fb8dfbab. Recording and analysis worked well on AI Dev Gallery (a large packaged WinUI app) in both attach and elevated mode. The fixes for overlapping load events, the removed "App callbacks" category, the --on sandbox rejection, the docs corrections and the wired-up fixtures all check out.

Must fix

1. The elevated recorder writes through paths an unelevated process can change

  • What is wrong: With --profile-mode elevated, the admin-level recorder writes <output>\trace.etl into a directory that the normal user can still write to. It doesn't reserve the file or reject a link first. The directory lease only stops the folder from being renamed (FileShare.ReadWrite), so new files can still be created inside it. The recorder also reads its settings from control.json after the UAC prompt, and that file sits in a folder where the user has FullControl. Any process that can read control.json can then send arm over the pipe with an executable name of its choice.
  • Show me (reproduced): Developer Mode allows unprivileged symlinks.
    1. An unelevated background job waited for .capture-owner to appear in the output directory.
    2. The job created trace.etl as a symlink to %TEMP%\victim.txt.
    3. I ran winapp run AIDevGallery.csproj --profile <dir> --profile-mode elevated --detach and approved UAC.
    • Actual: the victim file was overwritten with 15 MB of ETL data. The size limit didn't apply either, because the size check measures the 0-byte symlink.
    • Expected: the recorder refuses a link or a file that already exists.
    • The control.json swap was identified by reading the code; I didn't run it.
  • Why it matters: A process running as the user can turn the user's approval of a legitimate winapp UAC prompt into an admin-level overwrite of any file, such as one under C:\Windows. Most winapp users have Developer Mode enabled.
  • Smallest fix: Either option works:
    • (a) Have the elevated recorder create its ETL inside a directory it owns that normal-integrity processes can't write to, then publish the file after it stops. Pass the scope, limits and output directory to it as command-line arguments set before UAC, not through mutable control.json or pipe messages.
    • (b) Move elevated mode to a follow-up PR. In my runs, attach mode already bound to AI Dev Gallery at about 0.09 ms (6 threads, 3.3 MiB private). It recorded App.xbf (544 ms), MainWindow.xbf and HomePage.xbf parsing, so elevated mode added little for this app.
  • Location: PerfCaptureService.cs:31-114, PerfCaptureDirectoryLease.cs:39, PerfCaptureWorker.cs:60-64, 190-197, 361, PrivateEtwSession.cs:100-104

2. Attach captures of a real app always report partial results because of one undecodable diagnostic event

  • What is wrong: WinUI's FallbackError event is an error-logging event, not a timing event. Windows' event decoder (TDH) can't parse it, and that one failure marks the whole analysis incomplete.
  • Show me: winapp perf analyze on 3 of 3 attach-mode AI Dev Gallery captures (startup, and navigation between markers).
    • coverage.incompleteCalls: 0, but decodeErrors: 2-6, all FallbackError | Unsupported structured or variable-length TDH property.
    • The tool prints "Results may be incomplete" and exits 1.
    • Expected: exit 0, because nothing timing-related is missing.
  • Why it matters: Scripts and agents treat exit 1 as failure. If the exit code is nonzero on almost every real capture, it can no longer signal real data loss.
  • Smallest fix: Keep known diagnostic events like FallbackError that don't affect timing, but don't count them as missing coverage (or decode their struct). Unsupported begin and end events should still count.
  • Location: WinUiEventDecoder.cs:219, PerfQuery.cs:227-229, 378-380, PerfCommand.cs:272

Non-blocking

The winapp skill router doesn't send performance requests to the new skill

  • What is wrong: plugins/winapp/com.github.copilot/agents/winapp.agent.md has no row for winapp-performance. Its rule 3, "Ask before admin-only actions", doesn't mention --profile-mode elevated.
  • Smallest fix: Add a row for slow WinUI layout, scrolling, rendering or GC investigations that points to winapp-performance, and list elevated profiling in rule 3.

What was exercised

  • Build: NativeAOT dotnet publish -c Release -r win-x64.
  • Attach-mode launch profiling: winapp run … --profile … --detach --json reached completed / duration. The trace was 37 MB for 30 s, and analysis took about 4.5 s.
  • Attach with markers: perf start, mark, 5 page navigations with winapp ui invoke, mark, stop. Analysis of the marked range worked.
  • App exit during capture: stop reason target-exited, and the retained events still analyzed. The app's crash also happens without profiling, so it isn't related to this PR.
  • Elevated mode: coverage was recorded from launch (elevated session), analysis exited 0, and no WinApp-Perf session was left behind.
  • Sandbox: --on sandbox --profile was rejected before build.
  • Tests: targeted perf tests passed (analysis/resource/completion, ETW/lease, profile run modes). validate-plugin-package.ps1 and validate-llm-docs.ps1 passed.
  • Not exercised: arm64, the npm test suite, and the full CLI suite.

Morten Nielsen added 3 commits October 12, 2026 15:27
…e 3 requires asking before  --profile-mode elevated .
…er, which checks the pipe’s owner before sending any control request or credential.

The previous  CurrentUserOnly  option checks against  WindowsIdentity.Owner . For an elevated process, that can be the Administrators group, whereas our worker deliberately makes the pipe belong to  WindowsIdentity.User —the actual user’s SID. That mismatch caused the CI failures.

The helper explicitly compares the pipe owner with the user’s SID instead. It still rejects pipes owned by another user or by Administrators, and disposes the connection on failure. The normal controller and malformed-request tests use the same check. The server’s access permissions and request authentication are unchanged.
Analyze only complete, usable WinUI operation pairs. Keep unmatched events
as raw evidence without invalidating unrelated timings or reporting
missing-boundary failures.

Preserve matched parent durations while leaving unreliable self/exclusive
times unavailable. Reparent complete descendants past omitted scopes and
reject captures with no usable timings instead of reporting zero cost.

Classify FallbackError as diagnostic evidence so unsupported payloads do
not make timing queries partial. Preserve event-query decode errors,
recording-loss warnings, and existing GC coverage behavior.
ETW can deliver the enable notification to an in-process EventSource after EnableTraceEx2 returns, so the immediate IsEnabled assertion failed on CI.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
.NET 10 only enables an EventSource for ETW sessions that EnumerateTraceGuidsEx reports for the process. The CI runner does not report the process-private session, so the in-process EventSource never enabled and the diagnostic test wrote no events. Write the same self-describing events through EventWrite instead.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
@nmetulev
Nikola Metulev (nmetulev) merged commit ff06032 into microsoft:main Oct 13, 2026
38 checks passed
Nikola Metulev (nmetulev) added a commit that referenced this pull request Oct 16, 2026
Places main's new winapp-ui-automation facts in the core (scoped queries without a
selector, exact-name matching, click or hover at a point, horizontal wheel in the
exclusive list, performance pointer) or in references (touch --angle and horizontal
wheel examples in gestures.md, restoring a minimized window in dialogs-and-state.md).
The winapp run output change lands in winapp-identity's run-project-mode.md.

Also fixes what #837 broke in the benchmark: winapp-performance maps to winui.build in
a new current map (the old map stays for earlier results), and its description says
'crash analysis' instead of 'crash dumps', which collided with a demand fixture name.
Cross-skill relative links to winapp-performance now name the skill.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Nikola Metulev (nmetulev) added a commit that referenced this pull request Oct 16, 2026
…enchmarks/ui (#1053)

## What and why

Adds `benchmarks/ui/`, two repeatable benchmarks for `winapp ui`. Rerun
them after a `winapp ui` change to see its effect.

- **`run.ps1` (agent benchmark).** A Copilot CLI agent does seven
natural-language tasks in inbox Calculator and a pinned WinUI 3 Gallery
(`v2.9.3`, built once by `setup-gallery.ps1`). The tasks include
real-world friction: the app isn't running, two instances are open, the
window is minimized, a dialog was left open, and a cold start. The
harness scores the final app state itself through UIA, the process count
and the window state. It never trusts the agent's answer, and it flags
collateral damage such as extra instances, the wrong window changed, or
an app closed. The report shows the pass rate, wall time, turns, tool vs
model time, tokens and credits. Methods: `winapp` (this repo's plugin
and CLI) and `none` (no plugin, `winapp` hidden).
- **`perf.ps1` (scripted, no agent, no credits).** It times single
`winapp ui` commands (20 runs after a warm-up; median, p95 and max). It
also runs fixed command sequences that mirror the agent tasks, scored
with the same checks.

Both benchmarks refuse to start when Calculator or the Gallery is
already open (unless you pass `-Force`), close only the processes they
launched (by PID), and restore your Calculator mode afterward. `run.ps1`
resumes when you rerun it into the same `-OutDir`. Other tools can be
benchmarked locally with `-Method <file>` or `-Adapter <file>`; the
README documents both formats.

`benchmarks/agents/lib/Benchmark.psm1` is reused, not copied. Its only
change is additive: `Read-SessionEvents` now also returns tool time, API
time and session duration, and `Format-Count` is exported.

## Before / after

New scripts; nothing in `winapp` itself changes. Observed output of
`perf.ps1` on current `main` (x64 Xeon, NativeAOT build of `901e4cde`,
Calculator only; excerpt):

```text
> .\benchmarks\ui\perf.ps1 -Operation startup,list-windows,search,inspect-calculator,get-property,get-value,invoke,wait-for,focus,screenshot -Sequence calc-add,calc-two-instances,calc-minimized-scientific
  startup              winapp     median     91 ms  p95     93 ms  errors 0
  list-windows         winapp     median    102 ms  p95    105 ms  errors 0
  search               winapp     median    259 ms  p95    267 ms  errors 0
  get-value            winapp     median    144 ms  p95    158 ms  errors 0
  invoke               winapp     median    177 ms  p95    183 ms  errors 0
  focus                winapp     median    176 ms  p95    182 ms  errors 0
[1/3] calc-add | winapp ... pass | 1,914 ms | 10 commands
[1/3] calc-two-instances | winapp ... pass | 1,383 ms | 7 commands
[1/3] calc-minimized-scientific | winapp ... pass | 1,993 ms | 10 commands
```

Agent benchmark, first full run on 2026-10-09: branch `93b2cbb8` on main
`a87a8304`, ARM64 Snapdragon X2 Elite, Copilot CLI 1.0.93,
`claude-sonnet-5.5`, 5 iterations. **70/70 passed with no collateral
damage**, in 32 minutes and 497.5 credits.

| Method | Median wall time | Median turns | Credits (35 runs) |
|---|---|---|---|
| `winapp` | 19.4 s | 6 | 315.7 |
| `none` | 21.8 s | 5 | 181.8 |

`winapp` was 20–30% faster on the Gallery tasks, about even on simple
Calculator tasks, and slower on the minimized-window task (36.9 s vs
15.0 s). It used about 1.7× the credits, mostly to load the
UI-automation skill.

## Not tested / risks

- The agent numbers above predate several `main` changes and were not
re-run: minimized-window handling in `winapp ui` (#1054), the
`winapp-performance` skill (#837), and selector changes (#1102, #1104).
The `gallery-dialog-left-open` task also changed since then. It now asks
to turn the switch off, because the switch starts on. The
minimized-window result in particular is likely better now, because
`perf.ps1`'s `calc-minimized-scientific` sequence, which failed at its
first step on 10/9, passes 3/3 on current `main`.
- I did not re-run the Gallery perf sequences here, because the Gallery
is not installed on this machine.
- Neither benchmark runs in CI: both drive the desktop, and the agent
benchmark costs credits. Only the Pester suites are suitable for
automation.

## Checklist

- [x] Tests added or updated
- [x] Tested locally on Windows

<details><summary>Notes for reviewers</summary>

Validated on the merged head:
- `Invoke-Pester benchmarks\ui\tests`: 135/135 passed.
- `Invoke-Pester benchmarks\tasks\tests`: 54/54 passed.
- `Invoke-Pester benchmarks\agents\tests`: 157 passed, 2 failed. `main`
has the same 2 failures from the `winapp-performance` skill, and #1060
fixes them.
- `benchmarks\agents\run.ps1 -Lint` and `-Plan` give the same output as
`main`: 1 error, which is the same known issue, and 1,584 sessions.
- `benchmarks\ui\run.ps1 -Plan`, `run.ps1 -Lint` (0 errors) and
`perf.ps1 -Plan` all succeed.

</details>

---------

Co-authored-by: Nikola Metulev <711864+nmetulev@users.noreply.github.com>
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Zach Teutsch <88554871+zateutsch@users.noreply.github.com>
Copilot-Session: 0cf8e7b3-7171-42da-9482-89167c7668b8
Nikola Metulev (nmetulev) added a commit that referenced this pull request Oct 16, 2026
)

## What and why

The agent benchmark is broken on main since `winapp-performance` (#837)
and `winui-devtools` (#943) shipped:

- `benchmarks\agents\run.ps1 -Lint` reports 1 error, and benchmark
Pester has 2 failures.
- No capability map matches the current 14 winapp skills, so runs with
the current plugin can't be scored.
- `winapp perf ...` commands aren't recognized by the command extractor.

This PR:

- Adds a current `winapp` map for all 14 skills. `winapp-performance`
maps to no capability for now: no scenario measures runtime performance,
and crediting it as `winui.build` would let it pass unrelated build and
crash scenarios. `winapp perf` commands are recorded but count toward
nothing.
- Keeps the previous maps so older results still score: 13 skills
without `winapp-performance`, 13 without `winui-devtools`, and the
original 12.
- **User-visible:** changes one word in the `winapp-performance` skill
description, "not crash dumps" → "not crash analysis". A held-out
benchmark prompt mentions a "crash dump", so the old wording leaked into
routing and failed lint. The meaning is the same.

## Before / after

**Before (main)**
```
> .\benchmarks\agents\run.ps1 -Lint
error   demand c04-winui-hot-reload-toolbar-av   leak-bigram   shares 'crash dump' with skill winapp-performance
Linted 258 prompts: 1 errors, 54 warnings.

> Invoke-Pester benchmarks\agents\tests, benchmarks\tasks\tests, benchmarks\ui\tests
Passed 346 Failed 2
```

**After**
```
Linted 258 prompts: 0 errors, 54 warnings.
Passed 350 Failed 0
```

## Not tested / risks

No live agent runs. Validation covers lint, Pester, `-Plan`, and the
plugin validator.

## Checklist

- [x] Tests added or updated (they fail without the fix)
- [x] Tested locally on Windows
- [x] Docs, samples, and `plugins/winapp/skills/` updated if user-facing
behavior changed

<details><summary>Notes for reviewers</summary>

Commands run on this branch:
- `.\benchmarks\agents\run.ps1 -Lint`: 0 errors
- `Invoke-Pester -Path
.\benchmarks\agents\tests,.\benchmarks\tasks\tests,.\benchmarks\ui\tests`:
350 passed, 0 failed
- `.\benchmarks\agents\run.ps1 -Plan`: runs (1710 sessions)
- `.\scripts\validate-plugin-package.ps1`: passed

Skill-set hashes (computed on main's skill names): current 14 =
`e20195ae2f87`; without performance = `ea21288d7949` (main's former
current map); without devtools = `93d52ca8d877`; without both =
`4a4abc8b6541`.

Giving `winapp-performance` its own capability would need at least two
held-out scenarios (a test requires that for every capability), so it's
left for a follow-up. Ported from the benchmark part of draft #1060
only; none of its skill splits are included.

</details>

---------

Co-authored-by: Nikola Metulev <711864+nmetulev@users.noreply.github.com>
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 72865eb6-685a-4ede-8516-17cfe8abcc86
@dotMorten
Morten Nielsen (dotMorten) deleted the dotmorten/perf branch October 16, 2026 16:48
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.

4 participants