Servoの現状(2014年Q2)

Servoの現況概説的なおはなし - snyk_s logの方を未だに参照されてる人がなんか多いみたいなので。

スライド自体は先日のブラウザエンジン先端観測会で使ったものそのものなんだけど、2014年Q2現在の現状としてはこっちの方が正確です。

ServoのDOMバインディングの話

Servoのリポジトリ内にDOMバインディングのデザイン覚え書きを投入したので、現時点での設計について、そろそろ日本語で説明しておく(英語版は結構前からServoのWikiに書いています)。

この記事の中で使うSpiderMonkey API用語は、結構古いものだったりするので、そこに注意(ServoはFirefox 17とかその辺りのSpiderMonkeyを使ってる)。

ブラウザにおけるDOMバインディングとは

この分野については、エデンの園でおきたこと - steps to phantasienという名記事があるので参照されたし。WebKit Chromium port時代のBlinkの話だけど、だいたいどのブラウザでも似たような問題を抱えていて、それぞれ微妙に異なるアプローチで解決している。

改めて私の言葉で要約してみると、「現代の実用的なブラウザエンジンというものは、自身の保有するDOM構造をJavaScriptなどの言語から操作可能にしている。使う側からすれば便利極まりない仕組みだけれど、ブラウザエンジン側にしてみると結構悩みの種。なぜなら、ブラウザエンジンは一般的にC/C++のような自前で参照関係を解決する種類のメモリ管理を必要とする世界であるのに対して、その構造を操作することになるスクリプト言語はだいたいエンジンが専用GC機構を持ち、その機構によってメモリの生存期間が決まるような言語仕様になっているから、これを巧くつなぐ仕組みが必要になる」ということ。

(関係ないけど、モダンなブラウザエンジンのDOMバインディングについて日本語で情報を求めると、だいたいomoさんが、その昔に書いた物にぶち当たる。本当にお世話になっています)

さて、こうした面倒くさい問題に対して、各ブラウザエンジンは幾つかのアプローチで対処している。OSSな実装で言うと:

といった感じ。

IEは、Exploring IE9's Enhanced DOM Capabilities - IEBlog - Site Home - MSDN Blogsによると、IE8まではCOMベースのバインディングを使用していたものの、IE9Chakraと密にバインディングするようにしたらしい。とにかくアーキテクチャ的にもIE9以降は違う感じはわかる。

Servo のアプローチ

上に挙げたアプローチはどれも巧いこと回っているものの、どうせならJSエンジン側のGCで全部を管理するようにしてしまいたい。Cycle Collectorを再実装したくはないし、余計な謎マクロとか色々実装したくはない。

というわけで、本題。「ServoのDOMバインディングは、どのようにオブジェクトの生存に関わる問題を解決しているのか」。答えは単純で「SpiderMonkey GCに全てを任せている」。もちろん世代別GCにも対応済。やったね!

Servoのアプローチその昔: dom.js

先述のエデンの園でおきたことでは、ServoはSpiderMonkey GCにDOMの生存管理を任せる為に、dom.jsを使うらしいと2012年末当時の観測結果として書いてある。が、現在はこの方法を使っている訳ではありません。

とりあえずdom.js路線について自分の知っている範囲の説明をする。実は、この時代は自分も知らない。今よりも、もっと実験実験してたプロジェクトの頃だし、自分もコミットチャンスを見つけてやり始める前。具体的に実装が動いていたのかもわからない。が、どちらにせよ、2012年末頃の時点では、dom.jsでDOMのデータ構造そのものをJavaScriptでself-hostしようとしていた、らしい。

原理的には一番オーソドックスにSpiderMonkey GCの恩恵を受けられる訳だし、self-hostでおなじみの効果として、DOMデータ構造そのものに対してJITが立ち入れるので、理論上はJITコンパイラによる更なる最適化のチャンスが増える。現代のフロントエンドWebプログラミングの最大の速度障壁はDOMの遅さであるので、そこの速度向上がワンチャンなので、一見すると、筋のよいアプローチに見える。

が、実際には、この試みは断念される。過去のmeeting noteを色々参照したところ、理由は単純にパフォーマンス + 複雑性の問題の様子。JS側と(文脈的にレイアウト用の)DOMの2つのレイヤーツリーを持って、常に同期し続けなければならない。ましてや並列性を狙ったエンジンなので、コードが本当に複雑になるのは想像に難くない。

具体的に何のパフォーマンスが悪かったのかは追跡できなかったのだけど、私の推論としては、おそらくは2つのDOMツリーを作る過程で発生するメモリコピーが要因ではないかと感じる。Webページを1つパースすると、だいたい数百〜数千の単位でDOMの領域のデータ構造がつくられる。これがJSオブジェクトで作成され、しかも同様のツリー構造をもう一回作るとなると、かかるコストは結構なものになるのではないかと。

何はともあれ、この路線は挫折することになる。

SpiderMonkey APIを通してDOMを管理する

というわけで、現在の路線はSpiderMonkeyAPIに従って、ラッパーオブジェクトの生死に合わせて、RustなDOMオブジェクトの生存を管理する方法を取っている。

全体のデザインはjdmによるもので、ここに書くものはコードから読み解いて、本人に直接確認した内容の覚え書きみたいなもんです。

基本的なライフサイクルについて日本語で書くと以下の通り(英語で書いたのを殆どそのままです):

前提: RustのDOMの構造

Rustには、別の構造体のデータ構造を継承した構造体をビルトインで定義する方法が無いので、Cでクラスなデータ構造をやるような手法が用いられている。また、後述するGCからのトレースの為、GC管理対象のDOMオブジェクトをJS<T>という型のメンバとして保持している:

// BarがFooを継承している場合の構造体の図
struct Bar {
    foo: Foo, // 先頭に基底クラスを格納する
    field1: JS<Hoge>, // SpiderMonkey GCの管理対象
    field2: int, // SpidrMonkey GCの管理対象でないもの
}

こうした構造体全てに対して、WebIDLから継承関係に応じたTraitを実装することで、どの型からどの型に対してのキャストが可能なのかを属性として付与している。詳しくは dom::bindings::codegen::InheritTypes.rsを見てください。

DOMの生成時

ServoがRustのオブジェクトを生成するとき、bindingコード(CodegenRust.pyによってWebIDLから生成されたglueコード)は、ラッパーオブジェクトとなるJSObjectを生成し、これに対するポインタを自身のReflectorフィールドに格納する。このラッパーオブジェクトはもちろんDOMと完全に一対一になるように生成され(でないとJSの比較演算子が変なことになる)、生成と同時に、対応するDOMオブジェクトへのポインタを自分の中に格納する。これで対応関係の出来上がり。

SpiderMonkey GCの管理下に入るDOMオブジェクトは全てが最基底な位置にReflectorというフィールドを保持しており、ここがラッパーオブジェクトとの対応関係を作っているわけですね。

ちなみに、このようなライフサイクルを作る都合上、Servoでは全てのDOMオブジェクトの生成時に、対応するラッパーオブジェクトの全てを同時に生成している。GeckoやBlinkの場合は、JS側からDOMにアクセスしたタイミングでラッパーオブジェクトを作っているが、それらのアプローチと比べるとServoの方が初期段階ではメモリを使っているはず。と、同時に、Servoのアプローチの方が世代別GCの影響が色濃く出るだろう。ページロードの瞬間からすべてのDOMに対応するJSオブジェクトが生成されているのだから。

DOMのオブジェクトグラフのトレース

このように生成したDOMのオブジェクトグラフ上をSpiderMonkey GCが歩けるようにするために、Rustの言語機能をかなりトリッキーに使ってる。

  1. SpiderMonky GCはマーキングフェーズ時に、それぞれのbindingコード内に定義された各ラッパーオブジェクトの定義元のJSClass.trace()を呼び出す。
  2. JSClass.trace()は、InhertTypes.rsの定義に従って、Foo::trace()を呼び出す。
  3. Foo::trace()は今度はFoo::encode()を呼び出す。このFoo::encode()#[deriving(Encodable)]によって、コンパイラ自動生成されたもの。現在の設計ではDOMをEncodableとして扱う目的が無い + 下手なマクロよりも確実に展開されるという前提に立っているのでこのアプローチを採用している。
  4. Foo::encode()は、メンバとして格納しているJS<T>JS<T>::encode()を呼び出す。
  5. JS<T>::encode()からdom::bindings::trace::trace_reflector()を呼び出す。
  6. trace_reflector()は、対応するラッパーオブジェクトをJSTracerに伝える = GCに伝える。
  7. 1~6までの操作を、オブジェクトグラフの終点まで延々と繰り返す
  8. 最後に、Rustのオブジェクトグラフを全部通り抜けた結果、どのオブジェクトが死ぬべきかがわかるというわけ

DOMの破棄

こうしてトレースした結果から、ライフサイクルを終えたと判別されたDOMは破棄される必要が有る。

この破棄ロジックは非常に簡単でラッパーオブジェクトの破棄タイミングに、JSClass.finalize()が呼び出され、そこからbindingコード内のFooBinding::_finalize()が呼びだされる。このメソッド内では、ラッパーオブジェクト側に格納しているRustオブジェクトのポインタが、Rustのowning pointerにキャストされ、空の変数に代入される。Rustの流儀に則り、ここでライフタイムが終わる = ポインタが指し示すオブジェクトも安全にデストラクトされるというわけ。

SpiderMonkey GCのExact GC Rootingへの対応

ここまでは基本的な動きについて話してきた。この他に、SpiderMonkeyは世代別GCを実装するにあたり、Exact (precise) GC方式に移行しているので、それへの対応が必要になっている。これに辺り、Servoでは、4種類のJS Managedな型を導入している

  • JS<T>は、前段でも述べた通り、RustなDOMオブジェクト内に保持されるDOM型のメンバを表す型。内部的には、格納対象のRustオブジェクトへの生ポインタを保持しているだけだったりする。GCによって再帰的にトレースされる対象。
  • Temporary<T>は、DOM型を返す必要があるメソッドの返り値として用いられる(典型的なのはDocument::getElementById()とか)。これは内部に対応するラッパーオブジェクトへのポインタを含んでいるので、SpiderMonkey GCのConservative Stack Scannerによってスキャンされる対象になり、結果、この型の値が生きている間、GCのrootとなる。ただし、SpiderMonkey GCはRootの選定にあたりLIFOの順番を保証する必要が有るのだけれど、メソッド間で値が移動するため、その順序が崩れてしまう場合がある。そのため、Root<T>という後述の型を導入することで、うまくそこを回避(保証)することにしている
  • Root<T>は、Temporary<T>同様にラッパーオブジェクトへのポインタを保持している。これも同様にConservative Stack ScannerによってスキャンされることでGC Rootとなるのだけれども、先述のLIFOの順番を守るようになっている。トリックとしては、RootCollectionというリストを用意して、Root<T>の生成時にこれに登録していく。スコープを抜ける際に、生成されたRoot<T>LIFOで破棄されるはずなので、破棄タイミングでアサーションを仕込んでRootCollectionからも取り出すようにして、LIFOな順番を保証するようにしている(ただ、Rustの言語挙動にかなり依存している面は否めない……)
  • JSRef<T>は、Root<T>に対する単純な参照。なんども無駄にRoot<T>を生成したくはないしね!

とまあ、ざっとこんな感じになる。 こうやって、ServoはSpiderMonkey GCにDOMの生存管理を丸っと任せているというわけ。

Servo DOM bindingの今後

第1に、SpiderMonkeyを現在使用しているGecko 17相当から、Gecko 3x相当までアップグレードする必要が有る。性能試験という意味でも、先述した世代別GCの効果測定の意味でも、流石に古いままではテスト対象として微妙だからだ。

第2に、SpiderMonkeyに対してGC Rootを伝える方法の改善。今のようにStack Scannerが辿るよりも、明確にどれがRootであるのかを示すようにした方が良いのは言うまでもない。

第3に、JSObjectの中にRustオブジェクトを格納する。たしか、一発でアロケーションできるようにしたいとかそんな感じだったはず。しばらく全然進捗ないし、自分も昔一回見たっきりなので、効用までは詳しく追ってません……

第4に、DOM APIの実装。そもそも実装してるAPIがまだまだ足りないので、全然ベンチマークも取れないという問題が有るので、ゴリゴリ実装して行かなければ。

だいたいこんな感じ。こうやって構築したDOMツリーに対して、レイアウト側からどのようにさわっているかについては、そのうち書ければ書きたい。

Servoの夢

Rustについて、自分としては流行ってくれなくてもいいというか、別に流行ること自体は本当にどうでもよくて、言語自体の完成とServoが形になってくれさえすればそれでいいと思っている。Rust Samuraiなんてイベントやってるけれども、個人的に普及させる気がないと常々言っているのはこれが理由。

もちろん流行ることによるメリットはあって、処理系自体の改良に携わる人数の増加や、有為なツール群が出てきやすくなる土台ができるのでコミュニティにとっては重要なのだけど、個人的には二の次で、自分の第一目標はServoなんだな。

Mozillaという名前を聞いただけで抵抗感を感じる人がいるのは知っているし(それこそ泥舟のように避ける人も知ってる)、今も昔もMozillaの動きに「胡散臭さ」が伴うのは周知の通りだし、本当にハンドリングが下手だなと思うときもあるけれども、それでも、出してくる技術はイカれてて面白いし、やっぱりOSSだから進捗がしょっちゅう公開されるというのは楽しい。

Rust langが普及するか?と聞かれたら、普及の定義次第なところもあるけれども、個人的には難しいと思う。ずっとC++の首を狙うDもいるし(当然固定客がいる)、そもそもC++書ける人はそれでいいと思ってることもあるし(ここらへんaltJSとESerの関係に似てる)、最近だとUNIX+C+Googleという化物のようなブランド力を誇るGolang様もいるし(おまけにGoは地味に周辺が整ってる)。C++/Rust/Dとは用途が微妙に違うとはいっても、LLに比べればGoの速度で十分だろうし、たいていの用途はGolangでいいんじゃないのかな。自分もgo覚えたいし。

じゃあ、Rustのどこに価値があるのかというと、それはServoなんじゃないのかな。というよりも、実プロダクトとしてのプログラミング言語はビジョンを具現化する道具にすぎなくて、その道具が実用に足ることを証明できなければ意味がない=その言語でビジョンを実現できなければ意味がない。研究発表と実プロダクトの境界線はここだと思う。学術価値のある新規性なんてRustが体得する必要性はない。

かつて「Servoはブラウザエンジン界のPlan9」と揶揄したことがあったけれども、もしかしたら本当にそうなるかもしれない。過去20年に及ぶ歴史の積み重ねはどうにも重たく、また、Servoが成そうとする並列化の夢も、既存の漸進的進化で解決がなされてしまうかもしれない。そもそもHTMLに裏打ちされたWebがどこまで有効であるのかもわからない。

けれども、そこには未だに語られなかったアーキテクチャロマンがある。もはや語られなくなったインターネットとソフトウェアのアーキテクチャへのロマンがある。仮に夢が半ばで潰えたとしても、かならずそこから実を結ぶ何かがあるはずだ。そう信じなければやっていられない。

これがGoogleAppleであれば、大切なことは社内で成果になってから表に出てくるのかもしれないけれども、幸いにしてMozillaは早くから表に出してくれた。坂を駆け上る途上のコードをお披露目してくれた。だからこそServoは楽しい。だからこそブラウザは楽しい。

たとえ見果てぬ夢だとしても、そこに自分の時間を賭けるだけの価値がある。そう信じている。

Servo Architecture vol.1: ConstellationとPipeline

第三回Servo Readingの成果として、最初はServoのCompositorの話を書くつもりだったんだけど、いきなりCompositorの話を出してもややこしいので、まずはConstellationPipelineの話をしようと思う。

ブラウザエンジンと一口に言っても、よく知られている通り、その仕事は多岐に渡る。リソースの読み込み、画像のデコード、HTML/CSSのパース、描画ツリーの構築、DOMの構築、JSの実行、結果の出力etc。こうして並べてみるとわかるようにその仕事は多岐に渡る。ましてや最近はOSの抽象層みたいな事もやり始めたので、どんどんと管制対象は増えていくばかり。そうした処理を管制する役割の箇所をServoではConstellationと呼ぶ。最初はengineと呼ばれていたまさにエンジンの中央部分だ。どういったわけかリネームされて今の名前になったわけだけど、ECMAScriptで有名な某日本人とは関係ないらしい。servoのircで「まさかConstellationって名前のヤツがpull request送って来るとは思わなかったよ」みたいなこと言われてたくらいだし(最初に名前が変わった時も面白かったけど、ircがその話になった時は本当に爆笑した)。

現在の実装では、Constellationは渡したurlごとにインスタンスが生成されるようになっているので、処理単位としては、ブラウザの1タブごとに生成されているようなものだと考えるといいだろう。Firefoxの中身になじみの有る人にとっては、xul:browserあたりと対応させて想像しているかもしれない(この場合はもう少し低レイヤーなんだけどね)。この管制役は、そこに束ねた各主要タスクとのメッセージチャンネルを用いて、イベントメッセージの発生に応じて処理を振り分け、ブラウザを回転させることになる。

さて、ブラウザエンジンはただページをレンダリングしてDOMを構築するだけではブラウザたり得ない。そこにはWebをbrowseするための機能としてナビゲーション機能が必要だ。そして、ナビゲーションを行うにはページに相当する単位も必要になってくる。そこでServoではPipelineというモデルを用意している。このPipelineはそれぞれがScriptTaskRenderTaskLayoutTask、それとナビゲーションの状態などを持つ。1ページ=1 Pipelineとは言っても、iframeが絡んだ場合は構成がちょっと異なる。実質別のページだしね。親ページとoriginが違うiframeでは完全に別のPipelineが生成される。same origin iframeでもPipeline間でもScriptTaskのみが共有される。セキュリティ的な価値もあるけれども、設計としての明快さも感じられる。

そして、戻る・進むといったナビゲーションが発生すると、ConstellationはこのPipelineのリストの位置を差し替えることでページのナビゲーションを実現している。

ConstellationPipelineについての説明はだいたいこんな感じ。次回は、うーん、いつ頃にしましょうか。

Servo Reading part2: ざっくり全体構成編

  • 初回はRust復習会だったんで、実質的な初回なんだけど。
  • とりあえず a4baa7fc でコードリーディング
  • ざっくり全体構成を知るのに終止
  • 獣道すら存在しない前人未踏のコードの繁みに分け入る楽しさ

servo.rc (/components/servo.rc)

  • 素直なことしか書いてない
  • メッセージチャンネルが値を受け取るまで待機状態で処理を止めるのを利用して、main関数からメインループの追放を行っている
    • Rustのメッセージチャンネルの使い方の好例
  • ProfilerとCompositorTaskを生成し、それらへのチャンネルをConstellation(ブラウザのインスタンスそのもの)に引き渡し、Servoを起動している

CompositorTask (/components/compositing/mod.rs)

  • 入出力関係を制御しているのがこれ
  • OSによって作成されたスレッド上で実行される事を明示している
    • とはいえ、Rust 0.8preではstd::task::SchedModeからPlatformThreadが消えてる?っぽいのでどうなることやら
  • CompositorTask::create()でタスクを生成し、CompositorTask::run_main_loop()でループを回し、イベント入力を待ち受ける
  • 毎回10msecのsleep挟んで、ループを回し続ける
    • ちなみにここのsleepにはlibuvのラッパー絡んでます(そんなにlibuv詳しくないし深く追えてない)
  • こいつが待ち受けるのは、レンダリングタスクと、ウィンドウシステムからやってくるタスクの二種類
  • run_main_loop()、メチャクチャデカくてクロージャ作りまくりで読み難いですが、クロージャ作る事で変数なんでも参照できて書きやすくしてるのが狙いとも言える感じ。
    • これclean upできないかなー?

Constellation (/components/main/constellation.rs)

  • Servoのブラウザとしての機能を司る
  • 昔はengineって名前で、いつの間にかこの名前になりそのうち名前変えようかみたいな話も有ったりした
    • ECMAScriptで有名な某日本人のHNと被ってるのは偶然ぽい
      • とはいえネタとして大変に素晴らしい
  • Constellation::start()後にConstellation::run()してて、その中でループを回してるんだけど、ここでもメッセージの受け取りを使ってループを止めてる
    • Rustの非同期処理の常習パターンですね
  • ちなみにこのループもメッセージを受け取って、処理を振り分け続ける為のループ
    • 色々やってますよん
  • そして、URLを読み込む=静的なページ遷移が起こると、各ページに対してPipelineインスタンスを生成する(これはiframeの読み込みでも発生する)
    • で、ページ遷移が発生して、ブラウザの戻る・進む履歴からページが消えると、そのページのPipelineは破棄される

Pipeline (/components/main/pipeline.rs)

  • RenderTask, LayoutTask, ScriptTaskを生成する

だいたいここらへんで腹が減ったので、冷やしかつ丼食べに行って終わった

Servoコードリーディングに必要なもの

さすがにいつまでもServoへの一方的な愛を語るだけではいかんだろうと思い、腰を据えてServoを読むべくServoのコードリーディング会をやってみたのですが、とりあえず初回の結論だけ報告しますと

Servo読むならRustのチュートリアルくらい全部目を通しておかないと効率悪すぎ

という結論になりました。

関数追っていけば何とかなるだろうと思っていた所、ServoはRustパラダイムの結晶のようなコードなので、そのもくろみは開始後30分で破綻しました

てなわけで、そのうちコードリーディング会第二回をやりますが、参加者各位はタスクとそれらのコミュニケーションモデルと、Rustにおけるモジュールとcrateの概念についてざっくりと理解しておく事をオススメします。

Servoの現況概説的なおはなし

ゆるふわ系Rust勉強会で話した内容のスライドを公開していなかったので今更ながら話した事について簡単にまとめます。

About Servo

  • Mozillaが行っているRustで書かれたブラウザエンジン実験プロジェクト
    • Rustのセールスポイントである並列性・安全性にフォーカスした実験
    • 現時点における、Rustの主要使用用途にして最大のプロダクトのひとつ
    • Rust言語そのものの開発との事実上の相互フィードバック関係にある
  • Blinkと比較されることが多いが、WebKit Chromium portの延長であったBlinkに対し、Rustは完全な新規の実験プロジェクト。その域を脱してはいない。
  • モダンブラウザエンジン(デザイン・アーキテクチャに挑戦している的な意味で)

Servoの実験領域

  • モダンハードウェアにおける新しいブラウザエンジンの探求
  • メニーコアなどのモダンハードウェア環境における並列処理化
  • メモリ周りにまつわるセキュリティ問題の解決
  • 以上をRustを用いて行う(Rust自体の実験も入っている)
    • Rustでの各種ライブラリの再実装・ポート・バインディングなども研究範囲ではある

並列化によるブラウザエンジンパフォーマンス向上の取り組み案

DOM, CSS, Architecture

  • 第一回かつ筆者も勉強中という事もあり、有益な情報は話せていません

Servoのリポジトリ的な話

開発モデル

  • Githubだもの、Pull Requestです
  • ビルドボットあります
    • 最近は、ちゃんとビルドボット通らないとマージされない
  • ServoのCI手順についてはこれを参照

Servoの現在の状況

  • まずはAcid 1 testクリアが目標
  • 使い物になる段階ではない
    • とりあえずビルドして試せば分かる
    • 関係ないですが、Servoのビルド時間の半分は、Servoが持ってるRustのビルド時間
  • 最近人が増えてきた
    • Sumsungの人も若干名Pull request送ってる(そんなにリソース割いてる印象は無いけど……)

Servoで今後ありそうなおもしろそうなこと

  • ヘッドレステストへの対応?
    • レンダリング結果の出力パスが無いので、現在は動作しない
      • -oオプションつけると、その旨が警告出るはず(出るようにした)
  • WebKit 2 APIへの対応?
    • ひとまず実装してみて、モダンブラウザエンジンとしてどういうAPIモデルが必要なのかのたたき台にしたいんだろうなと推測している
    • Geckoと同じ事をやるんじゃ研究の意味ないだろうし
  • 内部文字コードの検討
  • DOMバインディング方針の決定?
    • とりあえずdom.js使わないことは、ほぼ確定しているっぽい(理由は色々)(できればそのうち書く)
  • CSSセレクタマッチングの並列化?

詳しく知りたい場合

上で書いた事はGithubのServoリポジトリのwikiにだいたい書いてあるので、そこを読みましょう。

だいたいこんな感じで話しました。