Skyframe

Informar um problema Ver a fonte Nightly · 8.0 7.4 . 7.3 · 7.2 · 7.1 · 7.0 · 6.5

Modelo de avaliação paralela e incrementalidade 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 do build. Exemplos: arquivos de entrada, arquivos de saída, destinos e destinos configurados.
  • SkyKey: um nome curto imutável para fazer referência a uma 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ó. Uma estrutura de dados que contém a relação de dependência entre nós.
  • Skyframe: nome de código do framework de avaliação incremental em que o Bazel se baseia.

Avaliação

Um build é alcançado avaliando o nó que representa a solicitação de build.

Primeiro, o Bazel encontra o SkyFunction correspondente à chave do SkyKey de nível superior. A função solicita a avaliação dos nós necessários para avaliar o nó de nível superior, o que resulta em outras chamadas SkyFunction, até que os nós de folha sejam alcançados. Os nós de folha geralmente representam arquivos de entrada no sistema de arquivos. Por fim, o Bazel termina 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 dirigido das dependências entre os nós envolvidos no build.

Uma SkyFunction pode solicitar SkyKeys em várias passagens se não for possível informar com antecedência todos os nós necessários para fazer 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 ele é um link simbólico e, portanto, 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 precisa buscar o destino.

As funções são representadas no código pela interface SkyFunction e pelos 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 função em si deverá retornar null. Nesse último caso, o nó dependente é avaliado, e o builder de nó original é invocado novamente, mas desta vez a mesma chamada env.getValue vai retornar um valor que não seja 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 a computação durante a invocação
  • Ter efeitos colaterais, por exemplo, gravar arquivos no sistema de arquivos. É necessário cuidado para que duas funções diferentes evitem pisar umas nas outras. Em geral, os efeitos colaterais de gravação (em que os dados fluem para fora do Bazel) são aceitáveis, mas os efeitos colaterais de leitura (em que os dados fluem para dentro do Bazel sem uma dependência registrada) não são, porque são uma dependência não registrada e, como tal, podem causar builds incrementais incorretos.

Implementações SkyFunction bem comportadas evitam acessar dados de qualquer outra maneira que não seja solicitando 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 fazer o trabalho, ela precisará retornar um valor que não seja null que indique a conclusão.

Essa estratégia de avaliação tem várias vantagens:

  • Hermética. Se as funções solicitarem apenas dados de entrada dependendo de outros nós, o Bazel poderá garantir que, se o estado de entrada for o mesmo, os mesmos dados serão retornados. Se todas as funções do Sky forem deterministas, todo o build também será determinista.
  • 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 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 executadas em paralelo, e o Bazel pode garantir que o resultado será o mesmo se elas forem executadas sequencialmente.

Incrementality

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 recriados: o fechamento transitivo reverso do conjunto de arquivos de entrada alterados.

Existem duas estratégias de incrementalidade possíveis: bottom-up e top-down. Qual delas é a ideal depende da aparência do gráfico de dependência.

  • Durante a invalidação bottom-up, depois que um gráfico é criado e o conjunto de entradas alteradas é conhecido, todos os nós são invalidados que dependem transitivamente de arquivos alterados. Isso é ideal se o mesmo nó de nível superior for criado novamente. A invalidação bottom-up exige a execução de stat() em todos os arquivos de entrada do build anterior para determinar se eles foram alterados. Isso pode ser melhorado usando 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 com fechamento transitivo limpo são mantidos. Isso é melhor se o gráfico de nós for grande, mas o próximo build precisar apenas de um pequeno subconjunto dele: a invalidação bottom-up invalidaria o gráfico maior do primeiro build, ao contrário da invalidação top-down, que apenas percorre o gráfico pequeno do segundo build.

O Bazel só faz invalidação bottom-up.

Para conseguir mais incrementalidade, o Bazel usa a redução de mudanças: se um nó é invalidado, mas, após a recriação, é descoberto que o novo valor é o mesmo que o antigo, os nós que foram invalidados devido a uma mudança nesse nó são "ressuscitados".

Isso é útil, por exemplo, se alguém mudar 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.

Vinculação / compilação incremental

A principal limitação desse modelo é que a invalidação de um nó é um assunto de tudo ou nada: quando uma dependência muda, o nó dependente é sempre reconstruído do zero, mesmo que um algoritmo melhor exista para mudar o valor antigo do nó com base nas mudanças. Confira 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.

O Bazel não oferece suporte a essas coisas de forma princípio por dois motivos:

  • Houve ganhos limitados de performance.
  • Dificuldade para validar que o resultado da mutação é o mesmo de uma reconstrução limpa, e o Google valoriza builds que são repetíveis bit a bit.

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

Como mapear para conceitos do Bazel

Este é um resumo de alto nível das principais implementações de SkyFunction e SkyValue que o Bazel usa para realizar um build:

  • FileStateValue. O resultado de uma lstat(). Para arquivos existentes, a função também calcula outras informações para 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 coisa que se preocupe com o conteúdo real ou o caminho resolvido de um arquivo. Depende do FileStateValue correspondente e de todos os links simbólicos que precisam ser resolvidos (como o 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 em casos em que o conteúdo do arquivo não é necessário. Por exemplo, o conteúdo do arquivo é irrelevante ao avaliar globais de 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. Usado por qualquer coisa que se preocupe 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 da 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 o conteúdo de um arquivo BUILD internamente.
  • ConfiguredTargetValue. Representa um destino configurado, que é um tupla do conjunto de ações geradas durante a análise de um destino e informações fornecidas a destinos configurados dependentes. Depende do PackageValue em que o destino correspondente está, do ConfiguredTargetValues de dependências diretas e de um nó especial que representa a configuração do build.
  • ArtifactValue. Representa um arquivo no build, seja uma origem ou um artefato de saída. Os artefatos são quase equivalentes a arquivos e são usados para se referir a arquivos durante a execução real das etapas de build. Os arquivos de origem dependem do FileValue do nó associado, e os artefatos de saída dependem do ActionExecutionValue de qualquer ação que gere o artefato.
  • ActionExecutionValue. Representa a execução de uma ação. Depende do ArtifactValues dos arquivos de entrada. A ação executada está contida na SkyKey, o que é contrário ao conceito de que as SkyKeys precisam ser pequenas. ActionExecutionValue e ArtifactValue não são usados se a fase de execução não for executada.

Como ajuda visual, este diagrama mostra as relações entre as implementações da SkyFunction após um build do próprio Bazel:

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