Repository for measuring startup performance, build times, and app sizes of .NET mobile apps using the dotnet/performance tooling. Currently supports Android, with iOS support planned.
- Android device (developer mode enabled) or emulator, visible via
adb devices -l - Python 3 (any supported 3.x version)
- curl (for downloading dotnet-install script and NuGet.config)
- git (for submodule initialization)
-
Clone the repo:
git clone --recurse-submodules https://github.com/ivanpovazan/coreclr-android-perf.git cd ./coreclr-android-perf -
Prepare the environment:
./prepare.sh
This will:
- Install the .NET SDK version pinned in
global.jsoninto.dotnet/ - Install Android and MAUI workloads
- Install the
xharnessCLI tool - Initialize the
dotnet/performancesubmodule - Generate sample apps via
dotnet newtemplates
- Install the .NET SDK version pinned in
-
Run startup measurements:
# Single configuration (android is the default platform) ./measure_startup.sh dotnet-new-android R2R # Explicit platform ./measure_startup.sh dotnet-new-android R2R --platform android # All configurations ./measure_all.sh --startup-iterations 10
-
Inspect results in
results/summary.csvor the console output.
Note: Pass
-ftoprepare.shto force a full reset of the environment.
The .NET SDK version is pinned in global.json. To test against a different SDK build:
- Edit
global.jsonand update theversionfield - Run
./prepare.sh -fto reinstall
Workload versions can be pinned using rollback.json:
./prepare.sh -f -userollback./measure_startup.sh <app> <build-config> [options]Apps: dotnet-new-android, dotnet-new-maui, dotnet-new-maui-samplecontent
Build configs: MONO_JIT, CORECLR_JIT, MONO_AOT, MONO_PAOT, R2R, R2R_COMP, R2R_COMP_PGO
Options:
--platform <android|ios>— Target platform (default:android; iOS coming soon)--startup-iterations N— Number of startup iterations (default: 10)--disable-animations— Disable device animations during measurement--use-fully-drawn-time— Use fully drawn time instead of displayed time--fully-drawn-extra-delay N— Extra delay for fully drawn time (seconds)--trace-perfetto— Capture a perfetto trace after measurements
Results are saved to results/<app>_<config>.trace.
Examples:
# R2R startup of dotnet new android
./measure_startup.sh dotnet-new-android R2R
# Mono JIT startup of MAUI app with animations disabled
./measure_startup.sh dotnet-new-maui MONO_JIT --disable-animations
# R2R Composite with PGO
./measure_startup.sh dotnet-new-maui-samplecontent R2R_COMP_PGO./measure_all.sh [options]Runs measure_startup.sh for all (app, config) combinations and produces a summary table and CSV.
Options:
--platform <name>— Target platform:android,ios(default:android)--app <name>— Measure only this app (can be repeated)--startup-iterations N— Iterations per config (default: 10)
Output: results/summary.csv with columns: app, config, avg_ms, min_ms, max_ms, pkg_size_mb, pkg_size_bytes, iterations.
Examples:
# All configurations with 10 iterations each
./measure_all.sh
# Quick sweep with 3 iterations
./measure_all.sh --startup-iterations 3
# Only Android app, all configs
./measure_all.sh --app dotnet-new-android./android/collect_nettrace.sh <app> <build-config> [options]Collects a .nettrace startup trace for a given (app, build-config) combination. The trace captures detailed runtime events (JIT compilation, assembly loading, GC, exceptions, thread pool, interop) that can be used to analyze startup behavior.
Flow:
- Starts
dotnet-dsrouterto bridge diagnostics from the Android device to the host - Builds and deploys the app with diagnostics enabled (
AndroidEnableProfiler=true) - Runs
dotnet-trace collectagainst the diagnostic port for the specified duration - Cleans up (stops dsrouter, uninstalls app from device)
Options:
--duration N— Trace duration in seconds (default: 60)--force— Re-collect even if a trace already exists--pgo-instrumentation— Include PGO instrumentation env vars for higher-quality traces
Output: traces/<app>_<config>/android-startup.nettrace
The trace directory also contains the build binlog and a logcat.txt dump for diagnostics.
Event providers captured:
Microsoft-Windows-DotNETRuntime— JIT, Loader, GC, Exception, ThreadPool, Interop eventsMicrosoft-Windows-DotNETRuntimePrivate— Additional runtime internals
Analyzing traces:
- PerfView (Windows) — Open the
.nettracefile directly for rich event analysis dotnet-trace convert— Convert to speedscope format (dotnet-trace convert android-startup.nettrace --format Speedscope) and open in speedscope.appdotnet-trace report— Generate summary reports from the command line
Examples:
# Collect a CoreCLR R2R trace with default 60s duration
./android/collect_nettrace.sh dotnet-new-android R2R
# Collect a Mono JIT trace with 30s duration
./android/collect_nettrace.sh dotnet-new-maui MONO_JIT --duration 30
# Re-collect an existing trace with PGO instrumentation
./android/collect_nettrace.sh dotnet-new-maui-samplecontent R2R_COMP_PGO --force --pgo-instrumentation./build.sh <app> <build-config> <build|run> <ntimes> [additional_args]Examples:
# Build dotnet new android with Mono JIT
./build.sh dotnet-new-android MONO_JIT build 1
# Run dotnet new maui with R2R + marshal methods
./build.sh dotnet-new-maui R2R run 1 "-p:AndroidEnableMarshalMethods=true"Build artifacts are copied to ./build/ for further inspection (APKs, binlogs).
./android/print_apk_sizes.sh [-unzipped]Scans the ./build/ directory for signed APKs and prints their sizes. Pass -unzipped to unpack and show extracted sizes.
| Config | Runtime | Description |
|---|---|---|
| MONO_JIT | Mono | Mono with JIT enabled |
| MONO_AOT | Mono | Mono with full AOT |
| MONO_PAOT | Mono | Mono with profile-guided AOT |
| CORECLR_JIT | CoreCLR | CoreCLR with JIT only |
| R2R | CoreCLR | CoreCLR with ReadyToRun |
| R2R_COMP | CoreCLR | CoreCLR with Composite ReadyToRun |
| R2R_COMP_PGO | CoreCLR | CoreCLR with Composite R2R + PGO profiles |
Configurations are defined in Directory.Build.props.
./clean.sh <all|dotnet-new-android|dotnet-new-maui|dotnet-new-maui-samplecontent>├── global.json # SDK version pinning
├── rollback.json # Workload version pinning
├── Directory.Build.props # Shared build configuration presets
├── Directory.Build.targets # Shared build targets
├── init.sh # Common helpers (platform resolution, paths)
├── prepare.sh # Environment setup (SDK, workloads, xharness, apps)
├── generate-apps.sh # Dynamic sample app generation
├── build.sh # Build/run sample apps (--platform aware)
├── measure_startup.sh # Startup measurement (--platform aware)
├── measure_all.sh # Run all configurations (--platform aware)
├── clean.sh # Clean build artifacts
├── dotnet-local.sh # Proxy to local .NET SDK
├── android/ # Android-specific files
│ ├── build-configs.props # Android build configuration presets
│ ├── build-workarounds.targets # Android build workarounds (R2R, etc.)
│ ├── collect_nettrace.sh # .nettrace startup trace collection
│ ├── print_apk_sizes.sh # APK size reporting
│ ├── env.txt # DiagnosticPorts config for profiling
│ └── env-nettrace.txt # PGO instrumentation env vars for trace collection
├── ios/ # iOS-specific files (placeholder)
│ └── README.md # iOS support roadmap
├── profiles/ # Shared PGO .mibc profiles
├── external/performance/ # dotnet/performance submodule
├── apps/ # Generated sample apps (gitignored)
├── .dotnet/ # Local .NET SDK install (gitignored)
├── build/ # Build artifacts (gitignored)
├── traces/ # Collected .nettrace traces (gitignored)
├── results/ # Measurement results (gitignored)
└── tools/ # Tools (dotnet-install.sh, xharness) (gitignored)