Note
Access to this page requires authorization. You can try signing in or changing directories.
Access to this page requires authorization. You can try changing directories.
Performance profiling is the process of measuring the performance of an application to identify areas for improvement. .NET MAUI and client applications, in general, are interested in:
- Startup time: The time it takes for the application to start and display the first screen.
- CPU usage: If specific methods are consuming too much CPU time: either through many calls or long-running operations.
- Memory usage: If many allocations are made beyond reason or if there are memory leaks.
The techniques and tools for improving these metrics are different, which we plan to demystify in this guide. The tools used to profile .NET MAUI applications can also vary depending on the platform. This guide covers Android, iOS, Mac Catalyst, and Windows profiling approaches.
Important
Always profile Release builds for accurate performance
measurements. Debug builds use the interpreter (UseInterpreter=true)
for C# hot reload support, which significantly impacts performance
and produces unrealistic results.
Prerequisites
Installing Diagnostic Tools
To profile .NET MAUI applications on iOS and Android, you need to install the following .NET global tools:
dotnet-trace- Collects CPU traces and performance datadotnet-dsrouter- Forwards diagnostic connections from remote devices to your local machinedotnet-gcdump- Collects memory dumps for analyzing managed memory usage
You can install these tools using the following commands:
$ dotnet tool install -g dotnet-trace
You can invoke the tool using the following command: dotnet-trace
Tool 'dotnet-trace' was successfully installed.
$ dotnet tool install -g dotnet-dsrouter
You can invoke the tool using the following command: dotnet-dsrouter
Tool 'dotnet-dsrouter' was successfully installed.
$ dotnet tool install -g dotnet-gcdump
You can invoke the tool using the following command: dotnet-gcdump
Tool 'dotnet-gcdump' was successfully installed.
Note
You need at least version 9.0.652701 of all the diagnostic tools to use the features described in this guide. Check dotnet-trace, dotnet-dsrouter, and dotnet-gcdump on NuGet for the latest versions.
Starting with version 9.0.652701, both dotnet-trace and
dotnet-gcdump include a --dsrouter option that automatically
launches and manages dotnet-dsrouter as a subprocess. This
eliminates the need to run dotnet-dsrouter separately, significantly
simplifying the profiling workflow.
See the .NET Conf session, .NET Diagnostic Tooling with AI, for a live demo of using these tools.
How the Tools Work Together
To use these diagnostic tools on iOS and Android, several components work together:
- The .NET global tools (
dotnet-trace,dotnet-gcdump,dotnet-dsrouter) run on your development machine - The Mono diagnostic component
(
libmono-component-diagnostics_tracing.so) is included in your application package dotnet-dsrouterforwards the diagnostic connection from the remote device or emulator to a local port on your machine- The diagnostic tools connect to this local port to collect profiling data
The --dsrouter option in dotnet-trace and dotnet-gcdump
automatically handles the complexity of starting dotnet-dsrouter and
coordinating the connection.
Building Your Application for Profiling
To enable profiling, your application must be built with special MSBuild properties that include the diagnostic components and configure the connection to the profiling tools.
Understanding Diagnostic Properties
The following MSBuild properties control how your application communicates with the diagnostic tools:
DiagnosticAddress: The IP address wheredotnet-dsrouteris listening. Use10.0.2.2for Android emulators (this is the host machine's loopback address from the emulator's perspective), and127.0.0.1for physical devices and iOS.DiagnosticPort: The port number for the diagnostic connection (default is9000).DiagnosticSuspend: Whentrue, the application waits for the profiler to connect before starting. Whenfalse, the application starts immediately and the profiler can connect later. Usetruefor startup profiling,falsefor runtime profiling and memory dumps.DiagnosticListenMode: Set toconnectfor Android (the app connects todotnet-dsrouter), orlistenfor iOS (the app listens fordotnet-dsrouterto connect to it).EnableDiagnostics: Whentrue, includes the Mono diagnostic component in the application package. This is implicitly set when setting any of theDiagnostic*MSBuild properties. This property works on Android, iOS, and Mac Catalyst.
Note
When using CoreCLR (currently experimental on Android, with iOS
support planned), the diagnostic component is built into the runtime
and EnableDiagnostics is not required.
Build Command Examples
When you run dotnet-trace or dotnet-gcdump with the --dsrouter
option, the tool displays instructions for building your application.
For example:
For Android emulators:
dotnet build -t:Run -c Release -f net10.0-android -p:DiagnosticAddress=10.0.2.2 -p:DiagnosticPort=9000 -p:DiagnosticSuspend=false -p:DiagnosticListenMode=connect
For Android devices:
dotnet build -t:Run -c Release -f net10.0-android -p:DiagnosticAddress=127.0.0.1 -p:DiagnosticPort=9000 -p:DiagnosticSuspend=false -p:DiagnosticListenMode=connect
For iOS devices and simulators:
dotnet build -t:Run -c Release -f net10.0-ios -p:DiagnosticAddress=127.0.0.1 -p:DiagnosticPort=9000 -p:DiagnosticSuspend=false -p:DiagnosticListenMode=listen
Note
Use -f net10.0-android or -f net10.0-ios for projects with
multiple target frameworks in $(TargetFrameworks).
Important
Applications built with these diagnostic properties should only be used for development and testing. Never release builds with diagnostic components enabled to production, as they can expose endpoints with deeper insights into your application's code.
Profiling CPU Usage
The dotnet-trace tool collects CPU sampling information in formats
like .nettrace and .speedscope.json. These traces show you the
time spent in each method, helping you identify performance
bottlenecks in your application.
The workflow for CPU profiling depends on whether you're measuring
startup time or profiling runtime operations. The key difference is
the -p:DiagnosticSuspend MSBuild property.
Profiling Startup Time
To capture accurate startup time measurements, suspend application startup until the profiler is ready. This ensures you capture the entire startup sequence from the very beginning.
In one terminal, start
dotnet-tracewith the--dsrouteroption:dotnet-trace collect --dsrouter android-emu --format speedscopeOr for a physical Android device:
dotnet-trace collect --dsrouter android --format speedscopeFor iOS devices and simulators, use
--dsrouter iosor--dsrouter ios-simrespectively.In another terminal, build and deploy your application with
-p:DiagnosticSuspend=trueto pause at startup:For Android emulators:
dotnet build -t:Run -c Release -f net10.0-android -p:DiagnosticAddress=10.0.2.2 -p:DiagnosticPort=9000 -p:DiagnosticSuspend=true -p:DiagnosticListenMode=connectFor Android devices:
dotnet build -t:Run -c Release -f net10.0-android -p:DiagnosticAddress=127.0.0.1 -p:DiagnosticPort=9000 -p:DiagnosticSuspend=true -p:DiagnosticListenMode=connectFor iOS (devices and simulators):
dotnet build -t:Run -c Release -f net10.0-ios -p:DiagnosticAddress=127.0.0.1 -p:DiagnosticPort=9000 -p:DiagnosticSuspend=true -p:DiagnosticListenMode=listenYour application will pause at the splash screen, waiting for
dotnet-traceto connect. Once connected, the application will start anddotnet-tracewill begin recording.Allow your application to fully start and reach the initial screen.
Press
<Enter>in thedotnet-traceterminal to stop recording.
The trace file will be saved to the current directory. Use the -o
option to specify a different output directory.
Profiling Runtime Operations
To profile specific operations during runtime (such as button taps,
navigation, or scrolling), use -p:DiagnosticSuspend=false and
connect the profiler after the application has launched.
Build and deploy your application with
-p:DiagnosticSuspend=false:dotnet build -t:Run -c Release -f net10.0-android -p:DiagnosticAddress=127.0.0.1 -p:DiagnosticPort=9000 -p:DiagnosticSuspend=false -p:DiagnosticListenMode=connectNavigate to the area of your application you want to profile.
Start
dotnet-tracewith the--dsrouteroption:dotnet-trace collect --dsrouter android --format speedscopePerform the operation you want to profile.
Press
<Enter>to stop the trace.
This approach produces a more focused trace file containing only the specific operation you're investigating.
Understanding Trace Output
When dotnet-trace is collecting a trace, you'll see output similar
to:
Process : $HOME/.dotnet/tools/dotnet-dsrouter
Output File : /tmp/hellomaui-app-trace
[00:00:00:35] Recording trace 1.7997 (MB)
Press <Enter> or <Ctrl+C> to exit...
After pressing <Enter>, the trace is finalized:
Stopping the trace. This may take up to minutes depending on the application being traced.
Trace completed.
Writing: hellomaui-app-trace.speedscope.json
Viewing Trace Files
The --format argument controls the output format:
nettrace(default): Can be viewed in PerfView or Visual Studio on Windowsspeedscope: JSON format that can be viewed on any platform at https://speedscope.app/
For cross-platform analysis, use --format speedscope:
dotnet-trace collect --dsrouter android --format speedscope
Profiling on Windows
While the cross-platform dotnet-trace tool works on Windows, the
platform offers additional native profiling options that may be more
convenient.
Using Visual Studio Performance Profiler
The Visual Studio Performance Profiler provides integrated profiling for .NET applications. See the Visual Studio profiling feature tour for comprehensive guidance.
Using PerfView
PerfView is a powerful, free performance analysis tool for Windows that can profile .NET MAUI applications with minimal setup.
To profile with PerfView:
Build your application for
Releasewith ReadyToRun enabled:dotnet publish -f net10.0-windows10.0.19041.0 -c Release -p:PublishReadyToRun=trueLaunch PerfView and select
Collect>Collect.In the Command field, filter on your app's executable (for example,
hellomaui.exe).Click Start Collection, then manually launch your app.
Click Stop Collection once your app has completed the operation you want to profile.
Open CPU Stacks to view timing information, or use the Flame Graph tab for a graphical view.
You can also save the PerfView data in SpeedScope format (File >
Save View As) to view it at https://speedscope.app/
for cross-platform analysis.
Measuring Windows Startup Time with PerfView
To measure precise startup times on Windows, you can use PerfView to capture Event Tracing for Windows (ETW) events:
In PerfView, open
Collect>Collectand expand Advanced Options.Configure the following:
- Enable Kernel Base
- Add
Microsoft-Windows-XAML:0x44:Informationalto Additional Providers
Click Start Collection, then launch and close your app 3-5 times.
Click Stop Collection.
Open the Events report and calculate startup time by finding:
- The
Windows Kernel/Process/Startevent for your app (note theTime MSecvalue) - The first
Microsoft-Windows-XAML/Frame/Stopevent for the same process ID - Subtract the start time from the stop time to get startup duration
- The
Run the app multiple times and average the results for more accurate measurements.
Using dotnet-trace on Windows
For unpackaged Windows applications, you can use dotnet-trace
directly:
dotnet publish -f net10.0-windows10.0.19041.0 -c Release -p:PublishReadyToRun=true -p:WindowsPackageType=None
dotnet trace collect --format speedscope -- bin\Release\net10.0-windows10.0.19041.0\win10-x64\publish\YourApp.exe
Profiling on iOS and Mac Catalyst with Instruments
For iOS and Mac Catalyst applications, Apple's Instruments tool provides native profiling with detailed insights into app launch time and performance.
Using Instruments for App Launch Profiling
Build your app for
Releasewith symbols preserved:dotnet build -c Release -f net10.0-ios -p:NoSymbolStrip=trueThe
NoSymbolStrip=trueproperty keeps native symbols in the executable, making stack traces in Instruments much more helpful.Install the app on your device:
dotnet build -t:Run -c Release -f net10.0-ios -p:NoSymbolStrip=trueLaunch Instruments (from Xcode or by running
open -a Instrumentsin Terminal).Select your iOS device at the top.
Select your app from the list of installed applications.
Choose the App Launch instrument template.
Click Choose, then click the Record button to start profiling.
The app will launch automatically. Stop the recording once the app has fully started.
In the results, select the App Lifecycle row to see the lifecycle timeline. The last row in the bottom table shows the time when the app completed launching (for example,
Currently running in the foreground...).
For more information about using Instruments, see Apple's documentation on Reducing Your App's Launch Time.
Profiling Memory Usage
Memory profiling helps you identify memory leaks and understand memory
allocation patterns in your application. Use dotnet-gcdump to create
snapshots of managed memory.
Collecting Memory Dumps
To collect a memory dump, use the same --dsrouter workflow as
dotnet-trace:
dotnet-gcdump collect --dsrouter android
Use --dsrouter android-emu, --dsrouter ios, or --dsrouter ios-sim for other targets.
Unlike CPU tracing, memory dumps do not require suspending application
startup. Build your application with -p:DiagnosticSuspend=false:
dotnet build -t:Run -c Release -f net10.0-android -p:DiagnosticAddress=127.0.0.1 -p:DiagnosticPort=9000 -p:DiagnosticSuspend=false -p:DiagnosticListenMode=connect
Once dotnet-gcdump connects, it creates a *.gcdump file in the
current directory. You can open this file in Visual Studio on Windows
or PerfView.
Analyzing Memory Dumps
When you open a *.gcdump file in Visual Studio, you can:
- View every managed object in memory
- See the total count and size of each type
- Inspect the reference tree to understand what's keeping objects alive
- Compare multiple snapshots to identify growing allocations
Visual Studio's Memory Usage diagnostic tool (Debug > Windows >
Diagnostic Tools) also allows you to take snapshots while debugging,
though you should disable XAML hot reload for accurate results.
Tip
Consider taking memory snapshots of Release builds, as code paths
can be significantly different when XAML compilation, AOT
compilation, and trimming are enabled.
Diagnosing Memory Leaks
Memory leaks in .NET MAUI applications manifest as steadily increasing memory usage, especially during repeated navigation or interactions. On mobile platforms, this can eventually cause the OS to terminate your application due to excessive memory consumption.
Symptoms of Memory Leaks
A typical symptom of a memory leak might be:
- Navigate from the main page to a details page
- Navigate back
- Navigate to the details page again
- Memory grows consistently with each cycle
Determining if a Leak Exists
To determine if a page is actually leaking, use finalizers with logging and forced garbage collection during debugging.
Add a finalizer with logging to the page class:
~MyDetailsPage() => System.Diagnostics.Debug.WriteLine("~MyDetailsPage() finalized");Force garbage collection in strategic places (for debugging only):
public MyDetailsPage() { GC.Collect(); // For debugging purposes only GC.WaitForPendingFinalizers(); InitializeComponent(); }Test a
Releasebuild and watch the console output using adb logcat (Android) or device logs (iOS).
If the finalizer runs when navigating away from the page, the page is being collected correctly. If the finalizer never runs, the page is leaking--something is holding a reference to it indefinitely.
Warning
Remove GC.Collect() calls after debugging. They're only for
diagnosing issues and should never be in production code.
Narrowing Down the Cause
Once you've identified a leak, narrow down the cause:
- Comment out all XAML content. Does the leak still occur?
- Comment out all C# code in code-behind. Does the leak still occur?
- Test on multiple platforms. Does it only happen on one platform?
Generally, an empty ContentPage should not leak. By systematically
removing code, you can identify which control or code pattern is
causing the problem.
Common Leak Patterns
C# Events
C# events can create circular references that prevent garbage collection. Consider a scenario where a child object subscribes to a parent's event, but the parent also holds a reference to the child. Both objects can end up living forever.
If the event source outlives the subscriber (like a Style in
Application.Resources), this can cause entire pages to leak.
Solution: Use WeakEventManager for events in .NET MAUI controls,
or unsubscribe from events when the object is no longer needed.
iOS and Mac Catalyst Circular References
On iOS and Mac Catalyst, circular references between C# objects and
native objects can cause leaks because C# objects that subclass
NSObject exist in both the garbage-collected .NET world and the
reference-counted Objective-C world.
Example of a problematic pattern:
class MyView : UIView
{
public MyView()
{
var picker = new UIDatePicker();
AddSubview(picker); // MyView -> UIDatePicker
picker.ValueChanged += OnValueChanged; // UIDatePicker -> MyView via event handler
}
void OnValueChanged(object? sender, EventArgs e) { }
}
Solutions:
Make event handlers
static:static void OnValueChanged(object? sender, EventArgs e) { }Use a proxy object that doesn't inherit from
NSObject:class MyView : UIView { readonly Proxy _proxy = new(); public MyView() { var picker = new UIDatePicker(); AddSubview(picker); picker.ValueChanged += _proxy.OnValueChanged; } class Proxy { public void OnValueChanged(object? sender, EventArgs e) { } } }
Note
These circular reference issues are specific to iOS and Mac Catalyst. They do not normally occur on Android or Windows.
Best Practices for Avoiding Leaks
Test
Releasebuilds: Memory behavior can differ significantly fromDebugbuilds due to optimizations, trimming, and AOT compilation.Use finalizers when investigating: Add finalizers with logging to key objects to quickly identify if they're being collected.
Unsubscribe from events: Always unsubscribe from events when objects are disposed or no longer needed.
Be cautious with events on long-lived objects: Avoid having long-lived objects (like those in
Application.Resources) hold references to short-lived objects (like pages or views).Profile regularly: Make memory profiling part of your regular testing process, especially after adding new features or making significant changes.
For more detailed information about memory leak patterns and techniques, see the .NET MAUI Memory Leaks wiki.
Alternative Profiling Approaches
Android ActivityManager Startup Logs
Android automatically logs startup time information through the
ActivityManager. You can view these logs using adb logcat:
adb logcat | grep "ActivityManager"
When your app starts, you'll see messages like:
ActivityManager: Displayed com.android.myexample/.StartupTiming: +3s534ms
This shows the time it took for your activity to be displayed. This is a quick way to measure startup time without any additional tooling or code changes.
For more information about Android app launch time and optimization techniques, see the Android documentation on app startup time.
Logging-Based Startup Measurement
For a lightweight approach to measuring startup time across all platforms, you can log messages at specific points in your application and measure the time between them:
Add a log message when your main page loads:
Loaded += (sender, e) => Dispatcher.Dispatch(() => Console.WriteLine("loaded"));Use a tool like the measure-startup sample to launch your app and measure the time until the log message appears.
On Android, you can filter
adb logcatoutput to watch for specific messages:adb logcat | grep "loaded"
This approach works across all platforms and is useful for continuous integration scenarios or quick checks.
Additional Resources
- .NET MAUI Profiling Wiki - Comprehensive wiki with advanced scenarios and troubleshooting
- Android Tracing Guide - Detailed Android-specific profiling instructions
- iOS/macOS Profiling Wiki - Platform-specific guidance for Apple platforms
- .NET Diagnostic Tools Documentation - Official
documentation for
dotnet-trace,dotnet-dsrouter, anddotnet-gcdump - PerfView User's Guide - In-depth guide to using PerfView for Windows profiling