Ordenando la casa: cómo por fin usamos bien la caché de GitHub 🧹
En la última entrada técnica medimos lo que realmente cuesta nuestra cadena de compilación y comprobación, y descubrimos que no es dinero, sino tiempo de espera y almacenamiento. Aquel artículo terminaba con “la limpieza está planificada, pero todavía no hecha”. Resultó ir más rápido de lo esperado: desde el mediodía de hoy está en la rama principal. Aquí van los números.
Primero el resultado. El uso de caché de nuestro repositorio ha bajado de 5,32 GB en 63 entradas a 2,46 GB en 13. El análisis de seguridad de C# que antes corría en cada pull request ahora tarda 4 segundos en lugar de 4 minutos y medio, y sigue revisando todo lo que se publica. Ambas cosas sin eliminar ni una sola comprobación que te proteja.
Brevemente: ¿para qué sirve una caché? Cada compilación empieza en una máquina recién creada y vacía. Para que no tenga que empezar de cero cada vez, puede guardar resultados intermedios: la próxima vez los descarga en lugar de volver a crearlos. Para eso tienes 10 GB por repositorio. Cuando se llena, GitHub elimina lo que lleva más tiempo sin usarse. Y ese era justo nuestro problema: el espacio estaba ocupado, pero por las cosas equivocadas.
Error 1: una clave sin plataforma. Unity crea una enorme carpeta intermedia al compilar. La usan cuatro compilaciones (Windows, Linux, macOS, WebGL) y cada una necesita la suya. La de Windows era la única cuya clave de caché no llevaba marca de plataforma. Así que coincidía con la primera entrada disponible, que era la de WebGL: 1,7 GB descargados que Unity reconocía correctamente como “esto no es de aquí” y volvía a importar desde cero. En el registro: 257 segundos de importación en lugar de 85. Ahora la clave es Library-Windows- y el fantasma desapareció.
Error 2: nadie la guardaba nunca. El hallazgo más vergonzoso. Las cuatro compilaciones intentaban obedientemente restaurar la carpeta intermedia, pero ningún flujo de trabajo la escribía. La única entrada que existía venía de una herramienta que se lanza a mano. Es decir, la caché más importante del proyecto se mantenía por casualidad. 🙈 Ahora se escribe a propósito.
La decisión que ahorra espacio: solo cuenta el camino crítico. Lo obvio habría sido guardar en caché las cuatro plataformas. Basta mirar los tiempos de un lanzamiento real para ver por qué sería absurdo: las cuatro compilaciones empiezan a la vez, Windows termina a los 11 minutos, Linux y macOS a los 13,7, y WebGL a los 31,6. Mientras WebGL siga corriendo, las demás esperan igualmente. Así que solo su caché acorta la espera; las otras tres habrían costado unos 4,5 GB de nuestros 10 GB y habrían ahorrado exactamente cero minutos. Por eso guardamos justo una.
Error 3: Docker se comió el resto. Al construir las imágenes de nuestro servidor, cada capa intermedia se guardaba como caché: unos 3,27 GB en 24 entradas, el 64 % de todo el presupuesto, renovados en cada push. Ese aluvión expulsaba una y otra vez la única caché de Unity que importa. Ahora solo se guarda la etapa final, y cada una de nuestras cuatro imágenes tiene su propio espacio de nombres; antes compartían uno y se echaban fuera mutuamente. Los 3,27 GB se han convertido en unos 150 MB.
Análisis de seguridad: revisar donde cuenta. CodeQL, la herramienta de GitHub para encontrar agujeros de seguridad, era nuestro segundo mayor consumidor de tiempo: 230 minutos en ocho días, casi todo en la parte de C#, que recompila todo el proyecto para su análisis, en cada pull request. Ahora se ejecuta completo al fusionar con la rama principal y una vez por semana; los pull requests conservan las comprobaciones rápidas. Medido tras el cambio: 4 segundos en un pull request y unos buenos 4 minutos, sin cambios, en la rama principal. Así que cada línea que de verdad llega hasta ti sigue revisándose, solo que no tres veces.
Pequeñeces que suman. Los paquetes de .NET ahora se guardan en caché, pero solo en el camino en el que espera una persona. Un emulador para arquitecturas de procesador ajenas solo se configura cuando realmente compilamos para una. Un comando de limpieza que no tenía nada que hacer en una máquina recién arrancada, pero costaba hasta 1,4 minutos por compilación, desapareció. Y los archivos intermedios de una compilación de lanzamiento se borran a los 7 días en lugar de a los 90: se habían acumulado 703 de ellos, con un total de 7,4 GB.
Lo que deliberadamente no hicimos. Dos propuestas de nuestro propio análisis se descartaron tras medir, en lugar de venderlas como bonitas líneas en un registro de cambios. Repartir las pruebas en dos trabajos paralelos habría ganado exactamente cuatro segundos (una parte tarda 2:54, la otra 4 segundos) y habría cambiado el nombre de una comprobación obligatoria, dejando cada pull request “esperando” para siempre. Y saltarse la gran puerta de pruebas antes de un lanzamiento tampoco habría ahorrado nada, porque termina mucho antes de que WebGL acabe de compilar. Una optimización que no gana nada pero quita una red de seguridad no es una optimización.
Y de ahí salió un error de verdad. En medio de esta limpieza, de pronto todos los pull requests fallaban por un límite de tiempo: una sola prueba tardaba 212 segundos cuando se permiten 120. La segunda más lenta de la misma ejecución: 10,5 segundos. La causa era una ruta de error de nuestro servidor que simplemente cortaba una petición HTTP defectuosa en lugar de cerrarla limpiamente. En Linux, el sistema solo recupera esas conexiones en un barrido que corre cada dos minutos, y el apagado esperaba pacientemente a que llegara. En Windows nunca se notó, por eso solo mordía en el servidor de compilación. Ahora el servidor responde a ese caso con un código de error honesto y cierra la conexión él mismo: la prueba termina en unos 20 segundos, y los jugadores reales que se topen con ese tropiezo recibirán un error comprensible en lugar de una conexión cortada. Un día de limpieza de CI que encuentra un error de producción es un buen día. 🐛
Nada de esto te cambia nada directamente: ningún bloque nuevo, ningún animal nuevo. Simplemente acorta el camino entre “terminado de programar” e “instalable para ti”. Y demuestra la misma frase de la última vez: primero mide, luego toca. 🚀
Comentarios