高速化日記 (8) - Load Average and Jiffies

とある機能 F を有効にするととある操作 T のあとでコマ落ちするという、ユーザ(上司)の手元では再現するが手元での再現率がいまいちなバグを直そうと苦戦しつつはや数週間。

ハイレベルには、F を有効にした状態で操作 T をトリガーすると瞬間的に CPU の負荷が跳ね上がり、画像パイプラインのクリティカルパスにある HAL API である A のレイテンシがフレームレートを超えてしまい、コマ落ちに至る。

A のレイテンシは普段は余裕で予算に収まるのだが、他のタスクが増えると CPU のサイクルを奪われて遅くなる。症状から A の重要性は明らかなので A が動くスレッドを SCHED_FIFO にしましょうと主張してパッチを書いたが、OS のひとにインパクトでかすぎるのでダメと押し返されうーむとなる。

なお HAL の一部では SCHED_FIFO はぼちぼち使われている...まあ「ぼちぼち」は overstatement かもしれないですね。はい。従ってこの問題は priority inheritance で解決されるのが筋だが、俺のせいじゃねー的 API のせいでその仕組みは使えないのだった。


それはさておき操作 T 発動時の CPU 負荷は機能 F の有無に関わらずどのみち高く、全てのコアが振り切る。機能 F のアルゴリズムが高価なのは事実だろうが、ほんとにコマ落ちの有無を決めるような規模のインパクトがあるのだろうか。こういう雑な仮説を盲目的に信じては痛い目に逢い続けて数年、いちおう検証しておこうと思い立つ。

タスクが多すぎてタイムスライスがもらえないとか、ロードアベレージみたいなのをみればいいんだっけ?と検索すると Brendan Gregg のブログがヒット。要約すると Linux の load average の数字は微妙なので他の指標を使っときなよとのこと。他の指標としては BPF が使えるよ、とかいうが Android でそれは大変。別の候補として紹介されている  /proc/schedstat ファイルを polling してみることにする。

ドキュメントおよびインターネットによると同ファイル内で "sum of all time spent waiting to run by tasks on this processor (in jiffies)" という数字を監視すればよいようだが、Jiffies ってなんやねん。その manpage およびインターネットによるとスケジューラのクロック、たとえば 100Hz だったら 10ms とからしい。なるほど・・・と値を眺めるが、全然合ってない。桁がいくつもちがう。最初は自分の雑なコードが計算ミスをしてるのかと思ったがそんなこともなさそうな雰囲気。はーなんだこりゃ・・・と落胆して帰宅。

翌日気を取り直してカーネルのコードを睨むと、数字の出どころは sched_clock() という関数らしい。実装が複数あるのでよくわからないが、ドキュメントは単位をナノ秒と主張している。 10ms と 1ns じゃだいぶ違うわけだわ・・・。ナノ秒として計算してみるとそれっぽい数字になる。やれやれ。カーネルのドキュメントも案外信用できない。


さてこうして計算したオレオレロードアベレージを定期的に ATrace_setCounter() しつつアプリをつついてトレースを眺めたところ・・・機能 F が有効だろうが無効だろうがロードに全然違いがない。

冷静になってトレースを見直すと機能 F はスレッドが一つ増えはするものの大した並列度はなく、そのスレッドも GPU に計算を投げたり他のスレッドの計算結果を待ったりで必ずしも全力では回っていない。仮説敗れる。はー・・・。

ではなぜ機能 F はコマ落ちを招くのだろうか。ユーザ(上司)の思い込み?でも自分の手元で問題がおきるときも機能 F の有無は影響ある気がするのだよなあ。


そして書いていて気づいたがトレースには sched の tracepoint が含まれているのだからカーネルが集めた stats を使うより自分で事後的に計算した方が色々わかることが多いきがする。たとえばカーネルの stats ではレイテンシの分布はわからない。ただその計算をするにはもうちょっとカーネル詳しくならんとムリだな。はー・・・。