Desafíos de la redacción de reglas

Informar un problema Ver fuente Por la noche · 7.2 · 7.1 · 7.0 · 6.5 · 6.4

En esta página, se ofrece una descripción general de los problemas y desafíos específicos de escritura de reglas de Bazel eficientes.

Resumen de requisitos

  • Suposición: Precisión, capacidad de procesamiento, facilidad de uso y Latencia
  • Suposición: Repositorios a gran escala
  • Suposición: Lenguaje de descripción similar a la COMPILACIÓN
  • Histórica: La separación estricta entre carga, análisis y ejecución es Está desactualizada, pero aún afecta la API
  • Funciones intrínsecas: La ejecución remota y el almacenamiento en caché son difíciles
  • Funciones intrínsecas: Uso de la información de cambio para compilaciones incrementales correctas y rápidas requiere patrones de codificación inusuales
  • Funciones intrínsecas: Evitar el tiempo cuadrático y el consumo de memoria es difícil

Suposiciones

Estas son algunas suposiciones hechas sobre el sistema de compilación, como la necesidad de precisión, facilidad de uso, capacidad de procesamiento y repositorios a gran escala. El siguientes secciones abordan estas suposiciones y ofrecen lineamientos para garantizar las reglas están escritas de una manera efectiva.

Apunta a la precisión, la capacidad de procesamiento, la facilidad de uso y latencia

Suponemos que, en primer lugar, el sistema de compilación debe ser correcto con con respecto a las compilaciones incrementales. Para un árbol de fuentes determinado, el resultado del La misma compilación siempre debería ser la misma, independientemente de cómo se vea el árbol de resultados. de Cymbal Direct. En la primera aproximación, esto significa que Bazel necesita saber cada uno de entrada que se incluye en un paso de compilación determinado, de modo que pueda volver a ejecutar ese paso si corresponde de las entradas cambia. Existen límites en cuanto a la forma correcta en que puede ser Bazel, debido a fugas. cierta información, como la fecha y la hora de la compilación, e ignora ciertos tipos de cambios, como cambios en los atributos del archivo. Zona de pruebas Ayuda a garantizar la precisión evitando lecturas a archivos de entrada no declarados. Además los límites intrínsecos del sistema, existen algunos problemas de corrección conocidos, la mayoría de las cuales están relacionadas con el conjunto de archivos o con las reglas de C++, que son duras problemas. A largo plazo, tenemos que tomar medidas para solucionarlos.

El segundo objetivo del sistema de compilación es tener una alta capacidad de procesamiento. somos superar de forma permanente los límites de lo que se puede hacer dentro del asignación de máquinas para un servicio de ejecución remota. Si la ejecución remota el servicio se sobrecarga, nadie puede realizar su trabajo.

La facilidad de uso es lo siguiente. De varios enfoques correctos con el mismo (o similar) del servicio de ejecución remota, elegimos el que sea y fácil de usar.

La latencia denota el tiempo que se tarda desde el inicio de una compilación hasta la obtención de la respuesta deseada. resultado, ya sea un registro de prueba de una prueba aprobada o fallida, o un error mensaje de que un archivo BUILD tiene un error tipográfico.

Ten en cuenta que estos objetivos a menudo se superponen. la latencia es tanto una función de la capacidad de procesamiento del servicio de ejecución remota, según corresponda para facilitar el uso.

Repositorios a gran escala

El sistema de compilación debe operar a la escala de repositorios grandes en los que haya escala significa que no cabe en un solo disco duro, por lo que es imposible un proceso de pago completo en casi todas las máquinas de los desarrolladores. Una compilación de tamaño medio deberá leer y analizar decenas de miles de archivos BUILD y evaluar cientos de miles de globs. Si bien es teóricamente posible leer todas BUILD en una sola máquina, aún no hemos podido hacerlo en un de tiempo y memoria razonables. Por lo tanto, es fundamental que los archivos BUILD pueden cargarse y analizarse de forma independiente.

Lenguaje de descripción similar a BUILD

En este contexto, asumimos que se usa un lenguaje de configuración similar a los archivos BUILD en la declaración de reglas binarias y de biblioteca y sus interdependencias. Los archivos BUILD se pueden leer y analizar de forma independiente. y evitamos siquiera mirar los archivos de origen siempre que podamos (excepto por existencia).

Lugar histórico

Hay diferencias entre las versiones de Bazel que plantean desafíos y algunas de estos, se describen en las siguientes secciones.

La separación estricta entre la carga, el análisis y la ejecución está desactualizada, pero aún afecta a la API

Técnicamente, es suficiente que una regla conozca los archivos de entrada y salida de una acción justo antes de que la acción se envíe a la ejecución remota. Sin embargo, el la base de código original de Bazel tenía una separación estricta de carga de paquetes y, luego, analizar reglas con una configuración (básicamente, marcas de la línea de comandos) para luego ejecutar acciones. Esta distinción sigue siendo parte de la API de reglas en la actualidad, aunque el núcleo de Bazel ya no lo requiere (más detalles a continuación).

Eso significa que la API de reglas requiere una descripción declarativa de la regla. (los atributos que tiene, los tipos de atributos). Existen algunos excepciones en las que la API permite la ejecución de código personalizado durante la fase de carga procesar nombres implícitos de archivos de salida y valores implícitos de atributos. Para ejemplo, una regla de java_library llamada "foo" genera implícitamente una salida llamada "libfoo.jar", al que se puede hacer referencia desde otras reglas en el gráfico de compilación.

Además, el análisis de una regla no puede leer ningún archivo de origen ni inspeccionar el resultado de una acción; sino que debe generar un bipartito dirigido gráfico de pasos de compilación y nombres de archivos de salida que solo se determina a partir de la regla y sus dependencias.

Intrínseco

Hay algunas propiedades intrínsecas que hacen que la escritura de reglas sea un desafío y algunos de los más comunes se describen en las siguientes secciones.

La ejecución remota y el almacenamiento en caché son difíciles

La ejecución remota y el almacenamiento en caché mejoran los tiempos de compilación en repositorios grandes, ya que en aproximadamente dos órdenes de magnitud en comparación con ejecutar la compilación en un solo máquina. Sin embargo, la escala a la que debe funcionar es sorprendente: de ejecución remota está diseñado para manejar una gran cantidad de solicitudes por por segundo, y el protocolo evita cuidadosamente viajes de ida y vuelta innecesarios trabajo innecesario por parte del servicio.

En este momento, el protocolo requiere que el sistema de compilación conozca todas las entradas de un realizar una acción con anticipación; el sistema de compilación procesa una acción única huella digital y le solicita al programador un acierto de caché. Si se encuentra un acierto de caché, el programador responde con los resúmenes de los archivos de salida; los archivos en sí están se abordará en un resumen más adelante. Sin embargo, esto impone restricciones al archivo de entrada, que deben declarar todos los archivos de entrada con anticipación.

El uso de información de cambios para compilaciones incrementales rápidas y correctas requiere patrones de programación inusuales

Arriba, argumentamos que, para ser correcto, Bazel necesita conocer todas las entradas que se incluyen en un paso de compilación para detectar si ese paso aun está actualizado. Lo mismo ocurre con la carga de paquetes y el análisis de reglas, y diseñaron Skyframe para que se encargue de esto en general. Skyframe es una biblioteca de grafos y marco de evaluación que toma un objetivo (como “crear //foo con estas opciones”) y lo desglosa las partes que las conforman, que luego se evalúan y combinan para obtener este resultado. Como parte del proceso, Skyframe lee paquetes, analiza reglas y que ejecuta acciones.

En cada nodo, Skyframe realiza un seguimiento exacto de qué nodos usa cada nodo para calcular su propia salida, desde el nodo objetivo hasta los archivos de entrada (que también son nodos de Skyframe). Tener este gráfico representado explícitamente en la memoria permite que el sistema de compilación identifique exactamente qué nodos se ven afectados por un en un archivo de entrada (incluida la creación o eliminación de un archivo de entrada), la cantidad mínima de trabajo para restablecer el árbol de resultados a su estado previsto.

Como parte de esto, cada nodo realiza un proceso de descubrimiento de dependencias. Cada puede declarar dependencias y, luego, usar su contenido para declarar aún más dependencias. En principio, esto se relaciona bien modelo de subproceso por nodo. Sin embargo, las compilaciones medianas contienen cientos de miles de nodos de Skyframe, lo que no es fácil con la versión actual de Java (y por razones históricas, actualmente estamos sujetos al uso de Java, por lo que sin subprocesos ligeros ni continuaciones).

En su lugar, Bazel usa un grupo de subprocesos de tamaño fijo. Sin embargo, eso significa que si un nodo declara una dependencia que aún no está disponible, es posible que debamos anular esa evaluación y reiniciarla (posiblemente en otro subproceso) cuando la dependencia disponibles. A su vez, esto significa que los nodos no deben hacerlo en exceso; pañal que declara N dependencias en serie podría reiniciarse N veces, lo que cuesta O(N^2) tiempo. En cambio, apuntamos a la declaración masiva por adelantado de dependencias, lo que a veces requiere reorganizar el código o incluso dividir de un nodo en varios nodos para limitar la cantidad de reinicios.

Ten en cuenta que esta tecnología no está disponible actualmente en la API de reglas. en su lugar, La API de reglas aún se define usando los conceptos heredados de carga, análisis, y ejecución. Sin embargo, una restricción fundamental es que todos los accesos otros nodos tienen que pasar por el framework para que pueda hacer un seguimiento del las dependencias correspondientes. Sin importar el lenguaje en que se use el sistema de compilación se implementa o en la que se escriben las reglas (no tienen que ser iguales), los autores de las reglas no deben usar bibliotecas ni patrones estándar que omitan Skyframe. Para Java, esto significa evitar java.io.File así como cualquier forma de reflexión y cualquier biblioteca que haga cualquiera de ellas. Bibliotecas que admiten dependencias de estas interfaces de bajo nivel aún deben configurarse correctamente para Skyframe.

Esto sugiere evitar exponer a los autores de las reglas a un entorno de ejecución de lenguaje completo. en primer lugar. El peligro del uso accidental de esas APIs es demasiado grande. varios errores de Bazel en el pasado se debían a reglas que usaban APIs no seguras, incluso aunque las reglas fueron redactadas por el equipo de Bazel o por otros expertos del dominio.

Evitar el tiempo cuadrático y el consumo de memoria es difícil

Para empeorar la situación, además de los requisitos que impone Skyframe, el las limitaciones históricas del uso de Java y la obsolescencia de la API de reglas, introducir accidentalmente tiempo cuadrático o consumo de memoria es una parte en cualquier sistema de compilación basado en reglas binarias y de biblioteca. Existen dos patrones muy comunes que introducen un consumo de memoria cuadrática (y, por lo tanto, consumo de tiempo cuadrático).

  1. Cadenas de reglas de bibliotecas Considera el caso de una cadena de reglas de biblioteca: A depende de B, depende de C, y y así sucesivamente. Luego, vamos a calcular alguna propiedad sobre el cierre transitivo de estas reglas, como la classpath de tiempo de ejecución de Java o el comando del vinculador C++ para cada biblioteca. Ingeniosamente, podríamos tomar una implementación de listas estándar. Sin embargo, esto introduce el consumo de memoria cuadrática: la primera biblioteca contiene una entrada en la ruta de clase, la segunda, la tercera y para un total de 1 + 2 + 3 +...+ N = O(N^2).

  2. Reglas binarias según las mismas reglas de la biblioteca Considera el caso en el que un conjunto de objetos binarios que dependen de la misma biblioteca de prueba (por ejemplo, si tiene varias reglas de prueba que prueban la misma el código de la biblioteca. Digamos que, de N reglas, la mitad de las reglas son binarias y la otra mitad de las reglas de la biblioteca. Ahora, considera que cada objeto binario hace una copia de alguna propiedad calculada sobre el cierre transitivo de las reglas de la biblioteca, como la ruta de clase del tiempo de ejecución de Java o la línea de comandos del vinculador C++. Por ejemplo, podría expandir la representación de la cadena de línea de comandos de la acción de vínculo de C++. N/2 copias de N/2 elementos es la memoria O(N^2).

Clases de colecciones personalizadas para evitar la complejidad cuadrática

Bazel se ve muy afectado por ambas situaciones, así que presentamos un conjunto de personalizadas que comprimen eficazmente la información en la memoria evitar la copia en cada paso. Casi todas estas estructuras de datos han establecido semántica, así que la llamamos depset (también conocido como NestedSet en la implementación interna). La mayoría de para reducir el consumo de memoria de Bazel en los últimos años para usar depsets en lugar de lo que se haya usado antes.

Lamentablemente, el uso de depsets no resuelve automáticamente todos los problemas. en particular, incluso la iteración sobre un descenso en cada regla vuelve a introducir consumo de tiempo cuadrático. Internamente, NestedSets también tiene algunos métodos auxiliares. Para facilitar la interoperabilidad con clases de colecciones normales lamentablemente, pasar accidentalmente un NestedSet a uno de estos métodos genera copias y vuelve a introducir el consumo de memoria cuadrática.