5 points par GN⁺ 4 시간 전 | 1 commentaires | Partager sur WhatsApp
  • La vitesse du terminal utilisé toute la journée influence directement l’efficacité du travail, et les micro-latences à l’ouverture d’un nouvel onglet, à la frappe ou à l’autocomplétion deviennent inefficaces lorsqu’elles s’accumulent des centaines de fois par jour
  • Un shell interactif complètement chargé, avec autocomplétion, coloration syntaxique, autosuggestions, fzf et direnv, démarre désormais en environ 30 millisecondes, et les nouveaux onglets s’ouvrent instantanément
  • Le plus grand secret est de ne pas utiliser de framework ni de gestionnaire de plugins comme oh-my-zsh ou prezto, mais de simplement git clone trois plugins puis les source dans le .zshrc
  • Le cache de compinit, le lazy-loading, un prompt asynchrone et un terminal accéléré par GPU permettent de minimiser à la fois la latence au démarrage, celle du prompt et celle de la saisie
  • La plupart des optimisations ne consistent pas à ajouter quelque chose, mais à retirer l’inutile, avec comme principe clé la discipline de n’ajouter intentionnellement que ce qui est réellement utilisé

Pourquoi un terminal rapide est nécessaire

  • Presque tout le travail se fait dans le terminal, avec Git, kubectl, tmux, les connexions SSH aux serveurs, etc., utilisés toute la journée
  • Les outils aussi fréquemment utilisés doivent donc être rapides, et la latence à l’ouverture d’un nouvel onglet, à la saisie de caractères ou à l’autocomplétion au Tab se ressent des centaines de fois par jour
  • L’accumulation de ces micro-latences ressemble à une mort par mille coupures

Résultats de mesure de la vitesse de démarrage du shell

  • Après optimisation, le shell démarre en environ 30 millisecondes, mesuré avec la commande for i in {1..5}; do /usr/bin/time zsh -i -c exit; done
  • Un shell interactif complet incluant autocomplétion, coloration syntaxique, autosuggestions, fzf et direnv se charge en moins de temps qu’une seule frame à 30 fps
  • Ce n’est pas le résultat d’un grand projet d’optimisation, mais d’une habitude prise au fil des années pour garder le shell minimal et rapide
  • Toute la configuration est publiée dans le dépôt dotfiles

Sans framework

  • Le plus gros gain vient de ce qui n’existe pas : aucun usage de oh-my-zsh, prezto ou de gestionnaire de plugins
  • Lorsqu’on n’utilise qu’environ 5 % des centaines de plugins et thèmes de oh-my-zsh, on paie malgré tout, à chaque ouverture du shell, le coût en temps et en ressources de calcul des 95 % restants
  • Un gestionnaire de plugins ajoute encore de l’overhead par-dessus
  • Exactement 3 plugins sont utilisés, clonés une fois par le script d’installation puis source dans le .zshrc
    • fzf-tab, zsh-autosuggestions, zsh-syntax-highlighting
    • Il n’y a pas de gestionnaire de plugins qui résout les dépendances au démarrage, et source des fichiers déjà présents sur le disque ne coûte pratiquement rien

Cache de l’autocomplétion

  • compinit est l’une des opérations les plus coûteuses dans un .zshrc classique, car il effectue par défaut un audit de sécurité de tous les fichiers d’autocomplétion à chaque ouverture du shell
  • La solution consiste à n’exécuter le lancement complet que si le cache (.zcompdump) a plus de 24 heures, et à utiliser sinon -C pour sauter la vérification
    • Le qualificateur glob #qNmh-24 signifie « existe et a été modifié dans les dernières 24 heures »
    • Un compinit complet n’est donc exécuté qu’une fois par jour, le reste du temps on utilise la lecture en cache
    Publicité

Chargement différé (Lazy-loading)

  • nvm est l’un des coupables les plus notoires du ralentissement au démarrage du shell : le charger immédiatement au lancement peut facilement ajouter 0,5 seconde
  • nvm n’est pas nécessaire dans tous les shells, seulement quand on tape nvm, donc il est encapsulé dans une fonction qui se remplace elle-même lors de sa première utilisation
    • Le premier appel à nvm supprime le stub, charge le vrai nvm (avec --no-use pour éviter aussi la résolution de version de node) puis transmet les arguments
  • L’autocomplétion de kubectl est gérée de la même façon : comme le script d’autocomplétion est généré en appelant le binaire kubectl, il n’est chargé qu’après la première exécution réelle
  • Tous les outils qui recommandent d’ajouter eval "$(tool init zsh)" dans le .zshrc sont des candidats au lazy-loading, car ils forkent un processus au démarrage puis évaluent sa sortie
  • direnv et fzf sont gardés en chargement immédiat parce qu’ils sont rapides et souvent utilisés ; il faut juger avec rigueur ce qui sert réellement souvent

Prompt non bloquant

  • Un prompt qui exécute git status de manière synchrone introduit de la latence dans les dépôts un peu volumineux, et cela peut être pire qu’un démarrage lent puisque c’est perceptible à chaque pression sur Entrée
  • Le prompt pure est utilisé : il s’affiche immédiatement puis remplit les informations Git de manière asynchrone lorsqu’elles sont prêtes
  • Un remplacement par vcs_info, intégré à zsh, a été brièvement essayé, mais le comportement asynchrone de pure était meilleur
  • Il serait aussi possible d’implémenter directement un git status asynchrone dans le prompt, mais pure encapsule déjà très bien ce cas d’usage

L’émulateur de terminal lui-même

  • Le démarrage du shell ne raconte que la moitié de l’histoire : l’émulateur lui-même ajoute aussi de la latence de saisie
  • Ghostty, un terminal natif accéléré par GPU, est utilisé, avec seulement 7 lignes de configuration
  • Combiné à l’alias t pour tmux new -A -s main, une nouvelle fenêtre de terminal ramène immédiatement à la session existante
Publicité

Comment mesurer les performances de son shell

  • Il est possible de mesurer directement dans le terminal où le temps est dépensé ; les trois latences à vérifier sont le temps de démarrage, la latence du prompt et la latence de saisie
  • La mesure de base consiste à exécuter time zsh -i -c exit plusieurs fois ; la première exécution est toujours plus lente à cause du cache froid
    • En dessous de 100 ms, c’est correct ; sous 50 ms, c’est excellent ; au-delà de 500 ms, il y a probablement quelque chose à corriger
  • Pour des statistiques précises, utiliser hyperfine : hyperfine --warmup 3 'zsh -i -c exit'
  • Utiliser aussi le profileur intégré à zsh
    • Ajouter zmodload zsh/zprof tout en haut du .zshrc, puis zprof tout en bas, pour afficher un tableau trié de l’endroit où le temps est consommé
    • Les éléments en tête sont souvent compinit, le source de nvm.sh ou eval "$(...)" ; il faut corriger à partir du haut de la liste puis relancer en boucle
    • Une fois terminé, supprimer ces deux lignes
  • Si zprof ne suffit pas, on peut suivre tout le démarrage avec des timestamps : zsh -ixc exit 2>&1 | ts -i '%.s' | sort -rn | head -20
    • Ou définir PS4='+%D{%s.%6.}: ' puis lancer zsh -ixc exit 2> startup.log afin d’identifier les grands sauts entre les lignes
  • Le démarrage peut être rapide alors que le redraw du prompt reste lent : il suffit de faire cd dans le plus gros dépôt Git, puis d’appuyer sur Entrée ; s’il y a un délai avant l’apparition du prompt suivant, c’est que le prompt effectue du travail synchrone
    • Les options sont alors de passer à un prompt asynchrone ou de retirer les fonctionnalités Git

Conclusion

  • La plupart des optimisations consistent à retirer des choses ; la clé est d’agir intentionnellement et de n’ajouter que ce qui sera réellement utilisé
  • Ainsi, les dizaines de sessions ouvertes chaque jour s’ouvrent toutes instantanément, et le terminal cesse d’être une application qui fait attendre pour devenir un prolongement de l’esprit
  • Pour un outil utilisé toute la journée, cette vitesse n’est pas négociable
  • Toute la configuration ci-dessus est publiée dans le dépôt dotfiles

1 commentaires

 
GN⁺ 4 시간 전
Avis sur Lobste.rs
  • Techniquement, dans la plupart des cas, on parle non pas du terminal, mais du shell

  • Mieux vaut utiliser un outil avec de bons réglages par défaut, donc il suffit d’utiliser fish

    • Le ZSH de mon entreprise est devenu absurdement lent il y a environ un an, alors j’ai essayé fish, et j’ai vraiment aimé ses fonctions qui améliorent la qualité de vie
      J’ai apprécié qu’il propose par défaut des fonctionnalités modernes comme une complétion par tabulation que l’on peut parcourir avec les touches fléchées ; sur ma machine personnelle, j’utilise encore ZSH, mais seulement parce que je n’ai pas eu le temps d’ajuster ma configuration Nix et home manager
    • J’aimerais que quelqu’un crée un fish compatible bash
      Un shell avec des valeurs par défaut raisonnables et une complétion intégrée rapide, sans devoir abandonner ou réécrire les outils basés sur bash
    • La vie est trop courte pour installer de nouveaux outils ; il suffit d’avoir des valeurs par défaut raisonnables
  • Je me demande parfois si des choses comme les prompts non bloquants ou les terminaux basés sur OpenGL valent vraiment mieux qu’un xterm avec juste PS1="\W: "

    • Pendant des années, j’ai volontairement évité xterm, mais après avoir regardé plusieurs émulateurs de terminal, j’ai été assez surpris de voir que xterm prend en charge les polices OpenType, l’UTF-8, la plupart des emojis, la couleur 24 bits et une faible consommation mémoire
      En plus, il est très rapide et a l’avantage d’être le « standard », donc les bugs qui restent sont en général mineurs ou concernent des programmes qui risquent de considérer ce comportement comme normal
      Du coup, je suis revenu à xterm
    • Ça n’en vaut pas vraiment la peine
      Le démarrage de zsh est à l’origine très rapide, et il ne devient lent que si l’utilisateur le rend lent
      Il suffit de ne pas y empiler des tas de choses qu’on ne comprend pas, y compris des bibliothèques qui se disent « minimalistes » tout en exécutant des centaines de commandes à chaque construction du prompt
      Ma configuration zsh fait quelques centaines de lignes, a évolué très lentement depuis les années 90, et je comprends chaque ligne ainsi que la raison de sa présence
      Je n’ai jamais cherché spécialement à la rendre rapide, mais elle démarre toujours en 20 ms, et si je fais une modification stupide susceptible de la ralentir, je m’en rends compte tout de suite et je peux la corriger
  • Je n’aime pas que des benchmarks cassés comme time zsh -i -c exit soient encore couramment utilisés
    Ils mesurent complètement la mauvaise chose, et certains gestionnaires de plugins zsh se sont optimisés pour cette métrique inutile au prix de la latence réelle au démarrage du shell
    zsh-bench a une section qui explique pourquoi ce benchmark n’a aucun sens : https://github.com/romkatv/zsh-bench#how-not-to-benchmark
    Des métriques comme la latence jusqu’au premier prompt ou la latence de saisie, que mesure zsh-bench, sont bien plus utiles

  • Je croyais qu’on allait parler de bugs de terminaux accélérés par GPU, donc j’étais content que ce ne soit pas le cas
    Le cache de complétion est un bon conseil, et j’utilise zsh sur un Mac d’entreprise où il suffit de penser à ouvrir un nouvel onglet pour voir apparaître la roue colorée, donc j’espère que ça aidera
    Pour la complétion kubectl, je me demande si la lenteur vient de la génération de la complétion ou de son chargement ; si c’est la première, je me demande si l’enregistrer dans un fichier puis la relire réduirait le temps de démarrage
    C’est ce qu’ils font dans jj, et en passant à jj, j’ai abandonné les prompts qui exécutent git status
    J’aurais aimé que l’auteur montre aussi ses propres temps, pour savoir si mes 0,287 s sont dans la moyenne ou plutôt lents
    Après avoir mesuré ensuite, un .bashrc presque vide prend 0,007 s, après les raccourcis clavier skim 0,043 s, après mise 0,115 s, après la complétion jj 0,186 s, et avec /etc/bashrc chargé aussi 0,294 s, donc il semble y avoir de la marge d’amélioration

    • L’article disait que le shell lui-même prenait 30 ms en amont, et sur le même test time shell -c exit, le mien tourne autour de 50 ms
      Ce qui m’agace le plus quand j’utilise l’environnement Linux de quelqu’un d’autre, ce sont toutes ces animations inutiles partout
      Sur mon ordinateur, quand j’appuie sur un raccourci, la fenêtre de terminal s’ouvre presque instantanément, et parfois on ne voit qu’un très bref clignotement entre la fenêtre et le prompt
      C’est pourquoi un test complet de bout en bout, consistant à ouvrir une nouvelle fenêtre, faire quelque chose dans le shell puis la refermer, est important ; en faisant time myterm puis Ctrl+D dans la fenêtre pour la fermer, j’étais toujours sous les 0,120 s
      Quand on supprime les animations et la composition inutiles, beaucoup de choses deviennent possibles ; même pour comparer deux feuilles de calcul, je maximisais deux fenêtres puis je basculais rapidement entre elles avec un raccourci d’enroulement de fenêtre, et la différence sautait immédiatement aux yeux
      Faire la même chose sous Windows avec les animations d’Excel est beaucoup trop distrayant
    • Moins de 100 ms semble difficile dans mon environnement
      Même avec une configuration vide, zsh -i -c exit fait en moyenne 129,8 ms, et avec ma configuration complète, c’est à peu près 250 ms, donc dans le même ordre de grandeur
      Le cache compinit m’a bien fait gagner environ 5 ms en moyenne, mais comme certaines complétions peuvent manquer, je ne pense pas que l’effort en vaille vraiment la peine
  • Récemment, le démarrage de zsh est devenu presque bloqué tant il était lent, et même si je n’ai pas identifié précisément la cause, j’ai confirmé que compinit occupait la majeure partie du chemin critique
    J’ai mis en place un cache presque identique à la méthode proposée dans l’article, ce qui a supprimé le ralentissement, et en voyant ce joli glob qualifier, je me suis dit que je devrais améliorer ma propre méthode
    Je ne savais même pas que ce genre de fonctionnalité existait, et pour être honnête ça a même un petit côté suspect, mais je vais quand même l’utiliser
    Avant cela, j’utilisais une approche assez grossière avec date -Id pour créer le chemin cible
    J’aime les outils configurés comme de véritables langages de programmation, comme zsh, où l’on peut implémenter soi-même ce genre de cache sans attendre que l’auteur ajoute la fonctionnalité
    J’utilise zsh depuis presque 20 ans sans jamais avoir recours à un framework ni à un gestionnaire de plugins, et j’ai l’impression que ces outils servent surtout au style
    J’ai la chance de ne pas me soucier de l’esthétique de mon environnement informatique, et mon prompt maison est lui aussi basique, compact et informatif, mais pas du tout tape-à-l’œil, avec le thème de terminal par défaut sur fond noir

    • Le cache de compinit est frustrant parce que le cache peut devenir obsolète
      Plusieurs instances de shell peuvent aussi faire la même chose en parallèle, ce qui m’est souvent arrivé en lançant des instances parallèles de test dans tmux
      On peut aussi partager son répertoire personnel entre plusieurs hôtes, en particulier entre des conteneurs ; j’ai donc fini par mettre de l’ordre là-dedans avec une méthode comprenant fichier de verrouillage, vérification d’expiration et gestion conditionnelle de zcompile
    • Le temps de chargement de ZSH était devenu tellement mauvais que j’ai simplement essayé fish
      Malheureusement, la configuration fish semble elle aussi avoir lentement dérivé dans la même direction ; je vais donc faire du profiling pendant une pause lundi pour vérifier si les techniques de chargement différé sont réellement utiles dans mon cas
      Je soupçonne que la majeure partie du temps perdu vient du module git de Starship, mais j’ai aussi pas mal d’alias et de fonctions utilitaires qui pourraient être chargés à la demande
  • Dans Emacs, on préinitialise depuis longtemps un shell de staging en arrière-plan
    Ouvrir un terminal consiste à ouvrir une nouvelle fenêtre sur ce buffer et à le renommer, puis à forker un thread qui prépare à nouveau le shell pour la prochaine fois
    Il n’y a donc aucune latence au démarrage
    Je me souviens avoir autrefois essayé de bricoler une solution hors d’Emacs avec reptyr, mais au final je n’ai pas continué à l’utiliser, sans bien me rappeler pourquoi
    https://github.com/nelhage/reptyr

    • Ça fait penser au processus zygote d’Android, et j’aime bien l’idée
  • En fouillant un peu de la même manière, j’ai découvert que zsh-abbr me coûtait environ 100 ms au démarrage, mais ça me va
    Je pourrais sûrement gratter 10 ms par-ci par-là, mais vu les fonctionnalités perdues, ça n’en vaut pas la peine
    Je vais vivre avec un temps de démarrage d’environ 300 ms ; c’est largement assez rapide, et j’ouvre rarement des terminaux à la chaîne ou avec la nécessité de taper immédiatement
    Cela dit, l’article était bon, j’ai découvert hyperfine, et ça m’a poussé à regarder quelques fichiers de démarrage zsh

  • Grâce à ça, j’ai enfin modifié mon zshrc, que je repoussais depuis longtemps, et je suis maintenant descendu à 80 ms, ce qui est excellent

  • Ma vie est assez longue pour supporter un terminal lent, et parfois j’aimerais même qu’un terminal soit plus lent
    Par exemple, si une console root imposait par défaut un délai de 5 secondes avant l’exécution réelle, pour laisser le temps d’annuler une faute de frappe avec Ctrl+C, ça m’aurait peut-être évité de perdre quelques jours pendant ma jeunesse rebelle