5 pontos por GN⁺ 4 시간 전 | 1 comentários | Compartilhar no WhatsApp
  • A velocidade do terminal que você usa o dia inteiro influencia diretamente sua eficiência, e pequenos atrasos ao abrir uma nova aba, digitar ou usar autocompletar se acumulam centenas de vezes por dia e viram ineficiência
  • Um shell interativo totalmente carregado, incluindo autocompletar, destaque de sintaxe, sugestões automáticas, fzf e direnv, passa a iniciar em cerca de 30 milissegundos, e novas abas passam a abrir instantaneamente
  • O maior segredo é não usar frameworks nem gerenciadores de plugins como oh-my-zsh ou prezto; em vez disso, apenas 3 plugins são clonados diretamente com git e carregados via source no .zshrc
  • Com cache do compinit, lazy-loading, prompt assíncrono e terminal com aceleração por GPU, os atrasos de inicialização, prompt e entrada são todos minimizados
  • A maior parte das otimizações não vem de adicionar coisas, mas de remover o desnecessário; a chave é a disciplina de acrescentar só o que você realmente usa

Por que um terminal rápido é necessário

  • Quase todo o trabalho acontece dentro do terminal, com uso constante de Git, kubectl, tmux e conexões ssh para servidores ao longo do dia
  • Ferramentas usadas com tanta frequência precisam ser rápidas, e o atraso ao abrir novas abas, digitar caracteres e usar autocompletar é sentido centenas de vezes por dia
  • Esse acúmulo de pequenos atrasos é como morrer por mil cortes (death by a thousand cuts)

Resultado da medição da velocidade de inicialização do shell

  • Após as melhorias, o shell inicia em cerca de 30 milissegundos, medido com o comando for i in {1..5}; do /usr/bin/time zsh -i -c exit; done
  • Um shell interativo completo com autocompletar, destaque de sintaxe, sugestões automáticas, fzf e direnv carrega em menos tempo do que um único frame a 30 fps
  • Isso não foi resultado de um grande projeto de otimização, mas de um hábito mantido por anos de deixar o shell mínimo e rápido
  • Todas as configurações estão públicas no repositório de dotfiles

Sem framework

  • O maior ganho vem justamente daquilo que não existe: não usar oh-my-zsh, prezto nem gerenciadores de plugins
  • Mesmo usando só cerca de 5% dos centenas de plugins e temas do oh-my-zsh, você acaba pagando o custo de tempo e recursos computacionais dos 95% restantes toda vez que abre o shell
  • Gerenciadores de plugins ainda adicionam overhead extra por cima disso
  • São usados exatamente 3 plugins, clonados uma única vez pelo script de instalação e carregados com source no .zshrc
    • fzf-tab, zsh-autosuggestions, zsh-syntax-highlighting
    • Não há gerenciador de plugins resolvendo dependências na inicialização, e dar source em arquivos que já estão no disco praticamente não tem custo

Cache de autocompletar

  • O compinit é uma das operações mais custosas em um .zshrc comum e, por padrão, faz uma auditoria de segurança em todos os arquivos de autocompletar toda vez que o shell é aberto
  • A solução é fazer a execução completa apenas quando o cache (.zcompdump) tiver mais de 24 horas; fora isso, usar -C para pular a verificação
    • O qualificador de glob #qNmh-24 significa "existe e foi modificado nas últimas 24 horas"
    • Assim, o compinit completo roda só uma vez por dia, e no restante do tempo usa leitura em cache
    Publicidade

Lazy-loading

  • O nvm é um dos vilões mais conhecidos da lentidão na inicialização do shell; se for carregado com source logo na partida, pode facilmente adicionar 0,5 segundo
  • Nem todo shell precisa de nvm, então ele só é necessário quando você digita nvm; por isso, ele é envolvido em uma função que se substitui no primeiro uso
    • A primeira chamada de nvm remove o stub, faz source do nvm real (com --no-use para impedir resolução de versão do node) e repassa os argumentos
  • O autocompletar do kubectl segue a mesma ideia: como ele chama o binário kubectl para gerar o script de autocompletar, só é carregado depois da primeira execução real
  • Toda ferramenta que manda colocar eval "$(tool init zsh)" no .zshrc cria um processo na inicialização e avalia a saída, então vira candidata a lazy-loading
  • direnv e fzf são rápidos e usados com frequência, então continuam sendo carregados imediatamente; é preciso julgar com rigor o que você realmente usa com frequência

Prompt não bloqueante

  • Um prompt que executa git status de forma síncrona sofre atrasos em repositórios minimamente grandes, e isso pode ser pior do que uma inicialização lenta porque é percebido toda vez que se pressiona Enter
  • É usado o pure, que renderiza o prompt imediatamente e preenche as informações do git de forma assíncrona quando estiverem prontas
  • Houve uma breve tentativa de trocar pelo vcs_info embutido do zsh, mas o comportamento assíncrono do pure foi melhor
  • Também seria possível implementar manualmente um git status assíncrono no próprio prompt, mas o pure já encapsula bem esse caso de uso

O próprio emulador de terminal

  • A inicialização do shell é só metade da história; o próprio emulador pode adicionar latência de entrada
  • É usado o Ghostty, um terminal nativo com aceleração por GPU, com uma configuração de apenas 7 linhas
  • Em conjunto com o alias t para tmux new -A -s main, uma nova janela do terminal retorna imediatamente para a sessão existente
Publicidade

Como medir o desempenho do seu shell

  • Dá para medir diretamente no terminal onde o tempo está sendo gasto, e há três tipos de atraso a observar: tempo de inicialização, atraso do prompt e atraso de entrada
  • A medição básica é executar time zsh -i -c exit algumas vezes; a primeira execução sempre é mais lenta por causa do cache frio
    • Menos de 100 ms está ok, menos de 50 ms é excelente, e acima de 500 ms já indica que há algo para ajustar
  • Para estatísticas mais precisas, use o hyperfine: hyperfine --warmup 3 'zsh -i -c exit'
  • Aproveite o profiler embutido do zsh
    • Coloque zmodload zsh/zprof no topo do .zshrc e zprof no fim para obter uma tabela ordenada mostrando onde o tempo foi gasto
    • Os itens no topo normalmente são compinit, source de nvm.sh e eval "$(...)"; corrija começando pelo primeiro e repita a medição
    • Ao terminar, remova essas duas linhas
  • Se o zprof não for suficiente, rastreie toda a inicialização com timestamps: zsh -ixc exit 2>&1 | ts -i '%.s' | sort -rn | head -20
    • Ou defina PS4='+%D{%s.%6.}: ' e rode zsh -ixc exit 2> startup.log, depois verifique os grandes saltos entre linhas
  • A inicialização pode ser rápida, mas o redraw do prompt ainda ser lento; entre no maior repositório git com cd e pressione Enter — se houver atraso até o próximo prompt aparecer, então o prompt está fazendo trabalho síncrono
    • Nesse caso, você pode migrar para um prompt assíncrono ou remover os recursos de Git

Encerrando

  • A maior parte da otimização consiste em remover coisas; o mais importante é agir com intenção e adicionar apenas o que você realmente vai usar
  • Assim, todas as dezenas de sessões abertas ao longo do dia passam a surgir instantaneamente, e o terminal deixa de ser um aplicativo que faz você esperar para virar uma extensão da sua mente
  • Para uma ferramenta usada o dia inteiro, essa velocidade é inegociável
  • Todas as configurações acima estão públicas no repositório de dotfiles

1 comentários

 
GN⁺ 4 시간 전
Opiniões no Lobste.rs
  • Tecnicamente, na maioria dos casos isso quer dizer shell, não terminal

  • É melhor usar uma ferramenta com bons padrões, então é só usar fish

    • O ZSH da empresa ficou absurdamente lento mais ou menos há um ano, então experimentei o fish, e gostei muito dos recursos de qualidade de vida
      Gostei de ele já vir com coisas modernas por padrão, como autocompletar com Tab em que dá para escolher com as setas; ainda uso ZSH na minha máquina pessoal, mas só porque não tive tempo de mexer nas configurações do Nix e do home manager
    • Seria bom se alguém fizesse um fish compatível com bash
      Um shell com padrões sensatos e autocompletar rápido embutido, sem precisar abandonar nem reescrever ferramentas baseadas em bash
    • A vida é curta demais para instalar ferramentas novas; só preciso de padrões sensatos
  • Às vezes me pergunto se coisas como prompt não bloqueante ou terminais baseados em OpenGL realmente valem mais do que usar algo como PS1="\W: " no xterm

    • Fiquei anos sem usar xterm de propósito, mas ao olhar vários emuladores de terminal me surpreendi ao ver que o xterm suporta fontes OpenType, UTF-8, a maioria dos emojis, cores de 24 bits e ainda usa pouca memória
      Além disso, é muito rápido e tem a vantagem de ser o “padrão”, então os bugs que restam em geral são pequenos ou os programas rodando nele provavelmente vão tratá-los como comportamento normal
      Então voltei a usar xterm
    • Não vale tanto assim
      A inicialização do zsh já é muito rápida por natureza, e só fica lenta quando o usuário a torna lenta
      Basta não encher de coisas que você não entende; isso também inclui bibliotecas que se dizem “minimalistas”, mas executam centenas de comandos toda vez que montam o prompt
      Minha configuração do zsh tem algumas centenas de linhas e evoluiu bem devagar desde os anos 90; entendo todas elas e sei por que estão lá
      Nunca tentei deixá-la especialmente rápida, mas ainda assim inicia em 20 ms, e se eu fizer alguma mudança idiota que a deixe lenta, percebo na hora e conserto
  • Não gosto que benchmarks quebrados como time zsh -i -c exit ainda sejam tão usados
    Eles medem um alvo completamente errado, e alguns gerenciadores de plugins do zsh chegaram a otimizar para essa métrica inútil sacrificando a latência real de inicialização do shell
    O zsh-bench tem uma seção explicando por que esse benchmark não faz sentido: https://github.com/romkatv/zsh-bench#how-not-to-benchmark
    Métricas como latência até o primeiro prompt ou latência de entrada, que o zsh-bench mede, são muito mais úteis

  • Fiquei feliz por não ser uma discussão sobre bugs de terminal com aceleração por GPU
    Cache de completions é uma boa dica; uso zsh no Mac do trabalho, e só de pensar em abrir uma aba nova já aparece a bolinha colorida, então espero que ajude
    No caso do completion do kubectl, fico curioso se a lentidão está na geração do completion ou no carregamento; se for o primeiro caso, talvez salvar em arquivo e depois carregar reduza o tempo de inicialização
    Faço isso no jj, e ao migrar para jj abandonei prompts que executavam git status
    Pena que o autor não mostrou também o próprio tempo, porque aí eu saberia se meus 0,287 s são média ou lentos
    Depois medi melhor: um .bashrc quase vazio leva 0,007 s, depois dos atalhos do skim 0,043 s, depois do mise 0,115 s, depois do completion do jj 0,186 s, e lendo até /etc/bashrc vai para 0,294 s, então parece haver espaço para melhorar

    • No texto, o próprio shell aparecia com 30 ms logo de cara, e no mesmo teste time shell -c exit o meu dá algo em torno de 50 ms
      O que mais me irrita ao usar o Linux de outras pessoas são as animações inúteis espalhadas por todo lado
      No meu computador, aperto o atalho e a janela do terminal abre quase instantaneamente; às vezes só vejo um piscar curto entre a janela e o prompt
      Por isso, o teste de ponta a ponta completo — abrir uma nova janela, fazer algo no shell e fechar — é o que importa, e com time myterm seguido de Ctrl+D para fechar a janela, sempre ficou abaixo de 0,120 s
      Quando você elimina animações inúteis e composição, muita coisa passa a ser possível; até para comparar duas planilhas, eu maximizava duas janelas e alternava rapidamente com um atalho de enrolar janelas, e a diferença aparecia na hora
      Tentar fazer o mesmo no Windows com as animações do Excel é dispersivo demais
    • Menos de 100 ms parece difícil no meu ambiente
      Mesmo com configuração vazia, zsh -i -c exit dá em média 129,8 ms, e com a configuração completa fica parecido, em torno de 250 ms
      Com cache do compinit consegui reduzir cerca de 5 ms em média, mas como completions podem ficar faltando, não acho que o esforço valha tanto a pena
  • Recentemente a inicialização do zsh ficou tão lenta que parecia quase travada; não descobri exatamente a causa, mas confirmei que o compinit ocupava a maior parte do caminho crítico
    Implementei cache quase do mesmo jeito sugerido no texto e eliminei a lentidão, e ao ver aquele glob qualifier elegante pensei que deveria melhorar minha própria solução
    Eu nem sabia que dava para fazer aquilo; sinceramente parece meio suspeito, mas ainda assim vou usar
    Antes, eu usava a abordagem relativamente grosseira de date -Id para criar o caminho de destino
    Gosto de ferramentas configuradas com uma linguagem de programação completa como o zsh, porque dá para implementar esse tipo de coisa sem esperar o autor adicionar um recurso de cache
    Em quase 20 anos usando zsh, nunca usei framework nem gerenciador de plugins; parece que essas coisas são usadas principalmente por estilo
    Tenho a sorte de não me importar com a estética do meu ambiente computacional, e meu prompt caseiro também é básico, pequeno e informativo, mas nada chamativo; uso o tema padrão do terminal com fundo preto

    • O cache do compinit é frustrante porque pode ficar desatualizado
      Várias instâncias do shell podem acabar fazendo a mesma coisa em paralelo, e eu via isso com frequência ao subir instâncias paralelas de laboratório no tmux
      Também dá para compartilhar o diretório home entre vários hosts, especialmente contêineres, então no fim organizei tudo com um esquema que inclui arquivo de lock, verificação de expiração e tratamento condicional de zcompile
    • O tempo de carregamento do ZSH ficou tão ruim que acabei simplesmente testando o fish
      Infelizmente, parece que a configuração do fish também foi escorregando aos poucos na mesma direção, e pretendo fazer profiling na folga de segunda-feira para ver se técnicas de lazy loading realmente ajudam no meu caso
      A maior parte da lentidão provavelmente vem do módulo git do Starship, mas também tenho várias aliases e funções auxiliares que poderiam ser carregadas sob demanda
  • No Emacs, há muito tempo já deixam um shell de staging em segundo plano pré-inicializado
    Abrir um terminal significa abrir uma nova janela para esse buffer e renomeá-lo, e fazer fork de uma thread que prepara outro shell para a próxima vez
    Então não há latência de inicialização
    Lembro que no passado tentei forçar uma solução fora do Emacs com reptyr, mas no fim não continuei usando aquilo, e já nem lembro direito o motivo
    https://github.com/nelhage/reptyr

    • Gosto disso; parece o processo zygote do Android
  • Investigando algo parecido, descobri que o zsh-abbr consome cerca de 100 ms do tempo de inicialização, mas isso para mim é aceitável
    Dá para cortar 10 ms aqui e ali, mas considerando a funcionalidade perdida, não parece valer a pena
    Vou viver com cerca de 300 ms de inicialização; é rápido o bastante, e raramente fico abrindo terminais em sequência ou preciso digitar imediatamente
    Ainda assim, o texto foi bom, conheci o hyperfine e acabei olhando alguns arquivos de inicialização do zsh

  • Graças a isso, finalmente mexi no zshrc que vinha adiando havia muito tempo, e agora caiu para 80 ms, o que está ótimo

  • Minha vida é longa o bastante para aguentar terminais lentos, e às vezes eu até queria que o terminal fosse mais lento
    Por exemplo, se no console root houvesse um atraso padrão de 5 segundos antes da execução real, para dar tempo de cancelar um erro de digitação com Ctrl+C, talvez eu tivesse economizado alguns dias na minha juventude rebelde