Skyframe

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

El modelo de incrementalidad y evaluación paralelo de Bazel.

Modelo de datos

El modelo de datos consta de los siguientes elementos:

  • SkyValue También se denominan nodos. SkyValues son objetos inmutables que contienen todos los datos compilados en el transcurso de la compilación y las entradas de la compilación. Por ejemplo: archivos de entrada, archivos de salida, destinos y archivos objetivos.
  • SkyKey Un nombre inmutable corto para hacer referencia a un SkyValue, por ejemplo, FILECONTENTS:/tmp/foo o PACKAGE://foo.
  • SkyFunction Compila nodos en función de sus claves y nodos dependientes.
  • Grafo de nodo Es una estructura de datos que contiene la relación de dependencia entre nodos.
  • Skyframe El nombre interno del marco de trabajo de evaluación incremental es Bazel es en función de ellos.

Evaluación

Para lograr una compilación, se evalúa el nodo que representa la solicitud de compilación.

Primero, Bazel encuentra el SkyFunction correspondiente a la clave del nivel superior. SkyKey Luego, la función solicita la evaluación de los nodos que necesita evaluar el nodo de nivel superior, lo que, a su vez, generará otras llamadas a SkyFunction hasta que se alcanzan los nodos hoja. Los nodos de hoja suelen ser los que representan archivos de entrada en el sistema de archivos. Por último, Bazel termina con el valor de la SkyValue de nivel superior, algunos efectos secundarios (como archivos de salida en el archivo sistema) y un grafo acíclico dirigido de las dependencias entre los nodos involucradas en la compilación.

Un SkyFunction puede solicitar SkyKeys en varios pases si no se sabe en todos los nodos que necesita para hacer su trabajo. Un ejemplo sencillo es evaluar un nodo de archivo de entrada que resulta ser un symlink: la función intenta leer el archivo, se da cuenta de que es un symlink y, por lo tanto, recupera el nodo del sistema de archivos que representan el destino del symlink. Pero eso en sí puede ser un symlink, en en cuyo caso la función original también deberá recuperar su objetivo.

Las funciones se representan en el código con la interfaz SkyFunction y la los servicios que le proporciona una interfaz llamada SkyFunction.Environment. Estos lo que las funciones pueden hacer:

  • Solicita la evaluación de otro nodo llamando a env.getValue. Si el nodo está disponible, se muestra su valor; de lo contrario, se muestra null y se espera que la función en sí muestre null. En el último caso, el nodo dependiente y, luego, el compilador de nodos original Se vuelve a invocar, pero esta vez la misma llamada a env.getValue mostrará un un valor que no es null.
  • Llama a env.getValues() para solicitar la evaluación de muchos otros nodos. Esto hace básicamente lo mismo, con la excepción de que los nodos dependientes que se evalúen en paralelo.
  • Hacer cálculos durante la invocación
  • Tener efectos secundarios, por ejemplo, escribir archivos en el sistema Necesidades de cuidado que dos funciones diferentes evitan pisarse la una de la otra en los dedos de los pies. En general, escribir efectos secundarios (en los que los datos fluyen hacia afuera desde Bazel) los efectos secundarios (donde los datos fluyen en Bazel sin una dependencia registrada) no lo son, porque son una dependencia no registrada lo que puede generar compilaciones incrementales incorrectas.

Las implementaciones de SkyFunction con un buen comportamiento evitan el acceso a los datos de cualquier otra manera. que solicitar dependencias (por ejemplo, leer directamente el sistema de archivos), ya que eso hace que Bazel no registre la dependencia de datos en el archivo. que se leyó, lo que da como resultado compilaciones incrementales incorrectas.

Una vez que una función tenga suficientes datos para realizar su trabajo, debería mostrar un error que no sea null. valor que indica la finalización.

Esta estrategia de evaluación tiene una serie de beneficios:

  • Hermeticidad. Si las funciones solo solicitan datos de entrada dependiendo de otros nodos, Bazel puede garantizar que, si el estado de entrada es el mismo, se devuelven los mismos datos. Si todas las funciones de cielo son determinísticas, que toda la compilación también será determinista.
  • Incrementalidad correcta y perfecta. Si todos los datos de entrada de todas las funciones se registra, Bazel puede invalidar solo el conjunto exacto de nodos que necesitan invalide cuando cambien los datos de entrada.
  • Paralelismo. Dado que las funciones solo pueden interactuar entre sí mediante solicitar dependencias, las funciones que no dependen unas de otras pueden ejecutar en paralelo, y Bazel puede garantizar que el resultado sea el mismo que se ejecutaron de forma secuencial.

Incrementalidad

Debido a que las funciones solo pueden acceder a los datos de entrada dependiendo de otros nodos, Bazel puede crear un grafo de flujo de datos completo desde los archivos de entrada hasta la salida y usar esta información solo para reconstruir los nodos que realmente necesitan que debe reconstruirse: el cierre transitivo inverso del conjunto de archivos de entrada modificados.

En particular, existen dos estrategias de incrementalidad posibles: la ascendente y el de arriba hacia abajo. ¿Cuál es la óptima depende de cómo el gráfico de dependencia cómo es el aspecto.

  • Durante la invalidación ascendente, después de que se crea un grafo y el conjunto de valores se conocen entradas, se invalidan todos los nodos que dependen transitivamente archivos modificados. Esto es óptimo si se compilará el mismo nodo de nivel superior de nuevo. Ten en cuenta que la invalidación ascendente requiere que se ejecute stat() en todos archivos de entrada de la compilación anterior para determinar si se modificaron. Esta Se puede mejorar usando inotify o un mecanismo similar para obtener información sobre archivos modificados.

  • Durante la invalidación descendente, el cierre transitivo del nodo de nivel superior y solo se mantienen los nodos cuyo cierre transitivo está limpio. Esto es mejor si el gráfico de nodos es grande, pero la próxima compilación solo necesita un pequeño subconjunto: la invalidación ascendente invalidaría el gráfico más grande. de la primera compilación, a diferencia de la invalidación descendente, que solo recorre la parte gráfico de la segunda compilación.

Bazel solo realiza invalidaciones ascendentes.

Para obtener una mayor incrementalidad, Bazel usa la reducción de cambios: si un nodo se se invalida, pero luego de reconstruir, se descubre que su nuevo valor es el mismo como su valor anterior, los nodos que se invalidaron debido a un cambio en este nodo son “resucitados”.

Esto resulta útil, por ejemplo, si se modifica un comentario en un archivo C++, el código .o generado a partir de él será el mismo, por lo que no es necesario llamar el vinculador de nuevo.

Vinculación / compilación incremental

La principal limitación de este modelo es que la invalidación de un nodo es asunto de todo o nada: cuando una dependencia cambia, el nodo dependiente siempre está reconstruir desde cero, incluso si existiera un algoritmo mejor que mutaría el valor anterior del nodo según los cambios. Algunos ejemplos en los que esto serán útiles:

  • Vinculación incremental
  • Cuando un solo archivo de clase cambia en un archivo JAR, es posible modificar el archivo JAR en su lugar, en lugar de compilarlo desde cero nuevamente.

El motivo por el que Bazel no admite estas cosas de manera fundamentada. tiene dos aspectos:

  • El rendimiento fue limitado.
  • Dificultad para validar que el resultado de la mutación es el mismo que el una recompilación limpia, y Google valora las compilaciones que son repetible.

Hasta ahora, era posible lograr un rendimiento suficientemente bueno al descomponer un un paso de compilación costoso y lograr una reevaluación parcial de esa manera. Por ejemplo: En una app para Android, puedes dividir todas las clases en varios grupos y DEX por separado. De esta forma, si no se modifican las clases de un grupo, la conversión a DEX que no es necesario rehacer.

Asigna a conceptos de Bazel

Este es un resumen de alto nivel de los elementos SkyFunction y SkyValue clave. implementaciones que Bazel usa para realizar una compilación:

  • FileStateValue. Es el resultado de un lstat(). En el caso de los archivos existentes, también procesa información adicional para detectar cambios en el archivo. Este es el nodo de nivel más bajo en el gráfico de Skyframe y no tiene dependencias.
  • FileValue predeterminado Se usa con cualquier elemento que se preocupe por el contenido real o resuelta de un archivo. Depende del FileStateValue y cualquier symlink que deba resolverse (como FileValue para a/b necesita la ruta de acceso resuelta de a y la ruta de acceso resuelta de a/b). El la distinción entre FileValue y FileStateValue es importante porque este último puede usarse en los casos en que el contenido del archivo no esté realmente necesarias. Por ejemplo, el contenido del archivo es irrelevante cuando Evaluación de los globs del sistema de archivos (como srcs=glob(["*/*.java"]))
  • DirectoryListingStateValue. El resultado de readdir(). Me gusta FileStateValue: Es el nodo de nivel más bajo y no tiene dependencias.
  • DirectoryListingValue. Se usa por todo lo que se preocupa por las entradas de un directorio. Depende del DirectoryListingStateValue correspondiente, como así como el FileValue asociado del directorio.
  • PackageValue: Representa la versión analizada de un archivo BUILD. Depende de el FileValue del archivo BUILD asociado y también de forma transitiva en cualquier DirectoryListingValue que se usa para resolver los globs del paquete (la estructura de datos que representa el contenido de un archivo BUILD de forma interna).
  • ConfiguredTargetValue. Representa un destino configurado, que es una tupla del conjunto de acciones generadas durante el análisis de un objetivo y información proporcionada a destinos configurados dependientes. Depende del PackageValue en el que se encuentra el destino correspondiente, el ConfiguredTargetValues de dependencias directas y un nodo especial que representa la compilación configuración.
  • ArtifactValue asociado. Representa un archivo en la compilación, ya sea una fuente o un de salida. Los artefactos son casi equivalentes a los archivos y se usan para hacer referencia a los archivos durante la ejecución real de los pasos de compilación. Archivos de origen Depende del FileValue del nodo asociado y de los artefactos de salida dependerá del objeto ActionExecutionValue de cualquier acción que genere la artefacto.
  • ActionExecutionValue. Representa la ejecución de una acción. Depende de el ArtifactValues de sus archivos de entrada. La acción que ejecuta está contenida dentro de su SkyKey, lo que es contrario al concepto de que se debe pequeño. Ten en cuenta que ActionExecutionValue y ArtifactValue no se usan si la fase de ejecución no se ejecuta.

Como ayuda visual, este diagrama muestra las relaciones entre Implementaciones de SkyFunction después de una compilación de Bazel:

Un gráfico de las relaciones de implementación de SkyFunction