O modelo de avaliação paralela e incrementabilidade do Bazel.
Modelo de dados
O modelo de dados consiste nos seguintes itens:
SkyValue
: Também chamados de nós.SkyValues
são objetos imutáveis que contêm todos os dados criados ao longo do build e as entradas o build. Exemplos: arquivos de entrada, de saída, destinos e arquivos de destino.SkyKey
: Um nome curto e imutável para se referir a umSkyValue
, por exemplo,FILECONTENTS:/tmp/foo
ouPACKAGE://foo
.SkyFunction
: Cria nós com base nas chaves e nos nós dependentes.- Gráfico de nós. Estrutura de dados contendo a relação de dependência entre nós.
Skyframe
: Codinome do framework de avaliação incremental Bazel é com base.
Avaliação
Um build é alcançado avaliando o nó que representa a solicitação de build.
Primeiro, o Bazel encontra a SkyFunction
correspondente à chave do
SkyKey
. Em seguida, a função solicita a avaliação dos nós necessários para
avaliar o nó de nível superior, que, por sua vez, resulta em outras chamadas SkyFunction
;
até que os nós de folha sejam atingidos. Os nós folha geralmente são aqueles que representam
arquivos de entrada no sistema de arquivos. Por fim, o Bazel termina com o valor
SkyValue
de nível superior, alguns efeitos colaterais (como arquivos de saída no arquivo
sistema) e um gráfico acíclico dirigido das dependências entre os nós
envolvidas no build.
Um SkyFunction
pode solicitar SkyKeys
em vários cartões se não puder informar
avançar todos os nós necessários para realizar o trabalho. Um exemplo simples é avaliar
um nó de arquivo de entrada que acaba sendo um link simbólico: a função tenta ler
o arquivo, percebe que é um link simbólico e, assim, busca o nó do sistema de arquivos
que representa o destino do link simbólico. Mas ele em si pode ser um link simbólico,
caso em que a função original também precisará buscar seu destino.
As funções são representadas no código pela interface SkyFunction
e pelo
fornecidos a ele por uma interface chamada SkyFunction.Environment
. Esses
são o que as funções podem fazer:
- Solicite a avaliação de outro nó chamando
env.getValue
. Se o nó estiver disponível, o valor dele será retornado. Caso contrário,null
será retornado. e que a própria função vai retornarnull
. No último caso, o nó dependente é avaliado, e o criador de nó original é invocado novamente, mas dessa vez a mesma chamadaenv.getValue
retornará uma valor diferente denull
. - Solicite a avaliação de vários outros nós chamando
env.getValues()
. Esse processo faz basicamente a mesma coisa, exceto que os nós dependentes são são avaliadas em paralelo. - Fazer cálculos durante a invocação
- Têm efeitos colaterais, como a gravação de arquivos no sistema de arquivos. Necessidades de atendimento duas funções diferentes evitem pisar na direção uma da outra os dedos dos pés. Em geral, criar efeitos colaterais, em que os dados fluem para fora do Bazel ler efeitos colaterais, em que os dados fluem para dentro do Bazel sem uma registrada) não são, porque são uma dependência não registrada. e, por isso, pode causar builds incrementais incorretos.
As implementações bem comportadas de SkyFunction
evitam acessar dados de outra forma
de solicitar dependências (como ler diretamente o sistema de arquivos),
porque isso faz com que o Bazel não registre a dependência de dados no arquivo.
que foi lido, resultando em builds incrementais incorretos.
Quando uma função tiver dados suficientes para fazer seu trabalho, ela retornará um não null
que indica a conclusão.
Essa estratégia de avaliação tem vários benefícios:
- Hermeticidade. Se as funções solicitam apenas dados de entrada dependendo outros nós, o Bazel pode garantir que, se o estado de entrada for o mesmo, o são retornados os mesmos dados. Se todas as funções do céu são determinísticas, isso significa que todo o build também será determinístico.
- Incrementabilidade correta e perfeita. Se todos os dados de entrada de todas as funções é gravado, o Bazel pode invalidar apenas o conjunto exato de nós que precisa ser invalidado quando os dados de entrada mudarem.
- Paralelismo. Como as funções só podem interagir entre si por meio de solicitações de dependências, as funções que não dependem umas das outras podem ser são executadas em paralelo, e o Bazel pode garantir que o resultado seja o mesmo em que elas foram executadas sequencialmente.
Incrementality
Como as funções só podem acessar dados de entrada dependendo de outros nós, o Bazel pode construir um gráfico de fluxo de dados completo, desde os arquivos de entrada até o e usar essas informações para recriar somente os nós que realmente precisam a ser reconstruído: o fechamento transitivo reverso do conjunto de arquivos de entrada alterados.
Existem duas estratégias de incrementabilidade possíveis: a de baixo para cima, e de cima para baixo. A escolha ideal depende de como o gráfico de dependências como é.
Durante a invalidação de baixo para cima, depois que um gráfico é criado e o conjunto de valores for conhecida, todos os nós serão invalidados e dependem transitivamente arquivos alterados. Isso é ideal se o mesmo nó de nível superior for criado de novo. A invalidação de baixo para cima requer a execução de
stat()
em todos arquivos de entrada do build anterior para determinar se eles foram alterados. Isso pode ser melhorada usandoinotify
ou um mecanismo semelhante para conhecer melhor arquivos alterados.Durante a invalidação de cima para baixo, o fechamento transitivo do nó de nível superior é verificado e apenas os nós são mantidos quando o fechamento transitivo está limpo. É melhor se o gráfico de nós for grande, mas o próximo build só precisar de uma um pequeno subconjunto dela: a invalidação de baixo para cima invalidaria o gráfico maior. da primeira versão, ao contrário da invalidação de cima para baixo, que analisa as pequenas da segunda versão.
O Bazel só faz a invalidação de baixo para cima.
Para aumentar ainda mais a incrementabilidade, o Bazel usa a remoção de alterações: se um nó é invalidado, mas após a recriação, é descoberto que seu novo valor é o mesmo como o valor antigo, os nós que foram invalidados devido a uma alteração neste nó são "ressuscitados".
Isso é útil, por exemplo, se alguém alterar um comentário em um arquivo C++:
O arquivo .o
gerado por ele será o mesmo, portanto, não será necessário chamar
o vinculador novamente.
Compilação / vinculação incremental
A principal limitação deste modelo é que a invalidação de um nó é um tudo ou nada: quando uma dependência muda, o nó dependente é sempre reconstruído do zero, mesmo que existisse um algoritmo melhor que poderia mudar o valor antigo do nó com base nas alterações. Alguns exemplos de como isso ser úteis:
- Vinculação incremental
- Quando um único arquivo de classe muda em um arquivo JAR, é possível modificar o arquivo JAR no local em vez de criá-lo do zero novamente.
Por que o Bazel não dá suporte a essas coisas de acordo com os princípios. é duplo:
- Houve ganhos de performance limitados.
- Dificuldade em validar se o resultado da mutação é o mesmo que esse de uma reconstrução limpa seria, e o Google valoriza builds bit a bit repetíveis.
Até agora, era possível ter um desempenho bom o suficiente decompor um uma etapa cara de build e conseguir uma reavaliação parcial dessa forma. Por exemplo: em um app Android, você pode dividir todas as turmas em vários grupos e dex separadamente. Dessa forma, se as classes de um grupo não forem alteradas, a dexação precisam ser refeitas.
Como mapear para conceitos do Bazel
Este é um resumo de alto nível das principais SkyFunction
e SkyValue
implementações que o Bazel usa para executar uma compilação:
- FileStateValue de valor. O resultado de um
lstat()
. Para arquivos existentes, a também calcula informações adicionais para detectar alterações em o arquivo. Este é o nó de nível mais baixo no gráfico Skyframe e não tem dependências. - FileValue. Usados por qualquer coisa que se importe com o conteúdo real ou
caminho resolvido de um arquivo. Depende do
FileStateValue
correspondente e todos os links simbólicos que precisam ser resolvidos (comoFileValue
paraa/b
) precisa do caminho resolvido dea
e do caminho resolvido dea/b
). O a distinção entreFileValue
eFileStateValue
é importante porque A segunda opção pode ser usada nos casos em que o conteúdo do arquivo não é realmente necessárias. Por exemplo, o conteúdo do arquivo é irrelevante quando avaliar globs do sistema de arquivos (comosrcs=glob(["*/*.java"])
). - DirectoryListingStateValue associado. Resultado de
readdir()
. GosteiFileStateValue
, é o nó de nível mais baixo e não tem dependências. - DirectoryListingValue associado. Usado por qualquer coisa que se importe com as entradas de
em um diretório. Depende do
DirectoryListingStateValue
correspondente, já que bem como oFileValue
associado do diretório. - PackageValue para código. Representa a versão analisada de um arquivo BUILD. Depende de
o
FileValue
do arquivoBUILD
associado e também transitivamente em qualquerDirectoryListingValue
, que é usado para resolver os globs no pacote. (a estrutura de dados que representa o conteúdo interno de um arquivoBUILD
). - ConfiguredTargetValue. Representa um destino configurado, que é uma tupla
conjunto de ações geradas durante a análise de um alvo e
informações fornecidas para destinos dependentes configurados. Depende do
PackageValue
a meta correspondente está, aConfiguredTargetValues
de dependências diretas e um nó especial que representa o build configuração do Terraform. - ArtifactValue. Representa um arquivo na versão, seja ele uma origem ou um
artefato de saída. Os artefatos são quase equivalentes aos arquivos e são usados para
consulte os arquivos durante a execução real das etapas de build. Arquivos de origem
depende do
FileValue
do nó associado, e os artefatos de saída depender doActionExecutionValue
de qualquer ação que gere o artefato. - ActionExecutionValue (em inglês). Representa a execução de uma ação. Depende de
o
ArtifactValues
dos arquivos de entrada. A ação que ele executa está contida da SkyKey, o que é contrário ao conceito de que as SkyKeys pequeno.ActionExecutionValue
eArtifactValue
não serão usados se a fase de execução não é executada.
Como auxílio visual, este diagrama mostra as relações entre Implementações do SkyFunction após uma versão do Bazel: