Edited at

V8 Context の作成を Snapshot を使って高速化した話

この記事は Chromium Browser Advent Calendar 2017 の 12 日目の記事です。

昨年から今年上旬に渡って作っていた高速化の裏側を説明しています。まだ標準でオンになっている機能ではないので「細かい話は別にいいから使ってみたい」という方は ユーザーとして使ってみるには をご覧ください。


V8 Context とは

Chromium (Blink) で使っている JavaScript エンジン V8 では、JS プログラムの変数がアクセスできる範囲のことを Context と呼んでいます。これは Web IDLEcmaScript の仕様で Realm と呼ばれているものに該当します。

細かいことを置いておくと、ウェブページを閲覧する際は、メインページ、<iframe>、Chrome Extension ごとに Context が存在しています。また Context の作成は基本的に 1 スレッド上で行われ、ほとんどがページの読み込み時に作られるので、Context の初期化時間は画面描画開始時間に強い相関があります。


V8 Context を作るためには

V8 Context の作成での重要な仕事は global_template を基に必要な callback 関数の登録をする事にあります。

Blink から Context を作成するほとんどのケースでは global_templateWindow interface が渡されます。その中には多くの Property や Method が定義されている他、いろんな interface の Interface Object も定義されているので、最終的に 1500 を超える関数を登録する必要があります。


Snapshot を使って高速化

とはいえ、基本的に定義される関数たちは作られる Context によらず同じもの (Isolate 単位で作られるもの) なので、毎度毎度 global_template を参照しつつ「このタイプの callback 関数があるから、この Object を用意して、この値を設定して」とやっていくのは効率が良くありません。「一通り定義したらこれだけのメモリが必要で、こんなレイアウトになってるから」という感じにできないでしょうか?

そこで我々は V8 が用意してくれていた Snapshot の機能を使って効率化することにしました。ちなみに純粋な V8 でも同様に Context を作る度に Math などの Built-in object を作るのが非効率ということで、この Snapshot 機能を作ったという背景があります。

ただ、この Snapshot 機能は本来 V8 が V8 のために用意した機能であり、それを Chromium から使えるように改造してもらったため、使う上でちょっと面倒ないくつかの注意点があります。

ちなみにこの Snapshot ファイル、Chrome では Chrome の実行ファイルを作る際に作られ、Chrome 実行ファイルとともに配布されていますので(少なくとも同じ OS、同じバージョンでは)全ユーザーに同じファイルが行き届いていますし、ユーザー毎の情報が書き込まれているなんてことはありません。


外部参照テーブル

Snapshot ファイルの作成時には「 JS 側の関数に対応する C++ 側の関数を指定する」という作業が問題になります。 Snapshot ファイルの作成は Chromium とは違う実行ファイル中で作成するため、関数の指定は関数ポインタではできませんし、勿論シンボルテーブルなんて置いていません。そのため、Snapshot 作成プログラムと Chromium で共通な並び方になっている外部参照テーブルを使うことで参照する関数を解決することになります。Snapshot 作成プログラムでは元になっている Context に埋め込まれた関数ポインタをテーブルの index に変換して保存し、逆に Chromium ではテーブルを参照して正しい関数ポインタに戻して Context を作ります。


環境依存な機能

また、Chromium/V8 では chrome://flags で on/off できる experimental 機能のような、条件によって使えるようになる機能があります。この Snapshot 機能を使った Context 作成もその1つです。それらの中には JavaScript の動作を変更するものもあり、これまでの Chromium では最初に interface template を作る際に登録していました。が、Snapshot を作る際にはどの機能を使うという情報が無いので、全てを disabled にした純粋な Context の Snapshot を作り、実行時には追加で必要な機能を Context ごとに追加登録する形をとっています。


ついでに高速化

ところで、この snapshot 機能、実はその Context 内で作ったオブジェクトについては一緒に保存することができます。また、Chromium 上での Context は多くの場合ウェブページ閲覧のために使われるので document が定義されています。なので Context Snapshot の特殊化として、document のラッパー込みの Context も作っています。これを利用することで、(Window ほどではないにしても) 多くの関数を登録する必要がある document の設定も省略することができます。ただ document ラッパーは内部情報として Blink の document オブジェクトへのポインタを持っており、V8 はその情報をどう処理すればいいか知らないので Snapshot 化するために処理の方法を伝えておく必要があります(v8::SerializeInternalFieldsCallbackv8::DeserializeInternalFieldsCallback)。


どれくらい高速化したのか

さて、これまでどの様に Snapshot 機能を作ってきたのかというお話ばかりでしたが、最後にその効果について検証してみましょう。時間計測は 17 日目 に keishi さんが書いてくれるらしい tracing を使って行います。ザックリ言うと



  1. chrome://tracing を開きます。

  2. 左上の [Record] をクリックします。

  3. “Javascript and rendering” を選んで [Record] をクリックします。

  4. 別のタブで適当なページを開きます。

  5. Tracing のタブに戻って [Stop] をクリックします。

  6. 結果が表示されるので “Renderer (pid: XXXX) 開いたページのタイトル” の枠から項目を見ます。

です。次節の “ユーザーとして使ってみるには” を参考に Snapshot 機能をオンにしたりオフにしたりにしてこの計測を行ってみましょう。ちなみにこの実験で重要な Metric になるのは LocalWindowProxy::Initialize です。

私の環境での一発テストの結果は、Snapshot 機能をオンにした状態で 4.5 ms、オフにした状態で 7.5 ms となりました。半減とまでは行かなくても随分速くなっていることがわかると思います。(機能追加のレポートのときには一発テストではなく 100 回くらい実験を行って統計情報を見てますが、その方法は面倒なので省略。)


ユーザーとして使ってみるには

冒頭にも書いたとおり、残念ながらこの機能はデフォルトでは on になっていません。なので使って見るためには以下のような手順を踏む必要があります。ちなみに Chrome ではバージョン 63 以降で実装されたものなので、安定版でまだ 62 から更新されてない方は強制的に更新をかけてみて下さい。また、色々な理由から Windows/MacOSX/Linux 用の Chrome でのみ使えます。下の絵だと ChromeOS でも使えるように見えるかもしれませんが、気のせいです。ChromeOS や Android では現在使えません。



  1. chrome://flags を開きます。

  2. “Use a snapshot to create V8 contexts” を ”Enabled” にします。

  3. Chrome/Chromium を再起動します。

flags.PNG

これだけで使えるようになります。ページ読み込みが爆速になるかどうか、お試しあれ。