3月末に、Hamlit v0.1.0を作りSlimやErubisより高速なHaml実装「Hamlit」をリリースしましたという記事を書いた。 haml-specを通しているのでHamlと高い互換性と持っていてかつ速いという宣言をしたものの、実際にリリースしてみると随所からバグ報告が上がり、
- hamlを置き換えただけでは動かない
- haml-specは互換性の保証にはならず、速いのは互換性が低いからでは?
- このベンチはHamlitがhtmlエスケープをしていないから速いだけでは?
のような声が随所から上がった。
今日、attributeのescapeに対応し、 全てのissueを潰した上で、Slimより速いベンチを出すことに成功した ので、v0.1.0での問題点やそこからの変更点などについて書きたいと思う。
v0.1.0での問題点
haml-specガバガバ問題
@k0kubun それな
— おっさん (@eagletmt) 2015, 3月 30
haml-specに記述されている仕様を担保するにはコーナーケースのチェックが十分でなく、完璧に実装しなくても通すのは簡単だよなあとは思っていて、リリース直後に「haml-specガバガバすぎる」という発言をした。が、HamlやFamlだとパースできない入力がパースできたり、Famlはhaml-specを通していなくてHamlitはhaml-specを通しているので、v0.1.0リリース時点では「少なくともFamlよりはHamlitのほうが互換性高いのでは?」と考えていた。
が、実際にリリースしてみるとhaml-specだとチェックできないケースにパフォーマンスにかなり影響のある仕様があり、それにFamlは対応していてHamlitには実装されていなかったので不当に速いベンチが出ている状態だった。 Famlの作者にパフォーマンスに影響のあるissueをいくつもいただいていたのだけれど、今日やっと直った。
htmlエスケープ
faml と slim、hamlit のパフォーマンスの差 - eagletmt's blogで、
なぜこのベンチマークで faml が遅いのか、結論から言うと、この中で faml だけ自動 html エスケープが有効になっているからだ。 haml はデフォルトでは自動 html エスケープは無効であり、hamlit もそれに倣っている。
ということが書かれていて、これは実際にその通りで、htmlエスケープはかなりパフォーマンスに影響があるにもかかわらずHamlitはベンチ上でこれをさぼっていたので、完全に不公平なベンチになっていた。前述の非互換性も含め、あまりにも不公平なので社内チャットで叩かれていた。
この仕様はhaml-specを通す都合でこうなっていたんだけど、htmlエスケープをデフォルトで無効にしたいケースは少ないと思ったので、Hamlitでもhtmlエスケープを必ず行うようにし、haml-specはHamlにescape_html: true
を渡した状態で通すように変更した。
というわけで、現在のベンチマークはHamlitもhtmlエスケープを行うようになっていて、公平なベンチに変わった。後述の工夫により、htmlエスケープが入った分Erubisに負けてしまったもののそれほどの差はなく、htmlエスケープを行わないSlimより速いベンチが出ている。
haml gemを置き換えても動かない
SlimやErubisより高速なHaml実装「Hamlit」をリリースしました - k0kubun's blogb.hatena.ne.jp
- [haml]
haml gemを置き換えただけだと動かなくて悩んでる / id:k0kubun 尻尾を捕まえたのでとりあえずissue起こしておきました
2015/03/31 12:04
このブコメをいただいたときは顔面蒼白でしたが、id:sho さんのテンプレートがハードタブでインデントされていて、Hamlit v0.1.0がタブのインデントに対応していなかったということでした。v0.3.4でタブのインデントに対応しました。 せっかくリリース直後に使っていただいたのにご迷惑をおかけしました。
v0.1.0からの変更点
attribute escape対応
faml と slim、hamlit のパフォーマンスの差 - eagletmt's blog
& や < 等が html エスケープされない
これはさすがに厳しいのでは
=
演算子の結果のhtmlエスケープは実装してあったんだけど、タグの属性のhtmlエスケープに対応していなかった。これが原因でhamlit導入を断念した人もいるくらいで、相当厳しいので直した。
当然Hamlitのベンチ用コードにhtmlエスケープが入ってくるので遅くなるんだけど、brianmario/escape_utilsというC拡張のhtmlエスケープライブラリを使うことであまり速度を落とさずに済んだ。ちなみにこれはFamlの受け売りです。
消えるboolean attribute
faml と slim、hamlit のパフォーマンスの差 - eagletmt's blog
i[:url] が nil や false だったとき、<a href='false'> のような出力になる
haml では、属性の値が nil または false のときは、その属性を出力しない仕様
ちなみに slim が生成したコードを見てわかるように、slim も同じ仕様
ただ、これに関しては「hamlit ではこういう仕様です」と言うこともできると思う
これも実害があり、たとえばdisabled=''
やdisabled='false'
はdisabled扱いなので、普通に挙動が変わってしまう。
しかし、hrefやclassとかに関していうと、hrefにnil
やfalse
が入ってくる時点で何かおかしいし、classの結果がnil
でclass=''
がレンダリングされたからといって実害があるわけではない。
そこで、Hamlitはcheckedやdisabledのようなboolean attributeだけ消えるようにした。 このページでboolean attributeと記述されている属性は全て消える。
こうやって限定しないと、毎回href='
のような文字列片を後からつなげることになり効率が悪く、遅くなってしまう。
Famlはなるべく互換性を保つ方向に仕様を倒しているけど、Hamlitはブラウザ上で挙動が変化しない範囲で互換性を削り高速化することにした。
バックトレースの行番号
テンプレートエンジンとバックトレース - eagletmt's blog という面白い記事がある。
対応していたつもりだったんだけどぶっ壊れていて、faml と slim、hamlit のパフォーマンスの差 - eagletmt's blogでもその問題について言及されている。2行直したらちゃんと動くようになった。
その他バグたち
いろいろあったけどこの記事を書いている時点で存在していたissueは全て直した。
正直最初のリリースでバグを出さないのは人間には不可能だと思っていて、多分v1.0.0にも細かいバグがたくさん眠っていると思うけど、しょうがない。人間だもの。
最終的なベンチマーク
残念ながらErubisには負けてしまったものの、当初の目的である打倒Slimを達成したのでそこそこ満足度が高い。
戦争と学び
僕はFamlの作者である id:eagletmt さんにペアプロをしていただいたことがあるけど、ペアプロは自分が考える前に答えがわかってしまうことが多く、学びが少ない。 反面、自分よりすごい人のライブラリに対して対抗実装を用意して戦争をふっかけると、自分が必死で考えた成果に対してより高度な意見をいただけてかなり勉強になる。優秀な人を見つけたら、戦争をすると良い。