Identificar el Jank relacionado con el jitter

Jitter es el comportamiento aleatorio del sistema que impide que se ejecute un trabajo perceptible. Esta página describe cómo identificar y abordar problemas de interferencias relacionados con la fluctuación.

Retraso en el programador de subprocesos de la aplicación

El retraso del programador es el síntoma más obvio de inquietud: un proceso que debería ejecutarse se vuelve ejecutable pero no se ejecuta durante un período de tiempo significativo. La importancia del retraso varía según el contexto. Por ejemplo:

  • Un hilo de ayuda aleatorio en una aplicación probablemente pueda retrasarse muchos milisegundos sin problemas.
  • El subproceso de la interfaz de usuario de una aplicación puede tolerar entre 1 y 2 ms de fluctuación.
  • Los kthreads del controlador que se ejecutan como SCHED_FIFO pueden causar problemas si se pueden ejecutar durante 500 us antes de ejecutarse.

Los tiempos ejecutables se pueden identificar en systrace mediante la barra azul que precede a un segmento en ejecución de un subproceso. Un tiempo de ejecución también puede determinarse por el período de tiempo entre el evento sched_wakeup para un subproceso y el evento sched_switch que señala el inicio de la ejecución del subproceso.

Hilos que duran demasiado

Los subprocesos de la interfaz de usuario de la aplicación que se pueden ejecutar durante demasiado tiempo pueden causar problemas. Los subprocesos de nivel inferior con tiempos de ejecución prolongados generalmente tienen causas diferentes, pero intentar llevar el tiempo de ejecución del subproceso de la interfaz de usuario a cero puede requerir solucionar algunos de los mismos problemas que causan que los subprocesos de nivel inferior tengan tiempos de ejecución prolongados. Para mitigar retrasos:

  1. Utilice cpusets como se describe en Regulación térmica .
  2. Aumente el valor de CONFIG_HZ.
    • Históricamente, el valor se ha establecido en 100 en las plataformas arm y arm64. Sin embargo, esto es un accidente de la historia y no es una buena opción para utilizarlo en dispositivos interactivos. CONFIG_HZ=100 significa que un santiamén dura 10 ms, lo que significa que el equilibrio de carga entre CPU puede tardar 20 ms (dos santiamén) en realizarse. Esto puede contribuir significativamente al bloqueo de un sistema cargado.
    • Los dispositivos recientes (Nexus 5X, Nexus 6P, Pixel y Pixel XL) se enviaron con CONFIG_HZ=300. Esto debería tener un costo de energía insignificante y al mismo tiempo mejorar significativamente los tiempos de ejecución. Si ve aumentos significativos en el consumo de energía o problemas de rendimiento después de cambiar CONFIG_HZ, es probable que uno de sus controladores esté usando un temporizador basado en santiamén sin formato en lugar de milisegundos y convirtiéndolo a santiamén. Esto suele ser una solución fácil (consulte el parche que solucionó los problemas del temporizador kgsl en Nexus 5X y 6P al convertir a CONFIG_HZ=300).
    • Finalmente, experimentamos con CONFIG_HZ=1000 en Nexus/Pixel y descubrimos que ofrece una notable reducción de rendimiento y energía debido a la disminución de la sobrecarga de la RCU.

Solo con esos dos cambios, un dispositivo debería verse mucho mejor durante el tiempo de ejecución del hilo de la interfaz de usuario bajo carga.

Usando sys.use_fifo_ui

Puede intentar reducir el tiempo de ejecución del subproceso de la interfaz de usuario a cero estableciendo la propiedad sys.use_fifo_ui en 1.

Advertencia : no utilice esta opción en configuraciones de CPU heterogéneas a menos que tenga un programador RT que tenga en cuenta la capacidad. Y, en este momento, NINGÚN PROGRAMADOR RT DE ENVÍOS ACTUALMENTE TIENE CONSCIENTE LA CAPACIDAD . Estamos trabajando en uno para EAS, pero aún no está disponible. El programador RT predeterminado se basa exclusivamente en las prioridades RT y en si una CPU ya tiene un subproceso RT de igual o mayor prioridad.

Como resultado, el programador RT predeterminado moverá felizmente su subproceso UI de ejecución relativamente larga desde un núcleo grande de alta frecuencia a un núcleo pequeño con una frecuencia mínima si un subproceso FIFO de mayor prioridad se activa en el mismo núcleo grande. Esto introducirá importantes regresiones en el rendimiento . Como esta opción aún no se ha utilizado en un dispositivo Android de envío, si desea utilizarla, póngase en contacto con el equipo de rendimiento de Android para que le ayude a validarla.

Cuando sys.use_fifo_ui está habilitado, ActivityManager rastrea el hilo de la interfaz de usuario y RenderThread (los dos hilos más críticos para la interfaz de usuario) de la aplicación principal y convierte esos hilos en SCHED_FIFO en lugar de SCHED_OTHER. Esto elimina efectivamente la fluctuación de la interfaz de usuario y RenderThreads; los seguimientos que hemos recopilado con esta opción habilitada muestran tiempos de ejecución del orden de microsegundos en lugar de milisegundos.

Sin embargo, debido a que el balanceador de carga RT no tenía en cuenta la capacidad, hubo una reducción del 30% en el rendimiento de inicio de la aplicación porque el hilo de la interfaz de usuario responsable de iniciar la aplicación se trasladaría de un núcleo Kryo dorado de 2,1 Ghz a un núcleo Kryo plateado de 1,5 GHz. . Con un balanceador de carga RT consciente de la capacidad, vemos un rendimiento equivalente en operaciones masivas y una reducción del 10 al 15 % en los tiempos de fotogramas de los percentiles 95 y 99 en muchos de nuestros puntos de referencia de UI.

Interrumpir el tráfico

Debido a que las plataformas ARM entregan interrupciones a la CPU 0 solo de forma predeterminada, recomendamos el uso de un equilibrador IRQ (irqbalance o msm_irqbalance en plataformas Qualcomm).

Durante el desarrollo de Pixel, vimos bloqueos que podrían atribuirse directamente a abrumar la CPU 0 con interrupciones. Por ejemplo, si el subproceso mdss_fb0 estaba programado en la CPU 0, había una probabilidad mucho mayor de bloquearse debido a una interrupción activada por la pantalla casi inmediatamente antes de la exploración. mdss_fb0 estaría en medio de su propio trabajo con una fecha límite muy ajustada y luego perdería algo de tiempo frente al controlador de interrupciones MDSS. Inicialmente, intentamos solucionar este problema configurando la afinidad de CPU del subproceso mdss_fb0 en las CPU 1 a 3 para evitar conflictos con la interrupción, pero luego nos dimos cuenta de que aún no habíamos habilitado msm_irqbalance. Con msm_irqbalance habilitado, jank mejoró notablemente incluso cuando mdss_fb0 y la interrupción MDSS estaban en la misma CPU debido a la reducción de la contención de otras interrupciones.

Esto se puede identificar en systrace mirando la sección sched y la sección irq. La sección programada muestra lo que se ha programado, pero una región superpuesta en la sección irq significa que se está ejecutando una interrupción durante ese tiempo en lugar del proceso programado normalmente. Si ve que se consume mucho tiempo durante una interrupción, sus opciones incluyen:

  • Haga que el controlador de interrupciones sea más rápido.
  • Evite que se produzca la interrupción en primer lugar.
  • Cambie la frecuencia de la interrupción para que esté desfasada con otro trabajo regular que pueda estar interfiriendo (si es una interrupción regular).
  • Establezca la afinidad de CPU de la interrupción directamente y evite que se equilibre.
  • Establezca la afinidad de CPU del subproceso con el que interfiere la interrupción para evitar la interrupción.
  • Confíe en el equilibrador de interrupciones para mover la interrupción a una CPU menos cargada.

Por lo general, no se recomienda configurar la afinidad de la CPU, pero puede resultar útil en casos específicos. En general, es demasiado difícil predecir el estado del sistema para las interrupciones más comunes, pero si tiene un conjunto de condiciones muy específico que desencadena ciertas interrupciones en las que el sistema está más restringido de lo normal (como la realidad virtual), la afinidad explícita de la CPU puede ser una buena solución.

Softirqs largos

Mientras se ejecuta un softirq, deshabilita la preferencia. softirqs también se puede activar en muchos lugares dentro del kernel y puede ejecutarse dentro de un proceso de usuario. Si hay suficiente actividad de softirq, los procesos de usuario dejarán de ejecutar softirqs y ksoftirqd se activará para ejecutar softirqs y equilibrar la carga. Normalmente esto está bien. Sin embargo, un solo softirq muy largo puede causar estragos en el sistema.


Los softirqs son visibles dentro de la sección irq de un seguimiento, por lo que son fáciles de detectar si el problema se puede reproducir durante el seguimiento. Debido a que un softirq puede ejecutarse dentro de un proceso de usuario, un softirq incorrecto también puede manifestarse como tiempo de ejecución adicional dentro de un proceso de usuario sin ninguna razón obvia. Si ve eso, consulte la sección irq para ver si los softirqs son los culpables.

Conductores que dejan la preferencia o las IRQ desactivadas por demasiado tiempo

Deshabilitar la preferencia o las interrupciones durante demasiado tiempo (decenas de milisegundos) produce una interrupción. Por lo general, el bloqueo se manifiesta como un subproceso que se vuelve ejecutable pero no se ejecuta en una CPU en particular, incluso si el subproceso ejecutable tiene una prioridad significativamente mayor (o SCHED_FIFO) que el otro subproceso.

Algunas pautas:

  • Si el subproceso ejecutable es SCHED_FIFO y el subproceso en ejecución es SCHED_OTHER, el subproceso en ejecución tiene la preferencia o las interrupciones deshabilitadas.
  • Si el subproceso ejecutable tiene una prioridad significativamente mayor (100) que el subproceso en ejecución (120), es probable que el subproceso en ejecución tenga la preferencia o las interrupciones deshabilitadas si el subproceso ejecutable no se ejecuta en dos santiamén.
  • Si el subproceso ejecutable y el subproceso en ejecución tienen la misma prioridad, es probable que el subproceso en ejecución tenga la preferencia o las interrupciones deshabilitadas si el subproceso ejecutable no se ejecuta dentro de los 20 ms.

Tenga en cuenta que ejecutar un controlador de interrupciones le impide atender otras interrupciones, lo que también desactiva la preferencia.


Otra opción para identificar regiones infractoras es con el rastreador preemptirqsoff (consulte Uso de ftrace dinámico ). Este rastreador puede brindar una visión mucho mayor de la causa raíz de una región ininterrumpible (como los nombres de funciones), pero requiere un trabajo más invasivo para habilitarlo. Si bien puede tener un mayor impacto en el rendimiento, definitivamente vale la pena intentarlo.

Uso incorrecto de colas de trabajo.

Los manejadores de interrupciones a menudo necesitan realizar un trabajo que pueda ejecutarse fuera de un contexto de interrupción, lo que permite distribuir el trabajo entre diferentes subprocesos del kernel. Un desarrollador de controladores puede notar que el kernel tiene una funcionalidad de tarea asincrónica muy conveniente en todo el sistema llamada colas de trabajo y podría usarla para trabajos relacionados con interrupciones.

Sin embargo, las colas de trabajo casi siempre son la respuesta incorrecta a este problema porque siempre son SCHED_OTHER. Muchas interrupciones de hardware se encuentran en la ruta crítica de rendimiento y deben ejecutarse de inmediato. Las colas de trabajo no tienen garantías sobre cuándo se ejecutarán. Cada vez que hemos visto una cola de trabajo en la ruta crítica de rendimiento, ha sido una fuente de bloqueos esporádicos, independientemente del dispositivo. En Pixel, con un procesador insignia, vimos que una única cola de trabajo podía retrasarse hasta 7 ms si el dispositivo estaba bajo carga, dependiendo del comportamiento del programador y otras cosas que se ejecutaban en el sistema.

En lugar de una cola de trabajo, los controladores que necesitan manejar trabajos similares a interrupciones dentro de un subproceso separado deben crear su propio subproceso SCHED_FIFO. Para obtener ayuda para hacer esto con las funciones kthread_work, consulte este parche .

Contención de bloqueo del marco

La contención de bloqueo del marco puede ser una fuente de bloqueos u otros problemas de rendimiento. Generalmente es causado por el bloqueo ActivityManagerService, pero también se puede ver en otros bloqueos. Por ejemplo, el bloqueo de PowerManagerService puede afectar el rendimiento de la pantalla. Si ve esto en su dispositivo, no hay una buena solución porque solo se puede mejorar mediante mejoras arquitectónicas en el marco. Sin embargo, si está modificando el código que se ejecuta dentro de system_server, es fundamental evitar mantener bloqueos durante mucho tiempo, especialmente el bloqueo ActivityManagerService.

Contención de bloqueo de carpeta

Históricamente, Binder ha tenido un único bloqueo global. Si el subproceso que ejecuta una transacción de vinculación fue reemplazado mientras mantenía el bloqueo, ningún otro subproceso puede realizar una transacción de vinculación hasta que el subproceso original haya liberado el bloqueo. Esto es malo; La contención de la carpeta puede bloquear todo en el sistema, incluido el envío de actualizaciones de la interfaz de usuario a la pantalla (los subprocesos de la interfaz de usuario se comunican con SurfaceFlinger a través de la carpeta).

Android 6.0 incluyó varios parches para mejorar este comportamiento al deshabilitar la preferencia mientras se mantiene el bloqueo de la carpeta. Esto era seguro sólo porque el bloqueo de la carpeta debía mantenerse durante unos microsegundos de tiempo de ejecución real. Esto mejoró drásticamente el rendimiento en situaciones no contendidas y evitó la contienda al evitar la mayoría de los cambios del programador mientras se mantenía el bloqueo de la carpeta. Sin embargo, la preferencia no se pudo deshabilitar durante todo el tiempo de ejecución mientras se mantenía el bloqueo de la carpeta, lo que significa que la preferencia se habilitó para funciones que podían suspenderse (como copy_from_user), lo que podría causar la misma preferencia que en el caso original. Cuando enviamos los parches al principio, rápidamente nos dijeron que ésta era la peor idea de la historia. (Estuvimos de acuerdo con ellos, pero tampoco pudimos discutir la eficacia de los parches para prevenir los bloqueos).

fd contención dentro de un proceso

Esto es raro. Probablemente tu problema no sea causado por esto.

Dicho esto, si tiene varios subprocesos dentro de un proceso que escribe el mismo fd, es posible ver contención en este fd; sin embargo, la única vez que vimos esto durante la activación de Pixel fue durante una prueba en la que los subprocesos de baja prioridad intentaron ocupar toda la CPU. tiempo mientras se ejecutaba un único subproceso de alta prioridad dentro del mismo proceso. Todos los subprocesos escribían en el marcador de seguimiento fd y el subproceso de alta prioridad podría bloquearse en el marcador de seguimiento fd si un subproceso de baja prioridad mantenía el bloqueo fd y luego era reemplazado. Cuando se deshabilitó el seguimiento de los subprocesos de baja prioridad, no hubo ningún problema de rendimiento.

No pudimos reproducir esto en ninguna otra situación, pero vale la pena señalarlo como una posible causa de problemas de rendimiento durante el seguimiento.

Transiciones inactivas de CPU innecesarias

Cuando se trata de IPC, especialmente canalizaciones multiproceso, es común ver variaciones en el siguiente comportamiento en tiempo de ejecución:

  1. El subproceso A se ejecuta en la CPU 1.
  2. El hilo A despierta el hilo B.
  3. El subproceso B comienza a ejecutarse en la CPU 2.
  4. El hilo A entra inmediatamente en modo de suspensión y el hilo B lo despierta cuando el hilo B ha terminado su trabajo actual.

Una fuente común de sobrecarga se encuentra entre los pasos 2 y 3. Si la CPU 2 está inactiva, se debe devolver a un estado activo antes de que el subproceso B pueda ejecutarse. Dependiendo del SOC y de la profundidad de la inactividad, esto podría pasar decenas de microsegundos antes de que el subproceso B comience a ejecutarse. Si el tiempo de ejecución real de cada lado del IPC está lo suficientemente cerca de la sobrecarga, el rendimiento general de esa canalización puede reducirse significativamente mediante transiciones inactivas de la CPU. El lugar más común para que Android alcance esto es en torno a las transacciones de Binder, y muchos servicios que usan Binder terminan pareciéndose a la situación descrita anteriormente.

Primero, use la función wake_up_interruptible_sync() en los controladores de su kernel y admita esto desde cualquier programador personalizado. Trate esto como un requisito, no como una pista. Binder usa esto hoy en día y ayuda mucho con las transacciones sincrónicas de Binder evitando transiciones innecesarias de CPU inactiva.

En segundo lugar, asegúrese de que los tiempos de transición de su cpuidle sean realistas y que el gobernador de cpuidle los tenga en cuenta correctamente. Si su SOC entra y sale de su estado de inactividad más profundo, no ahorrará energía yendo al estado de inactividad más profundo.

Inicio sesión

El registro no es gratuito para los ciclos de CPU o la memoria, así que no envíe spam al búfer de registro. Los costos de registro se realizan en su aplicación (directamente) y en el demonio de registro. Elimine cualquier registro de depuración antes de enviar su dispositivo.

Problemas de E/S

Las operaciones de E/S son fuentes comunes de fluctuación. Si un hilo accede a un archivo asignado en memoria y la página no está en el caché de la página, falla y lee la página del disco. Esto bloquea el subproceso (normalmente durante más de 10 ms) y, si ocurre en la ruta crítica de representación de la interfaz de usuario, puede provocar un bloqueo. Hay demasiadas causas de operaciones de E/S para analizarlas aquí, pero verifique las siguientes ubicaciones cuando intente mejorar el comportamiento de E/S:

  • Servicio Pinner . Agregado en Android 7.0, PinnerService permite que el marco bloquee algunos archivos en el caché de la página. Esto elimina la memoria para que la utilice cualquier otro proceso, pero si hay algunos archivos que se sabe a priori que se utilizan con regularidad, puede ser eficaz bloquearlos.

    En dispositivos Pixel y Nexus 6P con Android 7.0, bloqueamos cuatro archivos:
    • /system/framework/arm64/boot-framework.oat
    • /system/framework/oat/arm64/services.odex
    • /system/framework/arm64/boot.oat
    • /system/framework/arm64/boot-core-libart.oat
    Estos archivos se utilizan constantemente en la mayoría de las aplicaciones y en system_server, por lo que no deben paginarse. En particular, hemos descubierto que si alguno de ellos se pagina, se volverá a paginar y provocará un bloqueo al cambiar de una aplicación pesada.
  • Cifrado . Otra posible causa de problemas de E/S. Descubrimos que el cifrado en línea ofrece el mejor rendimiento en comparación con el cifrado basado en CPU o el uso de un bloque de hardware accesible a través de DMA. Lo más importante es que el cifrado en línea reduce la fluctuación asociada con la E/S, especialmente en comparación con el cifrado basado en CPU. Debido a que las recuperaciones del caché de la página a menudo se encuentran en la ruta crítica de la representación de la interfaz de usuario, el cifrado basado en CPU introduce una carga adicional de la CPU en la ruta crítica, lo que agrega más fluctuación que solo la recuperación de E/S.

    Los motores de cifrado de hardware basados ​​en DMA tienen un problema similar, ya que el kernel tiene que pasar ciclos administrando ese trabajo incluso si hay otro trabajo crítico disponible para ejecutar. Recomendamos encarecidamente a cualquier proveedor de SOC que cree hardware nuevo para incluir soporte para cifrado en línea.

Embalaje agresivo para tareas pequeñas

Algunos programadores ofrecen soporte para agrupar pequeñas tareas en núcleos de CPU únicos para intentar reducir el consumo de energía manteniendo más CPU inactivas durante más tiempo. Si bien esto funciona bien para el rendimiento y el consumo de energía, puede ser catastrófico para la latencia. Hay varios subprocesos de ejecución corta en la ruta crítica de la representación de la interfaz de usuario que pueden considerarse pequeños; Si estos subprocesos se retrasan a medida que se migran lentamente a otras CPU, se producirá un bloqueo. Recomendamos utilizar el embalaje para tareas pequeñas de forma muy conservadora.

Golpe de caché de página

Un dispositivo sin suficiente memoria libre puede volverse extremadamente lento de repente mientras realiza una operación de larga duración, como abrir una nueva aplicación. Un rastro de la aplicación puede revelar que está constantemente bloqueada en E/S durante una ejecución particular, incluso cuando a menudo no está bloqueada en E/S. Esto suele ser un signo de destrucción de la caché de la página, especialmente en dispositivos con menos memoria.

Una forma de identificar esto es tomar un seguimiento del sistema usando la etiqueta pagecache y alimentar ese seguimiento al script en system/extras/pagecache/pagecache.py . pagecache.py traduce solicitudes individuales para asignar archivos al caché de la página en estadísticas agregadas por archivo. Si descubre que se han leído más bytes de un archivo que el tamaño total de ese archivo en el disco, definitivamente está sufriendo una destrucción de caché de página.

Lo que esto significa es que el conjunto de trabajo requerido por su carga de trabajo (generalmente una sola aplicación más system_server) es mayor que la cantidad de memoria disponible para el caché de páginas en su dispositivo. Como resultado, a medida que una parte de la carga de trabajo obtiene los datos que necesita en la caché de la página, otra parte que se utilizará en un futuro próximo será desalojada y tendrá que recuperarse nuevamente, lo que provocará que el problema vuelva a ocurrir hasta que finalice la carga. ha completado. Esta es la causa fundamental de los problemas de rendimiento cuando no hay suficiente memoria disponible en un dispositivo.

No existe una forma infalible de solucionar la destrucción de la memoria caché de la página, pero hay algunas formas de intentar mejorar esto en un dispositivo determinado.

  • Utilice menos memoria en procesos persistentes. Cuanta menos memoria utilicen los procesos persistentes, más memoria estará disponible para las aplicaciones y el caché de la página.
  • Audite las exclusiones que tiene para su dispositivo para asegurarse de no eliminar memoria innecesariamente del sistema operativo. Hemos visto situaciones en las que las exclusiones utilizadas para la depuración se dejaron accidentalmente en las configuraciones del kernel de envío, consumiendo decenas de megabytes de memoria. Esto puede marcar la diferencia entre golpear o no la caché de la página, especialmente en dispositivos con menos memoria.
  • Si observa que la caché de la página se altera en system_server en archivos críticos, considere fijar esos archivos. Esto aumentará la presión de la memoria en otros lugares, pero puede modificar el comportamiento lo suficiente como para evitar la paliza.
  • Vuelva a sintonizar lowmemorykiller para intentar mantener más memoria libre. Los umbrales de lowmemorykiller se basan tanto en la memoria libre absoluta como en el caché de la página, por lo que aumentar el umbral en el que se eliminan los procesos en un nivel oom_adj determinado puede dar como resultado un mejor comportamiento a expensas de una mayor muerte de la aplicación en segundo plano.
  • Intente usar ZRAM. Usamos ZRAM en Pixel, aunque Pixel tiene 4 GB, porque podría ayudar con páginas sucias que rara vez se usan.