Railsの起動を高速化するbootsnapをコードリーディングで理解する

Rails 5.2 から標準 gem として入っている **bootsnap**
私自身は **install しておくだけで何かをキャッシュしてくれて起動が高速化する・・**ぐらいの理解で使っていたので、改めてコードリーディングを通して bootsnap がどういう仕組で何を解決しているのかを理解してみました。

🙇🙇肝心のコードリーディング部分はWIPです 🙇🙇

# bootsnap とは

bootsnap は Shopify 社によって開発された Rails/Ruby アプリケーションの起動時間を短縮するためのライブラリです。
Shopify 社によって開発される Shopify は、巨大なモノリシックの Rails アプリケーションであり、 アプリケーションの起動時間が長いことは開発体験の大きな毀損でした。
そこで開発されたのが bootsnap です。
Shopify 社曰く、 bootsnap を利用することで、Rails アプリケーションの起動時間を 50% ほど短縮することが見込めます。

[[参考リンク] Bootsnap: Optimizing Ruby App Boot Time](https://engineering.shopify.com/blogs/engineering/bootsnap-optimizing-ruby-app-boot-time)

# bootsnap の仕組み

bootsnap は、大きく分けて2つの機能で構成されています。
1. Path Pre-Scanning（パスの事前解決）
2. Compilation caching（コンパイルキャッシュ）

コードリーディングの前に、それぞれの機能を詳しく見てみましょう。

## Path Pre-Scanning（パスの事前解決）

ruby では、 `require` に相対パスが指定された時、　`$LOAD_PATH`（別名 `$:`） に示されるパスを順番に探し、最初に見つかったファイルをロードします。

[[参考リンク] module function Kernel.#require](https://docs.ruby-lang.org/ja/latest/method/Kernel/m/require.html)

ここで、 `$LOAD_PATH` のについて見てみます。
例えば以下の Gemfile が置かれている環境で `$LOAD_PATH` の中身を見てみましょう。
なお、 gem は vendor/bundle 配下にインストールしています。

```bash:Gemfile
# frozen_string_literal: true
source "https://rubygems.org"
git_source(:github) {|repo_name| "https://github.com/#{repo_name}" }
gem "pry"
```


```bash
$ bundle exec ruby -e 'puts $LOAD_PATH'
/Users/kekekenta/.rbenv/versions/2.5.1/lib/ruby/gems/2.5.0/gems/bundler-2.0.1/lib
/usr/local/Cellar/rbenv/1.1.1/rbenv.d/exec/gem-rehash
/Users/kekekenta/www/bootsnap-test/vendor/bundle/ruby/2.5.0/gems/pry-0.12.2/lib
/Users/kekekenta/www/bootsnap-test/vendor/bundle/ruby/2.5.0/gems/method_source-0.9.2/lib
/Users/kekekenta/www/bootsnap-test/vendor/bundle/ruby/2.5.0/gems/coderay-1.1.2/lib
/Users/kekekenta/.rbenv/versions/2.5.1/lib/ruby/gems/2.5.0/gems/bundler-2.0.1/lib/gems/bundler-2.0.1/lib
/Users/kekekenta/.rbenv/versions/2.5.1/lib/ruby/site_ruby/2.5.0
/Users/kekekenta/.rbenv/versions/2.5.1/lib/ruby/site_ruby/2.5.0/x86_64-darwin18
/Users/kekekenta/.rbenv/versions/2.5.1/lib/ruby/site_ruby
/Users/kekekenta/.rbenv/versions/2.5.1/lib/ruby/vendor_ruby/2.5.0
/Users/kekekenta/.rbenv/versions/2.5.1/lib/ruby/vendor_ruby/2.5.0/x86_64-darwin18
/Users/kekekenta/.rbenv/versions/2.5.1/lib/ruby/vendor_ruby
/Users/kekekenta/.rbenv/versions/2.5.1/lib/ruby/2.5.0
/Users/kekekenta/.rbenv/versions/2.5.1/lib/ruby/2.5.0/x86_64-darwin18
```

上記のように、 vendor/bundle 配下にインストールされた gem などが `$LOAD_PATH` に設定されていることがわかります。

例えば `require 'pry'` を実行した場合、 上記の `$LOAD_PATH` のパスを上から順にチェックしていき、マッチするファイルがあったらそれを読み込み、なければ次のパスをチェックしていく形になります。
今回のケースだと、上から3番目の `vendor/bundle/ruby/2.5.0/gems/pry-0.12.2/lib` でファイルが見つかるはずです。
見つかるまで順に実行するため、`require` の度にフルスキャンが走るということになります。

`$LOAD_PATH` を見るとわかるように、インストールした gem 毎に `$LOAD_PATH` が設定されるため、gem が増えるとそれに応じて `$LOAD_PATH` も増えていきます。
このことから、 Rails/Ruby アプリケーションが巨大になってきて、多数の gem に依存するようになると、 `require` の際のパスの解決に時間がかかってしまうことが理解できると思います。

これを解決するのが、 Path Pre-Scanning（パスの事前解決） です。

Path Pre-Scannningでは、それぞれの `require` に対して対象のパスを初回の解決時にキャッシュしておきます。
これにより、次回起動時に `require` する際にキャッシュからパスを引っ張ることで、フルスキャンのコストを抑えることができ、高速に起動することができます。

bootsnap では、 既存の `require` に対してモンキーパッチを当てていて、毎回 `$LOAD_PATH` からフルスキャンするのではなくキャッシュを参照する仕組みになっています。


## Compilation caching（コンパイルキャッシュ）

Compilation caching については、さらに以下の2種類に分類されます。
1. Ruby Compilation Caching（Rubyコンパイル結果のキャッシュ）
2. YAML Compilation Caching（YAMLコンパイル結果のキャッシュ）

それぞれについて見ていきましょう。

### Ruby Compilation Caching（Rubyコンパイル結果のキャッシュ）

Ruby では、コードが実行される際に、まずは **YARV 命令列** という **中間コード** に変換されます。
そして、 YARV 命令列を Ruby VM が読み込み実行することで、みなさんが書いた Ruby のコードが実行されるという流れです。

YARV や Ruby VM については、下の記事などを参考にしてみて下さい。
[RubyでRubyVMを実装してRubyVMの気持ちになってみる](https://qiita.com/nownabe/items/47cc5d95e8b4e01205a8) 

さて、この **中間コードへの変換** つまり、 **コンパイル** は当然コストが掛かります。
Rails/Ruby アプリケーションを起動するたびに同じコードのコンパイルをしていては、無駄なわけです。
そこで、この **コンパイル** 結果をキャッシュしてしまおう、というのが `Ruby Compilation Caching（Rubyコンパイル結果のキャッシュ）`　です。

Ruby では中間コードへのコンパイル処理をフックすることができ、これを利用して実現しています。（[RubyVM::InstructionSequence.load_iseq](https://docs.ruby-lang.org/ja/latest/class/RubyVM=3a=3aInstructionSequence.html) ）

bootsnap は、コンパイル処理をフックし、すでにコンパイル済みのものがある場合にはキャッシュから利用することで、コンパイルの処理をスキップすることができるわけです。

[[参考リンク] Ebisu.rb#18 で、RubyVM::ISeqの話をしました](https://www.kaeruspoon.net/articles/1177)

### YAML Compilation Caching（YAMLコンパイル結果のキャッシュ）

Rails のアプリケーションを開発していると、設定値等を YAML で管理する機会も多いと思います。
初回起動時に読み込む YAML ファイルが多くなると、 YAML の読み込みが無視できないくらいのコストになってしまいます。
そこで、 `MessagePack` や `Marshal` など、 YAML に比べて読み込みコストの低い形式に変換してキャッシュすることで、読み込み時間を抑える事ができます。


# コードリーディング

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/39619/9dd3df9e-2163-9ebb-ab16-f9645ad2ce7b.png)


🙇🙇コードリーディングのまとめが終わらなかったのでWIPです🙇🙇

# まとめ

今回は bootsnap の仕組みをコードリーディングを通してお伝えしました。
次回は Docker x bootsnap に関する記事を書こうと思います。
bootsnap の各種キャッシュをビルド時に生成して Docker イメージに含めることで、immutable な Docker コンテナの起動高速化を図る方法について解説します。