Es el modelo de evaluación paralela y de incrementalidad de Bazel.
Modelo de datos
El modelo de datos consta de los siguientes elementos:
SkyValue
, también llamados nodosSkyValues
son objetos inmutables que contienen todos los datos creados durante la compilación y las entradas de la compilación. Algunos ejemplos son los archivos de entrada, los archivos de salida, los destinos y los destinos configurados.SkyKey
: Es un nombre corto e inmutable para hacer referencia a unSkyValue
, por ejemplo,FILECONTENTS:/tmp/foo
oPACKAGE://foo
.SkyFunction
: Compila nodos en función de sus claves y nodos dependientes.- Es un 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 framework de evaluación incremental en el que se basa Bazel.
Evaluación
Una compilación se logra evaluando 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 SkyFunction
hasta que se alcanzan los nodos hoja. Por lo general, los nodos hoja son 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 los archivos de salida en el sistema de archivos) y un gráfico 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 saber de antemano todos los nodos que necesita para hacer su trabajo. Un ejemplo simple es evaluar un nodo de archivo de entrada que resulta ser un vínculo simbólico: la función intenta leer el archivo, se da cuenta de que es un vínculo simbólico y, por lo tanto, recupera el nodo del sistema de archivos que representa el destino del vínculo simbólico. Sin embargo, ese archivo puede ser un vínculo simbólico, 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 devuelve su valor; de lo contrario, se devuelvenull
y se espera que la función en sí devuelvanull
. 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 aenv.getValue
devolverá un valor que no seanull
. - Llama a
env.getValues()
para solicitar la evaluación de varios otros nodos. Básicamente, hace lo mismo, pero los nodos dependientes se evalúan en paralelo. - Realizar cálculos durante su invocación
- Tienen 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 de Bazel) son aceptables, mientras que los efectos secundarios de lectura (en los que los datos fluyen hacia adentro de Bazel sin una dependencia registrada) no lo son, ya que son una dependencia no registrada y, como tales, pueden causar compilaciones incrementales incorrectas.
Las implementaciones de SkyFunction
que funcionan correctamente evitan acceder a los datos de cualquier otra manera que no sea solicitando dependencias (por ejemplo, leyendo directamente el sistema de archivos), ya que esto 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 devolver un valor que no sea null
para indicar 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 devuelvan los mismos datos. Si todas las funciones de Sky son determinísticas, significa que toda la compilación también será determinística.
- Incrementabilidad 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í solicitando 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 si dependen 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 necesitan recompilarse: el cierre transitivo inverso del conjunto de archivos de entrada modificados.
En particular, existen dos estrategias posibles de incrementalidad: la ascendente y la descendente. La opción óptima depende de cómo se vea el 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 que se ejecute
stat()
en todos los archivos de entrada de la compilación anterior para determinar si se modificaron. Esto se puede mejorar usandoinotify
o un mecanismo similar para obtener información sobre los archivos modificados.Durante la invalidación de arriba hacia abajo, 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 próxima compilación solo necesita un pequeño subconjunto de él: 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 la invalidación de abajo hacia arriba.
Para obtener una mayor incrementalidad, Bazel usa la poda de cambios: Si se invalida un nodo, pero, tras la recompilación, se descubre que su valor nuevo es el mismo que el anterior, se "resucitan" los nodos que se invalidaron debido a un cambio en este nodo.
Esto es ú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 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 existiera un mejor algoritmo que mutara el valor anterior del nodo en función de los cambios. Estos son algunos ejemplos en los que sería útil:
- Vinculación incremental
- Cuando cambia un solo archivo de clase en un archivo JAR, es posible modificar el archivo JAR in situ en lugar de volver a compilarlo desde cero.
El motivo por el que Bazel no admite estas acciones de forma fundamentada es doble:
- Hubo ganancias de rendimiento limitadas.
- Sería difícil validar que el resultado de la mutación sea el mismo que el de una recompilación limpia, y Google valora las compilaciones que son repetibles bit por 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 dexearlas por separado. De esta manera, si las clases de un grupo no cambian, no es necesario 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 Bazel usa para realizar una compilación:
- FileStateValue. Es 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 usa cualquier elemento que se preocupe por el contenido real o la ruta resuelta de un archivo. Depende del
FileStateValue
correspondiente y de cualquier vínculo simbólico que deba resolverse (como elFileValue
paraa/b
, que necesita la ruta resuelta dea
y la ruta resuelta dea/b
). La distinción entreFileValue
yFileStateValue
es importante porque el último se puede usar en los casos en los que no se necesita el contenido del archivo. Por ejemplo, el contenido del archivo es irrelevante cuando se evalúan los comodines del sistema de archivos (comosrcs=glob(["*/*.java"])
). - DirectoryListingStateValue. Es el resultado de
readdir()
. Al igual queFileStateValue
, 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 delFileValue
asociado del directorio. - PackageValue. Representa la versión analizada de un archivo BUILD. Depende del
FileValue
del archivoBUILD
asociado y, también, de forma transitiva, de cualquierDirectoryListingValue
que se use para resolver los comodines en el paquete (la estructura de datos que representa el contenido de un archivoBUILD
de forma interna). - ConfiguredTargetValue. Representa un destino configurado, que es una tupla del conjunto de acciones generadas durante el análisis de un destino y la información proporcionada a los destinos configurados dependientes. Depende del
PackageValue
en el que se encuentra el destino correspondiente, elConfiguredTargetValues
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 los archivos 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 delActionExecutionValue
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 contradice el concepto de que las SkyKeys deben ser pequeñas. Ten en cuenta queActionExecutionValue
yArtifactValue
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: