Questa pagina si concentra sui fattori che contribuiscono alla latenza di output, ma un discorso simile vale per la latenza di input.
Supponendo che il circuito analogico non contribuisca in modo significativo, i principali fattori che contribuiscono alla latenza audio a livello di superficie sono i seguenti:
- Applicazione
- Numero totale di buffer nella pipeline
- Dimensioni di ogni buffer, in frame
- Latenza aggiuntiva dopo l'elaborazione dell'app, ad esempio da un DSP
Per quanto accurato possa essere l'elenco di collaboratori riportato sopra, è anche fuorviante. Il motivo è che il numero e le dimensioni del buffer sono più un effetto che una causa. Di solito accade che un determinato schema di buffer venga implementato e testato, ma durante il test si verifica un sottorun o un overrun audio che si manifesta come un "clic" o un "pop". Per compensare, il progettista del sistema aumenta le dimensioni o il numero di buffer. Il risultato auspicato è l'eliminazione degli underrun o degli overrun, ma ha anche il fastidioso effetto collaterale dell'aumento della latenza. Per ulteriori informazioni sulle dimensioni del buffer, guarda il video Latenza audio: dimensioni del buffer.
Un approccio migliore è comprendere le cause degli scostamenti inferiori e superiori e correggerli. In questo modo vengono eliminati gli artefatti udibili e possono essere consentiti buffer anche più piccoli o meno e quindi ridurre la latenza.
In base alla nostra esperienza, le cause più comuni di sottoutilizzazione e sovrautilizzazione sono:
- Linux CFS (Completely Fair Scheduler)
- Thread ad alta priorità con pianificazione SCHED_FIFO
- inversione della priorità
- latenza di pianificazione lunga
- gestori di interruzioni a lungo termine
- tempo di disattivazione dell'interruzione lunga
- gestione dell'alimentazione
- kernel di sicurezza
Pianificazione CFS e SCHED_FIFO di Linux
Il CFS di Linux è progettato per essere equo nei confronti dei carichi di lavoro in concorrenza che condividono una risorsa CPU comune. Questa equità è rappresentata da un parametro nice per thread. Il valore di nice va da -19 (meno gentile o più tempo CPU allocato) a 20 (più gentile o meno tempo CPU allocato). In generale, tutti i thread con un determinato valore di nice ricevono un tempo della CPU approssimativamente uguale e i thread con un valore di nice numericamente inferiore dovrebbero aspettarsi di ricevere più tempo della CPU. Tuttavia, il CFS è "equo" solo per periodi di osservazione relativamente lunghi. In finestre di osservazione a breve termine, CFS potrebbe allocare la risorsa CPU in modi imprevisti. Ad esempio, potrebbe rimuovere la CPU da un thread con un valore di nice numericamente basso a un thread con un valore di nice numericamente elevato. Nel caso dell'audio, questo può comportare un sottoutilizzo o un sovrautilizzo.
La soluzione ovvia è evitare il CFS per i thread audio ad alte prestazioni. A partire da Android 4.1, questi thread ora utilizzano il criterio di pianificazione SCHED_FIFO
anziché il criterio di pianificazione SCHED_NORMAL
(chiamato anche SCHED_OTHER
) implementato da CFS.
Priorità SCHED_FIFO
Anche se ora i thread audio ad alte prestazioni utilizzano SCHED_FIFO
, rimangono sensibili ad altri thread SCHED_FIFO
con priorità più elevata.
In genere si tratta di thread di lavoro del kernel, ma potrebbero essere presenti anche alcuni
thread utente non audio con il criterio SCHED_FIFO
. Le priorità SCHED_FIFO
disponibili vanno da 1 a 99. I thread audio vengono eseguiti con priorità 2 o 3. La priorità 1 rimane disponibile per i thread con priorità inferiore e le priorità da 4 a 99 per i thread con priorità superiore. Ti consigliamo di utilizzare la priorità 1, se possibile, e di riservare le priorità da 4 a 99 per i thread che hanno la garanzia di essere completati entro un determinato periodo di tempo, vengono eseguiti con un periodo più breve rispetto a quello dei thread audio e non interferiscono con la pianificazione dei thread audio.
Programmazione con tasso monotonico
Per ulteriori informazioni sulla teoria dell'assegnazione di priorità fisse, consulta l'articolo di Wikipedia sulla pianificazione a frequenza monotonica (RMS). Un punto chiave è che le priorità fisse devono essere assegnate in base al periodo, con priorità più elevate assegnate ai thread di periodi più brevi, non in base all'"importanza" percepita. I thread non periodici possono essere modellati come thread periodici, utilizzando la frequenza massima di esecuzione e il calcolo massimo per esecuzione. Se un thread non periodico non può essere modellato come un thread periodico (ad esempio, potrebbe essere eseguito con una frequenza illimitata o con un calcolo illimitato per esecuzione), non deve essere assegnata una priorità fissa in quanto sarebbe incompatibile con la pianificazione di thread periodici veri e propri.
Inversione della priorità
L'inversione di priorità è un classico modo di errore dei sistemi in tempo reale, in cui un'attività con priorità più alta è bloccata per un tempo illimitato in attesa che un'attività con priorità inferiore rilasci una risorsa come (stato condiviso protetto da) un mutex. Consulta l'articolo "Evitare l'inversione della priorità" per conoscere le tecniche per attenuarla.
Latenza di pianificazione
La latenza di pianificazione è il tempo che intercorre tra il momento in cui un thread è pronto per l'esecuzione e il completamento del relativo cambio di contesto, in modo che il thread venga effettivamente eseguito su una CPU. Maggiore è la latenza, peggiore è la qualità audio. La latenza di pianificazione lunga è più probabile che si verifichi durante le transizioni di modalità, ad esempio l'avvio o l'arresto di una CPU, il passaggio da un kernel di sicurezza al kernel normale, il passaggio dalla modalità a piena potenza a quella a basso consumo o la regolazione della frequenza e della tensione del clock della CPU.
Interruzioni
In molti progetti, la CPU 0 gestisce tutte le interruzioni esterne. Pertanto, un gestore di interruzioni di lunga durata potrebbe ritardare altre interruzioni, in particolare quelle di completamento dell'accesso diretto alla memoria (DMA) audio. Progetta gestori di interruzioni
per terminare rapidamente e rimandare il lavoro lungo a un thread (preferibilmente
un thread CFS o SCHED_FIFO
di priorità 1).
In modo equivalente, la disattivazione delle interruzioni sulla CPU 0 per un lungo periodo ha lo stesso risultato di ritardare la gestione delle interruzioni audio. Tempi di disattivazione delle interruzioni lunghi si verificano in genere durante l'attesa di un blocco spinlock del kernel. Controlla questi blocchi di rotazione per assicurarti che siano limitati.
Potenza, prestazioni e gestione termica
La gestione dell'alimentazione è un termine ampio che comprende le attività di monitoraggio e riduzione del consumo energetico, nonché l'ottimizzazione delle prestazioni. La gestione termica e il raffreddamento del computer sono simili, ma mirano a misurare e controllare il calore per evitare danni dovuti al calore eccessivo. Nel kernel di Linux, il governor della CPU è responsabile dei criteri di basso livello, mentre la modalità utente configura i criteri di alto livello. Le tecniche utilizzate includono:
- regolazione dinamica della tensione
- Scalabilità dinamica della frequenza
- Attivazione del nucleo dinamico
- passaggio da un cluster all'altro
- gating dell'alimentazione
- hotplug (hotswap)
- varie modalità di sospensione (halt, stop, idle, suspend e così via)
- Eseguire la migrazione dei processi
- affinità del processore
Alcune operazioni di gestione possono comportare "interruzioni del lavoro" o periodi di tempo durante i quali l'elaborazione dell'applicazione non viene eseguita. Queste interruzioni del lavoro possono interferire con l'audio, pertanto questa gestione deve essere progettata per un'interruzione del lavoro accettabile nel caso peggiore mentre l'audio è attivo. Ovviamente, quando il surriscaldamento è imminente, evitare danni permanenti è più importante dell'audio.
Kernel di sicurezza
Un kernel di sicurezza per il Digital Rights Management (DRM) può essere eseguito sugli stessi core dell'unità di elaborazione dell'applicazione utilizzati per il kernel del sistema operativo principale e il codice dell'applicazione. Qualsiasi tempo durante il quale un'operazione del kernel di sicurezza è attiva su un core è effettivamente un arresto del lavoro ordinario che normalmente viene eseguito su quel core. In particolare, potrebbero essere inclusi lavori audio. Per sua natura, il comportamento interno di un kernel di sicurezza è indecifrabile dai livelli di livello superiore e, pertanto, eventuali anomalie di prestazioni causate da un kernel di sicurezza sono particolarmente dannose. Ad esempio, le operazioni del kernel di sicurezza in genere non compaiono nelle tracce di switch di contesto. Lo chiamiamo "tempo scuro", ovvero il tempo che trascorre, ma che non può essere osservato. I kernel di sicurezza devono essere progettati per un interruzione del lavoro nel peggiore dei casi accettabile quando l'audio è attivo.