L'ottimizzazione delle app per Android richiede una visibilità approfondita dei flussi di lavoro simultanei e delle sospensioni delle coroutine.
Sebbene la profilazione locale offra un'istantanea utile, l'applicazione di questi insight alla produzione senza overhead rimane una sfida.
Questa sessione fornisce un progetto di architettura per un loop di feedback continuo sulle prestazioni.
androidx.tracing 2.0: scambia i backend in modo dinamico e propaga i contesti delle coroutine (flussi Perfetto) tra i thread.
Strumentazione intelligente: utilizza il tracciamento ART locale per trovare gli hotspot, quindi automatizza l'inserimento di punti di traccia a basso overhead.
Macrobenchmarking: monitora le regressioni e verifica i miglioramenti delle prestazioni sui dispositivi fisici in CI.
Tracciamento sul campo: vai oltre il tracciamento locale e CI con ProfilingManager.
traceCoroutine o passare i token di propagazione nel codice basato su callback.Trascrizione
Rahul Ravikumar
Buon pomeriggio.
Mi chiamo Rahul.
Sono un ingegnere del team Android Developer Experience.
Mi occupo delle prestazioni di app e librerie e questo intervento riguarda il tracciamento.
Prima di approfondire il tracciamento, voglio stabilire un vocabolario condiviso in modo che tu possa seguire.
Quindi, ogni volta che mi riferisco al tracciamento in questo intervento, parlo di tracciamento esplicito, ovvero della strumentazione manuale del codice per sapere cosa sta facendo l'app.
Non sto parlando dei profiler automatici, di cui parlerò più avanti.
Quindi, il tracciamento manuale è l'arte di lasciare tracce nella codebase, in modo da poterle analizzare in un secondo momento quando si verifica un problema di prestazioni o qualsiasi altro problema.
In genere è molto meglio dei log.
Non so se hai mai visto un output Logcat di un'app di grandi dimensioni.
In genere si tratta di un enorme blocco di testo che non è facile da analizzare.
Il tracciamento, invece, è molto più semplice.
È più facile da visualizzare.
Hai delle durate.
Hai una struttura.
Hai a disposizione alcuni strumenti, quindi puoi anche analizzare i dati in modo approfondito e vedere esattamente cosa è successo e quando.
Quindi è molto più bello.
E puoi, sai, indirizzare i tuoi agenti verso una traccia e dire, fai domande.
Puoi utilizzare strumenti come Trace Processor, benchmark e persino convertire le tracce in metriche.
Queste sono tutte cose che puoi fare con le tracce e che è difficile fare con Logcat.
La profilazione, invece, e parlo di profilazione automatica, di solito è una risposta a un problema che conosci.
Ad esempio, potresti aver notato che la tua app si avvia lentamente o che qualcosa non va e vuoi restringere il campo e capire cosa sta succedendo.
A questo punto, se non hai già tracce manuali che ti aiutino, entra in gioco la profilazione automatica.
Potresti utilizzare un metodo come la traccia metodo ART, in cui strumenti ogni singola voce e uscita del metodo.
In alternativa, potresti esaminare il campionamento dello stack con strumenti come Simpleperf.
Quindi, anziché utilizzare ogni singola voce e uscita del metodo e avere un overhead elevato, esamini gli esempi di stack di chiamate e vedi cosa si trova nel percorso critico della tua app.
Siamo nel 2026, la RAM è un problema ovunque, quindi potresti esaminare la memoria, le perdite di memoria e i profili heap.
Quindi, questo è un altro profiler di cui puoi usufruire.
Ad esempio, puoi esaminare i percorsi di conservazione dell'heap.
Puoi vedere quale codice sta allocando memoria e questi sono tutti i tipi di analisi che puoi eseguire con questo strumento.
Infatti,
Ora abbiamo questo vocabolario.
Ora parliamo del motivo per cui abbiamo creato una nuova libreria.
Le API di tracciamento 1.0 sono state introdotte originariamente in Android Jelly Bean.
Questo è successo nel 2012.
ovvero più di 14 anni fa.
Da allora, le API non si sono evolute quanto pensi.
Abbiamo aggiunto alcune estensioni.
Gli eventi e i contatori asincroni sono stati aggiunti in Android 10.
Questo è successo nel 2019.
E questo è tutto.
Da allora l'API non si è evoluta.
Tuttavia, l'ecosistema si è evoluto molto.
Quindi, il tracciamento della piattaforma è migliorato notevolmente, ma nulla di tutto ciò è stato trasferito in termini di aggiunte di API per te.
Questo è un aspetto che vogliamo cambiare.
L'altro grande problema è che il tracciamento ha un backend di tracciamento fisso nella versione 1.0, il che significa che ogni volta che aggiungi una sezione di traccia nel codice, questa finisce sempre nel buffer di tracciamento del sistema.
Atrace è il sottosistema di tracciamento di Android che possiede questi dati.
Quindi, se volete prendere le vostre tracce e convertirle in telemetria sul campo, è davvero molto difficile, perché ora, innanzitutto, dovete capire come atrace memorizza questi dati.
È un formato ben documentato, ma potenzialmente difficile da utilizzare.
Questo è un problema che vogliamo risolvere.
Per questo motivo, vogliamo fornirti gli strumenti per portare i tuoi dati di tracciamento ovunque tu voglia.
Inoltre, in Tracing 1.0 avevamo una limitazione per cui le etichette erano limitate a 128 caratteri.
Ora, questa era probabilmente una restrizione ragionevole nel 2012, ma nel 2026 sembra piuttosto strana.
Inoltre, non puoi aggiungere metadati extra alla traccia, il che è ancora più problematico.
Poi, c'è l'overhead.
Quindi, ogni volta che tracci la tua app, devi essere estremamente consapevole dell'overhead, perché l'atto di osservare un sistema non dovrebbe cambiare ciò che il sistema sta facendo dietro le quinte.
Quindi, se vuoi riprodurre una race condition, il tracciamento non dovrebbe farla scomparire.
Quindi, devi assicurarti di preservare tutta la semantica che stai utilizzando.
Tracing 1.0 aveva un overhead relativamente elevato e volevamo migliorarlo.
Ok.
Ho parlato molto dei problemi.
Ora, esaminiamo alcuni dei problemi visivamente e vediamo come si aggravano in pratica.
Qui abbiamo una traccia Perfetto.
È per un'app, com.example.app.
Abbiamo un thread principale e una singola sezione denominata initializeComponent.
Ora, idealmente, vogliamo maggiori dettagli.
Aggiungiamo un dettaglio.
Ora sappiamo che initializeComponent chiama performInitialize.
Non sappiamo ancora quale componente venga inizializzato.
Ora, l'unico strumento a nostra disposizione con Tracing 1 è il nome, quindi lo aggiungiamo al nome della sezione.
Ricorda che i nomi delle sezioni di traccia sono limitati a 128 caratteri, quindi vengono troncati nella traccia.
Ora, oltre al danno la beffa: l'overhead del troncamento è anche l'overhead che si verifica durante il tracciamento.
Ora, questo problema si presenta abbastanza rapidamente.
Quindi, questo è un aspetto che volevamo davvero correggere.
Hai visto la traccia di Perfetto e il suo aspetto visivo.
Ora, vediamo la cosa dal punto di vista della scrittura del codice per emettere queste sezioni di traccia.
Anche in questo caso, abbiamo l'API initializeComponent.
Accetta un contesto e un nome, chiama performInitialize.
Fin qui tutto bene.
Ora vogliamo aggiungere una traccia con l'API Tracing 1.0, quindi importiamo androidx.tracing.trace e utilizziamo un blocco di traccia.
Assegna un nome.
Qui è performInitialize.
Ma come volevamo fare prima, volevamo conoscere il nome del componente, quindi utilizzeremo l'interpolazione di stringhe di Kotlin e aggiungeremo il nome.
Ed ecco il primo problema.
Quindi, qui abbiamo il limite di 128 caratteri.
Rahul Ravikumar
Possiamo risolvere questo problema con Tracing 2.
Anziché androidx.tracing.trace, utilizza Tracer.
L'API è molto simile.
Hai Tracer.global.trace invece di trace.
Hai una categoria.
Parleremo delle categorie in modo più dettagliato in seguito, ma fondamentalmente hai un nome.
Inoltre, hai ancora il blocco di codice che stai cercando di tracciare.
Inoltre, non hai più il limite di caratteri di 128 caratteri.
Ho parlato di overhead di tracciamento.
Vorrei entrare un po' più nel dettaglio e vedere come abbiamo migliorato questo aspetto.
Su Pixel 6a con Android 16, l'inserimento di una singola sezione di inizio e di fine ha richiesto circa 1200 nanosecondi.
Potrebbe non sembrare molto, ma se hai un codice molto attivo che aggiunge molte sezioni di traccia, questo tempo viene sottratto a quello che la tua app potrebbe altrimenti dedicare al rendering dei frame.
Non vogliamo sottrarti questo tempo.
In realtà, abbiamo migliorato un po' le cose con l'SDK AndroidX Perfetto.
Si trattava di un SDK sperimentale che abbiamo introdotto per il tracciamento della composizione e che ha migliorato notevolmente la situazione.
Ora, non volevamo davvero che lo distribuissi perché questo è l'SDK Perfetto C con associazioni JNI e l'SDK C è di circa 4 MB all'interno dell'APK.
Non è un prodotto che avremmo mai voluto che tu spedissi.
Ora, con Tracing 2, abbiamo una libreria creata appositamente.
Abbiamo migliorato ulteriormente l'overhead e ora sarai felice di sapere che Tracing 2 è 7 volte più veloce, il che significa che hai più tempo per fare le cose che volevi fare e, come sai, l'infra richiede meno tempo.
Cosa rende possibile un'iniziativa del genere che prima non era possibile?
Un esempio è la funzionalità di tracciamento del runtime di Metro.
Per chi non lo sapesse, Metro è una libreria di inserimento delle dipendenze.
Supporta Kotlin Multiplatform.
Utilizza un plug-in del compilatore Kotlin.
Genera tutto il codice per l'inserimento delle dipendenze e si integra con Tracing 2.0 per semplificare la visualizzazione del costo di ogni singolo @Inject nel codice.
Ecco un esempio.
Supponiamo di avere un grafico dell'app.
In Metro, tutto è limitato a un grafico, quindi abbiamo un unico campo, AnalyticsTracker.
Abbiamo anche un binding @Provides.
Abbiamo un client di rete con OkHttpClient come impl, l'implementazione di AnalyticsTracker proviene da AnalyticsTrackerImpl e abbiamo bisogno di un'istanza del client di rete, che il grafico sa come fornire.
Quindi, in background, il plug-in del compilatore genera un codice simile a questo.
Quindi, se hai AnalyticsTracker, il getter crea un'istanza del client di rete e la passa al costruttore di AnalyticsTrackerImpl, il che è positivo.
Ma ora, con il tracciamento abilitato, puoi vedere il costo di creazione non solo dell'implementazione del tracker, ma di ogni singola dipendenza di cui ha bisogno.
Ora, quindi, ti consentiamo di vedere non solo il costo della creazione di un'istanza del tipo, ma anche tutte le sue dipendenze in una traccia, il che è davvero efficace.
Utilizzando queste informazioni, Wise, un'app finanziaria, ha migliorato l'avvio dell'app del 45%.
Sono stati in grado di rimandare le cose che non erano necessarie per l'inizializzazione durante l'avvio dell'app e hanno anche identificato alcuni elementi con un ambito errato, in cui le cose che avrebbero dovuto essere un singleton non erano effettivamente singleton per caso, e queste sono cose che puoi vedere e correggere facilmente nel tuo codice.
Se guardi le metriche della startup, sono davvero impressionanti.
Non solo hanno migliorato il tempo di visualizzazione del frame iniziale, ma anche quello di visualizzazione completa.
Un altro partner, BandLab, utilizza le stesse informazioni sul CI.
In questo modo, non solo conosci il costo di ogni singola associazione @Inject, ma sai anche quale thread ha sostenuto quel costo.
Quindi, una volta che lo sai, puoi trovare tutti i costosi binding @Inject sul thread principale e nella tua CI, puoi eseguire una query per dire, hey, dammi un elenco di tutti i costosi binding e poi attribuiscilo alla modifica che li ha introdotti e poi puoi ora incolpare la modifica effettiva e provare a risolverla.
Rahul Ravikumar
Ok.
Finora ho parlato di tutti i miglioramenti rispetto a Tracing 1, ma ora parlerò delle nuove funzionalità di Tracing 2 che prima non erano possibili.
Una delle funzionalità principali è la propagazione del contesto. Poiché le app moderne stanno diventando sempre più complesse, i workload vengono eseguiti su più thread, più coroutine e più processi.
Vuoi essere in grado di visualizzare il modo in cui una richiesta si propaga tra ogni singolo thread e processo.
Ed è proprio questo che rende possibile la propagazione del contesto.
Quindi, se ti dessi solo un elenco di sezioni senza dirti nulla su cosa ha causato cosa, sarebbero utili, ma non sarebbe facilissimo ragionarci sopra.
Se invece ti dicessi che questa è la catena causale degli eventi che si sono verificati, sarebbe molto più facile per te capire cosa è successo e associare il costo di ogni singola sezione.
Questo è ciò che rende disponibile la propagazione del contesto.
Ti farà piacere sapere che la propagazione del contesto è completamente supportata, quindi ecco un esempio reale.
Tornando alla traccia Perfetto di com.example.app, supponiamo che performInitialize richiedesse troppo tempo e che tu volessi posticipare il costo o spostarlo in un altro thread.
Qualcosa che puoi fare tranquillamente.
Supponiamo che io abbia spostato performInitialize in un altro thread.
Ora hai perso la natura gerarchica della sezione.
Quindi, ora non puoi più dire che initializeComponent è stato ciò che ha causato performInitialize, a meno che tu non guardi il codice.
In un mondo ideale, vorresti una freccia che ti dica che A ha causato B e poi puoi ragionare sulla catena causale degli eventi.
Potresti aver visto anche un tracciamento del sistema in cui lo facciamo per le transazioni di binder sulla piattaforma.
Qui puoi vedere che il benchmark di Compose effettua una transazione binder a system_server per preparare un frame.
Quindi, queste sono cose che sono sempre state possibili dal punto di vista del tracciamento della piattaforma, ma non abbiamo mai reso disponibili gli strumenti agli sviluppatori di app.
Ora la propagazione del contesto delle coroutine Kotlin è completamente supportata in Tracing 2.0.
Non devi fare nulla.
Basta estrarlo dalla confezione.
In questo caso, abbiamo un singolo metodo di sospensione chiamato fetchProfile.
Chiama un client.get, che è un altro metodo di sospensione, quindi passa a Dispatchers.Default per deserializzare la risposta.
Un codice di coroutine molto standard, qualcosa con cui probabilmente hai a che fare ogni giorno.
Ora, supponiamo di voler tracciare questo percorso.
Aggiungiamo una sezione di traccia.
Quindi, in questo caso, invece di trace, utilizzeremo l'API traceCoroutine, dato che si tratta di un blocco di codice di sospensione, e poi, per la chiamata interna prima di passare al contesto Dispatchers.Default, aggiungeremo un'altra sezione di traccia interna utilizzando la stessa API traceCoroutine.
In una traccia, vedrai qualcosa di simile.
Vedrai che il nostro lavoro è iniziato in worker-1 e poi, in realtà, ha cambiato contesto ed è passato a worker-3. Nella traccia vedrai delle frecce utili che lo indicano.
Inoltre, se ingrandisci il quadrato al centro, puoi vedere che la deserializzazione è iniziata effettivamente in worker-1 e la chiamata a withContext è ciò che ha causato il cambio di contesto, per poi continuare su worker-3 e poi fetchProfile è ripreso su worker-3 perché il meccanismo delle coroutine vuole ridurre al minimo il numero di cambi di contesto che si verificano.
Puoi vedere esattamente quando una coroutine è stata sospesa, quando è stata ripresa e puoi vedere l'intera catena causale degli eventi.
Un'altra cosa che Tracing 2 rende possibile è la possibilità di aggiungere metadati personalizzati a una traccia.
In passato, l'unico strumento a tua disposizione era il nome della sezione della traccia, ma ora puoi aggiungere un blocco di metadati che ti consente di aggiungere tutte le voci di metadati che vuoi.
Puoi aggiungere questa informazione alla traccia.
Quando esamini la traccia effettiva, puoi vedere che tutti i metadati che hai aggiunto sono effettivamente presenti nella traccia, pronti per essere utilizzati da tutti gli strumenti.
Ora puoi incorporare tutto ciò che sai, tutte le informazioni aggiuntive che hai sempre voluto inserire.
Ti ho fornito una serie di funzionalità astratte rese possibili dal tracciamento, ma tutto questo si distingue in una nuova funzionalità di Jetpack Compose chiamata Tracciamento della ricomposizione.
Questa funzionalità è inclusa in Compose 1.13, quindi è già pronta per l'uso.
Ecco un esempio.
Questo è il tracciamento della composizione che probabilmente conoscete tutti.
Questa funzionalità esiste fin dagli inizi di Compose.
Ti indica il costo relativo di un composable e di tutte le sue, sai, sottosezioni, nonché il costo di ciascuna sul thread in cui si trovavano.
Quindi è abbastanza semplice.
Ciò che non ti dice è il contesto.
Non ti dice perché è successo.
Non ti dice il motivo per cui gli elementi sono stati ricomposti.
Quindi, se dovessi rimpicciolire, questa è la regione di interesse che stavamo osservando nella slide precedente.
A sinistra, puoi vedere che si è verificato un evento di input di invio.
Forse è questo che ha modificato uno stato e forse è questo che ha causato la ricomposizione.
Ma non è chiaro.
Non lo sai con certezza.
Tuttavia, con il tracciamento della ricomposizione, ora aggiungiamo metadati personalizzati alla traccia che ti dicono esattamente perché si sono verificate le ricomposizioni.
Inoltre, ora è molto facile capire che questi eventi sono correlati.
Se fai clic su questi elementi, ti verrà mostrato esattamente lo stato che è cambiato.
Ti dirà che si trattava di uno stato, un valore di stato modificabile che è stato impostato su false, mentre in precedenza era true.
Inoltre, ti indicheremo lo stack di chiamate esatto che l'ha causato.
Quindi, qui puoi vedere che si è verificato AbstractClickableNode.performClick, che ha causato la modifica dello stato modificabile, che a sua volta ha causato la ricomposizione.
Si tratta di una funzionalità davvero interessante, che non era possibile utilizzare in precedenza.
Questi strumenti sono disponibili anche, scusa, tutte queste funzionalità sono disponibili anche nel nuovo strumento Android Performance Analyzer, se vuoi provare alcuni degli strumenti agentici.
E a destra.
Come ho detto, la propagazione del contesto è completamente supportata per le coroutine Kotlin.
Ma cosa succede se il codice utilizza un mix di coroutine e codice non coroutine, o se esegui i thread autonomamente, o se hai un codice brownfield troppo vecchio che utilizza i callback per qualche motivo?
Ok.
La buona notizia è che puoi comunque ottenere tutti i vantaggi della propagazione del contesto.
Supponiamo di avere un'API brownfield che utilizza un callback e un executor.
L'implementazione utilizza executor.execute, chiama runCatching, che è il dettaglio dell'implementazione, riceve la risposta e poi comunica la risposta al callback.
Ok.
Ora, se voglio tracciare questo, eseguo Tracer.global.trace, assegno un'etichetta e poi racchiudo il blocco.
Tuttavia, dato che voglio anche preservare la catena causale degli eventi, accetterò un nuovo tipo facoltativo, chiamato token di propagazione.
Quello che farò è semplicemente passare il token all'API Trace interna.
Quindi, chiunque chiami questo metodo può facoltativamente fornirmi il token di propagazione, in modo che io possa preservare la catena causale degli eventi che si verificano.
Ok.
Ora, il chiamante di quell'API può essere un'API di sospensione.
Qui ho una funzione di sospensione profileRequest, che ora chiamerà l'API che utilizza un callback e un esecutore.
In questo caso, poiché sto collegando due mondi, ovvero il mondo delle funzioni di sospensione con quello dei callback, utilizzerò il builder suspendCoroutine.
Riceverò una continuazione, poi richiamerò l'API e riprenderò la continuazione nel callback, una volta ricevuta la risposta.
Ecco l'esecutore che sto definendo, chiamato profileRequestContext.
Si tratta di un esecutore a thread singolo.
Inoltre, poiché traceCoroutine incorpora un token di propagazione all'interno del contesto della coroutine, posso ottenere il token di propagazione dall'interno del contesto della coroutine e poi passarlo come input all'API brownfield, in modo da preservare comunque la catena causale degli eventi.
Nella traccia, questo è molto evidente.
Quindi, qui abbiamo un'operazione che è iniziata in worker-3, è passata all'esecutore single-threaded, una volta eseguito il cambio di contesto, e poi è ripresa su worker.
È piuttosto interessante.
Rahul Ravikumar
Ok.
Un'altra cosa che puoi fare è personalizzare Tracer.global.
Finora, l'API Tracing 2.0 ti offre valori predefiniti davvero ottimi per Tracer.global, ma se vuoi personalizzarli, puoi farlo.
Inoltre, Tracing 2.0 supporta JVM e Android perché vogliamo che il tracciamento sia completamente supportato sia per le librerie Java che per quelle Android.
Quindi, i tre passaggi per l'inizializzazione sono: creare un sink di traccia,
Un sink di traccia è in pratica il modo in cui vengono archiviati i pacchetti di traccia.
Definisce il formato di rete e la posizione delle tracce di output in cui vengono archiviate.
Forniamo un'implementazione predefinita per un sink di traccia che supporta il formato Perfetto, ma puoi ovviamente scrivere il tuo sink di traccia.
Questo ci riporta al tentativo di risolvere il problema per cui atrace era l'unico backend disponibile in Tracing 1.
Volevamo risolvere questo problema.
Quindi, ora, se vuoi scrivere un sink OpenTelemetry o se vuoi utilizzare il tuo sink, è assolutamente possibile.
Puoi scegliere di eseguire l'override.
Una volta ottenuto un sink, puoi creare un driver di traccia e assegnargli il sink.
Inoltre, puoi scegliere facoltativamente quali categorie attivare.
È qui che le categorie danno il meglio di sé.
Quindi, puoi decidere che in release alcune categorie sono più importanti di altre, rispetto al debug, e puoi dire che queste sono le categorie che vuoi attivare.
In alternativa, puoi semplicemente dire: "Voglio attivare tutte le categorie indipendentemente dalla loro provenienza".
E questo è del tutto normale.
Puoi anche aggiungere attributi personalizzati globali alla traccia.
In questo modo, puoi incorporare elementi come la versione della build, il tipo o qualsiasi altro metadato che vuoi allegare a una traccia utilizzando l'API addAttribute.
Infine, puoi registrare un tracciatore chiamando setGlobalTracer.
In questo modo, Tracer.global viene associato a questa istanza del tracciatore e, di conseguenza, è disponibile per il processo.
Su Android, poiché i processi hanno un ciclo di vita molto più definito, tutto ciò che devi fare è implementare AbstractTraceDriver.Factory, eseguire l'override del relativo metodo create e fare esattamente ciò che hai fatto prima.
Quindi crei il sink e poi il driver.
Non è necessario registrarsi qui.
Eseguiremo la registrazione per te, perché sappiamo che è ciò che è successo per primo.
Ok.
Ok, voglio ribadire che il tracciamento è pronto per più piattaforme.
Si tratta di una scelta molto intenzionale perché sappiamo che Compose viene eseguito su più piattaforme e vogliamo darti la possibilità di eseguire il tracciamento nel maggior numero di posizioni possibile.
Oggi supportiamo Android e JVM, ma la nostra implementazione ci lascia la possibilità di supportare altre piattaforme in futuro, se lo desideriamo.
Rahul Ravikumar
Ti farà piacere sapere che R8, lo strumento utilizzato per ridurre e ottimizzare il codice sorgente su Android, utilizza Tracing 2.0.
Sfruttando le tracce, abbiamo ottimizzato R8 per ridurre e ottimizzare il codice più velocemente del 25%.
Ciò significa tempi di build più brevi e, di conseguenza, tempi di attesa più brevi per i passaggi di ottimizzazione.
Non esaminerò ogni singola ottimizzazione che abbiamo apportato in R8, ma voglio solo esaminarne una, per darti un'idea di ciò che il tracciamento rende possibile.
Quindi, R8, in sostanza, è un trasformatore da sorgente a sorgente.
In questo modo, ottieni un gruppo di JAR, li riduci e li ottimizzi e produci un altro set di JAR che puoi convertire in DEX.
In questo caso, AGP inizia leggendo i JAR, che non sono solo la tua app, ma tutte le sue dipendenze.
Quindi, utilizza i worker Gradle per iniziare a leggere tutti i JAR di input.
Hai moltissimi file JAR che vuoi leggere.
Una volta terminata la lettura dei JAR, R8 inizia ad analizzare il codice e a creare un modello di ciò che il codice rappresenta e di tutti i suoi punti di ingresso, in modo da poter iniziare a ottimizzare e ridurre il codice.
Ora, questa è la sezione, await, che rappresenta il momento in cui R8 è veramente pronto per avviare effettivamente il processo di ottimizzazione, giusto?
Potresti notare uno spazio vuoto nel mezzo e chiederti perché abbiamo questo tempo di CPU inutilizzato.
Il motivo è che l'API precedente attendeva che tutti i worker leggessero tutti i file prima di analizzare qualsiasi cosa.
Ovviamente, possiamo risolvere il problema, ed è esattamente ciò che abbiamo fatto.
In AGP 9, puoi vedere che non appena abbiamo finito di leggere i JAR di input, abbiamo iniziato subito a elaborarli.
Inoltre, non abbiamo dovuto attendere che ogni singolo worker finisse di leggere i propri input.
In questo modo, il tempo di attesa per leggere tutti gli input si è ridotto notevolmente.
Si tratta di un'ottimizzazione ovvia che puoi vedere chiaramente quando esamini una traccia, ma non altrettanto ovvia quando guardi solo il codice sorgente per capire cosa vale la pena ottimizzare.
Un altro strumento che utilizziamo ogni giorno come ingegneri AndroidX è Metalava.
Metalava è uno strumento che utilizziamo per assicurarci che tutte le API che forniamo abbiano le garanzie di compatibilità delle API che ti aspetti e siano completamente utilizzabili da persone esterne senza i problemi che vogliamo evitare.
Questo strumento viene eseguito su ogni singola API pubblica e API con restrizioni, quindi ogni secondo risparmiato qui è tempo che recuperiamo come ingegneri AndroidX.
Metalava ha utilizzato il tracciamento anche per migliorare i tempi di post-invio di un incredibile 15%.
Andando nella traccia per spiegare cosa è successo, Metalava esegue due tipi di controlli: il primo controlla le API con limitazioni e il secondo controlla le API pubbliche.
Ma forse stai notando un pattern.
Stiamo eseguendo una serie di configurazioni per eseguire questi controlli due volte.
Quindi, ciò che accade è che vengono letti tutti gli input del programma, creiamo la PSI, ovvero l'interfaccia della struttura del programma, o, sai, esattamente qual è la forma del tuo codice che può essere ragionata.
Poi, creiamo l'AST prima di iniziare ad applicare tutti i controlli di linting che vogliamo.
E stiamo eseguendo questo lavoro costoso due volte.
Come abbiamo risolto il problema?
L'abbiamo fatto una sola volta.
Quindi, invece di eseguire tutto il lavoro di configurazione due volte, lo abbiamo fatto una sola volta e abbiamo riutilizzato gli input sia per i controlli con restrizioni sia per i controlli pubblici, ottenendo così tutti questi risparmi.
Sì.
Ok.
Rahul Ravikumar
Ora che ti ho dato un'idea di ciò che Tracing 2.0 rende possibile, è importante sapere che, sebbene il tracciamento sia importante, sapere cosa tracciare è altrettanto importante.
Non è consigliabile inserire tracce ovunque.
Devi prestare molta attenzione a ciò che scegli e cercherò di darti alcune linee guida su ciò che vale la pena tracciare.
Quindi, la prima cosa da fare è trovare lacune inspiegabili nella traccia.
Quindi, ogni volta che i thread sono in stato di esecuzione o eseguibile e vedi lacune evidenti nel tracciamento del sistema, non riesci a spiegarle e non sai cosa sta facendo la tua app, potrebbe valere la pena indagare.
A quel punto, potrei acquisire una traccia metodo sovrapposta al tracciamento del sistema e vedere cosa vale la pena tracciare, quindi potrei aggiungere le sezioni di traccia pertinenti.
Inoltre, devi dare la priorità ai percorsi degli utenti.
Quindi, devi esaminare l'avvio dell'app.
Vuoi esaminare i percorsi dell'utente principali della tua app e capire se puoi spiegarli osservando una traccia.
Quindi, ogni volta che non riesci a ragionare su qualcosa, vale la pena tracciarlo.
Dovresti anche esaminare gli stack di chiamate profondi o l'utilizzo inspiegabile della CPU da una traccia e cercare di capire esattamente cosa sta succedendo, quindi convertire quelle parti del codebase in tracce.
In questo modo, puoi rintracciarli.
Inoltre, ogni volta che i thread sono in stato di sospensione, devi assicurarti di capire il motivo.
Dovrebbero essere in attesa di I/O, di una transazione di binder o di alcuni eventi di input esterni che prevedi che si verifichino.
Ma se hai, sai, un sonno inspiegabile, allora vale la pena indagare perché ovviamente potrebbe esserci qualcosa che si nasconde lì.
Poi, una volta inserite tutte le sezioni di traccia nel codice, devi convertirle in benchmark per poter monitorare le modifiche.
Quindi, man mano che la tua codebase si evolve, puoi vedere se le metriche stanno cambiando, se le tue dashboard riflettono effettivamente grandi cambiamenti, perché allora sai che è cambiato qualcosa di importante.
In alternativa, puoi saltare tutti i passaggi precedenti e lasciare che sia l'agente a farlo.
Prendi queste linee guida, trasformale in una skill, indirizza il tuo agente preferito verso di essa e fagli fare un tentativo.
Sì.
Rahul Ravikumar
Se ti ho convinto che il tracciamento è un'attività utile e che dovresti farlo, ecco la configurazione del progetto.
È piuttosto semplice.
Il tracciamento è solo una semplice libreria Java tradizionale, quindi definisci una dipendenza da androidx.tracing:tracing-wire, che è la combinazione di driver di traccia compatibile con Perfetto e tracciamento, e la versione stabile attuale è la 2.0.5.
Se utilizzi una libreria Java, è esattamente la stessa cosa; anche in questo caso, si tratta di un'API completamente multipiattaforma.
Vogliamo estenderla ad altre piattaforme quando ha senso farlo.
E sì, volevo solo fare un breve riepilogo.
Ho parlato di tutte queste nuove fantastiche funzionalità che abbiamo aggiunto.
Quindi la propagazione del contesto è davvero molto potente.
Abbiamo la possibilità di incorporare dati personalizzati, metadati, informazioni sullo stack di chiamate e ogni sorta di altre cose.
Non ho parlato di molte, molte cose.
Ovviamente puoi modificare il formato di tracciamento, puoi modificare il sink di traccia, puoi, sai, portare i dati di tracciamento su OpenTelemetry.
Queste sono tutte cose che puoi fare.
Il tracciamento è multipiattaforma.
Anche questa è una decisione molto, molto consapevole, perché vogliamo che il maggior numero possibile di persone possa trarne vantaggio.
Inoltre, tutti gli strumenti che forniamo, che si tratti di Perfetto, dei benchmark AndroidX, del nuovo analizzatore delle prestazioni o del vecchio Android Profiler, supportano Tracing 2.0.
Grazie per l'attenzione.
Spero che il Dev Summit sia di tuo gradimento. Grazie.