zsh の設定を見直し
調べたことのメモを兼ねて書き残しておく。
あらまし
人に言われて zsh の起動が遅いことに気がついたのがきっかけで設定の見直しを行なった。当時の設定は残していないがおおよそ 2000ms ほどかかっており、ターミナルの分割や IDE の中でターミナルを開く毎に待ち時間が発生していた。言われると気になり始めたのでその場で簡易に対処をした(nvm から asdf に変えて 1000ms 程度になった)。また、年末の大掃除ということで zinit を導入してオーバーホールも行った(最終的に 90ms 程度になった)。
以前の運用
基本的にはどの環境(macOS, Linux, WSL, etc…)でも prezto を入れて、その場で必要なツール(nvm, rbenv, etc…)の設定をそれぞれ追加していくという方針をとっていた。これはこれで気を使わなくて良いので楽ではあった。一応 git 周りはどの環境でもいじることが多かったので .gitconfig
は手動で共有したりしていた。
計測いろいろ
まずは .zshrc
のどこが遅いのかを調べて、遅い部分から潰していく。
以下で紹介する計測手法とコメントアウトなどを駆使して遅い部分を探す。
前提知識
計測に関係する zsh のオプション。
-i
or--interactive
:zprofile
,zlogin
を読み込む-l
or--login
:zshrc
を読み込む-c
: 渡したコマンド列を起動した環境で実行する
zshenv
についてはどの組み合わせでも読み込む。
参考
読み込み順などについては man zsh
の STARTUP/SHUTDOWN FILES
より。
Commands are then read from $ZDOTDIR/.zshenv. If the shell is a login shell, commands are read from /etc/zprofile and then $ZDOTDIR/.zprofile. Then, if the shell is interactive, commands are read from /etc/zshrc and then $ZDOTDIR/.zshrc. Finally, if the shell is a login shell, /etc/zlogin and $ZDOTDIR/.zlogin are read.
zsh のプロファイリング機能を使う方法
関数毎の呼び出し回数やかかっている時間などを確認する zprof
を使用する。(詳細は man zshmodules
)
# zshrcの冒頭に書く
zmodload zsh/zprof
...snip...
# zshrcの最後に書く
if type zprof >/dev/null 2>&1; then
zprof | less
fi
特定箇所の時間を計測する方法
# 計測開始地点に書く
zmodload zsh/datetime
function get_time_ms() {
strftime '%s%.'
}
function show_time() {
local start=$1
local end=$2
echo $((end - start))
}
start_time=$(get_time_ms)
...snip...
# 計測終了地点に書く
end_time=$(get_time_ms)
show_time $start_time $end_time
この表示を利用して平均値を取得する方法。
repeat 10 { zsh -i -c exit } \
| awk '{ total += $1 } END { print total/NR }'
全体的に測る
~/.zshrc
に限らずそのほかのファイルも含めた起動時間を測る方法。
TIMEFMTを設定することで zsh 組み込みの time の出力を制御できる。%mE
を指定してミリ秒の値を得ている。(1000ms とかを awk がいい感じに数字として解釈していそう)
( export TIMEFMT='%uE'; repeat 10 { time zsh -i -l -c exit } 2>&1 ) \
| awk '{ total += $1 } END { print total/NR }'
参考: hyperfine
コマンドの実行速度の計測に便利なツールとして hyperfine があるのでついでに紹介。
hyperfine 'zsh -i -l -c exit' 'zsh -i -c exit' 'zsh --no-rcs -c exit'
Benchmark 1: zsh -i -l -c exit
Time (mean ± σ): 86.9 ms ± 3.9 ms [User: 54.0 ms, System: 27.3 ms]
Range (min … max): 82.2 ms … 96.8 ms 34 runs
Benchmark 2: zsh -i -c exit
Time (mean ± σ): 80.6 ms ± 3.2 ms [User: 52.6 ms, System: 23.2 ms]
Range (min … max): 75.5 ms … 91.9 ms 33 runs
Benchmark 3: zsh --no-rcs -c exit
Time (mean ± σ): 6.3 ms ± 0.7 ms [User: 2.5 ms, System: 1.9 ms]
Range (min … max): 4.2 ms … 8.5 ms 329 runs
Warning: Command took less than 5 ms to complete. Results might be inaccurate.
Summary
'zsh --no-rcs -c exit' ran
12.77 ± 1.42 times faster than 'zsh -i -c exit'
13.78 ± 1.56 times faster than 'zsh -i -l -c exit'
最適化
zinit を用いて補完設定などを積極的に遅延実行させることで起動にかかる時間を短縮していく。単純に実行速度が遅いコマンドなどがあればそれを除去するのも効果がある。
zinit
この記事では zinit についての基本などはあまり解説しないので、公式のドキュメントや他の人が書いた記事を参考にすること。
- 公式
- おすすめの記事
詳しくは上記のリンク先などを見てもらうとして、簡単に紹介すると zsh 向けの高速なプラグインマネージャーということになる。そもそもシェルのプラグインとはなんぞやという話になるが、おおよそ読み込むと便利な関数などを追加してくれるもの程度の理解で問題はない。awesome-zsh-pluginsを見るともっと具体的にわかる。zinit 以外のプラグインマネージャーについては辛口ではあるもののComparison of ZSH frameworks and plugin managersが非常に参考になる。
私の zinit の使い方で特徴的な点をいくつか注意事項として述べておく。この記事では紹介しないが後述の dotfiles リポジトリの中を確認する際に把握しておいた方が良いと思われるものを含む。
- プラグインの設定に
for-syntax
を用いている。- The For-Syntax
zinit ice ...
とzinit load ...
という組で扱うのが標準だが、2 つのコマンドに分かれるのが嫌でこの構文を採用している。
- modifier 指定の際に
modifier=value
という構文を使っている。- Alternate Ice Syntax
- 区切り文字なしの書き方が一般的だが個人的には読み辛いためこの構文を採用している。他の情報源を見る際はこのことを頭に入れておいてほしい。
zdharma-continuum/null
- zinit ではリモートの git リポジトリなどからスクリプトを取得し、それを実行するというのが基本になっている。しかし、他の手段でインストール済みのスクリプトに対して zinit で読み込みタイミングや、事前コンパイルなどを行っておきたいことがある。そのような場合に
zdharma-continuum/null
をリポジトリに指定し、atpull
などで全く別の内容のスクリプトなどを処理するのがイディオムになっている。
- zinit ではリモートの git リポジトリなどからスクリプトを取得し、それを実行するというのが基本になっている。しかし、他の手段でインストール済みのスクリプトに対して zinit で読み込みタイミングや、事前コンパイルなどを行っておきたいことがある。そのような場合に
zinit-annex-bin-gem-node
- zinit では annex という zinit の挙動を拡張するようなプラグインが存在している。そのうちの 1 つであるzinit-annex-bin-gem-nodeによって提供される sbin modifier が非常に便利なため、私の設定では多用されている。
補完設定
zsh の補完周りの詳しい話は man zshcompsys
を見ること。zinit 固有の話はリポジトリのCompletionsにまとまっている。
zinit の補完周りは compdef
を差し替えて compinit
後に改めて zinit cdreplay
(zicompinit
) で適用するような形となる。この時に自分の制御外で compinit
を呼び出すようなスクリプトが存在すると速度に影響が出るため気をつける必要がある。(zprof
を使って確認できる)
また、補完周りのスクリプトの実行時間は長いことが多いため、遅延実行するとよい。zinit では wait modifier を設定することでプロンプト表示後に実行を遅延できる。詳しくはWikiを見てほしいが、0a
, 0b
, 0c
として表示直後に実行する中でもある程度の実行順制御ができるので、補完系の順序制御はこれを活用する。(順序を制御せずに遅延実行だけした場合にどうなるかというのはあまりわかっていないが、プロンプト表示後に補完が有効になるまでの時間が伸びたりする懸念はある)
私は以下のような形で活用している。
wait | 用途 |
---|---|
0a |
compdef を呼ぶ |
0b |
zicompinit を呼ぶ |
0 |
そのほか遅延実行したいもの |
zinit as="command" wait="0a" lucid light-mode for \
pick="asdf.sh" src="completions/_asdf" @asdf-vm/asdf
# compinit呼び出し後に実行されてほしいスクリプト
# (今はzinit管理になっていないスクリプトが押し込められている)
function after_completion_setup() {
autoload -Uz +X bashcompinit && bashcompinit
[[ -f /usr/local/bin/aws_completer ]] && complete -C /usr/local/bin/aws_completer aws
[[ -f "$GCLOUD_HOME/completion.zsh.inc" ]] && "$GCLOUD_HOME/completion.zsh.inc"
}
zinit wait="0b" lucid light-mode for \
atload="zicompinit; zicdreplay; after_completion_setup" \
zsh-users/zsh-syntax-highlighting
結果
執筆時点の内容は以下のようになっておりおおよそ 90ms を下回る速度で起動するようになった。 PZTM::completion
を呼んでいる部分や sdkman
, gcloud
周りなどあまり考えず適当に処理している部分もあるが、手元では素早く起動する上で、問題も発生していないので一旦記録を残そうということでこの記事を書いた。zinit 周りの細かいテクニックは各自で理解して欲しい。
$ hyperfine 'zsh -i -l -c exit'
Benchmark 1: zsh -i -l -c exit
Time (mean ± σ): 84.9 ms ± 3.7 ms [User: 52.3 ms, System: 26.8 ms]
Range (min … max): 82.1 ms … 99.9 ms 34 runs
余談
最近は .zshrc
のみならず、dotfiles をきちんと管理するということをしていなかった。しかし、この .zshrc
を見直したタイミングで管理をちゃんとやる気が出てきたので、rcmを用いて dotfiles リポジトリを管理してみることにした。今の所シンプルな用途では困っていないし、自分でインストーラーなどを管理する手間が省けているため好感触。