Como reduzimos o tempo de execução do Flutter no CI em cerca de 20%

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

Quantas xícaras de café seria possível tomar em 36,6h?

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.

Exemplo de pipeline rodando antes e depois do cache implementado

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.

- name: Cache build runner
  uses: actions/cache@v2
  with:
    path: |
      **/.dart_tool
      **/*.g.dart
      **/*.mocks.dart
      **/*.config.dart
    key: build-runner-${{ hashFiles('**/asset_graph.json', '**/*.dart', '**/pubspec.lock', '**/outputs.json') }}
    restore-keys: |
      build-runner-
Cache-Article-Chunk-Build-Runner

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.

name: PR Verification

on:
  push:
    branches:
      - develop
  pull_request:
jobs:
  pr-verification:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v2
      - uses: subosito/flutter-action@v2
        with:
          flutter-version: '2.10.13'
          cache: true
      - 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-
      - name: Cache build runner
        uses: actions/cache@v2
        with:
          path: |
            **/.dart_tool
            **/*.g.dart
            **/*.mocks.dart
            **/*.config.dart
          key: build-runner-${{ hashFiles('**/asset_graph.json', '**/*.dart', '**/pubspec.lock', '**/outputs.json') }}
          restore-keys: |
            build-runner-
      ## /Flutter caching
      - run: flutter pub get
      - run: flutter pub run build_runner build
      - run: flutter analyze
      - run: flutter test
Cache-Article-Whole-CI-File

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