🦀

Rustでrbenvよりもinitが6倍速いRubyバージョンマネージャーを作った

2021/09/18に公開

こんにちは、TaKO8Kiです。

この記事では、本日、2021年9月17日にRust.Tokyo 2021で「Rustでrbenvよりも7秒速いRubyのバージョンマネージャーを作った」というタイトルで発表した内容についてまとめています。
スライドは、こちらにあります。

作ったもの

frumというRust製Rubyバージョンマネージャーを作りました。

logo

https://github.com/TaKO8Ki/frum

きっかけは、具体的に何か課題があったというよりは、rbenv installを10秒くらい高速化できたら面白そうだなという軽い気持ちです。

rbenvを使ったことある方は分かると思いますが、普通にrbenvと同じような感じで使えます。

$ eval "$(frum init)"

$ frum install 2.6.5

$ frum local 2.6.5

ベンチマーク

タイトルには「rbenvよりもinitが6倍速い」とありますが、ベンチマークを見てみましょう。

まず、eval "$(rbenv init)"rbenv installrbenv localという一連の流れのベンチマーク結果は、下記のようになりました。これを見てみると、frumの方がrbenvよりも1.04倍速い(約7秒)ことが分かります。当初の目標ほどは速くならないという結果になりました。

eval "$(frum init)"frum installfrum local
vs
eval "$(rbenv init)"rbenv installrbenv local

Command Mean [ms] Min [ms] Max [ms] Relative
rbenv 239628.1 ± 2030.2 237681.6 245162.6 1.04 ± 0.01
frum 232944.6 ± 1224.0 230565.4 234863.5 1.01 ± 0.01
frum (pre-release) 230366.5 ± 882.7 228454.2 232340.5 1.00

次に、rbenv initfrum initを比較したベンチマークは下記のようになりました。これを見ると、frum initrbenv initよりも6.14倍速くなっていることが分かります。

frum init
vs
rbenv init

Command Mean [ms] Min [ms] Max [ms] Relative
eval "$(rbenv init -)" 49.5 ± 2.1 46.2 57.2 6.14 ± 0.50
eval "$(frum init)" 8.1 ± 0.7 7.0 11.8 1.00 ± 0.11
eval "$(frum init)" (pre-release) 8.1 ± 0.6 7.2 11.7 1.00

このように、installコマンドは速くはなったが、体感としてはさほど変わらない。一方で、initコマンドは6倍速くなったので、rbenvよりもシェルの起動を高速化することができました。

ベンチマークはGitHub Actionsを用いて取りました。詳細はこちらをご覧ください。

どこをどう改善したのか

bash scriptで書かれているruby-buildrbenvをRustで書き直したのはもちろんですが、主にrbenv initrbenv rehashで使用されているshimsというものを使用するのをやめ、別の方法にしました。

shimとは

Wikipediaでは、

コンピュータ・プログラミングにおいて、シムとは、APIコールを透過的に傍受し、渡された引数を変更したり、操作自体を処理したり、操作を別の場所にリダイレクトしたりするライブラリのことである。

と書かれています。

shimは直訳すると(すき間などに入れる)詰め木という意味らしいです。

rbenvではshimsがどのように使われているのか

rbenvを使用している状態で~/.rbenv/shims配下を見ると下記のようになっています。
これがrbenvでいうshimsで、その中にはbundle、gem、rubyなど色々あります。

# ~/.rbenv/shims
$ ls
bundle  bundler erb     gem     irb     rake    rdoc    ri      ruby

それらの中身は全て同じ内容で、下記のようになっています。

#!/usr/bin/env bash
set -e
[ -n "$RBENV_DEBUG" ] && set -x

program="${0##*/}"
if [ "$program" = "ruby" ]; then
  for arg; do
    case "$arg" in
    -e* | -- ) break ;;
    */* )
      if [ -f "$arg" ]; then
        export RBENV_DIR="${arg%/*}"
        break
      fi
      ;;
    esac
  done
fi

export RBENV_ROOT="/Users/foo/.rbenv"
exec "/usr/local/Cellar/rbenv/1.1.2/libexec/rbenv" exec "$program" "$@"

また、実際にwhich rubyで実行ファイルの場所を特定すると、shims配下のrubyになっていることが分かります。

$ which ruby
/Users/foo/.rbenv/shims/ruby

どのタイミングでshimsが作られるのか

実際にshimsを作るのはrbenv rehashというコマンドです。これをrbenv initが内部的に実行しています。じゃあ、rehashがどんなことをしているかというと、下記のような感じです。

  1. ~/.rbenv/shimsの存在確認を行い、無ければ作る
  2. ~/.rbenv/shims/.rbenv-shimの存在確認を行う(このファイルの有無で現在別に実行中のrbenv rehashがあるかどうかを判断していて、あればここでexitする)
  3. 2.で.rbenv-shimが無ければ作る
  4. インストールされている全てのRubyバージョンの/bin配下にあるファイルを全て見た後、プロトタイプファイル、.rbenv-shimの中身をそれぞれのshimsとしてコピーする
  5. .rbenv-shimを削除する

rbenvはこれらの手順をrbenv initを実行する度に呼んでいます。(先ほど、"shimの中身は全て同じ内容"と言いましたが、これは4で.rbenv-shimを元にshimを作っていることから分かると思います。)

そのため、これを改善することがrbenv initを高速化することに繋がるということです。

frumではどのような実装になっているか

frumでは、シンボリックリンクを用いてバージョンの切り替えを行なってます。該当コードは下記です。

src/commands/init.rs

fn create_symlink(config: &crate::config::FrumConfig) -> std::path::PathBuf {
    let system_temp_dir = std::env::temp_dir();
    // シンボリックリンクとして使用するのpathを作る
    let mut temp_dir = generate_symlink_path(&system_temp_dir);

    while temp_dir.exists() {
        temp_dir = generate_symlink_pah(&system_temp_dir);
    }

    create_symlink_dir(config.default_version_dir(), &temp_dir).expect("Can't create symlink!");
    temp_dir
}

具体的には、$TMPDIR配下にfrum_xxxxx_xxxxxxxxxxxxxxという名前で~/.frum/versions/[バージョン]へのシンボリックリンクを作成し、バージョン切り替えのタイミングでシンボリックリンクの向き先を変えるといったことをやっています。

このように、rbenvと違いファイルの作成や削除を行う必要がなく、単にシンボリックリンクを作成するだけなので、先ほどのベンチーマーク結果のようにfrum initrbenv initよりも約6倍速くなっています。

最後に

結果、frum initrbenv initよりも約6倍速くなり、シェルの起動時間を短縮することができました。実際に、バイト先のRubyのバージョン管理にはfrumを使っています。

また、最近は、RustでgobangというTUIのSQLクライアントを作ったりしてるので、そちらも良かったら見てみてください。

https://github.com/TaKO8Ki/gobang

以上です。ありがとうございました!

Discussion