Identificación de Jank relacionado con Jitter

Jitter es el comportamiento aleatorio del sistema que evita que se ejecute un trabajo perceptible. En esta página, se describe cómo identificar y abordar los problemas de bloqueo relacionados con la inestabilidad.

Retraso del programador de subprocesos de la aplicación

El retraso del programador es el síntoma más obvio de fluctuación: 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 subproceso de ayuda aleatorio en una aplicación probablemente se puede retrasar muchos milisegundos sin ningún problema.
  • El subproceso de la interfaz de usuario de una aplicación puede tolerar 1-2 ms de fluctuación.
  • Los kthreads del controlador que se ejecutan como SCHED_FIFO pueden causar problemas si se pueden ejecutar durante 500us antes de ejecutarse.

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

Temas 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 diferentes causas, pero intentar empujar el tiempo de ejecución del subproceso de la interfaz de usuario a cero puede requerir solucionar algunos de los mismos problemas que hacen que los subprocesos de nivel inferior tengan tiempos de ejecución prolongados. Para mitigar los retrasos:

  1. Use cpusets como se describe en Limitació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 un buen valor para usar en dispositivos interactivos. CONFIG_HZ=100 significa que un santiamén tiene una duración de 10 ms, lo que significa que el equilibrio de carga entre las CPU puede tardar 20 ms (dos santiamén) en realizarse. Esto puede contribuir significativamente al bloqueo en 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 mientras mejora 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 jiffies sin procesar en lugar de milisegundos y convirtiendo a jiffies. 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 un rendimiento notable y una reducción de energía debido a la disminución de la sobrecarga de RCU.

Solo con esos dos cambios, un dispositivo debería verse mucho mejor para el tiempo de ejecución del subproceso 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 configurando 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 de RT que tenga en cuenta la capacidad. Y, en este momento, NINGÚN PROGRAMADOR RT DE ENVÍO ACTUALMENTE TIENE CONOCIMIENTO DE LA CAPACIDAD . Estamos trabajando en uno para EAS, pero aún no está disponible. El programador de RT predeterminado se basa únicamente en las prioridades de RT y si una CPU ya tiene un subproceso de RT de igual o mayor prioridad.

Como resultado, el programador de RT predeterminado felizmente moverá su subproceso de interfaz de usuario de ejecución relativamente larga de un gran núcleo de alta frecuencia a un pequeño núcleo con una frecuencia mínima si un kthread FIFO de mayor prioridad se activa en el mismo gran núcleo. Esto introducirá importantes regresiones de 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 lo ayude a validarla.

Cuando sys.use_fifo_ui está habilitado, ActivityManager rastrea el subproceso de UI y RenderThread (los dos subprocesos más críticos de UI) de la aplicación superior y convierte esos subprocesos en SCHED_FIFO en lugar de SCHED_OTHER. Esto elimina efectivamente la fluctuación de la interfaz de usuario y RenderThreads; los rastros 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 subproceso de la interfaz de usuario responsable de iniciar la aplicación se moverí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 de RT consciente de la capacidad, observamos un rendimiento equivalente en las 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 la interfaz de usuario.

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 balanceador de IRQ (irqbalance o msm_irqbalance en plataformas Qualcomm).

Durante el desarrollo de Pixel, vimos bloqueos que podrían atribuirse directamente a CPU 0 abrumadora 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 que se activa en la pantalla casi inmediatamente antes del escaneo. mdss_fb0 estaría en medio de su propio trabajo con un plazo muy ajustado, y luego perdería algo de tiempo para el controlador de interrupciones de MDSS. Inicialmente, intentamos solucionar esto configurando la afinidad de CPU del subproceso mdss_fb0 en las CPU 1-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 tanto mdss_fb0 como 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 así como la sección irq. La sección sched 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 toman grandes cantidades de tiempo durante una interrupción, sus opciones incluyen:

  • Haga que el controlador de interrupciones sea más rápido.
  • Evite que ocurra la interrupción en primer lugar.
  • Cambie la frecuencia de la interrupción para que esté desfasada con respecto a otro trabajo regular con el 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 balanceador de interrupciones para mover la interrupción a una CPU menos cargada.

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

Largos tiros suaves

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 reactivará para ejecutar softirqs y equilibrar la carga. Por lo general, 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 motivo aparente. Si ve eso, consulte la sección irq para ver si los softirqs tienen la culpa.

Conductores que dejan la prioridad o las IRQ desactivadas durante demasiado tiempo

Deshabilitar la preferencia o las interrupciones durante demasiado tiempo (decenas de milisegundos) da como resultado un bloqueo. Por lo general, el bloqueo se manifiesta como un subproceso que se vuelve ejecutable pero que no se ejecuta en una CPU en particular, incluso si el subproceso ejecutable tiene una prioridad significativamente más alta (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 prioridad o las interrupciones deshabilitadas.
  • Si el subproceso ejecutable tiene una prioridad significativamente más alta (100) que el subproceso en ejecución (120), es probable que el subproceso en ejecución tenga la prioridad 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 prioridad o las interrupciones deshabilitadas si el subproceso ejecutable no se ejecuta en 20 ms.

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


Otra opción para identificar regiones infractoras es con el rastreador preemptirqsoff (consulte Usar ftrace dinámico ). Este rastreador puede brindar una comprensión mucho mayor de la causa raíz de una región ininterrumpida (como los nombres de las 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 las colas de trabajo

Los controladores de interrupciones a menudo necesitan hacer un trabajo que pueda ejecutarse fuera de un contexto de interrupción, lo que permite que el trabajo se distribuya a diferentes subprocesos en el kernel. Un desarrollador de controladores puede notar que el kernel tiene una funcionalidad de tareas asíncronas en todo el sistema muy conveniente llamada colas de trabajo y podría usarla para el trabajo relacionado con interrupciones.

Sin embargo, las colas de trabajo casi siempre son la respuesta incorrecta para este problema porque siempre son SCHED_OTHER. Muchas interrupciones de hardware se encuentran en la ruta crítica del 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 bloqueo esporádico, independientemente del dispositivo. En Pixel, con un procesador insignia, vimos que una sola cola de trabajo podría retrasarse hasta 7 ms si el dispositivo estaba bajo carga, según el comportamiento del programador y otras cosas que se ejecutan en el sistema.

En lugar de una cola de trabajo, los controladores que necesitan manejar un trabajo similar a una interrupción 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 bloqueo u otros problemas de rendimiento. Por lo general, es causado por el bloqueo de ActivityManagerService, pero también se puede ver en otros bloqueos. Por ejemplo, el bloqueo de PowerManagerService puede afectar el rendimiento de la pantalla. Si está viendo esto en su dispositivo, no hay una buena solución porque solo se puede mejorar a través de 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 de ActivityManagerService.

Contención de bloqueo de carpeta

Históricamente, el cuaderno ha tenido un único bloqueo global. Si el subproceso que ejecuta una transacción de enlace se adelantó mientras mantenía el bloqueo, ningún otro subproceso puede realizar una transacción de enlace 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 solo porque el bloqueo de la carpeta debe mantenerse durante unos microsegundos de tiempo de ejecución real. Esto mejoró drásticamente el rendimiento en situaciones no disputadas y evitó la contención al evitar que la mayoría de los cambios del programador se realizaran mientras se mantenía el bloqueo del archivador. Sin embargo, la preferencia no se pudo deshabilitar durante todo el tiempo de ejecución de mantener el bloqueo de carpeta, lo que significa que la preferencia se habilitó para las funciones que podían dormir (como copy_from_user), lo que podría causar la misma preferencia que en el caso original. Cuando enviamos los parches upstream, rápidamente nos dijeron que esta era la peor idea de la historia. (Estuvimos de acuerdo con ellos, pero tampoco pudimos discutir la eficacia de los parches para prevenir el bloqueo).

fd contención dentro de un proceso

Esto es raro. Su bloqueo probablemente 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 solo subproceso de alta prioridad dentro del mismo proceso. Todos los subprocesos estaban escribiendo 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 retenía el bloqueo fd y luego se adelantaba. 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 de 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 subproceso A se duerme inmediatamente, para ser despertado por el subproceso B cuando el subproceso B haya 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 volver a poner en estado activo antes de que se pueda ejecutar el subproceso B. Según el SOC y la profundidad de la inactividad, esto podría ser decenas de microsegundos antes de que el subproceso B comience a ejecutarse. Si el tiempo de ejecución real de cada lado de la IPC está lo suficientemente cerca de la sobrecarga, el rendimiento general de esa canalización puede verse significativamente reducido por las transiciones de inactividad de la CPU. El lugar más común para que Android haga esto es alrededor de 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. Trata esto como un requisito, no como una pista. Binder usa esto hoy en día, y ayuda mucho con las transacciones de enlace síncronas que evitan transiciones innecesarias de inactividad de la CPU.

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 está entrando y saliendo de su estado de inactividad más profundo, no ahorrará energía yendo a la inactividad más profunda.

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. Registro de ciclos de costos 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 inestabilidad. Si un subproceso accede a un archivo asignado a la memoria y la página no está en la memoria caché de la página, falla y lee la página del disco. Esto bloquea el subproceso (generalmente durante más de 10 ms) y, si sucede en la ruta crítica de la representación de la interfaz de usuario, puede provocar un bloqueo. Hay demasiadas causas de las operaciones de E/S para discutir 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 use cualquier otro proceso, pero si hay algunos archivos que se sabe a priori que se usan regularmente, puede ser efectivo bloquear esos archivos.

    En los 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.avena
    • /system/framework/arm64/boot-core-libart.oat
    Estos archivos están constantemente en uso por la mayoría de las aplicaciones y system_server, por lo que no deben ser paginados. En particular, hemos descubierto que si alguno de ellos está paginado, se volverá a paginar y provocará bloqueos 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 que es más importante, 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 búsquedas en el 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 de CPU adicional en la ruta crítica, lo que agrega más inestabilidad que solo la búsqueda 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 enfáticamente a cualquier proveedor de SOC que construya hardware nuevo para incluir soporte para el cifrado en línea.

Embalaje agresivo para tareas pequeñas

Algunos programadores ofrecen soporte para empaquetar tareas pequeñas en núcleos de CPU individuales para tratar de reducir el consumo de energía al mantener 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án bloqueos. Recomendamos usar empaques para tareas pequeñas de manera muy conservadora.

Golpe de caché de página

Un dispositivo sin suficiente memoria libre puede volverse extremadamente lento repentinamente mientras realiza una operación de larga duración, como abrir una nueva aplicación. Un seguimiento de la aplicación puede revelar que está constantemente bloqueada en E/S durante una ejecución en particular, incluso cuando a menudo no está bloqueada en E/S. Por lo general, esto es una señal de que la memoria caché de la página está sobrecargada, especialmente en dispositivos con menos memoria.

Una forma de identificar esto es tomar un systrace usando la etiqueta pagecache y enviar ese seguimiento al script en system/extras/pagecache/pagecache.py . pagecache.py traduce solicitudes individuales para asignar archivos en el 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á golpeando la memoria caché de la página.

Lo que esto significa es que el conjunto de trabajo requerido por su carga de trabajo (normalmente una sola aplicación más system_server) es mayor que la cantidad de memoria disponible para la 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 memoria caché de la página, otra parte que se usará en un futuro cercano será desalojada y deberá recuperarse nuevamente, lo que hará que el problema vuelva a ocurrir hasta que 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 corregir la hiperpaginación de la memoria caché de la página, pero hay algunas maneras de intentar mejorar esto en un dispositivo determinado.

  • Usa menos memoria en procesos persistentes. Cuanta menos memoria utilicen los procesos persistentes, más memoria estará disponible para las aplicaciones y la memoria caché de la página.
  • Audite las excepciones que tiene para su dispositivo para asegurarse de que no está eliminando memoria innecesariamente del sistema operativo. Hemos visto situaciones en las que las excepciones 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 la memoria caché de la página o no, especialmente en dispositivos con menos memoria.
  • Si está viendo una hiperpaginación de caché de página en system_server en archivos críticos, considere anclar 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 la memoria caché de la página, por lo que aumentar el umbral en el que se eliminan los procesos en un nivel de oom_adj determinado puede resultar en un mejor comportamiento a expensas de una mayor muerte de la aplicación en segundo plano.
  • Intenta usar ZRAM. Usamos ZRAM en Pixel, aunque Pixel tiene 4 GB, porque podría ayudar con las páginas sucias que rara vez se usan.