Il jitter è il comportamento casuale del sistema che impedisce l'esecuzione del lavoro percepibile. Questa pagina descrive come identificare e risolvere i problemi di jank legati al jitter.
Ritardo dello scheduler del thread dell'applicazione
Il ritardo dello scheduler è il sintomo più evidente del jitter: un processo che dovrebbe essere eseguito viene reso eseguibile ma non viene eseguito per un periodo di tempo significativo. L’entità del ritardo varia a seconda del contesto. Per esempio:
- Un thread di supporto casuale in un'app può probabilmente essere ritardato di molti millisecondi senza problemi.
- Il thread dell'interfaccia utente di un'applicazione potrebbe essere in grado di tollerare 1-2 ms di jitter.
- I kthread del driver in esecuzione come SCHED_FIFO possono causare problemi se sono eseguibili per 500us prima dell'esecuzione.
I tempi eseguibili possono essere identificati in systrace dalla barra blu che precede un segmento in esecuzione di un thread. Un tempo eseguibile può anche essere determinato dal periodo di tempo tra l'evento sched_wakeup
per un thread e l'evento sched_switch
che segnala l'inizio dell'esecuzione del thread.
Discussioni troppo lunghe
I thread dell'interfaccia utente dell'applicazione eseguibili per troppo tempo possono causare problemi. I thread di livello inferiore con tempi di esecuzione lunghi in genere hanno cause diverse, ma il tentativo di portare il tempo di esecuzione dei thread dell'interfaccia utente verso zero potrebbe richiedere la risoluzione di alcuni degli stessi problemi che causano tempi di esecuzione lunghi dei thread di livello inferiore. Per mitigare i ritardi:
- Utilizzare i CPUset come descritto in Limitazione termica .
- Aumentare il valore CONFIG_HZ.
- Storicamente, il valore è stato impostato su 100 sulle piattaforme arm e arm64. Tuttavia, questo è un incidente storico e non è un buon valore da utilizzare per i dispositivi interattivi. CONFIG_HZ=100 significa che un jiffy dura 10 ms, il che significa che il bilanciamento del carico tra le CPU potrebbe richiedere 20 ms (due jiffi) per avvenire. Ciò può contribuire in modo significativo al blocco di un sistema caricato.
- I dispositivi recenti (Nexus 5X, Nexus 6P, Pixel e Pixel XL) vengono forniti con CONFIG_HZ=300. Ciò dovrebbe avere un costo energetico trascurabile e migliorare significativamente i tempi di esecuzione. Se noti aumenti significativi nel consumo energetico o problemi di prestazioni dopo aver modificato CONFIG_HZ, è probabile che uno dei tuoi driver stia utilizzando un timer basato su jiffi non elaborati anziché millisecondi e convertendosi in jiffies. Di solito si tratta di una soluzione semplice (vedere la patch che ha risolto i problemi del timer kgsl su Nexus 5X e 6P durante la conversione in CONFIG_HZ=300).
- Infine, abbiamo sperimentato CONFIG_HZ=1000 su Nexus/Pixel e abbiamo scoperto che offre prestazioni notevoli e riduzione di potenza grazie alla diminuzione del sovraccarico dell'RCU.
Con queste due sole modifiche, un dispositivo dovrebbe avere un aspetto molto migliore per il tempo di esecuzione del thread dell'interfaccia utente sotto carico.
Utilizzando sys.use_fifo_ui
Puoi provare a portare a zero il tempo di esecuzione del thread dell'interfaccia utente impostando la proprietà sys.use_fifo_ui
su 1.
Avvertenza : non utilizzare questa opzione su configurazioni di CPU eterogenee a meno che non si disponga di uno scheduler RT in grado di riconoscere la capacità. E, in questo momento, NESSUN SHIPPING RT SCHEDULER È CAPACITY-AWARE . Stiamo lavorando su uno per EAS, ma non è ancora disponibile. Lo scheduler RT predefinito si basa esclusivamente sulle priorità RT e sul fatto che una CPU abbia già un thread RT con priorità uguale o superiore.
Di conseguenza, lo scheduler RT predefinito sposterà felicemente il thread dell'interfaccia utente relativamente lungo da un core grande ad alta frequenza a un core piccolo a frequenza minima se un kthread FIFO con priorità più elevata si sveglia sullo stesso core grande. Ciò introdurrà significative regressioni delle prestazioni . Poiché questa opzione non è stata ancora utilizzata su un dispositivo Android in vendita, se desideri utilizzarla contatta il team delle prestazioni Android per aiutarti a convalidarla.
Quando sys.use_fifo_ui
è abilitato, ActivityManager tiene traccia del thread dell'interfaccia utente e di RenderThread (i due thread più critici per l'interfaccia utente) dell'applicazione principale e rende tali thread SCHED_FIFO anziché SCHED_OTHER. Ciò elimina efficacemente il jitter dall'interfaccia utente e da RenderThreads; le tracce che abbiamo raccolto con questa opzione abilitata mostrano tempi eseguibili nell'ordine dei microsecondi anziché dei millisecondi.
Tuttavia, poiché il bilanciatore del carico RT non riconosceva la capacità, si è verificata una riduzione del 30% nelle prestazioni di avvio dell'applicazione perché il thread dell'interfaccia utente responsabile dell'avvio dell'app sarebbe stato spostato da un core Kryo color oro da 2,1 Ghz a un core Kryo color argento da 1,5 GHz. . Con un bilanciatore del carico RT in grado di riconoscere la capacità, osserviamo prestazioni equivalenti nelle operazioni di massa e una riduzione del 10-15% nei frame time del 95° e 99° percentile in molti dei nostri benchmark dell'interfaccia utente.
Interrompere il traffico
Poiché le piattaforme ARM forniscono interruzioni solo alla CPU 0 per impostazione predefinita, consigliamo l'uso di un bilanciatore IRQ (irqbalance o msm_irqbalance sulle piattaforme Qualcomm).
Durante lo sviluppo di Pixel, abbiamo riscontrato problemi che potrebbero essere direttamente attribuiti allo sfruttamento eccessivo della CPU con interruzioni. Ad esempio, se il thread mdss_fb0
fosse stato pianificato sulla CPU 0, ci sarebbe stata una probabilità molto maggiore di eseguire un jank a causa di un'interruzione attivata dal display quasi immediatamente prima dello scanout. mdss_fb0
sarebbe nel bel mezzo del proprio lavoro con una scadenza molto stretta e quindi perderebbe tempo a causa del gestore di interruzioni MDSS. Inizialmente, abbiamo tentato di risolvere questo problema impostando l'affinità CPU del thread mdss_fb0 su CPU 1-3 per evitare conflitti con l'interrupt, ma poi ci siamo resi conto che non avevamo ancora abilitato msm_irqbalance. Con msm_irqbalance abilitato, jank è stato notevolmente migliorato anche quando sia mdss_fb0 che l'interrupt MDSS erano sulla stessa CPU a causa della riduzione del conflitto da parte di altri interrupt.
Questo può essere identificato in systrace guardando la sezione sched così come la sezione irq. La sezione sched mostra ciò che è stato pianificato, ma una regione sovrapposta nella sezione irq significa che durante quel periodo è in esecuzione un'interruzione invece del processo normalmente pianificato. Se noti una notevole quantità di tempo impiegato durante un'interruzione, le tue opzioni includono:
- Rende più veloce il gestore delle interruzioni.
- Evitare innanzitutto che si verifichi l'interruzione.
- Modificare la frequenza dell'interruzione in modo che sia fuori fase rispetto ad altri lavori regolari con cui potrebbe interferire (se si tratta di un'interruzione regolare).
- Imposta direttamente l'affinità CPU dell'interrupt ed evita che venga bilanciato.
- Imposta l'affinità della CPU del thread con cui l'interruzione interferisce per evitare l'interruzione.
- Affidarsi al bilanciatore degli interrupt per spostare l'interrupt su una CPU meno caricata.
L'impostazione dell'affinità della CPU in genere non è consigliata ma può essere utile in casi specifici. In generale, è troppo difficile prevedere lo stato del sistema per gli interrupt più comuni, ma se si dispone di un insieme di condizioni molto specifico che attiva determinati interrupt in cui il sistema è più limitato del normale (come VR), l'affinità esplicita della CPU potrebbe essere una buona soluzione.
Softirq lunghi
Mentre un softirq è in esecuzione, disabilita la prelazione. softirq può anche essere attivato in molti punti all'interno del kernel e può essere eseguito all'interno di un processo utente. Se c'è abbastanza attività softirq, i processi utente smetteranno di eseguire softirq e ksoftirqd si riattiverà per eseguire softirq e bilanciare il carico. Di solito va bene. Tuttavia, un singolo softirq molto lungo può devastare il sistema.
i softirq sono visibili all'interno della sezione IRQ di una traccia, quindi sono facili da individuare se il problema può essere riprodotto durante la traccia. Poiché un softirq può essere eseguito all'interno di un processo utente, un softirq errato può anche manifestarsi come runtime aggiuntivo all'interno di un processo utente senza una ragione ovvia. Se lo vedi, controlla la sezione irq per vedere se la colpa è dei softirq.
I driver lasciano la prelazione o gli IRQ disabilitati troppo a lungo
La disabilitazione della prelazione o degli interrupt per un periodo troppo lungo (decine di millisecondi) provoca un rallentamento. In genere, il jank si manifesta quando un thread diventa eseguibile ma non viene eseguito su una particolare CPU, anche se il thread eseguibile ha una priorità (o SCHED_FIFO) significativamente più alta rispetto all'altro thread.
Alcune linee guida:
- Se il thread eseguibile è SCHED_FIFO e il thread in esecuzione è SCHED_OTHER, la prelazione o gli interrupt del thread in esecuzione sono disabilitati.
- Se il thread eseguibile ha una priorità significativamente più alta (100) rispetto al thread in esecuzione (120), è probabile che il thread in esecuzione abbia la prelazione o gli interrupt disabilitati se il thread eseguibile non viene eseguito entro due jiffies.
- Se il thread eseguibile e il thread in esecuzione hanno la stessa priorità, è probabile che il thread in esecuzione abbia la prelazione o gli interrupt disabilitati se il thread eseguibile non viene eseguito entro 20 ms.
Tieni presente che l'esecuzione di un gestore di interruzioni ti impedisce di servire altri interrupt, disabilitando anche la prelazione.
Un'altra opzione per identificare le regioni incriminate è con il tracciante preemptirqsoff (vedi Utilizzo di ftrace dinamico ). Questo tracciante può fornire una visione molto più approfondita della causa principale di una regione di continuità (come i nomi delle funzioni), ma richiede un lavoro più invasivo per essere abilitato. Sebbene possa avere un impatto maggiore sulle prestazioni, vale sicuramente la pena provarlo.
Utilizzo errato delle code di lavoro
I gestori di interrupt spesso devono svolgere un lavoro che può essere eseguito al di fuori di un contesto di interrupt, consentendo di distribuire il lavoro a thread diversi nel kernel. Uno sviluppatore di driver potrebbe notare che il kernel ha una funzionalità di attività asincrona molto comoda a livello di sistema chiamata code di lavoro e potrebbe utilizzarla per il lavoro relativo agli interrupt.
Tuttavia, le code di lavoro sono quasi sempre la risposta sbagliata a questo problema perché sono sempre SCHED_OTHER. Molti interrupt hardware si trovano nel percorso critico delle prestazioni e devono essere eseguiti immediatamente. Le code di lavoro non hanno garanzie su quando verranno eseguite. Ogni volta che abbiamo visto una coda di lavoro nel percorso critico delle prestazioni, è stata fonte di scherzi sporadici, indipendentemente dal dispositivo. Su Pixel, con un processore di punta, abbiamo visto che una singola coda di lavoro poteva essere ritardata fino a 7 ms se il dispositivo era sotto carico, a seconda del comportamento dello scheduler e di altre cose in esecuzione sul sistema.
Invece di una coda di lavoro, i driver che devono gestire un lavoro di tipo interrupt all'interno di un thread separato dovrebbero creare il proprio kthread SCHED_FIFO. Per assistenza su come eseguire questa operazione con le funzioni kthread_work, fare riferimento a questa patch .
Conflitto di blocco del framework
Il conflitto di blocchi del framework può essere fonte di jank o di altri problemi di prestazioni. Di solito è causato dal blocco ActivityManagerService ma può essere visualizzato anche in altri blocchi. Ad esempio, il blocco PowerManagerService può influire sulle prestazioni dello schermo. Se vedi questo sul tuo dispositivo, non esiste una soluzione valida perché può essere migliorato solo tramite miglioramenti dell'architettura al framework. Tuttavia, se stai modificando il codice eseguito all'interno di system_server, è fondamentale evitare di mantenere i blocchi per un lungo periodo, in particolare il blocco ActivityManagerService.
Contesa sul blocco del raccoglitore
Storicamente, il raccoglitore ha avuto un unico blocco globale. Se il thread che esegue una transazione del raccoglitore è stato bloccato mentre mantiene il blocco, nessun altro thread può eseguire una transazione del raccoglitore finché il thread originale non ha rilasciato il blocco. Questo non va bene; Il conflitto del raccoglitore può bloccare tutto nel sistema, incluso l'invio di aggiornamenti dell'interfaccia utente al display (i thread dell'interfaccia utente comunicano con SurfaceFlinger tramite il raccoglitore).
Android 6.0 includeva diverse patch per migliorare questo comportamento disabilitando la prelazione mantenendo il blocco del raccoglitore. Ciò era sicuro solo perché il blocco del raccoglitore doveva essere mantenuto per alcuni microsecondi di tempo di esecuzione effettivo. Ciò ha migliorato notevolmente le prestazioni in situazioni non contese e ha impedito il conflitto impedendo la maggior parte dei cambi di pianificazione mentre veniva mantenuto il blocco del raccoglitore. Tuttavia, la prelazione non poteva essere disabilitata per l'intero runtime di mantenimento del blocco del raccoglitore, il che significa che la prelazione era abilitata per le funzioni che potevano dormire (come copy_from_user), il che potrebbe causare la stessa prelazione del caso originale. Quando abbiamo inviato le patch a monte, ci hanno subito detto che questa era l'idea peggiore della storia. (Eravamo d'accordo con loro, ma non potevamo nemmeno discutere sull'efficacia dei cerotti nel prevenire il jank.)
fd contesa all'interno di un processo
Questo è raro. Probabilmente il tuo jank non è causato da questo.
Detto questo, se hai più thread all'interno di un processo che scrive lo stesso fd, è possibile vedere un conflitto su questo fd, tuttavia l'unica volta che l'abbiamo visto durante il caricamento di Pixel è durante un test in cui i thread a bassa priorità hanno tentato di occupare tutta la CPU tempo mentre un singolo thread ad alta priorità era in esecuzione all'interno dello stesso processo. Tutti i thread stavano scrivendo sul marcatore di traccia fd e il thread ad alta priorità poteva essere bloccato sul marcatore di traccia fd se un thread a bassa priorità manteneva il blocco fd e veniva quindi interrotto. Quando la traccia veniva disabilitata dai thread con priorità bassa, non si verificavano problemi di prestazioni.
Non siamo riusciti a riprodurre questo problema in nessun'altra situazione, ma vale la pena segnalarlo come potenziale causa di problemi di prestazioni durante il tracciamento.
Transizioni di inattività della CPU non necessarie
Quando si ha a che fare con IPC, in particolare con pipeline multiprocesso, è comune vedere variazioni sul seguente comportamento di runtime:
- Il thread A viene eseguito sulla CPU 1.
- Il thread A riattiva il thread B.
- Il thread B inizia a essere eseguito sulla CPU 2.
- Il thread A va immediatamente a dormire, per essere risvegliato dal thread B quando il thread B ha terminato il suo lavoro corrente.
Una fonte comune di sovraccarico è tra i passaggi 2 e 3. Se la CPU 2 è inattiva, deve essere riportata allo stato attivo prima che il thread B possa essere eseguito. A seconda del SOC e della profondità del periodo di inattività, potrebbero trascorrere decine di microsecondi prima che il thread B inizi l'esecuzione. Se il tempo di esecuzione effettivo di ciascun lato dell'IPC è sufficientemente vicino al sovraccarico, le prestazioni complessive di quella pipeline possono essere notevolmente ridotte dalle transizioni di inattività della CPU. Il luogo più comune in cui Android riesce a raggiungere questo obiettivo riguarda le transazioni di raccoglitori e molti servizi che utilizzano il raccoglitore finiscono per assomigliare alla situazione descritta sopra.
Innanzitutto, utilizza la funzione wake_up_interruptible_sync()
nei driver del kernel e supportala da qualsiasi scheduler personalizzato. Trattatelo come un requisito, non come un suggerimento. Binder lo usa oggi ed è di grande aiuto con le transazioni di raccoglitore sincrone evitando transizioni di inattività della CPU non necessarie.
In secondo luogo, assicurati che i tempi di transizione della CPU siano realistici e che il governatore della CPU ne tenga conto correttamente. Se il tuo SOC entra ed esce dallo stato di inattività più profondo, non risparmierai energia andando allo stato di inattività più profondo.
Registrazione
La registrazione non è gratuita per i cicli della CPU o la memoria, quindi non spammare il buffer di registro. La registrazione dei costi scorre ciclicamente nella tua applicazione (direttamente) e nel demone di log. Rimuovi eventuali registri di debug prima di spedire il dispositivo.
Problemi di I/O
Le operazioni di I/O sono fonti comuni di jitter. Se un thread accede a un file mappato in memoria e la pagina non è nella cache della pagina, va in errore e legge la pagina dal disco. Ciò blocca il thread (in genere per più di 10 ms) e, se si verifica nel percorso critico del rendering dell'interfaccia utente, può provocare errori. Ci sono troppe cause delle operazioni di I/O da discutere qui, ma controlla le seguenti posizioni quando provi a migliorare il comportamento di I/O:
- PinnerService . Aggiunto in Android 7.0, PinnerService consente al framework di bloccare alcuni file nella cache della pagina. Ciò rimuove la memoria per l'utilizzo da parte di qualsiasi altro processo, ma se sono presenti alcuni file noti a priori per essere utilizzati regolarmente, può essere efficace bloccare tali file.
Sui dispositivi Pixel e Nexus 6P con Android 7.0, abbiamo bloccato quattro file:- /system/framework/arm64/boot-framework.oat
- /system/framework/oat/arm64/services.odex
- /system/framework/arm64/boot.oat
- /system/framework/arm64/boot-core-libart.oat
- Crittografia . Un'altra possibile causa di problemi di I/O. Riteniamo che la crittografia in linea offra le migliori prestazioni rispetto alla crittografia basata sulla CPU o all'utilizzo di un blocco hardware accessibile tramite DMA. Ancora più importante, la crittografia in linea riduce il jitter associato all'I/O, soprattutto se confrontata con la crittografia basata sulla CPU. Poiché i recuperi nella cache delle pagine si trovano spesso nel percorso critico del rendering dell'interfaccia utente, la crittografia basata sulla CPU introduce un carico aggiuntivo sulla CPU nel percorso critico, che aggiunge più jitter rispetto al semplice recupero I/O.
I motori di crittografia hardware basati su DMA hanno un problema simile, poiché il kernel deve dedicare cicli alla gestione di quel lavoro anche se è disponibile per l'esecuzione altro lavoro critico. Consigliamo vivamente a qualsiasi fornitore SOC che crea nuovo hardware di includere il supporto per la crittografia in linea.
Imballaggio aggressivo per piccoli compiti
Alcuni scheduler offrono supporto per l'impacchettamento di piccole attività su singoli core della CPU per cercare di ridurre il consumo energetico mantenendo più CPU inattive più a lungo. Anche se questo funziona bene per la velocità effettiva e il consumo energetico, può essere catastrofico per la latenza. Esistono diversi thread di breve durata nel percorso critico del rendering dell'interfaccia utente che possono essere considerati piccoli; se questi thread vengono ritardati poiché vengono migrati lentamente su altre CPU, si causerà un blocco. Raccomandiamo di utilizzare l'imballaggio per piccole attività in modo molto conservativo.
Threshing della cache della pagina
Un dispositivo senza memoria libera sufficiente potrebbe improvvisamente diventare estremamente lento durante l'esecuzione di un'operazione di lunga durata, come l'apertura di una nuova applicazione. Una traccia dell'applicazione può rivelare che è costantemente bloccata nell'I/O durante una particolare esecuzione anche quando spesso non lo è. Questo di solito è un segno di esaurimento della cache della pagina, soprattutto su dispositivi con meno memoria.
Un modo per identificarlo è prendere una systrace utilizzando il tag pagecache e alimentare quella traccia allo script in system/extras/pagecache/pagecache.py
. pagecache.py traduce le richieste individuali di mappare i file nella cache della pagina in statistiche aggregate per file. Se scopri che sono stati letti più byte di un file rispetto alla dimensione totale di quel file sul disco, stai sicuramente colpendo il thrashing della cache della pagina.
Ciò significa che il working set richiesto dal carico di lavoro (in genere una singola applicazione più system_server) è maggiore della quantità di memoria disponibile per la cache delle pagine sul dispositivo. Di conseguenza, quando una parte del carico di lavoro ottiene i dati necessari nella cache della pagina, un'altra parte che verrà utilizzata nel prossimo futuro verrà eliminata e dovrà essere recuperata nuovamente, causando il ripetersi del problema fino al caricamento. ha completato. Questa è la causa fondamentale dei problemi di prestazioni quando su un dispositivo non è disponibile memoria sufficiente.
Non esiste un modo infallibile per correggere il thrashing della cache delle pagine, ma esistono alcuni modi per provare a migliorarlo su un determinato dispositivo.
- Utilizza meno memoria nei processi persistenti. Minore è la memoria utilizzata dai processi persistenti, maggiore è la memoria disponibile per le applicazioni e la cache delle pagine.
- Controlla i ritagli che hai per il tuo dispositivo per assicurarti di non rimuovere inutilmente memoria dal sistema operativo. Abbiamo visto situazioni in cui i carveout utilizzati per il debugging venivano lasciati accidentalmente nelle configurazioni del kernel spedite, consumando decine di megabyte di memoria. Questo può fare la differenza tra colpire o meno il thrashing della cache della pagina, specialmente su dispositivi con meno memoria.
- Se vedi il thrashing della cache delle pagine in system_server su file critici, valuta la possibilità di bloccare tali file. Ciò aumenterà la pressione della memoria altrove, ma potrebbe modificare il comportamento abbastanza da evitare di essere picchiato.
- Risintonizza lowmemorykiller per cercare di mantenere più memoria libera. Le soglie di lowmemorykiller si basano sia sulla memoria libera assoluta che sulla cache della pagina, quindi aumentare la soglia alla quale i processi a un determinato livello oom_adj vengono terminati può comportare un comportamento migliore a scapito di una maggiore morte delle app in background.
- Prova a usare ZRAM. Usiamo ZRAM su Pixel, anche se Pixel ha 4 GB, perché potrebbe aiutare con pagine sporche usate raramente.