Como reduzimos o tempo de execução do Flutter no CI em cerca de 20%
Já pensou quanto tempo é necessário para rodar todos os pipelines de Continuous Integration (Integração Contínua) do seu projeto mobile, assegurando a qualidade dos seus entregáveis?
Imagine que a cada abertura ou atualização de um PR (pull request), seja executada automaticamente uma verificação nos testes unitários do projeto. Podemos imaginar que o tempo que esse pipeline leva para ser finalizado tenha um impacto no fim do mês escalado de acordo com o tamanho do time.
Por exemplo, fazendo uma conta de padaria: se o pipeline leva 5 minutos para ser completado, o time tem 10 desenvolvedores e o pipeline roda pelo menos 2x por dia, teremos no mínimo 100 minutos de pipeline rodando por dia — o que rende, em média, 2200 minutos (cerca de 36,6h) por mês. 😲
Ou seja, o tempo dos jobs no CI pode ser algo crítico. Se pudermos diminuir 1 minuto desse job, já ganhamos muito tanto na agilidade do fluxo das tarefas do time quanto no custo.
Ok, mas… como isso se aplica no Flutter?
O segredo é reutilizar artefatos de compilações anteriores. Se não fizermos isso, vamos sempre perder aquele tempo compilando as mesmas coisas novamente.
Mas deve-se ter um certo cuidado sobre o que vamos armazenar no cache, pois não queremos inibir verificações sobre alterações que podem acabar levando bugs para o sistema.
Entendendo o processo de build
Existem algumas etapas necessárias antes de começar a rodar as verificações no projeto:
- Baixar o flutter
- Baixar as dependências do projeto
- Executar o Flutter build_runner
Cada um desses passos pode ser otimizado independentemente, caso não haja alterações. Por exemplo, você pode ter adicionado alguma dependência no projeto e alterado parte do código sem ter alterado a versão do Flutter — isso vai fazer com que o cache da versão do Flutter economize algum tempo de preparação do CI, mas as outras compilações continuem sendo executadas pois vão gerar novas chaves de cache.
Como configurar o cache no CI
Faremos isso utilizando o Github Actions, e a action cache para armazenar e recuperar as compilações anteriores.
Nessa action, temos alguns parâmetros necessários para seu funcionamento:
path: Esse parâmetro é onde vamos listar o que deverá ser armazenado no cache. O valor desse parâmetro não se limita a um único arquivo ou diretório, então podemos incluir diversos diretórios, por exemplo;
key: Uma chave em que vamos guardar aquele conjunto de arquivos;
restore-keys: Uma lista de chaves usadas para recuperar o cache. Para este exemplo, usaremos um único valor.
Dependências do pubspec
Esse cache se refere às dependências listadas no pubspec.yaml.
A chave muda de acordo com um hash do arquivo pubspec.lock, logo, se há alterações no arquivo de dependências, ele irá descartar o cache anterior.
- name: Cache pubspec dependencies
uses: actions/cache@v2
with:
path: |
${{ env.FLUTTER_HOME }}/.pub-cache
**/.packages
**/.flutter-plugins
**/.flutter-plugin-dependencies
**/.dart_tool/package_config.json
key: build-pubspec-${{ hashFiles('**/pubspec.lock') }}
restore-keys: |
build-pubspec-
Flutter build_runner
Por fim, armazenamos um cache do código gerado a partir do flutter build_runner, que por ser um processo demorado, é de suma importância que seja otimizado.
Os arquivos que estamos especificando para serem salvos no cache são:
- Arquivos .g.dart gerados por diversas libs;
- Arquivos .mocks.dart (para mocks de testes unitários usando Mockito);
- Arquivos .config.dart (usado pelo Injectable).
Na chave desse cache, incluiremos uma junção do hash do arquivo asset_graph.json, do anteriormente citado pubspec.lock, do arquivo outputs.json e de todos os arquivos .dart.
Importante ressaltar que, se você não usa o build_runner no seu projeto, esse passo é desnecessário.
E onde isso tudo entra no pipeline?
As definições do cache devem entrar logo antes dos comandos que você pretende rodar para fazer as devidas verificações.
Pronto! Isso deve ser o suficiente para as automatizações do seu projeto Flutter economizarem muito tempo da sua equipe, para ela focar no que realmente precisa.
O código incluso neste artigo foi escrito por mim e pelo Douglas Iacovelli.
Post originalmente publicado no Medium