Dev Study
← Rust で小さなブラウザを作る コース

29. 作っていない機能は本物のどこで動いているか — 通信・キャッシュ・XHR・時間・メモリ・保存・安全

このコースで作ったのは『 を受けて 1 回取り、変換して描き、script を 1 回走らせる』一本道です。本物のブラウザにはその周りに、通信を速く安全にする層、同じものを 2 度取らない層、時間を扱う層、メモリを回収する層、保存する層、守る層があります。この回は、それらが『どういう原理で』『本物のどこで』動いているかを、ソースの場所つきで書きます。実装はしていませんが、原理が分かれば自分で足す位置も分かります。

と TLS。この実装は http だけです。本物は の上に TLS を重ねてから を流し、 は socket/ssl_client_socket_impl.cc、Firefox は NSS(security/manager/ssl)がその層です。接続は使い捨てず、Chromium は TransportClientSocketPool に戻して次の要求で再利用し、同じへの要求を並行して出します。この実装に足すなら http.rs の を包む層として入りますが、TLS は だけでは書けないので外部が要ります。

(RFC 9111)。原理は 3 つです。(1) 鍵は「要求メソッドと対象 URI」で、Vary があれば指定された要求も鍵に入ります。(2) 保存してよい応答は条件があり、no-store が無く、・s-maxage・Expires・public などのどれかがあるか、ヒューリスティックに保存可能なコードであること。(3) 新鮮さは、共有キャッシュなら s-maxage、次に max-age、次に Expires から Date を引いた値、どれも無ければヒューリスティックで決め、現在の年齢(current_age)と比べます。新鮮でなければ を If-None-Match に付けて聞き、304 が返れば保存済みのをそのまま使い、ヘッダーだけ更新します。

その HTTP キャッシュの居場所は、Chromium では の中の HttpCache::Transaction(net/http/http_cache.h は「キャッシュの論理は 7234 に従う」と書く。7234 は 9111 の前身)で、保存先は net/disk_cache です。 には 2 つのバックエンドがあり、Windows は小さなエントリを ファイルに詰める blockfile、Android・ChromeOS・Linux・macOS は「1 エントリ 1 ファイル + メモリ上の索引」の simple cache、シークレットモードはメモリだけです。Firefox は netwerk/cache2 で、CacheStorageService → CacheStorage → CacheEntry → CacheFile の順に辿り、メモリだけの保存域とディスクの保存域があり、全エントリの索引(CacheIndex)をメモリに持って個々のファイルを開かずに追い出しを決めます。

画像を取りに行かない判断。これは HTTP キャッシュだけではなく、図 ① のとおり 3 層あります。一番上は HTML Standard の「」で、 ごとに (絶対 URL, 設定のモード, origin) の組を鍵に持ち、img の src を更新する手順の中でまずこの表を見ます。同じページの 2 つ目の img が即座に出るのはここです。次がので、 の MemoryCache(platform/loader/fetch/memory_cache.h)は「Web ページが使う下位資源(画像・script・)を保持する」URL を鍵にした表で、ResourceFetcher が要求ごとに「適切な Resource がもう無いか」を確かめます。Firefox では image/imgLoader.cpp の imgCacheTable です。最後が上で書いた HTTP キャッシュです。

と fetch。原理は図 ② の 1 行で、send() は通信を『並行して』始めてすぐ戻り、応答が届くと『networking 』にタスクが積まれ、がそれを取り出した時に readyState が進んで readystatechange・progress・load・loadend が発火します(XMLHttpRequest Standard の send() の手順)。だから send() の直後に responseText は読めません。Chromium の実体は core/xmlhttprequest/xml_http_request.cc で、通信自体は ResourceFetcher から Network Service の URLLoader に頼みます。 は core/fetch/fetch_manager.cc です。Firefox は dom/xhr/XMLHttpRequestMainThread.cpp と dom/fetch/FetchDriver.cpp です。別への要求を通すかどうか(CORS)は Chromium では services/network/cors/cors_url_loader.cc が見ます。

イベントループ・タイマー・イベント。HTML Standard のイベントループは、task source ごとのタスクキューから 1 つ選んで実行し、を全部片付け、必要なら (スタイル → レイアウト → )を行い、これを繰り返します。Blink では「JavaScript の実行、、CSS、レイアウト計算を含む重要なことの大半がで起きる」ので、仕事は TaskType を付けたタスクとしてフレームのタスクランナーに積まれ、入力のタスクが最優先、ユーザー操作の直後は他を 2 秒遅らせ、背景タブは凍結し、背景の JS タイマーは間引かれます(platform/scheduler/TaskSchedulingInBlink.md)。setTimeout は Blink が core/scheduler/dom_timer.cc、Firefox が dom/base/TimeoutManager.cpp(「 の setTimeout / setInterval の集合を管理する」)です。クリックなどのイベントは Blink の core/dom/events/event_dispatcher.cc と core/input/event_handler.cc、Firefox の dom/events/EventDispatcher.cpp が木を上下に伝えます。

メモリ。この実装は と で寿命を決め、GC を持ちません。本物は 3 か所で回収します。DOM は Blink が (GarbageCollected<T> をし、Member<T> / WeakMember<T> で参照し、Trace() で列挙するトレース GC。マーキングとスイープは並行)、Firefox が参照カウント + (xpcom/base/nsCycleCollector.cpp)で循環を回収します。JS は の がで、新しいオブジェクトは nursery に置いて Scavenger で生き残りだけ移し、全体は Mark-Compact で回収し、並列・増分・並行で停止時間を削ります。 も Nursery を持つ世代別・増分の GC です(js/src/gc)。もう 1 つの『メモリの上限』はプロセスの境界で、タブやサイトごとの描画プロセスが落ちても他は生きます。

保存。 は Chromium では Network Service の net/cookies/cookie_monster.cc が持ち、描画プロセスには渡しません。 は Blink の modules/storage/storage_area.cc から、ブラウザ側の components/services/storage/dom_storage/local_storage_impl.cc に書きます。Firefox は dom/storage/LocalStorage.cpp です。IndexedDB は Blink の modules/indexeddb と Firefox の dom/indexedDB。『戻る・進む』の履歴はページではなくが持ち、Chromium では content/browser/renderer_host/navigation_controller_impl.cc です。

安全。同一オリジンの単位は (, host, ) の origin で、Chromium は url/origin.h と Blink の platform/weborigin/security_origin.cc、Firefox は caps/BasePrincipal.cpp が表します。 は Blink の core/frame/csp/content_security_policy.cc と Firefox の dom/security/nsCSPContext.cpp。さらにプロセスの壁として、Chromium のは「描画プロセスを 1 つのサイト(scheme + eTLD+1)に固定し、ブラウザプロセスが IPC を監視して、ネットワーク側で別オリジンの読み取りを遮る(CORB / ORB)」の 3 つで成り立ち(docs/process_model_and_site_isolation.md)、Firefox の Fission は content プロセスを webIsolated=サイト の種類で分けます。描画プロセス自体は(docs/design/sandbox.md)の中です。

画面。この実装は文字のに書きます。本物は G 章の末尾に書いたとおり、Chromium が → → cc(タイルで並列)→ の で合成、Firefox が nsDisplayList → (Rust、GPU プロセス)です。ウィンドウとマウス・キーボードの入力は OS の API から来て、ブラウザプロセスが受けて描画プロセスの入力タスクとして積みます。

この実装に足すなら。原理が短いものから順に、(1) イベントループ: タスクキューと setTimeout を runtime.rs の外に置き、script 実行後にキューを回す。(2) XMLHttpRequest: 通信を std::thread に出し、結果を mpsc で受けて『コールバックを呼ぶタスク』に変える。(3) HTTP キャッシュ: <, CachedResponse> と RFC 9111 の新鮮さの計算(30 行程度)、If-None-Match と 304。(4) img の取得と list of available images。どれも std だけで書け、各 300〜600 行です。GC と TLS と GUI は外部クレートか OS が要るので、このコースの外です。

開発のどこに活かすか: 『同じものを 2 度取らない』が 3 層になっている構造は、業務システムの CDN・アプリ内キャッシュ・DB のキャッシュと同じ形です。どの層が鍵に何を使い、いつ古いと判断し、古いときに全部取り直すのか差分で確かめるのか(ETag と 304)を分けて考える癖は、キャッシュの不具合を追うときに直接効きます。send() がすぐ戻ってコールバックが後で来る構造は、 の非同期 I/O やメッセージキューと同じで、『誰がいつそのキューを回すか』を押さえれば順序の問題が読めます。

点線の付いた語はタップすると説明が出ます。用語集を開く →

① 画像を『取りに行かない』判断は 3 層ある

上から順に見て、どこかで当たればそこで止まる
list of available imagesHTML Standard。Document ごと。鍵は (絶対 URL, CORS モード, origin)
無ければ
描画プロセスのメモリキャッシュBlink MemoryCache: URL が鍵。画像・script・CSS を保持。Firefox は imgLoader の imgCacheTable
無ければ
HTTP キャッシュ(RFC 9111)Network Service の HttpCache + disk_cache / Firefox の cache2。新鮮なら返す、古ければ条件付き要求
新鮮でなく、304 でもなければ
ネットワークC 章で作った部分
  • ・同じページに同じ src の img が 2 つあっても 1 回しか取らないのは、1 層目と 2 層目の仕事
  • ・リロードで『取りに行くが 304 で済む』のは 3 層目の仕事(If-None-Match / If-Modified-Since)
  • ・Blink の MemoryCache は 2 段階で捨てる: まず最大寿命を過ぎたもの、まだ多ければ種類・頻度・コストで価値の低いもの(memory_cache.h の注釈)

『キャッシュで画像を取らない』は 1 つの仕組みではなく、Document → 描画プロセス → ネットワークサービスの 3 か所にある別々の表です。

② XMLHttpRequest の send() は、通信を『頼んで』すぐ戻る

XMLHttpRequest Standard の send() と HTML Standard のイベントループ
xhr.send()loadstart を発火し、fetch を『並行して』始めて、呼び出し元へ戻る
別の場所(Network Service / socket プロセス)で通信が進む
応答ヘッダーが届く『networking task source』にタスクを 1 つ積む
イベントループがそのタスクを取り出したとき
readyState = HEADERS_RECEIVED → LOADING → DONEreadystatechange / progress / load / loadend をこの順に発火
DOM を書き換えたら
update the renderingスタイル → レイアウト → 描画を必要な段からやり直す
  • ・だから send() の次の行では responseText はまだ空。コールバックは『後で来るタスク』の中で動く
  • ・Chromium: core/xmlhttprequest/xml_http_request.cc(fetch は core/fetch/fetch_manager.cc)。Firefox: dom/xhr/XMLHttpRequestMainThread.cpp(dom/fetch/FetchDriver.cpp)

この実装の script は DOM 完成後に 1 回だけ走り、通信を待ちません。XHR を足すには『タスクキュー』と『通信を別スレッドに出す』の 2 つが先に要ります。

③ プロセスの分け方(この実装は 1 プロセス)

Chromium: docs/process_model_and_site_isolation.md / Firefox: Process Model(Fission)
ブラウザ(parent)プロセスUI・タブ・ナビゲーション・Cookie の持ち主。子プロセスの IPC を検査する
描画プロセスChromium: renderer(1 サイト = scheme + eTLD+1 に固定)/ Firefox: content(webIsolated=サイト)。Blink / Gecko と JS エンジンはここ
通信プロセスChromium: Network Service(落ちたら再起動)/ Firefox: socket プロセス
GPU プロセスChromium: Viz が合成 / Firefox: WebRender が描く
  • ・描画プロセスはサンドボックスの中で、ソケットもファイルも直接は触れない。だから『通信』は別プロセスへの依頼になる
  • ・Firefox にはさらに RDD(動画の復号)、GMP、拡張機能用などの子プロセスがある

この実装の main.rs が 1 つでやっていることを、本物は『信頼できる度合い』で切り分けています。壊れた HTML や悪意ある JS を読むのは一番権限の低いプロセスです。

手元で動かす(配布物)

図やコードに出てくる用語(タップで説明)

公式ドキュメントで詳しく ↗