ActionView::Templateのcompile済み中間コードをキャッシュする

まずRailsのviewについて説明する。

Railsのviewはerbとかhamlとかで書く。この人間が書いたviewファイルはtemplateと呼ばれ、ActionView::Templateクラスで扱われる。一つのviewファイルが一つのTemplateオブジェクトになる。
このTemplateクラスはTemplate#compileというメソッドを持っている。このcompileメソッドは、人間が書いたerbなりhamlなりのコードをrubyのコード文字列に変換する。controllerでrenderメソッドを呼んだ時、実際に実行されるのは、このrubyコードである。

このcompile処理はそれなりに重いため、何度も呼びたくない。手元の環境では、でかいテンプレートのコンパイルには数百ミリ秒かかることもちょいちょいある。

しかしcompileは、render時に毎回走るわけではなく、一度compileされたtemplateは、ActionVew::Baseのメソッドとして保存されるため、同じtemplateを二回以上renderしても、二回目以降はcompileは走らない。*1つまりcompileが重い=Railsの起動直後に重い処理が走るっていうことになる。

compileが重くてなぜ問題になるのか

重いcompileが走るのが起動直後だけなら、運用上問題ないじゃんって思うかもしれないけど、世の中には、unicornのGCを止めてレスポンスを高速化するっていう、 id:secondlife 氏考案の変態ハックがあって、これを使ってるとRailsの初期化コストが問題になってくる。

例えば GC を止める・Ruby ウェブアプリケーションの高速化 - coリ・ー・ン<2nd life

このハックを使っているとレスポンスタイムが高速化する代わりに、unicornのworkerプロセスが肥大してどんどん死に、どんどん生まれ変わるから、初期化処理がしょっちゅう走ることになる。だからこなしているリクエストの数のわりにはCPU負荷が高めになる。ちょっとでも初期化コストを軽くしたい気持ちが伝わったかな???

キャッシュの時間です

というわけで、一度目のコンパイル後に中間コード(ruby)をファイルとして保存しておき、コンパイル処理が軽くなるようにする。中間コードがファイルに保存されていれば、unicornのworkerが生まれ変わっても、もう一度中間コード生成する必要がない。長い目で見るとCPU負荷を下げる効果が期待できる。

actionpack/lib/action_view/template.rb にモンキーパッチをあて、FileStoreキャッシュを入れる。

まずTemplate#compileのメソッドを丸コピしてくる。

ActionView::Template.class_eval do
  def compile_with_caching
    #    :
    # (略)
    #    :
    code = @handler.call(self)
    #    :
    # (略)
    #    :
  end
  alias_method_chain :compile, :caching
end

この @handler.call が、HamlなりERBなりのコンパイラを呼んでrubyコードを生成して文字列で返している部分。というわけでこれをキャッシュすればいい。

ActionView::Template.class_eval do
  def compile_with_caching
    #    :
    # (略)
    #    :
    cache_key = "template/#{self.inspect}"
    code = ActionController::Base.cache_store.fetch(cache_key) do
      @handler.call(self)
    end
    #    :
    # (略)
    #    :
  end
  alias_method_chain :compile, :caching
end

他にも色々書くべきことはあるけど、概念としてはこんな感じ。cache_storeはActiveSupport::Cache::FileStoreにしておいてね。