Como melhorar a velocidade do phpcs e do phpcbf


Uma tarefa comum quando você está desenvolvendo um projeto é rodar um linter. Linters são ferramentas que analisam o código fonte do seu projeto e buscam detectar diferentes problemas no código, incluindo possíveis falhas na lógica e também questões relacionadas ao estilo do código. Porém, se tem uma coisa que incomoda MUITO é quando você está desenvolvendo, QUASE finalizando o software, e precisa ESPERAR para que ferramentas de verificação assim (que incluem tanto linters quanto testes, que são importantes para qualquer projeto) assim rodem.

Esse é o clássico problema que o XKCD retratou extremamente bem no post "Compiling":

Tirinha do XKCD que traduz livremente para: A desculpa número 1 dos programadores para procrastinar legitimamente: 'meu código está compilando'

E isso incomoda AINDA MAIS quando a ferramenta em questão é single-thread ou mal otimizada, pois o seu computador (que assumo ter um processador com múltiplos núcleos hoje em dia, certo?), fica mais ou menos assim:

Imagem de vários trabalhadores ao redor de uma obra, com só um dos trabalhadores de fato trabalhando em um buraco. Fazendo analogia ao uso de 100% de apenas um núcleo só na maior parte das atividades cotidianas em um computador.

Bom. Agora vamos ao tópico do post: Já que é bem difícil simplesmente paralelizar o trabalho de uma ferramenta de forma eficiente, vamos tentar tornar a sua atuação mais inteligente: Uma forma de deixar o trabalho de qualquer script - ou qualquer coisa, na verdade - mais eficiente, é fazer com que tal script (ou coisa) faça MENOS trabalho.

No caso de um linter, uma forma bem fácil de fazer MENOS trabalho é simplesmente... não ficar rechecando arquivos que não são necessários. Isso é MUITO mais eficiente do que simplesmente paralelizar o algoritmo, uma vez que mesmo o número de núcleos em um computador moderno é limitado.

Para exemplificar isso, considere um projeto que tenha, digamos, 5000 arquivos para serem analisados, cada um levando aí algo em torno de 0.04 segundos. Um linter rodando sequencialmente levaria, portanto, 200 segundos (ou 3 minutos e 20 segundos) para analisar tudo.

Se você tem um processador com 8 núcleos e 16 threads no seu computador, normalmente um algoritmo paralelo tentaria disparar 16 threads para processar cada arquivo. Digamos que essas 16 threads consigam acelerar em 16 vezes a velocidade de processamento (algo que nunca acontece pois há diversas limitações que impedem que algo assim aconteça). Em vez de demorar 200 segundos, ou 3 minutos e 20 segundos, tal algoritmo paralelo levaria cerca de 12 segundos para processar todos os 5000 arquivos.

Levar 12 segundos em vez de 3 minutos e 20 segundos é uma vitória tremenda, certo? Porém, considere que você NÃO vai modificar todos os 5000 arquivos a todo momento. Pelo contrário, você vai modificar ali 1, 3, 5 arquivos por vez, enquanto trabalha no seu projeto. E é aqui que mesmo a paralelização deixa de fazer sentido: Para que checar todos os arquivos, se apenas alguns poucos foram modificados? Se apenas 5 arquivos foram modificados, e o linter leva 0.04 segundos para processar um arquivo, apenas 0.04 * 5 = 0.2 segundos (ou MENOS de um segundo!) deveriam ser necessários para processar tudo, o que já é 60 vezes mais rápido que o algoritmo paralelo ou 1000 vezes mais rápido que o algoritmo sequencial.

E é aqui que entra o script que é o assunto desse post.

Esse é o script, em bash:

#!/bin/bash

set -e

SCRIPT_NAME=$(basename "$0");

find_script() {
    script_name="$1"
    current_dir=$(pwd)
    parent_dir=$(dirname "$current_dir")
    
    # Check if the path exists in the directory exists in the current directory
    script_path="$current_dir/vendor/bin/$script_name"
    if [ -x "$script_path" ]; then
        echo "$script_path"
        return
    fi
    
    # Traverse up the directory tree and check if the script exists in any parent directory
    while [ "$parent_dir" != "$current_dir" ]; do
        script_path="$parent_dir/vendor/bin/$script_name"
        if [ -x "$script_path" ]; then
            echo "$script_path"
            return
        fi
        
        current_dir="$parent_dir"
        parent_dir=$(dirname "$current_dir")
    done
    
    # Script not found
    echo "Script $SCRIPT_NAME not found." > /dev/stderr
    exit 1
}


SCRIPT_PATH=$(find_script "$SCRIPT_NAME");

MODIFIED_FILES=$(git status --porcelain=v2 . | awk '{print $NF}');

if [ -z "$MODIFIED_FILES" ]; then
  echo "No modified files found in the current directory."
  exit 0
fi

echo "Running $SCRIPT_PATH for the following list of files:";
echo "$MODIFIED_FILES" | xargs -i{} echo "- {}";

echo "$MODIFIED_FILES" | xargs "$SCRIPT_PATH";

O que o script faz? É bem simples, vou explicar:

  1. Primeiramente, rodamos set -e para fazer com que o bash imediatamente interrompa a execução caso qualquer comando retorne erro (com código de status de saída diferenet de 0)
  2. Definimos uma variável chamada SCRIPT_NAME contendo o nome do arquivo atual, já explico mais abaixo o motivo disso
  3. Definimos uma função find_script para procurar o script. Essa função busca encontrar o script de nome SCRIPT_NAME dentro da pasta vendor/bin, e busca tanto no diretório atual quanto em qualquer superior. Para exemplificar, se você rodar o arquivo dentro de uma pasta /home/usuario/projetos/MeuProjeto, ele procurará um arquivo executável com o nome do script salvo em SCRIPT_NAME (digamos que tal nome seja phpcs) nas seguintes pastas:
    • /home/usuario/projetos/MeuProjeto/vendor/bin/phpcs
    • /home/usuario/projetos/vendor/bin/phpcs
    • /home/usuario/vendor/bin/phpcs
    • /home/vendor/bin/phpcs
    • /vendor/bin/phpcs
  4. Definimos uma variável SCRIPT_PATH contendo o caminho para o script encontrado pela função find_script
  5. Computamos uma lista de arquivos modificados a partir do git. Apenas arquivos modificados cujas mudanças não foram comittados ainda aparecerão aqui
  6. Verificamos se a lista de arquivos modificados possui, de fato, arquivos modificados. Se não houver arquivos modificados mostramos uma mensagem e encerramos o script
  7. Mostramos a lista de arquivos modificados, e passamos essa lista de arquivos modificados para o script em questão, como parâmetros.

Para usar esse script em uma computador Linux, basta você

  1. Criar uma pasta bin na sua $HOME se você ainda não tiver, assim: mkdir -p $HOME/bin
  2. Adicionar essa pasta bin no seu environment, adicionando no arquivo de configuração do seu shell (digamos, .bashrc) algo como export PATH=$PATH:$HOME/bin
  3. Adicionar esse script nessa pasta bin, com o nome da ferramenta que você quer otimizar. Você pode nomeá-lo como phpcs ou phpcbf, por exemplo
  4. Em um projeto PHP com o phpcbf ou phpcs instalado via Composer, executar o comando correspondente SEM usar vendor/bin como prefixo
  5. Pronto!

O comando só rodará de fato a ferramenta em questão SE houver arquivos modificados e não comittados ainda no repositório Git.

Também não executará se a ferramenta em questão não estiver instalada usando Composer, que já apresentei me outro post.

Finalmente, se você não estiver usando Git, o script não executará, pois ele não tem como rastrear quais arquivos modificados. Se você ainda não usa Git, verifque o meu post sobre, também. 😉

E você, tem alguma outra dica para agilizar tarefas demoradas - mas necessárias - do seu dia-a-dia? Compartilhe nos comentários! E se ficou alguma dúvida, pode madnar nos comentários que vou tentar fazer o meu melhor para ajudar, também. 😀

Valeu pela leitura!


Posts relacionados


Deixe um comentário

O seu endereço de e-mail não será publicado. Campos obrigatórios são marcados com *

Esse site utiliza o Akismet para reduzir spam. Aprenda como seus dados de comentários são processados.