2012年4月29日日曜日

ESウェブブラウザ通信 - HTTPリクエストの並行処理

今週は日本はゴールデン ウィーク真っ最中ですね。このブログを読みに来られてくる方は、ここぞとばかりコーディングに励んでいる方も多そうなので(謎)、いつも通り開発の進捗を報告していきます。

ESウェブブラウザは、ようやくな感じもありますが、r2646でウィンドウのリサイズに対応しました。Windowオブジェクトの大きさをOpenGLのglutReshapeFuncと連動させただけで特に大きな変更ではありませんが、こういった感じの細かな修正もリリースに向けて続けています。

r2646 - エスリルのページもはじめて全体を表示できるように。
今回このブログでは、HTTPリクエストの処理の効率化と、現状のレイアウトエンジンの問題点についてまとめておきます。

HTTPリクエストの並行処理


ESウェブブラウザは、HTTPプロトコル処理の実装にboostのasioライブラリを使っています。asioのライブラリ関数はTCP/IP関連の処理の開始をリクエストすると、そこでブロックすることなく、リクエストが完了したときに指定したコールバック関数を呼び出してくれるような枠組みを提供してくれます。ウェブブラウザのようにマウスやキーボードの入力も処理しないといけないアプリケーションでは、こういった非同期(asynchronous)の処理が使えると便利です。

ただasioの実装はすこし変わっていて(?)、コールバックが呼び出されるのは、アプリケーションから明示的にio_service::runio_service::pollを呼び出した時に限られています。

ESウェブブラウザはOpenGLのアプリケーションでもあるので、基本的には決まった周期でのフレーム単位で処理が進んでいます。この枠組みに、asioのio_service::pollを組み合せて実装したときの処理の流れを下図に示します。左側の#1, #2, ...がフレーム番号を示します。

io_service::pollを使った実装
フレーム#1は、赤いで示された(画像ファイルの読み込みのような)未処理のHTTPリクエストが3つある状態で始まっています。フレーム#1では、HTTPのGETリクエストを1つ発行します。ここでは、リクエストしたコンテント(ファイル)はシアン色で示した時間でサーバーからブラウザに転送が完了していることにしておきます。転送が完了したら、すぐに次のHTTPリクエストを発行できるとよいのですが、asioではコールバックが呼び出されるのはio_cervice::pollを呼び出したときだけです。したがってこの方法では、リクエストが転送済み(水色ので示されたリクエスト)になるのは次のフレーム#2になってしまっています。

以前の実装では、実際にこのような実装になっていました。ですので写真がたくさんリンクされているようなページでは、表示が完了するまでに最低でも写真の数と同じだけのフレーム数が必要になっていました。

r2666からは、 HTTPリクエストを(メインのイベントループを処理しているスレッドとは別の)専用のスレッドで処理するようにしています。専用のスレッドといっても内部で単にio_service::runを呼び出しているだけです。上図と同じ状況からフレーム#1がスタートした場合に、この新しい仕組みでどのように動くかを示したのが下図になります。

別スレッドでio_service::runを使った実装
今度は1つ目のHTTPリクエストの転送が完了したら、直ちにHTTP用のスレッドが次のリクエストを発行するので、フレーム#2が始まった時には3つのリクエストがすべて転送済みになっています。こうなっていれば、写真がたくさんあるようなページでも表示に要する時間を最小限に抑えることができます。実際に、とある新聞社のページでは表示に要する時間を約100秒から約16秒にまで短縮できました。

補足: 従来、非同期のコールバック系のライブラリというと特別な工夫なしに2つ目の新しい構成のような動作を提供していることが多かったと思うのですが、boost::asioの場合は様々なスレッドのライブラリと組み合せた場合のポータビリティーなどを考慮してこのようなデザインになっているのかもしれませんね。また新しい構成では、HTTPリクエスト キューはHTTP用のスレッドもメインのスレッドもどちらも操作することになるので排他制御が必要になっています。asioの場合、最初の実装のようにメインのスレッドだけからio_service::pollを呼び出すようにしていれば、排他制御の必要もないというメリットもあります。(なお、ESウェブブラウザではスレッドのライブラリとして、C++11のthreadを使っています。)

残ったフレーム毎の処理の課題


ネットワーク プロトコルの処理については妥当な時間で処理できるようになったところで、残っているフレーム毎の処理に関する問題についてまとめておきます。CSS 2.1 Test Suite #13でも触れたように、DOMツリーをCSSで表示する場合のステップは大きく下記の3つに分けられます:
  1. カスケーディング (各要素に適用するスタイルを計算する)
  2. レイアウト (計算したスタイルを元にDOMツリーからレンダーツリーを生成する)
  3. レンダリング (レンダーツリーからOpenGLの描画命令を発行する)
それぞれの処理に要する実際の時間はページによって大きく異なりますが、この3つの処理に要する時間の一例を大まかに図にしてみると下図のようになります。左の数字が処理に要するフレーム数を表しています(実際には各処理の比率や時間はページによって変わります)。

フレームレートのばらつき
傾向としては、レンダリングは、ノード数や文字数次第ですが、グラフィックス処理のバッグエンドがOpenGLということもあってGPUの性能にも助けられてそれほど時間はかかりません。 レイアウトに要する時間は現状ではレンダリングに要する時間の数倍程度です。一方、カスケーディングに要する時間は、ルールフィルタリングが使えないIDセレクタ、クラス セレクタ、タイプ セレクタ以外のセレクタを使っているルールの数が多いと、ノード数よりもCSSのルール数の方が影響が大きくなります。

以前のESウェブブラウザでは、DOMツリーの中に変化があるとカスケーディング処理から必ず処理をやり直していました。それをr2665からは、画像の読み込みが終わっただけ、といった場合のように各要素に適用するスタイル自体には変化がない場合には、カスケーディング処理をスキップして、レイアウト処理からはじめることができるようにしています。

また上図からは、例えば今後CSSのアニメーションをサポートするような場合には、カースケーディングをゼロからやり直していたのでは、とてもアニメーションと言えるような表示にならないことも分かります。CSSアニメーションに対応するには、各要素のスタイルの変化のある箇所だけ解決値を変更して、そのまま最低でもレイアウト処理から処理を始めないといけなさそうです。それでも1フレームが1/60秒だとしても、CSSアニメーションは毎秒20フレームでしか表示できないことがわかります。

今後よりブラウザの応答性を高めていくには、レンダリング エンジン自体も相当最適化していないといけなさそうです。たとえば、絶対配置されている要素の位置だけをJavaScriptを使って変更したような場合には、レイアウト処理もゼロからやり直すのではなくてボックスの座標だけ変える、というような最適化も必要かもしれません。そうしておけば、スプライトを使ったゲームなどはHTMLとJavaScriptだけで普通に動くような気もします。

ただ、どちらにしてもHTMLページのカスケーディングとレイアウトにどれくらい時間がかかるかは、そのページ次第です。仮にカスケーディング処理に30秒かかるページがあったとして、その間ウェブ ブラウザがキーボードにもマウスにも何も反応しない、ということではユーザーインターフェイスとしてよくありません。レンダリングだけは確実に毎フレーム実行して、このあたりの処理はネットワーク プロトコルの処理と同様にバックグラウンドで動かせた方がいいかもしれませんね。こういった点も最初のリリースまでに改善できているとよい部分です。

まとめ


今回はここまでです。HTTPリクエストの処理方法は今回でわりと改善できました。一方、レンダリング エンジンの方はまだまだ課題が多いので、地道な改善と同時に多少姑息でも速く動いているように見せるような仕掛けも必要になってきそうです。

次回以降もしばらくは今回のような感じで報告していきます。

2012年4月20日金曜日

ESウェブブラウザ通信 - フォーム コントロールとXBL 2.0

このブログは前回からすこし間隔があいてしまいましたが、4/19日にWeb IDLがW3CのCandidate Recommendation(勧告候補)になったようです。最近はHTML Living Standardのように仕様書の編集者たちが日々更新している版の重要性が高くなっていて、W3Cの勧告プロセスの考え方には誰もが賛同しているという状況ではなくなりつつあるように思います(CSS 2.1も勧告になったのはまだ去年の6月のことですが、話題の中心はかなり前からCSS 3に移っていましたよね)。それでもWeb IDLがCRまで進んだことは大きな前進だと思います。

さて、今回はESウェブブラウザの新しいユーザーインターフェイスや、ボタンやテキストボックスといったフォーム コントロールの実装の進め方の報告です。

新しいユーザー インターフェイス


ESウェブブラウザ本体(ナビゲーターと呼ぶことにしておきます)はHTMLで作成します、ということはESウェブブラウザをオープンソース化したときからアナウンスしているとおりですが、r2605で新しいUIを公開しました(アイコンにはVisualPharmのこちらを使わせていただいています)。

r2605
r2605でのナビゲーターはたった178行のHTMLファイルです。ここからさらに開発を進めて、r2611ではエスリルのページなどを新しいUI経由でURLを入力して表示できるようになりました。

r2611
ちょっとクラッシックな、でもブラウザらしい雰囲気になってきた感じがします。この段階でもナビゲーターは169行のHTMLファイルでした。

大事な点はナビゲーターはただのHTMLファイルなので、より近代的なデザインにしたかったり、あるいは全然違うUIを試したい、という場合でも、CSSとJavaScriptの知識があれば誰でも自由に変更できる、という点です。そういうわけなので、この新しいUIも当面動作チェックとしてこんな感じにしておきます、というのが正確な位置づけです。

フォーム コントロールとXBL


ESウェブブラウザでは、ウェブブラウザとして最低限必要な機能のうち未実装のものがまだたくさんあります。その1つがボタンやテキストボックスといったフォーム コントロールです。ウェブブラウザ本体のUIの開発にHTMLが使えるような時代ですので、フォーム コントロールの実装にもHTML(そしてCSSとJavaScript)を使いたい、と考える人が出てくるのは自然な流れです。HTML 5の仕様書のエディターでもあるHixieも2010年にHTML版のXBL2を提案しています。

ここではこのHTMLベースのXBL2を"Simplified XBL2"と呼ぶことにしておきます。Simplified XBL2にはいろいろな機能がありますが、その一つとして、
XBL could be used to implement the form controls in XForms or HTML.
とフォーム コントロールの(CSSとJavaScriptを使った)実装が念頭に置かれています。

Simplified XBL2の元になったXMLベースのXBL2は2007年に勧告候補になったものの、5年以上経過した今でもどのウェブ ブラウザもいまだに実装していません。そういう経緯があって"Since XBL2 wasn't getting much traction"とはじまるHixieの提案があったようです。(こういうことを書くとWeb IDLが勧告候補になったからといっても喜べない気がしてしまいそうですが、幸いWeb IDLはMicrosoft社もInternet Explorer 9の開発に使っているのでXBL2ほどひどい状況ではなさそうです。)

XBL2というのだからXBL1もあるのでは、 というのはその通りでMozillaで実装されています。より詳しい説明がこちらにもあるように、XBL1はW3Cの標準ではなくて、Mozillaで使われている独自の技術仕様です。そういう意味では、Internet Explorerで使われているHTML Components(HTC)も似たような独自の技術仕様のひとつです。

XBL1とHTCの資産が既にたくさんあるという状況も標準化を加速させない理由のひとつかもしれません。Simplified XBL2もその後2年近くが経過してやはりどのブラウザ ベンダーも動かない、という状況が続いています。さらにその間グーグルのDimitri GlazkovさんはSimplified XBL2とよく似た"Web Components"を新たに提案されています。こちらはWebKit用の実装がはじまっているようですが残念ながら仕様書がまだ揃っていません

W3C WebAppsワーキンググループの議長でもあるArthur Barstowさんからの問い合わせに対する各ベンダーからの反応を見てもこの領域の難しさを感じたりします。

で、ESウェブブラウザではどうしよう?というお話です。選択肢は、
  • XBL 2 (XML based)
  • Simplified XBL 2 (HTML based)
  • Web Components (HTML based?)
の3つになりますが、現状ではHTMLベースで仕様書が完結しているという意味でSimplified XBL 2だけしか現実的な候補になりません。

ただしSimplified XBL 2はそのままメジャーなウェブブラウザに導入される可能性は現状では非常に低そう、という点は考慮しておかないといけません。そこで当面はESウェブブラウザのナビゲーター本体の実装に必要な必要最小限のサブセットに限って実装を進めていきます。そして標準化の動向がより具体的になってきたらそちらに合わせて軌道修正していくという進め方をしていきます。

いずれにしても一番根底にある、よりCSSやJavaScript側に実装を、という考え方はどの仕様も同じだと思います。

ESウェブブラウザでのSimplified XBL2の簡単な解説


現状で最新のr2644では、Simplified XBL2の実装が少し進んで、下図のようにナビゲーターのテキスト ボックスやtextfield.html中のinput要素がXBL2で制御できるようになっています。

r2644
ESウェブブラウザでひとまず対応しているのはSimplified XBL2の次の要素技術です。
  • CSSのbindingプロパティー
  • HTMLのbinding要素
  • HTMLのtemplete要素
  • HTMLのimplementation要素
まだ完全なラジオボタンの実装にはなっていませんが、r2652でのラジオボタンの実現を例にこれらの新しい要素の説明をしていきます。

まず、デフォルトのスタイルシートでは、CSSのbindingプロパティーを使って、

input[type=radio] {
    binding: url(navigator.html#input-radio);
    display: inline-block;
    border-style: none;
    padding: 2px;
}

と、ラジオボタンの表示にはナビゲーター本体のidがinput-radioで指定された要素を使う、ということをbindingプロパティーで指定しています。またHTMLページの作成者のスタイルシートからカスタマイズできるように、bindingを使う要素の外側のスタイルもここで設定しておきます。そしてその要素がSimplified XBL2で記述されたbinding要素になります:

<binding id="input-radio">
<template><span></span></template>
<implementation>
  ({
    xblEnteredDocument: function() {
        this.boundElement.tabIndex = 0;
        var button = document.createTextNode(this.boundElement.checked ? '◉' : '○');
        this.shadowTree.firstChild.appendChild(button);
        this.addEventListener('click', function(event) {
            event.currentTarget.checked = !event.currentTarget.checked;
            button.data = event.currentTarget.checked ? '◉' : '○';
        }, false);
    }
  })
</implementation></binding>

template要素は元のドキュメント中のinput要素をどのように表示するか、その雛形を定義しています。この例では、typeがradioのinput要素は画面中ではその子要素にひとつだけspan要素を持っている状態で作りなさい、ということを指示しています。なお、このような元のドキュメントには現れない、画面表示のためだけに使われるノードのツリーのことを「シャドー ツリー」と呼んでいます。

そしてimplementation要素は、このinput要素に対応するJavaScriptのオブジェクトのプロトタイプ オブジェクトを記述します。input要素が実際にドキュメント内に展開されると、この中のxblEnteredDocumentメソッドが呼び出されます。xblEnteredDocumentメソッド内からは、元のinput要素はboundElementとして、シャドー ツリーはshadowTreeとして、それぞれ参照できます。この例では、もとのinput要素の状態に応じてspan要素の中にチェック済みを示す◉印か、未チェックを示す○をテキスト ノードとして追加しています。またマウスでクリックされたときには、ひとまずinput要素のcheckedの値を反転させるようにイベント ハンドラを登録しています。

4/24追記: このラジオボタンの例は初出時はr2644を使っていましたが、r2652の例に差し替えています。 boundElementのスタイルはimplementationからJavaScriptを使って設定するよりも、スタイルシート中にCSSで記載しておいた方がうまくいきます。

Simplified XBL2の仕様書にはもっと多くの機能が定義されていますが、ナビゲーターの実装にはおよそこれくらいでも足りそうな気がします。

もっともSimplified XBL2で難しいのはセキュリティー関連の事項で、binding要素を提供している側のドキュメント(この例ではnavigator.html)と、利用している側のドキュメント(この例ではtextfield.html)との間で不正な処理を行えてはいけない、という原則があります。実際に、textfield.html中のinput要素に対するxblEnterdDocumentメソッドは、navigator.htmlから作られた別のウィンドウ オブジェクト内で実行されます。navigator.htmlは特別な権限で実行することを想定しているので大丈夫なようにしますが、原則的にはcross-originのバインディングは使えないように既定されています。

まとめ


今回はすこし急ぎ足でESウェブブラウザの新しいUIとXBL2の紹介をしました。これからしばらくは、ESウェブブラウザの最初のリリースに向けて必要最小限の機能の作り込みを優先して進めていきます。そのためトピックがあちこち行ったりきたりするかもしれませんが、次回も今回のような感じで報告していく予定です。

2012年4月4日水曜日

ESウェブブラウザ通信 - ESウェブブラウザでのWeb IDLの利用

今回はそろそろW3Cから勧告候補になりそうなWeb IDLが、ESウェブ ブラウザでどんな風に使われているか紹介していきたいと思います。

メッセージ

ESウェブ ブラウザはC++でもObjectというクラスを基本に構築しているのでオブジェクト指向の設計のように見えるかもしれません。けれども一番基本的な要素は、オブジェクト間でやりとりされる『メッセージ』の方です。

ESウェブ ブラウザでは、すべてのオブジェクトに対して、
object.message_(selector, id, argc, argv);
というスタイルでメッセージを送ることができます。メッセージの応答は、Anyクラスの値として返されます。AnyクラスはWeb IDLで定義されているany型に対応するクラスです。(Web IDLで定義されているどんな型の値でも保持できるという意味でのAnyで、boost::anyのようにC++のどんな値でもとれるわけではありません。)

名前説明
uint32_tselectoridから自動的に生成されるハッシュ値。
const char*idメッセージの名前。ハッシュ値が被らなければ、0で構いません。
intargc引数argvの要素数。
Any*argvAny型の配列で表されるメッセージの引数。

基本はこれだけです。例えば、DOM4の、
Node appendChild(Node node);
というオペレーションをあるobjectに適用したい場合には、
Any param(node);
obect.message_(hash("appendChild"), "appendChild", 1, &param);
と言った具合にメッセージを送ると、objectがNodeであれば子ノードにnodeが追加されます。

「objectがNodeであれば」と書いたのは、ESウェブ ブラウザではクラスとかプロトタイプといったものは本質的なものとしては扱っていなくて、どんなオブジェクトに対しても"appendChild"という名前のメッセージを送りたければ送ることができるからです。C++で動的にダック・タイピングしているようなイメージです。

Web IDLの言語バインディング

原理的にはメッセージを送るだけ、と言っても何もかも、
obect.message_(hash("appendChild"), "appendChild", 1, &param);
のように書かないといけない、というのではちょっとプログラミングが大変です。JavaScriptでは、この処理は、
object.appendChild(node);
と書けます。そういうことを規程しているのが、Web IDLの仕様書の"ECMAScript binding"です。W3Cの仕様書の中などでWeb IDLで規定されているインターフェイスが、プログラミング言語上ではどんな風に見えるか、ということを規程したものは『バインディング』と呼ばれています。

ESウェブ ブラウザでは、ECMAScriptのバインディングに倣って、C++11のバインディングを独自に定義して、C++でも、
object.appendChild(node);
のように書けるようにしています。この中で何をするかは、もうほとんど明らかですね。
Node Node::appendChild(Node newChild)
{
    Any arguments_[1];
    arguments_[0] = newChild;
    return message_(0x139bc932, "appendChild", 1, arguments_).toObject();
}
こういった関数をひとつひとつ手で書いていてはキリがないので、Web IDLの定義ファイルから自動でコードを生成するツールを用意しています。それが『Web IDLコンパイラ』と呼ばれているもので、ESウェブ ブラウザで利用しているWeb IDLコンパイラはesidl(リンク先の記載はかなり前のバージョンのものなので、そのうち改訂します)と呼んでいます。コンパイラと名前は付いているものの、実際に出力するのはこのようなC++のソースファイルです。

ブリッジ

全般的にいまのところウェブ ブラウザはJavaScript以外の言語を使って実装されている部分が多いので、 JavaScriptで何かする場合にはJavaScriptからの呼び出しをC++の呼び出しに変換したり、あるいはC++からの呼び出しをJavaScriptの関数の呼び出しに変換したり、といった処理が必要になります。こういった処理を行う部分を『ブリッジ』と呼んでいます。C++とJavaScriptの間に橋を架けて、相互に行き来できるようにするわけです。

ESウェブ ブラウザではメッセージが基本になっているので、ブリッジの役割は、いきなり複数の言語間をつなげるのではなくて、各言語に合わせてメッセージを変換する、ということになります。

C++の関数呼び出しをメッセージに変換する部分は既に見ました。逆に受信したメッセージをC++のメンバー関数呼び出しにする部分はこのような感じになっています。
template <class IMP>
static Any dispatch(IMP* self, unsigned selector, const char* id, int argumentCount, Any* arguments)
{
    switch (selector) {
・・・snip・・・
     case 0x139bc932:
        if (argumentCount == 1)
            return self->appendChild(arguments[0].toObject());
・・・snip・・・
もちろんこのようなコードもWeb IDLコンパイラが自動生成するので、直接手で書く必要はありません。ESウェブブラウザのNodeの実装は単純に、
class NodeImp : ObjectMixin<NodeImp, EventTargetImp>
{
・・・snip・・・

    Node appendChild(Node newChild) {
・・・snip・・・
といった具合になるだけで、特にメッセージのことは意識する必要はありません。NodeImpとなっていることからも想像が付くように、Web IDLコンパイラが生成するのは基本的にはpimplイディオムの外側のようなものです。ObjectMixin<>って何、という話はいつかまた別の機会に。いまは、
class NodeImp : public EventTargetImp
と書くかわりにObjectMixin<>を挟むという約束だと思っておいてください。

JavaScript用のブリッジ

C++側でメッセージに変換したり、メッセージからメンバー関数呼び出しに変換する部分の概要は説明したので、次はJavaScriptとメッセージを交換する部分です。

SpiderMonkeyV8といったJavaScriptエンジンでは、C/C++言語のプログラムから操作出きるようにそれぞれJSAPIV8 APIというAPIを提供しています。ですのでJavaScript用のブリッジという場合にはメッセージとこれらのAPIを繋ぐことになります。

JSAPIであれば、メッセージからJavaScriptの関数の呼び出しは、
Any ProxyObject::message_(uint32_t selector, const char* id, int argc, Any* argv) {
・・・snip・・・
        jsval arguments[0 < argc ? argc : 1];
        for (int i = 0; i < argc; ++i)
            arguments[i] = convert(jscontext, argv[i]);
        if (JS_CallFunctionName(jscontext, jsobject, id, argc, arguments, &result) == JS_TRUE)
            return convert(jscontext, result);
・・・snip・・・
といった具合に、Any型の値をJSAPIのany型に相当するjsvalに変換して、JS_CallFunctionNameを呼び出すことで実現できます。

逆に、JavaScript側からC++のコードを呼び出す場合には、JSClassのcall関数で、
JSBool operation(JSContext* cx, uintN argc, jsval* vp) {
    if (JSObject* obj = JS_THIS_OBJECT(cx, vp)) {
        ObjectImp* native = static_cast<ObjectImp*>(JS_GetPrivate(cx, obj));
        Any arguments[argc];
        for (unsigned i = 0; i < argc; ++i)
            arguments[i] = convert(cx, JS_ARGV(cx, vp)[i]);
        Any result = native->message_(NativeClass::getHash(cx, vp, N), 0, argc, arguments);
        JS_SET_RVAL(cx, vp, convert(cx, result));
        return JS_TRUE;
    }
    return JS_FALSE;
 }
といった具合に、今度はjsvalの値をAnyの値に変換して、message_でメッセージを送信することで実現できます。

V8 APIの場合も考え方はまったく同様で、v8のHandle<Value>とAnyを相互に変換してFunction::Callやmessage_を呼び出すだけです。

まとめ

今回は簡単にES ウェブ ブラウザのメッセージング アーキテクチャ、Web IDLの言語バインディングの話題、それからC++およびJavaScript用のブリッジの実装についてまとめてみました。

Web IDLは、ようやくW3Cの勧告の候補版ができようとしているところで、CSS 2.1のようなテストスイートなどもまだ用意されていません。仕様書からただ実装しただけでは実際の動きは実装したベンダーによってバラバラになってしまう、という点はCSSでも実証済みだと思うので、実際に仕様が落ち着くまでにはまだ少し時間がかかりそうな気もします。それでも既に非常に便利なものになっているのは間違いありません

ES ウェブ ブラウザのC++11バインディングも、まだWeb IDLの最新の仕様のすべては取りこめていなくて、またパフォーマンス面でも修正していきたい部分がだいぶ残っています。ES ウェブ ブラウザでは、ソースツリー全体をまるごと修正、といった作業も特に問題なく進められてしまいますので、こうしたらもっといい、といった提案なども、もしあればぜひお知らせください。