Case study: in che modo il team Wear OS di Gmail ha migliorato del 50% l'avvio dell'app

L'avvio dell'app rappresenta la prima impressione dell'app per gli utenti. Gli utenti non vogliono aspettare, perciò devi assicurarti che l'app si avvii rapidamente. Per mostrarti in che modo un team di sviluppo di app reale ha trovato e diagnosticato problemi nell'avvio dell'app, di seguito ti spieghiamo cosa ha fatto il team per Wear OS di Gmail.

Il team di Gmail Wear OS ha intrapreso un'operazione di ottimizzazione, con particolare attenzione alle prestazioni dell'avvio delle app e del rendering in fase di runtime, per soddisfare i criteri di prestazioni delle app del team. Tuttavia, anche se non hai soglie specifiche di destinazione, c'è quasi sempre spazio per migliorare l'avvio dell'app se dedichi un po' di tempo a esaminarla.

Acquisisci una traccia e controlla l'avvio dell'app

Per iniziare l'analisi, acquisisci una traccia che includa l'avvio dell'app per un'analisi più approfondita in Perfetto o Android Studio. Questo case study usa Perfetto perché mostra cosa sta succedendo sul sistema di dispositivi, oltre all'app. Quando carichi la traccia in Perfetto, l'aspetto è simile a questo:

Figura 1. Vista iniziale della traccia in Perfetto.

Poiché l'obiettivo è migliorare l'avvio dell'app, individua la riga con la metrica personalizzata Startup di app Android; è utile bloccarla nella parte superiore della visualizzazione facendo clic sull'icona a forma di puntina che compare quando passi il mouse sopra la riga. La barra, o sezione, che vedi nella riga Avvio di app Android indica l'intervallo di tempo coperto dall'avvio dell'app, finché il primo frame dell'app viene visualizzato sullo schermo, quindi dovresti cercare eventuali problemi o colli di bottiglia.

Riga Avvio di app Android in cui è evidenziata l'opzione per fissare.
Figura 2. Fissa la metrica personalizzata Avvio di app Android nella parte superiore della dashboard per un'analisi più semplice.

Tieni presente che la metrica Avvio di app Android rappresenta il tempo per la prima visualizzazione, anche se utilizzi reportFullyDrawn(). Per identificare il tempo per la visualizzazione completa, cerca reportFullyDrawn() nella casella di ricerca Perfetto.

Controlla il thread principale

Per prima cosa, esamina cosa sta succedendo nel thread principale. Il thread principale è molto importante perché di solito è lì che viene eseguito tutto il rendering dell'interfaccia utente; quando è bloccato, non può essere eseguito alcun disegno e la tua app sembra essere bloccata. Quindi devi assicurarti che sul thread principale non vengano eseguite operazioni a lunga esecuzione.

Per trovare il thread principale, trova la riga con il nome del pacchetto dell'app ed espandila. Le due righe con lo stesso nome del pacchetto (di solito le prime due righe della sezione) rappresentano il thread principale. Delle due righe principali dei thread, la prima rappresenta lo stato della CPU, mentre la seconda rappresenta i punti di traccia. Fissa le due righe principali del thread sotto la metrica Avvio app per Android.

Avvio di app Android e righe dei thread principali fissate.
Figura 3. Fissa le righe del thread principale sotto la metrica personalizzata Avvio di app Android per facilitare l'analisi.

Tempo trascorso in stato eseguibile e contesa della CPU

Per ottenere una visualizzazione aggregata dell'attività della CPU durante l'avvio dell'app, trascina il cursore sul thread principale per acquisire l'intervallo di tempo di avvio dell'app. Il riquadro Stati del thread visualizzato mostra la quantità totale di tempo trascorso in ogni stato della CPU all'interno dell'intervallo di tempo selezionato.

Osserva il tempo trascorso nello stato Runnable. Quando un thread è nello stato Runnable, può funzionare, ma non è programmato alcun lavoro. Ciò potrebbe indicare che il dispositivo è sottoposto a un carico elevato e non è in grado di programmare attività ad alta priorità. L'app principale, visibile all'utente, ha la massima priorità nella pianificazione, pertanto un thread principale inattivo indica spesso che i processi intensivi all'interno dell'app, come il rendering dell'animazione, competono con il thread principale per il tempo di CPU.

Thread principale evidenziato con il tempo totale in diversi stati nel riquadro Stati dei thread.
Figura 4. Valuta il tempo relativo negli stati Runnable-Running per avere un'idea iniziale di quanta contesa della CPU ci sia.

Maggiore è il rapporto tra il tempo nello stato Runnable e il tempo nello stato Running, maggiore è la probabilità che si verifichi una contesa della CPU. Quando esamini i problemi di prestazioni in questo modo, concentrati prima sul frame a più lunga durata e lavora su quelli più piccoli.

Quando analizzi il tempo trascorso nello stato Runnable, prendi in considerazione l'hardware del dispositivo. Poiché l'app raffigurata è in esecuzione su un dispositivo indossabile con due CPU, è previsto che ci sia più tempo trascorso nello stato Runnable e maggiore contesa di CPU con altri processi rispetto a un dispositivo con più CPU. Anche se nello stato Runnable viene trascorso più tempo del previsto per una tipica app per smartphone, questo potrebbe essere comprensibile nel contesto degli indossabili.

Tempo trascorso nel mese di OpenDexFilesFromOat*

Ora controlla il tempo trascorso in OpenDexFilesFromOat*; nella traccia, avviene contemporaneamente alla sezione bindApplication. Questa sezione rappresenta il tempo impiegato per leggere i file DEX dell'applicazione.

Transazioni binder bloccate

Poi controlla le transazioni di Binder. Le transazioni di Binder rappresentano chiamate tra il client e il server: in questo caso, l'app (client) chiama il sistema Android (server) con un binder transaction e il server risponde con un binder reply. Assicurati che l'app non crei transazioni binder non necessarie durante l'avvio, poiché aumentano il rischio di contesa della CPU. Se possibile, posticipa il lavoro che prevede chiamate a Binder dopo il periodo di avvio dell'app. Se devi effettuare transazioni con Binder, assicurati che non richiedano più tempo della frequenza di aggiornamento di Vsync del dispositivo.

La prima transazione di Binder, che di solito si verifica contemporaneamente alla sezione ActivityThreadMain, sembra essere piuttosto lunga in questo caso. Per scoprire di più sulle possibili conseguenze, procedi come segue:

  1. Per visualizzare la risposta di binder associata e scoprire di più su come viene assegnata la priorità alla transazione di binder, fai clic sulla sezione di interesse della transazione di binder.
  2. Per visualizzare la risposta a Binder, vai al riquadro Selezione corrente e fai clic su Rispondi a Binder nella sezione Thread che segui. Il campo Thread indica anche il thread su cui si verifica la risposta di binder se vuoi accedervi manualmente; la procedura sarà diversa. Viene visualizzata una riga che collega la transazione e la risposta di Binder.

    Una linea collega la chiamata a Binder e la risposta.
    Figura 5. Identifica le transazioni di Binder che avvengono durante l'avvio dell'app e valuta se puoi differirle.
  3. Per vedere come il server di sistema gestisce questa transazione di Binder, fissa i thread Cpu 0 e Cpu 1 nella parte superiore dello schermo.

  4. Trova i processi di sistema che gestiscono la risposta di binder individuando le sezioni che includono il nome del thread di risposta di binder, in questo caso "Binder:687_11 [2542]". Fai clic sui processi di sistema pertinenti per ulteriori informazioni sulla transazione Binder.

Dai un'occhiata a questo processo di sistema associato alla transazione di interesse di Binder che si verifica sulla CPU 0:

Processo di sistema con stato finale "Runnable (Preempted).
Figura 6. Il processo di sistema è in stato Runnable (Preempted), a indicare che sta subendo un ritardo.

Lo stato finale indica Runnable (Preempted), il che significa che il processo viene ritardato perché la CPU sta facendo qualcos'altro. Per scoprire quali elementi vengono prerilasciati, espandi le righe Eventi Ftrace. Nella scheda Eventi di Ftrace che diventa disponibile, scorri e cerca gli eventi correlati al thread di interesse "Binder:687_11 [2542]". Nel momento in cui il processo di sistema viene prerilasciato, si sono verificati due eventi del server di sistema che includono l'argomento"decon", il che significa che sono correlati al controller display. Sembra ragionevole perché il controller posiziona i frame sullo schermo, un'attività importante. Lascia gli eventi invariati.

Eventi FTrace associati alla transazione di Binder di interesse evidenziata.
Figura 7. Gli eventi FTrace indicano che la transazione binder viene ritardata dagli eventi del controller display.

Attività JIT

Per esaminare l'attività di compilazione just-in-time (JIT), espandi i processi appartenenti alla tua app, trova le due righe "Pool di thread thread" e fissale nella parte superiore della visualizzazione. Poiché questa app trae vantaggio dai profili di riferimento durante l'avvio dell'app, si verifica pochissima attività JIT prima che venga disegnato il primo frame, indicato alla fine della prima chiamata Choreographer.doFrame. Tuttavia, nota il motivo dell'avvio lento JIT compiling void, che indica che l'attività di sistema che si verifica durante il punto di traccia etichettato Application creation sta causando molta attività JIT in background. Per risolvere il problema, aggiungi gli eventi che si verificano poco dopo che il primo frame è stato attirato nel profilo di riferimento espandendo la raccolta del profilo a un punto in cui l'app è pronta per essere utilizzata. In molti casi, puoi farlo aggiungendo una riga alla fine del test Macrobenchmark della raccolta del profilo di riferimento che attende la visualizzazione di un particolare widget dell'interfaccia utente sullo schermo, a indicare che la schermata è completamente compilata.

Pool di thread jit con la sezione "Jit compilazione void" evidenziata.
Figura 8. Se noti molte attività JIT, espandi il tuo profilo di riferimento fino al punto in cui l'app è pronta per essere utilizzata.

Risultati

In seguito a questa analisi, il team di Gmail Wear OS ha apportato i seguenti miglioramenti:

  • Poiché durante l'avvio dell'app hanno notato una contesa sull'attività della CPU, hanno sostituito l'animazione della rotellina usata per indicare che l'app si sta caricando con una singola immagine statica. Ha inoltre prolungato la schermata iniziale rinviando lo stato shimmer, il secondo stato della schermata utilizzato per indicare che l'app è in fase di caricamento, per liberare risorse della CPU. Questo ha migliorato la latenza di avvio delle app del 50%.
  • Dall'analisi del tempo trascorso in OpenDexFilesFromOat* e nell'attività JIT, ha consentito la riscrittura di R8 dei profili di riferimento. Ciò ha migliorato la latenza di avvio dell'app del 20%.

Ecco alcuni suggerimenti del team su come analizzare le prestazioni dell'app in modo efficiente:

  • Configura un processo continuo in grado di raccogliere automaticamente tracce e risultati. Valuta la possibilità di configurare il tracciamento automatico per la tua app utilizzando il benchmarking.
  • Utilizza il test A/B per le modifiche che ritieni miglioreranno le cose e rifiutale in caso contrario. Puoi misurare le prestazioni in scenari diversi utilizzando la libreria Macrobenchmark.

Per saperne di più, consulta le seguenti risorse: