Processamento de anotação incremental para acelerar construções de gradle

imagem


A partir das versões Gradle 4.7 e Kotlin 1.3.30, foi possível obter uma montagem incremental acelerada de projetos devido à operação correta do processamento incremental de anotações. Neste artigo, entendemos como a teoria da compilação incremental em Gradle funciona em teoria, o que precisa ser feito para liberar todo o seu potencial (sem perder a geração de código ao mesmo tempo) e que tipo de aumento na velocidade de montagens incrementais pode ser alcançado pela ativação do processamento incremental de anotações na prática.


Como a compilação incremental funciona


As construções incrementais no Gradle são implementadas em dois níveis. O primeiro nível é cancelar o início da recompilação dos módulos usando a compilação evitada . O segundo é a compilação diretamente incremental, iniciando o compilador na estrutura de um módulo apenas nos arquivos que foram alterados ou que dependem diretamente dos arquivos alterados.


Vamos considerar a compilação evitada em um exemplo (retirado de um artigo da Gradle) de um projeto de três módulos: app , core e utils .


A classe principal do módulo de aplicativo (depende do núcleo ):


public class Main { public static void main(String... args) { WordCount wc = new WordCount(); wc.collect(new File(args[0]); System.out.println("Word count: " + wc.wordCount()); } } 

No módulo principal (depende dos utils ):


 public class WordCount { // ... void collect(File source) { IOUtils.eachLine(source, WordCount::collectLine); } } 

No módulo utils :


 public class IOUtils { void eachLine(File file, Callable<String> action) { try { try (BufferedReader reader = new BufferedReader(new FileReader(file))) { // ... } } catch (IOException e) { // ... } } } 

A ordem da primeira compilação dos módulos é a seguinte (de acordo com a ordem das dependências):


1) utils
2) núcleo
3) app


Agora considere o que acontece quando você altera a implementação interna da classe IOUtils:


 public class IOUtils { // IOUtils lives in project `utils` void eachLine(File file, Callable<String> action) { try { try (BufferedReader reader = new BufferedReader(new InputStreamReader(new FileInputStream(file), "utf-8") )) { // ... } } catch (IOException e) { // ... } } } 

Essa alteração não afeta o módulo ABI. ABI (Application Binary Interface) é uma representação binária da interface pública do módulo montado. No caso em que a alteração se relacione apenas à implementação interna do módulo e não afete sua interface pública de forma alguma, a Gradle usará a compilação e evitará a recompilação apenas do módulo utils . Se a ABI do módulo utils for afetada (por exemplo, um método público adicional aparecer ou a assinatura do existente mudar), a compilação do módulo principal será iniciada adicionalmente, mas o módulo de aplicativo dependente do núcleo não será recompilado transitivamente se a dependência nele for conectada através da implementação .



Ilustração da compilação evitada no nível do módulo do projeto


O segundo nível de incremento é o incremento no nível de inicialização do compilador para arquivos alterados diretamente dentro de módulos individuais.


Por exemplo, adicione uma nova classe ao módulo principal :


 public class NGrams { // NGrams lives in project `core` // ... void collect(String source, int ngramLength) { collectInternal(StringUtils.sanitize(source), ngramLength); } // ... } 

E nos utils :


 public class StringUtils { static String sanitize(String dirtyString) { ... } } 

Nesse caso, nos dois módulos, é necessário recompilar apenas dois novos arquivos (sem afetar o WordCount e o IOUtils existentes e não alterados), pois não há dependências entre as classes nova e antiga.


Assim, o compilador incremental analisa dependências entre classes e recompila apenas:


  • classes contendo alterações
  • classes que dependem diretamente da mudança de classe


    Processamento de anotação incremental


    insira a descrição da imagem aqui



A geração de código usando o APT e o KAPT reduz o tempo necessário para escrever e depurar o código padrão, mas o processamento da anotação pode aumentar significativamente o tempo de criação. Para piorar a situação, por muito tempo, o processamento de anotações quebrou fundamentalmente as possibilidades de compilação incremental em Gradle.


Cada processador de anotação em um projeto informa ao compilador informações sobre a lista de anotações que processa. Mas, do ponto de vista da montagem, o processamento de anotações é uma caixa preta: Gradle não sabe o que o processador fará, em particular, quais arquivos ele gerará e onde. Até o Gradle 4.7, a compilação incremental era automaticamente desativada nos conjuntos de fontes em que os processadores de anotação eram usados.


Com o lançamento do Gradle 4.7, a compilação incremental agora suporta o processamento de anotações, mas apenas para o APT. No KAPT, o suporte à anotação incremental foi introduzido no Kotlin 1.3.30. Também requer suporte de bibliotecas que fornecem processadores de anotação. Os desenvolvedores de processadores de anotação têm a oportunidade de definir explicitamente a categoria do processador, informando Gradle das informações necessárias para que a compilação incremental funcione.


Categorias do processador de anotação


Gradle suporta duas categorias de processadores:


Isolamento - esses processadores devem tomar todas as decisões para geração de código com base apenas nas informações do AST associadas a um elemento de uma anotação específica. Essa é a categoria mais rápida de processadores de anotação, pois o Gradle pode não reiniciar o processador e usar os arquivos gerados anteriormente, se não houver alterações no arquivo de origem.


Agregação - usada para processadores que tomam decisões com base em várias entradas (por exemplo, análise de anotações em vários arquivos de uma vez ou com base no estudo do AST, que é alcançável transitivamente a partir de um elemento anotado). Cada vez, o Gradle iniciará o processador para arquivos que usam anotações do processador agregador, mas não recompilará os arquivos gerados se não houver alterações neles.


Para muitas bibliotecas populares baseadas na geração de código, o suporte à compilação incremental já está implementado nas versões mais recentes. Veja a lista de bibliotecas que oferecem suporte aqui .


Nossa experiência na implementação de processamento de anotação incremental


Agora, para projetos que começam do zero e usam as versões mais recentes de bibliotecas e plugins gradle, é provável que as construções incrementais estejam ativas por padrão. Mas a maior parte do aumento na produtividade da montagem pode ser alcançada pela incrementalidade do processamento de anotações em projetos grandes e de longa duração. Nesse caso, pode ser necessária uma atualização maciça da versão. Vale a pena na prática? Vamos ver!


Portanto, para que o processamento incremental de anotações funcione, precisamos:


  • Gradle 4.7+
  • Kotlin 1.3.30+
  • Todos os processadores de anotação em nosso projeto devem ter seu suporte. Isso é muito importante, porque se em um único módulo pelo menos um processador não suportar incrementalidade, o Gradle o desativará em todo o módulo. Todos os arquivos no módulo serão compilados novamente a cada vez! Uma das opções alternativas para obter suporte para compilação incremental sem atualizar versões é a remoção de todo o código usando processadores de anotação em um módulo separado. Nos módulos que não possuem processadores de anotação, a compilação incremental funcionará bem

Para detectar processadores que não atendem à última condição, você pode executar o assembly com o sinalizador -Pkapt.verbose = true . Se Gradle foi forçado a desativar o processamento de anotação incremental para um único módulo, no log de compilação, veremos uma mensagem sobre quais processadores e em quais módulos isso está acontecendo (consulte o nome da tarefa):


 > Task :common:kaptDebugKotlin w: [kapt] Incremental annotation processing requested, but support is disabled because the following processors are not incremental: toothpick.compiler.factory.FactoryProcessor (NON_INCREMENTAL), toothpick.compiler.memberinjector.MemberInjectorProcessor (NON_INCREMENTAL). 

Em nosso projeto de biblioteca com processadores de anotação não incrementais, havia 3:


  • Palito de dente
  • Quarto
  • PermissionsDispatcher

Felizmente, essas bibliotecas são ativamente suportadas e suas versões mais recentes já têm suporte à incrementalidade. Além disso, todos os processadores de anotação nas versões mais recentes dessas bibliotecas têm uma categoria ideal - isolamento. No processo de elevar as versões, tive que lidar com a refatoração devido a alterações na API da biblioteca Toothpick, que afetaram quase todos os nossos módulos. Mas, nesse caso, tivemos sorte e refatoramos completamente automaticamente usando os nomes de substituição automática dos métodos de biblioteca pública usados.


Observe que, se você usar a biblioteca de salas , precisará passar explicitamente o sinalizador room.incremental: true para o processador de anotações. Um exemplo No futuro, os desenvolvedores de salas planejam ativar esse sinalizador por padrão.


Para versões do Kotlin 1.3.30-1.3.50, você deve ativar o suporte ao processamento incremental de anotações explicitamente por meio de kapt.incremental.apt = true no arquivo gradle.properties do projeto. A partir da versão 1.3.50, essa opção é configurada como true por padrão.


Criação de perfil de montagem incremental


Após o aumento das versões de todas as dependências necessárias, é hora de testar a velocidade das compilações incrementais. Para fazer isso, usamos o seguinte conjunto de ferramentas e técnicas:


  • Gradle build scan
  • gradle-profiler
  • Para executar scripts com o processamento de anotação incremental ativado e desativado, a propriedade gradle kapt.incremental.apt = [true | false] foi usada
  • Para resultados consistentes e informativos, as assembléias foram realizadas em um ambiente de IC separado. Incrementalidade de compilação foi reproduzida usando gradle-profiler

O gradle-profiler permite preparar declarativamente scripts para benchmarks de construção incrementais. 4 cenários foram compilados com base nas seguintes condições:


  • A modificação de um arquivo afeta / não afeta sua ABI
  • Suporte para ligar / desligar o processamento incremental de anotação

A execução de cada um dos cenários é uma sequência de:


  • Reiniciando o daemon gradle
  • Lançar compilações de aquecimento
  • Execute 10 montagens incrementais, antes de cada um dos quais um arquivo ser alterado, adicionando um novo método (privado para alterações que não sejam da ABI e público para alterações da ABI)

Todas as compilações foram feitas com o Gradle 5.4.1. O arquivo envolvido nas alterações refere-se a um dos módulos principais do projeto (comum), dos quais 40 módulos (incluindo núcleo e recurso) são diretamente dependentes. Este arquivo usa a anotação para isolar o processador.


Também é importante notar que a execução do benchmark foi realizada em duas tarefas gradle : ompileDebugSources e assembleDebug . O primeiro inicia apenas a compilação de arquivos com código fonte, sem fazer nenhum trabalho com recursos e agrupar o aplicativo em um arquivo .apk. Com base no fato de que a compilação incremental afeta apenas os arquivos .kt e .java, a tarefa compileDedugSource foi escolhida para um benchmarking mais isolado e mais rápido. Em condições reais de desenvolvimento, quando você reinicia o aplicativo, o Android Studio usa a tarefa assembleDebug , que inclui a geração completa da versão de depuração do aplicativo.


Resultados de referência


Em todos os gráficos gerados pelo gradle-profiler, o eixo vertical mostra o tempo de montagem incremental em milissegundos e o eixo horizontal mostra o número inicial da montagem.


: compileDebugSource antes de atualizar os processadores de anotação


insira a descrição da imagem aqui
O tempo médio de execução para cada cenário foi de 38 segundos antes de atualizar os processadores de anotação para versões que suportam incrementalidade. Nesse caso, o Gradle desabilita o suporte à compilação incremental, portanto, não há diferença significativa entre os scripts.


: compileDebugSource após atualizar os processadores de anotação



CenárioAlteração incremental da ABIAlteração não incremental da ABIAlteração incremental não ABIAlteração não incremental e não-abi
dizer23978353702351434602
mediana23879350192342434749
min22618339692234333292
max26820380972565135843
stddev1193.291240,81888,24815,91

A redução mediana no tempo de montagem devido à incrementalidade foi de 31% para alterações no ABI e 32,5% para alterações não relacionadas ao ABI. Em valor absoluto, cerca de 10 segundos.


: assembleDebug após atualizar os processadores de anotação



CenárioAlteração incremental da ABIAlteração não incremental da ABIAlteração incremental não ABIAlteração não incremental e não-abi
dizer39902498503900552123
mediana38974496913871350336
min38563487823823348944
max48255523644173265941
stddev2953,281011,201015,375039.11

Para compilar a versão de depuração completa do aplicativo em nosso projeto, a redução média no tempo de compilação devido ao incremento foi de 21,5% para alterações de ABI e 23% para alterações que não são de ABI. Em termos absolutos, aproximadamente os mesmos 10 segundos, pois o incremento da compilação do código-fonte não afeta a velocidade de montagem dos recursos.


Construir Anatomia da Varredura no Gradle Build Scan


Para uma compreensão mais profunda de como o incremento foi alcançado durante a compilação incremental, comparamos as varreduras de montagens incrementais e não incrementais.


No caso de incremento desabilitado do KAPT, a parte principal do tempo de compilação é a compilação do módulo de aplicativo, que não pode ser paralelo a outras tarefas. A linha do tempo para o KAPT não incremental é a seguinte:


insira a descrição da imagem aqui


Execução da tarefa: kaptDebugKotlin do nosso módulo de aplicativo leva cerca de 8 segundos neste caso.


Linha do tempo para o caso com o incremento KAPT ativado:


insira a descrição da imagem aqui


Agora, o módulo do aplicativo foi recompilado em menos de um segundo. Vale a pena prestar atenção à desproporcionalidade visual das escalas das duas digitalizações na imagem acima. As tarefas que parecem mais curtas na primeira imagem não são necessariamente mais longas na segunda, onde parecem mais longas. Mas é muito perceptível o quanto a proporção de recompilação do módulo de aplicativo foi reduzida quando o KAPT incremental foi ativado. No nosso caso, ganhamos cerca de 8 segundos neste módulo e mais 2 segundos adicionais em módulos menores que são compilados em paralelo.


Ao mesmo tempo, o tempo total de execução de todas as tarefas * kapt para a incrementalidade desabilitada das anotações de processamento é de 1 minuto e 36 segundos contra 55 segundos quando ativado. Ou seja, sem levar em conta a montagem paralela dos módulos, o ganho é mais substancial.


Também é importante notar que os resultados do benchmark acima foram preparados em um ambiente de CI com a capacidade de executar 24 threads paralelos para montagem. Em um ambiente de 8 threads, o ganho ao ativar o processamento de anotação incremental é de cerca de 20 a 30 segundos em nosso projeto.


Paralelo incremental vs (?)


Outra maneira de acelerar significativamente a montagem (incremental e limpa) é executar tarefas de nivelamento em paralelo, dividindo o projeto em um grande número de módulos fracamente acoplados. De uma forma ou de outra, a modularização representa um potencial muito maior para acelerar as montagens do que usar o KAPT incremental. Porém, quanto mais monolítico o projeto for, e quanto mais a geração de código for usada nele, maior será o processamento incremental de anotações. É mais fácil obter o efeito de incrementalidade completa de montagens do que quebrar um aplicativo em módulos. No entanto, ambas as abordagens não se contradizem e se complementam perfeitamente.


Sumário


  • A inclusão do processamento incremental de anotações em nosso projeto nos permitiu alcançar um aumento de 20% na velocidade da reconstrução local
  • Para habilitar o processamento de anotação incremental, será útil estudar o log completo dos assemblies atuais e procurar mensagens de aviso com o texto "Processamento de anotação incremental solicitado, mas o suporte está desativado porque os seguintes processadores não são incrementais ...". É necessário atualizar versões de bibliotecas para versões com suporte para processamento incremental de anotações e ter as versões Gradle 4.7+, Kotlin 1.3.30+

Materiais e o que ler sobre o tópico


Source: https://habr.com/ru/post/pt484918/


All Articles