関連記事
概要
一言 : zshの起動が遅いのでなんとかしたい
tmuxでペインをどんどん作っては消したりしているとzshに起動の遅さでイライラしてくる。これは精神的に良くないのでできるだけ速くしてみた。
前提
dotfilesをあげているのでそれを見てくれると早いと思うがだいたい以下の様な流れで起動する。
環境はMac10.11、zsh5.2(brewでインストール)である。
- zshrcから
.zsh.d/*.zsh
を読み込む - プラグインマネージャーのzplugを使ってプラグインをロード
- 見た目はsindresorhus/pure。シンプルで速い。
- tmuxが立ちあがってなければtmuxを起動
また、oh-my-zshやpreztoなどのフレームワークは使わない。理由は以下のとおり。
- 遅いと思われる。(実際試してないからなんとも言えないが…)
- それに設定を自分で一度書いてしまったので移行するコストがめんどくさい。
時間の測定は以下のように、インタラクティブシェルの起動速度で測っている(終了する処理も含まれてしまうが)。
$ time ( zsh -i -c exit )
一回だけだとバラツキやすいので、以下のようにループするようにした。
for i in $(seq 1 10); do time zsh -i -c exit; done
高速化
zshの限界
そもそもzshはどれぐらいで起動するのか測ってみた。
$ : > /tmp/.zshrc
$ ( ZDOTDIR=/tmp/ zsh -i -c exit; ) 0.00s user 0.01s system 65% cpu 0.012 total
空のzshrcで0.012秒。これがzshの限界のようだ。
現状
さて、チューニング前の起動速度を測ってみた。
( zsh -i -c exit; ) 0.53s user 0.54s system 84% cpu 1.255 total
1.2秒かかっている。powerlineとか使ってたころはもっと遅かったと思うが、今ではこれでも遅いと感じてしまう。慣れとは恐ろしい。
プロファイル
とりあえず、なにがボトルネックとなっているのかzprofでプロファイリングしてみる。
.zshenv
の最初と.zshrc
の最後に以下を付け足す。
zmodload zsh/zprof && zprof
if (which zprof > /dev/null 2>&1) ;then
zprof
fi
これでzshを再起動したらプロファイリングが出てくる。プロファイリングしないときは、.zshenv
の1行をコメントアウトすればいい。
結果は以下のとおり
$ exec $SHELL -l
...
1) 3 754.93 251.64 77.10% 417.11 139.04 42.60% compinit
2) 1 164.71 164.71 16.82% 164.71 164.71 16.82% compdump
3) 4 125.24 31.31 12.79% 124.28 31.07 12.69% __parser__
4) 751 119.53 0.16 12.21% 119.53 0.16 12.21% compdef
5) 4 53.78 13.44 5.49% 53.78 13.44 5.49% compaudit
6) 1 103.18 103.18 10.54% 34.33 34.33 3.51% __zplug::zplug::cache::load
7) 1 27.25 27.25 2.78% 27.03 27.03 2.76% _zsh_highlight_load_highlighters
8) 6 253.20 42.20 25.86% 8.66 1.44 0.88% zplug
見方が自信ないけど
1) 3(呼び出し回数) 754.93(合計実行時間) 251.64(1回あたりの実行時間) 77.10% 417.11(自分自身の合計実行時間) 139.04(自分自身の一回あたりの実行時間) 42.60% compinit(関数名)
なのかな?
自分自身の実行時間というのはsource
などで外部ファイルを読み込んだ時にかかる時間を除いた時間だろう。
ともあれ、compinit
とzplug
が時間かかっている。compdef
もかかっているように見えるが、今時zcompdumpを削除したため必要となっただけで、普段からする処理ではないので無視する。
これを元に、zsh.d
以下のファイルをコメントアウトしていって、どれがボトルネックになっているのか探ってみた。
結果以下のファイルを読み込まないようにしたら62msで起動した。
- 1develop.zsh : anyenvなどを読み込んでいる。
- zplug_manager.zsh : プラグインマネージャーのzplugの設定、ロードをおこなっている。
compinit
compinitはzshの補完をするために必要なものなんで、おおよそ全てのzshrcに書かれているのではないかと思う。
しかし基本的に1回でいいので、3回も呼び出される必要はない。
最初はzplugでロードしたプラグインの中で呼び出されてるのかと思ったがzplugはなんとcompinitを一回だけ呼び出すようになっているらしい!すばらしい。
原因は1develop.zsh
で読み込んでいたBluemixのzsh_autocompleteの中でcompinitが呼ばれていたからだった。
該当部分をコメントアウトし、またzplugでcompinitが呼ばれるなら、自分で呼ぶ必要もないため.zshrc
からもcompinitを消した。
これでcompinitが1回になった。
zplug
次にzplug。
zplugはプラグインマネージャーの中では早いが、200msぐらいはかかっているようだ。
結局プラグインは4つしかロードしていないのでマネージャーを使わない方が早いのかもしれないが、流石にそこまでするのはめんどくさい。
仕方ないので、未インストールのチェックを飛ばすことにした。
if ! zplug check --verbose; then
printf "Install? [y/N]: "
if read -q; then
echo; zplug install
fi
fi
上記の処理がzplugのサンプルでは書いてあると思うが、これをコメントアウトした。プラグインを追加した時、手動でインストールしないといけなくなるが、そう頻繁にあることではないし、まあいいかと思う。これで0.1s高速化した。
zgenも試してみたが、そんなに変わらなかった(170~220ms)。しかし、自動インストールも含めての速度なのでzgenの方が自動インストールを含めると早いということになる。zgenのチェックは6msぐらいしかかかっていないかったのでおそらくzplugとは違う方法で行っているのだろう。
まだ調べていないが、zplug特有の高速化もあるかもしれないので調べてみたい。
anyenv
1deveolp.zsh
の中でもeval "$(anyenv init - )"
が処理の大半を占めていた。(zprofには出てこないのだが…)
❯ time (eval "$(anyenv init - )")
( eval "$(anyenv init - )"; ) 0.14s user 0.19s system 95% cpu 0.347 total
これは遅い!
とりあえず、--no-rehash
をつけてみる。
❯ time ( eval "$(anyenv init - --no-rehash)" )
( eval "$(anyenv init - --no-rehash)"; ) 0.09s user 0.11s system 84% cpu 0.229 total
initで書き出しされるスクリプトを保存しておき、それを読み込むことで高速化できるようだが、anyenvだとめんどくさそう。
とりあえずanyenv init
自体anyenv-init
のラッパーなので実態を直接叩くことで少し高速化すると思う。
❯ time (eval "$(env PATH="$ANYENV_ROOT/libexec:$PATH" $ANYENV_ROOT/libexec/anyenv-init - --no-rehash)")
( eval ; ) 0.07s user 0.10s system 87% cpu 0.192 total
zcompile
今まで知らなかったのだが、zcompile
で.zshrc
をコンパイルすることが出来る。これで外部ファイルの読み込みが速くなるはず。
変更した時自動でコンパイルされるように以下をzshrcに加えた。
if [ $DOTFILES/.zshrc -nt ~/.zshrc.zwc ]; then
zcompile ~/.zshrc
fi
ただ、自分の環境だとあんまり変化はなかった。せいぜい0.03sほど速くなった気がする。
結果
❯ time ( zsh -i -c exit )
Static loading...
( zsh -i -c exit; ) 0.17s user 0.17s system 92% cpu 0.368 total
3倍ほど高速化した!
けどまだ少しラグを感じる。0.3sを切るとだいぶ早く感じるのであと0.1s頑張りたい。
#追記(8/2)
一晩経ったら、0.5sまで遅くなっていた。おそらくcacheかなにかが効いていたのだろう。
anyenv 遅延ロード
やはりanyenvが遅いので少し手を加えた。
そもそもrbenvとかって毎回ロードする必要があるのだろうか?おそらく無いだろう。rbenvで設定したrubyが使えたら大抵の場合問題はない。
なので、rbenvなどは遅延ロードすることにした。
# anyenv
export ANYENV_ROOT="$(ghq root)/github.com/riywo/anyenv"
if [ -d $ANYENV_ROOT ]; then
export PATH="$ANYENV_ROOT/bin:$PATH"
for D in `command ls $ANYENV_ROOT/envs`
do
export PATH="$ANYENV_ROOT/envs/$D/shims:$PATH"
done
fi
function anyenv_init() {
eval "$(anyenv init - --no-rehash)"
}
function anyenv_unset() {
unset -f ndenv
unset -f rbenv
}
function ndenv() {
anyenv_unset
anyenv_init
ndenv "$@"
}
function rbenv() {
anyenv_unset
anyenv_init
rbenv "$@"
}
欠点は*envごとに関数を用意しないといけないこと。もはやanyenvの利点とは…
いっそことanyenvに組み込んだらいいのでは?という気がする。
追記:
上記の遅延ロードを行ったところ、npm install -g
でグローバルにインストールしたパッケージがbinにエイリアスされなくなった。eval "$(ndenv init)"
すれば治る。
結果
❯ time ( zsh -i -c exit )
( zsh -i -c exit; ) 0.16s user 0.12s system 99% cpu 0.273 total
❯ time (zplug load)
( zplug load; ) 0.10s user 0.07s system 93% cpu 0.174 total
これでイライラは大分解消された!
ただ、たまにzplugのloadが遅い時があるのだが(0.6sほど)、zplugのcacheを使う条件はどうなっているのか調べてみたい。
16/10/18追記
手動でプロファイルしていると、PATHを設定しているenv_path.zsh
やdevelop.zsh
、aliasを設定しているalias.zsh
が不思議と時間かかっている。
そんなはずはないので原因を探してみたところ、パス解決にbrew --prefix
やghq root
を使っていたからのようだ。
これが一回あたり0.02~0.03sかかっているみたい。なのでこれを固定値に変えた。
今のところこんな感じ。
❯ for i in $(seq 1 10); do time zsh -i -c exit; done
zsh -i -c exit 0.10s user 0.08s system 99% cpu 0.178 total
zsh -i -c exit 0.08s user 0.07s system 101% cpu 0.151 total
zsh -i -c exit 0.09s user 0.07s system 102% cpu 0.157 total
zsh -i -c exit 0.10s user 0.08s system 86% cpu 0.210 total
zsh -i -c exit 0.09s user 0.07s system 96% cpu 0.169 total
zsh -i -c exit 0.09s user 0.07s system 101% cpu 0.154 total
zsh -i -c exit 0.09s user 0.07s system 102% cpu 0.150 total
zsh -i -c exit 0.10s user 0.08s system 103% cpu 0.169 total
zsh -i -c exit 0.09s user 0.07s system 102% cpu 0.155 total
zsh -i -c exit 0.09s user 0.07s system 101% cpu 0.156 total
zplugが0.1~sかかっているので、これ以上求めるならプラグインを諦めないといけなさそう。