🏂

zshの起動が遅いのでjEnvからasdfに乗り換える

2023/11/01に公開

概要と結論

  • 🤔 macOSでzshの起動が遅いことが気になっていた
  • 😲 zprofでボトルネックを調べると_jenv_export_hookがヒット
  • 🎉 jEnvからasdfに乗り換えると起動時間が2.2秒から1.2秒弱に半減

といったことを試した記録です。

zshの起動が遅いのでなんとかする

このところzshの起動が遅いのが気になっていました。起動時間が遅いと

  • 待機中にブラウザを開くなど別作業を始める → なにしてたっけ?
  • じっと待機してからコマンドを打ち始める → タイプミスが増加する(気がする)

ような問題が発生します。どちらも別問題として対処できそうな気もしますが、それはそれとしてモヤモヤの原因は絶たねばなりません。短気はプログラマーの美徳ですから[1]

まずはzshの起動時間を計測してみます。私自身の備忘録として、それぞれのオプションの意味もまとめておきます。

$ time zsh -i -c exit
zsh -i -c exit  0.18s user 0.28s system 20% cpu 2.223 total
オプション 意味
-i インタラクティブシェルを起動する。コマンドを入力できる状態で起動するということ
-c 続く文字列をコマンドとして実行する。exit なら起動して即座に終了するということ

この 2.223 total の部分がコマンドが開始してから終了までの経過時間ですね。体感3秒くらいなのでこんなものでしょう。ちなみに実行環境は次のとおりです。

$ neofetch --off
OS: macOS 14.0 23A344 arm64
Host: Mac14,2
Kernel: 23.0.0
Shell: zsh 5.9
Terminal: iTerm2
CPU: Apple M2
Memory: 3174MiB / 16384MiB

ボトルネックを探る

こちらの記事を参考にしてzprofでボトルネックを探ります。

.zshenvの最初と.zshrcの最後に以下を加えて実行します。

~/.zshenv
# ファイルの最初に記載する
zmodload zsh/zprof && zprof
~/.zshrc
# ファイルの最後に記載する
if (which zprof > /dev/null 2>&1) ;then
  zprof
fi

その結果、処理時間の上位5個は以下でした。細かい見方はわかりませんが、きっとcallsが呼び出し回数ということでしょう。たとえば_omz_sourceは24回呼び出されていますが、3.08 \times 24 = 73.92 でだいたい74.02なので、3列目が処理全体の実行時間ということになりそうです。

num  calls                time                       self            name
-----------------------------------------------------------------------------------
 1)    1         319.20   319.20   60.90%    319.20   319.20   60.90%  _jenv_export_hook
 2)   24          74.02     3.08   14.12%     60.14     2.51   11.47%  _omz_source
 3)    4          38.31     9.58    7.31%     38.31     9.58    7.31%  compaudit
 4)    2          72.03    36.02   13.74%     33.72    16.86    6.43%  compinit
 5)    1          22.02    22.02    4.20%     22.02    22.02    4.20%  zrecompile

ボトルネックとなっている_jenv_export_hookは明らかにjEnvのものですね。
こいつをアンインストールして60.90%をなんとかしてやろう、というのが今回の趣旨です。

補足:jEnvについて

jEnvはJavaのバージョン管理ツールです。JavaのメジャーなLTSとして8, 11, 17などがありますが、それらを簡単に切り替えることができるようになります[2]

以前Javaを本格的に学んでやろうとインストールして、バージョンを切り替えながら動作を見たりしていました。ただ、現状JavaはAndroid Studioに入っているものだけで十分なので、まったく使っていません。

jEnvからasdfに乗り換える

ということで、jEnvをアンインストールして別のツールに乗り換えます[3]。以下の理由から、今回の乗り換え先はasdfです。

ただ、これで遅くなったとしても別にJavaは大して使わないし、ダメなら適当なものをグローバルインストールしておけばいいや、というテンションで進めます😬

補足:asdfについて

汎用的なバージョン管理ツールです。jEnvだけでなくpyenvnodenvのような〇〇envをひとまとめにしたようなものです。プラグインが見つかる限り何でもOKなツールです。

jEnvをアンインストールする

jEnvをアンインストールするため、まずはjenv versionsで紐づいているJavaを確認します。

$ jenv versions
  system
  11.0.15
  17.0
* 17.0.5 (set by (/Users/username/.jenv/version)
  openjdk64-11.0.15
  temurin64-17.0.5

久しぶりに触るので、そもそもどこにインストールされているか確認するところから始めます😇

jEnvに登録されたJavaは $HOME/.jenv/versions にシンボリックリンクとしてまとめられます。まずはこれを確認しましょう。

$ ls -classify $HOME/.jenv/versions
total 0
 9278947 0 drwxr-xr-x@ 12 username  staff  384  2  3  2023 ./
 9278913 0 drwxr-xr-x@ 18 username  staff  576  2  3  2023 ../
 9286652 0 lrwxr-xr-x@  1 username  staff   39  1  3  2023 11.0.17@ -> /opt/homebrew/Cellar/openjdk@11/11.0.17
11727628 0 lrwxr-xr-x@  1 username  staff   39  1 29  2023 11.0.18@ -> /opt/homebrew/Cellar/openjdk@11/11.0.18
 9279937 0 lrwxr-xr-x@  1 username  staff   62  1  3  2023 17.0.5@ -> /Library/Java/JavaVirtualMachines/temurin-17.jdk/Contents/Home
 9279939 0 lrwxr-xr-x@  1 username  staff   62  1  3  2023 17.0@ -> /Library/Java/JavaVirtualMachines/temurin-17.jdk/Contents/Home
 9286650 0 lrwxr-xr-x@  1 username  staff   39  1  3  2023 openjdk64-11.0.17@ -> /opt/homebrew/Cellar/openjdk@11/11.0.17
11727626 0 lrwxr-xr-x@  1 username  staff   39  1 29  2023 openjdk64-11.0.18@ -> /opt/homebrew/Cellar/openjdk@11/11.0.18
 9286491 0 lrwxr-xr-x@  1 username  staff   62  1  3  2023 temurin64-17.0.5@ -> /Library/Java/JavaVirtualMachines/temurin-17.jdk/Contents/Home
12296642 0 lrwxr-xr-x@  1 username  staff   59  2  3  2023 11.0.15@ -> /Applications/Android Studio.app/Contents/jbr/Contents/Home
 9286654 0 lrwxr-xr-x@  1 username  staff   39  1  3  2023 11.0@ -> /opt/homebrew/Cellar/openjdk@11/11.0.17
12296640 0 lrwxr-xr-x@  1 username  staff   59  2  3  2023 openjdk64-11.0.15@ -> /Applications/Android Studio.app/Contents/jbr/Contents/Home

散らかっていて恥ずかしいのですが、11.0.15, openjdk64-11.0.15 はいずれもAndroid Studio内のものでした。これらはそのままにするものとして、それ以外のものを片付けることにします。

どうやらopenjdkはHomebrewからインストールされたもののようです。これを確認して削除します。

$ brew list | grep openjdk
openjdk
openjdk@11
openjdk@17

$ brew uninstall openjdk@11 openjdk@17
Uninstalling /opt/homebrew/Cellar/openjdk@11/11.0.20.1... (667 files, 296MB)
Uninstalling /opt/homebrew/Cellar/openjdk@17/17.0.8.1... (635 files, 305MB)

一方、バージョンのないopenjdkはGradle経由でインストールされたものでした。正直Gradleも使っていないので削除します。また必要になったら入れればよいのです。

$ brew uninstall openjdk # 直接アンインストールしようとすると怒られる
Error: Refusing to uninstall /opt/homebrew/Cellar/openjdk/21
because it is required by gradle, which is currently installed.
You can override this and force removal with:
  brew uninstall --ignore-dependencies openjdk

$ brew uninstall gradle # 先にgradleからアンインストールする
Uninstalling /opt/homebrew/Cellar/gradle/8.4... (20,568 files, 423.7MB)

$ brew uninstall openjdk
Uninstalling /opt/homebrew/Cellar/openjdk/21... (600 files, 331MB)

$ brew list | grep openjdk # 何も表示されない
$ ls /opt/homebrew/Cellar | grep openjdk # 何も表示されない

ということできれいになりました。$HOME/.jenv/versionsにはまだシンボリックリンクが残っていますが、あとでディレクトリごと削除するので無視します。

あとは /Library/Java/JavaVirtualMachines のJavaを削除します。これ覚えがないのですが .pkgなどからインストールしたのでしょうか…。

$ sudo rm -rf /Library/Java/JavaVirtualMachines/temurin-17.jdk
Password:
jenv: version `17.0.5' is not installed

このJavaが登録されたjEnvから怒られるようになりました。これでOKですね。あとはAndroid Studio由来のものしか残っていないので、いざjEnv自体をアンインストールします。ホームディレクトリに残っている隠しファイルも忘れずに消しましょう。

$ brew uninstall jenv
Uninstalling /opt/homebrew/Cellar/jenv/0.5.6... (86 files, 78KB)

$ rm -rf ~/.jenv

.zshrcにもjEnvの記述が残っているので削除します。

~/.zshrc
# 以下の記載があれば削除する
export PATH="$HOME/.jenv/bin:$PATH"
echo 'eval "$(jenv init -)"

ここまででjEnvのアンインストールは完了です。ここで一度実験してみます。

$ time zsh -i -c exit
zsh -i -c exit  0.14s user 0.17s system 26% cpu 1.155 total

2.223から半分くらいになりました。体感的にもかなり速くなっています!

asdfの環境を整える

続いてasdfを導入します。インストールはHomebrewから行います。

brew install asdf

公式ドキュメントを参考にしてasdf.sh.zshrcに追加します。

echo -e "\n. $(brew --prefix asdf)/libexec/asdf.sh" >> ${ZDOTDIR:-~}/.zshrc

Javaのプラグインasdf-javaをインストールします。

$ asdf plugin add java
Plugin named java already added

適当なJavaをインストールしておきます。私の利用範囲ではv11系以上であれば問題ないのですが、せっかくなのでopenjdk-21.0.1を入れてみます[4]

# バージョンを確認
$ asdf list-all java | grep openjdk
(省略)
openjdk-21
openjdk-21.0.1

$ asdf install java openjdk-21.0.1
##################################### 100.0%
openjdk-21.0.1_macos-aarch64_bin.tar.gz
openjdk-21.0.1_macos-aarch64_bin.tar.gz: OK

これだけでは設定されないので、インストールされたJavaをグローバルに指定します。

$ asdf current
java            ______          No version is set. Run "asdf <global|shell|local> java <version>"

$ asdf global java openjdk-21.0.1
$ asdf current # asdfに反映されていることを確認
java            openjdk-21.0.1  /Users/username/.tool-versions

$ java -version # 一度シェルを再起動してから実行
openjdk version "21.0.1" 2023-10-17
OpenJDK Runtime Environment (build 21.0.1+12-29)
OpenJDK 64-Bit Server VM (build 21.0.1+12-29, mixed mode, sharing)

大丈夫そうですね!

あとはJAVA_HOMEを指定します。これにはasdf-javaにあるコードを記載すればOKです。zshの場合は以下を~/.zshrcに記載します。.sourceの意味なのでどちらでも大丈夫です。

~/.zshrc
. ~/.asdf/plugins/java/set-java-home.zsh
$ echo $JAVA_HOME
/Users/username/.asdf/installs/java/openjdk-21.0.1
$ /usr/libexec/java_home
/Library/Java/JavaVirtualMachines/openjdk-21.0.1/Contents/Home

こちらもいい感じですね。それぞれパスは異なりますが、後者は前者のシンボリックリンクなので実態は同じものです。

速度はどうなった?

この状態で起動速度を測ってみます。

$ time zsh -i -c exit
zsh -i -c exit  0.14s user 0.17s system 26% cpu 1.161 total

asdfの処理が追加されましたが、ほとんど変わらない結果になりました。無視できる程度の処理ということですね。体感的にも快適になったので満足です🎉

脚注
  1. 有名なので出典はいろいろ出せますが、私は『プリンシプルオブプログラミング 3年目までに身につけたい一生役立つ101の原理原則』で学びました。 ↩︎

  2. jEnvはJavaのインストール自体は自分で行い、それを登録するというステップが必要です。インストールもまとめて行いたいならSDKMAN!が簡単です。過去の自分に伝えたいです。 ↩︎

  3. このフックがやたらと遅いというのはGitHubのIssuesにも対処法とともに上がっていましたが、もう面倒くさいので削除します。 ↩︎

  4. 最近LTSが出たのでちょっと試してみたかったのです。ところでopenjdkの11系は出てこないようでした。サポート期間はOctober 2024になっているのですが、なぜなのでしょうか?🤔 ↩︎

Discussion