たぷつきません

おなかがでてきた。もうたぷついてるやん。

やばい最強テンプレートエンジンになってきたかも。

 ここ数ヶ月のもっぱらの仕事はmayaaをチューニングしつづけるということなんですが、かなり凄いことになってきました。メモリ消費を抑えることと高速化の両面から改良をしているのですが、高負荷環境でもかなり耐えられるmayaaになっていってます。同じ会社にmayaaコミッタが2人もいるので、内容が良ければその部分は本家に取り込んでもらえます。(^^;
 私が携わったのは、プロセッサツリーの最適化、消費メモリを軽減するためにURI,QName,PrefixAwareName,PrefixMappingといったオートオブジェクトをSoftReferenceと組み合わせてファクトリ管理するようにしたこと、オートビルド機能、ビルド結果のシリアライズ、ページキャッシュの細いメモリ管理などです。
 

オートビルド機能

 sugaさんからMLでアナウンスがありましたが、次のmayaaのバージョンにはオートビルドの機能が組み込まれます。オートビルド機能を有効化するとmayaa立ち上げ時に特定のパターンのテンプレート(普通は.html)を検索し逐次ビルドします。mayaaは初回表示が遅いという弱点があります。これはビルド・レンダリングの両方が実行されるためです。ビルド後はメモリキャッシュされますのでレンダリングのみとなり速くなります。クラスを作るわけではないですがJasperと雰囲気が似ています。オートビルドを生かしておけば、サーブレットコンテナを立ち上げて放置しておくだけで自動的にテンプレートを検索してビルドを実行してくれますので、利用者がアクセスしてくる時にはそれなりに速くなったように見えるというわけです。それ以外にビルドテストを一気にできるというメリットもあります。ビルドエラーの場合はエラーログが吐かれるので、未テストのページを検出することができます。また正常にビルドできたページはビルド時間がログに記録されますので、ブラウザで表示しなくても重いページはどれかが分かります。ServiceProviderファイルで、engineへのオプションとして、autoBuild=trueとすることでこの機能が有効になります。
 しかしオートビルドの実装と試験をしながら2点の問題に気付きました。まず1点目は、ビルドしてもSoftReferenceの働きによりメモリから追い出された場合は、リビルドが実行されるということです。たとえば現行たずさわっているあるプロジェクトでは何百ページもあるため、ページキャッシュに入ってもビルドしたそばから解放されるということが少なくありません。mayaaの場合はビルドしたページ・テンプレートを、SoftReferenceでメモリキャシュして次回の再利用に備えるという動作をしていますが、結構JVMの都合で気まぐれに解放されます。SUNのGCの性質的には若いものをGCしてそれでも生き抜いたものがOLD領域に行きますので、この若いうちのチェックでよく引っかかってサヨナラされることが多いような気がします。(実は全然SoftReferenceが効いていなかったことが後日発覚したのですが、それについてはのちほど)
 2点目は、起動時にテンプレートを探してビルドしようとビルドに掛かる時間はリクエスト時と変わりがないことです。定義されているテンプレート配置場所のディレクトリ階層を再帰追いして見つかったものからせっせとビルドしていきますが、開発中は普通はサーブレットコンテナを起動してからすぐにテストしようとしますよね。そうなるとオートビルドが終わるまでなんて待ってられません。むしろバックグラウンドで今すぐ見ないページをビルドするためにCPUパワーが使われることになりますので恩恵があまり無いように見えます。

というわけでシリアライズ

 せっかくビルドしたのだから、それを高速に保存・復元したい…というわけで、シリアライズ対応しました。なかなかに手が掛かりましたがなんとか完成。メモリから追い出されても復元する際には先にシリアライズしたファイルを読み込んでその中に保管されたビルド時のタイムスタンプがありますので2度以上のビルドを避けられます。オートビルドを使おうと使うまいと高負荷でのストレステストをするとリビルド回数がかなり増えますので、2回目以降はビルドではなくデシリアライズで済むというのはとても効果があります。もっと言えば、サーブレットコンテナを再起動してもデシリアライズが先ですので、テンプレートに変更がなければ開発中に再起動するたびに時間がかかるというストレスも軽減します。ServiceProviderファイルで、engineへのオプションとして、pageSerialize=trueを設定することで有効になります。WEB-INF/.mayaaSpecCache というディレクトリを作成して、*.serを .mayaa, .html毎に作成します。メモリ上にあった場合と同様の管理方法を引き継いでいますので混乱はないでしょう。mayaaのバージョンを替えてデプロイする場合だけ、このディレクトリを消してクリーンビルドさせればよいです。mayaaのビルド判定はページファイル・テンプレートファイルのタイムスタンプが、ビルド時のそれと異なっているかがきっかけです。リバートしてもOK。
 さてどれだけ速くなったでしょうか。現行プロジェクトで409のhtml(.mayaaとあわせると762ファイル)をオートビルドするのに掛かった時間を以下に示します。2回目とあるものは、jettyを再起動したことを示しています。

シリアライズなし 1回目
  page all build time:       page all build time: 181374 msec.
  
シリアライズなし 2回目
  page all build time:       page all build time: 177343 msec.
  

シリアライズあり 1回目
  page all build time:       page all build time: 189404 msec.

シリアライズあり 2回目
  page all build time:       page all build time: 24741 msec.

 …はい、きました!いくとこ行き着いてしまいました感じ!?もちろん3回目以降は2回目と同じ時間です。2回目以降はビルドがデシリアライズで済むので、7倍という恐るべき速さにまでなりました。1回目はシリアライズ自体を別スレッドで行っているので実際にはもっとCPUパワーを使っていますが、それを差し引いてもいい感じです。1回目のシリアライズに掛かる時間をどう見るかですが、以降何度も再起動することを考えたら効果は大きいのではないかと思っています。変更のあったページしかビルドされませんしそれもまたシリアライズされますし。特に開発時のストレスは軽減しそうです。ただビルドが速くなってもレンダー自体が元々重いページはなかなか解決しません。それとページがレンダリングされてようやく揃う情報や初回描画を以ってキャッシュされる情報などもありますので、デシリアライズが速くても初回描画はやっぱりちと重いです。ビルドが7倍速くなったからといって描画も含めて速いわけではないのです〜。惜しい。
 それと、独自のMLDを作成する場合はこれまで以上に気を使う必要がでてきます。シリアライズ機能と組み合わせるためには独自プロセッサでも対応しないとならないためです。ちなみにこの762個のシリアライズファイルの合計は12.3MBほどでした。

メモリリークの解消

 実はオートビルドをテストしている際に350ファイル目を超えた頃ぐらいからOutOfMemoryになることが分かりました。SoftReferenceで管理されていたはずのページ・テンプレートが実際には解放されていなかったのです。どうもこれはmayaa初期のころからの不具合でした。ノードツリー、プロセッサツリー、パラメータやスクリプトオブジェクトなどが複雑に絡み合っていて循環参照がいくつも起こっていたのです。parentとchildrenへの参照をそれぞれ持っているから、循環参照は当たり前なわけで、自動的に解放させることはできません。そんなわけで、自前でGC的な仕組みを用意することにしました。これにはPhantomReferenceとSoftReferenceの両方を組み合わせるテクニックで対応しました。まずは誰も参照しないゴミオブジェクトをnewしてSoftReferenceとPhantomReferenceに突っ込みます。PhantomReferenceのキュー監視スレッドを別途用意することでキューにPhantomReferenceがきたら(ゴミオブジェクトが解放されたら)用意しておいたイベントハンドラに通知します。イベントハンドラでは再度ゴミオブジェクトを載せます。これにより繰り返しハンドラが呼ばれることになります。VMからしたら折角ごみ掃除したのにまた捨てられるという迷惑な仕組みですが。(^-^;
 イベントにログを書いていろいろテストすると面白いことにVMのメモリ状態や負荷の状態などによって、イベントハンドラの呼び出し頻度は変わります。全然余裕がある場合やJava自体にアクセスが少ない場合は呼ばれにくいです。この辺りはVMによって制御に工夫がいろいろあるらしいです。で、このゴミ掃除したよイベントが発生するたびに、キャッシュしているページやテンプレートに持たせたカウンタをインクリメントして、閾値を超えたらキャッシュから除外してクリーンナップ(参照関係を解きほぐしてGC対象に)するという小賢しい真似を選択したというわけです。Webからキャッシュ済みのページ・テンプレートに、取得要求があったものはそのたびにカウンタをリセットすることで、使用頻度の高いものは解放されにくくしています。この閾値はengineパラメータでsurviveLimit=数値で指定します。実メモリとのバランスもあるのでこの数値の決定は経験を要しますが。本当は美しい参照関係を作りたかったのですが、利便性が失われ、ランタイムにより時間が掛かる方法になってしまうので、なんちゃってGCを取らざるをえなかったという感じです。
 メモリが潤沢で全ページをオンメモリにできる場合は、surviveLimit=0 にすれば、ページ・テンプレートに更新があった場合しかリロードされないようになります。

とどめのノードツリー最適化

 プロセッサツリーの最適化は描画速度のみの改善といえます。テンプレートから読み込んだノードツリーの中には、最適化の影響でいらなくなったものがゴミとして残っています。ノードツリーを崩すと影響が大きかったので中々手をつけられなかったのですが、ようやく綺麗に再構築することができました。これで消費メモリがかなり抑えられるようになったはずです。IDやXPathでインジェクションされない只のノードツリーは消え失せます。
 余談ですが、現在の1.1βシリーズで実装されたプロセッサツリーの最適化について。これも手掛けたんですが、結果オーライだけれども、せっかく生き物のようなプロセッサを土に還すというような実装なので、ものすごい限定した領域では副作用も出てしまうという問題があったりします。
 htmlへのmayaaのインジェクションの仕組みはコンポーネント(これもページの変化形)と組み合わせることで、xmlns宣言も、.mayaaからバインドするというように対応したのですが、一度ページがビルドされると動的だったxmlns宣言も静的なものに置き換わってしまうので、ページごとにネームスペースのプレフィックス定義をばらばらにしちゃっていた場合に、別名のprefixで同じURIを二重定義した結果で出力されるということになります。html、xhtmlではほとんどプリフィックスを使わないのであまり問題になりませんし、そもそも同じネームスペースURIに対していくつも異なるプリフィックスを使うことじたいが美しくないので軽微だとは思いますけどね。かっこ悪いけどエラーではないですし。
 ただチューニングや最適化を行うと犠牲もあるという感じです。TemplateBuilderへのオプションでoptimize=trueがmayaa1.1βでデフォルトになっています。プロセッサツリーの最適化+今回のノードツリー最適化がこれで効くことになります。

 …さて、こんな感じでmayaaは水面下で着々とバージョンアップの準備が進んでいます。手元では attribute プロセッサのvalueに、null代入すると、属性定義が削除されるという機能も追加してたりします。echoプロセッサではテンプレート上の全ての属性が出てしまいますが、部分的に削除したい場合に便利なんじゃないかと思います。ちょっと修正量が多すぎてコミットが大変そうですが、いつか1.1でβが取れる頃には、上であげたいくつかの機能が入ってくるかもしれません。お楽しみに。