1%
Google の指標によれば、Chrome は平均すれば速いものの、ときに極端に遅くなることもあります。そのようなユーザーの苦しみは、多くの指標の 99 パーセンタイルを見ればわかりますが、再現できないため、かなり対策が難しい問題です。データを詳しく分析すると、パフォーマンスのロングテールを経験しているのは遅いマシンを使っている 1% のユーザーではなく、多くのユーザーが 1% の確率で経験していることがわかります。
その 1% について考えてみましょう。1% といっても、実際はかなりの数になります。ここで使う中核的な指標は「ジャンク」です。これは、ユーザーが入力してからソフトウェアが反応するまでに明らかな遅延がある状態を指します。Chrome は 30 秒ごとにジャンクを測定します。そのため、あるユーザーの 1% のジャンクのサンプルを集めれば、それは 50 分ごとに 1 回起きているジャンクということになります。そのユーザーはその瞬間、Chrome が遅いと感じます。問題は、ユーザーの環境で Chrome が瞬間的に遅くなることの根本原因を突き止めて修正できるのか、ということです。
アプローチ
私たちエンジニアが習ってきた最適化とは、自分が所有するコンポーネントのアルゴリズムのパフォーマンスを改善することです。しかし、この 3 年間、複雑な Chrome のコードベースを分析してきたことで、実際の問題には複数分野にまたがる原因があることがわかってきました。つまり、関連性のない複数の機能に関するパフォーマンスのロングテール問題には、全体的な共通の根本原因があるということです。局所的な専門性や最適化を適用すると、全体的な最適解が見落とされてしまう可能性が高くなります。最初の直感を捨て、何の前提も設けず、すぐにわかることのさらに奥を探り、わからないことを徹底的に洗い出して、根本原因を見つけなくてはなりません。
見えないバグを追いかける
予測できず、再現性がなく、自分のものではなく、実質的に見ることができないバグを見つけるには、どうすればいいのでしょうか。
まずは、シナリオを決めることです。そのために、ユーザーが認識できるジャンクに注目します。そして Chrome が遅いと感じる瞬間をシステム的に突き止める方法として、ジャンクを
実際の環境で測定します。
次に、実用性の高いバグレポートを実際の環境で集めます。そのために、Chrome の
BackgroundTracing インフラストラクチャを使って Slow Report と呼ぶものを生成しました。匿名で指標を共有することに同意した一部の Canary ユーザーで、特定のシナリオを調査できる循環バッファ トレースを有効にします。すると、注目する指標があらかじめ設定されたしきい値に達したときに、トレース バッファが取得され、匿名化が行われて Google のサーバーにアップロードされます。
このバグレポートは、次のようなものです。
|
通常は健全なマシンで、AutocompleteController::UpdateResult() の 2 秒のジャンクを chrome://tracing で表示したもの |
犯人がわかりました。AutocompleteController を最適化すればいいのですね。いや、違います。まだ理由がわかっていないのです。何の前提も設けないようにしましょう。
BackgroundTracing をスタック サンプルで補足すると、ストールした AutoComplete イベント内で繰り返し起こっているスタックを見つけることができました。
RegEnumValueW
RegEnumValueWStub
base::win::RegistryValueIterator::Read()
gfx::`anonymous namespace\'::CachedFontLinkSettings::GetLinkedFonts
gfx::internal::LinkedFontsIterator::GetLinkedFonts()
gfx::internal::LinkedFontsIterator::NextFont(gfx::Font *)
gfx::GetFallbackFonts(gfx::Font const &)
gfx::RenderTextHarfBuzz::ShapeRuns(...)
gfx::RenderTextHarfBuzz::ItemizeAndShapeText(...)
gfx::RenderTextHarfBuzz::EnsureLayoutRunList()
gfx::RenderTextHarfBuzz::EnsureLayout()
gfx::RenderTextHarfBuzz::GetStringSizeF()
gfx::RenderTextHarfBuzz::GetStringSize()
OmniboxTextView::CalculatePreferredSize()
OmniboxTextView::ReapplyStyling()
OmniboxTextView::SetText...)
OmniboxResultView::Invalidate()
OmniboxResultView::SetMatch(AutocompleteMatch const &)
OmniboxPopupContentsView::UpdatePopupAppearance()
OmniboxPopupModel::OnResultChanged()
OmniboxEditModel::OnCurrentMatchChanged()
OmniboxController::OnResultChanged(bool)
AutocompleteController::UpdateResult(bool,bool)
AutocompleteController::Start(AutocompleteInput const &)
(...)
なるほど。オートコンプリートが悪いわけではありません。GetFallbackFonts() を最適化すればいいのですね。でも、待ってください。そもそも、いったいどういうわけで GetFallbackFonts() が呼ばれているのでしょうか。
それを突き止める前に、どうすればこれがパフォーマンスのロングテール問題全体の一番の根本原因だとわかるのでしょうか。とにかく、まだ 1 つのトレースしか見ていないのです ...
測定における難問
指標からは、どのくらいのユーザーが影響を受けているか、どの程度悪い状態なのかはわかります。しかし、根本原因がわかるわけではありません。
Slow Report からは、特定のユーザーの問題はわかりますが、どのくらい多くのユーザーが影響を受けているかはわかりません。また、Slow Report トレースのコーパスを検索することはできますが、これには本質的にバイアスがかかっているので、指標と 1 対 1 で対応することは不可能です。たとえば、Chrome はセッション 1 つにつきパフォーマンス悪化の最初の事例だけをレポートし、対象も Canary/Dev チャンネルのユーザーだけなので、起動と母集団の両方のバイアスがかかっています。
これは測定における難問です。ツールが提供するデータの実用性が高いほど、取得できるシナリオは少なくなり、強いバイアスがかかるようになります。深さをとるか、広さをとるかです。
両方を行おうとするツールはその中間にあたります。その場合、大きなデータセットを集計するので、欠陥のある入力に基づく結果を集計してしまうというリスクがあります(たとえば、注目したい部分が循環バッファ トレースから欠落しており、バイアスがかかった集計になるなど)。
そこで、科学的理論に基づき、最もエンジニアリング的でない選択肢を選びました。つまり、大量の Slow Report のトレースを手動で開くという方法です。これは、すでに定量化できている最重要な問題に対して、最も効果的な手法になりました。
たくさんのトレースを開いた結果、そのほとんどに、なんらかの形で前述のフォントの問題が現れていることがわかりました。影響を受けた厳密なユーザー数はわかりませんが、指標に現れていたユーザーの苦しみの主な原因はこれだと確信するには十分でした。
フォールバック フォント
そもそも GetFallbackFonts() が呼ばれる理由は何なのかを追求しました。先ほどの例の呼び出し元は、あるフォントでレンダリングされる Unicode 文字列のピクセル数を求めようとしていました。
その中のサブ文字列に、指定されたフォントではレンダリングできない
Unicode ブロック内の文字がある場合、システムが推奨するフォールバック フォントをリクエストするため、GetFallbackFont() が使われます。それに失敗すると、
リンクされているフォントをすべて試してレンダリングに最適なものを決めるため GetFallbackFonts() が呼び出されます。この 2 回目のフォールバックは遅くなります。
GetFallbackFont() が失敗することはないはずですが、実際はそこまで単純ではありません。Windows でこれを確実に行う方法は、
DirectWrite に照会することです。しかし、DirectWrite は Chrome がまだ Windows XP をサポートしていたころの Windows 7 で追加されたものでした。そのため、両方のバージョンの OS で動作するように、GetFallbackFont() のロジックで確実性が低い
試行錯誤的な Uniscribe+GDI を利用せざるを得ませんでした。それでもほとんどの場合はうまく動作したので、のちに Chrome で Windows XP のサポートが削除されたときも、この処理をクリーンアップできることに誰も気づきませんでした。パフォーマンスのロングテールを調査する新しいツールを使うことで、ジャンクの一番の原因(GetFallbackFonts() の不要な呼び出し)が明らかになったのです。
Google はこれを
修正し、GetFallbackFonts() の呼び出し回数を 4 分の 1 に削減しました。
まだゼロではないので、前述の AutoComplete の問題は引き続き Slow Report で確認できます。そのため、調査を続けましょう。DirectWrite の GetFallbackFont() の失敗は予期しないものでしたが、Slow Report は匿名化されているので、ユーザーが生成した文字列はアップロードできません。そのため、どのコードポイントが問題を起こしているのかを突き止めるのは難題です。そこでプライバシーのエキスパートとも相談し、
個人を特定できる情報が漏洩しないように、Unicode ブロックとテキスト ブロックのスクリプトを
HarfBuzz に通すことにしました。
絵文字の物語
この新しい記録が利用できるようになるとともに、Slow Report の次の波がやってきました。大半のレポートでは、DirectWrite に
Miscellaneous Symbols and Pictographs(その他の記号とピクトグラフ)内のコードポイント(Unicode 文字)のフォントを見つけるようリクエストしたときに、フォントのフォールバックが失敗していました。ローカルでその Unicode ブロックのすべてのコードポイントを試すスクリプトを書いたところ、問題を起こしていたのは何かがすぐにわかりました。U+1F3FB~U+1F3FF は、Unicode 8.0 で追加された修飾子で、別のコードポイントと組み合わせたときのみ意味を持ちます。たとえば、U+1F9D7(🧗)と U+1F3FF を組み合わせると 🧗🏿 となります。U+1F3FF 自体をレンダリングできるフォントはありません。そのため、フォントのフォールバックに正しいフォントを見つけるよう依頼しても、すべてのリンクされているフォントを調べた後にエラーになるのは正しい動作です。グラフこれはブラウザ側の Unicode
セグメンテーション ロジックのバグでした。バグによって 2 つの
コードポイントが誤って分割されるため、1 つの書記素としてではなく、別々にレンダリングするように DirectWrite にリクエストしていました。
でも、待ってください。Chrome は最新の Unicode をサポートしているのではなかったでしょうか。確かに、ウェブ コンテンツをレンダリングする Blink はサポートしています。しかし、ブラウザ側のロジックは、絵文字を描画することはないので、最新の絵文字(修飾子付きのもの)をサポートするように更新されてはいませんでした。ブラウザの UI(タブバー、ブックマーク バー、アドレスバーなど)が最新化され、Unicode をサポートするようになったのは、2018 年ごろになってからのことです。そのときから、以前のセグメンテーション ロジックが(見えない)問題になっていました。
そのうえ、キャッシュ ロジックはエラー時にキャッシュを行わないようになっていたので、たくさんのフォントがインストールされたユーザーでは、修飾子を自力でレンダリングしようとするたびに大きなジャンクが起きていました。皮肉なことに、このキャッシュは、ブラウザ UI に初めて Unicode サポートが追加されたとき、誤解されたボトルネックに対処するために追加されたものでした。フォント API のレイヤーでとどまるのではなく、フォントのロジックについて下層の実装に迫り続けたことが、主要なパフォーマンスの問題の修正だけでなく、他の
絵文字に関する修正にもつながりました。たとえば、🏳️🌈 をコードで表すと、U+1F3F3(🏳️)+U+1F308(🌈)となります。分割ロジックを修正するまで、この書記素はブラウザの UI で 🏳️🌈 と誤ってレンダリングされていました。
そして旅は続く …
Google の旅は、さまざまな Chrome のコンポーネントに迫り続けています。しかしそれは、いつも同じ基本戦術に従っています。それは、何の前提も設けないようにして、予想できず、再現できず、自分のものでもないバグを徹底的に追求することです。スタック ランキングの問題は不可能に近いですが(参照 : 測定の難題)、なんらかのツールで見つけたトップ 5 の問題を修正し、ロングテールに注目すれば、実際のユーザーの苦しみの大半に対処できることになります。
Google はこのアプローチによって、ここ 2 年半の間でユーザーの目に見えるジャンクを 10 分の 1 に減らし、狙いを定めた多くの機能でパフォーマンスのロングテールを改善しました。
|
30 秒間のサンプルにおいて 100 ミリ秒間隔で無応答になった数の 99 パーセンタイル |
すべての統計情報の出典 : Chrome クライアントから匿名で集計した実データ。
Reviewed by
Eiji Kitamura - Developer Relations Team