O que test-suite-speedup faz?
Visão Geral
Uma suíte lenta tenta dois movimentos errados: reescrever o arquivo aparentemente mais lento manualmente e relatar um cronômetro local como ganho. Ambos falham. O tempo geralmente está em alguns lugares estruturais — setup pesado repetido, arquivos executados serialmente que poderiam ser paralelos, um job de CI fazendo o trabalho de dois — e uma temporização local feita enquanto outra coisa está rodando é ruído. Esta habilidade classifica o custo na CI primeiro, escolhe a alavanca que move mais arquivos, e trata uma tabela antes/depois na CI como o teste de aceitação.
Princípio central: a entrega é uma comparação medida na CI, no mesmo caminho de CI, não uma temporização local e nem verificações verdes.
Mapa de referência — carregue apenas quando a etapa for executada:
| Arquivo | Contém |
|---|---|
references/ci-measurement.md | Etapas de classificação em várias execuções, classificação por arquivo a partir de um log pytest -v, ruído do tipo de runner, relatório antes/depois, atualização de duração por shard |
references/pytest-slow-test-causes.md | Classificação de um arquivo pytest lento por fase e assinatura; sleeps escapados; esperas inalcançáveis em código de produto; patches hermeticos em toda a suíte (clientes cloud, sleeps ociosos de bibliotecas) com testes de guarda; perfilamento de um fluxo de trabalho com um processo; restrição de um create_all em tempo de execução |
references/pytest-shared-setup.md | Dimensionamento da parte compartilhável de uma factory quente a partir de seus chamadores; compartilhamento seguro de um objeto pytest caro: provas antes e depois, fiação com mínimo de alteração, interação com seleção de mudança |
references/vitest-isolation.md | Suítes JS/vitest: classificação de arquivos entre espera e computação, arquivos lentos apenas na CI, custo dominado por importação, perfilamento de importação por módulo, pré-empacotamento de bibliotecas barril, pools isolados vs compartilhados, sondas de vazamento, verificações de embaralhamento |
references/parallel-runner.md | Sonda de segurança de paralelismo, partição serial, regras de implementação do runner, divisão de matriz de CI |
references/ci-redundant-runs.md | Execuções que não deveriam ocorrer: classificação de histórico de backmerge/push por árvore, reutilização do resultado de uma árvore idêntica, substituição por uma execução nova real (API do Actions, não parsing de mensagens), concorrência por job, teste de gates if: do workflow como código, manutenção de marcadores de skip fora do texto do commit |
scripts/ci_leg_series.sh | Segundos de etapa de teste por leg para cada push desde uma data (TSV) — a série para localizar mudanças de etapa; execute em segundo plano |
scripts/ci_durations_diff.py | Para pares de execuções antes/depois em torno de cada mudança de etapa: contagens coletadas, resumos do pytest, entradas novas ou crescidas de --durations |
scripts/boto_call_counter.py | Plugin pytest que conta chamadas reais ao botocore por teste — execute quando o cProfile mostrar esperas SSL/socket sob botocore |
Quando Usar
- Uma suíte de testes, ou o job de teste de um workflow de CI, está muito lento e o usuário quer acelerá-lo.
- O usuário pergunta "o que mais está lento?" após uma aceleração anterior.
- Não é para limpeza de cópia/duplicação sem uma meta de temporização — reestruturar testes para reutilização não é uma aceleração (veja
test-suite-consolidation).
Processo
Passo 1: Linha de base no mesmo caminho de CI
Tire a linha de base da CI, nunca de uma execução local. A linha de base deve exercitar o mesmo caminho de CI que a execução posterior: quando o PR base seleciona o caminho (por exemplo, prod executa a suíte completa dividida em legs de matriz enquanto dev executa uma leg com seleção de mudança), pegue a execução bem-sucedida mais recente de um PR para a mesma base, e verifique se as contagens de passed/skipped/deselected coincidem antes de comparar tempos. Extraia temporizações por etapa para essa execução, divida a etapa grande pelos banners de seção do runner e leia os comentários do arquivo de workflow — os mantenedores geralmente já escreveram a correção pretendida. Em seguida, classifique as etapas em várias execuções, não apenas uma, e verifique a contagem real de vCPUs do runner em relação ao número configurado de workers. Comandos → references/ci-measurement.md.
Antes de propor qualquer coisa, pesquise issues e PRs por uma issue de aceleração existente, incluindo as fechadas e PRs de experimentos descartados (gh pr list --state all --search "xdist", --search "test_durations"): eles registram alternativas medidas e rejeitadas, e as condições sob as quais foram rejeitadas — uma alavanca refutada quando o custo de setup dominava pode valer a pena uma vez que testes com limite de CPU o fizerem.
Feito quando: um ID de execução de linha de base no caminho de CI correspondente é nomeado, suas contagens de teste são registradas, e cada etapa com alguns segundos tem uma mediana e p90 em várias execuções.
Passo 2: Dimensione o ganho esperado antes de escolher uma alavanca
Ganho ≈ (parcela do tempo da suíte que o lote cobre) × (economia por arquivo medida nesse subconjunto). Um lote que é 2% da suíte dá cerca de 1–2% mesmo a −60% por arquivo — abaixo do ruído da CI, e o usuário lerá como "quase não mexeu". Prefira alavancas que movem centenas de arquivos de uma vez (um helper de teste compartilhado, um classificador de pool, o tipo de runner) e apresente migrações por arquivo como a cauda longa. Diga o ganho esperado antecipadamente.
Feito quando: cada alavanca candidata tem uma estimativa por escrito com suas entradas, e as estimativas abaixo do ruído da CI são rotuladas como tal.
Passo 3: Classifique os arquivos e classifique cada arquivo principal
Classifique os arquivos em toda a suíte a partir de uma execução de CI completa que registre pytest -v (uma execução com seleção de mudança não cobre todos os testes). Valide a classificação em uma execução com uma resposta conhecida primeiro — a execução pré-correção deve classificar o arquivo que você corrigiu em primeiro lugar. Em seguida, classifique cada arquivo principal por fase (setup / call / teardown) e forma antes de propor uma correção:
- pytest →
references/pytest-slow-test-causes.md - vitest / JS →
references/vitest-isolation.md
Temporizações locais por arquivo são apenas para forma: execute cada arquivo em segundo plano, olhe por que está lento (um spawn de processo por caso, sleeps reais — um sleep dentro da string de entrada de um teste não é um atraso), e nunca relate um número tirado enquanto outra suíte está rodando. Registre CPU (user+sys) junto com o tempo de parede para cada arquivo. Um arquivo que usa pouca CPU para seu tempo de parede está esperando em tempo real (limitadores, backoff, polling), e a correção é tempo falso no lado do teste; mais workers ou cortes de importação não ajudarão. Um arquivo que está lento na CI mas rápido localmente sob a contagem de workers e ambiente da CI precisa de uma reprodução no SO e runtime major da CI (um container node:<major> limitado às vCPUs da CI), não um palpite; leia as linhas de teste lento que o log da CI já imprime sob esse arquivo primeiro → references/vitest-isolation.md. Para o pytest, compare a razão CI/local por arquivo com a razão usual da suíte. Um arquivo várias vezes acima dela tem um custo apenas na CI, mas sua causa é uma hipótese até que a execução do próprio PR na CI mova esse arquivo: um A/B de container Linux (ex.: sqlite synchronous=OFF cortando simulações de escalonador ~35%) não se transferiu para o runner do GitHub, onde os mesmos testes mantiveram seus --durations. Envie tal alavanca apenas junto com um alvo mensurável na primeira execução do PR → references/pytest-slow-test-causes.md.
A causa de uma suíte lenta geralmente é uma combinação: workers seriais, arquivos em espera e importações pesadas por arquivo. Classifique as alavancas em uma tabela: causa · evidência (medida vs estimada) · segundos na CI · correção · economia esperada.
Uma causa adivinhada a partir de nomes de --durations ("provavelmente reanalisa o DAG") é uma hipótese até que uma execução de cProfile cumtime de 2–3 dos testes lentos mostre isso; perfile antes de a causa ir para um plano ou issue, e retire-a no relatório quando o perfil disser o contrário.
Feito quando: cada arquivo classificado no topo tem uma causa da tabela de classificação, ou está explicitamente marcado como "precisa de uma olhada".
Passo 4: Escolha a correção estrutural, nesta ordem
- Remova setup caro repetido dentro de um arquivo quente (um teste que reconstrói o mesmo objeto pesado por chamada — um saco de DAG, um app, um esquema). Reutilize a fixture de sessão existente do repositório antes de inventar um cache, e prove que o compartilhamento é seguro primeiro, depois prove que as cópias base e branch do arquivo falham nos mesmos mutantes de produção. Um cache de módulo/sessão só deduplica dentro de um processo pytest: em uma CI fragmentada, verifique em qual leg cada consumidor é executado antes de contar a economia. Quando um teste separado apenas relê o resultado caro de um caso parametrizado, mova suas asserções para aquele caso em vez de armazenar em cache (independente de shard), e quebre uma asserção movida para provar que ainda executa →
references/pytest-shared-setup.md. - Execute arquivos de teste independentes em paralelo dentro do entrypoint da CI.
- Divida o job de CI em legs de matriz para que as metades rodem lado a lado.
- Só então reescreva os spawns por caso dos arquivos mais lentos — grande alteração, ganho modesto. Ofereça; não tome como padrão.
Quando a maior parte do custo são execuções inteiras que não precisam acontecer (backmerges de bot, uma árvore idêntica já testada em outro branch, pushes prestes a ser ultrapassados), essa alavanca vem antes de todas as acima: classifique o histórico de execuções por árvore e pule ou reutilize execuções inteiras → references/ci-redundant-runs.md.
Feito quando: a alavanca escolhida é nomeada com sua estimativa do Passo 2, e qualquer alavanca de ordem superior ignorada tem uma razão de uma linha.
Passo 5: Prove a segurança de paralelismo e implemente a mudança no runner
Execute cada arquivo em paralelo sob um diretório de estado descartável, cada um em seu próprio arquivo de saída e código de saída, liste as falhas e reexecute cada falha sozinha para separar um bug de concorrência do ambiente. Particione os arquivos que editam a árvore de trabalho em um grupo serial com base em um marcador que eles já carregam. Em seguida, implemente o runner e, se escolhido, a divisão da matriz de CI. Procedimento e regras → references/parallel-runner.md.
Feito quando: cada falha da sonda de paralelismo é classificada (concorrência / ambiente / pré-existente), o grupo serial é derivado de um grep em vez de uma lista manual, e as verificações de status obrigatórias foram verificadas antes de qualquer renomeação de job.
Passo 6: Verifique na CI, não sentado em execuções locais
Mantenha o código de teste alterado de acordo com as diretrizes de codificação do próprio repositório como código de produto — sem wrappers de passagem, duplicação extraída, uma prova de mutação para qualquer novo helper. Execute localmente apenas os arquivos tocados mais lint, faça push e observe as verificações do PR em segundo plano em vez de segurar a vez. Leia os comentários de revisão do PR antes de escrever qualquer nota de verificação: revisores bot postam minutos após um push.
Quando um teste falha apenas em um ambiente local (um venv de dependência opcional, um serviço falso local), execute esse teste na base não modificada em uma worktree descartável antes de tocar na mudança: a mesma falha lá é ambiente, não regressão. Nomeie-a no trailer Not-tested: do commit e no corpo do PR, e feche-a a partir do job de CI que instala essa dependência (seu log bruto: a linha de resumo mais grep -c FAILED = 0), não da execução local.
Uma comparação local base-vs-branch de suíte completa suporta "sem novas falhas" apenas na medida em que foi reproduzida: quando as falhas apenas no branch compartilham um erro de ambiente, diga "N falhas apenas no branch, todas ; reproduzido na base" e deixe a execução de CI fechar o resto — "0 novas falhas" exagera e tem que ser corrigido em cada superfície que alcançou.
Antes de chamar um stub de suíte inteira de seguro para merge, enumere como ele poderia alterar resultados, cada um com uma sonda: (1) binds no nível do módulo que o contornam (git grep -nE "from <mod> import <fn>" — um nome vinculado na importação mantém a função real); (2) chamadores de produção que não capturam a exceção do stub, e se algum teste os executa sem patch (as contagens de chamadas por teste da execução anterior mostram isso); (3) testes que afirmam o caminho real — eles devem fazer patch em si mesmos, aninhados dentro do stub da sessão; (4) uma atualização de biblioteca em andamento que move o nome patchado (veja a regra de tolerância a versões em pytest-slow-test-causes.md). Relate cada um como verificado ou como uma inferência com o que o confirmaria.
Revisores bot podem executar por mais tempo que uma enquete curta: o conector Codex marca um PR 👀 enquanto revisa e o troca por 👍 quando termina sem achados, às vezes após 20 minutos. Limite a espera, depois releia reações e comentários uma vez antes do briefing de merge em vez de estender a enquete. Para saber se um 👍 permanente pertence ao push mais recente, leia seu timestamp (gh api graphql → pullRequest{reactions(first:20){nodes{content createdAt user{login}}}}) contra o tempo do push — um 👍 mais antigo que o push é o veredito da rodada anterior.
Uma execução pull_request testa o merge do head na ponta da base naquele momento, não o commit base do próprio PR: leia o sha base do log do job (HEAD is now at <m> Merge <head> into <base>) e compare com a execução de push desse commit base.
A métrica por arquivo é a medida posterior quando a suíte cresceu entre a linha de base e a execução do PR: os segundos de leg inteira então misturam testes de outros commits (compare as contagens de collected N items primeiro). Relate os segundos de CI do arquivo tocado de ambas as execuções como resultado e os tempos de leg como contexto. Após fazer merge da base no PR, reconstrua a banda de comparação a partir de execuções de outros PRs no mesmo período, não da banda anterior: a própria suíte da base continua crescendo, então a banda antiga pode fazer um ganho real parecer uma regressão. Diga no relatório quando a própria banda se moveu.
Feito quando: uma execução de CI do branch no mesmo caminho que a linha de base terminou, e cada achado de revisão aberto foi respondido.
Passo 7: Relate o antes/depois
Uma tabela com ambos os IDs de execução: tempo de parede do job (agora = a leg mais lenta), a etapa antes lenta e a metade inalterada, mais a linha de resumo de teste por leg. Para um ganho distribuído por vários PRs, uma tabela: linha de base → cada etapa mesclada → agora, com a parcela que cada PR contribuiu. Nomeie o caminho de CI que o PR não exercitou como não verificado, diga que é uma execução de cada (variância do runner não mensurável) e que o caminho real é a métrica. Feito quando: um resumo de uma linha com o tempo de parede do job atual está no PR, e o comentário que o contém está fixado como a última linha de resumo.
Notas de Implementação para o Agente
- Carregamento de referência: Leia o arquivo de referência do mapa somente quando a etapa que o referencia for executada. Cada etapa lista seu arquivo na coluna "Contém".
- Redação de PR: Antes de escrever qualquer nota de verificação, leia os comentários de revisão do PR. Revisores bot postam em minutos.
- Resultado: A entrega principal são dados: a linha de resumo da CI e a tabela de comparação de tempos de etapa. A explicação é secundária.