最速のZsh プラグインマネージャーを求めて

関連記事
- [zshの起動が遅いのでなんとかしたい](https://qiita.com/vintersnow/items/7343b9bf60ea468a4180)
- [zshの起動が遅いのでなんとかしたい 2](https://qiita.com/vintersnow/items/c29086790222608b28cf)

# TL;DR
速さを求めるならZpluginを使おう

# はじめに
普段もっとも良く使うアプリケーションであるシェルにおいて、なにが一番大事だろうか？
見た目？操作性？拡張性？
いや違う，**速さ**だ[^1]。
特に起動時の速さが重要だ。

たとえば、起動に1秒かかるシェルと0.1秒で起動出来るシェルがあったとする。
1回の起動あたりに900ms差が出る。
1日にシェルを最低100回起動するので[^2]、
$$
\frac{0.9 \cdot 100 \cdot 365}{60\cdot60} \simeq 9 \; \text{hours}
$$

なんということだ、1年で9時間も差が出てしまった。
人生を浪費にしないためにも、一刻でも早くシェルの起動を高速化しなければならないことが分かったと思う[^3]。

# 高速化における課題
素のZshの起動は高速だ。
遅くなる原因は設定やプラグインのせいだ。
しかし、あの便利さを経験してしまったら、プラグインなしのカスタマイズされていないZshは流石にたえられない。
そこで、利便性を維持したまま、高速化を行なうために必要なのは以下の２点である。

1. 必要な設定やプラグインのみ残し、余計な設定の排除
2. プラグインの読み込み速度の高速化

本稿では2点めの問題を解決するために、**既存のプラグインマネージャの起動・読み込み速度**について比較した。

# プラグインマネージャー

[このサイト](https://github.com/unixorn/awesome-zsh-plugins)に載っているのだけでも沢山のマネージャ・フレームワークが存在するので、そのすべて試すのは難しい（てかやりたくない）。
そこで、高速性を謳っていそうなプラグインマネージャにだけに絞って比較した。
今回比較したプラグインマネージャの2/6時点での情報を表にまとめた。
他にも速そうなものがあったら追加する。


プラグインマネージャ|star数|最終更新日(master)
---|---|---
[zplug](https://github.com/zplug/zplug)|2,873|Jul 4, 2018
[zgen](https://github.com/tarjoilija/zgen)|1,004|Oct 21, 2018
[antibody](https://github.com/getantibody/antibody)|821| Jan 24, 2019
[zplugin](https://github.com/zdharma/zplugin)|259|Feb 4, 2019
[zr](https://github.com/jedahan/zr)|51|Jan 30, 2019

# 測定方法

起動速度を測定するために[hyperfine](https://github.com/sharkdp/hyperfine)というベンチマークコマンドを使用した。

```zsh
hyperfine --max-runs 15 \
  --warmup 3 \
  --export-json result/$1.json \
  "zsh -d -f -l -c 'source plugin.zsh; exit'"
```

`--max-runs`が試行回数で、コマンドの実行にかかった時間の平均・標準偏差・最大値・最小値を測ってくれる。
zshの起動はI/Oやプラグインマネージャのキャッシュの影響で初回と２回目以降で速度が変わってしまうので`--warmup`で実際の測定前に複数回（3回）事前にコマンドを実行しおく。
`zsh -d -f -l -c 'source plugin.zsh; exit'`で、コンフィグをロードせずにZshを立ち上げ、プラグイン関係のスクリプトをロードしてから終了するまでの時間を計測した。

ちなみに自分はlinux OSで試している。違うOSだと違う結果になるかもしれない。

# 測定設定
測定する時は読み込むプラグインの数で場合分けした。

1. プラグイン0個
    2. プラグインマネージャによっては内部でcompinitを行っているものあるので、合わせるためにそれだけは行っておく
2. 普段使っているプラグイン10個

最初の設定では、プラグインマネージャのオーバーヘッドを測り、２つ目で総合的な速度を測定する。
プラグイン数が少ないない人は前者を、多くのプラグインを使う人は後者を参考にしたらいいと思う。

# プラグインマネージャ
実際に測定に使ったスクリプトを載せておく。
自分は各プラグインマネージャについてはチュートリアルを見たぐらいなので、「より高速な方法があるぞ」という場合は教えて欲しい。

## zplug

**プラグイン0個**

```zsh
source ~/.zplug/init.zsh

if ! zplug check; then
     install
fi

zplug load
```
**プラグイン10個**
起動時にはいらないプラグインについては遅延読み込みを行なう。

```zsh
source ~/.zplug/init.zsh

zplug "zsh-users/zsh-completions"
zplug "zsh-users/zsh-autosuggestions"
zplug "zsh-users/zsh-syntax-highlighting", defer:2, lazy:true
#
zplug "mafredri/zsh-async"
zplug "sindresorhus/pure"

zplug "marzocchi/zsh-notify", if:"has 'terminal-notifier' || has 'notify-send'"

zplug "b4b4r07/enhancd", use:"init.sh", lazy:true
zplug "vintersnow/anyframe", lazy:true
zplug "lukechilds/zsh-nvm", lazy:true, as:command
zplug "greymd/tmux-xpanes", lazy:true, as:command

zplug 'zplug/zplug', hook-build:'zplug --self-manage'

if ! zplug check; then
   zplug install
fi

zplug load
```

## zgen

**プラグイン0個**

```zsh
source "${HOME}/.zgen/zgen.zsh"

if ! zgen saved; then
  zgen save
fi
```

**プラグイン10個**
遅延読み込みはない？

```zsh
source "${HOME}/.zgen/zgen.zsh"

if ! zgen saved; then

  zgen load "zsh-users/zsh-completions" src
  zgen load "zsh-users/zsh-autosuggestions"

  zgen load "mafredri/zsh-async"
  zgen load "sindresorhus/pure"

  zgen load "marzocchi/zsh-notify"

  zgen load "vintersnow/anyframe"
  zgen load "b4b4r07/enhancd"
  zgen load "lukechilds/zsh-nvm"
  zgen load "greymd/tmux-xpanes"

  zgen load "zsh-users/zsh-syntax-highlighting"

  zgen save
fi
```

## antibody
antibodyはまず、プラグインのインストールなどをzshrcでは管理せず、コマンドで実行することで読み込みスクリプトを自動生成して（`antibody_bundle.zsh`）、それをzshrcで読み込む形をとる。
遅延読み込みを行なう方法はない？

**プラグイン0個**

```zsh
autoload -Uz compinit; compinit -iCd $HOME/.zcompdump
```

**プラグイン10個**

```zsh
source antibody_bundle.zsh

autoload -Uz compinit; compinit -iCd $HOME/.zcompdump
```

## zplugin
**プラグイン0個**

```zsh
### Added by Zplugin's installer
source '/home/vinter/.zplugin/bin/zplugin.zsh'
autoload -Uz _zplugin
(( ${+_comps} )) && _comps[zplugin]=_zplugin
### End of Zplugin's installer chunk

# compinitの代わりが用意されていた
zpcompinit
```

**プラグイン10個**
いろいろオプションがあってどうするべきなのかよく分からない。
[ここ](https://github.com/zdharma/zplugin/wiki)を参考にセットアップ

```zsh
### Added by Zplugin's installer
source '/home/vinter/.zplugin/bin/zplugin.zsh'
autoload -Uz _zplugin
(( ${+_comps} )) && _comps[zplugin]=_zplugin
### End of Zplugin's installer chunk

zplugin light "zsh-users/zsh-completions"

zplugin ice wait"!0" atload"_zsh_autosuggest_start"
zplugin light "zsh-users/zsh-autosuggestions"

zplugin light "mafredri/zsh-async"
zplugin light "sindresorhus/pure"

zplugin light "marzocchi/zsh-notify"

zplugin ice wait'!0'; zplugin light "vintersnow/anyframe"
zplugin ice wait'!0'; zplugin light "b4b4r07/enhancd"
zplugin ice wait'!0'; zplugin light "lukechilds/zsh-nvm"
zplugin ice wait'!0'; zplugin light "greymd/tmux-xpanes"

zplugin ice wait"!0" atinit"zpcompinit; zpcdreplay"
zplugin light "zsh-users/zsh-syntax-highlighting"
```

## zr
これもantibody同様にロードファイルを作っているだけ

**プラグイン0個**

```zsh
autoload -Uz compinit; compinit -iCd $HOME/.zcompdump
```

**プラグイン10個**

```zsh
if [[ ! -f ~/.zr/init.zsh ]]; then
  zr load \
    zsh-users/zsh-completions \
    zsh-users/zsh-autosuggestions \
    zsh-users/zsh-syntax-highlighting \
    mafredri/zsh-async \
    sindresorhus/pure \
    marzocchi/zsh-notify \
    vintersnow/anyframe \
    b4b4r07/enhancd \
    lukechilds/zsh-nvm \
    greymd/tmux-xpanes
fi

source ~/.zr/init.zsh
```

# 結果

![2019-02-06-230834_909x364_scrot.png](https://qiita-image-store.s3.amazonaws.com/0/34727/7c8551eb-f3f7-fca0-6474-f6cc3ba87108.png)


`0 plugin`の時はantibodyとzrはcompinitをしているだけなので最速だ。zgenやzplugはロード時にいろいろやっているのか遅い。
zpluginは本体をロードしているわりにはほぼオーバーヘッドがない。

一方で`10 plugin`では、顕著な差が出た。
zgenはほぼ速度が変わっていないのでかなり優秀なのだが、zplugin、お前`0 plugin`の時と比べて速くなっていないか（43ms → 32ms）？

zpluginで読み込みを行った時のプロファイルを見ると

```
num  calls                time                       self            name
-----------------------------------------------------------------------------------
num  calls                time                       self            name
-----------------------------------------------------------------------------------
 1)    1          11.83    11.83   36.78%     11.83    11.83   36.78%  prompt_pure_state_setup
 2)    4          24.34     6.08   75.63%      4.98     1.25   15.48%  -zplg-load-plugin
 3)   10           2.59     0.26    8.05%      2.33     0.23    7.24%  --zplg-shadow-autoload
 4)    5           2.81     0.56    8.75%      2.08     0.42    6.46%  add-zsh-hook
 5)   16          29.36     1.83   91.25%      1.77     0.11    5.51%  zplugin
 6)    2           1.64     0.82    5.09%      1.64     0.82    5.09%  async_init
 7)    4          26.79     6.70   83.25%      1.37     0.34    4.26%  -zplg-load
 8)    1           1.16     1.16    3.62%      1.16     1.16    3.62%  is-at-least
 9)    4           0.60     0.15    1.86%      0.60     0.15    1.86%  -zplg-register-plugin
10)    4           0.53     0.13    1.65%      0.53     0.13    1.65%  -zplg-shadow-off
11)    1          15.21    15.21   47.28%      0.45     0.45    1.41%  prompt_pure_setup
```

`-zplg-load-plugin`の呼び出し回数が4回でこれが遅延読み込みをしていないプラグインの読み込みのようだ。そしてzplugin全体で29msしか使用していないので、ほかのと比べてオーバーヘッドがかなり小さい。
結構な時間を喰うcompinitが出てきていのには驚く。これ遅延読み込み出来たんかい！

一応欠点としては、`wait ice`などturbo modeを使うにはZsh 5.3以上でないと行けないようだ

# おわりに
zpluginを使おう。

---

# 感想
zpluginが良いらしいと聞いて比較してみたくて始めたので、かなりzpluginよりの評価になっていると思う。（zpluginだけ書き方を工夫したりしたので）
でも他のプラグインのドキュメントを見る感じ、より高速な読み込みは出来そうにないと思う。
「もっと良い書き方があるよ！」「このプラグインマネージャなんで試してないの？」みたいな指摘を待っている。
zpluginさんのreadmeを見る感じまだまだ設定出来るところがあるのでもう少し調べたい。
ローカルフィアルもZpluginで管理出来るようなので、zshrc全体をZpluginで管理すればかなり高速な起動が出来そうだ。

# 参考

- [vintersnow/zsh_plugin_manager_speed](https://github.com/vintersnow/zsh_plugin_manager_speed)
- [【zsh高速化】え～、そんなプラグインマネージャーがあるんだったらもっと早く教えてよ、と姉が怒り出した - Zplugin](https://qiita.com/9sako6/items/23b164a95b91c644aade)
- [dotfilesをキレイキレイするついでに晒す](https://qiita.com/sei40kr/items/2a49fde94b8ed5c1a9d4)


[^1]: 個人的見解です。
[^2]: 個人差があります。
[^3]: 個人的見解です。