No toques mi memoria
Written by:
Carlos Falgueras25 de Abril de 2023
Hace unos meses llegó a las puertas de Fluendo un interesante proyecto de consultoría. Este cliente en particular usaba una aplicación basada en GStreamer sobre una NVIDIA Jetson. La aplicación era responsable de codificar el flujo de vídeo de una cámara, retransmitirlo en vivo a través de internet y guardar fragmentos de vídeo y capturas de imágenes a demanda. El cliente reportó los siguientes dos problemas con esta aplicación:
- A veces la captura de imágenes se demoraba hasta 4 segundos
- A veces el vídeo grabado contenía artefactos
En este artículo veremos cómo les ayudamos a resolverlos.
Captura lenta de imágenes
Analizamos los logs de GStreamer proporcionados por el cliente y, aunque pudimos verificar que las capturas a veces tardaban demasiado, no pudimos determinar la causa. A pesar de esto, continuamos la investigación con dos hipótesis:
-
Es culpa de “nvjpegenc”. Éste es un elemento de NVIDIA que transforma el flujo de vídeo de entrada en JPEG usando la GPU. Debido a que el flujo de entrada está alojado en la memoria de la GPU (“memory:NVMM”), el uso de este elemento ahorra copia a la CPU.
-
Es culpa del almacenamiento. Si el disco duro tuviese un mal rendimiento, podría explicar el retraso en las capturas. Esto es así porque las imágenes se capturan de forma síncrona (se espera a que sean guardadas a disco para continuar).
Pedimos al cliente que hiciera algunos test para ratificar o descartar estas hipótesis. Usar “jpegenc” (hace la conversión en CPU) en lugar de “nvjpegenc” no resolvió el problema, por lo que no parecía culpa de “nvjpegenc”.
Sin embargo, el cliente nos comentó que estaban usando una tarjeta SD como sistema de almacenamiento, lo que nos enfocó en la segunda hipótesis. Las memorias NAND Flash necesitan de mucha lógica para mantener la integridad de los datos.
Las tarjetas SD tienen un microcontrolador que se encarga de ejecutar esta lógica. Esto hace que el tiempo de escritura y lectura no sea siempre el mismo. A veces este microcontrolador se toma un tiempo para reorganizar los bloques de memoria antes de realizar la operación.
Finalmente, unas pruebas de rendimiento realizadas sobre la SD confirmaron que éste era el problema.
¡Genial, ya tenemos identificado el problema¡ ahora: ¿cómo lo resolvemos? Para el cliente no era una opción cambiar el sistema de almacenamiento a uno con mejor rendimiento, como un SSD.
Afortunadamente sí era asumible realizar las capturas de forma asíncrona, siempre y cuando se notificase de alguna forma cuando ésta se escribiera finalmente en la SD. Modificamos la aplicación para no esperar a que la imagen fuese guardada, y para que notificase cuando ésta era finalmente escrita en la SD. También añadimos una cola para que absorbiera estos picos en el tiempo de escritura, manteniendo las capturas en RAM hasta que todas se habían almacenado.
¡Un problema menos!
Artefactos en el vídeo
El cliente nos envió algunos videos dónde podían apreciarse algunos artefactos. Casi todos ellos parecían ser simplemente fotogramas perdidos. Pero había otros que nos llamaron mucho la atención, porque parecían ser fotogramas desordenados.
Los fotogramas perdidos se podían explicar también por la falta de rendimiento de la SD. Añadimos una cola y el problema prácticamente desapareció.
Por otro lado, que los fotogramas se desordenasen era muy inquietante. Nuestra primera hipótesis fue que era algo relacionado con los “B-frames”.
¿Qué son los “B-frames”?
Una técnica comúnmente utilizada en la compresión de video es no comprimir únicamente cada fotograma por separado, sino también usar las diferencias entre ellos. Esta información extra permite conseguir mejores ratios de compresión. Se podría, por ejemplo, enviar un primer fotograma completo y, a partir de ahí, sólo enviar las diferencias con este fotograma original. Esto normalmente funciona muy bien porque los vídeos suelen tener muchos elementos que no cambian, como el fondo.
Los “B-frames” en particular son aquellos que, no sólo usan las diferencias con los fotogramas anteriores, sino también con los posteriores. Se distinguen 3 tipos en total:
- I-frames: Éstas son imágenes completas. Pueden estar comprimidas, pero no usan ninguna información de los otros fotogramas.
- P-frames: Usan la información de los fotogramas anteriores. Pueden conseguir un mayor nivel de compresión que los “I-frames”.
- B-frames: Usan la información de los fotogramas anteriores y posteriores. Son los que consiguen alcanzar un mayor nivel de compresión.
Esta imagen forma parte de una maravillosa guía de introducción al video digital por parte de Leandro Moreira.
Sin embargo, después de inspeccionar el elemento usado para la codificación (“omxh264enc”), determinamos que no se estaban usando “B-frames”.
Replicando el problema
Tenemos una NVIDIA Jetson Nano en nuestro laboratorio, así que intentamos replicar el problema. Poder replicar el problema en casa nos permite realizar una mejor depuración del problema. No pudimos replicar exactamente el mismo escenario debido a que no disponíamos de todo el hardware necesario. Sin embargo, sí conseguimos replicar un problema muy similar con un escenario muy parecido.
Conseguimos aislar el problema, parecía sólo pasar cuando se usaba la memoria de la GPU en una parte concreta del “pipeline”. Esta parte era un “pad probe”, que es básicamente un “callback” que se llama cuando un buffer llega al elemento. En esta “pad probe” se hacía una copia del buffer de entrada y se enviaba a un elemento llamado “appsrc”, que lo inyectaba en un “pipeline” secundario responsable de la grabación del vídeo.
Esta copia del “buffer” (aquí, un fotograma) nos resultó sospechosa, simplemente por ser memoria de la GPU. Las copias de memoria en GPU tienen una implementación más compleja que las de CPU. Inspeccionamos el código de NVIDIA y descubrimos que “gst_buffer_copy()” no estaba implementado.
Esto hacía que GStreamer usara un fallback, que básicamente copiaba (“memcpy()”) el manejador de la superficie (llamamos “superficie” a la memoria de la GPU, y su “manejador” sería el equivalente a un puntero a esta memoria). Esto significa que no se hace una copia real de la memoria, sino que se toma una referencia a ella.
GStreamer tiene mecanismos para gestionar varias referencias al mismo “buffer”, lo que permite evitar copias innecesarias de memoria, y además previene que algunas partes del “pipeline” modifiquen un “buffer” que está siendo usado por otras partes. El problema aquí es que GStreamer todavía piensa que tiene una copia, cuando en realidad tiene una referencia, por lo que no se pueden aplicar estos mecanismos de los que hablamos. Nuestro buffer podría ser modificado, o incluso borrado, sin que nos diésemos cuenta.
A continuación hemos preparado unas animaciones sencillas para ayudar a entender visualmente cual es el problema:
Qué está pasando:
Qué debería pasar:
Qué debería pasar (opción 2, usando las referencias de GStreamer):
Además, cuando inspeccionamos el código de NVIDIA para los elementos de “upstream” (los que están a la izquierda), nos dimos cuenta de que estaban usando una “piscina de superficies” para generar los “buffers”. Esto significa que reservan memoria en la GPU para una serie de superficies y las reusan siempre y cuando los elementos de “downstream” (los que están a la derecha) hayan terminado con ellas.
Lo que realmente estaba pasando en nuestro caso es lo siguiente:
-
Un “buffer” llega al “pad probe”
-
Se realiza una copia falsa y se envía al “pipeline” secundario, donde se queda encolada por un tiempo.
-
El “buffer” real sigue propagándose hasta el final de su “pipeline” hasta que es destruido. En este momento, la superficie asociada a este “buffer” puede ser reusada de nuevo.
-
Estos pasos previos pueden repetirse varias veces, dejando múltiples copias falsas encoladas en el “pipeline” secundario.
-
Llega un momento en el que el elemento de “upstream” empieza a reusar las superficies. Él piensa que puede hacerlo porque ya han sido marcadas como liberadas, pero realmente hay algunas de ellas que siguen encoladas en el “pipeline” secundario.
-
En este momento tenemos un escenario donde una de las superficies encoladas ha sido modificada, y ahora contiene un fotograma más nuevo. Por lo tanto, el orden de los fotogramas ha sido alterado.
Finalmente, proporcionamos una solución al cliente que consistía básicamente en cambiar el “gst_buffer_copy()” por “gst_buffer_ref()” (más algunos cambios extra para hacerlo funcionar bien). También reportamos a NVIDIA el problema con la implementación de “gst_buffer_copy()”.
Recapitulación
Una aplicación multimedia es una criatura compleja, con muchos hilos de ejecución en su interior compartiendo recursos. Mantener un buen seguimiento del uso de estos es fundamental para el correcto funcionamiento de la aplicación. Si alguna de estas piezas esenciales falla, puede hacer que el problema sea extremadamente difícil de localizar.
Este proyecto es un excelente ejemplo de ello, y un caso de éxito por parte de Fluendo en sus Servicios de Consultoría. Ya sea que se trate de una corrección de errores, capacitación u optimización de cualquier producto multimedia, nuestro equipo de expertos con gusto llevará tu proyecto al siguiente nivel. ¡Contáctanos!