はじめまして!08年度新卒エンジニアの「きょろ」こと井上恭輔と申します。ミクシィではコミュニケーション開発チームというところで、mixi上の色々なコミュニケーションサービスの開発を担当しています。
 就職で東京に出てきて早10ヶ月、最初は周囲の歩く速度に付いて行けなくて悩んでいましたが、今では新宿駅を迷わず歩けるまでに成長しました。本日は慣れたついでに、そろろそエンジニアブログにも仲間入りしたいなと思いましたので、記事の初投稿に挑戦してみようと思います。
 曰く「ハードボイルドな技術ネタ」の多い当ブログですが、今回は頭を使わずに読める、文字通り「コーヒーブレイク」的な記事をお届けできればと思います。駄文ではありますが、お付き合い頂ければ幸いです。

エンジニアのガソリン「コーヒー」

 みなさんコーヒーはお好きですか?私はコーヒーが大好きで、1日にかなりの量のカフェインを摂取します。朝はブラックコーヒーを飲まないと始まりませんし、コーヒーが無ければコーディングの質も量も低下するような気がします。学生時代からの感覚値ではありますが、どうも研究者やエンジニアという生物にはコーヒー好きが多いようで、「コーヒーが無いと生きていけない!」と思っている人もきっと少なくは無いはず。ある意味「プログラマとはコーヒーに含まれるカフェインをコードに変換する職業」だと言っても過言ではありません。コーヒーは言わばエンジニアを動かすガソリンのようなものであり、私たちの「生命の源」だと言えるでしょう。

エンジニアの朝は一杯のコーヒーからはじまる

図1 エンジニアの朝は一杯のコーヒーからはじまる

マイ・コーヒーメーカーから始まった物語

 配属当時、残念ながらミクシィ開発部には美味しいブラックコーヒーを飲める環境がありませんでした。ティーサーバで紅茶・緑茶・玄米茶・烏龍茶、コーヒーサーバでラテ・カプチーノ・エスプレッソなどを自由に飲むことは出来ましたが、私が求めるのは“穢れ(けがれ)”を知らない純粋で無垢な無糖のブラックコーヒーであり、ミルクやシュガーは不純物以外の何者でもありません。 「美味しいブラックコーヒーを毎日浴びるように飲みたい!」 この想いは日々強くなっていき、ある時、カッとなった私は自腹でコーヒーメーカーを購入し、自席の横に設置することにしました。

図2 導入されたコーヒーメーカー

図2 導入されたコーヒーメーカー

 毎朝漂うコーヒーを炒れる豊潤な香り。 出社後にメールチェックをしながら飲む至福の一杯。  私の導入したコーヒーメーカーは意外にもチームメンバーや周りのエンジニアの方々に暖かく迎えられ、次第にコーヒーメーカーを利用してくれる方も増えてきました。しかし、全てが幸せに思われた一方で、何日か運用する過程でいくつかの問題点も出てきました。

  • 仕事に集中してるとコーヒーが入ったことに気づかない
  • やはりコーヒーは炒れたてが美味いので抽出直後に飲みたい
  • 遠くの人にコーヒーが入ったことを連絡するのが面倒
  • 一日にどのくらいの回数炒れているのか、今ストックしてあるコーヒーはいつ炒れたものなのかわからない

 弊社のエンジニアは私も含め、モニタに向かいヘッドホンで音楽を聴きながら仕事をしていることが多いので、コーヒーメーカーが近くにあっても、なかなか抽出完了に気が付きません。また、コーヒーが出来たことを離れた席の人に毎回伝えるのも面倒ですし、運用上は抽出記録が残らないのも何かと不便です。そこで私は、金曜日が自由研究時間になるODF(One Day Free)制度を利用して、コーヒーのドリップ状況をネットワークを介して通知してくれる機能を持った「オンラインコーヒーメーカー」を開発しようと考えました。

オンラインコーヒーメーカー「萌香」

 オンラインコーヒーメーカー「萌香(もか)」は、コーヒーのドリップ開始と完了時に、社内のIRC及びtwitterでコーヒーの抽出状況を教えてくれる機能を持った次世代コーヒーメーカーです。ドリップ完了時には抽出時間などに応じて一言メッセージも付け加えるようにしました。 利用者は既存のIRCクライアントやtwitterクライアントを使用することで、手軽にドリップ状況を知る事ができます。

図3 「萌香」によるIRC及びtwitter通知動作画面

図3 「萌香」によるIRC及びtwitter通知動作画面

 萌香の発言内容はtwitterでリアルタイムにご覧頂く事ができます。地球の裏側からミクシィ開発部のコーヒーメーカー動作状況を確認する場合などにご利用ください。


【Twitter/mixi_mocha】 http://twitter.com/mixi_mocha

 既製品のコーヒーメーカーを改造し、センサーと自作の制御回路を組み合わせることでコーヒーのドリップ状況を検出し、情報を私のPCに伝えます。私のPCでは萌香の制御ソフトがバックグラウンドで動作しており、検出状況に応じてIRCやtwitterにメッセージを投稿する仕組みです。

図4 「萌香」のシステム構成図

図4 「萌香」のシステム構成図

 コーヒーメーカーは安全性の観点から分解が難しい作りになっている場合が多く、改造は思ったよりも大変な作業でした。ドリップ状況の検知は、衛生上の観点からもタンクの残り水量を外付けセンサーで取得することはナンセンスです。また、抽出時は内部の部品が非常に高温になるため、物理的なセンサーの接触は極力避けなければいけないという制約事項もありました。 そこで、コーヒーメーカーのドリップ終了時の「自動電源OFF機能」に着目しました。パイロットランプの点状状況を光センサ(CdS光導電セル)で検出し、マイコン(今回はAVR-ATmega88を使用)を用いてA/D変換を行っあと、その値をUSBを経由してPCに送り、検出状況を制御用プログラムで判別してIRCやtwitterへの各種投稿処理を行います。パイロットランプが消灯→点灯で「ドリップ開始」、点灯→消灯で「ドリップ終了」と言った具合です。抽出量などは点灯時間を計測することで把握できます。

図5 「萌香」の制御回路はブレットボード上に実装

図5 「萌香」の制御回路はブレットボード上に実装

 萌香の導入後、チーム内のコーヒーコミュニケーションが劇的に効率化しました。離れた席でも炒れたてのコーヒーを常に知る事ができ、私もコーヒーを入れるたびにお知らせをする面倒から開放されました。光センサーゆえ、お昼時の直射日光が差し込むと、たまに暴走して発言が止まらなくなることもありますが、そこは「ドジっ子」という設定でフォローできちゃうのも擬人化ゆえの素敵なところです。萌香の設定に関して下記にまとめまておきますので、興味のある方はご覧ください。

図6 萌香たん

図6 萌香たん

名前 美釧 萌香
読み みくし もか / Mikushi Mocha
由来 「ミクシィ」とコーヒーの銘柄「モカ」から
性別 女性
血液型 AB型(Rh +)
年齢 16歳(高校生)
身長 152cm
好きなもの もちろん、美味しいコーヒー
将来の夢 Webエンジニアとしてインターネットの未来を作ること

 萌香のイラストは友人のつてを借りてイラストレータの「さもに凰花」さんにお願いさせて頂きました。あまりのかわいさに、ついカッとなって壁紙も作ってしまいましたので、ご興味のある物好きな方はご利用ください(笑)

萌香たん壁紙サムネイル

萌香たん壁紙1280×1024 (439KB)
萌香たん壁紙1024×768(289KB)

従来までのコーヒーポット状況通知システム

 実は、今日までのコンピュータ発展の歴史の中において「コーヒーメーカーの状況通知」というテーマはいつの時代にも考えられてきた事であり、様々な手法が提案されてきました。しかし、現代的な実装という点で考えた場合、どの手法もニーズを満たすものではありませんでした。

ケンブリッジコーヒーポット方式の問題点

 「コーヒーポットをネットワーク越しに監視する」というアイデアの最初期、かつ最も有名な実装として「ケンブリッジコーヒーポット」というものがあります。これは、1993年にイギリスのケンブリッジ大学のコンピュータ研究室に所属する学生たちによって作られたものです。「コーヒーを飲むためにポットのところに行ったら、まだコーヒーがたまっていなかった!」という非常に残念な問題を解決するために、コーヒーポットの前にビデオカメラを設置し、その画像をリアルタイムにウェブにアップロードすることで、自席に居ながらにしてコーヒーポットの残量を確認することができるという画期的なシステムです。今では「それ、ustreamでできるよ。」と軽く流されてしまいそうなものですが、当時はWindows95すらも発売されていない時代。日本の家庭にはまだインターネットがなく、当時の最新機PC-9821とパソコン通信が花形の時代です。地球の裏側のコーヒーポットの状況をリアルタイムに把握できるというのはまさに革命的な出来事で、新しい時代の到来を予感させるものでした。ケンブリッジコーヒーポットは設置からその後8年間稼動を続け、2001年の大学移転に伴ってスイッチが切られました。このコーヒーポットはeBayのオークションにおいて83万円の値が付き落札されたそうです。
 さて、このケンブリッジコーヒーポット、Webカメラを使った一見シンプルかつスマートで、エレガントな解決策のように思われますが、1つの大きな構造上の問題を抱えています。それは「コーヒー残量が目視できるガラス製のコーヒーポットでしか使用することが出来ない」ということです。ガラス製コーヒーポットは安価に入手可能ですが、その構造上、保温性が低く、残量がある限り常に底部のヒーターでコーヒーを加熱し続けなければいけません。コーヒーの保温には多くの電力を消費するため、エコが叫ばれる昨今の社会的情勢にマッチした解決策とは言いがたいものがあります。また、常に加熱を続けることでコーヒーが煮立ち、本来の味を損なってしまうのもコーヒー好きには許しがたいことで、現在ではポットに魔法瓶を採用した保温性の高いコーヒーメーカーが市販されています。オフィスにおいて非同期的に消費されるコーヒーには、魔法瓶式コーヒーメーカーの方が適しているため、ケンブリッジコーヒーポット方式は現実的なソリューションではありません。

図7 ケンブリッジコーヒーポットの確認ページ

図7 ケンブリッジコーヒーポットの確認ページ

HTCPCPの問題点

 HTCPCP(ハイパーテキストコーヒーポット制御プロトコル)はRFC2324で定められた世界標準のコーヒーポット制御用プロトコルです。 いわゆるジョークRFCと呼ばれるものの一種ですが、仕様自体は実装可能なものであり、HTCPCPに準拠したコーヒーメーカーの制作例なども存在します。しかし、実際の現場においてHTCPCPはあくまで「ジョーク」の領域を出るものではなく、次に上げるような問題点があり使い物になりません。

  • HTTPの拡張であるため、プロトコルシーケンスはクライアント駆動であり、コーヒーメーカー側から非同期にメッセージを送ることができない
  • たとえば、コーヒーメーカーから「コーヒーが入ったよ!」という通知を送るはできない
  • コーヒー炊き立て通知を実装するにはCOMETなどによるロングポーリングの手法を用いる必要があるが、コーヒーポットへの組み込みサーバは一般的に貧弱なため現実的な解決策ではない
  • IE、Firefox、Safari等、主要なブラウザはcoffee://URIスキームに対応していない。デファクトなクライアント実装も無い。開発しても利用者にインストールしてもらうのは面倒
  • というか、コーヒー粉をセットするときに席を立つんだから、その時にスイッチを操作したほうが便利だろ常識的に考えて
  • そもそも、RFCの内容がふざけており、適当すぎる。(ジョークRFCなので仕方ない)
  • ミルクやシロップを設定する拡張フィールドがあるが、俺はブラック以外をコーヒーとは認めない

 上記のような問題を解決した「萌香」は、Web時代におけるモダンなコーヒーメーカー状況通知システムの実装なのではないかなと勝手に考えております。

まとめ

 Web時代における現代的なコーヒーメーカー状況通知システムとして、オンラインコーヒーメーカー「萌香」を開発し、その開発背景と歴史、実現機能、実装方法などを紹介させて頂きました。また、「ミクシィのODF(OneDayFree)制度ってどのくらいFreeなんですか?」とよく聞かれることがあるのですが、「会社で朝からハンダこてを握って電子回路を組んでコーヒーメーカーを改造できるくらい自由です!」という事が少しでもお伝えできたなら幸いに思います。 Web企業でのハードウェア制作というのは、一見して異質な畑違いの行為のような感じもしますが、コミュニケーションを創造する仕事に従事するからには、ヒューマンインタフェースに関する研究やデバイスを通してのコミュニケーションのあり方についても積極的に考えて行きたいと個人的には思っています。コミュニケーションはブラウザの上で完結するものではないのですから。

図8 金曜日の私のデスクはWeb企業っぽくない

図8 金曜日の私のデスクはWeb企業に見えない

 この他にも、色々とハードウェアを絡めたデモプロダクトなんかを作っていたりします。機会があれば、今後もご紹介して行けたらいいなと思っていますので、生暖かい目で見守って頂ければ幸いです。  ハードウェアに興味を持たれる方がどれほどいるか不安で仕方ないのですが、もしこんな記事でもブクマなどが付くようでしたら、次回は「萌香」の実装に関して回路の制作方法、マイコン制御用プログラムの開発方法やソースコード、壁紙第2弾なんかを晒してみようかなと思っています。 それでは、最後までお読み頂き誠にありがとうございました!

どうも、こんにちは。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を開きます。そして、

  1. プロファイルボタンを押す
  2. プロファイル対象の操作を行う
  3. プロファイルボタンで終了

profile

今回は例として、以前、小山先生のエントリにて公開されました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;}
}

プロファイリングを行う場合にはこのような条件にも注目して実装することで、問題の発見を容易にすることができます。

これらを踏まえて、実装に手を加えプロファイルを取り直すと以下のようになります。
profile2
このようにすればボトルネックとなる関数がどれなのか、判別することができます。

時間感覚の話

Templateモジュールのようなコードを書く際に注意しなければならないのが、サーバサイドのページとの時間感覚の違いです。以下に感覚的なレベルでのサーバサイド、クライアントサイドのボトルネックに関して表にまとめたものを示します。
table1
サーバサイドの平均的なプログラムにおいて、もっともボトルネックとなるのはデータベースサーバ等への処理です。クライアントサイドも同じように外部環境ネットワークとの通信処理がもっともネックになります。注目するべきは、クライアントサイドプログラミングにおいて、DOMの描画リフレッシュ操作もボトルネックになるということです。

一方、サーバサイドにおいては処理の内容に応じてスケールアウトするための技術が盛んに議論されていますが、クライアントサイドにおいては1ユーザのために、ユーザのCPU時間を消費しますのでスケールアウトという観念はありません。しかし、GUIプログラムにおいては採用するアーキテクチャから、異なったアプローチでのレスポンス改善策があります。以下にプログラミングパラダイムから考えたクライアントサイドプログラミングとサーバサイドプログラミングの違いを示します。

table2
ほとんどの場合、サーバサイドプログラミングでは1リクエストあたりの処理は1OSレベルスレッドが割り当てられ、体感時間と実際にプログラミングが処理を行っている実働時間はイコールとなります。最近では、こういった状況に対応するためにJob Queue Serverなどを用いることで、プログラムの実行タイミングを遅延&平滑化することが注目されていますが、基本的にはこういったモデルが採用されています。一方で、クライアントサイドのGUIプログラミングにおいては、ユーザアクションに対して、レスポンスを返すまでの時間が体感時間となりますので、ユーザアクション以前に実行しておける処理が多くあります。このような状況では、如何にユーザアクションが発生する前にできうるかぎりの処理をしておくか、といったことが重要になります。

このようなベースを踏まえたうえで、HTML.Template(JavaScript)の各フェーズでの処理時間に注目してプロファイルをとってみると以下のようになります。
time1
evalと文字列処理という重い処理を実行するTemplate ParseとCompileに6割近い時間が消費されていることがわかるかと思います。そしてこれらの処理は多くの場合、ユーザアクションの前に実行可能です。

そこで、以下の図のような戦略をとります。
time2
もっとも時間のあるブラウザのアイドル時間に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エレメントを構築し、それを描画するという二つの処理に分解されます。
図に示すと以下のようになります。
time3
ということは、この内部的に行われている処理のDOMエレメント構築をプリアクション時に実行できれば、より高速なUXを実現することができます。
time4

$('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フロントエンドについての話題を本ブログで紹介できればと思います。

はじめましてhirokiです。こんにちは。新卒で弊社に入って一年が経過しようとしているので、そろそろエンジニアブロガーの仲間入りをしてみようかと思っています。

今回はJavaScriptのお話です。ハードボイルドなバックエンド側技術のご紹介が多い当ブログですが、スイーツ(笑)なフロントエンド技術もおもしろいんだよ!ということをアピってやろうという魂胆です><。

HTML.Template(JavaScript)

弊社では、サーバサイドによるHTMLの出力テンプレートエンジンにCPANモジュールであるHTML::Template::Proを使用しています。今回はそのJavaScript実装をオープンソースとして開発しましたので、紹介をさせていただきます。

HTML::Templateは貧弱で、冗長で、洗練されていないシンタックスでお馴染みのテンプレートエンジンですが、高速で必要以上のロジックを積み込まないようにできるという点で優れたView Templateモジュールです。

長年、このモジュールを使用してきたという経緯もあり、社内ではこのテンプレート言語に対するノウハウも蓄積されています。そんな中、昨今ではJavaScriptを利用したDHTML+XHRなアプリケーション開発要件も増加傾向にあり、案件ごとに必要なJavaScriptのテンプレートモジュールを探してみるという状況でした。

そこで、ノウハウの継承をしつつ高速でサーバサイドとの統一利用可能なJavaScript Template EngineとしてHTML.Template(JavaScript)を作成しました。

能書きはいいからドキュメントを見せろ!という方はこちらからどうぞ。Prototype1.6xxを必須としているのでご注意ください。

あとは能書きをだらだらと書かせていただきます。

テンプレートエンジンとは

JavaScriptでは、画面の書き換えに主に2つの方法を使用します。1つはinnerHTMLによる方法、もう一つがDOM操作による方法です。

それぞれコードで見てみましょう。

// innerHTMLによる置き換え
document
   .getElementById('bodyMainArea')
       .innerHTML = "Hello!World";
// DOM操作による追記
document
   .getElementById('bodyMainArea')
        .appendChild(
            document.createTextNode('helloWorld!')
	);

それぞれに一長一短があるのですが、大規模な書き換えの場合にはStringとしてHTMLを構築してinnerHTMLを行うという方法がもっとも単純です。

DOM構築の複雑さを補うscript.aculo.usのbuilder.jsのようなライブラリも存在しますが、若干のロジックが入ってくるとどちらの方法も記述が難しくなってきます。

// innerHTMLによる書き換え(変数)
document.getElementById('bodyMainArea').innerHTML=[
    '<div class="testBox">',
    '    <ol>',
    '        <li>'+hoge+'</li>',
    '    </ol>',
    '</div>'
].join('');

// 分岐
document.getElementById('bodyMainArea').innerHTML=[
    '<div class="testBox">',
    '    <ol>',
    (someCondition == true)?'<li>hello!</li>':'',
    '    </ol>',
    '</div>'
].join('');

//繰り返し
document.getElementById('bodyMainArea').innerHTML=[
    '<div class="testBox">',
    '    <ol>',
    (function(){
        var ret = [];
      for(
              var i    = 0,
                elem   = arguments[i],
                length = arguments.length;

             i < length;

             i++,elem = array[i]
        ){
             if(elem.num % 2 == 0 ) ret.push('<li>'+elem.text+'</li>');
         }

        return ret.join('');
    })(
        {num:1,text:'hoge'},
        {num:2,text:'fuga'},
        {num:3,text:'piyo'},
        {num:4,text:'moga'}
     ),
    '    </ol>',
    '</div>'
].join('');

このような文字列処理や関数処理を毎度行うのは面倒ですし、コーダーによるメンテナンスも難しくなります。場合によっては誰も手をつけることのできない残念なコードになりがちです。

もしもHTML.Template(JavaScript)があれば

先ほどの煩雑になったロジックの入った文字列操作をテンプレートを用いて記述すると次のように書くことができます。

var tmpl = new HTML.Template([
    '<div class="testBox">',
    '    <ol>',
    '    <TMPL_LOOP NAME=loop>',
    '    <TMPL_UNLESS NAME=__odd__>',
    '        <li><TMPL_VAR NAME=text></li>',
    '    </TMPL_UNLESS>',
    '    </TMPL_LOOP>',
    '    </ol>',
    '</div>'
].join(''));

tmpl.param({
    loop:[{text:'hoge'},{text:'fuga'},{text:'piyo'},{text:'moga'}]
});

$('bodyMainArea').update(tmpl);

また、テンプレートをコード中からHTMLコメントやXHRからの取得によって隠蔽することもできます。


// test.tmplとして保存
<div class="testBox">
    <ol>
    <TMPL_LOOP NAME=loop>
    <TMPL_UNLESS NAME=__odd__>
        <li><TMPL_VAR NAME=text></li>
    </TMPL_UNLESS>
    </TMPL_LOOP>
    </ol>
</div>
// テンプレートをXHRで取得
var tmpl = new HTML.Template('url:/test.tmpl');
  :
  :
tmpl.param({
    loop:[{text:'hoge'},{text:'fuga'},{text:'piyo'},{text:'moga'}]
});

$('bodyMainArea').update(tmpl);

あるいは、HTML中にコメントとして記述してネットワークリソースの節約や非同期性に伴う煩雑な処理を軽減して利用することもできます。

// html中に記述
<div class="HTML_TEMPLATE" id='test'><!--

<div class="testBox">
    <ol>
    <TMPL_LOOP NAME=loop>
    <TMPL_UNLESS NAME=__odd__>
        <li><TMPL_VAR NAME=text></li>
    </TMPL_UNLESS>
    </TMPL_LOOP>
    </ol>
</div>

--></div>
// テンプレートをHTMLコメントから取得
var tmpl = new HTML.Template('dom:test');

tmpl.param({
    loop:[{text:'hoge'},{text:'fuga'},{text:'piyo'},{text:'moga'}]
});

$('bodyMainArea').update(tmpl);

これは何をしてるの?

LLによるテンプレートエンジンといえば関数の生成と相場が決まっているのですが、HTML.Template(JavaScript)も多分にもれず関数の生成です。先ほどのテンプレートも内部では以下のような関数に変換されています。

function anonymous() {
    var $_R = [];
    var $_C = [this._param];
    var $_GF = HTML.Template.GLOBAL_FUNC;
    var $_T = this._param;
    var $_S = HTML.Template.Cache.STRING_FRAGMENT;
    var $_SELF = this;
    $_R.push($_S[43]);
    var $_L_40 = $A(($_T.loop ? $_T.loop: undefined)) || [];
    var $_LL_40 = $_L_40.length;
    $_C.push($_T);
    for (var i_40 = 0; i_40 < $_LL_40; i_40++) {
        $_T = (typeof $_L_40[i_40] == "object") ? $_L_40[i_40] : {};
        $_T.__first__ = (i_40 == 0) ? true: false;
        $_T.__counter__ = i_40 + 1;
        $_T.__odd__ = ((i_40 + 1) % 2) ? true: false;
        $_T.__last__ = (i_40 == $_LL_40 - 1) ? true: false;
        $_T.__inner__ = ($_T.__first__ || $_T.__last__) ? false: true;
        $_R.push($_S[44]);
        if (! ($_T.__odd__ ? $_T.__odd__: undefined)) {
            $_R.push($_S[45]);
            $_R.push(($_T.__counter__ ? $_T.__counter__: undefined));
            $_R.push($_S[46]);
        }
        $_R.push($_S[47]);
    }
    $_T = $_C.pop();
    $_R.push($_S[48]);
    return $_R.join("");
}

比較的可読性の高いコードジェネレートになっているかと思いますが、それでもやはり手で書く気のうせるイイ感じのコードに仕上がっています。

これらのコードはCopy and Pasteで再利用可能な形として出力可能ですので、関数生成のコストが気になる方はプロダクションコードとしてはダンプ結果を利用することもできます。ただ、オススメはしません。”evalは遅い、evilだ”と教え込まれた方にとっては受け入れがたいかもしれませんが、ほとんどのWebアプリケーションにとってブラウザのアイドル時間は膨大に存在しており、適切なタイミングでコンパイルされれば、UXに影響を与えることはないからです。

C/S同一のシンプルなテンプレートエンジンを使うということ

なぜ、フロントエンドのテンプレートとサーバサイドのテンプレートを同一にする必要があるんだ?という疑問もあるかと思います。たとえば、EJS(embeded javascript)のようなJavaScriptプログラマにとって分かりやすく、何でもできるテンプレートのほうが便利じゃないか、と。

確かに一面的にはそのとおりで、小規模開発や単発案件であればこういったモジュールのほうが便利だという局面もあるでしょう。たとえばPHP/erubyなどはそういった局面で非常に優れた言語です。

これらの回答としては、「HTMLコーディングのメンテナンス性」と「ViewとLogicの分離」という2点に尽きます。

次のようなケースを考えて見ましょう。
同一のテンプレート構成をPHPとEJSで記述しているケースです。

<!-- PHP -->
<div class="testBox">
    <ol>
    <?php
    $a = array("hoge","fuga","more","piyo");
    for($i=0; $i<count($a); $i++){
        if(($i%2) == 0){?>
	    <li><?= $a[$i] ?></li>
        <?}
    }
    ?>
    </ol>
</div>
<!-- EJS -->
<div class="testBox">
    <ol>
    <% var a = ["hoge","fuga","more","piyo"];
     for(var i=0; i<a.length; i++){
     if((i%2) == 0){ %>
         <li>
         <%= a[i] %>
         </li>
    <% }
    }%>
    </ol>
</div>

これらは、HTML中のUIパーツとしてユーザからは同じものとして見えています。どちらも同一のロジックで記述されています。このような状況下でデザイン変更が入ったとします。HTMLコーダは、PHP版のテンプレートとJS版のテンプレートを書き換えなければなりません。また、どちらもロジック部分の記述が生の言語環境を利用しているため、双方の言語についての基本的な知識が無ければ、うかつに変更することができません。

HTML.Template(JavaScript)とHTML::Templateを利用している環境であれば、単一のファイルの修正のみで対応できます。

同じく、JavaScriptの関数生成をして、PerlとJavaScript/ClientとServerで同一のテンプレートを利用可能にしているモジュールにJemplateがあります。TT(Tokyo TyrantじゃなくてTemplate Toolkit)のシンタクスで記述できます。Perl実装で、コマンドラインによるjsファイル生成を基本機能として提供していますが、そのままではメンテナンスには向いていないので、フレームワーク取り込みなど何らかの形で透過的に取り扱えるようにすれば非常に快適なテンプレートライフをエンジョイできます。

HTML.Template(JavaScript)は、Jemplateから一部設計思想をインスパイヤしつつ、Pure JavaScriptによる実装で、EJSのような取り扱いの容易さも実現しています。

まとめ

HTML::Templateのシンタックスを備えたテンプレートエンジンをJavaScriptで実装しました。個人ないし少人数でプロダクトを作っているとなかなか気づかないことですが、大規模なシステムを開発していくということはさまざまな分業で成り立っていて、学習コストが低く、適切なTemplateモジュールを採用することで、ますます複雑化するWebアプリケーション開発をスマートに記述できるようになったりします。

次回はもう一歩踏み込んで、JavaScriptテンプレートエンジンの開発と設計についてのちょっとしたノウハウをお話できたらと思います。

まだまだ、つたない実装ですがツッコミいれながら読んでいただければ幸いです。そんなわけで、この次も、ワッフルワッフル!

ついに発売されたスト4のコンシューマ機版をやりたくてしょうがないけど筐体を買ってもらえないので、駅前のゲーム屋のディスプレー前で垂涎するばかりのmikioです。今回は連載の最終回で、各種スクリプト言語を使ってお手軽にテーブルデータベースを操作する方法について説明します。

TokyoCabinet::TDB

まずは、TCのPerlバインディングRubyバインディングの最新版を入手してください。それぞれテーブルデータベースを扱うための TokyoCabinet::TDB というクラスが加わっています。以下のようなIDLによるガイドラインに準拠したインターフェイスが提供されますので、使い方は言語にかかわらず同じようになるはずです。

module TokyoCabinet {
  interface TDB {
    boolean open(in string path, in long omode);
    boolean close();
    boolean put(in string pkey, in Map cols);
    boolean out(in string pkey);
    Map get(in string pkey);
    boolean setindex(in string name, in long type);
    long long genuid();
  };
  interface TDBQRY {
    void addcond(in string name, in long op, in string expr);
    void setorder(in string name, in long type);
    void setmax(in long max);
    List search();
    boolean searchout();
    string hint();
  };
};

例によって、社員名簿を作ったり検索したりしてみます。Perlバインディングはこんな風になります。

use TokyoCabinet;

my $tdb = TokyoCabinet::TDB->new();
$tdb->open("casket.tct", $tdb->OWRITER | $tdb->OCREAT);
$tdb->setindex("name", $tdb->ITLEXICAL);

$tdb->put("1",   { "name" => "空条承太郎", "sex" => "male",
                   "hdate" => "20050321", "div" => "brd,dev" });
$tdb->put("81",  { "name" => "東方仗助", "sex" => "male",
                   "hdate" => "20060601", "div" => "dev" });
$tdb->put("92",  { "name" => "汐華初流乃", "sex" => "male",
                   "hdate" => "20070311", "div" => "hr" });
$tdb->put("127", { "name" => "空条徐倫", "sex" => "female",
                   "hdate" => "20070523", "div" => "brd,hr" });

my $qry = TokyoCabinet::TDBQRY->new($tdb);
$qry->addcond("name", $qry->QCSTRBW, "空条");
$qry->setorder("hdate", $qry->QONUMDESC);
my $res = $qry->search();
foreach my $pkey (@$res){
    my $cols = $tdb->get($pkey);
    printf("%s: %s\n", $pkey, $cols->{"name"});
}

$tdb->close();

一方、Rubyバインディングだとこんな風になります。Perlとほとんど変わらないですよね。

require 'tokyocabinet'
include TokyoCabinet

tdb = TDB::new()
tdb.open("casket.tct", TDB::OWRITER | TDB::OCREAT)
tdb.setindex("name", TDB::ITLEXICAL)

tdb.put("1",   { "name" => "空条承太郎", "sex" => "male",
                 "hdate" => "20050321", "div" => "brd,dev" })
tdb.put("81",  { "name" => "東方仗助", "sex" => "male",
                 "hdate" => "20060601", "div" => "dev" })
tdb.put("92",  { "name" => "汐華初流乃", "sex" => "male",
                 "hdate" => "20070311", "div" => "hr" })
tdb.put("127", { "name" => "空条徐倫", "sex" => "female",
                 "hdate" => "20070523", "div" => "brd,hr" })

qry = TDBQRY::new(tdb)
qry.addcond("name", TDBQRY::QCSTRBW, "空条")
qry.setorder("hdate", TDBQRY::QONUMDESC)
res = qry.search()
res.each() do |pkey|
    cols = tdb.get(pkey)
    printf("%s: %s\n", pkey, cols["name"])
end

tdb.close()

検索結果に対するアトミックな更新も、たった1ステップで書けちゃうんです。例えば、発現と同時に射程距離内の全ての社員の性転換をするには以下のようにします。まずはPerl版。procメソッドの引数に無名関数のリファレンスを渡すだけです。

$qry->proc(sub {
    my $pkey = shift;
    my $cols = shift;
    $cols->{"sex"} = $cols->{"sex"} eq "male" ? "female" : "male";
    $qry->QPPUT;
});

そしてRuby版。procメソッドのブロックに処理を記述します。もはや4行。チャリオッツレクイエムも驚く能力ですね。PerlでもRubyでも、ブロックの最後に必ずQPPUT(更新)、QPOUT(削除)、0(何もしない)のいずれかを評価することが重要です。

qry.proc() do |pkey, cols|
    cols["sex"] = cols["sex"] == "male" ? "female" : "male"
    TDBQRY::QPPUT
end

今までTCのテーブルDBは表結合ができないと説明してきましたが、実はこのアトミックな更新機能(というか該当レコードの更新機能付きイテレータ)の中で別のテーブルの値を引っ張ってくることで、内部結合も外部結合も自由に表現することができます。まあ、任意のコードが呼び出せるから何でもできると言っているだけなのですが…。

ところで、「エラーを例外として投げるようにAPIを改良してみました」というパッチをいただくことがよくあります。しかし、お気持ちは大変ありがたいのですが、本家のパッケージとしてはそれは採用できません。(最大公約数的な)IDLに準拠して、(私にとっての)保守性を維持し、(他の言語を使っている人にとっての)学習コストを下げることを重視したいからです。例外を使ったエラー処理を好むプログラマも多いことは知っていますが、本家としてはインターフェイスの細かい論争に時間を使いたくないので、「ラッパーを書いて適当に調整してくれ」というスタンスを貫きたいと思います。

TokyoTyrant::RDBTBL

TT経由でもTCを直接いじった場合と同じことがほとんど同じインターフェイスでできます。TokyoTyrant::RDB はTCの抽象データベースAPIに相当する機能(つまりハッシュDBとB+木DBの共通機能)を提供するものでしたが、そのサブクラスとしてテーブルDB独自の機能を持たせたものが TokyoTyrant::RDBTBL です。まずは社員名簿ピュアPerl版インターフェイスの例を見てください。

use TokyoTyrant;

my $rdb = TokyoTyrant::RDBTBL->new();
$rdb->open("localhost", 1978);
$rdb->setindex("name", $rdb->ITLEXICAL);

$rdb->put("1",   { "name" => "空条承太郎", "sex" => "male",
                   "hdate" => "20050321", "div" => "brd,dev" });
$rdb->put("81",  { "name" => "東方仗助", "sex" => "male",
                   "hdate" => "20060601", "div" => "dev" });
$rdb->put("92",  { "name" => "汐華初流乃", "sex" => "male",
                   "hdate" => "20070311", "div" => "hr" });
$rdb->put("127", { "name" => "空条徐倫", "sex" => "female",
                   "hdate" => "20070523", "div" => "brd,hr" });

my $qry = TokyoTyrant::RDBQRY->new($rdb);
$qry->addcond("name", $qry->QCSTRBW, "空条");
$qry->setorder("hdate", $qry->QONUMDESC);
my $res = $qry->search();
foreach my $pkey (@$res){
    my $cols = $rdb->get($pkey);
    printf("%s: %s\n", $pkey, $cols->{"name"});
}

$rdb->close();

openメソッドの引数以外は、TCとTTで全く同じ構造になっていることがお分かりいただけるかと思います。現に私もコピペと検索置換で上記のソースを書きました。もちろんピュアRuby版インターフェイスでも同様です。

require 'tokyotyrant'
include TokyoTyrant

rdb = RDBTBL::new()
rdb.open("localhost", 1978)
rdb.setindex("name", RDBTBL::ITLEXICAL)

rdb.put("1",   { "name" => "空条承太郎", "sex" => "male",
                 "hdate" => "20050321", "div" => "brd,dev" })
rdb.put("81",  { "name" => "東方仗助", "sex" => "male",
                 "hdate" => "20060601", "div" => "dev" })
rdb.put("92",  { "name" => "汐華初流乃", "sex" => "male",
                 "hdate" => "20070311", "div" => "hr" })
rdb.put("127", { "name" => "空条徐倫", "sex" => "female",
                 "hdate" => "20070523", "div" => "brd,hr" })

qry = RDBQRY::new(rdb)
qry.addcond("name", RDBQRY::QCSTRBW, "空条")
qry.setorder("hdate", RDBQRY::QONUMDESC)
res = qry.search()
res.each() do |pkey|
    cols = rdb.get(pkey)
    printf("%s: %s\n", pkey, cols["name"])
end

rdb.close()

ダラダラとソースを載せたのは、データベースを使ったプログラムはそんなに難しいものじゃないということを主張したいからです。おそらく、単純なファイルを読み書きするよりも簡単だと思っていただけたと思います。SQLなんてない方が簡潔だし直感的ですよね。そして、データベースがローカルにあろうがリモートにあろうがほとんど同じやり方でプログラミングできることもおわかりいただけたと思います。これで、私が本来目標としていた、「個人ブログを簡単に作るためのストレージ」が実現できました。とりあえずはこれで満足です。

永続的だが時限的なキャッシュ

マーケティング戦略上は、TTはmemcachedとMySQLの隙間を埋める技術として位置付けるのが妥当でしょう。リレーショナルデータベースとして必要十分な機能と性能を備えるMySQLと、大規模Webサイトで求められるスループットを実現するための単純かつ高速なキャッシュサーバであるmemcachedの間のギャップはものすごく大きいわけですが、とりあえずはmemcached寄りの位置づけで考えてみます。すなわち、以下のような存在です。

  1. memcachedのように高速かつ並列に操作できる。
  2. メモリでなくファイルにデータを格納することでデータを永続化でき、実メモリ容量以上のデータも扱える。
  3. 単純なkey/valueでなく、各レコードに複数のプロパティを持たせられ、それを対象として検索できる。
  4. 不要な(古い)データは勝手に消え、データベースサイズを一定に保つことができる。

私は別にTCやTTで食っているわけじゃないのですが、世のニーズに応えてみるとなにげにいいことがあるかもしれません。で、現状のTTを考えるに、1と2は既に実現できています。最近はSSDが一般化してきたので、データベースファイルをSSD上に置けば、スループットをmemcached並に保ったままで、1台あたり数10〜数100ギガバイトのキャッシュを安価に提供することができるようになりました。3に関しては、今回のテーブルデータベースがサポートされたことで実現できました。一般的なWebサービスのデータ管理に必要な機能はほとんど提供できていると思います。そして最後に残ったのは、4の、古いデータを自動的に消す機能です。これはmemcachedの最大の特徴のひとつですが、キャッシュサーバではないTTにとっては結構厄介な要求になります。しかし、今回はちょっとしたトリックを使ってその要求を充足してみます。

memcachedでは各レコードに “exptime” という属性値を付与することで、そこで指定された時間が過ぎたらそのレコードが暗黙的に消されるようにできます。TTでもテーブルデータベースを使えば、レコードに消去されるべき日時の属性値を付与することができそうです。例えば、各レコードに “x” というコラムとして消去予定日時を持たせて登録する処理と、不要なデータを消す処理は、以下のRubyコードをクライアント側から実行することで実現できます。

def putx(rdb, pkey, cols, lifetime)
  cols["x"] = Time.now.to_i + lifetime
  rdb.put(pkey, cols)
end

def expire(rdb)
  qry = RDBQRY::new(rdb)
  qry.addcond("x", RDBQRY::QCNUMLT, Time.now.to_i.to_s)
  qry.searchout
end

コラム “x” に数値型のインデックスを張っておけば、QCNUMLT演算子の処理が高速化されるので、レコードの削除は一瞬で済むようになります。性能的には問題ないし、レコードを格納する際に上記のputxメソッドを使うようにするのも直感的でしょう。ダサいのは、定期的にexpireメソッドをクライアントが発行しなければならない点ですね。CGIやmod_xxxなどのWebサーバ上で実行されるアプリケーションがクライアントである場合には、それらに定期的な実行を任せるのは無理があります。TTと同じマシンにcronスクリプトを仕掛けてexpireを定期実行させるのも手ですが、それくらいの機能はTT自体に持たせてしまってもよいでしょう。ということで、TTのLua拡張を使って、任意の周期で任意の処理を呼び出せる仕組みを容易しました。例えば上記のexpireはLuaだと以下のように記述できます。

function expire()
   local args = {}
   local cdate = string.format("%d", _time())
   table.insert(args, "addcond\0x\0NUMLE\0" .. cdate)
   table.insert(args, "out")
   local res = _misc("search", args)
   if not res then
      _log("expiration was failed")
   end
end

このファイルをttexpire.luaという名前で保存したとしたら、以下のようにTTのサーバを起動させることで、expire関数を1秒毎に定期実行させることができます。-extpcオプションは複数回指定することができ、呼び出した関数ではレコードの削除だけでなく任意の処理を行えるので、バックアップやハートビートなど、様々な用途に応用することができます。

ttserver -ext ttexpire.lua -extpc expire 1.0 "casket.tct"

memcachedでは “flags” という属性をレコードに付与して、クライアント側で任意の意味を持たせて運用することができます。TTでもテーブルのコラムとしてそれを持たせれば同様のことができます。多くの言語のmemcachedクライアントではリストやハッシュなどの複合データ構造を直列化する方法の種類を持たせるためにそれを利用しているようです。ただ、その程度の利用法ならば直列化したデータの先頭1バイトにそのフラグを接頭させればよいので、テーブルデータベースを使うまでもないと個人的には思っています。サーバ側の処理に活用しないデータ片をわざわざ独立させて空間効率を悪化させるのは嫌なので、この件には深入りしないでクライアントライブラリに任せることにします。

超まとめ

単純なハッシュデータベースの値に構造を持たせることから始まり、それにインデックスとクエリオプティマイザをつけてRDBMSのテーブルっぽく利用できるようにしたのがTCのデーブルデータベースです。それを抽象データベースAPIにマッピングした上でネットワーク対応を施し、さらに言語バインディングを実装しまくることで、今こうして、そこそこ使いやすいデータベース機能を提供できているのではないかと思っています。特に「永続的だが時限的なキャッシュ」は、一見矛盾した概念でありながらも、Web業界でのニーズをがっちり捉えているのではないかと勝手に予感してワクワクしています。テーブル関連の機能は自分でもまだそれほど使い込んでいないので、これからアプリケーションを作り込んだりその実運用をする活動を経て、さらに機能と性能の改善を図っていきたいと思っています。

どんなに優秀で経験を積んだ技術者であろうと、未来(その製品がどんな風に使われて、どんな不具合が出て、どんなクレームがつけられるか)を予見する能力には限界があります。したがって、TCやTTのような実用を目的とする製品は、実際のサービスで運用していただいて、発生したクレームをフィードバックする過程を経ねば成立しません(それでも全てのユースケースを網羅することは不可能なので「完成」とは言えない)。そういう意味では、私のような未熟者でも、実サービスに触れて開発者や運用者と密に意思疎通をとりながら仕事が進められるおかげで、そこそこ使える製品を世に出せているような気がします。

つまり何が言いたいのかというと、実用的なものを作りたいのであれば、実用してくれる人々の近くにいると有利だということです。例えば分散ストレージを作りたいのであれば(TC/TTは分散ストレージではありませんが)、弊社やその他のWebサービス企業に籍を置くのも一興でしょう。あるいは界隈で開かれる勉強会に参加して人づてにニーズを聞き出して製品に反映させるのもよいでしょう。作った製品をオープンソースで公開してフィードバックを集めるのもよいでしょう。私も現状に甘んじず、様々な取り組みをしていこうと思います。ということで、世の皆様(そして弊社の人々)、TCやTTの新機能を実際に使ってみてご意見いただけると幸いです。また、各種イベントにて喋る機会があれば参上する所存なので、お声がけくださいませ。

コアライブラリを一生懸命書くとユーティリティやバインディングなどの周辺機能がおろそかになり、逆も然りで、工数割り当てのジレンマが歯がゆいmikioです。今回は余談として、Tokyo Cabinetのテーブルデータベース(TCTDB)を作る途中で思いついた更新機能と性能検証について述べます。

アトミックな更新 再び

TCTDBで好評だったっぽいアトミックな更新機能をその他のデータベースでも実装してみました。例えばハッシュデータベース(TCHDB)では以下の関数が提供されます。

typedef void *(*TCPDPROC)(const void *vbuf, int vsiz, int *sp, void *op);

bool tchdbputproc(TCHDB *hdb, const void *kbuf, int ksiz,
                              const char *vbuf, int vsiz,
                              TCPDPROC proc, void *op);

関数tchdbputprocを呼び出す際には、kbufはキーのポインタ、ksizはキーのサイズ、vbufは初期値のポインタ、ksizは初期値のサイズを指定します。キーに該当するレコードがまだ無ければ、ここで指定したキーと値のレコードを新規に格納します。キーに該当するレコードが既に存在していた場合、procで指定したコールバック関数が呼ばれます。コールバック関数は、引数vbufとvsizで指定された既存の値を参照しつつ何らかの処理を行い、新しい値を記述した領域へのポインタを返します。その領域のサイズはspの参照先に格納します。引数opはtchdbputprocの引数opがそのまま渡ります。コールバック関数がNULLを返した場合、既存のレコードは変更されません。この一連の処理は全て該当のレコードに対してロックをかけてその中でアトミックに行われます。

…とかいう何だか複雑そうな話を聞いて便利そうだと思っていただける人がどれだけいるかわかりませんが、いわゆるCAS(compare and swap)という技法に使え、いわゆるセマフォをデータベース上で実現できるのがツボなのです。

ここで、「トイレの個室が空室か満室か」を管理する例を考えましょう。「満室ならば空室になるのを待ちつつ、空室ならば入って満室にし、用を足して、出て空室にする」とというフローを実装します。そして各々の個室が満室(full)か空室(empty)かの状態をTCHDBのレコードとして管理します。ここで重要なのは、「空室かどうか判断する」のと「入って満室にする」のをアトミックに行うことです。そうでないと、空室だと同時に判断した二人(2スレッド)が一つの個室に入るという悲劇が起こってしまいます。

void *enterroom(const void *vbuf, int vsiz, int *sp, void *op){
  if(vsiz == 4 && memcmp(vbuf, "full", 4) == 0) return NULL;
  *sp = 4;
  return strdup("full");
}

void poo(TCHDB *hdb, const char *room){
  int len = strlen(room);
  while(!tchdbputproc(hdb, room, len, "full", 4, enterroom, NULL)){
    printf("%s: waiting\\n", room);
    usleep(1000 * 1000);
  }
  printf("%s: Poo!\\n", room);
  tchdbput(hdb, room, len, "empty", 5);
}

実は、アクセス許可の二値(真偽)を管理したいのであれば、putprocを使わなくてもputkeep(無ければ作る)とout(消す)を利用するだけで実現可能です。しかし、三値以上の状態を管理したい場合や数値の範囲を条件にしたい場合にはputprocでないと勤まりません(もしくは外部にロックを持つしかない)。また、状態値にタイムスタンプを付与してデッドロックの検出に利用するのも一興でしょう。putprocはTCHDBだけでなくその他のDB(TCBDB/TCFDB/TCMDB/TCNDB/TCADB)にも実装してあるので、似たようなことにお悩みの方はぜひご活用くださいませ(とはいえ、ユーザが少なそうな機能なので「隠しAPI」扱いであり、ヘッダファイルにしか説明はありませんが)。

TT上でのテーブルデータベースの性能

TTのリモートデータベースAPI(TCRDB)に実装してあるテーブル拡張APIを使って、TT上のテーブルデータベースに対する各種操作の性能を計測しました。以前の記事に書いた通り、a,b,c,d,eの5つのコラムに文字列と数値を記録した100万件のレコードを格納し、その主キーによる取得と、文字列の完全一致による検索を実行します。サーバは「ttserver ‘casket.tct#bnum=1000000#idx=str:lex#idx=num:dec#lcnum=4000」で立ち上げました。また、比較のために、ハッシュデータベースに対する100万件の読み書きの性能も測定しました。結果は以下になります。

TCTDB TCHDB
put 102.04 (9800qps) 41.12 (24319qps)
get 50.97 (19619qps) 41.01 (24384qps)
search 53.80 (18587qps) -

この実験では、TTを経由するレコードの書き込みはTCHDBの40%程度のスループットになることがわかりました。一方でレコードの読み込みはTCTDBとTCHDBであまり差が出ないことがわかりました。また、検索クエリの実行とレコードの取得はほぼ同じスループットになることがわかりました。主キーによるアクセスではTCTDBとTCHDBの処理内容はほぼ同じであっても、レコードのサイズがTCTDBの方が大きくなる傾向にあるので、TCTDBの処理速度はTCHDBよりも遅くなるのが一般的です。TCを直接叩く場合にはread/writeやそれに伴うバッファリングのオーバーヘッドがレコード長に比例することによって両者に差が出ると考えられます。TTを経由する場合には、TCの層でのread/writeというよりもむしろ、TTの層でのsend/recvおよびそれに伴うバッファリングのオーバーヘッドがレコード長に比例することで差が出ると考えられます。

それにしても、TCTDBのTT経由の書き込み性能は読み込み性能に比べてよろしくないようです。多分、ttserverの実装がタコで、リクエストのデータが長い場合の処理効率をあまり考えていないことが原因だと思います。この点に関しては今後の課題として改善していくつもりです。

SSDで性能テストしてみた

近頃、HDD(Hard Disk Drive)に代わるストレージデバイスとしてSSD(Solid State Drive)が市場に出回って来ています。SSDはフラッシュメモリで作られているので、物理的に円盤とアームを動かすHDDに比べるとランダムアクセスの読み込みがとても速いらしいです。鬼のようにランダムアクセスを欲するDBMにはSSDはうってつけの技術ですよね。ということで、HDDに比べてどのくらい性能がよくなるのか調べてみました。

実施テスト内容
HASH-WRITE: ハッシュDBのシーケンシャル書き出し(tchtest write casket.tch …)
HASH-READ: ハッシュDBのシーケンシャル読み込み(tchtest read casket.tch)
HASH-RANDOM: ハッシュDBのランダム書き出し(tchtest rcat casket.tch.rnd …)
BTREE-WRITE: B+木DBのシーケンシャル書き出し(tcbtest write casket.tcb …)
BTREE-READ: B+木DBのシーケンシャル読み込み(tcbtest read casket.tcb)
TABLE-WRITE: テーブルDBのシーケンシャル書き出し(tcttest write casket.tch …)
TABLE-READ: テーブルDBのシーケンシャル読み込み(tcttest read casket.tch)
TABLE-RANDOM: テーブルDBのランダム書き出し(tcttest rcat casket.tch.rnd …)
1000万レコードの結果
SSD HDD file size
HASH-WRITE 41.766 61.560 361,947,424
HASH-READ 12.473 12.511 361,947,424
HASH-RANDOM 124.149 1525.510 365,624,992
BTREE-WRITE 9.691 9.654 244,107,264
BTREE-READ 8.457 8.452 244,107,264
TABLE-WRITE 83.492 120.177 717,176,384
TABLE-READ 26.239 25.962 717,176,384
TABLE-RANDOM 326.432 6496.903 795,370,720
1億レコードの結果
SSD HDD file size
HASH-WRITE 532.014 1122.110 3,602,657,584
HASH-READ 147.523 163.096 3,602,657,584
HASH-RANDOM 6864.622 140365.053 3,718,352,064
BTREE-WRITE 104.700 118.166 2,063,838,208
BTREE-READ 91.573 103.135 2,063,838,208
TABLE-WRITE 1188.017 (too slow) 7,570,654,800
TABLE-READ 422.496 (too slow) 7,570,654,800
TABLE-RANDOM 15471.373 (too slow) 8,424,192,816

RAM容量が4GBのマシンで実験を行ったので、データベースファイルのサイズがファイルシステムのキャッシュとして使われる2GBくらいを越えたあたりで、ストレージデバイスへのアクセスが発生することになります。そして、1億レコードのTCHDBのランダムアクセス(上記のHASH-RANDOM)は、SSDだと14567qps、HDDだと712qpsでした。予想通り、巨大なDBMのランダムアクセスの性能において、SSDはHDDのおよそ20倍のスループットが出ることがわかりました。テーブルデータベースに関してもほぼ同じ傾向です。一方で、1億レコードのTCHDBのシーケンシャルアクセス(上記のHASH-WRITE)に関しては、SSDが2394292qps、HDDが1624431qpsということになりました。これも予想通りで、SSDが1.5〜2倍くらい速いという結果になっています。HDDが健闘している原因は、HDDの物理的特性がシーケンシャルアクセスに向いていて、またファイルシステムのキャッシュも有効活用されるためだと思われます。

ファイルシステムのキャッシュに乗らない規模のデータベースに対するランダムアクセスは、確実にストレージデバイスへのランダムアクセスを引き起こします。これは宿命であり、どんなに優れたアルゴリズムと実装をもってしても、1回のレコード参照は、1回以上のデバイスへのアクセスを伴います。この時点で、オンメモリの状態に比べると3桁の性能劣化になります。したがって、巨大なデータベースの性能を決めるのは、ストレージデバイスの性能です。ソフトウェアエンジニアとしては少し残念なことではありますが、DBMなどの単純な仕組みを採用してソフトウェア層を最適化する努力なんてのは、ハードウェア層の性能の前では誤差のようなものなのです。ということで、SSDサイコー! SSDとDBMのコンビでいろいろ遊べそうです。

まとめ

TCの新機能であるアトミックな更新について説明しました。アトミックな更新はセマフォに使えるので、並列処理のリソース管理を効率的に行うために有用です。また、TCTDBをTT経由で操作した場合の性能を測定しました。書き込みで10000qps弱、読み込みで20000qps弱のスループットが出ることがわかりました。SSDとHDDの比較も行いました。SSDを使うと、キャッシュに乗らない規模のハッシュデータベースでも15000qps弱のスループットが出ることがわかりました。

次回は、TTとテーブルデータベースを組み合わせて、保存期限付きのレコードを永続ストレージ上で管理する方法について説明します。