steps to phantasien

炭坑の庭師

image

Chromium と WebKit は二つの独立したプロジェクトだ。 ソースツリーはそれぞれ別で、そこにはインテグレーションの苦労がある。 WebKit 以外にも V8 や Skia など Chromium が依存している外部のプロジェクトは山ほどあるけれど, WebKit とは特にぴったりくっついている。 そのぶん二つの足並みをあわせる手間も際立つ。

以前、書籍 ”アジャイル開発の本質とスケールアップ” で リリーストレイン という大規模プロジェクトのインテグレーション手法を読んだ。 プロジェクトの内部で一段細かい時限リリースを設け、そのタイミングでインテグレーションする方法。 内部リリースにあわせてプロジェクト同士が依存している相手のバージョンを上げ、 壊れたところをなおすわけ。

Chromium と WebKit もこまめに相手のバージョンを新しくする。 主たる依存の向きは Chromium -> WebKit だから、 Chromium の依存する WebKit のバージョンアップは特に気を使う。 足並みをあわせる間隔はとても短い。 Chromium は一日に何度も WebKit のバージョンを上げている。 このこまめな同期によってビッグバンインテグレーションの不確定性から逃れている。 (なおリリーストレインと違い, Chromium の依存プロジェクトはそれぞれ勝手にバージョンをあげる。 依存関係のグラフが Chromium に集中しているので、 全体の足並みを揃えなくてもなんとかなるのだろう。)

リリース「トレイン」に各駅で待ち合わせる暗喩があるとしたら、 Chromium-WebKit 間の同期は並走している列車の間を飛び移る感じ。 アクロバティックでかっこよさげに見えなくもないけれど、 Chromium も WebKit も一日のコミット数が 100 から 200 はある高速鉄道。 スタントプレイの苦労も多い。今日はその様子を紹介してみたい。

Gardener と Sheriff

Chromium WebKit チームには Gardener と呼ばれる当番制の仕事がある。 だいたい 2-3 ヶ月に一度は割り当てが回ってきて、数日のあいだ gardening に精を出す。

Gardener の仕事は Chromium が依存する WebKit のバージョンを上げること。 Chromium のツリーには DEPS というファイルがあり、依存するライブラリのリビジョンを指定している。 Ruby の Gemfile, Maven の POM みたいなもの。 DEPS は Chromium 製の depot_tools によって解釈される。WebKit のリビジョンもこの DEPS に指定されている。

一日に数回、gardener はこの DEPS ファイルに書かれた WebKit のバージョンを上げてチェックインする。この作業を Roll という。 Roll していいのはビルドがうまくいく green なリビジョンだけ。仮にまずいリビジョンを roll して Chromium のビルドを壊すと、あっという間にパッチは revert されてしまう。

Gardener とは別に、Chromium には Sheriff という当番がある。 うっかりな gardener がやらかした不味い DEPS の更新を revert するのは sheriff の仕事だ。

Chromium の ビルドサーバは様々な構成 でビルド(=コンパイルとテスト)を動かしており、 sheriff はこれら一群のビルドを見張っている。そしてビルドの失敗をみつけたらすかさずツリーを close し、原因を調べて手を打つ。 ツリーの close はビルドシステムが管理しているフラグで、フラグが close されている間はツールがチェックインを受け付けなくなる。 問題が解決すると sheriff はフラグを取り下げてツリーを open する。こうして sheriff はツリーの健康を守っている。

組織によっては sheriff のことを Build cop と呼ぶこともあるらしい。

Canary

build.webkit.org

Chromium Sheriff が見張っているビルドサーバでは、Chromium の最新リビジョンを WebKit の DEPS 指定リビジョンと一緒にビルドしている。 厳しい close ポリシーのおかげでビルドたちはおおむねいつも緑を保っている。

それとは別に、Chromium の最新リビジョンと WebKit の最新リビジョンを組み合わせたビルドサーバ群がある。 これは Canary と呼ばれている。Chromium Sheriff と違い、WebKit Gardener は canary を気にかける。

WebKit は Chromium と違って開発者の所属が様々なため、 特定ブラウザチームの close ポリシーを強制することはできない。おかげでよく炎上して赤くなる。 Chromium WebKit のカナリアは、煙たちこめるこのビルドの炭坑に飛んでいく。 Gardener はその行方に目を凝らし、炎の覆う炭坑の森を抜け生き延びた小鳥の知らせる緑を掴もうとする。

…ただ現実にはいくら待っていても安全な green リビジョンはやってこない。 降り注ぐチェックインがビルドに引火するのはよくあること。カナリアは炎に巻かれ次々と死んでいく。 Gardener は真っ赤に燃え上がるツリーをどうにかして鎮火し、 緑を取り戻さなければならない。 ツリーの greenness をめぐるこのたたかいが gardening の山場となる。

作戦 1: Revert

赤く燃えるツリーを緑に戻すにはどうしたらいいだろう。 一番手っ取り早いのは revert。問題のあったチェックインを取り消せばいい。

これは一番正しい方法でもある。実際 Chromium 本体では壊れたパッチがあるとすぐに revert される。 WebKit も基本的には同じ合意があるはずなのだが、実際はもう少しゆるやかに運用されている。 よくあるパターンは、自分のいるポート以外の人がビルドを壊してしまうケース。 たとえば Mac 版 WebKit の面倒をみている人々のチェックインが Chromium のビルドを壊してしまう。 ポート間の設定の違いなどから、こういうビルドエラーはよく起こる。

全ポートのビルドを燃やすうかつな変更なら気兼ねなく revert できる。 ただ別のポートではビルドできるのに Chromium だけ引火することもあって、そんな変更を revert するのはやや胃が痛い。 簡単なビルドエラーなら直してしまった方が波風が立たなかろうと黙って修正することはよくある。 (実際のところ Chromium チームのチェックインが他ポートのビルドを燃やしてしまうこともある。それもこっそり直しておく。)

コンパイルエラーは大抵簡単に直せるからいい。厄介なのは、 特定の環境 (たとえば Mac) では動くのに別の環境 (たとえば Windows 版 Chromium) ではテストに失敗するようなもの。 こういうエラーを直すには実環境でデバッグするのが正攻法だけれど、 Linux や Mac で仕事をしている人に Windwos でデバッグしろといってもムリがある。 仕方ないので次に説明する「マーキング」でしのぐ。

ただ深刻そうな失敗(たとえばクラッシュ)はマーキングで先送りするのも気が引ける。 しかし同業他社のパッチを revert するのも胃が痛い。revert の閾値は gardener の肝っ玉できまる。

ある夜そんな場面で困り果てた肝の座らない私は、助けを求めるメールを書き逃げるように帰宅した。 すると夜中のうちに、ある強気な同僚(太平洋時間勤務)が問題のチェックインを revert していた。 そして問題を追跡する Bugzilla では元のパッチを書いた同業他社のエンジニアが苦情を申し立てている。 いたた・・・

すかさず「すまんかったクラッシュを直してからチェックインしなおすので見逃してください」コメントし、 当番があけた翌週に後始末をしてチェックインした。 このときは結局 Chromium のテストドライバにバグがあり、 そのせいでテストがクラッシュしていたのだった。やれやれ・・・

Commit queue

image

厳しく管理されたプロジェクトではそもそもビルドの通らない変更がチェックインされることはない。 かわりにビルドを確認してからチェックインする仕組みを持つ。 Chromium では commit queue という仕組で事前のビルドを確認している。 Commit queue をバイパスしてチェックインすることもできるものの、行儀の悪い行いとされている。 バイパスチェックインがビルドを壊すとだいぶ怒られる。(経験者は語る・・・)

WebKit のような雑居所帯には commit queue をはじめとするインフラを保守するチームがない。 レビュー済パッチのチェックインを自動化する仕組みや、 事前に様々なポートのビルドを確認する仕組みもあるにはある。一部のタフな開発者が兼業で保守している。 ただ参加者によって計算資源や開発チームの規模、開発プロセスなどがまちまちのため、 Chromium 本体ほどインフラやプロセスの足並みを揃えることはできない。 結果としてどうしても荒れ模様になりがち。

こう書くと Chromium が一方的に迷惑を被っているみたいだけれど現実にはお互い様で、 Chromium 由来の変更が他ポートのビルドを壊すこともある。

作戦 2: マーキング

さて、諸事情により revert できないテストの失敗に出くわしてしまった。 そんな時は「このテストは失敗する」と マークをつける ことになっている。 テストドライバがマークの指定を読み取り、そのテストは失敗しても無視される。

とはいえ片端から失敗をマークしていったらテストの意味がない。 マーキングはテストの網羅率を下げてしまう。 マーキングは新しいバグが煙幕に隠れないよう既存の炎に蓋をする暫定的なしくみ。 マークしてもバグが消えるわけではないから、さっさと直さないといけない。 だからマークをつける gardener は同時に新しいバグを登録して「このテストが失敗する」と関係者に伝える。

マークの一覧は WebKit のポート単位で用意されている。 Chromium WebKit の gardener は Chromium WebKit のマーク一覧を保守する。 Chromium チームはマークされたテストの件数を追跡しており、増え始めるとチーム内に注意報が発令される。 バグ修正強化期間がはじまることもある。

作戦 3: Rebaselining

テストが失敗しても、必ずしもバグがあるとは限らない。テストの期待値が間違っていることもある。

WebKit のテストの中には、入力の HTML をレンダリングして結果を期待画像と比べる仕組みがある。 同様に、レイアウト結果のテキスト表現をダンプし、そのテキスト表現を期待値と比べるものもある。 WebKit ではもともとはこれらの結果比較方式が主だったテスト方法だった。 今でもレイアウトや描画に関係するテストはこの形式で書かれている。

結果比較方式のテストは書きやすさに優れるものの欠点も多い。 数ある欠点の中でも、結果の期待値画像が WebKit のポート毎に異なる点が gardener にとって一番困る。 たとえば GTK 版 WebKit の開発者が新しくテストを追加して、ついでに GTK 版の期待画像を追加したとしよう。 あるいは既存のテスト結果を訂正したとしよう。 フォントや描画ライブラリの違いから Chromium と GTK WebKit では結果画像が違う。 けれど GTK WebKit の開発者は Chromium 用の期待画像を持ってない。 結果として Chromium WebKit では新しいテストが失敗する。 Chromium は Windows, Linux, Mac でそれぞれ結果が違うから尚更ややこしい。

こうした新しいテスト、結果の変わったテストに対して、gardener は Chromium 用期待値画像を追加/更新しなければいけない。 テスト自体はサーバで実行されていているので、テストの実行が終わった頃合いを見て生成された新しい結果画像をダウンロードすればいい。 ダウンロードした新しい結果が正しいことを確認し、既存の値を上書きする。 この期待値(baseline)更新作業を Rebaselining という。 WebKit プロジェクト謹製のスクリプトが一連の Rebaseline 作業を助けてくれる。

ある結果に不一致があったとき、それが予期されたのものなのかバグなのかわからないことも多い。 ちょっとだけ結果画像にズレがあるとき、それはフォントの扱いが変わった副作用なのか。テストサーバの個体差が生んだノイズなのか。バグなのか。 判断に悩んだときはバグとしてマーキングした上で詳しそうな人に伝え、判断を任せる。

ノイズだと思って画像を rebaseline したら実はバグだった、なんてこともたまにある。油断できない。 Chromium WebKit が使っている描画ライブラリ Skia の roll (バージョンアップ) に際しては gardener から悲鳴があがる。 前バージョンからラスタライズのアルゴリズムが変わると、大量の結果画像が軒並み食い違いはじめるからだ。 多くの画像は機械的に更新して問題ない。でも Skia 自体の新しいバグもあるかもしれない。 バグと期待動作をより分けつつ rebaseline をするのはさぞかし骨の折れる仕事だろう。 幸い私は自分の当番中に Skia の大変更に鉢合わせしたことはない。

Reftests

このように画像比較ベースのテストは変化に弱く保守が大変なので、 一年ほど前に Reftests と呼ばれる新しいタイプのテストがサポートされた。 Reftests ではテストの期待値に画像ではなく、別の(より単純な)HTMLを使うことができる。 たとえば CSS のセレクタをテストしたいとする。 このとき入力には新しいセレクタを使った HTML を、期待値にはインラインの style 属性で装飾した HTML を使えば良い。 Reftests はプラットホームや描画ライブラリによる差が小さいぶん保守がずっとラク。 願わくば広く受け入れられて欲しい。使う人と使わない人がいるのよなー。

Reftests はもともと Mozilla が使っている仕組みで、 W3C 製のテストスイートにも使っているものがある。 Reftests にはこうしたサードパーティのテストを持ち込める利点もある。

作戦 4: 再実行

テスト自体の出来が悪いときも、まぎらわしいビルド失敗をひきおこす。出来の悪いテストはたまに失敗する。 たまに失敗するテストを Chromium では Flaky なテストという。 (xUnit Test Patterns では “Erratic” と呼ばれていた。) どう見ても変更と無関係なテストが失敗していたら、それは単に flaky なだけかもしれない。 そういう時はテストをもう一回実行してみる…といってもどのみち新しいチェックインがあればテストが動く。 しばらくそれを待ってみる。

gardener が見張っているテストのうち、WebKit の LayoutTests は特に flaky なものが多い。 そのためテストドライバ自体が再実行の機能を備えている。テストの flaky ぶりを判断するツールとして、 過去の失敗履歴を記録した Flakiness dashboard なんてのもある。

Flaky なテストは本来なんとかして直すべきだし直されることもあるのだが、 なぜかわからないけど flaky なテストというのも多く頭が痛い。

芝生の消防士

image

この仕事、 gardener というからには芝刈りとゴミ拾いくらいの苦労で済んで欲しい。 余裕があれば垣根を刈ってもいい。 平和な日は・・・半分以上は平和な日々だとおもう・・・実際そんなかんじで何事もなく終わる。

ただ荒れ模様の日もある。 出勤する。ビルドを一覧できる canary の dashboard を開く。赤い。ツリーが燃えている。 芝刈りの庭仕事が炭坑の火消しに一変する。そんな日の gardener には素早い判断が求められる。

炎上の原因はなんだろう。 単に baseline が古いだけなのか、flaky なのか。本当のエラーなのか。 エラーなら原因となったリビジョンはどれか。そのリビジョンは revert してよさそうか。 ツールのリンクをたどって失敗のログを読み、原因を調べる。 画像をじっと睨む。 rebaseline してよさそうだとあたりをつける。

しかし Windows 上でのテストが終わっておらず必要な期待値画像が出揃っていないのに気づく。 dashboard を眺めて Windows ビルドの進み具合をみる・・・とコンパイルに失敗している。 おおそりゃテストされないわけだよ。じっとログを睨んで修正箇所を特定、直してチェックイン。 やれやれ・・・とおもったら IRC でよそのポートの人から 「きみんとこの変更でうちのビルドが壊れてるんだけど?」などとつつかれる。 ごめんなさいと今度は別のログを睨む。あるチェックインが WebKit2 を見落としており、修正漏れがあったようだ。直してチェックイン。

とかいってるうちに今度は Mac 版 Chromium で火の手が上がり始める。 テストがクラッシュしているらしい。 しかし Mac 版のビルドは二時間ほど続いていたコンパイルエラーから立ち直ったばかり。 その間のテストが動いていない。しかもよりによって大規模なチェックインが続いている。どのチェックインが問題を起こしたのだろう。 Blamelist(容疑者名簿)に目をやると、激しい炎上を起こすと有名なあのひとの名前が・・・! これなのか?でもこないだ濡れ衣 revert しちゃったばかりなんだよなあ・・・。

などとばたばたしている間に日が暮れる。 私は要領の良さと縁のない人間なので、同時並行しておこるトラブルを次々にやっつけることができない。 結局一度も roll できず、太平洋時間の当番に「ごめんムリだった・・・」と引き継ぐこともある。 そんな日はややへこむ。

苦痛の分配

Gardening は楽しい仕事ではないけれど、 これを下っ端や専業に押し付けず当番制にしたのは正しい判断だと思う。 まず雑なチェックインがどれだけプロジェクトの健康を損ねるのかを体感できる。 ツールやインフラの不備も見える。腕のいいエンジニアの中には、 当番の間にみつけたツールの不備をいつも手直ししている人もいる。 メーリングリストでもたびたび gardening の苦行ぶりは話題にあがり、 そのたびに少しずつプロセスやツールがよくなっていく。 私が入社した頃はビルドサーバから期待値画像をダウンロードする仕組みもなかったし、 revert を自動化するツールもなかった。苦痛を分配することで、 苦痛を減らすアイデアやボランティアを引き出すことができたのだろう。

もう一つ面白いのは誰も同期の間隔を広げてラクしようと言い出さないところで、 ここはやはり always on the tip of tree の会社なのだと実感する。

そういえば検索の会社では、 サーバサイドのプロジェクトでもチームによってはリリースが当番制だと聞いた。 似たような動機からきた制度だろうし、もしかすると当番制は単一トランク主義のように文化の一部なのかもしれない。 さすがにでかいやつには専任がいるだろうけれど・・・

私はむかしリリースやビルドの保守を押し付けたり押し付けられたりして苦労した記憶がある。 ローテーションにしておけばよかったのかもしれないと今は思う。 煤まみれの当番開けにぼんやりそんなことを考えていたら、カナリアが六桁の数字を告げた。


画像: