Das Leben ist zu kurz für einen langsamen Terminal
(mijndertstuij.nl)- 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,
fzfunddirenvin 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 cloneeingebunden und in.zshrcpersourcegeladen - 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,tmuxsowie 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,
fzfunddirenvlä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 cloneholt und die dann in.zshrcpersourcegeladen werdenfzf-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
compinitist in einer typischen.zshrceiner 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-24bedeutet „existiert und wurde innerhalb der letzten 24 Stunden geändert“ - So läuft
compinitnur einmal pro Tag vollständig, und den Rest der Zeit wird der Cache verwendet
- Der Glob-Qualifier
Lazy-Loading
nvmist einer der berüchtigtsten Bremsklötze für den Shell-Start; wenn es direkt beim Start gesourct wird, kommen schnell 0,5 Sekunden hinzunvmwird nicht in jeder Shell gebraucht, sondern nur, wenn mannvmeingibt; deshalb wird es in eine Funktion verpackt, die sich beim ersten Aufruf selbst ersetzt- Der erste
nvm-Aufruf entfernt den Stub, sourct das echtenvm(mit--no-use, damit keine Node-Version aufgelöst wird) und reicht dann die Argumente weiter
- Der erste
- Die
kubectl-Autovervollständigung funktioniert nach demselben Prinzip: Sie erzeugt ihr Skript durch Aufruf deskubectl-Binaries und wird daher erst nach der ersten tatsächlichen Nutzung geladen - Alle Tools, die dazu auffordern,
eval "$(tool init zsh)"in.zshrceinzufügen, sind Kandidaten für Lazy-Loading, weil sie beim Start einen Prozess forken und dessen Ausgabe auswerten direnvundfzfsind 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 statussynchron 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_infozu ersetzen, fiel zugunsten des asynchronen Verhaltens von pure aus - Man könnte asynchronen
git statusauch 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
tfürtmux new -A -s mainlandet ein neues Terminalfenster sofort wieder in der bestehenden Sitzung
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 exiteinige 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
.zshrczmodload zsh/zprofund ganz untenzprofein, erhält man eine sortierte Tabelle, die zeigt, wo Zeit verbraucht wird - Ganz oben stehen meist
compinit, das Sourcen vonnvm.shodereval "$(...)"; man behebt die obersten Einträge zuerst und wiederholt die Messung - Danach entfernt man die beiden Zeilen wieder
- Fügt man oben in
- Wenn
zprofnicht 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ührtzsh -ixc exit 2> startup.logaus, um große Sprünge zwischen den Zeilen zu finden
- Alternativ setzt man
- Selbst wenn der Start schnell ist, kann das Redraw des Prompts langsam sein; wenn man in das größte Git-Repository
cdwechselt 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
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
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
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
Ich frage mich manchmal, ob Dinge wie nicht blockierende Prompts oder OpenGL-basierte Terminals wirklich mehr wert sind als einfach xterm mit
PS1="\W: "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
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 exitimmer noch so verbreitet sindDamit 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
jjwird es so gemacht, und beim Umstieg aufjjhabe ich auch einen Prompt aufgegeben, dergit statusausführtSchade, 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
.bashrcbraucht 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/bashrc0,294 Sekunden; da scheint also noch Verbesserungspotenzial zu seintime shell -c exitkomme ich auf etwa 50 msDas 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 mytermund anschließendem Schließen per Ctrl+D im Fenster lag ich immer unter 0,120 SekundenWenn 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
Selbst mit leerer Konfiguration liegt
zsh -i -c exitim Mittel bei 129,8 ms, und mit vollständiger Konfiguration sind es ähnlich rund 250 msMit 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 -IdverwendetIch 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
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
zcompilegelandetLeider 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
Bei einer ähnlichen Untersuchung habe ich festgestellt, dass
zsh-abbretwa 100 ms Startzeit frisst, aber das ist für mich in OrdnungMan 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
hyperfinekennengelernt und ein paar meiner zsh-Startdateien genauer angesehenDadurch 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