Skyframe

Informar un problema Ver código fuente Nightly · 8.0 7.4 . 7.3 · 7.2 · 7.1 · 7.0 · 6.5

El modelo de evaluación en paralelo y de incrementalidad 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 a lo largo de la compilación y las entradas de la compilación. Algunos ejemplos son: archivos de entrada, archivos de salida, destinos y destinos configurados.
  • SkyKey: Es un nombre inmutable corto para hacer referencia a un SkyValue, por ejemplo, FILECONTENTS:/tmp/foo o PACKAGE://foo.
  • SkyFunction: Compila nodos según sus claves y nodos dependientes.
  • Gráfico de nodos. Es una estructura de datos que contiene la relación de dependencia entre los nodos.
  • Skyframe: Es el nombre de código del marco de trabajo de evaluación incremental en el que se basa Bazel.

Evaluación

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

Primero, Bazel encuentra el SkyFunction que corresponde a la clave del SkyKey de nivel superior. Luego, la función solicita la evaluación de los nodos que necesita para evaluar el nodo de nivel superior, lo que, a su vez, genera otras llamadas a SkyFunction hasta que se alcanzan los nodos hoja. Los nodos hoja suelen ser los que representan los 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 los archivos de salida en el sistema de archivos) y un grafo acíclico dirigido de las dependencias entre los nodos involucrados en la compilación.

Un SkyFunction puede solicitar SkyKeys en varios pases si no puede indicar con anticipación 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 representa el destino del symlink. Sin embargo, eso puede ser un symlink, en cuyo caso la función original también deberá recuperar su destino.

Las funciones se representan en el código con la interfaz SkyFunction y los servicios que le proporciona una interfaz llamada SkyFunction.Environment. Estas son las acciones que pueden realizar las funciones:

  • 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 muestre null. En el último caso, se evalúa el nodo dependiente y, luego, se vuelve a invocar al compilador de nodos original, pero esta vez la misma llamada a env.getValue mostrará un valor que no sea null.
  • Llama a env.getValues() para solicitar la evaluación de varios otros nodos. Esto hace lo mismo, excepto que los nodos dependientes se evalúan en paralelo.
  • Realizar cálculos durante su invocación
  • Tener efectos secundarios, por ejemplo, escribir archivos en el sistema de archivos Se debe tener cuidado para que dos funciones diferentes no se superpongan. En general, los efectos secundarios de escritura (en los que los datos fluyen hacia afuera desde Bazel) están bien, los efectos secundarios de lectura (en los que los datos fluyen hacia adentro en Bazel sin una dependencia registrada) no, ya que son una dependencia no registrada y, como tal, pueden causar compilaciones incrementales incorrectas.

Las implementaciones de SkyFunction que se comportan bien evitan acceder a los datos de cualquier otra manera que no sea solicitando dependencias (como 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 genera compilaciones incrementales incorrectas.

Una vez que una función tiene suficientes datos para realizar su trabajo, debe mostrar un valor que no sea null que indique que se completó.

Esta estrategia de evaluación tiene varios 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 muestran los mismos datos. Si todas las funciones de Sky son deterministas, esto significa que toda la compilación también será determinista.
  • Proporciona una incrementalidad correcta y perfecta. Si se registran todos los datos de entrada de todas las funciones, Bazel puede invalidar solo el conjunto exacto de nodos que se deben invalidar cuando cambian los datos de entrada.
  • Paralelismo Dado que las funciones solo pueden interactuar entre sí a través de la solicitud de dependencias, las funciones que no dependen entre sí se pueden ejecutar en paralelo, y Bazel puede garantizar que el resultado sea el mismo que si se ejecutaran de forma secuencial.

Incrementalidad

Dado que las funciones solo pueden acceder a los datos de entrada dependiendo de otros nodos, Bazel puede compilar un gráfico de flujo de datos completo desde los archivos de entrada hasta los archivos de salida, y usar esta información para volver a compilar solo los nodos que realmente necesitan recompilarse: el cierre transitivo inverso del conjunto de archivos de entrada modificados.

En particular, existen dos estrategias de incremento posibles: la ascendente y la descendente. La opción óptima depende del aspecto del gráfico de dependencias.

  • Durante la invalidación ascendente, después de que se compila un gráfico y se conoce el conjunto de entradas modificadas, se invalidan todos los nodos que dependen de forma transitiva de los archivos modificados. Esto es óptimo si se volverá a compilar el mismo nodo de nivel superior. Ten en cuenta que la invalidación ascendente requiere ejecutar stat() en todos los archivos de entrada de la compilación anterior para determinar si se modificaron. Esto se puede mejorar con el uso de inotify o un mecanismo similar para obtener información sobre los archivos modificados.

  • Durante la invalidación de arriba abajo, se verifica la clausura transitiva del nodo de nivel superior y solo se conservan aquellos nodos cuya clausura transitiva esté limpia. Esto es mejor si el gráfico de nodos es grande, pero la siguiente compilación solo necesita un subconjunto pequeño: 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 el gráfico pequeño de la segunda compilación.

Bazel solo realiza invalidaciones ascendentes.

Para obtener más incrementalidad, Bazel usa la poda de cambios: si un nodo se invalida, pero, cuando se vuelve a compilar, se descubre que su valor nuevo es el mismo que el anterior, los nodos que se invalidaron debido a un cambio en este nodo se “resucitan”.

Esto es útil, por ejemplo, si se cambia un comentario en un archivo C++: el archivo .o que se genera a partir de él será el mismo, por lo que no es necesario volver a llamar al vinculador.

Vinculación o compilación incremental

La principal limitación de este modelo es que la invalidación de un nodo es un asunto de todo o nada: cuando cambia una dependencia, el nodo dependiente siempre se vuelve a compilar desde cero, incluso si existe un algoritmo mejor que mutaría el valor anterior del nodo en función de los cambios. Estos son algunos ejemplos en los que esto sería útil:

  • Vinculación incremental
  • Cuando cambia un solo archivo de clase en un archivo JAR, es posible modificarlo en su lugar en lugar de volver a compilarlo desde cero.

El motivo por el que Bazel no admite estos elementos de manera coherente es doble:

  • Se obtuvieron mejoras de rendimiento limitadas.
  • Dificultad para validar que el resultado de la mutación sea el mismo que el de una compilación limpia, y Google valora las compilaciones que se pueden repetir bit a bit.

Hasta ahora, era posible lograr un rendimiento lo suficientemente bueno si se decomponía un paso de compilación costoso y se lograba una reevaluación parcial de esa manera. Por ejemplo, en una app para Android, puedes dividir todas las clases en varios grupos y dexarlos por separado. De esta manera, si las clases de un grupo no cambian, no se debe volver a realizar el proceso de dexing.

Asignación a conceptos de Bazel

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

  • FileStateValue. El resultado de un lstat(). En el caso de los archivos existentes, la función también calcula 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. Lo usan todos los elementos que se preocupan por el contenido real o la ruta de acceso resuelta de un archivo. Depende del FileStateValue correspondiente y de los symlinks que se deban resolver (como el FileValue para a/b, que necesita la ruta de acceso resuelta de a y la ruta de acceso resuelta de a/b). La distinción entre FileValue y FileStateValue es importante porque este último se puede usar en los casos en que no se necesita el contenido del archivo. Por ejemplo, el contenido del archivo es irrelevante cuando se evalúan los globs del sistema de archivos (como srcs=glob(["*/*.java"])).
  • DirectoryListingStateValue. El resultado de readdir(). Al igual que FileStateValue, este es el nodo de nivel más bajo y no tiene dependencias.
  • DirectoryListingValue. Lo usan todos los elementos que se preocupan por las entradas de un directorio. Depende del DirectoryListingStateValue correspondiente, así como del FileValue asociado del directorio.
  • PackageValue. Representa la versión analizada de un archivo BUILD. Depende del FileValue del archivo BUILD asociado y, también de forma transitiva, de cualquier DirectoryListingValue que se use para resolver los globs en el paquete (la estructura de datos que representa el contenido de un archivo BUILD de forma interna).
  • ConfiguredTargetValue. Representa un objetivo configurado, que es una tupla del conjunto de acciones generadas durante el análisis de un objetivo y la información proporcionada a los objetivos configurados dependientes. Depende del PackageValue en el que se encuentra el destino correspondiente, el ConfiguredTargetValues de las dependencias directas y un nodo especial que representa la configuración de compilación.
  • ArtifactValue. Representa un archivo en la compilación, ya sea un artefacto de origen o de salida. Los artefactos son casi equivalentes a los archivos y se usan para hacer referencia a ellos durante la ejecución real de los pasos de compilación. Los archivos fuente dependen del FileValue del nodo asociado, y los artefactos de salida dependen del ActionExecutionValue de cualquier acción que genere el artefacto.
  • ActionExecutionValue. Representa la ejecución de una acción. Depende del ArtifactValues de sus archivos de entrada. La acción que ejecuta se encuentra dentro de su SkyKey, lo que es contrario al concepto de que las SkyKeys deben ser pequeñas. Ten en cuenta que ActionExecutionValue y ArtifactValue no se usan si no se ejecuta la fase de ejecución.

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

Gráfico de relaciones de implementación de SkyFunction