Panoramica della misurazione del rendimento dell'app

Questo documento ti aiuta a identificare e risolvere i principali problemi di prestazioni della tua app.

Problemi di rendimento principali

Esistono molti problemi che possono contribuire a un cattivo rendimento di un'app, ma di seguito sono riportati alcuni problemi comuni da verificare nella tua app:

Latenza di avvio

La latenza di avvio è il tempo che intercorre tra il tocco dell'icona dell'app, della notifica o di un altro punto di contatto e la visualizzazione dei dati dell'utente sullo schermo.

Nelle tue app, punta ai seguenti obiettivi di avvio:

  • Avvio a freddo in meno di 500 ms. Un avvio completo si verifica quando l'app che viene avviata non è presente nella memoria di sistema. Questo accade quando si tratta del primo avvio dell'app dal riavvio o dal momento in cui il processo dell'app viene interrotto dall'utente o dal sistema.

    Al contrario, un avvio a caldo si verifica quando l'app è già in esecuzione in background. Un avvio a freddo richiede il massimo impegno da parte del sistema, in quanto deve caricare tutto dallo spazio di archiviazione e inizializzare l'app. Cerca di fare in modo che gli avvii a freddo richiedano al massimo 500 ms.

  • Latenze P95 e P99 molto vicine alla latenza mediana. Se l'app impiega molto tempo per avviarsi, l'esperienza utente è negativa. Le comunicazioni interprocessuali (IPC) e le operazioni di I/O non necessarie durante il percorso critico dell'avvio dell'app possono causare contese per i blocchi e introdurre incoerenze.

Jitter dello scorrimento

Jank è il termine che descrive l'interruzione visiva che si verifica quando il sistema non è in grado di creare e fornire frame in tempo per visualizzarli sullo schermo alla cadenza richiesta di 60 Hz o superiore. Il problema è più evidente durante lo scorrimento, quando invece di un flusso animato fluido si verificano dei problemi. Il jitter si verifica quando il movimento si interrompe per uno o più frame, poiché l'app impiega più tempo per eseguire il rendering dei contenuti rispetto alla durata di un frame sul sistema.

Le app devono avere come target frequenze di aggiornamento a 90 Hz. Le frequenze di rendering convenzionali sono di 60 Hz, ma molti dispositivi più recenti funzionano in modalità 90 Hz durante le interazioni dell'utente, come lo scorrimento. Alcuni dispositivi supportano frequenze ancora più elevate, fino a 120 Hz.

Per vedere la frequenza di aggiornamento utilizzata da un dispositivo in un determinato momento, attiva un overlay utilizzando Opzioni sviluppatore > Mostra frequenza di aggiornamento nella sezione Debug.

Transizioni non fluide

Questo si verifica durante interazioni come il passaggio da una scheda all'altra o il caricamento di una nuova attività. Questi tipi di transizioni devono essere animazioni fluide e non devono includere ritardi o sfarfallio visivo.

Inefficienze energetiche

Lavorare riduce la carica della batteria, mentre lavorare inutilmente riduce la durata della batteria.

Le allocazioni di memoria, che derivano dalla creazione di nuovi oggetti nel codice, possono causare un lavoro significativo nel sistema. Questo perché non solo le allocazioni richiedono impegno da parte dell'ambiente di runtime Android (ART), ma anche il successivo sblocco di questi oggetti (garbage collection) richiede tempo e impegno. Sia l'allocazione che la raccolta sono molto più veloci ed efficienti, soprattutto per gli oggetti temporanei. Sebbene in passato fosse buona prassi evitare di allocare oggetti, ti consigliamo di fare ciò che è più adatto alla tua app e alla tua architettura. Risparmiare sulle allocazioni con il rischio di avere un codice non manutenibile non è una best practice, dato che ART è in grado di fare molto di più.

Tuttavia, richiede impegno, quindi tieni presente che può contribuire a problemi di prestazioni se stai allocando molti oggetti nell'istruzione for interna.

Identificare i problemi

Abbiamo consigliato il seguente flusso di lavoro per identificare e risolvere i problemi di prestazioni:

  1. Identifica e controlla i seguenti Critical User Journey:
    • Flussi di avvio comuni, inclusi quelli dal programma di avvio e dalle notifiche.
    • Schermate in cui l'utente scorre i dati.
    • Transizioni tra le schermate.
    • Flussi di lunga durata, come la navigazione o la riproduzione di musica.
  2. Controlla cosa succede durante i flussi precedenti utilizzando i seguenti strumenti di debug:
    • Perfetto: ti consente di vedere cosa succede nell'intero dispositivo con dati di temporizzazione precisi.
    • Memory Profiler: consente di vedere quali allocazioni di memoria vengono eseguite nell'heap.
    • Simpleperf: mostra un grafico a fiamme delle chiamate funzione che utilizzano più CPU durante un determinato periodo di tempo. Quando identifichi qualcosa che richiede molto tempo in Systrace, ma non sai perché, Simpleperf può fornire informazioni aggiuntive.

Per comprendere e eseguire il debug di questi problemi di prestazioni, è fondamentale eseguire manualmente il debug delle singole esecuzioni di test. Non puoi sostituire i passaggi precedenti analizzando dati aggregati. Tuttavia, per capire cosa vedono effettivamente gli utenti e identificare quando potrebbero verificarsi delle regressioni, è importante configurare la raccolta delle metriche nei test automatici e sul campo:

  • Flussi di avvio
  • Jank
    • Metriche dei campi
      • Dati vitali del frame di Play Console: in Play Console non puoi limitare le metriche a un percorso dell'utente specifico. Registra solo il jitter complessivo in tutta l'app.
      • Misurazione personalizzata con FrameMetricsAggregator: puoi utilizzareFrameMetricsAggregator per registrare le metriche relative al jitter durante un determinato flusso di lavoro.
    • Test di laboratorio
      • Scorrimento con Macrobenchmark.
      • Il macrobenchmark raccoglie i tempi dei frame utilizzando i comandi dumpsys gfxinfo che racchiudono il percorso di un singolo utente. Questo è un modo per comprendere la variazione del jitter in un determinato percorso dell'utente. Le metriche RenderTime, che mostrano il tempo necessario per disegnare i frame, sono più importanti del conteggio dei frame con balbuzie per identificare regressioni o miglioramenti.

I link alle app sono link diretti basati sull'URL del tuo sito web di cui è stata verificata la proprietà. Di seguito sono riportati i motivi che possono causare il fallimento delle verifiche di App Link.

  • Ambiti dei filtri per intent: aggiungi autoVerify solo ai filtri per intent per gli URL a cui può rispondere la tua app.
  • Switch di protocollo non verificati: i reindirizzamenti lato server e dei sottodomini non verificati sono considerati rischi per la sicurezza e non superano la verifica. Causano il fallimento di tutti i linkautoVerify. Ad esempio, il reindirizzamento dei link da HTTP ad HTTPS, come da example.com a www.example.com, senza verificare i link HTTPS può causare la mancata verifica. Assicurati di verificare i link alle app aggiungendo filtri di intent.
  • Link non verificabili: l'aggiunta di link non verificabili a scopo di test può impedire al sistema di verificare i link all'app per la tua app.
  • Server inaffidabili: assicurati che i server possano connettersi alle app client.

Configurare l'app per l'analisi delle prestazioni

È essenziale eseguire una configurazione corretta per ottenere benchmark accurati, ripetibili e strategici da un'app. Esegui il test su un sistema il più simile possibile alla produzione, eliminando al contempo le fonti di rumore. Le sezioni seguenti mostrano una serie di passaggi specifici per APK e sistema che puoi seguire per preparare una configurazione di test, alcuni dei quali sono specifici per i casi d'uso.

Tracepoint

Le app possono eseguire l'instrumentazione del codice con eventi di traccia personalizzati.

Durante l'acquisizione delle tracce, il monitoraggio comporta un piccolo overhead di circa 5 μs per sezione, quindi non inserirlo in ogni metodo. Il monitoraggio di blocchi di lavoro più grandi di oltre 0,1 ms può fornire informazioni significative sui colli di bottiglia.

Considerazioni relative agli APK

Le varianti di debug possono essere utili per la risoluzione dei problemi e la simbolizzazione dei sample dello stack, ma hanno gravi ripercussioni sulle prestazioni. I dispositivi con Android 10 (livello API 29) e versioni successive possono utilizzare profileable android:shell="true" nel file manifest per attivare il profiling nelle build di release.

Utilizza la configurazione di ottimizzazione del codice di livello di produzione. A seconda delle risorse utilizzate dall'app, questo può avere un impatto significativo sulle prestazioni. Alcune configurazioni di ProGuard rimuovono i tracepoint, quindi ti consigliamo di rimuovere queste regole per la configurazione su cui stai eseguendo i test.

Compilation

Compila l'app sul dispositivo in uno stato noto, in genere speed per semplicità o speed-profile per una maggiore corrispondenza con il rendimento in produzione (anche se questo richiede l'abilitazione dell'applicazione e il dumping dei profili o la compilazione dei profili di riferimento dell'app).

Sia speed che speed-profile riducono la quantità di codice in esecuzione interpretato da dex e, di conseguenza, la quantità di compilazione just-in-time (JIT) in background che può causare interferenze significative. Solo speed-profile riduce l'impatto del caricamento delle classi di runtime da dex.

Il seguente comando compila l'applicazione utilizzando la modalità speed:

adb shell cmd package compile -m speed -f com.example.packagename

La modalità di compilazione speed compila completamente i metodi dell'app. La modalità speed-profile compila i metodi e le classi dell'app in base a un profilo dei percorsi di codice utilizzati raccolti durante l'utilizzo dell'app. Può essere difficile raccogliere i profili in modo coerente e corretto, quindi, se decidi di utilizzarli, verifica che raccolgano ciò che ti aspetti. I profili si trovano nella seguente posizione:

/data/misc/profiles/ref/[package-name]/primary.prof

Considerazioni sul sistema

Per misurazioni a basso livello e ad alta fedeltà, calibra i dispositivi. Esegui confronti A/B sullo stesso dispositivo e con la stessa versione del sistema operativo. Possono esserci variazioni significative nel rendimento, anche nello stesso tipo di dispositivo.

Sui dispositivi con root, ti consigliamo di utilizzare uno script lockClocks per i microbenchmark. Tra le altre cose, questi script eseguono le seguenti operazioni:

  • Posiziona le CPU a una frequenza fissa.
  • Disattiva i core piccoli e configura la GPU.
  • Disattiva la limitazione termica.

Sconsigliamo di utilizzare uno script lockClocks per i test incentrati sull'esperienza utente, come il lancio dell'app, i test DoU e i test di judder, ma può essere essenziale per ridurre il rumore nei test di microbenchmark.

Se possibile, ti consigliamo di utilizzare un framework di test come Macrobenchmark, che può ridurre il rumore nelle misurazioni e impedire l'inaccuratezza delle misurazioni.

Avvio lento dell'app: attività di trampoline non necessaria

Un'attività di trampolino può prolungare inutilmente il tempo di avvio dell'app ed è importante sapere se la tua app lo sta facendo. Come mostrato nell'esempio seguente activityStart è seguito immediatamente da un altro activityStart senza che la prima attività disegni alcun frame.

alt_text Figura 1. Una traccia che mostra l'attività sul trampolino.

Questo può accadere sia in un punto di contatto per le notifiche sia in un punto di contatto normale per l'avvio dell'app e spesso puoi risolverlo tramite il refactoring. Ad esempio, se utilizzi questa attività per eseguire la configurazione prima dell'esecuzione di un'altra attività, estrai questo codice in un componente o una libreria riutilizzabili.

Allocazioni non necessarie che attivano GC frequenti

In un Systrace potresti notare che le raccolte dei rifiuti (GC) si verificano più spesso del previsto.

Nell'esempio seguente, ogni 10 secondi durante un'operazione di lunga durata è un indicatore che l'app potrebbe eseguire allocazioni inutilmente, ma in modo coerente nel tempo:

alt_text Figura 2. Una traccia che mostra lo spazio tra gli eventi GC.

Potresti anche notare che uno stack di chiamate specifico esegue la maggior parte delle allocazioni quando utilizzi lo strumento di analisi della memoria. Non è necessario eliminare tutte le allocazioni in modo aggressivo, poiché ciò può rendere più difficile la manutenzione del codice. Inizia invece con gli hotspot delle allocazioni.

Frame instabili

La pipeline grafica è relativamente complicata e possono esserci alcune sfumature nel determinare se un utente potrebbe vedere un frame perso. In alcuni casi, la piattaforma può "salvare" un frame utilizzando il buffering. Tuttavia, puoi ignorare gran parte di queste sfumature per identificare i frame problematici dal punto di vista della tua app.

Quando i frame vengono disegnati con un lavoro ridotto richiesto dall'app, i tracepoint Choreographer.doFrame() si verificano con una cadenza di 16,7 ms su un dispositivo a 60 FPS:

alt_text Figura 3. Una traccia che mostra frame rapidi frequenti.

Se diminuisci lo zoom e navighi nella traccia, a volte noti che i frame impiegano un po' più di tempo per essere completati, ma non è un problema perché non impiegano più del tempo allocato di 16,7 ms:

alt_text Figura 4. Una traccia che mostra frame rapidi frequenti con picchi periodici di lavoro.

Quando noti un'interruzione di questa cadenza regolare, si tratta di un frame discontinuo, come mostrato nella figura 5:

alt_text Figura 5. Una traccia che mostra un frame discontinuo.

Puoi esercitarti a identificarli.

alt_text Figura 6. Una traccia che mostra frame più discontinui.

In alcuni casi, devi aumentare lo zoom su un punto tracciante per avere ulteriori informazioni su quali visualizzazioni vengono gonfiate o su cosa sta facendo RecyclerView. In altri casi, potrebbe essere necessario effettuare ulteriori accertamenti.

Per ulteriori informazioni su come identificare i fotogrammi a scatti e eseguire il debug delle relative cause, consulta Rendering lento.

Errori comuni di RecyclerView

L'invalidazione non necessaria dell'intero set di dati di supporto di RecyclerView può portare a tempi di rendering dei frame lunghi e a scatti. Per ridurre al minimo il numero di viste da aggiornare, convalida solo i dati che cambiano.

Consulta la sezione Presentare dati dinamici per scoprire come evitare chiamate notifyDatasetChanged()costose, che causano l'aggiornamento dei contenuti anziché la loro sostituzione completa.

Se non supporti correttamente ogni RecyclerView nidificato, il RecyclerView interno potrebbe essere ricreato completamente ogni volta. Ogni RecyclerView interno nidificato deve avere un RecycledViewPool impostato per garantire che le visualizzazioni possano essere riutilizzate tra ogni RecyclerView interno.

Se non esegui il pre-caricamento di dati sufficienti o non lo esegui in modo tempestivo, la visualizzazione della parte inferiore di un elenco scorrevole può risultare spiacevole quando un utente deve attendere altri dati dal server. Anche se tecnicamente non si tratta di jerk, poiché non vengono perse scadenze per i frame, puoi migliorare notevolmente l'esperienza utente modificando i tempi e la quantità del precaricamento in modo che l'utente non debba attendere i dati.

Eseguire il debug dell'app

Di seguito sono riportati diversi metodi per eseguire il debug del rendimento dell'app. Guarda il seguente video per una panoramica del monitoraggio del sistema e dell'utilizzo del profiler di Android Studio.

Eseguire il debug dell'avvio dell'app con Systrace

Consulta la sezione Tempi di avvio dell'app per una panoramica della procedura di avvio dell'app e guarda il seguente video per una panoramica del monitoraggio del sistema.

Puoi distinguere i tipi di avvio nelle seguenti fasi:

  • Avvio a freddo: inizia creando un nuovo processo senza stato salvato.
  • Avvio a caldo: ricrea l'attività riutilizzando il processo o ricrea il processo con lo stato salvato.
  • Avvio a caldo: riavvia l'attività e inizia all'inflazione.

Ti consigliamo di acquisire i file Systrace con l'app di monitoraggio del sistema sul dispositivo. Per Android 10 e versioni successive, utilizza Perfetto. Per Android 9 e versioni precedenti, utilizza Systrace. Ti consigliamo inoltre di visualizzare i file di traccia con il visualizzatore di tracce Perfetto basato sul web. Per ulteriori informazioni, consulta la Panoramica del monitoraggio del sistema.

Ecco alcuni aspetti da tenere presenti:

  • Concorrenza per il monitoraggio: la concorrenza per le risorse protette dal monitoraggio può introdurre un ritardo significativo nell'avvio dell'app.
  • Transazioni del binder sincrono: cerca transazioni non necessarie nel percorso critico della tua app. Se una transazione necessaria è costosa, ti consigliamo di collaborare con il team della piattaforma associato per apportare miglioramenti.

  • GC simultanea: si tratta di un problema comune e con un impatto relativamente ridotto, ma se lo riscontri spesso, ti consigliamo di esaminarlo con il profiler della memoria di Android Studio.

  • I/O: controlla le operazioni di I/O eseguite durante l'avvio e cerca eventuali arresti anomali prolungati.

  • Attività significativa su altri thread: possono interferire con il thread dell'interfaccia utente, quindi fai attenzione al lavoro in background durante l'avvio.

Ti consigliamo di chiamare reportFullyDrawn al termine dell'avvio dal punto di vista dell'app per migliorare i report sulle metriche di avvio dell'app. Per ulteriori informazioni sull'utilizzo di reportFullyDrawn, consulta la sezione Tempo per visualizzare tutto. Puoi estrarre gli orari di inizio definiti dall'RFD tramite il gestore delle tracce Perfetto e viene emesso un evento traccia visibile all'utente.

Utilizzare la funzionalità di monitoraggio del sistema sul dispositivo

Puoi utilizzare l'app a livello di sistema chiamata Monitoraggio sistema per acquisire un monitoraggio sistema su un dispositivo. Questa app ti consente di registrare le tracce dal dispositivo senza doverlo collegare o connettere a adb.

Utilizzare Memory Profiler di Android Studio

Puoi utilizzare lo strumento Android Studio Memory Profiler per controllare la pressione sulla memoria che potrebbe essere causata da perdite di memoria o da modelli di utilizzo errati. Fornisce una panoramica in tempo reale delle allocazioni degli oggetti.

Puoi risolvere i problemi di memoria nella tua app seguendo le informazioni ottenute dall'utilizzo di Profiler della memoria per monitorare il motivo e la frequenza con cui si verificano le GC.

Per eseguire il profiling della memoria dell'app, svolgi i seguenti passaggi:

  1. Rileva i problemi di memoria.

    Registra una sessione di profilazione della memoria del percorso dell'utente su cui vuoi concentrarti. Cerca un aumento del numero di oggetti, come mostrato nella figura 7, che alla fine porta alle GC, come mostrato nella figura 8.

    alt_text Figura 7. Aumento del numero di oggetti.

    alt_text Figura 8. Garbage collection.

    Dopo aver identificato il percorso dell'utente che sta aumentando la pressione sulla memoria, analizza le cause principali della pressione sulla memoria.

  2. Diagnostica i punti di maggiore pressione sulla memoria.

    Seleziona un intervallo nella sequenza temporale per visualizzare sia le allocazioni sia le dimensioni ridotte, come mostrato nella figura 9.

    alt_text Figura 9. Valori per Allocazioni e Dimensione superficiale.

    Esistono diversi modi per ordinare questi dati. Di seguito sono riportati alcuni esempi di come ogni visualizzazione può aiutarti ad analizzare i problemi.

    • Ordina per classe: utile per trovare le classi che generano oggetti che altrimenti vengono memorizzati nella cache o riutilizzati da un pool di memoria.

      Ad esempio, se noti che un'app crea 2000 oggetti della classe "Vertex" ogni secondo, il conteggio delle allocazioni aumenta di 2000 ogni secondo e lo vedi quando ordini per classe. Se vuoi riutilizzare questi oggetti per evitare di generare dati inutilizzati, implementa un pool di memoria.

    • Ordina per callstack: utile quando vuoi trovare un percorso caldo in cui viene allocata la memoria, ad esempio all'interno di un loop o di una funzione specifica che esegue molto lavoro di allocazione.

    • Dimensioni ridotte: vengono monitorate solo le dimensioni in memoria dell'oggetto stesso. È utile per monitorare classi semplici composte principalmente da valori primitivi.

    • Dimensioni conservate: mostra la memoria totale dovuta all'oggetto e ai riferimenti a cui fa riferimento esclusivamente l'oggetto. È utile per monitorare la pressione sulla memoria dovuta a oggetti complessi. Per ottenere questo valore, esegui un dump completo della memoria, come mostrato nella Figura 10, e aggiungi Dimensioni conservate come colonna, come mostrato nella Figura 11.

      alt_text Figura 10. Dump completo della memoria.

      Colonna Dimensioni trattenute.
      Figura 11. Colonna Dimensioni trattenute.
  3. Misurare l'impatto di un'ottimizzazione.

    Le GC sono più evidenti e consentono di misurare più facilmente l'impatto delle ottimizzazioni della memoria. Quando un'ottimizzazione riduce la pressione sulla memoria, vengono visualizzate meno raccolte dei rifiuti.

    Per misurare l'impatto dell'ottimizzazione, misura il tempo tra le GC nella sequenza temporale del profiler. Puoi quindi notare che il tempo tra un GC e l'altro è più lungo.

    Gli effetti finali dei miglioramenti della memoria sono i seguenti:

    • È probabile che gli arresti anomali per esaurimento della memoria vengano ridotti se l'app non raggiunge costantemente la pressione della memoria.
    • Avere meno GC migliora le metriche relative al jitter, in particolare nel P99. Questo accade perché le operazioni GC causano contese sulla CPU, il che può comportare il differimento delle attività di rendering durante l'operazione GC.