Skip to content

feat: add maui profile command for Android startup tracing#60

Draft
simonrozsival wants to merge 3 commits intodotnet:mainfrom
simonrozsival:feature/maui-profile-command
Draft

feat: add maui profile command for Android startup tracing#60
simonrozsival wants to merge 3 commits intodotnet:mainfrom
simonrozsival:feature/maui-profile-command

Conversation

@simonrozsival
Copy link
Copy Markdown
Member

Closes #54

Summary

Adds a new maui profile command that collects a startup .nettrace for a .NET MAUI Android app using dotnet-trace + dotnet-dsrouter, both invoked via dnx so no pre-installation is required.

How it works

Two-phase build approach:

  1. Phase 1 — compile + package the APK (dotnet build)
  2. Phase 2 — start dsrouter, start dotnet-trace (waiting for connection), then deploy + launch (dotnet build -t:Run with DiagnosticSuspend=true)

Tool invocation via dnx:

  • dnx -y dotnet-dsrouter -- android-emu (or android for physical devices)
  • dsrouter prints pid=<N> to stdout on startup — we parse that to get the PID
  • dnx -y dotnet-trace -- collect --process-id <pid>
  • No dotnet tool install required; always uses the latest published version

Auto-stop support via Microsoft.Maui.StartupProfiling:

  • New opt-in NuGet helper package
  • Add a ProjectReference, call StartupProfilingMarker.Complete() at logical startup end
  • Uses [ModuleInitializer] + EventSource (Microsoft.Maui.StartupProfiling, event StartupComplete)
  • Pass --stopping-event-provider-name Microsoft.Maui.StartupProfiling --stopping-event-event-name StartupComplete to auto-stop the trace

Key technical notes

  • --stopping-event-provider-name only configures the stop condition — the provider must also be in --providers for EventPipe to actually deliver events. The command handles this automatically.
  • cpu-sampling profile is Linux-only in dotnet-trace 9+; dotnet-common,dotnet-sampled-thread-time is the EventPipe equivalent that works with dsrouter.
  • Any --providers arg suppresses dotnet-trace's implicit defaults, so we explicitly inject dotnet-common,dotnet-sampled-thread-time when a stopping event is configured.

Testing

dotnet run --project src/Cli/Microsoft.Maui.Cli -- profile \
  --project /path/to/MyMauiApp \
  -f net10.0-android \
  --stopping-event-provider-name Microsoft.Maui.StartupProfiling \
  --stopping-event-event-name StartupComplete

Open the resulting .nettrace in PerfView or Visual Studio Performance Profiler.

simonrozsival and others added 3 commits April 2, 2026 18:49
Adds a new top-level `maui profile` command that collects a startup
.nettrace for a .NET MAUI Android app using dotnet-trace + dotnet-dsrouter.

## What's new

### `maui profile` command (ProfileCommand.cs)
- Two-phase build: compile-only phase 1, then deploy+launch in phase 2
  so the APK is built once and the trace collector starts before the app.
- Starts dotnet-trace with `--dsrouter android-emu` (emulator) or
  `--dsrouter android` (physical device) for startup tracing.
- Passes `DiagnosticSuspend=true` and `--resume-runtime` so the
  beginning of app startup is never missed.
- Supports `--stopping-event-provider-name` / `--stopping-event-event-name`
  for automatic stop when the app signals startup is complete.
- Automatically injects `dotnet-common,dotnet-sampled-thread-time` profiles
  alongside the stopping event provider so runtime and CPU-sampling events
  are still collected (specifying `--providers` alone would suppress the
  dotnet-trace defaults).
- Clears inherited MSBuild SDK env vars before spawning child builds to
  prevent SDK version pollution when invoked via `dotnet run`.
- Resolves macOS `/tmp` symlinks to avoid MAUI SourceGen MAUIG1001 errors.

### `Microsoft.Maui.StartupProfiling` helper package
- Tiny opt-in assembly: add the NuGet reference and call
  `StartupProfilingMarker.Complete()` at the logical end of startup.
- `[ModuleInitializer]` pre-registers the EventSource provider so
  dotnet-trace sees it before any user code runs.
- `MAUI_STARTUP_PROFILING_AUTO_EXIT=1` env var causes the app to call
  `Environment.Exit(0)` after emitting the marker — useful in CI.
- `IsProfilingSession` property lets apps skip work that skews traces.

### ProcessRunner changes
- Added `environmentVariablesToRemove` parameter to strip inherited env
  vars before spawning subprocesses.

### Diagnostic improvements
- `SpectreOutputFormatter` now shows native error output in error messages.
- New error codes for prerequisite and config validation failures.

## Key technical notes
- `--profile cpu-sampling` is incompatible with `--dsrouter` (Linux kernel
  perf only); `dotnet-sampled-thread-time` uses EventPipe and works.
- `--stopping-event-provider-name` configures the stop condition but does
  NOT add the provider to the EventPipe session; `--providers` must also
  include it explicitly for events to be delivered.
- Default collection (dotnet-common + dotnet-sampled-thread-time) must be
  injected explicitly whenever `--providers` is specified, because any
  `--providers` arg suppresses the implicit defaults.

Closes dotnet#54

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…SDK)

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
eng/Common.props packs the repo-root README into every package at '/README.md'.
The StartupProfiling project also includes its own README at the same path,
causing NU5118 (treated as error in CI via --ci). Suppress NU5118 so the
package-specific README is used.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
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.

[Proposal] Add startup profiling capabilities

1 participant