zshの起動が遅かったので早くできないかチャレンジした

普段 vimrc をちょこちょこいじることはあっても zshrc をいじることってあんまりない。
変更する時は実行ファイルがある場所にパスを通したり source <(kubectl completion zsh) のように補完したり alias 設定したりするくらい。

ある時気が付いた。

俺の zsh 起動遅いのでは。。。。

普段は tmux 使っていてセッション作成したりペインを分割しては削除したりと zsh が起動する機会が多い。
ただ起動する時にちょっと待ちが発生していて、だんだんそれに耐えきれなくなった。

計測

まずはどれくらい時間がかかっているか計測してみる。

$ for i in {1..10} ; do time ( zsh -i -c exit ); done
( zsh -i -c exit; )  0.91s user 0.58s system 102% cpu 1.463 total
( zsh -i -c exit; )  0.92s user 0.58s system 107% cpu 1.394 total
( zsh -i -c exit; )  0.92s user 0.56s system 108% cpu 1.356 total
( zsh -i -c exit; )  0.94s user 0.57s system 109% cpu 1.379 total
( zsh -i -c exit; )  1.02s user 0.59s system 105% cpu 1.536 total
( zsh -i -c exit; )  0.96s user 0.62s system 103% cpu 1.522 total
( zsh -i -c exit; )  0.91s user 0.58s system 105% cpu 1.416 total
( zsh -i -c exit; )  1.04s user 0.58s system 105% cpu 1.541 total
( zsh -i -c exit; )  0.94s user 0.62s system 107% cpu 1.458 total
( zsh -i -c exit; )  0.97s user 0.61s system 106% cpu 1.471 total

だいたい1秒ちょいかかっている。
毎回セッション作成したりタブ開いたりした時に1秒ちょいかかるのは地味に辛い。

どこがボトルネックになってるのか確認

zsh には zprof というプロファイルをとるためのツールが用意されいるのでこれを使う。

使い方としては

  • ~/.zshenv の一番はじめに zmodload zsh/zprof && zprof を追加
  • ~/.zshrc の一番最後に次のコマンドを追加する
   if (which zprof > /dev/null 2>&1) ;then
     zprof | less
   fi

あとはタブとか開いて zsh を起動するだけ。

num  calls                time                       self            name
-----------------------------------------------------------------------------------
 1)    1         143.73   143.73   33.30%    143.73   143.73   33.30%  goenv
 2)    1          86.60    86.60   20.06%     86.28    86.28   19.99%  __kubectl_bash_source
 3)    4          84.72    21.18   19.63%     84.72    21.18   19.63%  compaudit
 4)    3          73.20    24.40   16.96%     33.58    11.19    7.78%  pmodload
 5)    1          32.21    32.21    7.46%     31.93    31.93    7.40%  __helm_bash_source
 6)    3         103.85    34.62   24.06%     19.13     6.38    4.43%  compinit
 7)    2          13.23     6.61    3.06%     13.23     6.61    3.06%  promptinit
 8)    2          12.60     6.30    2.92%      5.58     2.79    1.29%  prompt_sorin_setup
 9)    1           5.37     5.37    1.24%      5.37     5.37    1.24%  async_init
10)   10           2.10     0.21    0.49%      2.10     0.21    0.49%  (anon)
11)    9           2.00     0.22    0.46%      2.00     0.22    0.46%  add-zsh-hook
12)    2           6.65     3.33    1.54%      1.28     0.64    0.30%  async
13)    2           0.84     0.42    0.19%      0.84     0.42    0.19%  is-at-least
14)    1          14.21    14.21    3.29%      0.53     0.53    0.12%  set_prompt
15)    3           0.43     0.14    0.10%      0.43     0.14    0.10%  compdef
16)    3           0.82     0.27    0.19%      0.39     0.13    0.09%  complete
17)    1          14.56    14.56    3.37%      0.34     0.34    0.08%  prompt
18)    3           0.12     0.04    0.03%      0.12     0.04    0.03%  bashcompinit
19)    1           0.06     0.06    0.01%      0.06     0.06    0.01%  is-callable

これから分かったこととしては

  • goenv が一番時間かかってる
    • eval "$(goenv init -)" してるとこかな
  • 補完系の compinit が時間かかってる
    • __kubectl_bash_source もたぶん補完のやつ

ということで今回は rbenv init - とか goenv init - とかの *env を init してる部分と source <(kubectl completion zsh) とかでコマンドの補完が効くようにしている部分を改善する。

改善方針

今回対象になったコマンドはどれも毎回?使うわけではない。 例えば source <(kubectl completion zsh) の補完系の処理は kubectl を使う時しか使わない。

そこで遅延して読み込めないかと考える。

zplugというzshというプラグインマネージャーがあってこれが lazy というオプションを提供しており、これを使うことで遅延して読み込めそう。
github.com

ただ元々 zplug 使っていただけではないし、このために入れるのもなんか違う気がしたので別の方法を考えてみた。

実際にやったこと

今回は次のような関数を定義することで初回起動を高速化した。

例えば kubectl の場合は↓を .zshrc に追加する。

kubectl() {
  unfunction "$0"
  source <(kubectl completion zsh)
  $0 "$@"
}

kubectl というコマンドを定義している。
いつものように kubectl logs をするとこの関数が呼ばれる。
実際には関数の最後で $0 "$@" しているのでちゃんと動作はする。
補完の source <(kubectl completion zsh) も実行している。 しかし毎回コマンドを実行するたびに source を実行するのは微妙。。。

そこでこの関数の一番のポイントが unfunction "$0" !! これを実行することで今回定義した kubectl 関数を破棄してくれるので、2度目に実行する時はいつもの kubectl を実行してくれる。

これを rbenv init - とか遅そうなところに適用していく。

注意点としては rbenv init -コマンドラインで実行するとわかるけど、shims への PATH の export とかしているので、これは .zshrc にベタ書きする必要がある。
これをしないと rbenv でいれた ruby が使えなくなってしまう。

どれくらい早くなったのか

for i in {1..10} ; do time ( zsh -i -c exit ); done                                                                                                                                            
( zsh -i -c exit; )  0.08s user 0.06s system 96% cpu 0.147 total
( zsh -i -c exit; )  0.08s user 0.06s system 97% cpu 0.140 total
( zsh -i -c exit; )  0.08s user 0.06s system 96% cpu 0.151 total
( zsh -i -c exit; )  0.08s user 0.06s system 98% cpu 0.142 total
( zsh -i -c exit; )  0.08s user 0.06s system 98% cpu 0.139 total
( zsh -i -c exit; )  0.10s user 0.08s system 96% cpu 0.185 total
( zsh -i -c exit; )  0.08s user 0.06s system 97% cpu 0.149 total
( zsh -i -c exit; )  0.08s user 0.05s system 96% cpu 0.139 total
( zsh -i -c exit; )  0.09s user 0.06s system 97% cpu 0.149 total
( zsh -i -c exit; )  0.08s user 0.06s system 97% cpu 0.138 total

0.15くらい!!!!! めっちゃ早くなった

最後に

今回は遅くなった zsh の起動を計測して改善してみました。
もっと良い方法があれば教えてください!!!