Skyframe

Informar um problema Acessar fonte

A avaliação paralela e o modelo de 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 dele. Exemplos: arquivos de entrada, arquivos de saída, destinos e destinos configurados.
  • SkyKey. Um nome curto e imutável para fazer referência 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 que contém a relação de dependência entre nós.
  • Skyframe: codinome do framework de avaliação incremental em que o Bazel é baseado.

Avaliação

Uma versão é alcançada avaliando o nó que representa a solicitação de versão.

Primeiro, o Bazel encontra o SkyFunction correspondente à chave do SkyKey de nível superior. Em seguida, a função solicita a avaliação dos nós necessários para avaliar o nó de nível superior, o que, por sua vez, resulta em outras chamadas SkyFunction, até que os nós de folha sejam alcançados. Os nós de folha geralmente são aqueles que representam arquivos de entrada no sistema de arquivos. Por fim, o Bazel acaba com o valor do SkyValue de nível superior, alguns efeitos colaterais (como arquivos de saída no sistema de arquivos) e um gráfico acíclico direcionado das dependências entre os nós envolvidos na compilação.

Um SkyFunction pode solicitar SkyKeys em várias transmissões se não conseguir informar antecipadamente todos os nós necessários para realizar o trabalho. Um exemplo simples é avaliar um nó de arquivo de entrada que é um link simbólico: a função tenta ler o arquivo, percebe que é um link simbólico e busca o nó do sistema de arquivos que representa o destino do link simbólico. Mas ele pode ser um link simbólico. Nesse caso, a função original também vai precisar buscar o destino.

As funções são representadas no código pela interface SkyFunction e os serviços fornecidos a ela por uma interface chamada SkyFunction.Environment. Estas são as coisas 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 a própria função retornará null. No último caso, o nó dependente é avaliado e o builder de nós original é invocado novamente, mas desta vez a mesma chamada env.getValue retorna um valor não null.
  • Solicite a avaliação de vários outros nós chamando env.getValues(). Isso faz basicamente o mesmo, exceto que os nós dependentes são avaliados em paralelo.
  • Fazer computação durante a invocação
  • ter efeitos colaterais, como gravar arquivos no sistema de arquivos. É preciso ter cuidado para que duas funções diferentes evitem pisar no pé uma da outra. Em geral, os efeitos colaterais de gravação (em que os dados fluem para fora do Bazel) são corretos, mas os efeitos colaterais de leitura (em que os dados fluem para o Bazel sem uma dependência registrada) não são porque são uma dependência não registrada e podem causar builds incrementais incorretos.

Implementações SkyFunction bem comportadas evitam o acesso a dados de qualquer outra maneira que não seja solicitar dependências (como lendo 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 realizar seu job, ela retornará um valor diferente de null, indicando a conclusão.

Essa estratégia de avaliação tem vários benefícios:

  • Hermética. Se as funções solicitam apenas dados de entrada dependendo de outros nós, o Bazel pode garantir que, se o estado de entrada for o mesmo, os mesmos dados serão retornados. Se todas as funções do céu forem deterministas, 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 forem registrados, o Bazel poderá invalidar apenas o conjunto exato de nós que precisam ser invalidados quando os dados de entrada forem alterados.
  • Paralelismo. Como as funções só podem interagir entre si por meio de solicitação de dependências, funções que não dependem umas das outras podem ser executadas em paralelo, e o Bazel pode garantir que o resultado seja o mesmo que se tivessem sido executadas em sequência.

Incrementabilidade

Como as funções só podem acessar dados de entrada dependendo de outros nós, o Bazel pode criar um gráfico de fluxo de dados completo dos arquivos de entrada para os arquivos de saída e usar essas informações para recriar apenas os nós que realmente precisam ser recompilados: 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 a de cima para baixo. Qual deles é ideal depende da aparência do gráfico de dependências.

  • Durante a invalidação, depois que um gráfico é criado e o conjunto de entradas alteradas é conhecido, todos os nós são invalidados e dependem transitivamente dos arquivos alterados. Isso é ideal quando o mesmo nó de nível superior é criado novamente. A invalidação ascendente requer a execução de stat() em todos os arquivos de entrada do build anterior para determinar se eles foram modificados. Isso pode ser melhorado com o uso de inotify ou um mecanismo semelhante para saber mais sobre os 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 cujo fechamento transitivo está limpo. Isso é melhor se o gráfico de nós for grande, mas o próximo build precisa apenas de um pequeno subconjunto dele: a invalidação de baixo para cima invalidaria o gráfico maior do primeiro build, ao contrário da invalidação de cima para baixo, que apenas analisa o pequeno gráfico do segundo build.

O Bazel só realiza a invalidação de baixo para cima.

Para ter mais incrementabilidade, o Bazel usa a remoção de mudanças: se um nó for invalidado, mas após a recompilação, descobrir que o novo valor é o mesmo que o antigo, os nós que foram invalidados devido a uma mudança nesse nó são "restabelecidos".

Isso é útil, por exemplo, se alguém alterar um comentário em um arquivo C++: o arquivo .o gerado a partir dele será o mesmo. Portanto, não é necessário chamar o vinculador novamente.

Compilação / vinculação incremental

A principal limitação desse modelo é que a invalidação de um nó é uma questão de tudo ou nada. Quando uma dependência muda, o nó dependente é sempre recriado do zero, mesmo que exista um algoritmo melhor que mude o valor antigo do nó com base nas mudanças. Alguns exemplos em que isso seria útil:

  • 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.

Há dois motivos para o Bazel não oferecer suporte a esses itens de maneira consistente:

  • Os ganhos de desempenho foram limitados.
  • Dificuldade de validar se o resultado da mutação é o mesmo de uma reconstrução limpa seria, e os valores do Google seriam builds bit a bit repetíveis.

Até agora, era possível alcançar um bom desempenho decompondo uma etapa de build cara e realizando uma reavaliação parcial dessa maneira. Por exemplo, em um app Android, você pode dividir todas as classes em vários grupos e fazer a dex separadamente. Dessa forma, se as classes de um grupo não forem alteradas, a dexação não precisará ser refeita.

Mapeamento para conceitos do Bazel

Este é um resumo detalhado das principais implementações de SkyFunction e SkyValue que o Bazel usa para executar um build:

  • FileStateValue (em inglês). O resultado de uma lstat(). Para arquivos existentes, a função também calcula informações adicionais a fim de detectar alterações no arquivo. Esse é o nó de nível mais baixo no gráfico do Skyframe e não tem dependências.
  • FileValue. Usado por qualquer elemento que se importe com o conteúdo real ou o caminho resolvido de um arquivo. Depende do FileStateValue correspondente e quaisquer links simbólicos que precisem ser resolvidos (como FileValue para a/b precisa do caminho resolvido de a e do caminho resolvido de a/b). A distinção entre FileValue e FileStateValue é importante porque o último pode ser usado nos casos em que o conteúdo do arquivo não é realmente necessário. Por exemplo, o conteúdo do arquivo é irrelevante ao avaliar globs do sistema de arquivos (como srcs=glob(["*/*.java"])).
  • DirectoryListingStateValue. O resultado de readdir(). Como FileStateValue, esse é o nó de nível mais baixo e não tem dependências.
  • DirectoryListingValue (em inglês). Usado por qualquer coisa que se importe com as entradas de um diretório. Depende do DirectoryListingStateValue correspondente, bem como do FileValue associado do diretório.
  • PackageValue. Representa a versão analisada de um arquivo BUILD. Depende do FileValue do arquivo BUILD associado e também transitivamente de qualquer DirectoryListingValue usado para resolver os globs no pacote (a estrutura de dados que representa internamente o conteúdo de um arquivo BUILD).
  • ConfiguredTargetValue. Representa um destino configurado, que é uma tupla do conjunto de ações geradas durante a análise de um destino e das informações fornecidas aos destinos configurados dependentes. Depende da PackageValue em que o destino correspondente está, do ConfiguredTargetValues das dependências diretas e de um nó especial que representa a configuração do build.
  • ArtifactValue. Representa um arquivo no build, seja ele uma origem ou um artefato de saída. Os artefatos são quase equivalentes aos arquivos e são usados para se referir a arquivos durante a execução real das etapas de build. Os arquivos de origem dependem da FileValue do nó associado, e os artefatos de saída dependem do ActionExecutionValue de qualquer ação que gere o artefato.
  • ActionExecutionValue (em inglês). Representa a execução de uma ação. Depende do ArtifactValues dos arquivos de entrada. A ação executada está contida dentro da SkyKey, o que é diferente do conceito de que elas são pequenas. Observe que ActionExecutionValue e ArtifactValue não serão usados se a fase de execução não for executada.

Como auxílio visual, este diagrama mostra as relações entre as implementações do SkyFunction após uma compilação do próprio Bazel:

Gráfico das relações de implementação do SkyFunction