実用性皆無の勉強用でRailsのViewハンドラーを書いてみました。YAMLとERBで記述して、JSONを返すハンドラです。
インストールと使い方
Gemfileにyjを書いてインストールするだけgem 'yj'
使い方はviewファイルを拡張子 json.yjで作成してYAMLとERBでそれっぽく書いていきます。
string_property: <%= @string %>
integer_property: 123
array_property:
<% @items.each do |item| -%>
- <%= item %>
<% end -%>
object_property:
single_partial: <%= r.(partial: 'api/record', locals: { record: { name: 123, code: 100 } })%>
collection_partial: <%= r.(partial: 'api/record', collection: @records, as: :record) %>
YAMLがJSONとして解釈されるだけで、内部では MultiJson.dump(YAML.load(source))
を呼び出しています。
こんな感じなレスポンスを返してくれます
{
"string_property": "string",
"integer_property": 123,
"array": [
1,
2,
3,
4,
5
],
"object_property": {
"single_partial": {
"a": 123,
"b": 100
},
"collection_partial": [
{
"a": 123,
"b": 234
},
# ...
]
}
}
patialを使うときは r.(partial: ‘partial_template’, …)
の形式で呼び出します。内部的にはrenderを呼び出していますが、collectionを指定した場合は、YAMLの配列となるように調整しています。
例えば以下のようなpartialを呼び出すとき、
foo: <%= record.foo %>
bar: <%= record.bar %>
単体であればこのままJSONで出力すれば良いですが配列の場合は
- foo: foo_value1
bar: bar_value1
- foo: foo_value2
bar: bar_value2
というようにハイフンとインデントを良い感じに配置したいので、そこを調整しています。
パフォーマンスについて
jbuilderの1.5倍遅いです!wdevelopmentだと〜3倍程度のパフォーマンスが出ます。以下ベンチマークスクリプトと結果です。
require 'action_dispatch/testing/integration'
puts '* Rendering 1 partials via render_partial'
ActionDispatch::Integration::Session.new(Rails.application).get '/api.json?bench=1&n=1'
puts '* Rendering 10 partials via render_partial'
ActionDispatch::Integration::Session.new(Rails.application).get '/api.json?bench=1&n=10'
puts
puts '* Rendering 100 partials via render_partial'
ActionDispatch::Integration::Session.new(Rails.application).get '/api.json?bench=1&n=100'
puts
puts '* Rendering 1000 partials via render_partial'
ActionDispatch::Integration::Session.new(Rails.application).get '/api.json?bench=1&n=1000'
development環境
* Rendering 10 partials via render_partial
Warming up --------------------------------------
jbuilder 8.000 i/100ms
yj 7.000 i/100ms
Calculating -------------------------------------
jbuilder 74.519 (±18.8%) i/s - 360.000 in 5.106019s
yj 72.851 (±12.4%) i/s - 357.000 in 5.013330s
Comparison:
jbuilder: 74.5 i/s
yj: 72.9 i/s - same-ish: difference falls within error
* Rendering 100 partials via render_partial
Warming up --------------------------------------
jbuilder 1.000 i/100ms
yj 3.000 i/100ms
Calculating -------------------------------------
jbuilder 13.339 (±22.5%) i/s - 65.000 in 5.235478s
yj 32.640 (±12.3%) i/s - 162.000 in 5.073978s
Comparison:
yj: 32.6 i/s
jbuilder: 13.3 i/s - 2.45x slower
* Rendering 1000 partials via render_partial
Warming up --------------------------------------
jbuilder 1.000 i/100ms
yj 1.000 i/100ms
Calculating -------------------------------------
jbuilder 1.688 (± 0.0%) i/s - 9.000 in 5.333550s
yj 4.924 (± 0.0%) i/s - 25.000 in 5.090824s
Comparison:
yj: 4.9 i/s
jbuilder: 1.7 i/s - 2.92x slower
production環境
* Rendering 10 partials via render_partial
Warming up --------------------------------------
jbuilder 53.000 i/100ms
yj 28.000 i/100ms
Calculating -------------------------------------
jbuilder 533.308 (± 5.6%) i/s - 2.703k in 5.084701s
yj 283.096 (± 6.4%) i/s - 1.428k in 5.065776s
Comparison:
jbuilder: 533.3 i/s
yj: 283.1 i/s - 1.88x slower
* Rendering 100 partials via render_partial
Warming up --------------------------------------
jbuilder 7.000 i/100ms
yj 4.000 i/100ms
Calculating -------------------------------------
jbuilder 70.163 (± 7.1%) i/s - 350.000 in 5.019945s
yj 49.175 (± 4.1%) i/s - 248.000 in 5.054820s
Comparison:
jbuilder: 70.2 i/s
yj: 49.2 i/s - 1.43x slower
* Rendering 1000 partials via render_partial
Warming up --------------------------------------
jbuilder 1.000 i/100ms
yj 1.000 i/100ms
Calculating -------------------------------------
jbuilder 6.514 (± 0.0%) i/s - 33.000 in 5.076581s
yj 4.959 (± 0.0%) i/s - 25.000 in 5.056460s
Comparison:
jbuilder: 6.5 i/s
yj: 5.0 i/s - 1.31x slower
ちなみにproductionで1 partialで2.6倍遅いとか言われたので、ERBやYAMLロードのオーバーヘッドが高そうです。
実際stackprofで雑にプロファイリングしてみましたが、ERBがボトルネックになっていそうでした。うむー
TOTAL (pct) SAMPLES (pct) FRAME
167 (65.0%) 48 (18.7%) ActionView::CompiledTemplates#_app_views_api__record_json_yj___1428987586935647713_70240367401040
55 (21.4%) 42 (16.3%) ERB::Compiler::ExplicitScanner#scan
14 (5.4%) 14 (5.4%) Psych::Nodes::Scalar#initialize
11 (4.3%) 11 (4.3%) ActiveSupport::ToJsonWithActiveSupportEncoder#to_json
14 (5.4%) 9 (3.5%) Psych::ScalarScanner#tokenize
jbuilderはオブジェクト経由でハッシュを作成して最後にMultiJson.dumpする方式なので、ERBなどの余計なコストはかからないです。また、yjを作るにあたって
- モンキーパッチはしたくない
- N+1 find_template_pathsを避けたい
- Railsのcollection partial renderingのしくみを使いたい
collectionのpartial renderingはArray#join, String#html_safeがかかってくるので、ここをモンキーパッチしないとなると文字列連結で頑張って表現するしかないんですが、yjの場合は一旦JSONに変換してごにょごにょしていて、それも遅さの原因になっていそうです。
ということで実用性皆無のgemが出来上がりましたが、ハンドラー作るに当たってはjbuilderやjbやRailsのView周りのコードを読んでとても勉強になりました。