Skyframe

Informar um problema Mostrar fonte Por noite · 7,4 do Google. 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 do build. Exemplos: arquivos de entrada, arquivos de saída, destinos e destinos configurados.
  • 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: nome de código do framework de avaliação incremental em que o Bazel se baseia.

Avaliação

Um build consiste em avaliar o nó que representa a solicitação de build. Esse é o estado que queremos, mas há muito código legado no caminho. Primeiro, o SkyFunction é encontrado e chamado com a 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, que, por sua vez, resulta em outras invocações de função e assim por diante, até que os nós folhas sejam alcançados (que geralmente são nós que representam arquivos de entrada no sistema de arquivos). Por fim, temos 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.

Um SkyFunction pode solicitar SkyKeys em várias passagens se não puder 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, e, 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 os serviços fornecidos a ela por uma interface chamada SkyFunction.Environment. As funções podem fazer o seguinte:

  • 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. No último caso, o nó dependente é avaliado e, em seguida, 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 é basicamente o mesmo, exceto que os nós dependentes são avaliados em paralelo.
  • Fazer cálculos durante a invocação
  • Têm efeitos colaterais, como a gravação de arquivos no sistema de arquivos. É preciso tomar cuidado para que duas funções diferentes não pisem na ponta da outra. Em geral, não há problemas com os efeitos colaterais de gravação (em que os dados fluem do Bazel para fora do Bazel). Já os efeitos colaterais de leitura, quando 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, por isso, podem causar builds incrementais incorretos.

As implementações de SkyFunction não podem acessar dados de outra maneira que não seja a solicitação de dependências, como a leitura direta do sistema de arquivos. Isso faz com que o Bazel não registre a dependência de dados no arquivo lido, o que resulta 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 indicando a conclusão.

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

  • Hermeticidade. Se as funções solicitarem dados de entrada apenas 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 céu forem determinísticas, isso significa que toda a construção 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 seja o mesmo que seria executado em sequência.

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 incrementabilidade possíveis: a de baixo para cima e a de cima para baixo. A escolha ideal depende da aparência do gráfico de dependências.

  • Durante a invalidação de baixo para cima, depois que um gráfico é criado e o conjunto de entradas alteradas é conhecido, todos os nós são invalidados e dependem transitivamente de arquivos alterados. Isso é ideal se soubermos que o mesmo nó de nível superior será criado novamente. A invalidação de baixo para cima requer a execução de stat() em todos os arquivos de entrada do build anterior para determinar se eles foram mudados. Isso pode ser melhorado usando inotify ou um mecanismo semelhante para aprender 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 que têm um fechamento transitivo limpo são mantidos. Isso é melhor se soubermos que o gráfico de nó atual é grande, mas precisamos apenas de um pequeno subconjunto dele no próximo build: 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.

No momento, só fazemos a invalidação de baixo para cima.

Para aumentar ainda mais a incrementabilidade, usamos a remoção de alterações: se um nó for invalidado, mas, após a recriação, descobrir que o novo valor é igual ao anterior, os nós que foram invalidados devido a uma alteração nesse nó serão "ressurgidos".

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, então 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 caso de tudo ou nada: quando uma dependência muda, o nó dependente é sempre reconstruído do zero, mesmo que exista um algoritmo melhor que mude o valor antigo do nó com base nas mudanças. Veja alguns exemplos em que isso pode ser útil:

  • Vinculação incremental
  • Quando um único arquivo .class muda em uma .jar, poderíamos, teoricamente, modificar o arquivo .jar em vez de criá-lo do zero novamente.

Atualmente, o Bazel não oferece suporte a essas coisas de uma forma com princípios (temos alguma compatibilidade com a vinculação incremental, mas ela não é implementada no Skyframe) é dupla: tínhamos apenas ganhos de desempenho limitados, e era difícil garantir que o resultado da mutação fosse o mesmo de uma recriação limpa seria repetível, e os builds de valores do Google que são repetíveis bit a bit.

Até agora, sempre podíamos ter um desempenho bom o suficiente simplesmente decompondo uma etapa de build cara e fazendo uma reavaliação parcial dessa forma: isso divide todas as classes de um app em vários grupos e faz a dexação separadamente. Dessa forma, se as classes em um grupo não mudarem, a decodificação não precisará ser refeita.

Como mapear para conceitos do Bazel

Esta é uma visão geral aproximada de algumas das implementações SkyFunction que o Bazel usa para executar um build:

  • FileStateValue. O resultado de um lstat(). Para arquivos existentes, também calculamos outras informações para detectar mudanças 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 importe com o conteúdo real e/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 FileStateValue é importante porque, em alguns casos (por exemplo, ao avaliar globs do sistema de arquivos, como srcs=glob(["*/*.java"])), o conteúdo do arquivo não é realmente necessário.
  • DirectoryListingValue associado. Basicamente, o resultado de readdir(). Depende do FileValue associado associado ao diretório.
  • PackageValue para código. 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 as informações fornecidas a destinos configurados que dependem dele. Depende do 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 um artefato de origem ou 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). Para arquivos de origem, depende da FileValue do nó associado. Para artefatos de saída, depende da ActionExecutionValue de qualquer ação que gera o artefato.
  • ActionExecutionValue. Representa a execução de uma ação. Depende do ArtifactValues dos arquivos de entrada. A ação que ele executa está atualmente contida na chave Sky, o que é contrário ao conceito de que as chaves Sky precisam ser pequenas. Estamos trabalhando para resolver essa discrepância. Observe que ActionExecutionValue e ArtifactValue não serão usados se não executarmos a fase de execução no Skyframe.