alias_method_chain と prepend を同時に同じメソッドに適用できない問題
Rails 5.0 のリリースが近づいてきてますが、Rails 5.0 から alias_method_chain
を使っていると deprecation warning が出るようになりました https://github.com/rails/rails/pull/19434 。
単純に alias_method_chain
を prepend
に書き換えればいいかと思いきや、かなりのレアケースではあるけれども、以前実際に失敗した事例の紹介です。
#!/usr/bin/env ruby require 'active_support/all' module M def foo p 'M#foo' super end end class C def foo p 'C#foo' end end class C def foo_with_modified p 'C#foo modified' foo_without_modified end alias_method_chain :foo, :modified prepend M end C.new.foo
これを実行すると
"M#foo" "C#foo modified" "C#foo"
という出力結果が得られる。予想通り。
#!/usr/bin/env ruby require 'active_support/all' module M def foo p 'M#foo' super end end class C def foo p 'C#foo' end end class C def foo_with_modified p 'C#foo modified' foo_without_modified end prepend M alias_method_chain :foo, :modified end C.new.foo
これを実行すると SystemStackError。変わったのは alias_method_chain
と prepend
の順序だけ。
言われてみればたしかにそうなるのはわかるけれども、たとえば activerecord をモンキーパッチで拡張するような gem が複数あった場合、たまたま同じメソッドに対する alias_method_chain
と prepend
が混在していると、初期化順でエラーになったりならなかったりする。
2015年の思い出
しゃかいじんにねんめ
去年は http://eagletmt.hateblo.jp/entry/2014/12/31/032313
仕事
去年末から今年の前半にかけて簡単な外向きの API サーバや社内アプリを書いていて、仕事で初めて最初から自分で書いた Web アプリになった。 まぁ最初からといっても既にある機能の置き換えみたいなやつなので、ちょっと設計考えて実装したくらい。
今年の中盤くらいからは開発環境向けの MySQL サーバをさわる機会を得た。個人ではずっと PostgreSQL で MySQL は全然わかってなかったのでいい経験だった。 去年と比べてサーバの面倒を見る時間が圧倒的に増えた。 MySQL もそうだけど、CI サーバを CentOS から Ubuntu にしたり、RRRSpec の面倒を見たり。 Web アプリ書くのも全然嫌いじゃないけど今はこっちのほうが楽しい。
発表
なんか隣の人に唆されて RubyKaigi 2015 で発表してた http://k0kubun.hatenablog.com/entry/2015/12/12/000037 。
hamlx の噂を聞いてから全然動きないなーと思って、haml も slim も文法同じなんだから slim と同じパフォーマンスはいけるでしょと 書き始めて *1 、完全に趣味で書いてたけど色々あって 本番で動くレベルになって 、最終的に RubyKaigi での発表になって一年を通して haml だった。
僕は勉強会とかカンファレンスはあんまり行かないけど、まぁこのまま1年に1回くらいのペースでできたらいいな。
そういえば RubyConf 2015 に行った。海外は ICPC で行ったことがあったけど、アメリカというか英語圏は初めて。 英語の案内とかは結構読めるんだけど、人が何言ってるかなかなか聞き取れなくて大変だった。人と話せないのは英語関係なく日本語でもそうなのである意味いつも通りだった…
インフラ
仕事で少しさわってるけどよくわからんなーというのは、積極的に個人の環境でも使うようにしてみている。 去年は puppet とか zabbix だったけど、今年は itamae とか Jenkins だった。あとはつい最近 OpenLDAP も立てた。 仕事で使ってるツールが OSS だと個人でも使えていい。 来年はネットワークの知識をなんとかしたいと思っている。ルータ自作してみたり自宅と VPS の間に拠点間 VPN 張ってみたりすると経験値貯まるのかなー。
あと ISUCON 5 に参加した。去年とは違って普通に予選を (ギリギリ) 勝ちぬいて、本選でもそこそこの順位をとれてよかった。 http://eagletmt.hateblo.jp/entry/2015/11/03/013045
来年は Ubuntu 16.04 LTS がリリースされてついに systemd を使えるようになるので楽しみ。
アニメ
去年末からの続きだけど SHIROBAKO と クロスアンジュがとてもよかった。全然違う作品だけどどちらも今後しばらく忘れないと思う。 あとあんまり話題にならなかったけどデス・パレードが本当に大好きだった。ぜひ最初の2話を順番に見てほしい。
新規に見たものの中でとくに好きだったのはユーフォニアム、終わりのセラフ、SHOW BY ROCK、グリザイアの楽園あたり。グリザイアはまぁ去年からの続編だけど。 DOG DAYS''、ジョジョ、デレマス、黒バス、Fate UBW、銀魂、プリズマイリヤ、ゆるゆり、うたわれるものあたりはもう鉄板というか当然見て当然面白かった。 あともうずっと見続けてるけどアイカツも。未だにソレイユ大好きだけどダンシングディーヴァもいいですね。
BD 買ったのは SHIROBAKO、アイカツ (映画含む)、プリズマイリヤ、SHOW BY ROCK、ストライクウィッチーズOVA、ゆるゆりなちゅやちゅみ、くらいか。来年はもっと増やしていいかも。
漫画・ラノベ
bookwalker で183冊買ってた。セールのときに一気に買ってる割合もそれなりにあるけど… 新規に買ったものだと終わりのセラフ、明日、今日の君に逢えなくても、やがてきみになる、小百合さんの妹は天使、あたりかなぁ。 終わりのセラフは小説版も買おうか迷ってる。
音楽
ついに日本でも始まった Google Play Music を使うようになった。 といっても手元にある mp3 をアップロードして便利に同期するツールとしてで、曲は相変わらず Amazon から買ってダウンロードしている。Google Play Music 品揃え悪い… 本当は全部 mp3 で買いたいんだけど、一部 CD でしか出てないので仕方なく CD も買っている。アイカツとか…
ゲーム
ムジュラの仮面3D、FE if、Splatoon、そして最近シャープ FE。 普段買わない系統で Splatoon を買って長く楽しめたのは大きい。今も定期的にやってる。 Splatoon のおかげで久しぶりに結構ゲームやってたと思う。
systemd-run によるリソース制御
systemd というと unit ファイルを書いてデーモンを起動して、というイメージが強いかもしれないけど、systemd-run を使うと単発のコマンドを systemd の管理下で実行できる。 こうすることで、CPUQuota=50% とか MemoryLimit=10M とか BlockIOWeight=10 のようにリソースを制限でき、しかも実行中に変更することもできる。
たとえば http://hb.matsumoto-r.jp/entry/2015/12/02/133448 にあるような CPU 使用率を制限しながら yes を実行する例だと、
% sudo systemd-run --scope --uid=eagletmt -p CPUQuota=10% yes > /dev/null Running scope as unit run-rcab5dc0a5f8e4620a996d95d40f7c95a.scope.
のようになる。ここから
% sudo systemctl set-property --runtime run-rcab5dc0a5f8e4620a996d95d40f7c95a.scope CPUQuota=50%
というように動的にリソース制限を変更できる。 systemd のすべてのディレクティブを systemd-run に指定できるわけではないが、リソース制御関連のディレクティブは対応している。
この例では --scope
を使ったので出力はそのままになっているけど、--scope
をつけなければ service unit として実行されるので、コマンドの出力は通常の service unit と同様にデフォルトでは journald に送られる。
参考
Unicorn 5.0.0 の sd_listen_fds(3) emulation とは
Unicorn 5.0.0 がリリースされていて、most boring major release と自称している中、新機能として sd_listen_fds(3) emulation がある http://bogomips.org/unicorn-public/20151101-unicorn-5.0.0-rele@sed/t/ 。
sd_listen_fds(3) は systemd の socket activation という機能に対応するための単純な関数で、socket activation は簡単に言うと systemd が listen してその fd を service unit のプロセスに渡す機能。 この機能の詳細やモチベーションを説明するのは面倒なので作者のブログ記事などを読んでほしい http://0pointer.de/blog/projects/socket-activation.html 。
今回 Unicorn はそれに対応したため、以下のような unicorn.service と unicorn.socket を用意することで、socket activation が可能になる。
[Unit] Description=Unicorn [Service] User=non-root WorkingDirectory=/path/to/current ExecStart=/usr/bin/bundle exec --keep-file-descriptors unicorn -c config/unicorn.rb -E production ExecReload=/bin/kill -HUP $MAINPID ExecStop=/bin/kill -QUIT $MAINPID PIDFile=/path/to/current/tmp/pids/unicorn.pid [Install] WantedBy=multi-user.target
[Socket] ListenStream=/run/unicorn.sock [Install] WantedBy=sockets.target
設定ファイルの unicorn.rb 側には変更は不要。socket activation を利用すると listen の設定が無視されるくらい。
bundle exec に --keep-file-descriptors を渡す必要がある点に気付くまでしばらくはまっていた。 ちゃんと LISTEN_PIDS と LISTEN_FDS は設定されてるのに、なぜか fd=3 が socket ではない別の何かになっていて謎だった…… Ruby 2.0.0 から 0, 1, 2 以外の fd は Kernel#exec するときにデフォルトでは閉じられてしまうそうで、それを防ぐためのオプションが bundle exec にある。 https://github.com/bundler/bundler/pull/2629 https://bugs.ruby-lang.org/issues/5041
また Unicorn のアナウンスでは「You may now stop using PID files and other process monitoring software when using systemd.」とあるけど、Unicorn の SIGUSR2 による graceful restart を利用している場合、 master の pid (systemd 的に言うと main pid) が変わるので、PIDFile は相変わらず必要になるはず。 勘違いだったら教えてください。
実際 Unicorn が socket activation に対応して嬉しいかというと、まぁそんなにメリットは無いと思う。 ほとんどアクセスが無いような個人のサービスで、実際にアクセスがくるまで Unicorn の起動を遅延できる、とかはあるかも…?
socket activation することで、Unicorn 自身が graceful restart しなくても、graceful shutdown ができればクライアントからのコネクションを途中で切ることなくアプリケーションを再起動できるメリットはある。 ただ、新しい worker を用意しつつ準備ができたら切り替える、というような細かい制御は当然 systemd 側からはできず、一旦完全に Unicorn を落としてから再度 socket activation によって起動する流れになるので、接続に失敗することはないけど Unicorn の起動にかかる時間だけブロックする形になる。 どうしても Unicorn を SIGUSR2 によるリロードではなく真っ新な状態から再起動したいときの手段が用意されているのは便利かもしれない。
ISUCON5 本戦に参加して5位でした
去年 に引き続き†空中庭園†《ガーデンプレイス》として @ryotarai、@gfx と参加して、結果は5位でした http://isucon.net/archives/45869121.html 。コードは https://github.com/ryotarai/isucon5 。
ISUCON5 予選の反省として、最初はプロファイルと手元での動作に最低限必要な変更以外はせずにコードを読むと決めていて、nginx の log_format を変えたり stackprof、lineprof を仕込んだりしてベンチマークを実行した後は、各自アプリケーションの理解に集中していた。 僕は /data から叩かれている外部 API の挙動をとくに調べていて、
- GET リクエストしかないし、エンドポイントの情報は DB に入ってるけど更新されることもない
- ken と ken2 という2つの似たエンドポイントがあるけど、実は全く同じっぽくて、しかも zipcode に対して返ってくる答えは一定っぽいので、2つをまとめた上で zipcode をキーにしてキャッシュできそう
- surname、givenname もそれぞれ q をキーにしてずっとキャッシュできそう
- tenki は毎回違う値が返ってくる
- 実際には3秒くらいで変わることに @ryotarai が後で気付いて、その期間だけキャッシュするようにしていた
- perfectsec、perfectsec_attacked の2つは https でリクエストするようになっていて、http だと通らない
- perfectsec、perfectsec_attacked の2つも毎回違う値が返っていてキャッシュできなさそう
- 実際には perfectsec_attacked は20秒くらいはキャッシュできることが後からわかって、最終的にはキャッシュが入ってた
- それぞれのエンドポイント間に依存関係はなくて、全部並列にとってこれそう
- このときは適当に Thread.start すればいいかと思っていたけど、@gfx が並列化を実装するときにちゃんとスレッドプールくらいは使いたいという話から expeditor 使おうということになった
あたりを見ていた。
だいたい12:30頃から実際に app.rb を編集し始めて、僕は
- ken2 の結果を Redis に無期限でキャッシュしてベンチマークが通るかどうか → 通った
- endpoints を DB から Ruby の定数にもってくる
- ken を ken2 にマージしてベンチマークが通るかどうか → 通った (これで実質 ken もキャッシュされている)
- surname、givenname も無期限でキャッシュしてベンチマークが通るかどうか → 通った
- users を PostgreSQL から Redis へ。同時にパスワードをハッシュするのをやめて直接保存するように
ということをしていた。このへんが終わったのが15時過ぎくらいで、1台のみ使って2万点くらい出していた気がする。 ここから @ryotarai が3台使うような構成に変更していって、5万点をこえ始めていた。 Web API コールがどうしても遅くて /data が詰まると unicorn のワーカを使い切ってしまうので、unicorn のワーカ数をかなり大きめにしつつ3台に配置し、a にはベンチマーカからリクエストを受ける nginx を置いて3台にプロキシし、b には Redis を置くようにした。 endpoints は Ruby の定数にしたし、users は /initialize で Redis に入れてるし、subscriptions も @gfx が /initialize で Redis に入れるようにしていたので、すでに PostgreSQL は /initialize 以外では不要になっていた。
ベンチマークがたまに fail するようになっていて、そのデバッグで16:30頃から1時間くらい使っていた。何で fail しているのか分かるまで時間がかかってしまったのがつらい。 最終的にわかったのはどうも /initialize がたまに30秒以内に終わってないっぽくて、/initialize は Redis がいる b に常にプロキシするようにしたら安定した。 スコアは5万点だったり3万点だったりして、あんまり安定しなくて結構不安だった。
最後は a にいる nginx の CPU 使用率が高いのが気になっていて、@ryotarai が振り分けの重み付けを調整しながら僕はアセットを gzip して gzip_static するようにしたりしていた。 終了15分前くらいに念のため3台とも reboot してみて、ちゃんと Redis が起動してベンチマークが fail しないことを確認して終了。 最終結果のスコアは86,210点で、競技中のベストよりも高い結果だった。
終了後に聞いた話の中では RACK_ENV=production
が非常に痛かった http://diary.sorah.jp/2015/11/02/isucon5f 。3位との差はそんなに大きくなかったので、もしかしたら3位には入れていたかもしれないと思うと悔しいけどしょうがない。
去年から参加し始めたけど去年よりよくなってる感触はあるので次は入賞したい。
faml と slim、hamlit のパフォーマンスの差
http://k0kubun.hatenablog.com/entry/2015/03/31/004021 を見て、「faml は slim と同等とか言いながら slim よりずっと遅いじゃん」と思われると悔しいので一応解説しておく。 なお、このエントリは slim 3.0.3 と hamlit 0.4.2 に基いている。
なぜこのベンチマークで faml が遅いのか、結論から言うと、この中で faml だけ自動 html エスケープが有効になっているからだ。 haml はデフォルトでは自動 html エスケープは無効であり、hamlit もそれに倣っている。
slim のベンチマークで用いている slim のビューでは ==
を使って明示的に html エスケープを無効化している https://github.com/slim-template/slim/blob/v3.0.3/benchmarks/view.slim 。
そのため、faml だけが自動 html エスケープを行っている。
faml が行ってる slim とのベンチマーク比較では、元々の view.slim から ==
を =
に変更して html エスケープを有効にしている。
そして haml, hamlit でも escape_html: true
というオプションを渡して slim のベンチマークを実行すると、travis-ci 上のある実行では以下のような結果になった。
https://travis-ci.org/eagletmt/faml/jobs/57304778
ruby benchmark/slim.rb Calculating ------------------------------------- Haml 1.261k i/100ms Faml 3.741k i/100ms Hamlit 4.369k i/100ms Slim 3.846k i/100ms ------------------------------------------------- Haml 14.524k (± 2.6%) i/s - 73.138k Faml 52.477k (± 2.3%) i/s - 265.611k Hamlit 64.082k (± 2.8%) i/s - 323.306k Slim 55.219k (± 1.7%) i/s - 276.912k Comparison: Hamlit: 64082.1 i/s Slim: 55219.4 i/s - 1.16x slower Faml: 52477.4 i/s - 1.22x slower Haml: 14524.2 i/s - 4.41x slower
faml はたしかに slim より遅い結果になっているが、それほど大きな差は無い。
実際、このベンチマークで生成される Ruby のコードにはほとんど差が無い。差といえば、Faml::Helpers
を extend してるかどうか、出力する html に改行文字が含まれるかどうか、余分な変数代入や to_s
があるかどうか、くらいだ。
% bundle exec slimrb --compile benchmark/view.slim _buf = []; _buf << ("<!DOCTYPE html><html><head><title>Simple Benchmark</title></head><body><h1>".freeze); ; ; ; ; ; _buf << (::Temple::Utils.escape_html((header))); ; _buf << ("</h1>".freeze); unless item.empty?; ; _buf << ("<ul>".freeze); ; for i in item do; ; if i[:current]; ; _buf << ("<li><strong>".freeze); ; _buf << (::Temple::Utils.escape_html((i[:name]))); ; _buf << ("</strong></li>".freeze); else; ; _buf << ("<li><a".freeze); ; _slim_codeattributes1 = i[:url]; case (_slim_codeattributes1); when true; _buf << (" href=\"\"".freeze); when false, nil; else; _buf << (" href=\"".freeze); _buf << (::Temple::Utils.escape_html((_slim_codeattributes1))); _buf << ("\"".freeze); end; _buf << (">".freeze); _buf << (::Temple::Utils.escape_html((i[:name]))); ; _buf << ("</a></li>".freeze); end; end; _buf << ("</ul>".freeze); else; ; _buf << ("<p>The list is empty.</p>".freeze); ; end; _buf << ("</body></html>".freeze); _buf = _buf.join
% bundle exec faml compile benchmark/view.haml _buf = []; extend ::Faml::Helpers; _buf << ("<!DOCTYPE html>\n<html>\n<head>\n<title>Simple Benchmark</title>\n</head>\n<body>\n<h1>".freeze); ; ; ; ; ; ; _faml_compiler1 = (header; ; ); _buf << (::Temple::Utils.escape_html((_faml_compiler1.to_s))); _buf << ("</h1>\n".freeze); unless item.empty?; ; _buf << ("<ul>\n".freeze); ; for i in item; ; if i[:current]; ; _buf << ("<li>\n<strong>".freeze); ; _faml_compiler2 = (i[:name]; ; ); _buf << (::Temple::Utils.escape_html((_faml_compiler2.to_s))); _buf << ("</strong>\n</li>\n".freeze); else; ; _buf << ("<li>\n<a".freeze); ; _faml_html1 = (i[:url]); case (_faml_html1); when true; _buf << (" href".freeze); when false, nil; else; _buf << (" href='".freeze); _buf << (::Temple::Utils.escape_html((_faml_html1))); _buf << ("'".freeze); end; _buf << (">".freeze); _faml_compiler3 = (i[:name]; ; ); _buf << (::Temple::Utils.escape_html((_faml_compiler3.to_s))); _buf << ("</a>\n</li>\n".freeze); end; end; _buf << ("</ul>\n".freeze); else; ; _buf << ("<p>The list is empty.</p>\n".freeze); ; end; _buf << ("</body>\n</html>\n".freeze); _buf = _buf.join
さて、このベンチマークではたしかに hamlit が faml や slim をはっきり上回っている。hamlit 0.4.2 がどのような Ruby のコードを生成しているか見てみる。
% bundle exec ruby -e 'require "hamlit"; puts Hamlit::Engine.new(escape_html: true).call(File.read("benchmark/view.haml"))' _buf = []; _buf << ("<!DOCTYPE html>\n<html>\n<head>\n<title>Simple Benchmark</title>\n</head>\n<body>\n<h1>".freeze); ; ; ; ; _buf << (::Temple::Utils.escape_html(((header).to_s))); _buf << ("</h1>\n".freeze); ; unless item.empty?; _buf << ("<ul>\n".freeze); for i in item; if i[:current]; _buf << ("<li>\n<strong>".freeze); _buf << (::Temple::Utils.escape_html(((i[:name]).to_s))); _buf << ("</strong>\n</li>\n".freeze); ; ; ; else; _buf << ("<li>\n<a href='".freeze); _buf << (i[:url]); _buf << ("'>".freeze); _buf << (::Temple::Utils.escape_html(((i[:name]).to_s))); _buf << ("</a>\n</li>\n".freeze); ; ; end; ; end; ; _buf << ("</ul>\n".freeze); ; ; else; _buf << ("<p>The list is empty.</p>\n".freeze); ; end; ; _buf << ("</body>\n</html>\n".freeze); ; ; _buf = _buf.join
header
や for i in item
のような埋め込み式の行番号がずれてしまってる問題は今回は無視するとして*1、よく見てみると i[:url]
がそのまま埋め込まれている。
これには2つの問題がある。
というわけで、この差によって slim のベンチマークにおいて hamlit が高速になっていると考えられる。
最後に恣意的に用意したマイクロベンチマークの結果を自慢しておくと、実行時に Hash のマージと attribute 全体のレンダリングを行う必要があるケースでは faml が最も高速になっている。 というか、slim は文法はともかくコンパイラは本当によくできていて、この点くらいしかぼくには勝てそうにない。 先程と同じ travis-ci 上での実行から引用すると、
ruby benchmark/rendering.rb benchmark/attribute_builder.haml benchmark/attribute_builder.slim Calculating ------------------------------------- Haml 693.000 i/100ms Faml (Array) 2.417k i/100ms Faml (String) 2.064k i/100ms Hamlit (Array) 1.896k i/100ms Hamlit (String) 1.712k i/100ms Slim (Array) 2.005k i/100ms Slim (String) 1.653k i/100ms ------------------------------------------------- Haml 7.767k (± 4.4%) i/s - 38.808k Faml (Array) 30.186k (±17.5%) i/s - 147.437k Faml (String) 28.314k (±10.8%) i/s - 140.352k Hamlit (Array) 22.381k (±20.5%) i/s - 106.176k Hamlit (String) 21.914k (±10.4%) i/s - 109.568k Slim (Array) 26.194k (± 9.7%) i/s - 130.325k Slim (String) 20.715k (± 5.7%) i/s - 104.139k Comparison: Faml (Array): 30186.2 i/s Faml (String): 28313.7 i/s - 1.07x slower Slim (Array): 26194.2 i/s - 1.15x slower Hamlit (Array): 22381.0 i/s - 1.35x slower Hamlit (String): 21914.2 i/s - 1.38x slower Slim (String): 20714.7 i/s - 1.46x slower Haml: 7766.6 i/s - 3.89x slower
となっている。haml 遅いですね……
haml の高速なレンダリングエンジン faml を書いた
haml との互換性にはかなり気を使っているけど、一部意図的に非互換にしていたり、正確な仕様がわからず再現できていない箇所があったり、haml の奇妙な挙動が直っていたりして、完全に全く同じ動作にはなっていない。
先日 faml を本番に投入して今も動いている。faml 導入にあたって実際にはアプリケーション側のビューを一部書き換えたけど、大量のビューがある中での変更点は十分少なかったと思う。
なお、このエントリ内での「元々の haml」は haml 4.0.6 を指している。過去のバージョンは知らない。
どれくらい高速なのか
元々 faml を書き始めたきっかけは「haml と slim に文法的に大きな差があるわけではないんだし、slim と同程度高速な haml のレンダリングエンジンは書けるはず」という点だった。 なので、slim と同じくらい高速ではあるものの、slim 以上に高速というわけではない。ただ、faml は一部 C 拡張を利用しているので slim よりも若干高速なケースは存在する。
マイクロベンチは faml のリポジトリ内で rake benchmark
すれば実行できる。
travis-ci の after_script で rake benchmark
を実行しているので、そこで結果を見ることができる。
例: https://travis-ci.org/eagletmt/faml/jobs/57138591
なぜ高速なのか
まず slim と同じバックエンドの temple を使っている。 これはシンプルながら強力なライブラリで、temple が定めた S 式にコンパイルすると temple がそれを Ruby のコードに変換してくれ、更に S 式を変換するフィルタを間に挟むことができる。 このフィルタにより最適化を行ったり、独自の文法を導入しつつそのコンパイラを挟んだりできる。
また、属性の描画を高速化するためになるべく静的にコンパイルするようにしている。
例えば %input{checked: false}
とあったらリテラルしかないので静的に <input>
という文字列まで作ることができる。
これを達成するには Ruby のコードを解釈する必要があるので、そのためのライブラリとして parser を使っている。
Ruby のパーサとしては標準ライブラリの ripper や ruby_parser があるけど、僕は parser が一番使いやすいと思っている。
各 AST ノードに場所の情報がちゃんと入っていたり、その場所に書いてあったコード片を文字列として取り出せるのがいい。
属性を静的にコンパイルできなかった場合は実行時に描画することになるが、この部分で C 拡張を使っている。 これは slim には無い特徴なので、動的な属性が多く存在するようなページでは slim よりも若干高速になるかもしれない。
実際に Web アプリケーションが高速になるのか
完全にアプリケーションによると思っていて、1ページ内で多くのビューをレンダリングしていて haml が支配的である場合も、そうでない場合もある。 多くの場合、スロークエリを改善したり、適切にキャッシュを導入したり、アルゴリズムを改善するほうがずっと効果がありそう。 ただ、faml は haml と高い互換性を維持しているので、アプリケーションコードを変更せずとりあえず faml に入れ替えてみるだけで、一部のページで効果があるかもしれない。この点は魅力的だと思う。
非互換な部分は何なのか
属性に Hash を渡したときの挙動
元々の haml では %span{foo: {bar: 'baz'}}
は <span foo-bar='baz'></span>
となるが、faml では <span foo='{:bar=>"baz"}'></span>
となる。
ただし、%span{data: {bar: 'baz'}}
は <span data-bar='baz'></span>
となる。
このように、Hash を渡したときは data 属性の場合に限り hyphenate され、それ以外は単に to_s
される。
高速化のために意図的にこのような非互換性を導入している。
Hash の hyphenate は属性の描画の中では比較的重いものなので、なるべく to_s
だけで済ませたい。
この挙動は data 属性のときにしか普通使わないはずだと思い、data 属性を特別扱いしている。
ちなみに、一部の属性を特別扱いするのは元の haml にも存在して、id
と class
が該当する。
id
に Array を渡すと join("_")
した値がセットされ、class
に Array を渡すと join(" ")
した値がセットされる。
class
のほうの挙動を知ってる人は多そうだけど、id
のほうは知らなかった人も結構いるんじゃないかと思う。
常時自動 html エスケープ
元々の haml では Rails プロジェクトでなければデフォルトでは自動 html エスケープが無効になっている。
たまに sinatra を使ったときにはまったりするので、faml では自動 html エスケープを常に有効化している。デフォルトを無効にする手段は提供していない。
Rails プロジェクト以外で html エスケープを無効化したい箇所では !=
等の文法を使う。
ugly モードのみ
元々の haml では html をインデントするかどうかを示す ugly というフラグがあったけど、faml では常に ugly、つまりインデントを行わない。
Haml::Helpers 無し
succeed や surround といった haml 固有のヘルパメソッドが存在していたけど、faml では preserve 以外は提供していない。 preserve は使い所がまれに存在するけど、それ以外は無くても困らないと思って外した。 もし他に本当に必要なヘルパメソッドがあったら issue を立てて教えてください。
Object reference 無し
この機能を使ってる人いるんだろうか…… http://haml.info/docs/yardoc/file.REFERENCE.html#object_reference_
whitespace removal の仕様
haml には >
と <
によって空白を取り除く文法があって、これの詳細な仕様が不明で再現しきれてない。
もし明らかに間違っているものを見つけたら issue を立てて教えてください。
元々の haml 側の挙動も怪しくて、例えば
%div< hello world %div< #{'hello'} world
を render するとどうなるかというと、
<div>hello world</div> <div>helloworld</div>
になる。faml は前者の挙動が正しいと判断し、
<div>hello world</div> <div>hello world</div>
となるようにしている。
haml のバグ、あるいは一貫性の無い仕様
plain フィルタに interpolation が含まれていたときの空行
whitespace removal で触れたケースに似てるけど、
:plain hello world %br :plain #{'hello'} world %br
を ugly mode で render すると、元々の haml では
hello world <br> hello world <br>
となって、謎の改行が入る。faml ではこれも haml のバグだと判断し、
hello world <br> hello world <br>
となるようにしている。 ちなみに markdown フィルダでも同様の謎の改行が存在する。
空の plain フィルタでの空行
また、同じく :plain
において、:plain
の中身が空だったときに空行が入るかどうかに差がある。
%br :plain %br
これを元々の haml で render すると
<br> <br>
になる一方、faml では
<br> <br>
になる。
! または & で始まるテキスト
haml において、行頭の !
または &
には意味があり、それぞれ html エスケープをしない、する、という意味を持つ。
!=
ではなく !
だけの場合は、その後にくるテキスト内の interpolation において html エスケープをするかしないかを制御する。
%span!hello !hello %span! hello ! hello
を元々の haml が render すると
<span>hello</span> !hello <span>hello</span> hello
となり、タグ付きの場合は !
の後に空白がなくても !
が haml の記号として解釈される一方、タグ無しのテキストの場合は !
の後に空白がないと haml の記号として解釈されない。
faml では
<span>hello</span> hello <span>hello</span> hello
となり、どちらの場合も行頭の !
を haml の記号として解釈する。&
の場合も同様。
!
や &
で始まるテキストを書く場合は、行頭を ==
または \
でエスケープする必要がある。
まとめ
既に haml から slim に移行してしまったり、最近だとクライアントサイド JS での描画やモバイルアプリケーションがメインになってサーバサイドは単なる JSON API サーバになってるところも多そうだけど、もし haml が好きで使ってるけど遅いのが気になっている人がいれば是非 faml を試してみてほしいです。