Skyframe

Relatar um problema Conferir código-fonte Por noite · 7,3 · 7,2 · 7,1 · 7,0 · 6,5

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 um SkyValue, por exemplo, FILECONTENTS:/tmp/foo ou PACKAGE://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 retornar null. No último caso, o nó dependente é avaliado, e o criador de nó original é invocado novamente, mas dessa vez a mesma chamada env.getValue retornará uma valor diferente de null.
  • 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 usando inotify 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 (como FileValue para a/b) precisa do caminho resolvido de a e do caminho resolvido de a/b). O a distinção entre FileValue e FileStateValue é 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 (como srcs=glob(["*/*.java"])).
  • DirectoryListingStateValue associado. Resultado de readdir(). Gostei FileStateValue, é 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 o FileValue associado do diretório.
  • PackageValue para código. Representa a versão analisada de um arquivo BUILD. Depende de o FileValue do arquivo BUILD associado e também transitivamente em qualquer DirectoryListingValue, que é usado para resolver os globs no pacote. (a estrutura de dados que representa o conteúdo interno de um arquivo BUILD).
  • 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á, a ConfiguredTargetValues 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 do ActionExecutionValue 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 e ArtifactValue 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:

Um gráfico de relações de implementação do SkyFunction