Skyframe

Informar un problema Ver fuente Noche /}1}

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 a lo largo de la compilación y las entradas de esta. Por ejemplo: archivos de entrada, archivos de salida, destinos y destinos configurados.
  • SkyKey: Es un nombre inmutable y corto que hace 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 los nodos.
  • Skyframe. Es el nombre interno en el que se basa Bazel para el framework de evaluación incremental.

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 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 de 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 SkyValue de nivel superior, algunos efectos secundarios (como 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 determinar por adelantado todos los nodos que necesita para hacer su trabajo. Un ejemplo simple 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 se le proporcionan mediante 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 misma muestre null. En este último caso, se evalúa el nodo dependiente y, luego, se vuelve a invocar el compilador de nodos original, pero esta vez la misma llamada a env.getValue mostrará un valor distinto de null.
  • Llama a env.getValues() para solicitar la evaluación de muchos otros nodos. En esencia, esto hace lo mismo, excepto que los nodos dependientes se evalúan en paralelo.
  • Hacer cálculos durante la invocación
  • Tener efectos secundarios, por ejemplo, escribir archivos en el sistema Se debe tener cuidado de que dos funciones diferentes eviten pisarse de pie la una de la otra. En general, son correctos los efectos secundarios de escritura (en los que los datos fluyen hacia afuera desde Bazel), no los efectos secundarios de lectura (en los que los datos fluyen hacia Bazel sin una dependencia registrada), ya que son una dependencia no registrada y, por lo tanto, pueden causar compilaciones incrementales incorrectas.

Las implementaciones de SkyFunction que se comportan correctamente evitan el acceso a los datos de otra manera que no sea solicitar dependencias (por ejemplo, leer directamente el sistema de archivos), ya que, como resultado, Bazel no registra la dependencia de datos en el archivo leído y, por lo tanto, se generan compilaciones incrementales incorrectas.

Una vez que una función tiene suficientes datos para realizar su trabajo, debe mostrar un valor distinto de null que indique 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 muestran los mismos datos. Si todas las funciones de Sky son deterministas, esto significa que toda la compilación también será determinista.
  • 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í mediante la solicitud de dependencias, las funciones que no dependen unas de otras 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 crear un grafo de flujo de datos completo desde los archivos de entrada hasta los archivos de salida y usar esta información para reconstruir solo los nodos que realmente deben volver a compilarse: el cierre transitivo inverso del conjunto de archivos de entrada modificados.

En particular, existen dos estrategias de incrementalidad posibles: la de arriba abajo y la de arriba abajo. La opción óptima depende de cómo se vea el gráfico de dependencia.

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

  • Durante la invalidación descendente, se verifica el cierre transitivo del nodo de nivel superior y solo se conservan aquellos nodos cuyo cierre transitivo está limpio. Esto es mejor si el gráfico de nodos es grande, pero la siguiente compilación solo necesita un pequeño subconjunto de él: la invalidación ascendente invalidaría el grafo más grande de la primera compilación, a diferencia de la invalidación de arriba hacia abajo, que solo recorre el gráfico pequeño 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 invalida, pero, luego de volver a compilar, se descubre que su valor nuevo es el mismo que su valor anterior, los nodos que se invalidaron debido a un cambio en este nodo se “resucitarán”.

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

Vinculación / 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 una dependencia cambia, el nodo dependiente siempre se reconstruye desde cero, incluso si existiera 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 un solo archivo de clase cambia en un archivo JAR, es posible modificarlo en su lugar en lugar de compilarlo desde cero.

Bazel no admite estas cosas de manera de principios por dos motivos:

  • El rendimiento fue limitado.
  • La dificultad para validar que el resultado de la mutación es el mismo que el de una recompilación limpia sería, y las compilaciones de valores de Google que son repetibles bit a bit.

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

Asigna a conceptos de Bazel

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

  • FileStateValue Es el resultado de un lstat(). Para archivos existentes, la función también procesa información adicional con el fin de detectar cambios en el archivo. Este es el nodo de nivel más bajo del grafo de Skyframe y no tiene dependencias.
  • FileValue predeterminado Lo usa todo lo que se preocupa por el contenido real o la ruta de acceso resuelta de un archivo. Depende del FileStateValue correspondiente y de cualquier symlink que se deba resolver (como FileValue para a/b necesita la ruta resuelta de a y la ruta resuelta de a/b). La distinción entre FileValue y FileStateValue es importante porque esta última se puede usar en casos en los que el contenido del archivo no sea realmente necesario. 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 usa todo lo que se preocupa 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 internamente).
  • ConfiguredTargetValue. Representa un destino configurado, que es una tupla del conjunto de acciones generadas durante el análisis de un objetivo y la información proporcionada a 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 asociado. Representa un archivo en la compilación, ya sea una fuente o un artefacto de salida. Los artefactos son casi equivalentes a los archivos y se usan para hacer referencia a estos durante la ejecución real de los pasos de compilación. Los archivos de origen dependen del FileValue del nodo asociado, y los artefactos de salida dependen del ActionExecutionValue de cualquier acción que genere el artefacto.
  • ActionExecutionValue predeterminado. Representa la ejecución de una acción. Depende del ArtifactValues de sus archivos de entrada. La acción que ejecuta está incluida en su SkyKey, lo que contradice el concepto de que SkyKeys debería ser pequeño. Ten en cuenta que ActionExecutionValue y ArtifactValue no se usan si no se ejecuta la fase de ejecución.

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

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