Analizza con il rendering GPU del profilo

Lo strumento Rendering GPU profilo indica il tempo relativo impiegato da ogni fase della pipeline di rendering per eseguire il rendering del frame precedente. Queste informazioni possono aiutarti a identificare i colli di bottiglia nella pipeline in modo da ottimizzare per migliorare le prestazioni di rendering della tua app.

Questa pagina spiega brevemente cosa succede in ogni fase della pipeline e illustra i problemi che possono causare colli di bottiglia. Prima di leggere questa pagina, dovresti acquisire familiarità con le informazioni presentate in Velocità di rendering della GPU del profilo. Inoltre, per capire come si combinano tutte le fasi, può essere utile esaminare il funzionamento della pipeline di rendering.

Rappresentazione visiva

Lo strumento Rendering GPU mostra le fasi e i relativi tempi sotto forma di grafico: un istogramma con codici colore. La figura 1 mostra un esempio di questo tipo di visualizzazione.

Grafico del rendering GPU del profilo
Figura 1. Grafico del rendering GPU

Ogni segmento di ogni barra verticale visualizzata nel grafico Rendering GPU del profilo rappresenta una fase della pipeline ed è evidenziato utilizzando un colore specifico nel grafico a barre. La figura 2 mostra una legenda del significato di ogni colore visualizzato.

Legenda del grafico Rendering GPU
Figura 2. Legenda del grafico di rendering della GPU del profilo

Una volta compreso il significato di ogni colore, puoi scegliere come target aspetti specifici della tua app per cercare di ottimizzarne il rendimento di rendering.

Fasi e relativi significati

Questa sezione spiega cosa succede durante ogni fase e le cause dei colli di bottiglia da tenere d'occhio.

Gestione dell'input

La fase di gestione dell'input della pipeline misura il tempo che l'app ha dedicato alla gestione degli eventi di input. Questa metrica indica il tempo trascorso dall'app per l'esecuzione del codice chiamato in seguito ai callback degli eventi di input.

Quando questo segmento è grande

Valori elevati in questa area sono in genere il risultato di un lavoro eccessivo o troppo complesso che si verifica all'interno dei callback degli eventi input-handler. Poiché questi callback si verificano sempre sul thread principale, le soluzioni a questo problema si concentrano sull'ottimizzazione del lavoro direttamente o sul trasferimento del lavoro a un thread diverso.

Anche lo scorrimento di un LazyColumn o di un LazyRow può essere visualizzato in questa fase. Una volta che il tocco di un utente viene qualificato come scorrimento, la lista pigra utilizza gli eventi tocco per comporre e disporre gli elementi in modo dinamico. Se la tua app esegue operazioni personalizzate in risposta alle modifiche della posizione di scorrimento, è importante rendere questa operazione il più veloce possibile per evitare interruzioni del frame. Strumenti di profilazione come CPU Profiler in Android Studio o Perfetto possono aiutarti a eseguire ulteriori indagini. Per saperne di più, consulta la panoramica della tracciatura del sistema.

Animazioni

La fase delle animazioni mostra il tempo impiegato per valutare tutti gli stati di animazione in esecuzione in quel frame. Alcune API di animazione comuni in Compose sono animate*AsState, Transition e Animatable. Inoltre, durante questa fase viene eseguito Recomposer per elaborare le modifiche allo stato dello snapshot e aggiornare le composizioni. Ciò significa che l'overhead di ricomposizione spesso emerge direttamente nella fase di animazione.

Per le UI Jetpack Compose, includi la libreria Compose Runtime Tracing per visualizzare tracce di composizione dettagliate insieme agli eventi di sistema.

Quando questo segmento è grande

I valori elevati in questa area sono in genere il risultato di un lavoro in esecuzione a causa di modifiche dello stato determinate dall'animazione. Ad esempio, un'animazione di scorrimento rapido, che scorre l'elenco LazyColumn o LazyRow, causa la composizione, la misurazione e l'allocazione rapide di nuovi elementi dell'elenco.

Misura

Per disegnare i composable sullo schermo, Android esegue tre fasi nei nodi del layout nell'albero UI.

Innanzitutto, il sistema misura i nodi del layout. Ogni composable ha vincoli e modificatori specifici che descrivono i limiti di dimensione dell'oggetto sullo schermo. Alcuni composable possono avere una dimensione specifica e fissa, mentre altri hanno una dimensione che si adatta ai vincoli ereditati dal contenitore del layout principale.

In secondo luogo, il sistema posiziona i nodi del layout. Una volta che Compose calcola le dimensioni dei nodi secondari durante la fase di misurazione, può procedere con la fase di posizionamento, in cui dimensiona e posiziona i nodi del layout sullo schermo.

Il sistema esegue sempre questo layout a una passata per efficienza. Quando un layout componibile viene invalidato, Compose misura quel nodo specifico e propaga gli aggiornamenti del layout alle gerarchie principali solo se le dimensioni o i vincoli del figlio cambiano.

Quando questo segmento è grande

Un segmento di grandi dimensioni in questa area indica che l'app trascorre troppo tempo nella fase di layout, che consiste nel posizionare e determinare le dimensioni dei nodi del layout. Queste operazioni includono l'esecuzione di modificatori di misurazione e posizionamento per i componenti componibili, che possono ritardare la preparazione dei frame se l'albero del layout è eccessivamente complesso. In questi casi, migliorare le prestazioni significa eseguire il benchmarking dell'app Compose e seguire le best practice per il rendimento.

Utilizza CPU Profiler in Android Studio o Perfetto per esaminare i passaggi del layout e identificare i colli di bottiglia. Per saperne di più, consulta Panoramica della tracciatura del sistema.

Disegna

La fase di disegno traduce le operazioni di rendering, come il disegno di uno sfondo, una forma o un testo, in una sequenza di comandi di disegno nativi. Il sistema acquisisce questi comandi in un elenco di visualizzazione per l'esecuzione della GPU.

La barra di disegno registra il tempo necessario per completare l'acquisizione dei comandi nell'elenco di visualizzazione, per tutti i nodi di layout che dovevano essere aggiornati sullo schermo per questo frame. Il tempo misurato si applica anche a qualsiasi logica di disegno personalizzata che potresti avere all'interno di modificatori di disegno o di un composable Canvas.

Quando questo segmento è grande

In termini semplificati, puoi interpretare questa metrica come il tempo impiegato per eseguire tutti i comandi di disegno per ogni nodo di layout invalidato. Questa misurazione include il tempo impiegato per inviare questi comandi ai nodi secondari e ai drawables vettoriali. Per questo motivo, quando vedi questo picco della barra, la causa potrebbe essere che molti composable sono diventati improvvisamente non validi. L'invalidazione rende necessario eseguire nuovamente i comandi di disegno e rigenerare gli elenchi di visualizzazione dei nodi di layout. In alternativa, un tempo prolungato potrebbe essere il risultato di alcuni composable o canvas personalizzati che hanno una logica estremamente complessa nella loro implementazione di DrawScope.

Inoltre, Compose spesso gestisce le passate di misurazione e layout interne in quella che la piattaforma considera la fase di disegno. Di conseguenza, una barra di disegno elevata può essere causata da operazioni di misurazione/layout interne costose o eccessive anziché solo da comandi di disegno. In caso di dubbi, acquisisci una traccia Perfetto per verificare se il sovraccarico deriva dalle routine di disegno o dalle passate di misurazione e layout di Compose.

Carica

La metrica di caricamento rappresenta il tempo necessario per trasferire gli oggetti bitmap dalla memoria della CPU alla memoria della GPU durante il frame corrente.

In quanto processori diversi, la CPU e la GPU hanno aree di RAM diverse dedicate all'elaborazione. Quando disegni una bitmap su Android, il sistema la trasferisce alla memoria GPU prima che la GPU possa eseguirne il rendering sullo schermo. La GPU memorizza nella cache la bitmap in modo che il sistema non debba trasferire nuovamente i dati, a meno che la texture non venga eliminata dalla cache delle texture della GPU.

Nota:sui dispositivi Lollipop, questa fase è viola.

Quando questo segmento è grande

Tutte le risorse per un frame devono risiedere nella memoria della GPU prima di poter essere utilizzate per disegnare un frame. Ciò significa che un valore elevato per questa metrica potrebbe indicare un numero elevato di caricamenti di risorse di piccole dimensioni o un numero ridotto di risorse molto grandi. Un caso comune è quando un'app visualizza una singola bitmap vicina alle dimensioni dello schermo. Un altro caso si verifica quando un'app mostra un numero elevato di miniature.

Per ridurre le dimensioni di questa barra, puoi utilizzare tecniche come:

  • Assicurati che le risoluzioni bitmap non siano molto più grandi delle dimensioni con cui verranno visualizzate. Ad esempio, evita di visualizzare un'immagine 1024x1024 come immagine 48x48.
  • Sfruttando librerie moderne come Coil per pre-caricare in modo asincrono una bitmap prima della fase di sincronizzazione successiva.

Eseguire comandi

Il segmento dei comandi di emissione rappresenta il tempo necessario per emettere tutti i comandi necessari per disegnare gli elenchi di visualizzazione sullo schermo.

Affinché il sistema disegni gli elenchi di visualizzazione sullo schermo, invia i comandi necessari alla GPU. In genere, esegue questa azione tramite l'API OpenGL ES.

Questa procedura richiede un po' di tempo, poiché il sistema esegue la trasformazione finale e il ritaglio per ogni comando prima di inviarlo alla GPU. Viene quindi generato un overhead aggiuntivo sul lato della GPU, che calcola i comandi finali. Questi comandi includono trasformazioni finali e ritagli aggiuntivi.

Quando questo segmento è grande

Il tempo trascorso in questa fase è una misura diretta della complessità e della quantità di elenchi di visualizzazione che il sistema esegue il rendering in un determinato frame. Ad esempio, molte operazioni di disegno, soprattutto nei casi in cui ogni primitiva di disegno ha un piccolo costo intrinseco, potrebbero aumentare questo tempo. Ad esempio:

for (i in 0 until 1000) {
    canvas.drawPoint()
}

è molto più costoso da emettere rispetto a:

canvas.drawPoints(thousandPointArray)

Non sempre esiste una correlazione 1:1 tra l'emissione di comandi e il disegno effettivo degli elenchi di visualizzazione. A differenza della barra dei comandi dei problemi, che acquisisce il tempo necessario per inviare i comandi di disegno alla GPU, la metrica Disegno rappresenta il tempo necessario per acquisire i comandi emessi nell'elenco di visualizzazione.

Questa differenza si verifica perché gli elenchi visualizzati vengono memorizzati nella cache del sistema, ove possibile. Di conseguenza, ci sono situazioni in cui uno scorrimento, una trasformazione o un'animazione richiedono al sistema di inviare nuovamente un elenco di visualizzazione, ma non di ricostruirlo da zero, ovvero di acquisire nuovamente i comandi di disegno. Di conseguenza, puoi vedere una barra dei comandi di emissione elevata senza vedere una barra dei comandi di disegno elevata.

Scambia buffer

Una volta che Android ha terminato l'invio dell'elenco di visualizzazione alla GPU, il sistema emette un comando finale per comunicare al driver grafico che il frame corrente è stato completato. A questo punto, il driver può finalmente presentare l'immagine aggiornata sullo schermo.

Quando questo segmento è grande

È importante capire che la GPU esegue il lavoro in parallelo con la CPU. Il sistema Android invia comandi di disegno alla GPU e poi passa all'attività successiva. La GPU legge questi comandi di disegno da una coda e li elabora.

Nelle situazioni in cui la CPU invia comandi più velocemente di quanto la GPU li consumi, la coda di comunicazione tra i processori può riempirsi. Quando ciò si verifica, la CPU si blocca e attende che ci sia spazio nella coda per inserire il comando successivo. Questo stato di coda piena si verifica spesso durante la fase di scambio dei buffer, perché a quel punto è stato inviato un intero frame di comandi.

La chiave per mitigare questo problema è ridurre la complessità del lavoro che si svolge sulla GPU, in modo simile a quanto faresti per la fase dei comandi di emissione.

Varie

Oltre al tempo necessario al sistema di rendering per svolgere il proprio lavoro, sul thread principale viene eseguito un ulteriore insieme di operazioni che non hanno nulla a che fare con il rendering. Il tempo impiegato per questo lavoro viene segnalato come tempo vario. Il tempo vario in genere rappresenta il lavoro che potrebbe verificarsi sul thread dell'interfaccia utente tra due frame di rendering consecutivi.

Quando questo segmento è grande

Se questo valore è elevato, è probabile che la tua app abbia callback, intent o altre operazioni che devono essere eseguite su un altro thread. Strumenti come CPU Profiler in Android Studio o Perfetto possono fornire visibilità sulle attività in esecuzione nel thread principale. Queste informazioni possono aiutarti a scegliere come migliorare il rendimento. Per saperne di più, consulta Panoramica della tracciatura del sistema.