Pure JavaScript Template Engine その弐
どうも、こんにちは。hirokiです。前回は、HTML::Template::ProのJavaScript実装を紹介させていただきました。今回はその実装部においてのちょっとした工夫についてと、Webフロントエンドのパフォーマンスチューニングについて簡単にまとめさせていただきます。
正規表現の話
通常、テキストベースのDSL評価系を作成する際にはyacc/lexなどで文法記述を行うのが定石なんですが、
- シンプルな文法であること
- 構文木の評価からコードジェネレートに方向性を転換した
- テキストマッチングの回数や高速化のための制御がしやすい
などの理由から、HTML Template(JavaScript)では正規表現ベースの文法解釈を行っています。( HTML::Template::Proのyacc文法ファイルをそのまま使えば記述は楽だったのですが...)以下に実際に利用している正規表現を示します。
/<(\/)?TMPL_(VAR|LOOP|IF|ELSE|ELSIF|UNLESS|INCLUDE)\s*(?:(?:DEFAULT)=(?:'([^'>]*)'|"([^">]*)"|([^\s=>]*)))?\s*(?:(?:ESCAPE)=(?:(JS|URL|HTML|0|1|NONE)))?\s*(?:(?:DEFAULT)=(?:'([^'>]*)'|"([^">]*)"|([^\s=>]*)))?\s*(?:(NAME|EXPR)=(?:'([^'>]*)'|"([^">]*)"|([^\s=>]*)))?\s*(?:(?:DEFAULT)=(?:'([^'>]*)'|"([^">]*)"|([^\s=>]*)))?\s*(?:(?:ESCAPE)=(?:(JS|URL|HTML|0|1|NONE)))?\s*(?:(?:DEFAULT)=(?:'([^'>]*)'|"([^">]*)"|([^\s=>]*)))?\s*>/
正規表現に慣れている方でしたら、ふむふむなるほどね。となるところでしょうが、私自身はそんなに正規表現が得意でないので、これ自体をちょっと記述しようとかメンテナンスしようという気にはなりませんでした。
Perl/Rubyなどには改行可能な正規表現リテラルが存在しますが、JavaScriptにはこういったものは存在しません。存在しなければ作ればいいということで、正規表現を構築する関数を作成し、以下のような記述を可能にしました。
HTML.Template.createMatcher('%',[ "<", "(%/)?",{map:'close'}, "TMPL_", "(VAR|LOOP|IF|ELSE|ELSIF|UNLESS|INCLUDE)",{map:'tag_name'}, "%s*", "(?:", "(?:DEFAULT)=", "(?:", "'([^'>]*)'|",{map:'default'}, '"([^">]*)"|',{map:'default'}, "([^%s=>]*)" ,{map:'default'}, ")", ")?", "%s*", "(?:", "(?:ESCAPE)=", "(?:", "(JS|URL|HTML|0|1|NONE)",{map:'escape'}, ")", ")?", "%s*", "(?:", "(?:DEFAULT)=", "(?:", "'([^'>]*)'|",{map:'default'}, '"([^">]*)"|',{map:'default'}, "([^%s=>]*)" ,{map:'default'}, ")", ")?", "%s*", /* NAME or EXPR */ "(?:", "(NAME|EXPR)=",{map:'attribute_name'}, "(?:", "'([^'>]*)'|",{map:'attribute_value'}, '"([^">]*)"|',{map:'attribute_value'}, "([^%s=>]*)" ,{map:'attribute_value'}, ")", ")?", /* DEFAULT or ESCAPE */ '%s*', "(?:", "(?:DEFAULT)=", "(?:", "'([^'>]*)'|",{map:'default'}, '"([^">]*)"|',{map:'default'}, "([^%s=>]*)" ,{map:'default'}, ")", ")?", "%s*", "(?:", "(?:ESCAPE)=", "(?:", "(JS|URL|HTML|0|1|NONE)",{map:'escape'}, ")", ")?", "%s*", "(?:", "(?:DEFAULT)=", "(?:", "'([^'>]*)'|",{map:'default'}, '"([^">]*)"|',{map:'default'}, "([^%s=>]*)" ,{map:'default'}, ")", ")?", "%s*", ">" ]);
第一引数にescape charを示して、配列として正規表現を記述します。captureの次の要素(正確には次でなくても良いが)にはキャプチャされた値のプロパティ名を指定できるようにしています。実現方法はぜひ実装をご覧ください。
コメント可能で可読性の良い正規表現を記述したいという際には、このような方法も頭のすみにおいておくと良いかもしれません。
※ちなみにこの正規表現は「完全な」HTML::Templateのシンタックスを表現していませんが、マナーのよいほとんどすべてのテンプレートをパースすることができます。
パフォーマンスチューニングの話
HTML Template(JavaScript)の実装に際しては、順次リファクタリングする必要がありそうだという点と、インタフェースがある程度決まっているということから、TDD(テスト駆動開発)を心がけました。また、JavaScriptライブラリでのUnitTestにはscript.aculo.usのunittest.jsを採用しました。unittest.jsはベンチマーク用関数も同梱されており、あらかじめ必要と思われるベンチマークとテストを記述した上で、パフォーマンスチューニングを行いました。
unittest.jsを用いると以下のようにベンチマークを記述することができます。
benchmark(function(){ new HTML.Template({type:'text',source:COMPLEX_TMPL+Math.random()}); },100);
しかし、ベンチマークだけではパフォーマンスに悪影響を与えている箇所の特定は難しいので他の言語と同様プロファイリングをすることで、ボトルネックを発見します。
プロファイリングの話
JavaScriptのプロファイリングは特別な環境はほとんど必要なくなりましたが、あまり利用されていないのでここで簡単に紹介させていただきます。
現在、Safari4(Webkit Nightly Build)かFirefox3.x+Firebugで簡単にプロファイリングすることができます。今回はFirefox&Firebugでのプロファイリング方法を示します。
document.getElementById('test').addEventListener('click',function(){ // DO SOME ACTION },false);
まず、上記のように任意の操作可能なエレメントに対して、イベントを付与しプロファイルをとりたいアクションを記述してください。
この際に本質的動作でないalertやconsole.log、同期ネットワーク処理、DOMのリードライト繰り返しなどの副作用のある操作は控えたほうがより正確にコード自体のボトルネックを発見することができます。
準備ができたら、FirefoxおよびFirebugを起動し、該当HTMLを開きます。そして、
- プロファイルボタンを押す
- プロファイル対象の操作を行う
- プロファイルボタンで終了
今回は例として、以前、小山先生のエントリにて公開されましたJavaScriptによるCamellia実装を用いました。
プロファイルを行うと(?)()という関数名がたくさん表示されるのがお分かりになるかと思います。これは無名関数をあらわすプロファイル上の表現なのですが、これではどの関数がボトルネックであるのか分かりにくいですね。
これは、
PACKAGE.prototype._xor_block = function (x, y, l) {(略)}
のように関数が実装されており、PACKAGE.prototype._xor_block関数がプロファイラ上には無名関数の代入として表現されているため、関数に「処理系上の公式な名称」が付与されないために、このような状態になっています。
Firebugプロファイラの場合、以下のような方法で「処理系上の公式な名称」を付与することができます。
// function文 function test(){ return true; } // 名前付function式 var vtest = function test(){ return true; } // オブジェクトリテラル中 var Test = { test:function(){return true;} }
プロファイリングを行う場合にはこのような条件にも注目して実装することで、問題の発見を容易にすることができます。
これらを踏まえて、実装に手を加えプロファイルを取り直すと以下のようになります。
このようにすればボトルネックとなる関数がどれなのか、判別することができます。
時間感覚の話
Templateモジュールのようなコードを書く際に注意しなければならないのが、サーバサイドのページとの時間感覚の違いです。以下に感覚的なレベルでのサーバサイド、クライアントサイドのボトルネックに関して表にまとめたものを示します。 サーバサイドの平均的なプログラムにおいて、もっともボトルネックとなるのはデータベースサーバ等への処理です。クライアントサイドも同じように外部環境ネットワークとの通信処理がもっともネックになります。注目するべきは、クライアントサイドプログラミングにおいて、DOMの描画リフレッシュ操作もボトルネックになるということです。
一方、サーバサイドにおいては処理の内容に応じてスケールアウトするための技術が盛んに議論されていますが、クライアントサイドにおいては1ユーザのために、ユーザのCPU時間を消費しますのでスケールアウトという観念はありません。しかし、GUIプログラムにおいては採用するアーキテクチャから、異なったアプローチでのレスポンス改善策があります。以下にプログラミングパラダイムから考えたクライアントサイドプログラミングとサーバサイドプログラミングの違いを示します。
ほとんどの場合、サーバサイドプログラミングでは1リクエストあたりの処理は1OSレベルスレッドが割り当てられ、体感時間と実際にプログラミングが処理を行っている実働時間はイコールとなります。最近では、こういった状況に対応するためにJob Queue Serverなどを用いることで、プログラムの実行タイミングを遅延&平滑化することが注目されていますが、基本的にはこういったモデルが採用されています。一方で、クライアントサイドのGUIプログラミングにおいては、ユーザアクションに対して、レスポンスを返すまでの時間が体感時間となりますので、ユーザアクション以前に実行しておける処理が多くあります。このような状況では、如何にユーザアクションが発生する前にできうるかぎりの処理をしておくか、といったことが重要になります。
このようなベースを踏まえたうえで、HTML.Template(JavaScript)の各フェーズでの処理時間に注目してプロファイルをとってみると以下のようになります。
evalと文字列処理という重い処理を実行するTemplate ParseとCompileに6割近い時間が消費されていることがわかるかと思います。そしてこれらの処理は多くの場合、ユーザアクションの前に実行可能です。
そこで、以下の図のような戦略をとります。
もっとも時間のあるブラウザのアイドル時間にParseとCompileを実行し、その後にユーザアクションが予見可能なプリアクションが存在すれば、そのタイミングでデータのアサインとテンプレートテキストの出力を行います。
そしてもっともユーザが即時の応答を期待するユーザアクション時にはDOMのリフレッシュ処理のみを動作させるようにするというものです。
実際のコードでは以下のようになります。
// ブラウザのアイドル時間にtemplatesクラスの付いているテンプレートを // コンパイルする。 HTML.Template.precompileBySelector.defer('.templates'); /* .HTML_TEMPLATE .HTML_TEMPLATE_DEFERRED は予約され、それぞれDOM構築時、DOM構築後最初のアイドル時間 にコンパイルされる */ // クリック時にテンプレートを呼び出す // コンパイルされていれば、キャッシュが効いていて動作 // コンパイルされていなければ、コンパイルして実行 $('button').observe('click',function(){ var template = new HTML.Template('dom:hoge_tmpl'); template.param({ test:1 }); $('hoge').update(template); });
HTML.Template(JavaScript)では自動的にDOM構築時や最初のブラウザアイドル時間にテンプレートをコンパイルするように指示できるclassNameを定義しています。マークアップによる記述のみでこれらの非同期で煩雑な処理を隠蔽することができるため、テンプレートの処理自体にかかる動作はほとんど意識することなく、プログラミングを行うことが出来ます。
また、これは実験的な内容なのですが、DOMの更新処理時間そのものをより、高速にすることはできないのだろうかと勘案した内容を紹介します。DOM更新処理は内部的には、DOMエレメントを構築し、それを描画するという二つの処理に分解されます。
図に示すと以下のようになります。
ということは、この内部的に行われている処理のDOMエレメント構築をプリアクション時に実行できれば、より高速なUXを実現することができます。
$('button').observe('click',function(){ var template = new HTML.Template('dom:hoge_tmpl'); template.param({ test:1 }); // renderを呼び出すとtoElementメソッドが自動生成され、 // documentFragmentを構成する。 template.render(); $('hoge').update(template); });
//内部処理 if(document.createDocumentFragment){ var dfrag = document.createDocumentFragment(); // 事前にエレメントを構成して、DocumentFragmentとしておく Element._getContentFromAnonymousElement( tagName,this.output() ).each( function(e){ dfrag.appendChild(e); }); // DocumentFragmentのコピーを引き渡す this.toElement = function(){ var tmp = dfrag.cloneNode(true); return tmp; } }
テキストのElement化としては、実験的内容であったためにPrototype.jsのプライベートメソッドを利用しています。そのうち、実用レベルになった際には、別途実装するようにします。このようにすることで、update性能にこそ差はほとんど見られませんでしたが、insert時(DOM追加時)の性能は約3倍になりました。まだ、透過的に実用できる状態にはなっていないのですが、パラメータが定義されてから、アイドル時間が発生すると自動的にrenderしておくこともできます。
まとめ
今回はTemplateエンジンの実装にかかわるちょっとしたノウハウとパフォーマンスチューニングについて、簡単に紹介させていただきました。Webフロントエンドのアーキテクチャは、バックエンドのモデルと異なるアーキテクチャを採用しているためそれぞれのアーキテクチャに適合したパフォーマンスチューニングの方法論があり、適切に状況を見ることでより快適なアプリケーションを構築することができるようになります。これからも、トピックがあればWebフロントエンドについての話題を本ブログで紹介できればと思います。