Le jitter est le comportement aléatoire du système qui empêche l'exécution d'un travail perceptible. Cette page explique comment identifier et résoudre les problèmes de à-coups liés au jitter.
Délai du planificateur de threads de l'application
Le retard du planificateur est le symptôme le plus évident du jitter: un processus qui doit être exécuté est rendu exécutable, mais ne s'exécute pas pendant une période significative. L'importance du délai varie selon le contexte. Exemple :
- Un thread d'assistance aléatoire dans une application peut probablement être retardé de plusieurs millisecondes sans problème.
- Le thread d'UI d'une application peut tolérer 1 à 2 ms de jitter.
- Les kthreads du pilote exécutés en tant que SCHED_FIFO peuvent entraîner des problèmes s'ils sont exécutables pendant 500 µs avant l'exécution.
Les temps d'exécution peuvent être identifiés dans systrace par la barre bleue précédant un segment d'exécution d'un thread. Un temps d'exécution peut également être déterminé par la durée écoulée entre l'événement sched_wakeup
d'un thread et l'événement sched_switch
qui signale le début de l'exécution du thread.
Threads qui s'exécutent trop longtemps
Les threads d'UI de l'application qui peuvent s'exécuter trop longtemps peuvent entraîner des problèmes. Les threads de niveau inférieur avec des temps d'exécution longs ont généralement des causes différentes, mais essayer de pousser le temps d'exécution du thread d'interface utilisateur vers zéro peut nécessiter de résoudre certains des mêmes problèmes qui font que les threads de niveau inférieur ont des temps d'exécution longs. Pour réduire les retards:
- Utilisez des cpusets comme décrit dans la section Limitation thermique.
- Augmentez la valeur CONFIG_HZ.
- Par le passé, cette valeur était définie sur 100 sur les plates-formes arm et arm64. Toutefois, il s'agit d'un accident de l'histoire, et ce n'est pas une bonne valeur à utiliser pour les appareils interactifs. CONFIG_HZ=100 signifie qu'un jiffy dure 10 ms, ce qui signifie que l'équilibrage de charge entre les processeurs peut prendre 20 ms (deux jiffies). Cela peut contribuer de manière significative à la saccade sur un système chargé.
- Les appareils récents (Nexus 5X, Nexus 6P, Pixel et Pixel XL) sont livrés avec CONFIG_HZ=300. Cela devrait avoir un coût énergétique négligeable tout en améliorant considérablement les temps d'exécution. Si vous constatez une augmentation significative de la consommation d'énergie ou des problèmes de performances après avoir modifié CONFIG_HZ, il est probable que l'un de vos pilotes utilise un minuteur basé sur des jiffies bruts au lieu de millisecondes et qu'il les convertisse en jiffies. Ce problème est généralement facile à résoudre (voir le correctif qui a résolu les problèmes de minuteur kgsl sur les Nexus 5X et 6P lors de la conversion en CONFIG_HZ=300).
- Enfin, nous avons testé CONFIG_HZ=1000 sur Nexus/Pixel et avons constaté qu'il offre une réduction notable des performances et de la consommation d'énergie en raison de la réduction des frais généraux de RCU.
Avec ces deux modifications, un appareil devrait être beaucoup plus performant pour le temps d'exécution du thread d'UI sous charge.
Utiliser sys.use_fifo_ui
Vous pouvez essayer de définir la durée d'exécution du thread d'UI sur zéro en définissant la propriété sys.use_fifo_ui
sur 1.
Avertissement: N'utilisez pas cette option sur des configurations de CPU hétérogènes, sauf si vous disposez d'un planificateur RT tenant compte de la capacité.
Pour le moment, AUCUN PLANIFICATEUR RT LIVRAISON EN COURS N'EST CONSCIENCIEUX DE LA CAPACITÉ. Nous en travaillons un pour EAS, mais il n'est pas encore disponible. Le planificateur RT par défaut est basé uniquement sur les priorités RT et sur le fait qu'un CPU dispose déjà d'un thread RT de priorité égale ou supérieure.
Par conséquent, l'ordonnanceur RT par défaut déplace votre thread d'UI relativement long d'un grand cœur à haute fréquence vers un petit cœur à fréquence minimale si un kthread FIFO de priorité supérieure se réveille sur le même grand cœur. Cela entraînera une régression significative des performances. Cette option n'a pas encore été utilisée sur un appareil Android commercialisé. Si vous souhaitez l'utiliser, contactez l'équipe chargée des performances Android pour qu'elle vous aide à la valider.
Lorsque sys.use_fifo_ui
est activé, ActivityManager suit le thread d'UI et RenderThread (les deux threads les plus critiques pour l'UI) de l'application principale et les définit sur SCHED_FIFO au lieu de SCHED_OTHER. Cela élimine efficacement le jitter de l'UI et des threads de rendu. Les traces que nous avons collectées avec cette option activée indiquent des temps d'exécution de l'ordre de microsecondes au lieu de millisecondes.
Toutefois, comme l'équilibreur de charge RT n'était pas conscient de la capacité, les performances de démarrage de l'application ont été réduites de 30 %, car le thread d'UI chargé de démarrer l'application a été déplacé d'un cœur Kryo gold 2,1 GHz à un cœur Kryo silver 1,5 GHz. Avec un équilibreur de charge RT tenant compte de la capacité, nous observons des performances équivalentes dans les opérations groupées et une réduction de 10 à 15% des délais de frame au 95e et 99e percentile dans de nombreux benchmarks d'interface utilisateur.
Interrompre le trafic
Étant donné que les plates-formes ARM n'envoient des interruptions au processeur 0 que par défaut, nous vous recommandons d'utiliser un équilibreur d'IRQ (irqbalance ou msm_irqbalance sur les plates-formes Qualcomm).
Lors du développement du Pixel, nous avons constaté des à-coups qui pouvaient être attribués directement à la surcharge du processeur 0 avec des interruptions. Par exemple, si le thread mdss_fb0
était planifié sur le processeur 0, il était beaucoup plus probable que le thread soit interrompu en raison d'une interruption déclenchée par l'écran presque immédiatement avant le balayage. mdss_fb0
serait au milieu de son propre travail avec un délai très serré, et il perdrait du temps par rapport au gestionnaire d'interruption MDSS. Au départ, nous avons essayé de résoudre ce problème en définissant l'affinité de processeur du thread mdss_fb0 sur les processeurs 1 à 3 pour éviter les conflits avec l'interruption, mais nous avons ensuite réalisé que nous n'avions pas encore activé msm_irqbalance. Avec msm_irqbalance activé, le à-coup a été considérablement amélioré, même lorsque mdss_fb0 et l'interruption MDSS se trouvaient sur le même processeur en raison de la réduction des conflits avec d'autres interruptions.
Vous pouvez l'identifier dans systrace en examinant la section "sched" ainsi que la section "irq". La section "sched" indique ce qui a été planifié, mais une région qui se chevauche dans la section "irq" signifie qu'une interruption s'exécute à ce moment-là au lieu du processus normalement planifié. Si vous constatez des périodes de temps importantes prises lors d'une interruption, vous pouvez procéder comme suit:
- Accélérez le gestionnaire d'interruption.
- Empêchez l'interruption de se produire en premier lieu.
- Modifiez la fréquence de l'interruption pour qu'elle soit en phase avec d'autres tâches régulières avec lesquelles elle peut interférer (s'il s'agit d'une interruption régulière).
- Définissez directement l'affinité de processeur de l'interruption et empêchez son équilibre.
- Définissez l'affinité de processeur du thread avec lequel l'interruption interfère pour éviter l'interruption.
- Utilisez l'équilibreur d'interruptions pour déplacer l'interruption vers un processeur moins chargé.
Définir l'affinité de processeur n'est généralement pas recommandé, mais peut s'avérer utile dans certains cas. En règle générale, il est trop difficile de prédire l'état du système pour la plupart des interruptions courantes. Toutefois, si vous disposez d'un ensemble de conditions très spécifiques qui déclenchent certaines interruptions lorsque le système est plus contraint que d'habitude (comme la VR), une affinité de processeur explicite peut être une bonne solution.
Softirqs longs
Lorsqu'un softirq est en cours d'exécution, il désactive la préemption. Les softirqs peuvent également être déclenchés à de nombreux endroits dans le noyau et peuvent s'exécuter dans un processus utilisateur. S'il y a suffisamment d'activité softirq, les processus utilisateur cessent d'exécuter des softirq, et ksoftirqd se réveille pour exécuter des softirq et équilibrer la charge. Cela ne pose généralement pas de problème. Toutefois, un seul softirq très long peut causer des ravages sur le système.
Les softirqs sont visibles dans la section irq d'une trace. Ils sont donc faciles à repérer si le problème peut être reproduit lors du traçage. Étant donné qu'un softirq peut s'exécuter dans un processus utilisateur, un softirq incorrect peut également se manifester sous la forme d'un temps d'exécution supplémentaire dans un processus utilisateur sans raison apparente. Si vous constatez cela, vérifiez la section irq pour voir si les softirqs sont en cause.
Pilotes laissant la préemption ou les IRQ désactivées trop longtemps
La désactivation de la préemption ou des interruptions pendant trop longtemps (des dizaines de millisecondes) entraîne des à-coups. En règle générale, le "jank" se manifeste lorsqu'un thread devient exécutable, mais qu'il ne s'exécute pas sur un CPU particulier, même si le thread exécutable est nettement prioritaire (ou SCHED_FIFO) par rapport à l'autre thread.
Voici quelques consignes:
- Si le thread exécutable est SCHED_FIFO et que le thread en cours d'exécution est SCHED_OTHER, la préemption ou les interruptions sont désactivées pour le thread en cours d'exécution.
- Si le thread exécutable a une priorité nettement plus élevée (100) que le thread en cours d'exécution (120), la préemption ou les interruptions sont probablement désactivées pour le thread en cours d'exécution si le thread exécutable ne s'exécute pas dans les deux jiffies.
- Si le thread exécutable et le thread en cours d'exécution ont la même priorité, la préemption ou les interruptions sont probablement désactivées pour le thread en cours d'exécution si le thread exécutable ne s'exécute pas dans les 20 ms.
N'oubliez pas que l'exécution d'un gestionnaire d'interruption vous empêche de gérer d'autres interruptions, ce qui désactive également la préemption.
Une autre option pour identifier les régions concernées consiste à utiliser le traceur preemptirqsoff (voir Utiliser ftrace dynamique). Ce traceur peut fournir des informations beaucoup plus détaillées sur la cause première d'une région non interruptible (comme les noms de fonction), mais son activation nécessite des travaux plus invasifs. Bien que cela puisse avoir un impact plus important sur les performances, cela vaut vraiment la peine d'essayer.
Utilisation incorrecte des files d'attente de travail
Les gestionnaires d'interruptions doivent souvent effectuer des tâches qui peuvent s'exécuter en dehors d'un contexte d'interruption, ce qui permet de répartir le travail entre différents threads du noyau. Un développeur de pilotes peut remarquer que le noyau dispose d'une fonctionnalité de tâche asynchrone très pratique à l'échelle du système appelée workqueues et peut l'utiliser pour le travail lié aux interruptions.
Toutefois, les files d'attente de travail sont presque toujours la mauvaise réponse à ce problème, car elles sont toujours SCHED_OTHER. De nombreuses interruptions matérielles se trouvent sur le chemin critique des performances et doivent être exécutées immédiatement. Il n'existe aucune garantie quant au moment où les files d'attente de travail seront exécutées. Chaque fois que nous avons vu une file d'attente de travail dans le chemin critique des performances, elle a été une source de saccades sporadiques, quel que soit l'appareil. Sur un Pixel, avec un processeur phare, nous avons constaté qu'une seule file d'attente de travail pouvait être retardée jusqu'à 7 ms si l'appareil était sous charge, en fonction du comportement du planificateur et d'autres éléments exécutés sur le système.
Au lieu d'un workqueue, les pilotes qui doivent gérer des tâches semblables à des interruptions dans un thread distinct doivent créer leur propre kthread SCHED_FIFO. Pour savoir comment procéder avec les fonctions kthread_work, consultez ce correctif.
Conflit de verrouillage du framework
Le conflit de verrouillage du framework peut être une source de saccades ou d'autres problèmes de performances. Il est généralement causé par le verrouillage ActivityManagerService, mais peut également être observé dans d'autres verrouillages. Par exemple, le verrouillage PowerManagerService peut affecter les performances de l'écran allumé. Si vous voyez ce message sur votre appareil, il n'existe pas de solution satisfaisante, car il ne peut être amélioré que par des améliorations architecturales du framework. Toutefois, si vous modifiez du code qui s'exécute dans system_server, il est essentiel d'éviter de maintenir des verrous pendant une longue période, en particulier le verrou ActivityManagerService.
Conflit de verrouillage de la reliure
Par le passé, le binder disposait d'un seul verrouillage global. Si le thread exécutant une transaction de liaison a été préempté alors qu'il détenait le verrou, aucun autre thread ne peut effectuer de transaction de liaison tant que le thread d'origine n'a pas libéré le verrou. Ce n'est pas bon. La contention du liaisonneur peut bloquer tout le système, y compris l'envoi de mises à jour de l'UI à l'écran (les threads d'UI communiquent avec SurfaceFlinger via le liaisonneur).
Android 6.0 inclut plusieurs correctifs pour améliorer ce comportement en désactivant la préemption tout en maintenant le verrouillage du liaisonneur. Cette opération n'était sûre que parce que le verrouillage du liaisonneur devait être maintenu pendant quelques microsecondes d'exécution réelle. Cela a considérablement amélioré les performances dans les situations sans conflit et empêché les conflits en empêchant la plupart des changements de planificateur pendant le verrouillage du liaisonneur. Toutefois, la préemption ne pouvait pas être désactivée pour toute la durée d'exécution de la détention du verrouillage du liaisonneur, ce qui signifie que la préemption était activée pour les fonctions pouvant être en veille (telles que copy_from_user), ce qui pouvait entraîner la même préemption que dans le cas d'origine. Lorsque nous avons envoyé les correctifs en amont, ils nous ont rapidement dit que c'était la pire idée de l'histoire. (Nous étions d'accord avec eux, mais nous ne pouvions pas non plus contester l'efficacité des correctifs pour éviter les à-coups.)
Conflit de fd dans un processus
Cela est rare. Ce n'est probablement pas la cause de votre à-coup.
Toutefois, si plusieurs threads d'un même processus écrivent le même fd, il est possible de constater une contention sur ce fd. Toutefois, la seule fois où nous avons observé cela lors de la mise en service de Pixel était lors d'un test où des threads de faible priorité tentaient d'occuper tout le temps de processeur alors qu'un seul thread de haute priorité s'exécutait dans le même processus. Tous les threads écrivaient dans le fd du repère de trace, et le thread de priorité élevée pouvait être bloqué sur le fd du repère de trace si un thread de priorité faible détenait le verrouillage du fd et était ensuite préempté. Lorsque le traçage a été désactivé dans les threads à faible priorité, aucun problème de performances n'a été détecté.
Nous n'avons pas pu reproduire ce problème dans aucune autre situation, mais il est utile de le signaler comme une cause potentielle de problèmes de performances lors du traçage.
Transitions inutiles du processeur au mode inactif
Lorsque vous travaillez avec l'IPC, en particulier avec les pipelines multiprocessus, il est courant de constater des variations du comportement d'exécution suivant:
- Le thread A s'exécute sur le processeur 1.
- Le thread A réveille le thread B.
- Le thread B commence à s'exécuter sur le CPU 2.
- Le thread A passe immédiatement en veille, pour être réveillé par le thread B une fois qu'il a terminé son travail en cours.
Une source courante de frais généraux se situe entre les étapes 2 et 3. Si le processeur 2 est inactif, il doit être remis à un état actif avant que le thread B puisse s'exécuter. En fonction du SOC et de l'état d'inactivité, cela peut prendre des dizaines de microsecondes avant que le thread B ne commence à s'exécuter. Si l'environnement d'exécution réel de chaque côté de l'IPC est suffisamment proche des frais généraux, les performances globales de ce pipeline peuvent être considérablement réduites par les transitions au ralenti du processeur. Le cas le plus courant où Android rencontre ce problème concerne les transactions de liaison, et de nombreux services qui utilisent la liaison finissent par ressembler à la situation décrite ci-dessus.
Tout d'abord, utilisez la fonction wake_up_interruptible_sync()
dans vos pilotes de kernel et prenez-en en charge à partir de n'importe quel planificateur personnalisé. Traitez cela comme une exigence, et non comme un indice. Binder l'utilise aujourd'hui, et cela aide beaucoup les transactions de liaisons synchrones à éviter les transitions d'inactivité du processeur inutiles.
Deuxièmement, assurez-vous que vos temps de transition cpuidle sont réalistes et que le gouverneur cpuidle les prend correctement en compte. Si votre SOC passe fréquemment de l'état d'inactivité le plus profond à l'état d'inactivité le plus profond, vous n'économiserez pas d'énergie en passant à l'état d'inactivité le plus profond.
Journalisation
La journalisation n'est pas sans frais en termes de cycles de processeur ou de mémoire. Par conséquent, n'envoyez pas de spam dans le tampon de journalisation. Les coûts de journalisation sont cycliques dans votre application (directement) et dans le daemon de journalisation. Supprimez tous les journaux de débogage avant d'expédier votre appareil.
Problèmes d'E/S
Les opérations d'E/S sont des sources courantes de jitter. Si un thread accède à un fichier mappé en mémoire et que la page ne se trouve pas dans le cache de pages, il génère une erreur et lit la page à partir du disque. Cela bloque le thread (généralement pendant plus de 10 ms) et, si cela se produit sur le chemin critique du rendu de l'UI, cela peut entraîner des à-coups. Il existe trop de causes d'opérations d'E/S pour en discuter ici, mais vérifiez les emplacements suivants lorsque vous essayez d'améliorer le comportement des E/S:
- PinnerService. Ajouté dans Android 7.0, PinnerService permet au framework de verrouiller certains fichiers dans le cache de pages. Cela supprime la mémoire pour tout autre processus, mais si certains fichiers sont connus a priori comme étant utilisés régulièrement, il peut être efficace de les mlock.
Sur les appareils Pixel et Nexus 6P équipés d'Android 7.0, nous avons verrouillé quatre fichiers :- /system/framework/arm64/boot-framework.oat
- /system/framework/oat/arm64/services.odex
- /system/framework/arm64/boot.oat
- /system/framework/arm64/boot-core-libart.oat
- Chiffrement. Autre cause possible de problèmes d'E/S Nous constatons que le chiffrement intégré offre les meilleures performances par rapport au chiffrement basé sur le processeur ou à l'utilisation d'un bloc matériel accessible via DMA. Plus important encore, le chiffrement en ligne réduit le jitter associé aux E/S, en particulier par rapport au chiffrement basé sur le processeur. Étant donné que les récupérations dans le cache de page se trouvent souvent sur le chemin critique du rendu de l'interface utilisateur, le chiffrement basé sur le processeur introduit une charge de processeur supplémentaire sur le chemin critique, ce qui ajoute plus de jitter que la simple récupération d'E/S.
Les moteurs de chiffrement matériel basés sur DMA présentent un problème similaire, car le noyau doit dépenser des cycles pour gérer cette tâche, même si d'autres tâches critiques sont disponibles à l'exécution. Nous recommandons vivement à tout fournisseur de SOC qui crée du matériel de prendre en charge le chiffrement en ligne.
Emballage agressif de petites tâches
Certains planificateurs permettent de regrouper de petites tâches sur des cœurs de processeur uniques afin de réduire la consommation d'énergie en maintenant plus de processeurs inactifs plus longtemps. Bien que cela fonctionne bien pour le débit et la consommation d'énergie, cela peut être catastrophique pour la latence. Plusieurs threads de courte durée dans le chemin critique du rendu de l'interface utilisateur peuvent être considérés comme petits. Si ces threads sont retardés lors de leur migration lente vers d'autres processeurs, cela entraînera des à-coups. Nous vous recommandons d'utiliser le petit empaquetage de tâches de manière très conservatrice.
Épuisement du cache de page
Un appareil qui ne dispose pas de suffisamment de mémoire libre peut soudainement devenir extrêmement lent lors de l'exécution d'une opération de longue durée, comme l'ouverture d'une nouvelle application. Une trace de l'application peut révéler qu'elle est constamment bloquée en E/S lors d'une exécution particulière, même si elle n'est souvent pas bloquée en E/S. Il s'agit généralement d'un signe de remplissage du cache de page, en particulier sur les appareils disposant de moins de mémoire.
Pour identifier ce problème, vous pouvez créer une trace système à l'aide de la balise pagecache et transmettre cette trace au script à l'emplacement system/extras/pagecache/pagecache.py
. pagecache.py traduit les requêtes individuelles visant à mapper des fichiers dans le cache de pages en statistiques agrégées par fichier. Si vous constatez que plus d'octets d'un fichier ont été lus que la taille totale de ce fichier sur le disque, vous rencontrez certainement un problème de remplissage du cache de pages.
Cela signifie que l'ensemble de travail requis par votre charge de travail (généralement une seule application plus system_server) est supérieur à la quantité de mémoire disponible pour le cache de pages de votre appareil. Par conséquent, alors qu'une partie de la charge de travail obtient les données dont elle a besoin dans le cache de pages, une autre partie qui sera utilisée prochainement sera supprimée et devra être récupérée à nouveau, ce qui entraînera le problème à nouveau jusqu'à ce que la charge soit terminée. C'est la cause fondamentale des problèmes de performances lorsque la mémoire disponible sur un appareil est insuffisante.
Il n'existe aucun moyen infaillible de résoudre le problème de remplissage du cache de page, mais il existe plusieurs façons d'essayer de l'améliorer sur un appareil donné.
- Utilisation de moins de mémoire dans les processus persistants. Moins de mémoire est utilisée par les processus persistants, plus la mémoire est disponible pour les applications et le cache de pages.
- Auditez les découpages que vous avez effectués pour votre appareil afin de vous assurer que vous ne supprimez pas inutilement de la mémoire du système d'exploitation. Nous avons constaté des cas où des découpages utilisés pour le débogage ont été accidentellement laissés dans les configurations du noyau, consommant ainsi des dizaines de mégaoctets de mémoire. Cela peut faire la différence entre un remplissage du cache de page et un non-remplissage, en particulier sur les appareils disposant de moins de mémoire.
- Si vous constatez un remplissage du cache de page dans system_server sur des fichiers critiques, envisagez d'épingler ces fichiers. Cela augmentera la pression sur la mémoire ailleurs, mais cela peut modifier suffisamment le comportement pour éviter les accès aléatoires.
- Réajustez lowmemorykiller pour essayer de libérer plus de mémoire. Les seuils de lowmemorykiller sont basés à la fois sur la mémoire libre absolue et le cache de pages. Par conséquent, augmenter le seuil auquel les processus à un niveau oom_adj donné sont arrêtés peut entraîner un meilleur comportement au détriment d'une augmentation des arrêts d'applications en arrière-plan.
- Essayez d'utiliser ZRAM. Nous utilisons ZRAM sur le Pixel, même si le Pixel dispose de 4 Go, car cela peut aider avec les pages sales rarement utilisées.