Identifica el bloqueo relacionado con el jitter

El jitter es el comportamiento aleatorio del sistema que impide que se ejecute el trabajo perceptible. En esta página, se describe cómo identificar y abordar los problemas de bloqueo relacionados con el jitter.

Retraso del programador de subprocesos de la app

La demora del programador es el síntoma más obvio del jitter: un proceso que debería ejecutarse se puede ejecutar, pero no se ejecuta durante un período significativo. La importancia de la demora varía según el contexto. Por ejemplo:

  • Es probable que un subproceso de ayuda aleatorio en una app se retrase durante muchos milisegundos sin problemas.
  • Es posible que el subproceso de IU de una app pueda tolerar entre 1 y 2 ms de jitter.
  • Los subprocesos de K del controlador que se ejecutan como SCHED_FIFO pueden causar problemas si se pueden ejecutar durante 500 μs antes de ejecutarse.

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

Subprocesos que se ejecutan demasiado tiempo

Los subprocesos de la IU de la app que se pueden ejecutar durante demasiado tiempo pueden causar problemas. Los subprocesos de nivel inferior con tiempos de ejecución largos suelen tener diferentes causas, pero intentar acercar el tiempo de ejecución del subproceso de IU a cero puede requerir la corrección de algunos de los mismos problemas que hacen que los subprocesos de nivel inferior tengan tiempos de ejecución largos. Para mitigar las demoras, haz lo siguiente:

  1. Usa cpusets como se describe en Limitación térmica.
  2. Aumenta el valor de CONFIG_HZ.
    • Históricamente, el valor se estableció en 100 en las plataformas arm y arm64. Sin embargo, este es un accidente de la historia y no es un buen valor para usar en dispositivos interactivos. CONFIG_HZ=100 significa que un jiffies tiene 10 ms de duración, lo que significa que el balanceo de cargas entre las CPU puede tardar 20 ms (dos jiffies) en ocurrir. Esto puede contribuir de manera significativa a la inestabilidad 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 despreciable y, al mismo tiempo, mejorar significativamente los tiempos de ejecución. Si observas aumentos significativos en el consumo de energía o problemas de rendimiento después de cambiar CONFIG_HZ, es probable que uno de tus controladores esté usando un temporizador basado en jiffies sin procesar en lugar de milisegundos y que los convierta en jiffies. Por lo general, esto es fácil de solucionar (consulta el parche que corrigió los problemas del temporizador kgsl en Nexus 5X y 6P cuando se convirtió a CONFIG_HZ=300).
    • Por último, experimentamos con CONFIG_HZ=1000 en Nexus/Pixel y descubrimos que ofrece una reducción notable del rendimiento y la energía debido a la disminución de la sobrecarga de RCU.

Con solo esos dos cambios, un dispositivo debería verse mucho mejor en cuanto al tiempo de ejecución del subproceso de IU durante la carga.

Usa sys.use_fifo_ui

Puedes intentar reducir a cero el tiempo ejecutable del subproceso de la IU configurando la propiedad sys.use_fifo_ui en 1.

Advertencia: No uses esta opción en configuraciones de CPU heterogéneas, a menos que tengas un programador de RT consciente de la capacidad. Y, en este momento, NO HAY NINGÚN PROGRAMADOR DE RT DE ENVÍO ACTUAL QUE TOME EN CUENTA LA CAPACIDAD. Estamos trabajando en una para EAS, pero aún no está disponible. El programador de RT predeterminado se basa únicamente en las prioridades de RT y en si una CPU ya tiene un subproceso de RT de prioridad igual o superior.

Como resultado, el programador de RT predeterminado moverá con gusto tu subproceso de IU de ejecución relativamente larga de un núcleo grande de alta frecuencia a un núcleo pequeño con la frecuencia mínima si se activa un kthread de FIFO de prioridad más alta en el mismo núcleo grande. Esto provocará regresiones de rendimiento significativas. Como esta opción aún no se usó en un dispositivo Android de envío, si quieres usarla, comunícate con el equipo de rendimiento de Android para que te ayude a validarla.

Cuando se habilita sys.use_fifo_ui, ActivityManager realiza un seguimiento del subproceso de IU y de RenderThread (los dos subprocesos más críticos de la IU) de la app superior y establece esos subprocesos como SCHED_FIFO en lugar de SCHED_OTHER. Esto elimina de manera eficaz el jitter de la IU y RenderThreads. Los seguimientos que recopilamos con esta opción habilitada muestran tiempos de ejecución del orden de microsegundos en lugar de milisegundos.

Sin embargo, como el balanceador de cargas de RT no tenía en cuenta la capacidad, se produjo una reducción del 30% en el rendimiento del inicio de la app porque el subproceso de IU responsable de iniciar la app 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 cargas de RT que tenga en cuenta la capacidad, observamos un rendimiento equivalente en las operaciones masivas y una reducción del 10% al 15% en los tiempos de fotogramas del percentil 95 y 99 en muchas de nuestras comparativas de la IU.

Cómo interrumpir el tráfico

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

Durante el desarrollo de Pixel, observamos un bloqueo que se podía atribuir directamente a la CPU 0 sobrecargada con interrupciones. Por ejemplo, si el subproceso mdss_fb0 se programó en la CPU 0, era mucho más probable que se produjera un bloqueo debido a una interrupción que activa la pantalla casi de inmediato antes de la exploración. 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. Al principio, intentamos solucionar este problema configurando la afinidad de la CPU del subproceso mdss_fb0 en las CPUs 1 a 3 para evitar la contención con la interrupción, pero luego nos dimos cuenta de que aún no habíamos habilitado msm_irqbalance. Con msm_irqbalance habilitado, el bloqueo mejoró notablemente, incluso cuando mdss_fb0 y la interrupción de MDSS estaban en la misma CPU debido a la reducción de la contención de otras interrupciones.

Esto se puede identificar en systrace si se observa la sección de programación y la sección de IRQ. La sección sched muestra lo que se programó, 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 de forma normal. Si ves fragmentos significativos de tiempo durante una interrupción, puedes hacer lo siguiente:

  • Hacer que el controlador de interrupciones sea más rápido
  • Evita que se produzca la interrupción en primer lugar.
  • Cambia la frecuencia de la interrupción para que esté fuera de fase con otro trabajo normal con el que pueda interferir (si se trata de una interrupción normal).
  • Establece la afinidad de la CPU de la interrupción directamente y evita que se equilibre.
  • Establece la afinidad de la CPU del subproceso con el que interfiere la interrupción para evitarla.
  • Usa 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 en casos específicos. En general, es muy difícil predecir el estado del sistema para las interrupciones más comunes, pero si tienes un conjunto muy específico de condiciones que activan ciertas interrupciones en las que el sistema está más restringido de lo normal (como la realidad virtual), la afinidad de CPU explícita puede ser una buena solución.

Softirqs largas

Mientras se ejecuta una softirq, inhabilita la preempción. Las softirqs también se pueden activar en muchos lugares dentro del kernel y pueden ejecutarse dentro de un proceso de usuario. Si hay suficiente actividad de softirq, los procesos del usuario dejarán de ejecutar softirqs, y ksoftirqd se activará para ejecutar softirqs y equilibrar la carga. Por lo general, esto está bien. Sin embargo, una sola softirq muy larga puede causar estragos en el sistema.


Las 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 una softirq puede ejecutarse dentro de un proceso de usuario, una softirq defectuosa también puede manifestarse como un tiempo de ejecución adicional dentro de un proceso de usuario sin motivo aparente. Si ves eso, revisa la sección de irq para ver si las softirqs son las culpables.

Los controladores dejan la anulación o las IRQ inhabilitadas durante demasiado tiempo.

Inhabilitar la preempción o las interrupciones durante demasiado tiempo (decenas de milisegundos) genera bloqueos. Por lo general, el bloqueo se manifiesta como un subproceso que se puede ejecutar, 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.

Estos son algunos lineamientos:

  • Si el subproceso ejecutable es SCHED_FIFO y el subproceso en ejecución es SCHED_OTHER, el subproceso en ejecución tiene inhabilitadas las interrupciones o la prioridad.
  • Si el subproceso ejecutable tiene una prioridad mucho más alta (100) que el subproceso en ejecución (120), es probable que el subproceso en ejecución tenga inhabilitada la prioridad o las interrupciones si el subproceso ejecutable no se ejecuta en dos jiffies.
  • Si el subproceso ejecutable y el subproceso en ejecución tienen la misma prioridad, es probable que el subproceso en ejecución tenga inhabilitada la preempción o las interrupciones si el subproceso ejecutable no se ejecuta en un plazo de 20 ms.

Ten en cuenta que ejecutar un controlador de interrupción te impide atender otras interrupciones, lo que también inhabilita la usurpación.


Otra opción para identificar las regiones infractoras es con el generador de perfiles preemptirqsoff (consulta Cómo usar ftrace dinámico). Este generador de registros puede proporcionar una información mucho más detallada sobre la causa raíz de una región no interrumpible (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 probarlo.

Uso incorrecto de las filas de trabajo

Los controladores de interrupciones a menudo deben realizar tareas que se pueden ejecutar fuera de un contexto de interrupción, lo que permite que el trabajo se subcontrate a diferentes subprocesos en el kernel. Un desarrollador de controladores puede notar que el kernel tiene una funcionalidad de tareas asíncronas muy conveniente para todo el sistema llamada colas de trabajo y puede 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 de rendimiento y deben ejecutarse de inmediato. Las filas de trabajo no tienen garantías sobre cuándo se ejecutarán. Cada vez que vimos una lista de tareas en la ruta crítica de rendimiento, fue una fuente de bloqueos esporádicos, independientemente del dispositivo. En Pixel, con un procesador insignia, observamos que una sola lista de tareas en cola podría retrasarse hasta 7 ms si el dispositivo estaba bajo carga, según el comportamiento del programador y otros elementos en ejecución en el sistema.

En lugar de un workqueue, los controladores que necesitan controlar un trabajo similar a una interrupción dentro de un subproceso independiente deben crear su propio kthread SCHED_FIFO. Para obtener ayuda para hacerlo con las funciones de kthread_work, consulta este parche.

Contención de bloqueo del framework

La contención de bloqueo del framework puede ser una fuente de bloqueos o de otros problemas de rendimiento. Por lo general, se debe al 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 ves esto en tu dispositivo, no hay una solución adecuada, ya que solo se puede mejorar mediante mejoras arquitectónicas en el framework. Sin embargo, si modificas el código que se ejecuta dentro de system_server, es fundamental evitar mantener bloqueos durante mucho tiempo, en especial el bloqueo de ActivityManagerService.

Contención de bloqueo de Binder

Históricamente, Binder tuvo un solo bloqueo global. Si el subproceso que ejecuta una transacción de Binder se apropió mientras sostenía el bloqueo, ningún otro subproceso puede realizar una transacción de Binder hasta que el subproceso original libere el bloqueo. Esto es malo, ya que la contención de Binder puede bloquear todo el sistema, lo que incluye el envío de actualizaciones de la IU a la pantalla (los subprocesos de la IU se comunican con SurfaceFlinger a través de Binder).

Android 6.0 incluyó varios parches para mejorar este comportamiento inhabilitando la preempción mientras se mantiene el bloqueo del Binder. Esto era seguro solo porque el bloqueo del vinculador se debía mantener durante unos microsegundos del tiempo de ejecución real. Esto mejoró de forma significativa el rendimiento en situaciones sin contención y evitó la contención, ya que impidió la mayoría de los interruptores de programador mientras se mantenía el bloqueo del vinculador. Sin embargo, no se pudo inhabilitar la preempción para todo el entorno de ejecución de la retención del bloqueo de Binder, lo que significa que la preempción estaba habilitada para las funciones que podían suspenderse (como copy_from_user), lo que podría causar la misma preempción que el caso original. Cuando enviamos los parches upstream, nos dijeron rápidamente que esta era la peor idea de la historia. (Estuvimos de acuerdo con ellos, pero tampoco pudimos discutir la eficacia de los parches para evitar la inestabilidad).

Contención de fd dentro de un proceso

Esto es poco frecuente. Es probable que esto no sea la causa de la lentitud.

Dicho esto, si tienes varios subprocesos dentro de un proceso que escriben el mismo fd, es posible ver una contención en este fd. Sin embargo, la única vez que vimos esto durante el inicio de Pixel fue durante una prueba en la que los subprocesos de baja prioridad intentaron ocupar todo el tiempo de la CPU mientras se ejecutaba un solo subproceso de alta prioridad dentro del mismo proceso. Todos los subprocesos escribían en el fd del marcador de seguimiento, y el subproceso de alta prioridad podía bloquearse en el fd del marcador de seguimiento si un subproceso de baja prioridad retenía el bloqueo de fd y, luego, se le quitaba la prioridad. Cuando se inhabilitó el seguimiento de los subprocesos de prioridad baja, no hubo problemas 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 innecesarias de inactividad de la CPU

Cuando se trata de IPC, en especial de canalizaciones de varios procesos, es común ver variaciones en el siguiente comportamiento del entorno de ejecución:

  1. El subproceso A se ejecuta en la CPU 1.
  2. El subproceso A activa el subproceso B.
  3. El subproceso B comienza a ejecutarse en la CPU 2.
  4. El subproceso A se suspende de inmediato para que el subproceso B lo active cuando termine 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 un estado activo antes de que se pueda ejecutar el subproceso B. Según el SOC y la profundidad del estado inactivo, esto podría ser de decenas de microsegundos antes de que comience a ejecutarse el subproceso B. 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 debido a las transiciones de inactividad de la CPU. El lugar más común en el que Android experimenta este problema es en las transacciones de Binder, y muchos servicios que usan Binder terminan pareciendo la situación descrita anteriormente.

Primero, usa la función wake_up_interruptible_sync() en tus controladores de kernel y admite esto desde cualquier programador personalizado. Trata esto como un requisito, no como una sugerencia. Binder usa esto hoy en día y ayuda mucho con las transacciones síncronas de Binder, ya que evita transiciones de inactividad innecesarias de la CPU.

En segundo lugar, asegúrate de que los tiempos de transición de cpuidle sean realistas y de que el regulador de cpuidle los tenga en cuenta correctamente. Si el SOC entra y sale de tu estado inactivo más profundo, no ahorrarás energía si pasas al estado inactivo más profundo.

Registro

El registro no es gratuito para los ciclos de CPU ni la memoria, por lo que no debes enviar spam al búfer de registro. Registra los ciclos de costos en tu app (directamente) y en el daemon de registro. Quita todos los registros de depuración antes de enviar el dispositivo.

Problemas de E/S

Las operaciones de E/S son fuentes comunes de jitter. Si un subproceso accede a un archivo asignado a la memoria y la página no está en la caché de páginas, se produce un error y se lee la página desde el disco. Esto bloquea el subproceso (por lo general, durante más de 10 ms) y, si ocurre en la ruta crítica de la renderización de la IU, puede provocar bloqueos. Hay demasiadas causas de las operaciones de E/S para analizarlas aquí, pero revisa las siguientes ubicaciones cuando intentes mejorar el comportamiento de E/S:

  • PinnerService. PinnerService, que se agregó en Android 7.0, permite que el framework bloquee algunos archivos en la caché de páginas. Esto quita la memoria para que la use cualquier otro proceso, pero si hay algunos archivos que se sabe de antemano que se usan con regularidad, puede ser eficaz mlock esos archivos.

    En dispositivos Pixel y Nexus 6P que ejecutan 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
    La mayoría de las apps y system_server usan estos archivos constantemente, por lo que no se deben paginar. En particular, descubrimos que, si se pagina cualquiera de ellos, se volverá a paginar y se producirá un error de bloqueo cuando se cambie de una app pesada.
  • Encriptación. Otra posible causa de problemas de E/S. Consideramos que la encriptación intercalada ofrece el mejor rendimiento en comparación con la encriptación basada en la CPU o el uso de un bloque de hardware al que se puede acceder a través de DMA. Lo más importante es que la encriptación intercalada reduce el jitter asociado con la E/S, en especial, en comparación con la encriptación basada en la CPU. Debido a que las recuperaciones de la caché de páginas suelen estar en la ruta crítica de la renderización de la IU, la encriptación basada en la CPU introduce una carga adicional de la CPU en la ruta crítica, lo que agrega más jitter que solo la recuperación de E/S.

    Los motores de encriptación de hardware basados en DMA tienen un problema similar, ya que el kernel tiene que dedicar ciclos a administrar ese trabajo, incluso si hay otro trabajo crítico disponible para ejecutar. Recomendamos a los proveedores de SOC que compilan hardware nuevo que incluyan la compatibilidad con la encriptación intercalada.

Empaquetamiento agresivo de tareas pequeñas

Algunos programadores admiten el empaquetado de tareas pequeñas en un solo núcleo de CPU para intentar reducir el consumo de energía manteniendo más CPUs inactivas durante más tiempo. Si bien esto funciona bien para la capacidad de procesamiento y el consumo de energía, puede ser catastrófico para la latencia. Hay varios subprocesos de corta duración en la ruta crítica de la renderización de la IU que se pueden considerar pequeños. Si estos subprocesos se retrasan a medida que se migran lentamente a otras CPU, causarán interrupciones. Recomendamos usar el empaquetado de tareas pequeñas de forma muy conservadora.

Intercambio de caché de páginas

Un dispositivo sin suficiente memoria libre puede volverse muy lento de repente mientras realiza una operación de larga duración, como abrir una app nueva. Un registro de la app puede revelar que se bloquea de forma coherente en la E/S durante una ejecución en particular, incluso cuando a menudo no se bloquea en la E/S. Por lo general, esto es un signo de fragmentación de la caché de páginas, especialmente en dispositivos con menos memoria.

Una forma de identificar esto es tomar un systrace con la etiqueta pagecache y alimentar ese seguimiento a la secuencia de comandos en system/extras/pagecache/pagecache.py. pagecache.py traduce las solicitudes individuales para asignar archivos a la caché de páginas en estadísticas agregadas por archivo. Si descubres que se leyeron más bytes de un archivo que el tamaño total de ese archivo en el disco, es seguro que estás experimentando la fragmentación de la caché de páginas.

Esto significa que el conjunto de trabajo que requiere tu carga de trabajo (por lo general, una sola app más system_server) es mayor que la cantidad de memoria disponible para la caché de páginas en tu dispositivo. Como resultado, a medida que una parte de la carga de trabajo obtiene los datos que necesita en la caché de páginas, otra parte que se usará en un futuro cercano se desalojará y se deberá volver a recuperar, lo que hará que el problema vuelva a ocurrir hasta que se complete la carga. Esta es la razón fundamental de los problemas de rendimiento cuando no hay suficiente memoria disponible en un dispositivo.

No hay una forma infalible de corregir el intercambio de la caché de páginas, pero hay algunas formas de intentar mejorar esto en un dispositivo determinado.

  • Usar menos memoria en los procesos persistentes Cuanto menos memoria usen los procesos persistentes, más memoria estará disponible para las apps y la caché de páginas.
  • Audita las reservas que tienes para tu dispositivo para asegurarte de que no quites memoria del SO de forma innecesaria. Observamos situaciones en las que los recortes que se usan para la depuración se dejaron accidentalmente en las configuraciones de envío del kernel, lo que consume decenas de megabytes de memoria. Esto puede marcar la diferencia entre si se produce un intercambio de caché de página o no, especialmente en dispositivos con menos memoria.
  • Si ves un intercambio de caché de páginas en system_server en archivos críticos, considera fijar esos archivos. Esto aumentará la presión de la memoria en otro lugar, pero podría modificar el comportamiento lo suficiente como para evitar la fragmentación.
  • Vuelve a ajustar lowmemorykiller para intentar mantener más memoria libre. Los umbrales de lowmemorykiller se basan en la memoria libre absoluta y en la caché de páginas, por lo que aumentar el umbral en el que se cancelan los procesos en un nivel determinado de oom_adj puede generar un mejor comportamiento a costa de aumentar la eliminación de apps en segundo plano.
  • Intenta usar ZRAM. Usamos ZRAM en Pixel, aunque tiene 4 GB, porque podría ayudar con las páginas no sincronizadas que se usan con poca frecuencia.