C++
Chrome
Chromium
ServiceWorker
PWA

ServiceWorkerがちょっとずつスピードアップしてる話

More than 1 year has passed since last update.

この記事はChromium Browser Advent Calendar 2017の18日目の記事になります。

12/3の僕の記事ではプロセスモデルに基づいてDedicated Worker, Shared Worker, Service Workerの動きについて紹介しました。今日はその話から発展して、ここ1年半(つまり僕がChromiumに参入してから)Service Workerに関してどのような高速化が行われたかを紹介しようと思います。

No fetch handler

Service Workerの大きな機能として、FetchEventがあります。FetchEventの動きについてまず簡単に確認してみましょう。

worker.js
addEventListener('fetch', (e) => {
  e.respondWith(fetch(e.request.url));
});
main.html
<!doctype html>
<script>
navigator.serviceWorker.register('worker.js');
</script>

このようなスクリプトとページを用意すると、初回のページ訪問時にService Workerが保存され、次回の訪問時にはworker.js経由でレスポンスを取得します。その際に、前回の記事でちらっと紹介したように、worker.jsはmain.htmlとは別のプロセスで動く可能性があります。以下にその状態を図に示してみます。

まずmain.htmlを開くとそのリクエストはブラウザプロセスで処理されます。Service Workerが存在する場合、Service WorkerのあるレンダラプロセスへとFetch Eventが投げられます。
fetchevent-1.png

先程の例のように、worker.jsでfetch()を実行すると、Service Workerからリクエストを発行します。そのレスポンスはService Workerが処理します。

fetchevent-3.png

fetch()の返り値として受け取ったレスポンスをe.respondWith()へ渡しています。つまり、Service Workerのレンダラプロセスからmain.htmlのレンダラプロセスへとレスポンスを返していることになります。

fetchevent-7.png

さて、ここですでにお気づきかもしれませんが、先程の例ではただService Worker経由でリクエストを処理しているだけでした。ここでもしCache Storageなどを利用してネットワークを使わずにレスポンスを返しているのであれば高速にレスポンスを返すことができますが、この例では特に何もしていません。つまり、Service Workerを経由する分だけ余計に時間がかかることになります。たとえばPush通知を使いたいだけだから、ということでこのようなFetchハンドラを登録してしまうと、ネットワークリクエストの処理に時間がかかってしまうことになります。
これを避けるために実装されたのがNo Fetch Optimization (issue, patch)です。これは、Service Workerを登録する際にFetch Eventのハンドラが存在しなかった場合、それ以降Service Workerを起動することなく通常通りリクエストを処理するという改善になります。
chrome://serviceworker-internalsにアクセスすると、実は登録されているService Workerの一覧を見ることができます。ここに、Fetch handler existenceという項目があり、これがDOES_NOT_EXISTになっているページではNo fetch handlerとみなされています。

Direct connection (Servicification, a.k.a. S13nServiceWorker)

最近、ChromiumではServicificationと呼ばれる大規模リファクタリングプロジェクトが行われています。Service Workerでも同様で、Servicification Service Worker (S13nServiceWorker) に関連するコードと// S13nServiceWorker: …というようなコメント (code search)がいたるところに存在します。
Servicificationとは、簡単に説明するとIPCを中央集権的なアーキテクチャから各モジュール同士が直接通信できるような地方分権的な方式へと変え、IPCをAPIレイヤとしてモジュールをより疎結合にしようという試みです。これにより、コードのメンテナンス性が上がり、必要なモジュールだけを含んだバイナリを作りやすくなるというようなメリットがあります。このServicificationを実現するために、IPCのライブラリをChromium IPC (Legacy IPCと呼ばれていたりする)からMojoというものへ移行しつつあります。これをMojoficationと呼んでいたりします。

さて、このMojoficationでの大きな変更点は、ブラウザプロセスとレンダラプロセスの通信だけではなく、レンダラプロセス同士も通信できるようになったことです。通信の端点になるオブジェクトをプロセスをまたいで送りつけることができるようになったため、自由なコネクションを張れるようになりました。
これを利用してService Workerが改善される点として、直接Fetch Eventを発行できるようになるということがあります。先程の例でFetch Eventを実行する部分を見てみましょう。

fetchevent-1.png

もしこの図にレンダラプロセス同士のコネクションが存在すると、worker.jsへ直接Fetch Eventが発行できることはわかると思います。

directconnection.png

現在進んでいる実装では、このDirect connectionをmain.htmlのサブリソースのリクエストに関して利用するようになっています。このため、一度Service Workerによって提供されたメインリソースからのFetch Eventの処理が早くなることが期待されています。

Script Streaming

Fetch EventなどによりService Workerを起動する必要があるとき、ボトルネックの一つとして、メインスレッドが忙しくて処理が進まないというものがあります。これについてまず説明します。

Fetch Eventの実行の際、まずブラウザプロセスから適当なプロセス上にService Workerの実行環境を用意します。このとき、以前の実装ではService Workerのスクリプト(worker.js)を取ってくる手順は以下のようになっていました。

previous-scriptload.png

Service Workerの起動リクエストがレンダラプロセスへくると、まずメインスレッドでそれを受けとり、スクリプトのロードをはじめます。その後、ブラウザプロセスがスクリプトをディスクから読み取って返し、スクリプトの準備が終わるとWorkerスレッドを起動していました。
しかし、もし他のページと同じプロセスにService Workerがいた場合、メインスレッドでの起動リクエストの受取りやスクリプトの受取りが他のページのJavaScriptなどによって止まってしまうという問題がありました。

previous-scriptload-blocked.png

実際には必要なスクリプトは起動時にすでにすべてわかっているため、いちいちメインスレッドから問い合わせをする必要はありません。そこで実装したのがScript Streaming for Service Workerという機構になります(どうやら他にもScript Streamingと呼ばれる機能があるようですが、ここではServiceWorkerのScript Streamingを指します)。Script Streamingでは、起動リクエストと同時にスクリプトのデータをブラウザプロセスから送りつけるという最適化になります。プロセス・スレッドをまたがっていろいろデータをやりとりする機構だったので結構実装が大変だった・・・

以下にDesign Docから図を引っ張ってきて貼っておきます。(サボったわけじゃなくもないよ!)
上の図がもともとの動きで、下の図がScript Streamingを導入したあとの動きを表しています。

designdoc-previous.png

designdoc-after.png

実際に、ローカルですこし試した感じでスクリプトを読み込む部分で数ms(デスクトップ)〜数十ms(Android)かかっていたものがうまくオーバーラップできたことがわかりました。

この最適化はChromiumのM64(バージョン64.0.xxxx移行)でデフォルトで有効になっています。M63では、chrome://flags#enable-service-worker-script-streamingをEnableにすることで試すことができます。

おわりに

うわーん、ギリギリになって慌てて書くと書きたいことが全部書ききれなかった!!!

言い訳をすると、最近ゲームマーケットに行ったので買ったボドゲをひたすら消費したり(おかげさまで全部消費できました)、PSVRを買っちゃったのでV!勇者のくせになまいきだRにはまっちゃったり、Hidden Agendaとかいうゲームがスマホつかって6人までできるとかでおもしれーといって始めちゃったり、ガチアサリが始まったり、忙しかったんですよ・・・(言い訳になっていない)

ということで、本当はもっとメインスレッドを飛ばして直接fetch (implemented by @horo)やrespondWith (implemented by me)できるようになった話とか、Service Workerの起動をメインスレッドをスキップしてできると嬉しいよねという話(future work)とかもっといろいろあったのですが、まぁとりあえず頑張ってる感が伝わるといいなとおもって書きました。笑

バグなどありましたらhttps://crbug.com へどしどしお寄せください。興味をもって実装を見たりじっさいにコントリビューションしてもらえると嬉しいなと思ったりしてます。Chromium、結構平和なコミュニティーだとおもうので!

それでは、良いお年を!

明日は@edwardkenfoxさんによるLayoutTestsから始めるChromiumソースコードリーディング、です。