Identificare il jitter correlato al jitter

Il jitter è il comportamento casuale del sistema che impedisce l'esecuzione di un lavoro percepibile. Questa pagina descrive come identificare e risolvere i problemi di jank correlati al jitter.

Ritardo dello scheduler del thread dell'app

Il ritardo dell'organizzatore è 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'importanza del ritardo varia in base al contesto. Ad esempio:

  • Un thread di supporto casuale in un'app può probabilmente essere ritardato per molti millisecondi senza problemi.
  • Il thread dell'interfaccia utente di un'app potrebbe essere in grado di tollerare 1-2 ms di jitter.
  • I thread k del driver in esecuzione come SCHED_FIFO possono causare problemi se sono eseguibili per 500 μs prima dell'esecuzione.

I tempi di esecuzione possono essere identificati in systrace dalla barra blu che precede un segmento in esecuzione di un thread. Un tempo di esecuzione può essere determinato anche dall'intervallo di tempo tra l'evento sched_wakeup per un thread e l'evento sched_switch che segnala l'inizio dell'esecuzione del thread.

Thread che vengono eseguiti per troppo tempo

I thread dell'interfaccia utente dell'app eseguibili per troppo tempo possono causare problemi. I thread di livello inferiore con tempi di esecuzione lunghi hanno generalmente cause diverse, ma il tentativo di ridurre il tempo di esecuzione del thread dell'interfaccia utente a zero potrebbe richiedere la correzione di alcuni degli stessi problemi che causano tempi di esecuzione lunghi dei thread di livello inferiore. Per ridurre i ritardi:

  1. Utilizza i set di CPU come descritto in Ritardo termico.
  2. Aumenta il valore CONFIG_HZ.
    • In passato, il valore è stato impostato su 100 sulle piattaforme arm e arm64. Tuttavia, si tratta di un caso fortuito e non è un buon valore da utilizzare per i dispositivi interattivi. CONFIG_HZ=100 significa che un jiffy è lungo 10 ms, il che significa che il bilanciamento del carico tra le CPU potrebbe richiedere 20 ms (due jiffies). Ciò può contribuire notevolmente al jitter su un sistema caricato.
    • I dispositivi recenti (Nexus 5X, Nexus 6P, Pixel e Pixel XL) sono stati spediti con CONFIG_HZ=300. Ciò dovrebbe comportare un costo energetico trascurabile, migliorando notevolmente i tempi di esecuzione. Se noti aumenti significativi del consumo di energia o problemi di prestazioni dopo aver modificato CONFIG_HZ, è probabile che uno dei tuoi driver utilizzi un timer basato su jiffies non elaborati anziché su millisecondi e che lo stia convertendo in jiffies. Di solito è una soluzione facile (consulta la patch che ha risolto i problemi relativi al timer kgsl su Nexus 5X e 6P durante la conversione a CONFIG_HZ=300).
    • Infine, abbiamo sperimentato CONFIG_HZ=1000 su Nexus/Pixel e abbiamo riscontrato che offre un notevole miglioramento delle prestazioni e una riduzione del consumo energetico grazie alla diminuzione dell'overhead RCU.

Solo con queste due modifiche, un dispositivo dovrebbe avere un tempo di esecuzione del thread UI molto migliore sotto carico.

Utilizza sys.use_fifo_ui

Puoi provare a impostare il tempo di esecuzione del thread dell'interfaccia utente su zero impostando la proprietà sys.use_fifo_ui su 1.

Avviso: non utilizzare questa opzione su configurazioni CPU eterogenee, a meno che tu non disponga di un programmatore RT sensibile alla capacità. Al momento, NESSUNO PIANIFICATORE RT IN PRODUZIONE È CONSAPEVOLE DELLA CAPACITÀ. Stiamo lavorando a una funzionalità di questo tipo per EAS, ma non è ancora disponibile. Il programmatore RT predefinito si basa esclusivamente sulle priorità RT e sul fatto che una CPU abbia già un thread RT di priorità uguale o superiore.

Di conseguenza, lo scheduler RT predefinito sposterà il thread dell'interfaccia utente in esecuzione relativamente a lungo termine da un core big ad alta frequenza a un core little alla frequenza minima se un kthread FIFO con priorità più elevata si riattiva nello stesso core big. Ciò comporterà significative regressioni delle prestazioni. Poiché questa opzione non è ancora stata utilizzata su un dispositivo Android in spedizione, se vuoi utilizzarla, contatta il team di rendimento di Android per farti aiutare a convalidarla.

Quando sys.use_fifo_ui è attivato, ActivityManager monitora il thread UI e il thread RenderThread (i due thread più critici per l'interfaccia utente) dell'app principale e li imposta su SCHED_FIFO anziché su SCHED_OTHER. In questo modo viene eliminato efficacemente il jitter da UI e RenderThreads. Le tracce che abbiamo raccolto con questa opzione attivata mostrano tempi di esecuzione nell'ordine di microsecondi anziché millisecondi.

Tuttavia, poiché il bilanciatore del carico RT non era sensibile alla capacità, si è verificata una riduzione del 30% delle prestazioni di avvio dell'app perché il thread dell'interfaccia utente responsabile dell'avvio dell'app sarebbe stato spostato da un core Kryo gold da 2,1 GHz a un core Kryo silver da 1,5 GHz. Con un bilanciatore del carico RT sensibile alla capacità, riscontriamo un rendimento equivalente nelle operazioni collettive e una riduzione del 10-15% dei tempi di frame del 95° e 99° percentile in molti dei nostri benchmark dell'interfaccia utente.

Interrompere il traffico

Poiché le piattaforme ARM inviano interruzioni alla CPU 0 solo per impostazione predefinita, consigliamo di utilizzare un bilanciatore IRQ (irqbalance o msm_irqbalance sulle piattaforme Qualcomm).

Durante lo sviluppo di Pixel, abbiamo riscontrato un problema di balbuzie che poteva essere attribuito direttamente al sovraccarico della CPU 0 con interruzioni. Ad esempio, se il thread mdss_fb0 è stato pianificato sulla CPU 0, la probabilità di avere un ritardo era molto più elevata 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 ravvicinata e perderebbe un po' di tempo per l'handler di interruzione MDSS. Inizialmente abbiamo tentato di risolvere il problema impostando l'affinità della CPU del thread mdss_fb0 sulle CPU 1-3 per evitare conflitti con l'interruzione, ma poi abbiamo capito che non avevamo ancora attivato msm_irqbalance. Con msm_irqbalance abilitato, il jitter è migliorato notevolmente anche quando sia mdss_fb0 sia l'interruzione MDSS erano sulla stessa CPU a causa della riduzione della contesa da parte di altre interruzioni.

Questo può essere identificato in systrace esaminando la sezione sched e la sezione irq. La sezione sched mostra ciò che è stato pianificato, ma una regione in sovrapposizione nella sezione irq indica che durante quel periodo è in esecuzione un'interruzione anziché il processo normalmente pianificato. Se noti intervalli di tempo significativi durante un'interruzione, le opzioni disponibili sono:

  • Accelera il gestore delle interruzioni.
  • Evitare che l'interruzione si verifichi.
  • Modifica la frequenza dell'interruzione in modo che non sia in fase con l'altro lavoro regolare con cui potrebbe interferire (se si tratta di un'interruzione regolare).
  • Imposta direttamente l'affinità della CPU dell'interruzione e impedisci che venga bilanciata.
  • Imposta l'affinità della CPU del thread con cui l'interruzione interferisce per evitarla.
  • Utilizza il bilanciatore delle interruzioni per spostare l'interruzione su una CPU meno carica.

In genere, l'impostazione dell'affinità della CPU non è consigliata, ma può essere utile per casi specifici. In generale, è troppo difficile prevedere lo stato del sistema per la maggior parte delle interruzioni comuni, ma se hai un insieme molto specifico di condizioni che attivano determinate interruzioni quando il sistema è più vincolato del normale (ad esempio la realtà virtuale), l'affinità della CPU esplicita potrebbe essere una buona soluzione.

Softirq lunghi

Quando è in esecuzione, un softirq disattiva la preemption. I softirq possono anche essere attivati in molti punti all'interno del kernel e possono essere eseguiti all'interno di un processo utente. Se l'attività softirq è sufficiente, i processi utente smetteranno di eseguire softirq e ksoftirqd si risveglierà per eseguire softirq e bilanciare il carico. Di solito non è un problema. Tuttavia, un singolo softirq molto lungo può causare gravi danni al sistema.


Gli softirq sono visibili nella sezione irq di una traccia, quindi sono facili da individuare se il problema può essere riprodotto durante la tracciatura. Poiché un softirq può essere eseguito all'interno di un processo utente, un softirq errato può anche manifestarsi come tempo di esecuzione aggiuntivo all'interno di un processo utente senza motivo apparente. In questo caso, controlla la sezione irq per verificare se sono responsabili i softirq.

Driver che lasciano la preemption o le IRQ disattivate per troppo tempo

La disattivazione della preemption o delle interruzioni per troppo tempo (decine di millisecondi) provoca un effetto jitter. In genere, il jitter si manifesta quando un thread diventa eseguibile, ma non viene eseguito su una determinata CPU, anche se il thread eseguibile ha una priorità notevolmente superiore (o SCHED_FIFO) rispetto all'altro thread.

Alcune linee guida:

  • Se il thread eseguibile è SCHED_FIFO e il thread in esecuzione è SCHED_OTHER, il thread in esecuzione ha il prerilascio o le interruzioni disattivati.
  • Se il thread eseguibile ha una priorità (100) notevolmente superiore rispetto al thread in esecuzione (120), è probabile che la preemption o le interruzioni siano disattivate 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 prerilascio o le interruzioni siano disattivati nel thread in esecuzione se il thread eseguibile non viene eseguito entro 20 ms.

Tieni presente che l'esecuzione di un gestore di interruzioni ti impedisce di gestire altre interruzioni, il che disattiva anche la preemption.


Un'altra opzione per identificare le regioni in violazione è il tracciante preemptirqsoff (vedi Utilizzo di ftrace dinamico). Questo tracker può fornire informazioni molto più dettagliate sulla causa principale di una regione non interrompibile (ad esempio i nomi delle funzioni), ma richiede un intervento più invasivo per essere attivato. Sebbene possa avere un impatto maggiore sulle prestazioni, vale la pena provarlo.

Utilizzo errato delle code di lavoro

Gli handler di interruzioni devono spesso eseguire operazioni che possono essere eseguite al di fuori di un contesto di interruzione, consentendo di distribuire il lavoro a thread diversi nel kernel. Uno sviluppatore di driver potrebbe notare che il kernel dispone di una funzionalità di attività asincrona di sistema molto comoda chiamata workqueues e potrebbe utilizzarla per il lavoro relativo alle interruzioni.

Tuttavia, le code di lavoro sono quasi sempre la risposta sbagliata per questo problema perché sono sempre SCHED_OTHER. Molte interruzioni hardware si trovano nel percorso critico del rendimento e devono essere eseguite immediatamente. Le code di lavoro non forniscono alcuna garanzia in merito al momento in cui verranno eseguite. Ogni volta che abbiamo rilevato una coda di lavoro nel percorso critico del rendimento, è stata una fonte di arresti anomali sporadici, indipendentemente dal dispositivo. Su Pixel, con un processore di punta, abbiamo notato che un singolo workqueue poteva subire un ritardo fino a 7 ms se il dispositivo era sotto carico, a seconda del comportamento dello schedulatore e di altri elementi in esecuzione nel sistema.

Invece di una coda di lavoro, i driver che devono gestire un lavoro simile a un'interruzione in un thread separato devono creare il proprio kthread SCHED_FIFO. Per assistenza su come eseguire questa operazione con le funzioni kthread_work, consulta questa patch.

Conflitto del blocco del framework

La contesa per l'acquisizione del blocco del framework può essere fonte di problemi di prestazioni o altri problemi. In genere è causato dal blocco ActivityManagerService, ma può essere visualizzato anche in altri blocchi. Ad esempio, il blocco di PowerManagerService può influire sulle prestazioni dello schermo. Se vedi questo messaggio sul tuo dispositivo, non esiste una soluzione efficace perché il problema può essere risolto solo tramite miglioramenti dell'architettura del framework. Tuttavia, se modifichi il codice in esecuzione all'interno di system_server, è fondamentale evitare di mantenere le chiavi per molto tempo, in particolare la chiave ActivityManagerService.

Conflitto del blocco del binder

In passato, Binder aveva un unico blocco globale. Se il thread che esegue una transazione del binder è stato eseguito con priorità mentre deteneva il blocco, nessun altro thread può eseguire una transazione del binder finché il thread originale non ha rilasciato il blocco. Questo è un problema: la contesa del binder 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 binder).

Android 6.0 includeva diverse patch per migliorare questo comportamento disattivando la preemption mentre si tiene premuto il blocco del binder. Questo era sicuro solo perché il blocco del binder doveva essere mantenuto per alcuni microsecondi di tempo di esecuzione effettivo. In questo modo, abbiamo migliorato notevolmente le prestazioni in situazioni senza conflitti e abbiamo evitato le contese impedendo la maggior parte dei passaggi del programmatore mentre il blocco del binder era attivo. Tuttavia, la preemption non poteva essere disattivata per l'intero runtime del blocco del binder, il che significa che la preemption era attivata per le funzioni che potevano andare in sospensione (come copy_from_user), il che potrebbe causare la stessa preemption della richiesta originale. Quando abbiamo inviato le patch, ci hanno subito detto che era la peggiore idea della storia. (Eravamo d'accordo con loro, ma non potevamo negare l'efficacia delle patch per la prevenzione del jitter).

Concorrenza fd all'interno di un processo

Si tratta di un caso raro. Probabilmente il problema non è causato da questo.

Detto questo, se all'interno di un processo sono presenti più thread che scrivono nello stesso fd, è possibile che si verifichi una contesa su questo fd. Tuttavia, l'unica volta che abbiamo riscontrato questo problema durante l'inizializzazione di Pixel è stato durante un test in cui i thread a bassa priorità hanno tentato di occupare tutto il tempo della CPU mentre un singolo thread ad alta priorità era in esecuzione nello stesso processo. Tutti i thread scrivevano nell'attributo fd dell'indicatore di traccia e il thread con priorità elevata poteva bloccarsi nell'attributo fd dell'indicatore di traccia se un thread con priorità bassa deteneva il blocco dell'attributo fd e veniva poi eseguito in preemption. Quando il monitoraggio è stato disattivato nei thread a bassa priorità, non si sono verificati problemi di rendimento.

Non siamo riusciti a riprodurre questo problema in altre situazioni, ma vale la pena indicarlo come potenziale causa di problemi di prestazioni durante il monitoraggio.

Transizioni inutilizzate della CPU in stato inattivo

Quando si ha a che fare con l'IPC, in particolare con le pipeline multi-processo, è normale osservare variazioni nel seguente comportamento di runtime:

  1. Il thread A viene eseguito sulla CPU 1.
  2. Il thread A riattiva il thread B.
  3. Il thread B inizia a funzionare sulla CPU 2.
  4. Il thread A entra immediatamente in modalità di sospensione per essere riattivato dal thread B quando quest'ultimo ha completato il lavoro corrente.

Una fonte comune di overhead si trova tra i passaggi 2 e 3. Se la CPU 2 è inattiva, deve essere riportata a uno stato attivo prima che il thread B possa essere eseguito. A seconda del SOC e della profondità dello stato inattivo, potrebbero trascorrere decine di microsecondi prima che il thread B inizi a funzionare. Se il tempo di esecuzione effettivo di ciascun lato dell'IPC è sufficientemente vicino al sovraccarico, le prestazioni complessive della pipeline possono essere ridotte notevolmente dalle transizioni inattive della CPU. Il problema si verifica più spesso su Android per le transazioni binder e molti servizi che utilizzano binder finiscono per avere la situazione descritta sopra.

Innanzitutto, utilizza la funzione wake_up_interruptible_sync() nei driver del kernel e supportala da qualsiasi programma di pianificazione personalizzato. Trattalo come un requisito, non un suggerimento. Binder lo utilizza oggi e aiuta molto con le transazioni di binder sincrone evitando transizioni inattive della CPU non necessarie.

In secondo luogo, assicurati che i tempi di transizione di cpuidle siano realistici e che il gestore cpuidle li tenga conto correttamente. Se il SOC è in stato di thrashing e esce da questo stato, non risparmierai energia passando allo stato di minimo profondo.

Logging

La registrazione non è senza costi per i cicli della CPU o la memoria, quindi non inviare spam al buffer dei log. La registrazione dei cicli di costo avviene direttamente nell'app e nel daemon di log. Rimuovi eventuali log 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, viene generato un errore e la pagina viene letta dal disco. Questo blocca il thread (di solito per più di 10 ms) e, se accade nel percorso critico del rendering dell'interfaccia utente, può causare scatti. Esistono troppe cause di operazioni di I/O da discutere qui, ma controlla le seguenti posizioni quando cerchi di migliorare il comportamento di I/O:

  • PinnerService. Aggiunta in Android 7.0, PinnerService consente al framework di bloccare alcuni file nella cache della pagina. In questo modo, la memoria viene rimossa per essere utilizzata da qualsiasi altro processo, ma se sono presenti file che si sa a priori essere utilizzati regolarmente, può essere efficace mlockarli.

    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
    Questi file sono costantemente in uso dalla maggior parte delle app e da system_server, pertanto non devono essere espulsi dalla pagina. In particolare, abbiamo riscontrato che se una di queste viene rimossa dalla pagina, verrà reinserita e causerà arresti anomali quando si passa da un'app pesante.
  • Crittografia. Un'altra possibile causa di problemi di I/O. Abbiamo riscontrato che la crittografia in linea offre le migliori prestazioni rispetto alla crittografia basata su CPU o all'utilizzo di un blocco hardware accessibile tramite DMA. Soprattutto, la crittografia in linea riduce il jitter associato all'I/O, soprattutto se confrontata con la crittografia basata su CPU. Poiché i recuperi nella cache della pagina si trovano spesso nel percorso critico del rendering dell'interfaccia utente, la crittografia basata sulla CPU introduce un ulteriore carico della CPU nel percorso critico, il che aggiunge più jitter rispetto al recupero I/O.

    I motori di crittografia hardware basati su DMA hanno un problema simile, poiché il kernel deve spendere cicli per gestire questo lavoro anche se è disponibile altro lavoro critico da eseguire. Consigliamo vivamente a qualsiasi fornitore di SOC che costruisce nuovo hardware di includere il supporto della crittografia in linea.

Imballaggio aggressivo di piccole attività

Alcuni pianificatori offrono il supporto per il raggruppamento di piccole attività su singoli core della CPU per provare a ridurre il consumo energetico mantenendo più CPU inattive per più tempo. Anche se questo funziona bene per il throughput e il consumo energetico, può essere catastrofico per la latenza. Nel percorso critico del rendering dell'interfaccia utente sono presenti diversi thread con breve tempo di esecuzione che possono essere considerati di piccole dimensioni. Se questi thread vengono ritardati durante la migrazione graduale ad altre CPU, causeranno un comportamento discontinuo. Consigliamo di utilizzare il raggruppamento di piccole attività in modo molto conservativo.

Thrashing della cache di pagina

Un dispositivo senza memoria libera sufficiente potrebbe diventare improvvisamente estremamente lento durante l'esecuzione di un'operazione di lunga durata, ad esempio l'apertura di una nuova app. Un'analisi dell'app potrebbe rivelare che è costantemente bloccata in I/O durante una determinata esecuzione, anche se spesso non è bloccata in I/O. In genere, questo è un segno di thrashing della cache di pagina, in particolare sui dispositivi con meno memoria.

Un modo per identificare questo problema è eseguire un systrace utilizzando il tag pagecache e alimentare la traccia nello script in system/extras/pagecache/pagecache.py. pagecache.py traduce le singole richieste di mappatura dei file nella cache di pagine in statistiche aggregate per file. Se noti che sono stati letti più byte di un file rispetto alle dimensioni totali del file sul disco, stai sicuramente riscontrando un thrashing della cache di pagina.

Ciò significa che il set di lavoro richiesto dal carico di lavoro (in genere una singola app più system_server) è maggiore della quantità di memoria disponibile per la cache di pagine sul dispositivo. Di conseguenza, mentre una parte del carico di lavoro ottiene i dati di cui ha bisogno nella cache della pagina, un'altra parte che verrà utilizzata nel prossimo futuro verrà eliminata e dovrà essere recuperata di nuovo, provocando il ripetersi del problema fino al completamento del caricamento. Questa è la causa fondamentale dei problemi di prestazioni quando non è disponibile memoria sufficiente su un dispositivo.

Non esiste un modo infallibile per correggere il thrashing della cache di pagina, ma esistono alcuni modi per provare a migliorare la situazione su un determinato dispositivo.

  • Utilizza meno memoria nei processi permanenti. Meno memoria viene utilizzata dalle attività permanenti, più memoria sarà disponibile per le app e la cache di pagine.
  • Controlla le esenzioni di cui disponi per il tuo dispositivo per assicurarti di non rimuovere inutilmente la memoria dal sistema operativo. Abbiamo riscontrato situazioni in cui i carveout utilizzati per il debug sono stati accidentalmente lasciati nelle configurazioni del kernel di produzione, consumando decine di megabyte di memoria. Questo può fare la differenza tra colpire o meno il thrashing della cache di pagina, soprattutto su dispositivi con meno memoria.
  • Se noti un utilizzo elevato della cache di pagina in system_server su file critici, prendi in considerazione la possibilità di bloccarli. Ciò aumenterà la pressione sulla memoria altrove, ma potrebbe modificare il comportamento in modo sufficiente per evitare il thrashing.
  • Reimposta lowmemorykiller per provare a mantenere più memoria libera. Le soglie di lowmemorykiller si basano sia sulla memoria libera assoluta sia sulla cache di pagina, pertanto l'aumento della soglia a cui vengono interrotti i processi a un determinato livello oom_adj può comportare un comportamento migliore a scapito dell'aumento dell'interruzione delle app in background.
  • Prova a utilizzare ZRAM. Utilizziamo ZRAM su Pixel, anche se ha 4 GB, perché potrebbe essere utile per le pagine sporche usate raramente.