Skyframe

Informar un problema Ver fuente Por la noche · 7.4 de Google Cloud. 7.3 · 7.2 · 7.1 · 7.0 · 6.5

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. Algunos ejemplos son: archivos de entrada, archivos de salida, destinos y destinos configurados.
  • 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

Una compilación consiste en evaluar el nodo que representa la solicitud de compilación (este es el estado que deseamos, pero hay mucho código heredado en el camino). Primero, se encuentra su SkyFunction y se lo llama con 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 invocaciones de funciones, y así sucesivamente, hasta que se alcanzan los nodos hoja (que suelen ser nodos que representan archivos de entrada en el sistema de archivos). Por último, obtenemos 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 que participaron 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 el compilador de nodos original, pero esta vez la misma llamada a env.getValue mostrará un valor que no es null.
  • Llama a env.getValues() para solicitar la evaluación de muchos otros nodos. Básicamente, 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 no se pisen entre sí. En general, están bien 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 no deben acceder a los datos de ninguna otra manera que no sea solicitando dependencias (como leyendo 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 tenga suficientes datos para realizar su trabajo, debería mostrar un valor distinto de null que indique la finalización.

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 muestren los mismos datos. Si todas las funciones de Sky son determinísticas, 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í mediante la solicitud de dependencias, las funciones que no dependen unas de otras pueden ejecutarse en paralelo, y Bazel puede garantizar que el resultado sea el mismo que si se ejecutaran secuencialmente.

Incrementalidad

Dado que las funciones solo pueden acceder a los datos de entrada dependiendo de otros nodos, Bazel puede crear 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 se deben volver a compilar: el cierre transitivo inverso del conjunto de archivos de entrada modificados.

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

  • 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 sabemos que se volverá a compilar el mismo nodo de nivel superior. Ten en cuenta que la invalidación de abajo hacia arriba requiere ejecutar stat() en todos los archivos de entrada de la compilación anterior para determinar si se modificaron. Esto se puede mejorar usando inotify o un mecanismo similar para conocer 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 sabemos que el gráfico de nodos actual es grande, pero solo necesitamos un subconjunto pequeño en la siguiente compilación: 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.

Por el momento, solo realizamos invalidaciones ascendentes.

Para obtener más incrementalidad, usamos la poda de cambios: si se invalida un nodo, 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 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 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 una dependencia cambia, el nodo dependiente siempre se reconstruye desde cero, incluso si existiera un algoritmo mejor que mutara 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 .class en un .jar, en teoría, podríamos modificar el archivo .jar en lugar de volver a compilarlo desde cero.

El motivo por el que Bazel actualmente no admite estos elementos de manera coherente (tenemos cierta compatibilidad con la vinculación incremental, pero no está implementada en Skyframe) es doble: solo tuvimos mejoras de rendimiento limitadas y fue difícil garantizar que el resultado de la mutación sea el mismo que el de una compilación limpia. Además, Google valora las compilaciones que se pueden repetir bit a bit.

Hasta ahora, siempre podíamos lograr un rendimiento lo suficientemente bueno simplemente descomponiendo un paso de compilación costoso y logrando una reevaluación parcial de esa manera: divide todas las clases de una app en varios grupos y las dexifica por separado. De esta manera, si las clases de un grupo no cambian, no es necesario volver a realizar el proceso de dexing.

Asigna a conceptos de Bazel

Esta es una descripción general aproximada de algunas de las implementaciones de SkyFunction que usa Bazel para realizar una compilación:

  • FileStateValue Es el resultado de un lstat(). En el caso de los archivos existentes, también procesamos información adicional para detectar cambios en el archivo. Se trata del nodo de nivel más bajo del gráfico 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 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 FileStateValue es importante porque, en algunos casos (por ejemplo, la evaluación de globs de sistemas de archivos, como srcs=glob(["*/*.java"])), el contenido del archivo no es realmente necesario.
  • DirectoryListingValue. Básicamente, el resultado de readdir(). Depende del FileValue asociado con el 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 que dependen de este. 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 artefactos 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). En el caso de los archivos fuente, depende del FileValue del nodo asociado. En el caso de los artefactos de salida, depende 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. Actualmente, la acción que ejecuta se encuentra dentro de su tecla de cielo, lo que va en contra del concepto de que las teclas de cielo deben ser pequeñas. Estamos trabajando para resolver esta discrepancia (ten en cuenta que ActionExecutionValue y ArtifactValue no se usan si no ejecutamos la fase de ejecución en Skyframe).