Generación de perfiles de rendimiento

La generación de perfiles de rendimiento es el proceso de medir el rendimiento de una aplicación para identificar áreas de mejora. Las aplicaciones cliente y MAUI de .NET, en general, están interesadas en:

  • Tiempo de inicio: el tiempo que tarda la aplicación en iniciarse y mostrar la primera pantalla.
  • Uso de CPU: si los métodos específicos consumen demasiado tiempo de CPU: a través de muchas llamadas o operaciones de larga duración.
  • Uso de memoria: si se realizan muchas asignaciones más allá del motivo o si hay pérdidas de memoria.

Las técnicas y herramientas para mejorar estas métricas son diferentes, que tenemos previsto desmitificar en esta guía. Las herramientas que se usan para generar perfiles de aplicaciones MAUI de .NET también pueden variar en función de la plataforma. En esta guía se tratan los enfoques de generación de perfiles de Android, iOS, Mac Catalyst y Windows.

Importante

Generar siempre las compilaciones de perfiles Release para mediciones de rendimiento precisas. Debug Las compilaciones utilizan el intérprete (UseInterpreter=true) para el soporte de recarga de código en caliente de C#, lo que afecta significativamente al rendimiento y genera resultados poco realistas.

Prerrequisitos

Instalación de herramientas de diagnóstico

Para generar perfiles de aplicaciones .NET MAUI en iOS y Android, debe instalar las siguientes herramientas globales de .NET:

  • dotnet-trace - Recopila seguimientos de CPU y datos de rendimiento
  • dotnet-dsrouter - Reenvía las conexiones de diagnóstico desde dispositivos remotos a la máquina local.
  • dotnet-gcdump - Recopila volcados de memoria para analizar el uso de memoria administrada.

Puede instalar estas herramientas mediante los siguientes comandos:

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

Nota:

Necesita al menos la versión 9.0.652701 de todas las herramientas de diagnóstico para usar las características descritas en esta guía. Compruebe dotnet-trace, dotnet-dsrouter y dotnet-gcdump en NuGet para ver las versiones más recientes.

A partir de la versión 9.0.652701, tanto dotnet-trace como dotnet-gcdump incluyen la opción --dsrouter que inicia y administra automáticamente dotnet-dsrouter como un subproceso. Esto elimina la necesidad de ejecutarse dotnet-dsrouter por separado, lo que simplifica significativamente el flujo de trabajo de generación de perfiles.

Consulte la sesión de la .NET Conf, Herramientas de diagnóstico de .NET con IA, para una demostración en vivo del uso de estas herramientas.

Cómo funcionan conjuntamente las herramientas

Para usar estas herramientas de diagnóstico en iOS y Android, varios componentes funcionan juntos:

  • Las herramientas globales de .NET (dotnet-trace, dotnet-gcdump, dotnet-dsrouter) se ejecutan en la máquina de desarrollo.
  • El componente de diagnóstico Mono (libmono-component-diagnostics_tracing.so) se incluye en el paquete de la aplicación.
  • dotnet-dsrouter reenvía la conexión de diagnóstico desde el dispositivo remoto o emulador a un puerto local en el equipo.
  • Las herramientas de diagnóstico se conectan a este puerto local para recopilar datos de perfilado

La opción --dsrouter en dotnet-trace y dotnet-gcdump maneja automáticamente la complejidad de iniciar dotnet-dsrouter y coordinar la conexión.

Compilación de la aplicación para la generación de perfiles

Para habilitar la generación de perfiles, la aplicación debe compilarse con propiedades especiales de MSBuild que incluyan los componentes de diagnóstico y configuren la conexión a las herramientas de generación de perfiles.

Comprensión de las propiedades de diagnóstico

Las siguientes propiedades de MSBuild controlan cómo se comunica la aplicación con las herramientas de diagnóstico:

  • DiagnosticAddress: la dirección IP donde dotnet-dsrouter está escuchando. Utiliza 10.0.2.2 para emuladores de Android (es la dirección de retorno de la máquina host desde el emulador) y 127.0.0.1 para dispositivos físicos e iOS.

  • DiagnosticPort: número de puerto de la conexión de diagnóstico (el valor predeterminado es 9000).

  • DiagnosticSuspend: cuando true, la aplicación espera a que el generador de perfiles se conecte antes de iniciarse. Cuando false, la aplicación se inicia inmediatamente y el generador de perfiles puede conectarse más adelante. Se usa true para la generación de perfiles de inicio, false para la generación de perfiles en tiempo de ejecución y los volcados de memoria.

  • DiagnosticListenMode: Establecer en connect para Android (la aplicación se conecta a dotnet-dsrouter), o en listen para iOS (la aplicación escucha a dotnet-dsrouter para conectarse a ella).

  • EnableDiagnostics: cuando true, incluye el componente de diagnóstico Mono en el paquete de la aplicación. Esto se establece implícitamente al establecer cualquiera de las Diagnostic* propiedades de MSBuild. Esta propiedad funciona en Android, iOS y Mac Catalyst.

Nota:

Cuando se usa CoreCLR (actualmente experimental en Android, con compatibilidad con iOS planeada), el componente de diagnóstico se integra en el entorno de ejecución y EnableDiagnostics no es necesario.

Ejemplos de comandos de compilación

Al ejecutar dotnet-trace o dotnet-gcdump con la --dsrouter opción , la herramienta muestra instrucciones para compilar la aplicación. Por ejemplo:

Para emuladores de Android:

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

Para dispositivos Android:

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

Para dispositivos y simuladores de iOS:

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

Nota:

Use -f net10.0-android o -f net10.0-ios para proyectos con varias plataformas de destino en $(TargetFrameworks).

Importante

Las aplicaciones compiladas con estas propiedades de diagnóstico solo deben usarse para el desarrollo y las pruebas. Nunca publique compilaciones con componentes de diagnóstico habilitados en producción, ya que pueden exponer endpoints con información más detallada sobre el código de la aplicación.

Generación de perfiles de uso de CPU

La dotnet-trace herramienta recopila información de muestreo de CPU en formatos como .nettrace y .speedscope.json. Estas trazas muestran el tiempo invertido en cada método, lo que le ayuda a identificar los cuellos de botella de rendimiento en su aplicación.

El flujo de trabajo para la generación de perfiles de CPU depende de si estás midiendo el tiempo de inicio o generando perfiles de las operaciones en tiempo de ejecución. La diferencia clave es la -p:DiagnosticSuspend propiedad de MSBuild.

Tiempo de inicio de generación de perfiles

Para capturar medidas precisas de tiempo de inicio, suspenda el inicio de la aplicación hasta que el generador de perfiles esté listo. Esto garantiza que captures toda la secuencia de inicio desde el comienzo.

  1. En un terminal, inicie dotnet-trace con la opción --dsrouter.

    dotnet-trace collect --dsrouter android-emu --format speedscope
    

    O para un dispositivo Android físico:

    dotnet-trace collect --dsrouter android --format speedscope
    

    En el caso de los dispositivos y simuladores de iOS, use --dsrouter ios o --dsrouter ios-sim respectivamente.

  2. En otro terminal, compile e implemente la aplicación con -p:DiagnosticSuspend=true para pausar en el inicio:

    Para emuladores de Android:

    dotnet build -t:Run -c Release -f net10.0-android -p:DiagnosticAddress=10.0.2.2 -p:DiagnosticPort=9000 -p:DiagnosticSuspend=true -p:DiagnosticListenMode=connect
    

    Para dispositivos Android:

    dotnet build -t:Run -c Release -f net10.0-android -p:DiagnosticAddress=127.0.0.1 -p:DiagnosticPort=9000 -p:DiagnosticSuspend=true -p:DiagnosticListenMode=connect
    

    Para iOS (dispositivos y simuladores):

    dotnet build -t:Run -c Release -f net10.0-ios -p:DiagnosticAddress=127.0.0.1 -p:DiagnosticPort=9000 -p:DiagnosticSuspend=true -p:DiagnosticListenMode=listen
    
  3. La aplicación se pausará en la pantalla de presentación, esperando a que dotnet-trace se conecte. Una vez conectado, la aplicación se iniciará y dotnet-trace comenzará a grabar.

  4. Permitir que la aplicación se inicie completamente y llegue a la pantalla inicial.

  5. Presione <Enter> en el terminal para detener la dotnet-trace grabación.

El archivo de seguimiento se guardará en el directorio actual. Use la -o opción para especificar un directorio de salida diferente.

Generación de perfiles de operaciones en tiempo de ejecución

Para generar perfiles de operaciones específicas durante el tiempo de ejecución (como pulsaciones de botón, navegación o desplazamiento), use -p:DiagnosticSuspend=false y conecte el generador de perfiles una vez iniciada la aplicación.

  1. Compile e implemente la aplicación con -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
    
  2. Dirígete a la parte de la aplicación que deseas analizar.

  3. Inicie dotnet-trace con la --dsrouter opción:

    dotnet-trace collect --dsrouter android --format speedscope
    
  4. Realice la operación que desea perfilar.

  5. Presione <Enter> para detener el seguimiento.

Este enfoque genera un archivo de seguimiento más centrado que contiene solo la operación específica que está investigando.

Comprensión de la salida del seguimiento

Cuando dotnet-trace está recopilando un seguimiento, verá una salida similar a:

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

Después de presionar <Enter>, se finaliza el seguimiento:

Stopping the trace. This may take up to minutes depending on the application being traced.

Trace completed.
Writing:    hellomaui-app-trace.speedscope.json

Visualización de archivos de seguimiento

El --format argumento controla el formato de salida:

  • nettrace (valor predeterminado): se puede ver en PerfView o Visual Studio en Windows
  • speedscope: formato JSON que se puede ver en cualquier plataforma en . https://speedscope.app/

Para el análisis multiplataforma, use --format speedscope:

dotnet-trace collect --dsrouter android --format speedscope

Generación de perfiles en Windows

Aunque la herramienta multiplataforma dotnet-trace funciona en Windows, la plataforma ofrece opciones de generación de perfiles nativas adicionales que pueden ser más convenientes.

Uso de Visual Studio Performance Profiler

Visual Studio Performance Profiler proporciona generación de perfiles integrada para aplicaciones .NET. Consulte el recorrido por la función de generación de perfiles de Visual Studio para obtener orientación completa.

Uso de PerfView

PerfView es una eficaz herramienta de análisis de rendimiento libre para Windows que puede generar perfiles de aplicaciones MAUI de .NET con una configuración mínima.

Para generar perfiles con PerfView:

  1. Compile la aplicación para Release con ReadyToRun habilitado:

    dotnet publish -f net10.0-windows10.0.19041.0 -c Release -p:PublishReadyToRun=true
    
  2. Inicie PerfView y seleccione Collect>Collect.

  3. En el campo Comando, filtre por el archivo ejecutable de la aplicación (por ejemplo, hellomaui.exe).

  4. Haga clic en Iniciar recopilación y, a continuación, inicie manualmente la aplicación.

  5. Haz clic en Detener recopilación una vez que la aplicación haya completado la operación que quieres generar perfiles.

  6. Abra Pilas de CPU para ver información sobre el tiempo o use la pestaña Flame Graph para una vista gráfica.

También puede guardar los datos de PerfView en formato SpeedScope (File>Save View As) para verlos en https://speedscope.app/ para el análisis multiplataforma.

Medición del tiempo de inicio de Windows con PerfView

Para medir los tiempos de inicio precisos en Windows, puede usar PerfView para capturar eventos de Event Tracing for Windows (ETW).

  1. En PerfView, abra Collect>Collect y expanda Opciones avanzadas.

  2. Configurar lo siguiente:

    • Habilitación de la base de kernel
    • Agregar Microsoft-Windows-XAML:0x44:Informational a proveedores adicionales
  3. Haga clic en Iniciar recopilación y, a continuación, inicie y cierre la aplicación de 3 a 5 veces.

  4. Haga clic en Detener recopilación.

  5. Abra el informe Eventos y calcule el tiempo de inicio mediante la búsqueda de:

    • El evento Windows Kernel/Process/Start de tu aplicación (ten en cuenta el valor Time MSec)
    • El primer Microsoft-Windows-XAML/Frame/Stop evento para el mismo identificador de proceso
    • Resta la hora de inicio de la hora de detención para obtener la duración del inicio.

Ejecute la aplicación varias veces y promedia los resultados para mediciones más precisas.

Uso de dotnet-trace en Windows

Para las aplicaciones de Windows sin empaquetar, puede usar dotnet-trace directamente:

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

Generación de perfiles en iOS y Mac Catalyst con Instruments

En el caso de las aplicaciones iOS y Mac Catalyst, la herramienta Instruments de Apple proporciona perfiles nativos con información detallada sobre el tiempo de inicio y el rendimiento de la aplicación.

Uso de Instruments para la generación de perfiles de inicio de aplicaciones

  1. Cree tu aplicación para Release con símbolos intactos.

    dotnet build -c Release -f net10.0-ios -p:NoSymbolStrip=true
    

    La NoSymbolStrip=true propiedad mantiene símbolos nativos en el ejecutable, lo que hace que los seguimientos de pila en Instruments sean mucho más útiles.

  2. Instale la aplicación en el dispositivo:

    dotnet build -t:Run -c Release -f net10.0-ios -p:NoSymbolStrip=true
    
  3. Inicie Instruments (desde Xcode o ejecutando open -a Instruments en Terminal).

  4. Seleccione el dispositivo iOS en la parte superior.

  5. Seleccione la aplicación en la lista de aplicaciones instaladas.

  6. Elija la plantilla de instrumento Lanzamiento de la Aplicación.

  7. Haga clic en Elegir y, a continuación, haga clic en el botón Grabar para iniciar la generación de perfiles.

  8. La aplicación se iniciará automáticamente. Detenga la grabación una vez que la aplicación se haya iniciado por completo.

  9. En los resultados, seleccione la fila Ciclo de vida de la aplicación para ver la escala de tiempo del ciclo de vida. La última fila de la tabla inferior muestra la hora en que se completó el inicio de la aplicación (por ejemplo, Currently running in the foreground...).

Para obtener más información sobre el uso de Instruments, consulte la documentación de Apple sobre la reducción del tiempo de inicio de la aplicación.

Perfilado del uso de memoria

La generación de perfiles de memoria le ayuda a identificar pérdidas de memoria y a comprender los patrones de asignación de memoria en la aplicación. Use dotnet-gcdump para crear instantáneas de memoria administrada.

Recopilación de volcados de memoria

Para recopilar un volcado de memoria, use el mismo --dsrouter flujo de trabajo que dotnet-trace:

dotnet-gcdump collect --dsrouter android

Utilice --dsrouter android-emu, --dsrouter ios, o --dsrouter ios-sim para otros destinos.

A diferencia del seguimiento de CPU, los volcados de memoria no requieren suspender el inicio de la aplicación. Compile la aplicación con -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

Una vez que dotnet-gcdump se conecta, crea un archivo *.gcdump en el directorio actual. Puede abrir este archivo en Visual Studio en Windows o PerfView.

Análisis de volcados de memoria

Al abrir un *.gcdump archivo en Visual Studio, puede hacer lo siguiente:

  • Visualización de todos los objetos administrados en memoria
  • Ver el recuento total y el tamaño de cada tipo
  • Inspeccione el árbol de referencia para comprender lo que mantiene activos los objetos
  • Comparación de varias instantáneas para identificar asignaciones crecientes

La herramienta de diagnóstico de uso de memoria (Debug>Windows>Diagnostic Tools) de Visual Studio también permite tomar instantáneas durante la depuración, aunque debe deshabilitar la recarga activa de XAML para obtener resultados precisos.

Sugerencia

Considere la posibilidad de tomar instantáneas de memoria de las compilaciones Release, ya que las rutas de código pueden ser significativamente diferentes cuando se habilita la compilación XAML, la compilación AOT y el recorte.

Diagnóstico de pérdidas de memoria

Las pérdidas de memoria en las aplicaciones MAUI de .NET se manifiestan como un aumento constante del uso de memoria, especialmente durante la navegación repetida o las interacciones. En plataformas móviles, esto puede provocar que el sistema operativo finalice la aplicación debido al consumo excesivo de memoria.

Síntomas de pérdidas de memoria

Un síntoma típico de una pérdida de memoria puede ser:

  1. Navegar desde la página principal a una página de detalles
  2. Navegar hacia atrás
  3. Vuelva a ir a la página de detalles.
  4. La memoria crece de forma coherente con cada ciclo

Determinar si existe una fuga

Para determinar si una página se está filtrando realmente, use finalizadores con el registro y la recolección forzada de elementos no utilizados durante la depuración.

  1. Agregue un finalizador con funcionalidades de registro a la clase de página.

    ~MyDetailsPage() => System.Diagnostics.Debug.WriteLine("~MyDetailsPage() finalized");
    
  2. Forzar la recolección de basura en lugares estratégicos (solo para depuración):

    public MyDetailsPage()
    {
        GC.Collect(); // For debugging purposes only
        GC.WaitForPendingFinalizers();
        InitializeComponent();
    }
    
  3. Pruebe una Release compilación y vea la salida de la consola mediante adb logcat (Android) o registros de dispositivos (iOS).

Si el finalizador se ejecuta al navegar fuera de la página, la página está siendo correctamente recogida. Si el finalizador nunca se ejecuta, la página está produciendo una fuga, algo está manteniendo una referencia a la página indefinidamente.

Advertencia

Elimine GC.Collect() las llamadas después de la depuración. Solo son para diagnosticar problemas y nunca deben estar en código de producción.

Restringir la causa

Una vez que haya identificado una fuga, reduzca la causa:

  1. Comente todo el contenido XAML. ¿Todavía se produce la fuga?
  2. Convierta en comentario todo el código de C# en el código subyacente. ¿Todavía se produce la fuga?
  3. Prueba en varias plataformas. ¿Solo sucede en una plataforma?

Por lo general, un vacío ContentPage no debe filtrarse. Al quitar sistemáticamente el código, puede identificar qué control o patrón de código está causando el problema.

Patrones comunes de fuga

Eventos de C#

Los eventos de C# pueden crear referencias circulares que impiden la recolección de basura. Considere un escenario en el que un objeto hijo se suscribe a un evento del padre, pero el padre también mantiene una referencia al objeto hijo. Ambos objetos pueden acabar viviendo para siempre.

Si el origen del evento sobrevive al suscriptor (como Style en Application.Resources), esto puede hacer que se produzca una fuga de páginas completas.

Solución: se usa WeakEventManager para eventos en controles MAUI de .NET o cancela la suscripción a eventos cuando el objeto ya no es necesario.

Referencias circulares en iOS y Mac Catalyst

En iOS y Mac Catalyst, las referencias circulares entre objetos de C# y objetos nativos pueden provocar fugas porque los objetos de C# que son subclases de NSObject existen tanto en el mundo de .NET gestionados por el recolector de basura como en el mundo de Objective-C con conteo de referencias.

Ejemplo de un patrón problemático:

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) { }
}

Soluciones:

  1. Crear controladores de eventos static:

    static void OnValueChanged(object? sender, EventArgs e) { }
    
  2. Use un objeto proxy que no herede de 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) { }
        }
    }
    

Nota:

Estos problemas de referencia circular son específicos de iOS y Mac Catalyst. Normalmente no se producen en Android o Windows.

Procedimientos recomendados para evitar fugas

  • Compilaciones de pruebaRelease: el comportamiento de la memoria puede diferir significativamente de Debug las compilaciones debido a optimizaciones, recortes y compilación AOT.

  • Use finalizadores al investigar: agregue finalizadores con registro a objetos clave para identificar rápidamente si están siendo recopilados.

  • Cancelar la suscripción a eventos: siempre cancela la suscripción de los eventos cuando se eliminan los objetos o ya no se necesitan.

  • Tenga cuidado con los eventos en objetos de larga duración: Evite permitir que objetos de larga duración (como los de Application.Resources) mantengan referencias a objetos de corta duración (como páginas o vistas).

  • Perfil regularmente: Incorpore el perfilado de memoria como parte del proceso habitual de pruebas, especialmente después de añadir nuevas características o realizar cambios significativos.

Para obtener información más detallada sobre los patrones y técnicas de pérdida de memoria, consulte el wiki de .NET MAUI sobre pérdidas de memoria.

Enfoques alternativos de generación de perfiles

Registros de inicio de Android ActivityManager

Android registra automáticamente la información de tiempo de inicio a través de ActivityManager. Puede ver estos registros mediante adb logcat:

adb logcat | grep "ActivityManager"

Cuando se inicie la aplicación, verá mensajes como:

ActivityManager: Displayed com.android.myexample/.StartupTiming: +3s534ms

Esto muestra el tiempo necesario para que se muestre la actividad. Se trata de una manera rápida de medir el tiempo de inicio sin necesidad de realizar ningún cambio de código ni herramientas adicionales.

Para obtener más información sobre las técnicas de tiempo de inicio y optimización de la aplicación Android, consulte la documentación de Android sobre el tiempo de inicio de la aplicación.

Medición del inicio basada en registros

Para un enfoque ligero para medir el tiempo de inicio en todas las plataformas, puede registrar mensajes en puntos específicos de la aplicación y medir el tiempo entre ellas:

  1. Agregue un mensaje de registro cuando se cargue la página principal:

    Loaded += (sender, e) => Dispatcher.Dispatch(() => 
        Console.WriteLine("loaded"));
    
  2. Utiliza una herramienta como el ejemplo measure-startup para iniciar tu aplicación y medir el tiempo hasta que aparezca el mensaje de registro.

  3. En Android, puede filtrar la adb logcat salida para ver los mensajes específicos:

    adb logcat | grep "loaded"
    

Este enfoque funciona en todas las plataformas y es útil para escenarios de integración continua o comprobaciones rápidas.

Recursos adicionales