Identifier les interférences liées à la gigue

La gigue est le comportement aléatoire du système qui empêche l'exécution d'un travail perceptible. Cette page décrit comment identifier et résoudre les problèmes de jank liés à la gigue.

Délai du planificateur de threads d'application

Le retard du planificateur est le symptôme le plus évident de la gigue : un processus qui devrait être exécuté est rendu exécutable mais ne s'exécute pas pendant une période de temps significative. L'importance du retard varie selon le contexte. Par exemple:

  • Un fil d'assistance aléatoire dans une application peut probablement être retardé de plusieurs millisecondes sans problème.
  • Le thread d’interface utilisateur d’une application peut tolérer 1 à 2 ms de gigue.
  • Les kthreads du pilote exécutés sous SCHED_FIFO peuvent provoquer des problèmes s'ils peuvent être exécutés pendant 500 us avant d'être exécutés.

Les temps exécutables peuvent être identifiés dans systrace par la barre bleue précédant un segment en cours d'exécution d'un thread. Un temps d'exécution peut également être déterminé par la durée entre l'événement sched_wakeup pour un thread et l'événement sched_switch qui signale le début de l'exécution du thread.

Sujets trop longs

Les threads de l’interface utilisateur d’application qui peuvent être exécutés trop longtemps peuvent provoquer 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 tenter de pousser le temps d'exécution des threads d'interface utilisateur vers zéro peut nécessiter de résoudre certains des mêmes problèmes qui entraînent des temps d'exécution longs pour les threads de niveau inférieur. Pour atténuer les retards :

  1. Utilisez les cpusets comme décrit dans Limitation thermique .
  2. Augmentez la valeur CONFIG_HZ.
    • Historiquement, la valeur a été fixée à 100 sur les plateformes arm et arm64. Cependant, 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 jiffy) pour se produire. Cela peut contribuer de manière significative aux erreurs 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 de fonctionnement. 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 une minuterie basée sur les jiffies bruts au lieu des millisecondes et se convertit en jiffies. Il s'agit généralement d'une solution simple (voir le correctif qui corrige les problèmes de minuterie kgsl sur les Nexus 5X et 6P lors de la conversion en CONFIG_HZ=300).
    • Enfin, nous avons expérimenté CONFIG_HZ=1000 sur Nexus/Pixel et avons constaté qu'il offre des performances et une réduction de puissance notables en raison de la diminution de la surcharge RCU.

Avec ces deux seuls changements, un appareil devrait être bien meilleur en termes de temps d'exécution du thread d'interface utilisateur sous charge.

Utilisation de sys.use_fifo_ui

Vous pouvez essayer de réduire le temps d'exécution du thread d'interface utilisateur à zéro en définissant la propriété sys.use_fifo_ui sur 1.

Attention : n'utilisez pas cette option sur des configurations de processeur hétérogènes, sauf si vous disposez d'un planificateur RT prenant en compte la capacité. Et, à l’heure actuelle, AUCUN PLANIFICATEUR RT D’EXPÉDITION ACTUELLEMENT N’EST CONSCIENT DE LA CAPACITÉ . Nous travaillons sur un modèle 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 processeur dispose déjà d'un thread RT de priorité égale ou supérieure.

En conséquence, le planificateur RT par défaut déplacera volontiers votre thread d'interface utilisateur relativement long d'un gros cœur à haute fréquence vers un petit cœur à fréquence minimale si un kthread FIFO de priorité plus élevée se réveille sur le même gros cœur. Cela introduira des régressions de performances significatives . Comme cette option n'a pas encore été utilisée sur un appareil Android livré, si vous souhaitez l'utiliser, contactez l'équipe de performances Android pour vous aider à la valider.

Lorsque sys.use_fifo_ui est activé, ActivityManager suit le thread d'interface utilisateur et RenderThread (les deux threads les plus critiques pour l'interface utilisateur) de l'application principale et crée ces threads SCHED_FIFO au lieu de SCHED_OTHER. Cela élimine efficacement la gigue de l'interface utilisateur et des RenderThreads ; les traces que nous avons collectées avec cette option activée affichent des temps d'exécution de l'ordre de la microseconde au lieu de la milliseconde.

Cependant, comme l'équilibreur de charge RT ne tenait pas compte de la capacité, les performances de démarrage de l'application ont été réduites de 30 %, car le thread d'interface utilisateur responsable du démarrage de l'application serait déplacé d'un cœur Kryo doré à 2,1 Ghz vers un cœur Kryo argenté à 1,5 GHz. . Avec un équilibreur de charge RT sensible à la capacité, nous constatons des performances équivalentes dans les opérations en masse et une réduction de 10 à 15 % des temps d'image aux 95e et 99e centiles dans bon nombre de nos tests d'interface utilisateur.

Interrompre le trafic

Étant donné que les plates-formes ARM délivrent des interruptions au CPU 0 uniquement par défaut, nous recommandons l'utilisation d'un équilibreur IRQ (irqbalance ou msm_irqbalance sur les plates-formes Qualcomm).

Au cours du développement de Pixel, nous avons constaté des erreurs qui pouvaient être directement attribuées à une surcharge du processeur 0 avec des interruptions. Par exemple, si le thread mdss_fb0 était planifié sur le processeur 0, il y avait beaucoup plus de chances d'être interrompu en raison d'une interruption déclenchée par l'affichage presque immédiatement avant l'analyse. mdss_fb0 serait au milieu de son propre travail avec un délai très serré, et il perdrait alors du temps au profit du gestionnaire d'interruptions MDSS. Initialement, nous avons tenté de résoudre ce problème en définissant l'affinité CPU 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é, Jank a été sensiblement amélioré même lorsque mdss_fb0 et l'interruption MDSS étaient sur le même processeur en raison de la réduction des conflits provenant d'autres interruptions.

Cela peut être identifié dans systrace en examinant la section sched ainsi que la section irq. La section sched montre ce qui a été planifié, mais une région qui se chevauche dans la section irq signifie qu'une interruption est en cours d'exécution pendant cette période au lieu du processus normalement planifié. Si vous constatez qu'une interruption prend beaucoup de temps, vos options incluent :

  • Rendre le gestionnaire d'interruption plus rapide.
  • Empêchez l’interruption de se produire en premier lieu.
  • Modifiez la fréquence de l'interruption pour qu'elle soit déphasée par rapport aux autres travaux réguliers avec lesquels elle peut interférer (s'il s'agit d'une interruption régulière).
  • Définissez directement l'affinité CPU de l'interruption et empêchez son équilibrage.
  • Définissez l’affinité CPU du thread avec lequel l’interruption interfère pour éviter l’interruption.
  • Fiez-vous à l'équilibreur d'interruption pour déplacer l'interruption vers un processeur moins chargé.

La définition de l'affinité CPU n'est généralement pas recommandée mais peut être utile dans des cas spécifiques. En général, il est trop difficile de prédire l'état du système pour les interruptions les plus courantes, mais si vous disposez d'un ensemble très spécifique de conditions qui déclenchent certaines interruptions où le système est plus contraint que la normale (comme VR), une affinité explicite du processeur peut être une bonne solution.

Longues softirqs

Pendant qu'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 du noyau et peuvent s'exécuter à l'intérieur d'un processus utilisateur. S'il y a suffisamment d'activité softirq, les processus utilisateur cesseront d'exécuter softirqs et ksoftirqd se réveillera pour exécuter softirqs et équilibrer la charge. Habituellement, c'est bien. Cependant, un seul softirq très long peut faire 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 au sein d'un processus utilisateur, un mauvais softirq peut également se manifester par un temps d'exécution supplémentaire au sein d'un processus utilisateur sans raison évidente. Si vous voyez cela, vérifiez la section irq pour voir si les softirqs sont à blâmer.

Pilotes laissant la préemption ou les IRQ désactivés trop longtemps

La désactivation de la préemption ou des interruptions pendant une durée trop longue (des dizaines de millisecondes) entraîne des erreurs. En règle générale, le jenk se manifeste par un thread qui devient exécutable mais ne s'exécute pas sur un processeur particulier, même si le thread exécutable a une priorité nettement plus élevée (ou SCHED_FIFO) que l'autre thread.

Quelques lignes directrices :

  • Si le thread exécutable est SCHED_FIFO et que le thread en cours d'exécution est SCHED_OTHER, le thread en cours d'exécution a la préemption ou les interruptions désactivées.
  • 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 du thread en cours d'exécution sont probablement désactivées si le thread exécutable ne s'exécute pas en deux jiffies.
  • Si le thread exécutable et le thread en cours d'exécution ont la même priorité, le thread en cours d'exécution a probablement la préemption ou les interruptions désactivées si le thread exécutable ne s'exécute pas dans les 20 ms.

Gardez à l’esprit que l’exécution d’un gestionnaire d’interruptions vous empêche de traiter d’autres interruptions, ce qui désactive également la préemption.


Une autre option pour identifier les régions incriminées consiste à utiliser le traceur preemptirqsoff (voir Utilisation de ftrace dynamique ). Ce traceur peut donner un aperçu beaucoup plus précis de la cause profonde d'une région ininterruptible (telle que les noms de fonctions), mais nécessite un travail plus invasif pour être activé. Même si cela peut 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 un travail qui peut s'exécuter en dehors d'un contexte d'interruption, ce qui permet de confier le travail à différents threads du noyau. Un développeur de pilotes peut remarquer que le noyau dispose d'une fonctionnalité de tâche asynchrone à l'échelle du système très pratique appelée files d'attente de travail et peut l'utiliser pour des travaux liés aux interruptions.

Cependant, les files d'attente de travail constituent presque toujours une mauvaise réponse à ce problème, car elles sont toujours SCHED_OTHER. De nombreuses interruptions matérielles se situent dans le chemin critique des performances et doivent être exécutées immédiatement. Les files d'attente n'ont aucune garantie quant au moment où elles seront exécutées. Chaque fois que nous avons constaté une file d'attente de travail sur le chemin critique des performances, cela a été une source de perturbations sporadiques, quel que soit le périphérique. Sur Pixel, doté d'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'une file d'attente de travail, les pilotes qui doivent gérer un travail de type interruption dans un thread séparé doivent créer leur propre kthread SCHED_FIFO. Pour obtenir de l'aide avec les fonctions kthread_work, reportez-vous à ce patch .

Conflit de verrouillage du framework

Les conflits de verrouillage du framework peuvent être une source de parasites ou d'autres problèmes de performances. Cela est généralement dû au verrou ActivityManagerService, mais peut également être observé dans d'autres verrous. Par exemple, le verrouillage PowerManagerService peut avoir un impact sur l'écran sur les performances. Si vous voyez cela sur votre appareil, il n'y a pas de bonne solution car cela ne peut être amélioré que via des améliorations architecturales du framework. Cependant, si vous modifiez du code qui s'exécute à l'intérieur de system_server, il est essentiel d'éviter de conserver des verrous pendant une longue période, en particulier le verrou ActivityManagerService.

Conflit de verrouillage du classeur

Historiquement, Binder a eu un seul verrou 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 une transaction de liaison jusqu'à ce que le thread d'origine ait libéré le verrou. C'est mauvais; les conflits de classeur peuvent tout bloquer dans le système, y compris l'envoi de mises à jour de l'interface utilisateur à l'écran (les threads de l'interface utilisateur communiquent avec SurfaceFlinger via le classeur).

Android 6.0 incluait plusieurs correctifs pour améliorer ce comportement en désactivant la préemption tout en maintenant le verrouillage du classeur. Cela n'était sûr que parce que le verrouillage du classeur devait être maintenu pendant quelques microsecondes d'exécution réelle. Cela a considérablement amélioré les performances dans les situations non conflictuelles et a évité les conflits en empêchant la plupart des changements de planificateur pendant que le verrouillage du classeur était maintenu. Cependant, la préemption n'a pas pu être désactivée pendant toute la durée d'exécution du verrouillage du classeur, ce qui signifie que la préemption a été activée pour les fonctions qui pouvaient dormir (telles que copy_from_user), ce qui pourrait provoquer la même préemption que dans le cas d'origine. Lorsque nous avons envoyé les correctifs en amont, ils nous ont immédiatement 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 prévenir les erreurs.)

fd conflit dans un processus

C'est rare. Votre vacarme n'est probablement pas causé par cela.

Cela dit, si vous avez plusieurs threads au sein d'un processus écrivant le même fd, il est possible de voir des conflits sur ce fd, mais la seule fois où nous avons vu cela lors de l'affichage de Pixel, c'est lors d'un test où des threads de faible priorité ont tenté d'occuper tout le processeur. moment où un seul thread de haute priorité était en cours d’exécution dans le même processus. Tous les threads écrivaient sur le marqueur de trace fd et le thread de haute priorité pouvait être bloqué sur le marqueur de trace fd si un thread de faible priorité détenait le verrou fd et était ensuite préempté. Lorsque le traçage était désactivé à partir des threads de faible priorité, il n’y avait aucun problème de performances.

Nous n'avons pu reproduire cela dans aucune autre situation, mais cela mérite d'être souligné comme cause potentielle de problèmes de performances lors du traçage.

Transitions inutiles d'inactivité du processeur

Lorsqu'il s'agit d'IPC, en particulier de pipelines multi-processus, il est courant de constater des variations dans le comportement d'exécution suivant :

  1. Le thread A s'exécute sur le processeur 1.
  2. Le thread A réveille le thread B.
  3. Le thread B commence à s'exécuter sur le processeur 2.
  4. Le thread A s'endort immédiatement, pour être réveillé par le thread B lorsque le thread B a terminé son travail en cours.

Une source courante de surcharge se situe entre les étapes 2 et 3. Si le processeur 2 est inactif, il doit être ramené à un état actif avant que le thread B puisse s'exécuter. En fonction du SOC et de la profondeur de l'inactivité, cela peut prendre des dizaines de microsecondes avant que le thread B ne commence à s'exécuter. Si le temps d'exécution réel de chaque côté de l'IPC est suffisamment proche de la surcharge, les performances globales de ce pipeline peuvent être considérablement réduites par les transitions d'inactivité du processeur. L'endroit le plus courant où Android rencontre ce problème concerne les transactions de classeur, et de nombreux services qui utilisent le classeur finissent par ressembler à la situation décrite ci-dessus.

Tout d’abord, utilisez la fonction wake_up_interruptible_sync() dans vos pilotes de noyau et prenez-la en charge à partir de n’importe quel planificateur personnalisé. Considérez cela comme une exigence et non comme un indice. Binder l'utilise aujourd'hui, et cela aide beaucoup avec les transactions de classeur synchrones en évitant les transitions inutiles d'inactivité du processeur.

Deuxièmement, assurez-vous que les temps de transition de votre cpuidle sont réalistes et que le régulateur de cpuidle les prend correctement en compte. Si votre SOC entre et sort de son état d'inactivité le plus profond, vous n'économiserez pas d'énergie en passant au ralenti le plus profond.

Enregistrement

La journalisation n'est pas gratuite pour les cycles du processeur ou la mémoire, alors ne spammez pas le tampon de journal. La journalisation des cycles de coûts dans votre application (directement) et dans le démon 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 gigue. 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 échoue et lit la page à partir du disque. Cela bloque le thread (généralement pendant plus de 10 ms) et si cela se produit dans le chemin critique du rendu de l'interface utilisateur, cela peut entraîner des erreurs. Il existe trop de causes d'opérations d'E/S pour être discutées 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 des pages. Cela supprime la mémoire destinée à être utilisée par tout autre processus, mais si certains fichiers sont connus a priori pour être utilisés régulièrement, il peut être efficace de verrouiller ces fichiers.

    Sur les appareils Pixel et Nexus 6P fonctionnant sous Android 7.0, nous avons verrouillé quatre fichiers :
    • /system/framework/arm64/boot-framework.oat
    • /système/framework/oat/arm64/services.odex
    • /system/framework/arm64/boot.oat
    • /system/framework/arm64/boot-core-libart.oat
    Ces fichiers sont constamment utilisés par la plupart des applications et par system_server, ils ne doivent donc pas être paginés. En particulier, nous avons constaté que si l'un d'entre eux était paginé, il serait renvoyé et provoquerait des parasites lors du passage d'une application lourde.
  • Chiffrement . Une autre cause possible de problèmes d'E/S. Nous constatons que le chiffrement en ligne 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 la gigue associée aux E/S, en particulier par rapport au chiffrement basé sur le processeur. Étant donné que les extractions vers le cache de pages se situent souvent dans le chemin critique du rendu de l'interface utilisateur, le chiffrement basé sur le processeur introduit une charge CPU supplémentaire dans le chemin critique, ce qui ajoute plus de gigue que la simple récupération d'E/S.

    Les moteurs de chiffrement matériel basés sur DMA ont un problème similaire, puisque le noyau doit passer des cycles à gérer ce travail même si d'autres travaux critiques peuvent être exécutés. Nous recommandons fortement à tout fournisseur SOC créant un nouveau matériel d'inclure la prise en charge du chiffrement en ligne.

Emballage agressif pour petites tâches

Certains planificateurs permettent de regrouper de petites tâches sur des cœurs de processeur uniques pour tenter de réduire la consommation d'énergie en gardant davantage de processeurs inactifs plus longtemps. Bien que cela fonctionne bien en termes de débit et de consommation d’énergie, cela peut être catastrophique en termes de latence. Il existe plusieurs threads de courte durée dans le chemin critique du rendu de l’interface utilisateur qui peuvent être considérés comme petits ; si ces threads sont retardés lors de leur migration lente vers d'autres processeurs, cela provoquera des parasites. Nous vous recommandons d’utiliser l’emballage des petites tâches de manière très prudente.

Détruit le cache des pages

Un appareil ne disposant 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, telle que l'ouverture d'une nouvelle application. Une trace de l'application peut révéler qu'elle est systématiquement bloquée dans les E/S au cours d'une exécution particulière, même si elle n'est souvent pas bloquée dans les E/S. C'est généralement le signe d'une destruction du cache de pages, en particulier sur les appareils dotés de moins de mémoire.

Une façon d'identifier cela consiste à effectuer une trace système à l'aide de la balise pagecache et à transmettre cette trace au script à l'adresse system/extras/pagecache/pagecache.py . pagecache.py traduit les demandes individuelles de mappage de fichiers dans le cache de pages en statistiques globales 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 êtes définitivement confronté à une destruction 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 sur votre appareil. Par conséquent, à mesure qu'une partie de la charge de travail récupère les données dont elle a besoin dans le cache des pages, une autre partie qui sera utilisée dans un avenir proche sera expulsée et devra être récupérée à nouveau, provoquant la répétition du problème jusqu'au chargement. a completé. C’est la cause fondamentale des problèmes de performances lorsqu’il n’y a pas suffisamment de mémoire disponible sur un appareil.

Il n'existe pas de moyen infaillible de corriger le problème du cache des pages, mais il existe plusieurs façons d'essayer d'améliorer cela sur un appareil donné.

  • Utilisez moins de mémoire dans les processus persistants. Moins les processus persistants utilisent de mémoire, plus il y a de mémoire disponible pour les applications et le cache des pages.
  • Vérifiez les exclusions dont vous disposez pour votre appareil afin de vous assurer que vous ne supprimez pas inutilement de la mémoire du système d'exploitation. Nous avons vu des situations dans lesquelles des exclusions utilisées pour le débogage étaient accidentellement laissées dans les configurations du noyau d'expédition, consommant des dizaines de mégaoctets de mémoire. Cela peut faire la différence entre frapper ou non le cache des pages, en particulier sur les appareils avec moins de mémoire.
  • Si vous constatez des problèmes de cache de pages dans system_server sur des fichiers critiques, envisagez d'épingler ces fichiers. Cela augmentera la pression de la mémoire ailleurs, mais cela peut modifier suffisamment le comportement pour éviter les problèmes.
  • Réglez lowmemorykiller pour essayer de garder plus de mémoire libre. Les seuils de lowmemorykiller sont basés à la fois sur la mémoire libre absolue et sur le cache de pages, donc augmenter le seuil auquel les processus à un niveau oom_adj donné sont tués peut entraîner un meilleur comportement au détriment d'une augmentation de la mort des applications en arrière-plan.
  • Essayez d'utiliser ZRAM. Nous utilisons ZRAM sur Pixel, même si Pixel dispose de 4 Go, car cela pourrait aider avec les pages sales rarement utilisées.