2 puntos por GN⁺ 2024-11-30 | 1 comentarios | Compartir por WhatsApp
  • Ninja, publicado en 2011, comenzó como un sistema de build parecido a Make y terminó convirtiéndose en un caso representativo de éxito de código abierto, extendiéndose a Chrome, partes de Android, Meson y proyectos basados en CMake
  • El eje que definió su éxito y sus fracasos no fue tanto el código en sí, sino la arquitectura y, aún más, los problemas sociales como el mantenimiento y las expectativas de los usuarios
  • Ninja lee los comandos, entradas y salidas escritos en ninja.build, revisa las marcas de tiempo de modificación de archivos y luego ejecuta en paralelo las tareas necesarias, enfocándose en acelerar los builds incrementales en proyectos grandes
  • La decisión de diseño más importante fue no incorporar directamente funciones de build de alto nivel y quedarse como un ejecutor de gráfica de acciones (action graph), separando la lógica compleja para que la manejen los generadores
  • La integración con CMake, el soporte para Windows y el valor predeterminado de ejecución en paralelo impulsaron su adopción, pero las solicitudes y contribuciones que chocaban con sus objetivos de diseño terminaron siendo una gran carga para el mantenimiento de código abierto

El contexto en que Ninja llegó a usarse ampliamente

  • Ninja es un sistema de build similar a Make, publicado hace unos 9 años; comenzó como un proyecto personal compartido con algo de vergüenza, pero luego se difundió ampliamente
  • Sus principales casos de uso son los siguientes
    • Chrome eventualmente eliminó por completo todos los builds que no usaban Ninja
    • Android usa Ninja en algunas partes grandes del sistema
    • El proyecto Meson usa Ninja y parece ser un sistema de build cada vez más utilizado en el mundo del software libre
    • También hay muchos proyectos que usan Ninja junto con CMake, y la guía de compilación de Swift también exige tener Ninja instalado
  • Ninja se lanzó en 2011, la propiedad del proyecto cambió en 2014 y luego pasó a un tercer maintainer
  • La experiencia central que deja esta retrospectiva es que, más que el código, influye la arquitectura, y más que la arquitectura, los temas sociales

Funcionamiento básico para builds rápidos

  • Lo que hace Ninja es relativamente simple
    • El usuario escribe en el archivo ninja.build los comandos a ejecutar, los archivos que consume cada comando y los archivos que genera
    • Ninja lee este archivo y revisa las marcas de tiempo de modificación de varios archivos
    • Ejecuta en paralelo los comandos necesarios para dejar todo actualizado
  • Comparado con Make, el lenguaje de build de entrada tiene menos funciones y está enfocado en ejecutar esas pocas funciones con mucha rapidez
  • El flujo de ejecución se divide en tres etapas
    • Parseo e interpretación del archivo de build
    • Verificación de las marcas de tiempo de modificación de los archivos de entrada
    • Ejecución de los comandos necesarios
  • La meta es llegar a la tercera etapa lo más rápido posible, incluso en proyectos grandes con más de 100 mil archivos de entrada
  • Como ejemplo de optimización, Ninja mapea lo antes posible las rutas de archivos de entrada a objetos únicos en memoria, y luego usa comparación de punteros para verificar si las rutas son iguales
  • Los detalles de rendimiento de más bajo nivel se tratan en el capítulo de Ninja en The Performance of Open Source Software
  • A lo largo de los años también aparecieron reimplementaciones de Ninja
    • llbuild y Shake aceptan archivos Ninja como entrada
    • samurai es un proyecto reimplementado casi archivo por archivo; tiene menos código, pero también menos funciones y no tiene pruebas
  • La implementación divertida del 20% es fácil, pero el 80% restante está lleno de detalles, y no se conoce una implementación más rápida

Decisiones de diseño que fueron importantes

  • Representación del grafo

    • Make no maneja bien los casos en que una sola regla de build genera varios archivos
    • Ninja usa un grafo bipartito entre archivos y comandos, no un grafo solo entre archivos
    • Los nodos de archivo se conectan como aristas que entran a nodos de comando, y los nodos de comando vuelven a conectarse como aristas hacia archivos de salida
    • Esta estructura captura mejor la forma real de un build
    • Si cambia una de las entradas, el comando queda desactualizado, y cuando se ejecuta actualiza todas sus salidas
    • Un archivo determinado puede tener como máximo una arista de entrada
    • La propia línea de comandos también puede verse como una entrada del nodo de comando, así que si cambian los flags de la línea de comandos, el comando y sus salidas quedan desactualizados
  • deps log y dependencias de headers en C

    • Para manejar correctamente las dependencias de headers en C, hay que consumir los datos adicionales de dependencias que genera el compilador de C
    • Entre introducir una base de datos o priorizar la simplicidad, se terminó creando el formato de representación de deps log
    • Este formato es bastante compacto, pero sigue estando mal en algunos aspectos importantes
  • Diseño end-to-end / crash-only

    • Ninja no es un proceso daemon persistente; en cada ejecución empieza desde cero
    • Esta elección estuvo influida por el principio end-to-end y por el concepto de crash-only software
    • Si se hace rápida la ruta que empieza desde cero, no hace falta crear una ruta de código separada para el modo “online”
    • Los proyectos que pueden quedar residentes en memoria tienden al final a descuidar el rendimiento de arranque
  • Verificación del estado de archivos

    • Una de las razones por las que se espera que una herramienta de build permanezca residente en memoria es para cachear el estado de los archivos en disco
    • En la práctica, el kernel ya cachea esta información en memoria, y volver a cachearla en espacio de usuario no ahorra demasiado
    • En Linux, obtener el estado de un archivo es muy rápido, y Ninja lo hace en un solo hilo
    • Incluso en una máquina “rápida” de hace 10 años, se puede hacer stat de 30 mil archivos en unas decenas de milisegundos
  • Compromiso entre escala y especificación

    • Como regla general, escalar 2x puede resolverse con optimización, pero escalar 10x requiere re-arquitectura
    • Ninja se diseñó alrededor del build de Chrome, que en ese momento tenía unas 30 mil etapas de build
    • Ahora también se usa en entornos más pequeños, pero en esos casos puede que su ventaja de velocidad no sea necesaria
    • En entornos más grandes, como el build de Android, ya se está topando con límites de escala y quizá haga falta otro enfoque
    • Ninja exige que el usuario proporcione suficiente información para la ejecución en paralelo, pero no obliga a conocer por completo todo el build
    • Hay compromisos entre exactitud, comodidad y rendimiento, y una herramienta que sacrifica algo de exactitud por comodidad puede terminar dando resultados más correctos en el ecosistema completo que otra más exacta pero incómoda

La metáfora del “assembler” y la separación que produjo

  • Los sistemas de build ofrecen muchas funciones de alto nivel, y la forma en que cada herramienta se describe a sí misma es tan amplia que cuesta compararlas
  • La intuición central de Ninja es que, sin importar qué funciones de alto nivel ofrezca un sistema de build, al final tiene que construir una gráfica de acciones para mantener los archivos actualizados
  • Ninja solo ejecuta esa gráfica de acciones, y deja al usuario decidir qué generador poner por encima
  • La separación entre estos dos programas se creó originalmente porque encajaba bien con el proyecto Chrome, pero después se convirtió en la contribución más importante de Ninja
  • Tiene dos ventajas
    • Ninja en sí se mantiene simple y rápido
    • Las tareas costosas, como hacer glob de *.c, se empujan al lado del generador
  • A diferencia de otros sistemas de build que resuelven todo de una sola vez, la estructura de Ninja hace que la gráfica de acciones calculada se guarde como un snapshot en disco
  • Como resultado, la gráfica de acciones queda efectivamente cacheada entre builds
  • El generador puede ser tan de alto nivel como se quiera
    • Por ejemplo, puede recorrer con glob todo el árbol de código fuente y encontrar como tests los archivos cuyo nombre contenga test
  • El desarrollador tiene que decidir directamente por qué costo quiere pagar
    • Si el generador hace glob de todo el disco, se puede, pero también queda más claro por qué ese build es lento
  • La separación entre generador y gráfica de acciones no es realmente simple, y en Ninja también hay muchos detalles sobre qué capa debe encargarse de qué
  • Los sistemas de build de Xcode o Visual Studio también podrían, en teoría, precalcular y luego guardar un snapshot del resultado del mismo modo, pero se considera que no lo logran bien por la tentación de mezclar capas
  • Make intentó incluir todas las funciones orientadas a programadores, como globbing, expansión de variables, substring y funciones; terminó siendo un lenguaje demasiado débil para expresar bien todo lo necesario, pero lo bastante potente como para escribir Makefiles lentos
  • Ninja intentó evitar esa dirección

Los valores predeterminados dominan el rendimiento percibido

  • Ninja ejecuta en paralelo los comandos deseados por defecto
  • Make también puede ejecutar en paralelo con el flag -j, pero su valor predeterminado es la ejecución serial
  • Los Makefiles suelen escribirse sin declarar suficientes dependencias, por lo que con frecuencia no son seguros para ejecutar en paralelo
  • Ninja siempre ejecuta en paralelo, incluso en sistemas de un solo núcleo, así que este tipo de errores sale a la luz antes
  • Los programas que encajan bien con Ninja normalmente también terminan siendo seguros para builds paralelos
  • Ninja no tiene un sistema sofisticado para detectar dependencias incorrectas; simplemente, como los builds incorrectos ocurren más a menudo, los problemas se hacen visibles
  • Como muchos usuarios olvidan o ni siquiera conocen el flag de ejecución paralela de Make, solo ese valor predeterminado puede hacer que Ninja se sienta “más del doble de rápido” que Make en el uso real
  • Una optimización que el usuario nunca llega a experimentar tiene poco valor

Métricas de velocidad que realmente importaban

  • El rendimiento de un sistema de build puede significar varias cosas
    • Cuánto tarda un build completo desde cero
    • Cuánto tarda reconstruir después de modificar un solo archivo cuando ya se había hecho un build antes
  • Ninja se enfocó en acelerar el ciclo editar-compilar en los builds incrementales de bases de código grandes
  • Cuando se creó Ninja, se buscaba igualar la velocidad de blaze, es decir, bazel, porque se recordaba como extremadamente rápido
  • Viéndolo después, blaze no era especialmente rápido en las métricas que realmente interesaban, y al ser un programa Java hasta mostrar la ayuda era lento
  • La obsesión con los builds incrementales venía de pensar que el tiempo de iteración influye mucho en la satisfacción de los programadores
  • Ninja se usa en el ciclo editar-compilar, donde la diferencia entre 1 segundo y 4 segundos importa
  • Es difícil comunicar al usuario qué significa “rápido”, y el manual de Ninja advierte que en proyectos pequeños el efecto en velocidad puede sentirse poco o nada
  • Pero “rápido” vende bien, así que incluso usuarios de apps pequeñas frustrados por la falta de funciones también intentan usar Ninja
  • Aunque Ninja se enfocó en reconstrucciones incrementales, algunos usuarios también reportaron mejores tiempos en builds completos
    • Como Ninja casi no hace nada por sí mismo, usa poco CPU durante el build y le quita menos CPU al trabajo real de compilación
  • La salida también es muy concisa
    • Un build exitoso normalmente imprime solo una línea
    • Otros sistemas de build pueden imprimir muchas etapas e información de tiempos, lo que los hace sentir más pesados
  • Esa forma de hablar poco hace que Ninja se sienta más “invisible”

La integración con CMake y el soporte para Windows impulsaron su expansión

  • CMake

    • Ninja se creó originalmente para usarse con el sistema de build especial de Chrome
    • Peter Collingbourne fue quien hizo el trabajo de conectar Ninja con el mucho más difundido sistema de build CMake
    • Esta integración comenzó por trabajo relacionado con LLVM, y exigió agregar nueva semántica no solo a CMake sino también a Ninja
    • Se considera que Peter fue la persona con más mérito en el éxito real de Ninja
    • Después, los autores de CMake asumieron la integración, pero no respondieron lo suficiente a solicitudes e inquietudes
    • El desarrollador nunca ha usado CMake directamente hasta ahora
  • Windows

    • Como Chrome también apuntaba a Windows, Ninja terminó funcionando en Windows
    • Buena parte del soporte para Windows fue escrita por un contribuidor
    • Técnicamente, el soporte para Windows es una gran molestia
    • La ejecución de procesos y la captura de salida son diferencias del tipo que obliga a aprender APIs específicas de cada plataforma desde cero
    • El diseño de Ninja depende de poder obtener rápidamente la hora de última modificación de archivos que el kernel ya tiene cacheados, pero eso no se cumple en Windows
    • Aun así, Windows es una plataforma con una enorme cantidad de desarrolladores
    • Con herramientas elegantes para Linux suele existir un fuerte impulso por compartirlas, mientras que con herramientas para Windows suele existir un fuerte impulso por venderlas, así que hay relativamente menos herramientas disponibles libremente
    • Que hubiera muchos usuarios de Windows entre los primeros usuarios de Ninja fue sorprendente en su momento, pero tiene sentido porque hay tantos desarrolladores de Windows que basta con que solo una parte se interese para que ya aparezcan como usuarios

Entender el trabajo relacionado

  • Como Ninja empezó casi como un hack de demostración de fin de semana, se lamenta no haber investigado mejor el trabajo relacionado antes de construirlo
  • Al crear algo, es importante realmente entender el espacio de diseño
  • El término “action graph” no era una expresión usada al crear Ninja, sino que fue tomado del sistema de build blaze/bazel de Google
  • bazel explica que un grafo de targets de alto nivel como “library” o “binary” genera un action graph, que es un grafo de comandos
  • La idea de tratar el texto de la línea de comandos como una entrada igual que un archivo es un caso de cómputo incremental
  • El cómputo incremental se conecta no solo con sistemas de build sino también con la incrementalidad en interfaces de usuario
  • En el blog de Jane Street hay una introducción a Incremental, y también se conecta con formas de construir UI como React
  • "Build Systems à la Carte" es un paper que discute el cómputo incremental en el contexto de sistemas de build, y se menciona como un material que habría sido ideal conocer antes de escribir Ninja

La carga del mantenimiento de código abierto

  • La experiencia como maintainer de código abierto no fue particularmente agradable
  • Los sentimientos hacia el proyecto mezclan el orgullo que surge cuando alguien dice algo bueno con una decepción aún mayor
  • Como se publicó gratis, a veces había gente que pedía cosas con amabilidad, pero mucho más a menudo llegaban usuarios enojados exigiendo cosas, y los agradecimientos eran raros
  • También hubo repetidos casos de personas que amenazaban con hacer un fork si no se aceptaban sus demandas
  • Las contribuciones razonables que chocaban con los objetivos de diseño eran otra dificultad
    • Cuando una persona amable e inteligente enviaba una contribución, se quería responder con una explicación suficiente al rechazarla, y ese esfuerzo mismo resultaba agotador
  • La programación empezó a través del software libre y se escribió código queriendo devolver algo de lo recibido, pero el software libre actual parece menos una práctica de compartir entre pares y más una dinámica donde los usuarios se ven a sí mismos como clientes y tratan al autor como alguien de quien pueden quejarse ante un gerente
  • La motivación actual se acerca más a impresionar a unos pocos hackers respetados o estar a la altura de sus expectativas que a lograr un éxito masivo
  • El éxito de Ninja dejó muchos aprendizajes, pero se piensa que un éxito más pequeño quizá habría permitido aprender casi lo mismo

Maintainers y contribuidores

  • Nico Weber fue un colaborador cuidadoso y fue maintainer de Ninja durante años
  • Jan Niklas Hasse tomó el proyecto después de Nico y parece estar haciéndolo bien
  • Además, muchos otros contribuidores de Ninja participaron en el proyecto

1 comentarios

 
GN⁺ 2024-11-30
Comentarios de Hacker News
  • La frase “se habla de programación como si fuera escribir código, pero en realidad la arquitectura importa más que el código, y los temas sociales importan más que la arquitectura” expresa con precisión algo que he pensado desde hace mucho tiempo

    • Totalmente de acuerdo. Peopleware 1987 [1] dice lo mismo
      El capítulo 1 del libro sostiene que “los principales problemas de nuestro trabajo tienen más un carácter sociológico que técnico”, y trata temas sociales y políticos como la química y cohesión del equipo, el “tiempo de concentración”, los entornos de trabajo silenciosos y el costo de la rotación laboral
      [1] https://en.wikipedia.org/wiki/Peopleware:_Productive_Project...
    • La programación y sus herramientas son para las personas, no para las computadoras, y salvo tareas muy triviales, se necesita a varias personas para crear algo que funcione sobre una computadora
      Por eso creo que la ingeniería de software está mucho más cerca de las ciencias sociales de lo que muchos reconocen o con lo que se sienten cómodos. Como las ciencias sociales son más fluidas y menos predecibles que las ciencias naturales, parece haber una tendencia a evitar la parte social o tratarla solo de forma muy primitiva. Nos obsesionamos con los detalles atómicos de la tecnología, pero aun así los productos que hacen los equipos terminan siendo demasiado malos con demasiada frecuencia
    • https://en.wikipedia.org/wiki/Conway's_law
      “Las organizaciones que diseñan sistemas están condenadas a producir diseños que son copias de las estructuras de comunicación de esas organizaciones.” — Melvin E. Conway, How Do Committees Invent?
    • En mi experiencia, aproximadamente el 80% de los problemas técnicos surgen porque dos personas o dos equipos simplemente evitan sentarse a hablar
    • Pero tampoco es una dicotomía. Una buena arquitectura satisface lo que las personas le exigen al sistema y al mismo tiempo hace que siga siendo comprensible para los humanos
  • Ya se había discutido en su momento:
    The Success and Failure of Ninja - https://news.ycombinator.com/item?id=23157783 - mayo de 2020, 38 comentarios
    Repostearlo después de alrededor de un año está bien, y el enlace al hilo anterior es para lectores que quieran profundizar más

  • Es graciosa la parte de “Android usa Ninja para algún gran componente del sistema, pero nunca entendí exactamente para qué”
    Ninja en realidad ocupa un lugar importante en AOSP. El sistema de compilación al principio usaba Makefile, pero se volvió rápidamente complejo por soong, un sistema de compilación declarativo personalizado, y por una migración fallida e interrumpida a Bazel. Google creó kati(https://github.com/google/kati) para convertir Makefile en archivos de compilación de Ninja, y el resultado es realmente enorme:
    λ wc -l out/build-qssi.ninja
    3035442 out/build-qssi.ninja
    El proceso de pasar de Makefile/soong a Ninja es doloroso y toma varios minutos incluso en máquinas modernas, pero una vez que Ninja lo recibe, corre rapidísimo

    • Nunca he usado Ninja, así que me da curiosidad qué ventajas tiene frente a Makefile
      También me pregunto si vale la pena introducir otra herramienta más para traducir de una herramienta a otra. Más aún si los archivos de Ninja pueden ser así de grandes y difíciles de leer para humanos
  • En la parte que dice “creo que los programadores perciben la latencia, y que esta afecta su estado de ánimo aunque no se den cuenta. Google investigó esto recientemente y en cierta medida confirmó mi creencia”, me pregunto si ese estudio sobre latencia llegó a publicarse realmente

  • Ninja es bastante popular entre los desarrolladores de videojuegos
    Me pareció divertida la frase “Windows sigue siendo una plataforma enorme desde el punto de vista del desarrollador, y esos desarrolladores están hambrientos de herramientas”. Como alguien que desarrolla principalmente en Windows, más bien me parece que quienes están hambrientos de herramientas son los desarrolladores de Linux que viven sin un buen depurador como Visual Studio o un profiler como Superluminal. Últimamente parece que la brecha entre ambas plataformas se está reduciendo, y en general también me gustan las utilidades de Rust que funcionan de forma multiplataforma

    • Cada vez que alguien pone Visual Studio y “bueno” en la misma oración, me da una extraña sensación de que el universo es una simulación y las demás personas vienen de otro universo
      No se me ocurre ninguna interacción con Visual Studio que yo llamaría “buena”. Se me ocurren algunas que apenas llamaría “aceptables”, pero la depuración no entra en esa lista. Aunque en 2022 el depurador ya no está lleno de bugs, así que quizá se refieran a eso
    • No sé en qué mundo el depurador de Visual Studio es considerado bueno. A menos que por fin lo hayan arreglado, la última vez que lo usé era insoportablemente lento, al punto de tardar varios segundos por cada paso de ejecución de una sola línea
    • Hay una comunidad que ve a UNIX como si fuera la totalidad de las herramientas para desarrolladores, y siento que por eso terminan viendo el bosque y perdiéndose los árboles
      Conozco bien UNIX, hasta el punto de haber usado varias variantes desde que conocí Xenix en 1993, pero casi no uso WSL salvo para correr contenedores Docker de Linux
  • Algunas de las pocas cosas para las que todavía usaba Ninja las cambié por samurai, y ha mejorado en todos los aspectos posibles
    Aun así, creo que este tipo de sistema de build va en la dirección equivocada. Lo que quiero de un sistema de build es que haga hash del contenido de todas las entradas transitivas y consulte si ese resultado está en el registro

    • Sí. Los sistemas de build que soportan caché distribuida básicamente usan digests en lugar de marcas de tiempo para detectar cambios: Bazel, Pants, Buck, etc.
      Pero todos son tremendamente complejos. Si nos limitamos a builds locales, tengo entendido que SCons y Waf también usan hashes para detectar cambios
    • Te podría interesar n2, hecho por el autor de Ninja
    • Creo que esa era la idea de NetKernel
      Hice algo parecido con la librería de Deno “TDAR”[1] y funciona bien, pero sí requiere algo de trabajo envolver herramientas de línea de comandos que asumen que corren sobre un sistema de archivos mutable, fingiendo llamarlas como si fueran funciones puras
      [1] Todavía no la han separado del proyecto principal[2], pero hablé de eso en este video de YouTube: https://youtu.be/sty29o8sUKI
      [2] Si a alguien le interesa este tipo de cosas, puede insistirme para que publique el código fuente. togos zero zero at gee mail dot comb
    • Me da curiosidad qué hace mejor Samurai. Pensaba que era un subconjunto compatible de Ninja
      Y que “no era lo que yo quería” no significa automáticamente “estaba mal”. Hay gente en el mundo con gustos distintos
    • No sé si Samurai sigue vivo. Envié un pull request para mejorar el manejo de señales y lleva más de medio año ignorado
  • Como en CMake se necesita Ninja para usar módulos de C++20, Ninja probablemente va a seguir por bastante tiempo

  • La parte que me pareció más interesante fue donde dice que “a menudo hay que hacer concesiones entre corrección y conveniencia o rendimiento, y hay que ser consciente de en qué punto de ese continuo se elige estar”
    Algunos programadores ven esta dinámica de forma demasiado rígida y creen que uno de esos valores debe tener prioridad obvia. Pero en la práctica su interacción es bastante sutil. Por ejemplo, una herramienta que sacrifica algo de corrección y elige conveniencia puede producir resultados más correctos para todo el ecosistema que una alternativa más correcta pero menos conveniente. Eso pasa si los programadores evitan esta última

    • Esta observación es oro puro. Por eso Python, Go, TypeScript/JavaScript son muchísimo más populares que Haskell/OCaml
  • Eso de “perdonen el nombre vergonzoso”, para nada, el nombre está excelente
    P. D.: si implementan esto, también podría volverse más rápido: https://github.com/ninja-build/ninja/issues/2157 aunque, como se explica en el artículo, esta herramienta deliberadamente no tiene estado, ni siquiera pistas diminutas provenientes de la ejecución anterior