Shiki’s Weblog

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

2012/04/29 #ESウェブブラウザ通信

今週は日本はゴールデン ウィーク真っ最中ですね。このブログを読みに来られてくる方は、ここぞとばかりコーディングに励んでいる方も多そうなので(謎)、いつも通り開発の進捗を報告していきます。
ESウェブブラウザは、ようやくな感じもありますが、r2646でウィンドウのリサイズに対応しました。Windowオブジェクトの大きさをOpenGLのglutReshapeFuncと連動させただけで特に大きな変更ではありませんが、こういった感じの細かな修正もリリースに向けて続けています。

r2646 - エスリルのページもはじめて全体を表示できるように。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リクエストの処理方法は今回でわりと改善できました。一方、レンダリング エンジンの方はまだまだ課題が多いので、地道な改善と同時に多少姑息でも速く動いているように見せるような仕掛けも必要になってきそうです。
次回以降もしばらくは今回のような感じで報告していきます。