5 Punkte von GN⁺ 4 시간 전 | 1 Kommentare | Auf WhatsApp teilen
  • Die Geschwindigkeit des Terminals, das man den ganzen Tag nutzt, bestimmt die Arbeitseffizienz; wenn sich minimale Verzögerungen beim Öffnen neuer Tabs, Tippen und bei der Autovervollständigung hunderte Male am Tag summieren, wird das ineffizient
  • Eine vollständig geladene interaktive Shell startet trotz Autovervollständigung, Syntax-Highlighting, Autosuggestions, fzf und direnv in rund 30 Millisekunden, und neue Tabs öffnen sich sofort
  • Der größte Hebel ist, keine Frameworks oder Plugin-Manager wie oh-my-zsh oder prezto zu verwenden; stattdessen werden nur drei Plugins direkt per git clone eingebunden und in .zshrc per source geladen
  • Mit compinit-Caching, Lazy-Loading, asynchronem Prompt und einem GPU-beschleunigten Terminal werden Start-, Prompt- und Eingabeverzögerungen konsequent minimiert
  • Die meisten Optimierungen bestehen nicht darin, etwas hinzuzufügen, sondern Unnötiges wegzulassen; entscheidend ist die Disziplin, nur das bewusst zu ergänzen, was man wirklich häufig nutzt

Warum ein schnelles Terminal wichtig ist

  • Fast alle Arbeiten finden im Terminal statt, und Git, kubectl, tmux sowie SSH-Verbindungen zu Servern werden den ganzen Tag verwendet
  • Gerade deshalb müssen häufig genutzte Werkzeuge schnell sein; Verzögerungen beim Öffnen neuer Tabs, bei der Texteingabe und bei der Tab-Autovervollständigung fallen hunderte Male pro Tag auf
  • Solche kleinen Verzögerungen, die sich aufsummieren, sind wie der Tod durch tausend Schnitte (death by a thousand cuts)

Messergebnisse zur Shell-Startgeschwindigkeit

  • Nach den Optimierungen startet die Shell in etwa 30 Millisekunden; gemessen wurde mit for i in {1..5}; do /usr/bin/time zsh -i -c exit; done
  • Eine vollständige interaktive Shell mit Autovervollständigung, Syntax-Highlighting, Autosuggestions, fzf und direnv lädt in weniger als der Dauer eines einzelnen Frames bei 30 fps
  • Dahinter steckt kein einzelnes großes Optimierungsprojekt, sondern das über Jahre gepflegte Verhalten, die Shell klein und schnell zu halten
  • Die gesamte Konfiguration ist im dotfiles-Repository veröffentlicht

Keine Frameworks

  • Der größte Vorteil kommt von Dingen, die gar nicht vorhanden sind: Es werden weder oh-my-zsh noch prezto oder Plugin-Manager verwendet
  • Wenn man von den hunderten Plugins und Themes in oh-my-zsh nur etwa 5 % nutzt, bezahlt man beim Öffnen jeder Shell dennoch den Zeit- und Rechenaufwand für die übrigen 95 %
  • Plugin-Manager legen darauf noch zusätzlichen Overhead
  • Verwendet werden genau 3 Plugins, die ein Installationsskript einmal per git clone holt und die dann in .zshrc per source geladen werden
    • fzf-tab, zsh-autosuggestions, zsh-syntax-highlighting
    • Es gibt keinen Plugin-Manager, der beim Start Abhängigkeiten auflöst; Dateien zu sourcen, die bereits auf der Platte liegen, kostet praktisch nichts

Caching der Autovervollständigung

  • compinit ist in einer typischen .zshrc einer der teuersten Schritte, weil standardmäßig bei jedem Öffnen der Shell ein Sicherheitsaudit für alle Autovervollständigungsdateien durchgeführt wird
  • Die Lösung ist, den kompletten Lauf nur dann auszuführen, wenn der Cache (.zcompdump) älter als 24 Stunden ist; sonst wird die Prüfung mit -C übersprungen
    • Der Glob-Qualifier #qNmh-24 bedeutet „existiert und wurde innerhalb der letzten 24 Stunden geändert“
    • So läuft compinit nur einmal pro Tag vollständig, und den Rest der Zeit wird der Cache verwendet
    Anzeige

Lazy-Loading

  • nvm ist einer der berüchtigtsten Bremsklötze für den Shell-Start; wenn es direkt beim Start gesourct wird, kommen schnell 0,5 Sekunden hinzu
  • nvm wird nicht in jeder Shell gebraucht, sondern nur, wenn man nvm eingibt; deshalb wird es in eine Funktion verpackt, die sich beim ersten Aufruf selbst ersetzt
    • Der erste nvm-Aufruf entfernt den Stub, sourct das echte nvm (mit --no-use, damit keine Node-Version aufgelöst wird) und reicht dann die Argumente weiter
  • Die kubectl-Autovervollständigung funktioniert nach demselben Prinzip: Sie erzeugt ihr Skript durch Aufruf des kubectl-Binaries und wird daher erst nach der ersten tatsächlichen Nutzung geladen
  • Alle Tools, die dazu auffordern, eval "$(tool init zsh)" in .zshrc einzufügen, sind Kandidaten für Lazy-Loading, weil sie beim Start einen Prozess forken und dessen Ausgabe auswerten
  • direnv und fzf sind schnell und werden oft genutzt, deshalb bleiben sie direkt geladen; entscheidend ist eine strenge Einschätzung, was man tatsächlich häufig verwendet

Nicht blockierender Prompt

  • Ein Prompt, der git status synchron ausführt, verursacht in halbwegs großen Repositories Verzögerungen; da man sie bei jedem Druck auf Enter spürt, kann das schlimmer sein als ein langsamer Start
  • Verwendet wird pure, das den Prompt sofort rendert und Git-Informationen asynchron nachliefert, sobald sie bereit sind
  • Ein kurzer Versuch, es durch das in zsh eingebaute vcs_info zu ersetzen, fiel zugunsten des asynchronen Verhaltens von pure aus
  • Man könnte asynchronen git status auch direkt im eigenen Prompt implementieren, aber pure kapselt genau diesen Anwendungsfall sehr gut

Der Terminal-Emulator selbst

  • Der Shell-Start ist nur die halbe Geschichte; auch der Emulator selbst kann Eingabeverzögerung hinzufügen
  • Verwendet wird Ghostty, ein nativer GPU-beschleunigter Terminal-Emulator, und die Konfiguration umfasst nur sieben Zeilen
  • In Kombination mit dem Alias t für tmux new -A -s main landet ein neues Terminalfenster sofort wieder in der bestehenden Sitzung
Anzeige

So misst man die Leistung der eigenen Shell

  • Man kann im Terminal selbst messen, wo Zeit verloren geht; relevant sind drei Arten von Verzögerung: Startzeit, Prompt-Verzögerung und Eingabeverzögerung
  • Eine Grundmessung besteht darin, time zsh -i -c exit einige Male auszuführen; der erste Lauf ist wegen kaltem Cache immer langsamer
    • Unter 100 ms ist okay, unter 50 ms ist hervorragend, und ab 500 ms gibt es klaren Optimierungsbedarf
  • Für präzisere Statistiken kann man hyperfine verwenden: hyperfine --warmup 3 'zsh -i -c exit'
  • Nutzung des in zsh eingebauten Profilers
    • Fügt man oben in .zshrc zmodload zsh/zprof und ganz unten zprof ein, erhält man eine sortierte Tabelle, die zeigt, wo Zeit verbraucht wird
    • Ganz oben stehen meist compinit, das Sourcen von nvm.sh oder eval "$(...)"; man behebt die obersten Einträge zuerst und wiederholt die Messung
    • Danach entfernt man die beiden Zeilen wieder
  • Wenn zprof nicht reicht, lässt sich der gesamte Start per Zeitstempeln verfolgen: zsh -ixc exit 2>&1 | ts -i '%.s' | sort -rn | head -20
    • Alternativ setzt man PS4='+%D{%s.%6.}: ' und führt zsh -ixc exit 2> startup.log aus, um große Sprünge zwischen den Zeilen zu finden
  • Selbst wenn der Start schnell ist, kann das Redraw des Prompts langsam sein; wenn man in das größte Git-Repository cd wechselt und nach Enter eine Verzögerung vor dem nächsten Prompt bemerkt, arbeitet der Prompt synchron
    • Dann kann man auf einen asynchronen Prompt umsteigen oder Git-Funktionen daraus entfernen

Fazit

  • Die meisten Optimierungen bestehen darin, Dinge wegzulassen; entscheidend ist, bewusst vorzugehen und nur das hinzuzufügen, was man wirklich nutzt
  • Dann öffnen sich die dutzenden Sitzungen, die man pro Tag startet, alle sofort, und das Terminal fühlt sich nicht wie eine Anwendung an, auf die man warten muss, sondern wie eine Erweiterung des eigenen Kopfes
  • Für ein Werkzeug, das man den ganzen Tag benutzt, ist diese Geschwindigkeit nicht verhandelbar
  • Die gesamte oben gezeigte Konfiguration ist im dotfiles-Repository veröffentlicht

1 Kommentare

 
GN⁺ 4 시간 전
Lobste.rs-Kommentare
  • Streng genommen ist damit meistens nicht das Terminal, sondern die Shell gemeint

  • Es ist besser, ein Werkzeug mit sinnvollen Standardeinstellungen zu verwenden, also einfach fish

    • Das ZSH der Firma ist seit etwa einem Jahr absurd langsam geworden, also habe ich fish ausprobiert, und die Funktionen zur Verbesserung der Lebensqualität haben mir wirklich gefallen
      Besonders gut fand ich, dass moderne Tab-Vervollständigung mit Auswahl per Pfeiltasten standardmäßig eingebaut ist; auf meinen privaten Geräten nutze ich zwar noch ZSH, aber nur, weil ich noch keine Zeit hatte, meine Nix-Konfiguration und den home manager anzupassen
    • Es wäre schön, wenn jemand ein bash-kompatibles fish bauen würde
      Eine Shell mit vernünftigen Standards und schneller eingebauter Vervollständigung, bei der man bash-basierte Tools nicht aufgeben oder neu schreiben muss, wäre ideal
    • Das Leben ist zu kurz, um neue Tools zu installieren; vernünftige Standardeinstellungen reichen völlig
  • Ich frage mich manchmal, ob Dinge wie nicht blockierende Prompts oder OpenGL-basierte Terminals wirklich mehr wert sind als einfach xterm mit PS1="\W: "

    • Ich hatte xterm einige Jahre bewusst nicht benutzt, aber nachdem ich mir verschiedene Terminal-Emulatoren angesehen habe, war ich ziemlich überrascht, dass xterm OpenType-Schriften, UTF-8, die meisten Emojis, 24-Bit-Farben und geringen Speicherverbrauch unterstützt
      Dazu ist es sehr schnell und hat den Vorteil, der „Standard“ zu sein, sodass verbliebene Bugs meist klein sind oder Programme darin das Verhalten eher als normal betrachten
      Deshalb benutze ich inzwischen wieder xterm
    • Es ist den Aufwand nicht wert
      zsh-Start ist von Haus aus sehr schnell, und langsam wird es nur, wenn Benutzer es selbst langsam machen
      Man sollte einfach nicht jede Menge Dinge hineinladen, die man nicht versteht; dazu gehören auch Bibliotheken, die sich „minimal“ nennen und beim Erzeugen des Prompts Hunderte Befehle ausführen
      Meine zsh-Konfiguration besteht aus ein paar hundert Zeilen, die sich seit den 90ern sehr langsam entwickelt haben, und ich verstehe jede Zeile und weiß, warum sie da ist
      Ich habe nie gezielt versucht, sie besonders schnell zu machen, und trotzdem startet sie noch immer in 20 ms; wenn ich irgendeine dumme Änderung einbaue, die sie verlangsamt, merke ich das sofort und kann es beheben
  • Ich mag es nicht, dass kaputte Benchmarks wie time zsh -i -c exit immer noch so verbreitet sind
    Damit wird etwas völlig Falsches gemessen, und einige zsh-Plugin-Manager haben sogar auf diese nutzlose Kennzahl optimiert, obwohl das auf Kosten echter Verzögerungen beim Shell-Start ging
    In zsh-bench gibt es einen Abschnitt, der erklärt, warum dieser Benchmark bedeutungslos ist: https://github.com/romkatv/zsh-bench#how-not-to-benchmark
    Kennzahlen wie die Verzögerung bis zum ersten Prompt oder die Eingabelatenz, die zsh-bench misst, sind deutlich nützlicher

  • Ich dachte erst, es ginge um Bugs in GPU-beschleunigten Terminals, und war froh, dass es nicht so war
    Caching für Vervollständigungen ist ein guter Tipp, und ich nutze zsh auf meinem Firmen-Mac; schon beim Gedanken an einen neuen Tab erscheint der Beachball, also hoffe ich, dass das hilft
    Bei kubectl-Vervollständigung würde mich interessieren, ob der langsame Teil die Erzeugung oder das Einlesen der Vervollständigung ist; falls es Ersteres ist, könnte das Speichern in einer Datei und anschließende Einlesen die Startzeit vielleicht verkürzen
    Bei jj wird es so gemacht, und beim Umstieg auf jj habe ich auch einen Prompt aufgegeben, der git status ausführt
    Schade, dass der Autor seine Zeiten nicht mit angegeben hat; dann hätte ich besser einordnen können, ob meine 0,287 Sekunden Durchschnitt oder eher langsam sind
    Später habe ich gemessen: eine fast leere .bashrc braucht 0,007 Sekunden, nach den skim-Keybindings 0,043 Sekunden, nach mise 0,115 Sekunden, nach jj-Vervollständigung 0,186 Sekunden und mit zusätzlichem Einlesen von /etc/bashrc 0,294 Sekunden; da scheint also noch Verbesserungspotenzial zu sein

    • Im Artikel hieß es, die Shell selbst liege vorne bei 30 ms, und bei demselben Test time shell -c exit komme ich auf etwa 50 ms
      Das Nervigste an Linux-Umgebungen anderer Leute sind für mich all die unnötigen Animationen überall
      Auf meinem Rechner öffnet sich das Terminalfenster nach einem Tastenkürzel praktisch sofort, und gelegentlich sieht man nur ein kurzes Flackern zwischen Fenster und Prompt
      Deshalb ist ein kompletter End-to-End-Test wichtig, bei dem man ein neues Fenster öffnet, in der Shell etwas macht und es wieder schließt; mit time myterm und anschließendem Schließen per Ctrl+D im Fenster lag ich immer unter 0,120 Sekunden
      Wenn man unnötige Animationen und Compositing loswird, ist vieles möglich, und selbst beim Vergleich zweier Tabellen habe ich einfach zwei Fenster maximiert und per Tastenkürzel zum Einrollen schnell zwischen ihnen gewechselt, sodass Unterschiede sofort sichtbar waren
      Unter Windows mit Excel-Animationen wäre dieselbe Aufgabe viel zu unruhig
    • Unter 100 ms erscheint mir in meiner Umgebung schwierig
      Selbst mit leerer Konfiguration liegt zsh -i -c exit im Mittel bei 129,8 ms, und mit vollständiger Konfiguration sind es ähnlich rund 250 ms
      Mit compinit-Caching spare ich im Schnitt etwa 5 ms, aber da dabei Vervollständigungen fehlen können, halte ich den Aufwand nicht für besonders lohnend
  • In letzter Zeit war zsh-Start fast so langsam, als würde es hängenbleiben; die genaue Ursache habe ich zwar nicht gefunden, aber ich konnte bestätigen, dass compinit den Großteil des kritischen Pfads ausmacht
    Ich habe Caching fast genauso implementiert wie im Artikel vorgeschlagen und die Verlangsamung damit beseitigt; die schicke Glob-Qualifier-Syntax hat mir außerdem gezeigt, dass ich meine eigene Lösung verbessern sollte
    Ich wusste gar nicht, dass so etwas möglich ist, und ehrlich gesagt wirkt es ein bisschen dubios, aber ich werde es trotzdem nutzen
    Bisher habe ich für den Zielpfad die eher grobe Methode date -Id verwendet
    Ich mag Tools, die wie zsh über eine vollwertige Programmiersprache konfiguriert werden, weil man so Dinge wie Caching selbst implementieren kann, ohne darauf warten zu müssen, dass der Autor die Funktion hinzufügt
    In fast 20 Jahren mit zsh habe ich nie ein Framework oder einen Plugin-Manager benutzt; solche Dinge scheinen vor allem fürs Styling verwendet zu werden
    Ich habe das Glück, dass mir die Ästhetik meiner Computing-Umgebung egal ist, und mein selbstgebauter Prompt ist ebenfalls schlicht, klein und informativ, aber überhaupt nicht auffällig; außerdem nutze ich das Standard-Terminal-Theme mit schwarzem Hintergrund

    • compinit-Caching ist frustrierend, weil der Cache veralten kann
      Mehrere Shell-Instanzen können parallel dasselbe tun, und das ist mir oft passiert, wenn ich in tmux parallele Instanzen zum Experimentieren gestartet habe
      Außerdem kann man sein Home-Verzeichnis über mehrere Hosts hinweg teilen, besonders zwischen Containern, daher bin ich am Ende bei einer Lösung mit Lockdatei, Ablaufprüfung und bedingtem zcompile gelandet
    • Die Ladezeit von ZSH war so schlecht geworden, dass ich einfach fish ausprobiert habe
      Leider scheint die fish-Konfiguration mit der Zeit in eine ähnliche Richtung zu driften; ich werde wohl in einer Pause am Montag mal Profiling machen, um zu sehen, ob Lazy-Loading-Techniken in meinem Fall wirklich hilfreich sind
      Der Großteil der verlorenen Zeit dürfte vom Git-Modul von Starship kommen, aber ich habe auch einige Aliasse und Hilfsfunktionen, die sich verzögert laden ließen
  • In Emacs wird schon seit Langem im Hintergrund eine vorgestartete Shell initialisiert
    Ein Terminal zu öffnen bedeutet dort, ein neues Fenster mit diesem Buffer zu öffnen und es umzubenennen; anschließend wird ein Thread geforkt, der die nächste Shell vorbereitet
    Dadurch gibt es keine Startverzögerung
    Ich erinnere mich, dass ich früher mit reptyr versucht habe, mir auch außerhalb von Emacs eine Lösung zusammenzubasteln, aber ich bin am Ende nicht dabei geblieben und weiß nicht mehr genau, warum
    https://github.com/nelhage/reptyr

    • Das gefällt mir, hat etwas vom Zygote-Prozess unter Android
  • Bei einer ähnlichen Untersuchung habe ich festgestellt, dass zsh-abbr etwa 100 ms Startzeit frisst, aber das ist für mich in Ordnung
    Man kann hier und da zwar jeweils 10 ms einsparen, aber wenn man die verlorenen Funktionen mit einrechnet, scheint es den Aufwand nicht wert zu sein
    Ich lebe dann eben mit etwa 300 ms Startzeit; das ist schnell genug, und ich öffne ohnehin selten pausenlos Terminals oder muss sofort losschreiben
    Trotzdem war der Artikel gut, ich habe hyperfine kennengelernt und ein paar meiner zsh-Startdateien genauer angesehen

  • Dadurch habe ich endlich meine lange aufgeschobene zshrc-Anpassung gemacht, und jetzt bin ich bei 80 ms angekommen, was großartig ist

  • Mein Leben ist lang genug, um ein langsames Terminal zu ertragen, und manchmal wünschte ich sogar, das Terminal wäre noch langsamer
    Zum Beispiel hätte mir eine standardmäßige 5-Sekunden-Verzögerung vor der tatsächlichen Ausführung in einer Root-Konsole vielleicht ein paar Tage meiner rebellischen Jugend erspart, weil ich Tippfehler noch mit Ctrl+C hätte abbrechen können