paperclip の Interpolator を自作して URL生成を高速化した

最近 ぼくが携わっているサービスで [paperclip](https://github.com/thoughtbot/paperclip)のURL生成におけるパフォーマンス改善を行ったので、知見を一般化して共有します。

## paperclipのパフォーマンスにおける問題

paperclipのURL生成は難読化のために`:hash`を用いている状況下でとても遅くなります。これは、`Paperclip::Interpolations::interpolate`（[paperclip/interpolations.rb](https://github.com/thoughtbot/paperclip/blob/v4.2.0/lib/paperclip/interpolations.rb#L30)）における計算量が大きくなることが原因のようでした。

Paperclip::Interpolations は URLパターンの一部である`:boo`のような表現箇所を適切な値に置換する責務を持つモジュールであり、paperclipにおけるデフォルトのinterpolatorとして定義（[paperclip/attachment.rb](https://github.com/thoughtbot/paperclip/blob/v4.2.1/lib/paperclip/attachment.rb#L21)）されています。

「デフォルトのinterpolator」と述べた通り、これはオプションで変更可能です。ということで、Interpolator を自作することで高速化を試み、ベンチマークをとってみます。

* [mizoR/paperclip-benchmark-app](https://github.com/mizoR/paperclip-benchmark-app)

## 作ったもの

* ロジックは異なりますが [`Paperclip::Interpolations::interpolate`](https://github.com/thoughtbot/paperclip/blob/v4.2.0/lib/paperclip/interpolations.rb#L30)でやっていることとほぼ同じです。

```rb
# lib/paperclip/custom_interpolator.rb
# see: https://github.com/mizoR/paperclip-benchmark-app/blob/master/lib/paperclip/custom_interpolator.rb

module Paperclip
  class CustomInterpolator
    include Interpolations

    def interpolate pattern, *args
      pattern = args.first.instance.send(pattern) if pattern.kind_of? Symbol
      pattern.gsub(/:([a-z][a-z0-9_]+)/) do |match|
        method_name = $1
        respond_to?(method_name) ? send(method_name, *args) : match
      end
    end

    def plural_cache
      Interpolations.plural_cache
    end
  end
end
```

## 使い方

`has_attached_file` にオプションとして `interpolator: CustomInterpolator.new` を渡す。

```rb
# app/models/user.rb
class User < ActiveRecord::Base
  has_attached_file :avatar,
    styles: { medium: "300x300>", thumb: "100x100>" },
    default_url: "/images/:style/missing.png",
    url: "/system/:rails_env/:class/:attachment/:id/:hash.:extension",
    hash_secret: "785AFEE8C45E71086D262DB0B0A31442",
    interpolator: Paperclip::CustomInterpolator.new    # <-- こんな感じ

  validates_attachment_file_name :avatar,
    matches: [/png\Z/, /jpe?g\Z/]
end
```

## ベンチマーク

* [paperclip-benchmark-app/performance.rake at master · mizoR/paperclip-benchmark-app](https://github.com/mizoR/paperclip-benchmark-app/blob/master/lib/tasks/performance.rake#L17)
* ちょうど1.6倍くらいの高速化に成功した。

```
Calculating -------------------------------------
Paperclip::Interpolations
                         9.000  i/100ms
Paperclip::CustomInterpolator
                        16.000  i/100ms
-------------------------------------------------
Paperclip::Interpolations
                        100.019  (± 3.0%) i/s -    999.000
Paperclip::CustomInterpolator
                        162.002  (± 3.1%) i/s -      1.632k
```

## 問題

作成した `Paperclip::CustomInterpolator`は、オリジナルの `Paperclip::Interpolations` と比べると以下の挙動に違いが生まれています。たとえば、

```rb
url: '/files/:idwhat.:extension'
```

のようなURLパターンでは、


```
# Paperclip::Interpolations の場合 `:id`部分が置換される
/files/1234what.png

# Paperclip::CustomInterpolator の場合 （`#idwhat` というメソッドが定義されていないため）置換されない
/files/:idwhat.png
```

となってしまいます。ので、このようなパターンを利用しているサービスの場合はもうすこし工夫が必要になります。

## まとめ

* Paperclip::Interpolations::interpolate は結構遅い
* Interpolator を自作することでパフォーマンスを改善できるかも

## リンク

* [thoughtbot/paperclip](https://github.com/thoughtbot/paperclip)
* [mizoR/paperclip-benchmark-app](https://github.com/mizoR/paperclip-benchmark-app)