Spinach Forest

#ANDROID

/ Link: Block ads in apps on Android (including video ads) - unlike kinds   / Memory Allocation onDraw()   / google/mobly: E2E test framework for tests with complex environment requirements.   / Link: Androidにおけるパフォーマンスチューニング実践 - Speaker Deck   / Link: Profilo · An Android performance library   / FBJNI   / Building The OS   / android_library   / logd   / Understanding Android Surface   / Performance Team, SRE and Android as Distributed Systems   / Bugrepot and Thread Dump   / Big.Little Sucks (or Not)   / 草の根ツール   / Thread Priorities   / On a Layout   / UI/Application Exerciser Monkey  |  Android Developers   / Link: Android Developers Blog: Introducing Android 9 Pie   / A Case of How Open-Source Works at Mobile   / NDK, pthread and Systrace   / AS As APK Browser   / Snapdragon Profiler   / Gralloc, Treble and ION   / Dogfooding   / Multi-threading   / On Device Tracing   / Cold Start and Busy Start on Android   / binder_driver   / Systrace Buffer size   / Why Static Analysis Sucks   / Services on Android   / Android Notification API   / Cache vs. Sync (Is a False Conflict)   / AutoFactory   / Architecture Components   / Kotlin and Conceptual Backporting   / Android Rendering Profiler   / What Was DI, or What Is It, Still?   / How WebView Sucks   / Users of an Orphan App   / MTTF and Apps   / ... 

Link: Block ads in apps on Android (including video ads) - unlike kinds

via Block ads in apps on Android (including video ads) - unlike kinds

フーンどうやるのかな、とおもってコードを覗こうとしたが GPL3 だったので諦め。周辺情報を総合すると VPN として振る舞うらしい。

これを使えば /etc/hosts の代替品となるのではなかろうか。でも VPN ってことはアプリをぜんぶのパケットが通ってくのかな。それは雑に作ると性能でなそうだが・・・。

そしてなぜか app store でなく f-droid  で配布してるけれど、Ad-blocker は Playstore の規約に触れるのかな。まあ触れてもおかしくないな。自分用なら adb install すればよさそうではある。

サンプルの ToyVPN を見ると・・・ IP packet がストリームとしてやってくるのか。厳しい。一方で VpnService.Builder をみると DNS を指定できる。

なんとなくインプロセスの DNS を実装してそいつが /etc/hosts 的に twitter.com などをブロックし、IP パケットは素通しする、というような実装ができると一番簡単に思えるがどうなのだろうな。素通しは必要なシステムコールを特定すればなんとかなるとして、インプロセスの DNS は手強い・・・。なお ToyVPN は linux の tunneling の仕組みに丸投げする実装になっている。

DNS はインプロセスじゃなくてどっかに自分で立てる、という方向性もありうるか。どのみちそこまでがんばって実装する気にはならないなあ・・・。

ここまで。Android の VPN の API を眺めることができたのでよしとする。


更にスレを読み進めると、Android P はユーザが DNS サーバを上書きできるのか。(Cloudflare の説明) これでやるのが一番簡単そうだなあ。しかし DNS サーバを運用するのみならず DNS-over-TLS をサポートしたやつを使うとか大変そうすぎる・・・。誰かそういうサーバ立ててくれないかなー・・・。

Memory Allocation onDraw()

|

Android の昔からある高速化 tips というか性能アンチパターンとして onDraw() でのメモリアロケーションが挙げられているがホンマかいなと思う・・・

・・・と書こうとして証言を探したが、公式ドキュメントは割とまともだった (1, 2). 特に後者は "Object allocation and garbage collection (GC) have become significantly less of an issue since ART was introduced ... Note that significant amounts of allocation can mean more CPU resources spent on GC" とページを締めくくっており、これがまさに自分の言いたかったことなのだよね。ついでにいうと onDraw() は別に毎フレーム呼ばれねーよ、というのがある。

なぜこの話を書こうとおもったのか忘れたけれど、古い知識で性能しったかぶりするのはよくないから実際に測ってからなんか言おうね、といういつもの話でした。まあ GC の影響は測るのが難しいから公式ドキュメントが cargo cult busting してくれてるのは良いことである。Android の公式資料, たまにちゃんと書かれてるね。

google/mobly: E2E test framework for tests with complex environment requirements.

via google/mobly: E2E test framework for tests with complex environment requirements.

最近性能テストの自動化にこれを使っている。まあまあ良い。良さの一部は lab の実機で CI するインフラを誰かが用意してくれているから、というツール自体とは無関係なものだけれど、テストコードが host 側で動き、しかもそれが Python だというのも良い。

原則としてこうした E2E のテストは保守性の点で望ましくないことが多く最小限にすべきなのだが、性能のベンチマークばかりは実機でやりたいし、環境をいじる必要もあるのでホスト側の adb で色々やりたい。そして自分の関心は特定のコードの挙動でなくシステムとしての遅さなのだった。

Mobly をランダムな他人に進めるかというと・・・どうだろうね。ちょっと大げさすぎる気はする。しばらくは plain python でがんばってみて、辛くなったら調べてみるくらいでいいのではないかな。ホスト側でテストを動かすというコアのアイデアが重要だと思う。


それはさておき仕事で python を書くのは新しく学ぶことが多くて楽しい。文法とかは今更特に学ぶところはないが、社内インフラをはじめ色々なライブラリを使い、他人のコードレビューを通せる程度にはゴミでないコードを書くと、余暇プロジェクトや書き捨てのスクリプトとは違う仕事の手触りがある。

 

Link: Androidにおけるパフォーマンスチューニング実践 - Speaker Deck

via Androidにおけるパフォーマンスチューニング実践 - Speaker Deck

世間の期待値的には性能の話がこれでいいのか・・・別にリンク先のひとを腐したいわけではなく、2019 年にこんな話なのか・・・と割と心の底からびっくりしてしまったのでひっそり記録したかったという話。なお別にこういう話をしているからその人たちのアプリが遅いといいたいわけではない。地味にコツコツやればそこそこ速くなるジャンルはたくさんあるから。それでいいならそれでよいのです。


つい Twitter に余計なことを書いてしまったが完全に言いがかりだったと反省。

講演きいてないのでわからないけれど、リンク先の資料に問題があるとすれば計測して、詳しく見て問題を特定して、それを速くする、というサイクルが感じられないところかな。高速化、そのサイクルが一番下にないとカーゴカルト黒魔術みたいのになりがち。


追記

スライドの人が dex.fm に出ていたことに気づいたので聴いてみた。ちゃんと仕事してる人だった。我ながら言いがかりよくないわ。

dex.fm — 064: Performance Tuning

こういう人の話を聴いてみると、自分のやってる仕事は単に職場のインフラを exploit しているだけだなあと思う。たとえばベンチマークの自動化にしても lab なり dashboarding なり scheduled execution なりの仕組みはもうあるので、それを寄せ集めれば良い。

というか自分はそれすらできず、半年くらい前にやってきた別の人が別の目的で寄せ集めた自分のチーム向けのセットアップに相乗りしている。一番大変な connecting pieces 作業はそのひとが済ませてくれたので、自分はその上でちょっとコードを書けば良い。

自分の作業はアプリ側からメトリクスを expose する側に寄っているのでちょっと守備範囲が違うとも言えるが、そのアプリ側の仕事にしても昔だれかがやったまま放置していたコードを整理復旧して使い物にしただけで、特にすごく何かをがんばったわけではない。まあこれは前のチームで割とがんばった部分なのでやればできるとは思うが・・・。

こうした仕事に価値がないとは思わない。実際自動化によってカバレッジがあがり、早い段階で性能バグを発見できるようになったわけだから。ただまあ、リンク先の人のような全方位的活躍とは程遠い。

他人の仕事を寄せ集めるだけで成果がでるのも、他人の仕事を寄せ集めるのが大変なのも、大企業であることよ。最近やってきたできるエンジニアは「俺はコードは書かない。他人のコードをコピペするだけだ」と冗談交じりに言っていて、そういう面なあるなと思う。コピペ元コードの出処が社内コードサーチか SO かの違いで世間のプログラマも割とそんなもんなのではという気もする。はさておき。

部品を寄せ集める大変さを考えると大企業で社内インフラに乗るのだろうがスタートアップでオープンソースに乗るのだろうが大差なくね、というのは常々感じているオープンソースもっと使わせろという不満につながるわけだが、自動化のインフラはコードだけでなくサービスの operation も必要な類なのでまあ仕方ないかなと思う。過剰な大変さはなんとかしてほしい気もする一方、サーバ側と比べた時の社内のモバイル回りの筋の悪さには諦めもある。

Link: Profilo · An Android performance library

via Profilo · An Android performance library

コードを眺めているが・・・。すごいなこれ。

すごいところを列挙するのはあとでやるとして、Android の C++ レイヤの練度で見ると Facebook は自分の勤務先を含む多くの会社を圧倒している気がする。

たとえば Chrome なんかは単純に C++ のコードベースとして見れば割と比類ない練度だと思うけれども、基本的にはデスクトップアプリだから Android らしさというのはない。Facebook から出てくるライブラリの C++ は Android らしさを感じる。しかも Android が好きで良さを引き出そうというよりは、Android の弱点にくまなく付け込んで攻略するという方向性。これは自分の好みに近い

自分の勤務先は根本的にはウェブの会社で、モバイル対応はなんとなく場当たり的に行われた。上の方は mobile first と旗を振ったけれど、なにをすべきなのかは誰もよくわかっていなかった。各チームがそれぞれ勝手に色々やっていたため、全社共通の基盤みたいのの整備が遅れた。さすがに今はちゃんとしてきたけれども、インフラから整然と作っていったサーバ側とはだいぶ趣が違う。そしてアプリインフラ的なレイヤを主導しているのが Java の人なせいか C++ レイヤは未だに荒野感がある。まあ Android の低レベルの仕事をしたいプログラマの多くはアプリじゃなくて OS やると思うんだよね。アプリとして C++ 書きたい人、そんなにいない気がする。

この話とはちょっと矛盾するけれども、初期にモバイル対応 (Android アプリ開発) を主導した現場の人の多くは Android OS 出身者で、自分のアプリのために Android のギリギリを攻めて行こうというよりは Android ecosystem の良き市民であろうという意識が強かった。だから platform の .so を dlopen() してシンボルをぶっこぬく、とか多分やらない。Java レイヤの hidden API を reflection でよぶくらいがせいぜい。自分たちのチームにしても、OS やデバイスがやるべきことは自分で妙なハックをしたりせず OS やデバイスの人に頼む。(自分はそれがあまり好きでないが、妥当だとは思っている。)

ふりかえって Facebook を見るに、彼らの mobile first はなにをすべきかはっきりしていた。Facebook アプリに全部の機能を詰め込んでちゃんと動くようにすればよい。そう決断したタイミングがすごく早かったとは思わないけれども、とにかくチームが、そしてコードベースが一つのアプリを中心に編成された。だからライブラリなどのインフラも大規模コードベースらしく整備されているし、よく書けてる。

しかも低レベルでいろいろやりたいプログラマに OS をやるという選択肢がない。彼らはそのかわりにアプリの中で色々無茶なハックをしてウサを晴らしたのだろう。OS のコードをコピーし、アプリ固有の事情にあわせツールチェインを魔改造し・・・といった具合に。

まあこれらは完全な想像だけれども、Profilo および依存先 C++ のコードには大量につっこまれたエンジニアリング資源の匂いがあるね。Android がアプリレイヤでどこまで無茶できるかを体験するには面白いところなのだろうな Facebook. まあコード読んで勉強させていただきますわ。

FBJNI

|

Facebook が公開した Spectrum というモバイル画像ライブラリのコードを眺めていたら FBJNI というライブラリが含まれていた。Spectrum のみならず FB の JNI 依存プロジェクトには軒並み含まれているので、社内ライブラリのスナップショットをコピーしてるのだろうな。

コンパイル時プログラミングで JNI の method signature string を生成してくれるのが素晴らしい。独立したライブラリに切り出してくれないかなあ。

Building The OS

|

年末でコードレビューを頼む相手もおらず暇なのと、去年は OS のバグに苦しめられたのとで、手元で OS をビルドできるようにしておこう計画を進めてみる。すなわち repo init, repo sync して本体をビルドしてみる。

でかい。社内ビルドなのでアプリも全部入っている。それだけでなく、ビルドをしていると中間生成物でディスク残量がみるみる減っていく。複数チェックアウトを手元にもつのは厳しい感じのサイズ。Chrome のときもでかいと思ったけど、比べ物にならないね。コードの複雑さ以前に物理的なバイト数で。このクソでかいのにたいした専用インフラなしにやってる OS の一味やばいと思わざるをえない。せめて Ninja になってたのはよかったね。

ワークステーションは、最近 Mac Pro もびっくりの業務用超速くてデカくてうるさいやつにアップデートしてもらったのでディスクサイズを別にするとびくともしない。ビルドもファンがうるさいが速い。すごい。ただしアプリ開発には overkill だな。

そしてビルドとインストールはできたけど仮にまたバグがあったとしてデバッグできる気が全くしない・・・。

android_library

|

via Android Rules - Bazel

慣れ以外で Bazel を使うそれなりにマシな理由としては native code  が Android Studio 推奨の方法よりラクそう、というのがある。一方でその他のツーリングとかは標準 Android の方が良い気もするので、NDK まわりだけ Bazel を使えないかと考えてみる。

で android_library. 期待どおり AAR を出してくれる。つまり .so を含めることができる。Bazel で C++ と周辺の Java を書いて AAR にして、そっから先は好きにする、という方法はあるかもなあ。

そのうちためそう。

logd

|

Android の logging suppression ("chatty" とマークされるやつ),  M だか N だかくらいで入った logd という daemon に実装されているらしい。それまではカーネルのバッファに直行していたのが logd を経由するようになった。

しかしひどいコードだなー特に驚くことではないとはいえ・・・。しかも std::list とかマジかよ。もうちょっとがんばってメモリアロケーションとか控えめにしてくれよ。それに色々コピーしすぎだよ・・・バッファに直に read してくれよ・・・。あなたけっこう CPU 使ってらっしゃいますのよ?

そして普通にコードきたないというかなんというか、泣ける。Pruning のロジックは LogBuffer::log あたりだけどもう興味なくなったのでどうでもいいです・・・。

コードを読むとき、自分はコードにこめられた人類の叡智のようなものを期待しているのだなあと思う。Android は読んでも大概疲弊するだけでつらい。

Understanding Android Surface

|

以下の話題についてより包括的な理解をしたい人は Graphics architecture を参照されたし。


Surface というクラスがある。この名前は完全に間違っていて、実際は ImageQueue みたいな名前だと思えばよい。ここでいう Image は、たとえば GPU が処理したりできる、ART の外で確保された、プロセスを跨いでやり取りされる画像のための共有メモリである。ではなぜ Surface という名前なのか、と考えるのは時間の無駄なうえに精神衛生を危険に晒すので、十分な資産を形成し引退するまではやめておいた方がよい。

Producer / Consumer

Android のサブシステムには画像 (Image) を生成するコンポーネントと Image を消費する(受け取って処理する)コンポーネントがある。

Image を生成する Producer side には以下のようなものがある:

  • MediaCodec: 圧縮された動画ファイルをデコードし、動画のフレーム画像を Image として produce する。
  • Camera: カメラがセンサーから吸い上げた画像をproduce する。動画として次々と生成することも、写真の撮影リクエストごとにひとつの image を生成することもできる。
  • OpenGL: eglSwapBuffers() のタイミングで GL の描画結果 Image をproduce する。

これら producer side のサブコンポーネントは結果の画像を書き込む ImageQueue すなわち Surface を受け取る。例: MediaCodec#configure()

画像を受け取って処理する consumer side には以下のようなものがある:

  • ふたたび MediaCodec. 流れ込んできた画像列を動画として圧縮する。
  • SurfaceView: 流れ込んできた画像を View の一部として画面に表示する。
  • SurfaceTexture: 流れ込んできた画像を OpenGL の texture に割り当てる。

これら consumer side コンポーネントは、画像を受け取るための ImageQueue すなわち Surface を公開している。例: SurfaceView#getHolder().getSurface(). SurfaceTexture はちらっとドキュメントを眺めただけだと Surface を取り出せないようにみえるが、Surface のコンストラクタが SurfaceTexture を引数にうけとるという形で公開している。なぜそんな API デザインなのかは、やはり考えるだけ時間の無駄である(e.g. 単にゴミなだけ。)

Operating On Pixels

Surface の上を流れる画像にアプリケーションからアクセス、加工する代表的な手段は OpenGL である。すなわち画像を SurfaceTexture 経由で GL texture にマップし、GL でレンダリングがてら pixel shader などでなんかする。

GPU でなく CPU で処理したい人には ImageReader というクラスが用意されている。これは ImageQueue すなわち Surface の consumer side コンポーネントの一つ。そのコンポーネントが自ら画像をなんかするかわりに、Java のレイヤに Image というオブジェクトをとりだす API がある。この Image オブジェクトを使うと ByteBuffer ごしに画像のピクセルを読み書きできる。

CPU で処理したいユースケースの代表格はファイルへの書き出し。たとえばカメラの画像を JPEG で保存するとか。

ImageReader で読み出した画像は用が済んだら close() のうえ捨ててもいいし、ImageWriter オブジェクトを使って他の ImagQueue すなわち Surface にそのイメージを enqueue することもできる。ImageWriter は ImageReader の対, producer side のクラスである。

Image#close() するのはかったるいが、Image の実体は Java heap でなく希少な共有メモリ・そのうえサイズがでかいので、こまめに開放しないとすぐ枯渇してしまう。やむなし。

SurfaceView

SurfaceView は ImageQueue/Surface の consumer である。ではどうやってその image を consume し、画面に表示するのだろうか。

すべての Activity は最低一つの SurfaceView を持っている。以下ではこれを Root SurfaceView と呼ぶ。Root SurfaceView には持ち主のアプリが描画される。アプリが描画した画面の Image を実際に消化するのは SurfaceFlinger と呼ばれるプロセスである。これは複数のアプリの描画結果をくみあわせ最終的な画面を描画するプロセスで、他の OS だとWindow Manager とか Window System などと呼ばれるものの機能の一部である。SurfaceFinger は最終的な画面を OpenGL で描くこともあるし、可能ならより省電力で 2D 専用の composition hardware を使う。

つまりアプリの描画結果はアプリ自体のプロセスから ImageQueue/Surface を介して SurfaceFlinger に送られる。SurfaceView は画像だけでなく画像の表示場所や大きさすなわちレイアウト結果も SurfaceFlinger に伝える。(こうした位置情報の伝達は Surface と独立した API/Binder をたどる。) 

Root SurfaceView ではなく、アプリケーションが確保した SurfaceView も基本的には同じように振る舞う。すなわち、受け取った画像はレイアウト結果ともども SurfaceFlinger に送られる。

別の言い方をすると、アプリは SurfaceView をレイアウトはすれど描画はしない。たとえば OpenGLで SurfaceView に何か書き込んでもアプリ自身の描画結果には反映されない。Android が内部で確保したアプリの Root SurfaceView とアプリのコードが確保した SurfaceView はそれぞれ独立に描画され、それぞれの Image を SurfaceFlinger に送る。SurfaceFlinger はそれを重ね合わせ、辻褄のあった最終的な画面を描画する。

プロセスをまたぎ最終的な画面画像の合成プロセス (composition という) に介入する荒々しさを理解していないと, SurfaceView はすごく奇妙な振る舞いをするように見える。そういう意味で SurfaceView という名前は misleading とは言わないまでももうちょっとそういう事情を反映した名前のほうがよかった。たとえば CompositionView とか。

ところで Android は Camera や VideoCodec にも独立したプロセスがある。こうしたコンポーネントはそのプロセス内で Image を生成し、ImageQueue/Surface を介してアプリに届ける。しかし SurfaceView の実際の consumer は SurfaceFlinger プロセスなので、VideoCodec や Camera を SurfaceView に繋ぐと Image はアプリのプロセスをかすりもせず SurfaceFlinger に届く。なのでアプリの UI thread が多少もたついてもビューファインダやビデオはコマ落ちせずに再生される。この低遅延な振る舞いは SurfaceView の大きな利点である。

TextureView

TextureView は SurfaceView と同じく Surface すなわち ImageQueue を介して画像を受け取る consumer である。しかし SurfaceView と異なり、TextureView は実際にアプリのプロセスが描画する。

各アプリには RenderThread  と呼ばれるスレッドがあり、このスレッドは UI thread の作ったディスプレイリストを OpenGL で描画する。 TextureView は RenderThread に SurfaceTexture を作って画像を受けとり、RenderThread が描画する画面の一部としてそのテクスチャを描画する。これは LAYER_TYPE_HARDWARE を指定した View の振る舞いとよく似ているが、RenderThread がテクスチャの中身を描くかわりに Surface の向こうの producer からテクスチャの中身が届く点が異なる。

TextureView は普通の View と同じく RenderThread で描画されるため、レイアウトとかでややこしいことにならない。だから扱いやすい。一方で一回余計な GL の描画を挟むぶん 1 フレーム遅延があるし、電力効率も悪いし、UI thread がもたつくと巻き込まれる。利便性と性能をトレードオフしている。

TextureView も名前が悪いね。SurfaceView とは逆に実装の詳細を晒しすぎている。LayerView とか呼べばいいのに。

Pipeline

どこかの producer から 受け取った Image のストリームをどのような方法で画面に描くか、画面に書く前の加工をどうするか。Surface を中心とした一連の画像処理を pipeline と呼ぶことがある。Pipeline のデザインはメディア系アプリの性能や安定性に大きく影響する。同時に HAL や OS の都合で思わぬ制限も多い。実験しつつ良いデザインを探求する価値がある。が、世の中は割とコピペと cargo cult に支配されがちに見える。ここに書いたシステムのデザインの意図みたいのは理解しておくと少しマシな判断ができる、かもしれない。(できないかもしれない。)

Performance Team, SRE and Android as Distributed Systems

|

Android には Performance Team というのがあって、「なんか遅い」みたいなバグに対してアプリ開発者が音を上げると登場して triage して責任者を突き止め、場合によっては自分で OS を直して帰っていく。最初は懐疑的だった自分もなかなかの仕事ぶりに評価を改め、いまは頼りになる人たちだと思っている。サーバ側でいうところの SRE みたいなもの、というとわかりやすい。

なんでクライアントサイドに SRE (的な仕事)がいるのかというと、結局システムとしての Android には分散システム的な側面があるからだと思う。Binder は RPC みたいなもんでたまに congestion を起こすし、ハードウェアの故障はともかくプロセスは LMK で死ぬ。CPU もメモリもいつも足りておらずプロセスは資源を奪い合っている。本物の分散システムとの大きな違いの一つは network partition がないところだけど、OS 本体はともかくアプリから見ると offline 状態というのは network partition みたいなもので、それなりに graceful に扱わないといけない。

そしてなんか電話機/アプリが遅いというとき、それはしばしばシステムとしての問題である。すなわち overload や contention のような system-wide で inter-component な問題が遅さを引き起こしている。もちろんアプリの出来が悪いだけということも多いけれども。

というわけでクライアントサイド OS の開発組織に SRE じゃなかった Performance Team があり、そうした system-wide の性能について日々考えているというのは、まあまあ理にかなっている。


クライアントサイドの性能問題は、いくつかの点でサーバサイドより簡単である。

まずなんだかんだで本当の分散システムではない: 多くの問題は単一デバイスに閉じている。だから理論上 OS は system-global view を持っている。もちろん production には millions of devices があってそいつらには long tail の問題があるわけだが、少なくともデバイス同士が干渉しあうことは、そんなにない。(クライアントであるデバイスが送るリクエストがサーバ側のレイテンシを生み出すことはよくあるが、ちょっと脇においておく。)

あと、突然の高負荷でシステム全体がダウン、みたいなことも起こりにくい。なのでふつう pager で呼ばれたりはしない。まあサーバサイドで flag flip をしたら突然アプリがバタバタと死にはじめることはあり、そういうときはアプリの人も呼び出されるわけだけれども、flag flip は昼間やってちょうだいね、ということで。

一方、サーバサイドにない難しさもある。

まずなんといってもモニタリングがヘボい。これは半分は本質的な問題で、半分は特定の実装のできの悪さだと思う。

本質的な問題。モバイルデバイス、とにかく計算資源がない。CPU 遅い、メモリ少ない、ストレージ少ない、ネットワーク帯域細い。なんでとりあえず Prometheus の agent をインストールしましょう、というわけにはいかない。モニタリングをするにあたってもかなりケチくさくやらざるをえない。

実装のへぼさ。 つまり、そうはいってももうちょっとなんかないの?というね・・・。Performance Team もなんらかの clue がないとできることは限られてしまうし、ましてアプリ開発者(自分)はもうわけわかんないログ睨むの疲れたよ・・・。ただ最近は Perfetto というプロジェクトでそのへんをなんとかしようとしているらしいので、首を長くして待っている。

クライアントサイド固有の難しさそのに。Production の制限の多さ。要するに我々は SRE と違って世の中の電話機に adb したりできないのである。いや、できたら困るんですよ。わかってますよ。でも出来なくて困ることもあるんだよ!Systrace できないじゃん!みたいな話です。

まあ Lambda や App Engine とかも開発者はコンテナにアクセスできないので似たような面はある。すると app-level の instrumentation がいよいよ重要になってくるため、結局は計算資源がボトルネックといえる。

クライアントサイド固有の難しさそのさん、といえるかどうかはわからないが違うところとして、ロードが burst 的というのがある。だから広い sliding window で time series の log をとる、とかやると重要な情報すなわち burst/spike を見逃してしまう。これは実際に時系列のデータを見て意見してるわけではないので間違いかもしれないが・・・。

スマホ、大半の時間は寝て過ごしている。画面をつついている時間も大多数は平和なものだ。アプリを起動する瞬間、データをロードする瞬間、カメラのシャッターを切る瞬間、なんらかのデータを submit する瞬間、モードを切り替える瞬間など、ほんのゼロコンマ数秒から数秒の間に起こる色々な細部が重要で、その数秒に問題があるとアプリが固まったり描画が乱れたりする。これはたくさんのクライアントから並列でリクエストが飛んできて結果として全体のマクロな負荷は平滑化されて見えるサーバの皆さんの典型的なロード特性とは異なる。

Burst 的、別の言い方をするとロードが時系列方向に heterogeneous である、と言えるかもしれない。なんでサーバサイドと同じようなモニタリングしてもダメだと思うのだよな。ではどうするか、は、アプリ次第だけれども、system-wide でいうと AcitivityManager や LMK の活動は重要な指標になりうる。Historian はそのへんよくできており、こいつやるな、と思う(エラそう)。

他の切り口でいうと、homogeneous な負荷の世界ではスタックトレースのサンプルをソートした framegraph が重要で, 負荷の heterogeneous な世界では普通の時系列 trace が重要。クライアントサイドでも、たとえば IPC のスループットをなんとかしたい、みたいな話になると tracing より framegraph が役に立つ。むかし Chrome の IPC 載せ替えを手伝っていたときは tracing より perf と framegraph が友達だった。

とにかく Android アプリは app-level および system-wide 双方が structured  で resource efficient な continuous かつ in-production の instrumentation をがんばらないといけない、ということです。もっとかっこいいツールとかダッシュボードとかみながらタタターンってかんじで性能改善したいじゃん。させろ!

 

Bugrepot and Thread Dump

|

手元では再現しないが厄介なバグを割り振られ、ここ一週間くらいじっとログやコードを睨んでみたり再現を試みたりしていたがあまり打ち手がなく、仕方ないので問題が起きた時にそれを検知してこっそりアプリを殺す、という回避策コードをコードレビューにだしたところ「これほんとに治るの?それとも speculative fix なの?」というので「speculative だし fix ですらない partial mitigation ですよたぶん dead lock かなんかっぽいんだけど・・・」と返事。すると「Dead lock なら再現したときに bugreport とると thread dump が入るからわかるよ」と指摘される。なんだって!Bugreport なら最初にもらったやつが一個だけあるよ!さっそくログをにらみ直すとたしかに最後の方に thread dump が。そしてたしかに dead lock (じゃないんだけど似たような事態) がおきている!

普段はそのへんのノイズをフィルタし表示してくれるビューアを使っていたのでまったく気づいてなかった。この一週間はなんだったのか・・・といっても dead lock ぽいなと気づいたのは数日前。この数日はなんだったのか・・・。

適当な修正を書き、問題の箇所の持ち主にレビューを出す。そしてあちこちたらい回された結果、どうもその問題はちょっと前に依存関係の奥の方で修正されていたとわかる。そりゃ再現しないわけだわ・・・。

というわけで自分の書いたコードたちはチェックインされることなくバグがなくなり、めでたく締め切り前のノルマ完了。まあそれらのコードは会話のきっかけみたいなもんだったのでどうでもいいんだけど、ストレスがしんどい一週間だった・・・。

Bugreport に thread dump が入っているのを知らなかったのは残念だったが、それ以前に自分はほんとにログを読むのが下手だなあと思う。あとからみると、たしかに根本にあった問題を匂わせる出力はログに含まれていた。しかしノイズをかき分けてそれに気づくだけの才覚がなかった。

しょうじきこの「Bugreport のログから色々読み取る」というスキルはレガシーかつドメイン固有な上に持つと色々嫌なものを引き寄せてしまう気がするのであまり上達したいとも思わないのだが、一方で性能改善の仕事をしているとログを相手にトラブルシュートしなければいけないことは多い。生き延びるためにある程度は鍛えないといけないのだろうなあ。やだやだ・・・。

この「手元では再現しないがおかしい」という類のバグは、特に性能がらみでよくおこる(「なんか遅かった」的なやつ。) On-device tracing があるとだいぶ違うが大抵 trace なんてとってくれないので bugreport の限られた情報から憶測するしかない。多くの場合はしばらく睨んだ末に「わからん。降参」となって終わり。この triage 作業は今の仕事で一番イヤな部分と言えよう。やりたくないけど、性能仕事をしている限りは逃げられない。厳しい。色々対策は考えてるけど、どうなるかね。

Big.Little Sucks (or Not)

|

ARM の Big.Little いまいちじゃね?と思うのだが世間の人々はどう評価しているのだろうな。

Pixel2 とかは Big x4, Little x4 の CPU が載っている。仕事でやっている CPU intensive なアプリだと、なにかと処理を並列化していくのでけっこう CPU は使い切る、のはピークの瞬間だけだけれどもたとえばアプリ起動時で 4 コアくらいは使い切る。

そういう高負荷時の Systrace をみると, Big コアのロードが真っ先に埋まっている。そこから更に負荷が上がると残りの Little コアも埋まっていく。この挙動が妥当かどうかはさておき、アプリは自分のスレッドがどのコアに割り振られるかはわからないので性能を予想しづらい。スレッドの特定のコアを指定する API も少なくとも Java レイヤにはない。ベンチマークのスコアもいまいち安定しない。(なので言語処理系とかで本気の厳密なベンチマークをしたい人たちは遅いコアを全部殺したうえで計測したりする。ついでに thermal governor も殺してクロックも固定する。電話機が熱で溶けないのか心配。)

たとえば Halide みたいな data parallel なコードが「よし 8 コアあるぞ!」とスレッドを 8 個起動したとする。しかしそれらの並列処理は速度が揃わない。他プロセスの影響だの cache coherency だの言い出すまでもなく CPU が違うから。そりゃタスク粒度をあげて細かく worker に振り分けていけば unevenness は隠せるし、そうでなくても速い CPU が空き次第スケジューラが遅い CPU からスレッドを migrate してくれればいいっちゃいいのだが・・・それよか 8 個の並列処理がだいたい同じ速さで終わると思ってfork-join をかける方が簡単じゃね?

そしてアプリのスレッドを速いコアから順に割り振るのはいいのか?省電力とかいう話はどこいった?画面が消えているときはモードが変わって速いコアを止めているのだろうか。

など色々自明でないことが多すぎて、アプリプログラマには手に負えない。それよか速いコアだけ 6 個、とかにした方がよかったんじゃないのかなあ。遅いコア2つ速いコアを1つ買えるという計算があってるのかはわからないが・・・。 LWN の記事を読んでも辛いという気持ちしにかならない。

最初の記事では Big と Little の CPU を排他的に使う (Big だけで動くモードと Little だけで動くモードをスイッチする) ということが書いてあり、マジかと思う。まあこれは 2012 年に書かれた記事で自分の Systrace などの観察とは一致していないので、少なくともいまは違うのだろう。

Apple はどうしてるのかと Wikipedia を眺めていたら、どうも彼らの CPU も Big.Little 類似のアーキテクチャを採用しており、しかも A10 くらいまでは LWN にあるような Big/Little 排他動作のアプローチだったらしい。まじか。でもたしかに iPhone のコア数って 2-3 くらいと伝え聞いていたので、整合性はある。そして A11 からはでかいのと小さいのを同時に使えるようになったと書いてある。そりゃ iPhone X 速くなるわけだわ。

むー。Big.Little は失敗におわった実験かと思ってたけど、全部自前でやりたい放題かつ実際にベンチマークでは Snapdragon 勢に圧勝している Apple の CPU が同じことをしているとなると、自分の脊髄反射とは裏腹にそれなりに成功したアーキテクチャーなのかなあ。ぜんぜん納得行かないが・・・。

探すと関連論文ちょこちょこあるね。ちらっと読んでみようかなあ。

 

草の根ツール

|

Battery Historian (GitHub) というツールがある。Android の bugreport  dump を時系列に可視化してくれる。名前からもわかるようにバッテリーの浪費をチェックするためのツールだが、結果としてシステムの挙動を longitudinal かつ holistic に眺める役に立つ。Systrace のように細かいことはわからないが、そのぶん広い時間の幅をカバーしてくれる良さがある。なんだかわからない性能バグがやってきたら、とりあえず軽く Historian をチェックする。

このツール何がすごいかというと、必要な情報はぜんぶ bugreport dump から集めているところだよな。

Android の bugreport dump はすごいアドホックな非構造テキストデータの塊で、基本的には logcat  バッファの中身と dumpsys にはじまるいくつかの inspection 系コマンドの実行結果が詰まっている。ないよりはマシだが、しょうじき大したデータではない。Historian はそのしょぼいデータをまあまあいい感じに可視化し、なんらかの知見を引き出している。もちろん中の人が作っているので Historian 自体の必要に応じ OS が収集しているデータもあるだろうが (batterystats とか), それにしてもこのしょぼい中がんばったなと感心する。

常識的に考えたら Android がやるべきことはシステムの活発度に応じ粒度を変えながら iostats やら vmstats 的なデータを構造化した状態で time series 用 ring buffer のストレージにダンプし続け, かつそのストレージを様々な subsystem から書き込めるよう公開し、システム全体の時系列のロードを常時収集のうえ bugreport に含めることである。ぜんぶ proto なサーバ側を見よ。

しかし Android にそうした大局的なまともさは期待できない。(いいかげんやってもバチはあたらないとおもうが。) かわりに正しい問題を見定めたビジョンと実力のある現場のエンジニアが手持ちの時間の中でギリギリできることをやる。その結果が Historian なのだろう。(きっと。)これは Android 的だなと思う。Systrace も似たような雰囲気あるでしょ。

こうしたツールやシステムに対する自分の気分は複雑。ギリギリじゃなくてちゃんとまともにやれと思う一方、まともにやろうと人を集めて作ると大げさで使えないゴミの負債を生んでしまうのではという恐怖心もある。オープンソースな世界だと市場原理でいいやつが残る期待があるけれど、企業のコードベースというのは計画経済で、かつオープンなプラットフォームだと一度入れてしまったものはゴミでもなかなか切り捨てられない (例: renderscript)。あんまりムリに背伸びはしないでくれよな、などと思ってしまう。我ながら上から目線な上に余計なお世話にもほどがありますが・・・。

Android に限らず、しょぼいが便利な内製、製品固有のツールやライブラリが本腰を入れたプロジェクトとして書き直されゴミ化する様を何度も見てきた。そのせいで警戒心がある。しょぼいツールに資源が投入された結果よくなった例もきっとあるのだろうけれど。

個人的な意見としては、問題意識のあるプログラマがヒマを見つけて気の利いたツールの萌芽を作り、それに気づいた周りのプログラマが手を貸し育てていく、というパターンが内製ツール成功の定石。Android のひとたちにはきっとヒマが足りてない。これも余計なお世話だが。

プログラマは適度にヒマにしておくのがよい。ヒマの大半は無駄に使われるだろうけれど、時々生まれる成果はトップダウンには置き換えられないものだから、それに免じて見逃してちょいだい。と思う。そういう意味で優秀なプログラマほどヒマなほうが良い。

Thread Priorities

|

Android では UI thread に高いプライオリティが割り振られているという。それは Systrace を睨めばわかるが、ふと思い立って実際にどのくらいなのか調べてみた。この SO の記事を真似してよく知っているアプリすなわち仕事アプリのスレッドを眺めてみる。

  • UI Thread と Render Thread は同じくらい高い pri がある: 29
  • HwBinder のスレッドは更に高い: 31
  • ランダムなワーカーの皆様は 19
  • Thread#setPriority で釣り上げを試みたと思しき奴らは 21. つまり Java の API をいじっている範囲では UI Thread には勝てない。setpriority() とか使うと変えられるのだろうか。常識的に考えるとダメそうだが。
  • ART の GC 用スレッドは低め: 15

最終的に UI thread をブロックする処理は下手に worker に逃がすより UI thread でやってしまったほうが良い場合があるとわかる。まあ CPU は1個だけじゃないし他プロセスとの兼ね合いもあるので一概には言えないけれど。

ps の出力には PRI の他に NICE もあるが、上下関係は PRI に準じている。違いが気になるけど調べるのはまた今度。

 

On a Layout

|

仕事のアプリは諸事情により一部にカスタムなレイアウトのロジックを持っている。面倒そうなコードなのでなるべく近づかないようにしていたが、ちょっと手直しをする仕事が回ってきたので仕方なくじっと睨む。

これは書いた人がレイアウトというものをわかってないなあ・・・というコード。

べつにすごい深い知識を求めているのではなく、レイアウトというのはまず子の大きさを求めて次に位置を決めてあげる、という二段階構成が基本なんだよとか、ただし位置を決めるプロセスで制約を満たすために大きさを直したりすることもあるんだよ、だとか、縦に積むとか行を詰めてくとか代表的なパターンがいくつかあるんだよ、みたいな。Android の View にしても measure() と layout() の二段構成になっている。

そのカスタムレイアウトは諸事情により View のライフサイクルとは独立したコードになっていて、それはいいのだがレイアウトが二段構成だとか基本的なパターンがあるみたいな事実がコードにまったく反映されていない。厳しい。

しかも興味深いことに、コードの挙動をよく睨むと現状の仕様はそれほど厳しいものではなく、上に書いた conventional な感じに書き直すことができることに気づく。つまりコードが変なだけでカスタムとはいえすごく変な振る舞いをしているわけではない。

これ書いた人たちはどのくらいわかっているのだろうか・・・。たぶんすごく最初の素朴なバージョンはわかってる人がデザインして雑に書いたのを、他の人がいじるうちにわけわからなくなってしまった、とかなのだろうなあ。というわけで肝心の変更をするまえにクリーンな状態に書き直し。これでバグらず直せるぜ。


レイアウトの振る舞い自体にも奇妙なところがあるにはあるのだが、これは書き直しているとよく考えられた巧妙な仕組みであることに気づく。ダメなコードは振る舞い自体もダメなことが多いが、クリーンに書き直すことでコアのアイデアが浮かび上がってくることもたまにある。今回はそういうケース。歴代の UX のひとがよく考えた結果なのだろうな。

自分は UI のデザインをモック画像だけでコミュニケーションするのはよくないと感じている。その理由の一つがわかった。モック画像はコンパイルしたバイナリみたいなもので、UX person の考えた rationale が失われてしまう。Design Language みたいなテンプレ、モジュール化はそうした lost in translation を防ぐ試みとも解釈できる。けれどテンプレや再利用ではない、自分がいま直したコードのようにアプリケーションのユニークな振る舞いを捉えることはできない。

どうすべきか。ひとつには UX person との対話を通じて相手の意図をコードに落としてあげる、つまりモックの一方通行ではなく相手と話をする、DDD みたいなやり方が良いのだろう。自分のチームは対話するところは割とできてる感じがあるけれど、今回直した部分についてはプログラマが意図をエンコードするのに失敗していた。しかもそのために必要なのは readable code 的な hygine skill ではなく UI レイアウトというドメインの知識だった。

という上から目線の話を書きたかったわけではなく、コードはともかくレイアウトの振る舞いがすごくよく考えられていて UX 氏やるな、とおもったという話を書きたかったのだった。でも実例なしに書くのが難しく話がそれてしまったね。

UI/Application Exerciser Monkey  |  Android Developers

$ adb shell monkey -p your.package.name -v 500

via UI/Application Exerciser Monkey  |  Android Developers

あなたのコード、特定の条件下 (デバイス、OS など)で起動するとすぐ落ちますよ、ということを伝えるためのコマンド。再現手順を簡潔に伝えられて良い。

そんなクラッシュコードがチェックインされるとかどうなの・・・と思うかもしれないけれど依存関係の奥の方で変更があったりするとそういうこともあるのだよ。翌朝来たら誰かががんばって bisect して直していた。おつかれさまでした・・・。


追記

とりあえず何十回か回す one-liner を書いて動かすとなかなか楽しい。まあ monkey test ってのはもともとそういうものなわけだが。monkey 実行前の条件づくりをもうちょっとがんば良いテストになりそうだな。というかテストの人たちはそうやってるのであろうな。

Link: Android Developers Blog: Introducing Android 9 Pie

via Android Developers Blog: Introducing Android 9 Pie

出たらしい。しかし電話機付属ソフトウェア部門的には新しい電話機が出るまでが遠足なのだった。OS の人々は休暇に入ったりしており羨ましい・・・。

 

A Case of How Open-Source Works at Mobile

|

Link: Manu Sridharan - Challenges in Large-Scale Mobile App Performance - YouTube

Uber がモバイルの性能頑張ってるよ、という話。Android と iOS でひとつづつトピックを紹介している。

Android 側では nanoscope というプロファイリングを強化した ART のフォークを紹介し、iOS では Swift のコンパイラの最適化を upstream する話をしている。

なんちゅうか、皮肉な話だよなあ。

Android はだいたい全部オープンソースだが、コミュニティドリブンでない。年に一回変更がダンプされてくるだけなので、何かを upstream するのはすごく大変で一部のメーカーくらいしかやってない。iOS は Darwin くらいかオープンソースじゃなかったわけだが、あとから登場した Swift はびっくりするほどオープンにオープンソースしている。

結果として Uber は Android では成果をフォークをして iOS では upstream している。まあ Swift はコンパイラなので OS からは unbundle されている影響もあるかもだが・・・。実際 ART は AOSP をトランクとして作業してるらしいので年に一回ダンプするモデルではないが、まあ最新の Android でだけ修正されてしかも成果は来年までおあづけ、とかだと盛り上がらない。

最近 AndroidX が AOSP  ベースになったとアナウンスしていたけれど、どうなるかねえ・・・。


ところで Nanoscape を紹介する記事 Introducing Nanoscope: An Extremely Accurate Method Tracing Tool for Android で彼らが発見したとしている性能上の問題、ぜんぶ userdebug の Systrace みればわかるのだよね。おまえらカーネルいじる前にやることあんぞ。同時に我ながら Systrace おじさんすぎて苦笑。

 

NDK, pthread and Systrace

|

ART はロックの競合を Systrace に記録してくれため、どのスレッドがどのスレッドを待っているのかがひと目でわかる。これは素晴らしい機能で、マルチスレッドコードのクリティカルパス解読を助けてくれる。

自分が仕事でみているアプリは大量にネイティブコードがあるのだが、こいつらが pthread でロックを待つと Systrace 上にはただ "pthread の lock を待ってます" とだけ表示される。嬉しくない。もうちょっと詳しく教えてくれ・・・。

まあアプリのレイヤで使う pthread が不透明なのは自業自得といえなくもないが、platform のコードも同じ問題も持っているのでやはりお前らなんとかしろ!と思う。なんとかしていただきたいものです。

 

AS As APK Browser

|

とある APK の中身, 具体的には AndroidManifest.xml を覗きたいのだが、バイナリ XML をさっと読む良い方法がない。Apktool でもいいのだけれど、得体の知れない jar を業務用ワークステーションで実行するのもねえ・・・

という話をしたら、Android Studio で APK を開けば読めますよと教わる。どれどれ・・・と試す。おお、ほんとだ。これは便利だなー。挙動から判断するに APK Analyzer なるツールがもとになっているっぽい雰囲気。

Snapdragon Profiler

|

というものがあるので試してみたが・・・いまいちだった。

基本的には Systrace の UI を C# で書き直しました、みたいなアプリ。Linux では Mono/GTK# で動く。GPU まわりなど Systrace では取れないデータが若干とれるのが売り。しかし UI が厳しい・・・。ゲームとか GPU べったりなアプリを作ってる人ならこの UI に適応するのもアリかもしれないけれど、自分はそのシグナルを Systrace から読めるようなんとか upstream してくれや・・・とおもいつつ退散。

ただ Systrace と比べると UI が軽い気がする。Systrace, そろそろスケーラビリティの上限に達している気がしているのでなんとかしてほしいなあ。そこが extensible かつ素敵な感じになったら Qualcomm もこんな無駄再発明をせず plug-in してくれるかもしれないじゃん。

Snapdragon Profiler, どうやってデータ収集を実装されているのだろうなあ。特にカウンタ系, /proc なり /dev なりのファイルなどを polling しているだろうか。それとも ftrace に必要なデータは入っていて、それを Systrace が解釈できないだけなのかあ。

追記

Systrace ファイルでかすぎてもうダメぽ・・・と社内でつぶやいたところ、最新の Android Studio ならでかいトレースがとれると教わる。おお!と思ったら既存のダンプを開く方法はなさそうなので feature request しとくか、とおもってバグトラッカーをみたら "public に file してね" と書いてあったので public に file してみた.

更に、目先の逃げ道として適当に systrace の要らない行を削るのでもよいというので自分の見なそうな項目を削ったところ、ようやくブラウザが落ちなくなった. まったく...

Gralloc, Treble and ION

|

カメラの画像など Surface を介しプロセスをまたいで流れていくメモリ (buffer) は Gralloc というアロケータで確保される。Gralloc は HAL である。

ところで Android は O ではじまった Project Treble によって多くの HAL が独立したプロセスに追い出された。Binderization という。昔の Windows の COM-fication および Mozilla でおきた反動の De-com-ificaton を思い出すがまあそれはいい。

これが意味するところは Gralloc も binderized で別プロセスに追い出されたということである。マジで?とおもったけど Systrace を睨むとそれっぽいプロセスがいるのだよな実際。無駄にかっこいいなおい・・・。Image の確保開放、微妙に遅いと思っていたがこのせいという面は否めない。


Treble, 資料を眺めると色々思わぬことが書いてある。たとえば above HAL の binder とは別の "binder context" を使って congestion を回避しているという。そしてそのためにカーネルもちょっとかわっている。Systrace で "HwBinder" というトレースがところどころにあるのはなんだろうとおもっていたけれど、それが別コンテクストで動く HAL 用の binder なのだろう。

更に HAL の Binder は binder といいつつ IDL は AIDL variant の HIDL というフォーマットで、AIDL と違い C++ もサポートしている、というか C++ がメインらしい。君たちは IDL コンパイラ二種類サポートしてくのか。それより AIDL 直してくれや・・・。


ところで Gralloc の話にもどると、このひと HAL とはいえ結局中では何をしているのだろう。というと、多くの場合は ION に落ちるらしい。例:

ION は Android がドライバ実装のため Linux に入れた shared memory の仕組み。アプリのレイヤで使う ashmen とは違い、たとえば物理メモリ上で連続した領域を確保したい、みたいなことができるという。LWN にいくつか解説がある。

ION があるならもう Gralloc は HAL じゃなくて AOSP 側で持ち、そのレイヤから直に ION に行けばいいのでは・・・という気もするけれど、Gralloc は最初の頃からあるのに対し ION は GB あたりで入ったらしいので歴史的経緯なのかもしれない。あと上でリンクした Exynos のコードをみると実装固有のフラグを Gralloc のレイヤから ION の下のデバイス固有コードに渡すようなことをしているので、Gralloc のレイヤにフックがあるのに意味はある・・・ような気もするが、基本的には Gralloc の usecase を伝えているだけなのでそれが ION に入ってればよくね?とも思う。どうせ Android 用なわけだから。あとだしの感想ですが。


ION は複数種類のヒープをサポートしている(ion.h). Linux のカーネルが普通に管理している (物理的には page にわかれている) メモリからアロケートする SYSTEM ヒープ、カーネルの起動時に reserve しておく CARVEOUT ヒープ (物理メモリは自動的に continuous になる)、そして DMA ヒープがある。

CARVEOUT はお前は昔のゲーム機か、というかんじでゲンナリするけどまあそういうの必要なこともあるでしょう。 (共存している co-processor を動かす別の OS と連携するとか。) 感心したのは DMA ヒープで、こいつは Continuous Memory Allocator 略して CMA からメモリを確保する。

CMA は名前の通り continuous な物理メモリを確保できるのだが、CARVEOUT のような雑なことはせずカーネルがもっているメモリから必要に応じてアロケートできる。正しい。しかし誰得なのだろうなと思ったら、パッチを書いていたのは Samsung の人だった。エライ。Samsung, 伊達に Android 界の頂点に立ってないなと見直した。

そしてこういうヘンなやつらもちゃんと仮想メモリにマップしないといけない Linux の VMM は大変だね。


ところで Gralloc がメモリを ION から確保するということは、そいつらは userland から普通に map できるということである。つまり GPU に渡すメモリを CPU から触れるのでは? SGI unified memory architecture なのでは? とおもったら O から NDK に AHarewareBuffer という API がたされており、これがまさにそういうクラスっぽい。(そして古いバージョンでもplatform の中のコードを無理やり触っている人たちがいたらしい) べんり! まあ CPU 遅いのでできることそんなになさそうだけど。


なお ION は雑すぎて脆弱だよと主張する ION Hazard という論文がある。Android PoV から ION を理解するのにはこの中の説明が一番わかりやすくてよかった。

Dogfooding

|

ウェブの Dogfooding

ウェブ開発では dogfooding の重要性、というか特異性が際立たなくなったと感じていた。ただ今のチームはウェブ開発と違うところが多く、dogfooding を見直した。

ウェブ開発での dogfooding は簡単である。開発中のソフトウェアを配るのに特別な仕組みがいらず、適当にホストした dev.example.com みたいなバージョンにアクセスすればいいし、開発もインクリメンタルでリリースが頻繁なため開発版とはいえそれほど派手に壊れない。使いはじめるバーも、使い続けるバーも低い。

そしてユーザの行動履歴を集めて集計するのが簡単。なので自分で使って良し悪しを判断するよりたとえばベータ版やインクリメンタルなデプロイ, dark launch などを使い実際に撒いて試す方が良い部分が増えていく。エッジケースでのクラッシュでもユーザにちょっと配るだけですぐわかる。逆にテストについても、dogfood とか言う前に自動テストでカバーしといてくれや、という空気がある。

もちろん実際のユーザに配る時点でわかっても手遅れな問題は沢山あるし、自動テストでは捕獲できずデプロイしないと現れないバグも沢山ある。だから dogfood に意味はあるし、やったほうがよい。ただなんというか、騒ぐような特別なものではない感じがする。「ドッグフードしてます!」とか言われても、あっそう、くらいにしか思えなかった。自分は勤務先アカウントでさわる職場のウェブアプリはすべて dogfood 版なはずだが、だからどうという感じもしない。UI が刷新されたとき、たまにオッとなるくらい。

OS の Dogfooding

自分のチームが開発しているのは OS 付属品というか特定電話機付属のアプリで、いちおう Play Store 経由でアップデートできるがそれでも年に数回しかリリースしない。そして下で動いているOSは基本的に年に一回しかリリースしない。フォローアップのマイナーリリースが追加で一回くらいとセキュリティパッチはあるけれど、とにかくリリースが少ない。そして複雑な割に自動テストが手薄。これらせいだけではないけれど、リリースから遠く離れた時期の開発バージョン OS は割と派手に壊れる。

テストがしょぼいのはさておき、リリース頻度が低いのはスマホの OS という性質を考えると割と仕方なく、同情の余地はある。更に OS とアプリ群の interaction はアプリ単体とは桁違いに複雑なので、ちょっとプログラマが自動テストを追加したくらいでは全然カバーできない。

ちょっとではなく沢山自動テストするというアプローチはありうる。たぶん Windows のような歴史のある OS は色々とがんばっていることだろうし、他の OS も成熟の過程で同じ進化を遂げるだろう。でもここはまだ進化の手前というか途中。

そういう世界では dogfooding が未だに特別な意味を持っている。つまり、テストではカバーできない問題を見つけてくれる。そして被験者として dogfood に手を染めるは普通に大変である。よく壊れるから。ウェブとは違うし、単体のアプリとも違う。自分のいるチームが作っているのは単体のアプリだけれど hardware への依存が高い上に resource intensive なので OS の人々と協力して問題にとりくむ機会が多い。

Be Good At Dogfooding

アプリほど気楽に撒けない制限だけが dogfooding の価値を高めているわけでもない。プライバシーやら帯域やらの理由で、ユーザから集められるデータには限りがある。そしてクラッシュみたいなわかりやすい問題のデータは集めることができるけれど、なんか遅いだとか UI がおかしいみたいな相対的に小さいが無視はできない問題のデータを集めるのは不可能ではないにせよ難しい。

Dogfooding はこうした自動で見つけたり集めたりするのが難しい問題をみつけるのに役立つ。Dogfood をしておかしな挙動に気づいた時、開発者はすかさず bugreport 情報をダンプし、あわよくば Systrace もキャプチャしてバグを file する。

これも「ユーザフィードバックの UI つければいいじゃない?」と思うかもしれないけれど、Dogfood ビルドの OS は一般のユーザからは集められない sensitive な情報をたくさん bugreport につっこむし、Systrace に至っては問題を再現しなおさないといけない。そして dogfooder は file した bug を通じて議論に参加し、必要に応じて追加の情報を提供することを期待されている。こういう込み入ったやり取りは、自動化されたエンドユーザからの情報だけでは実現できない。

個人的にはもうちょっとちゃんとデータを集め解析インフラも整備して dogfood への依存を減らした方が良いと思っているけれども、とはいえ dogfood でないと見つからず、直せない問題はあるとも思う。

Dogfooding には上手い下手がある。普段から奥の方のコードをいじって性能バグなどと戦っている人は傾向として dogfood がうまい。つまり、目の前で問題がおきたときにすかさず必要な情報を集めて適切な bug を file できる。練度の低い dogfooder は bugreport をとらずに曖昧な bug を file し、triage の質疑のなかで不完全なデータをもたもたと提供して開発者を疲れされる。もっと質が悪いのは社内 social media 上に文句を書くだけで bug を fileしないひと。おまえはなんのために dogfood してんだ、という気分になる。そんな troll はごく少数だけどね。

組織の VP みたいのがビシっとわかった感じの bug を file してくるとそうした exec への評価は高まる。モバイル OS 部門の higher management が管理職としてどのくらい立派なのかここでの評価を控えるけれども、少なくとも bug の filing ability はみなおしなべて高い。その点はすごく偉いなと思っている。

見方を変えると、制度としての dogfooding にだって出来不出来があると言える。参加者に対する適切な onboarding 教育はあるか。bug reporting を支援するツールは充実しているか。Bug tracker に何かを投稿するというのは、たとえばスマホのバグならモバイルの開発経験がないとプログラマであっても敷居が高いし、まして開発部門の外から参加している人にとっては恐怖さえあるかもしれない。開発者は未熟な dogfooder による bug を appreciate し、親身になって参加を encourage しているだろうか。

など、モバイルでの dogfooding は難しく、しかし得るものもあり、その運用を目の当たりにすると発見がある。先に書いたとおり自分はもともと dogfood をそれほど重視していなかったけれど、考えを改めた。私物の電話機に開発版 OS をインストールした。アプリも手元でビルドして入れた。

そしてインストールの前に二段階認証のバックアップをとりわすれておたおたする火曜日。

Multi-threading

|

定期的にマルチスレッド辛いという話を書いている気がする(前回)が、またそういう話です。

仕事のアプリはかなりマルチスレッドで、そのせいでしばしばスレッドまわりのバグがおこる。アプリケーションの性質上 compute intensive な部分や I/O (Binder) bound な部分がくまなくあるため、マルチスレッドを頑張っている事実はよい。しかしデザインが厳しい。

アプリは UI まわり以外はだいたいどこかの worker でコードが動く。そして UI 以外のコードは沢山ある。そうしたコード、というかクラスは thread-safe に書かれている。

でも thread-safe なコードを書くって大変じゃん?それよりは thread-safety というか concurrent access は一部の shared state 管理クラスにまかせ、あとは並列アクセスが発生しないように messaging based で作るほうがいいべ・・・。世の中サーバ側は割とそういう風になっている気がするのだが、クライアントサイドはなぜそうならないのだろうね。

前のプロジェクトは、所属するスレッドをあわらす context オブジェクトみたいのを持ち回るアーキテクチャを強いることでコードの実行スレッドを制限していた。Scala  だったら implicit parameter でやるようなことを手でやる雰囲気。これは機能していたが、一方でコードの古さから制限の仕方の筋が悪く、生産性は低かった。

オブジェクトの所属するスレッドを決める (広い意味で) actor 的なアプローチとは別に、Rx とかは状態をなるべく immutable にすることで thread-safety を実現している。これはこれでよい。アプリだとすべての状態を immutable にするのは現実的でない (Flux とかの連中はほっとくとしましょう) ので、Rx と actor をいい感じで使い分けて thread-safe なデザインを生み出してほしいものであることよ。

まあ世の中の大半のアプリのように background に追い出したいのが File I/O と Network プラスアルファくらいなら actor 的な部分はほとんど必要なく, Rx だけでよい。仕事のアプリはもうちょっと色々あるせいで話が単純でないから現状に至った結論を責める気はない。が、辛いことに代わりはない。


今のプロジェクトと前のプロジェクトに共通する問題。本来 thread-sanity をまあまあ実現できるデザインの意図があったことを読み取れる一方、その本来の意図が守られていない。architectural violation が発生しまくっている。そのせいで (たとえば thread context の持ち回りなど) 強制的な制限が残る一方で (immutability など) すり抜けやすい soft-constraint はすり抜けられ、バグがおこる。

これを architecture を守らないコードを書くやつが悪い、というのは簡単だけど、コードを書く側が守る気になれないようなデザインが悪いという方が自分の感覚に近い。デザインを決めた人 (TL とか) と下々のエンジニアの間に大きな実力差があり、かつ TL が micro-managing で下々が大人しい独裁的プロジェクトでは多少理不尽でも architectural constraint / contract を守り抜くことはできるけれど、もうちょっと下々がまともだと押し付けは成立しない。なので従うことにありがたみが感じられるようなデザインでないと生き残れない。そしてそういうデザインは、経験的には稀である。

のでかっこいいデザインを考えたい人は下々の気持ちになってがんばってちょ、と思うのだった。

On Device Tracing

|

Android P では on-device tracing すなわちデバイス上で Systrace をとる機能が追加された。これは性能バグ担当者からすると割と game changer である。

...というのを上司がファイルしてきた性能バグで学ぶ。この上司はもともと Android の中のひとだっただけあって色々よくわかっており、性能バグに bugreport ファイルだけでなくデバイス上で採取した ctrace ファイル(圧縮 Systrace ダンプ) を添付してくれた。たくさんのアプリを起動しまくり、かつネットワークも怪しい環境での Systrace. 今までに見たことのない現象が色々記録されている。すごい。こりゃいいわ。まあそういう厳しい環境下で怪しい挙動をする、とかいわれても直せないのだが・・・。

ところで Systrace が出力する HTML はでかい。でかすぎると Chrome が crash するため、事実上 Systrace ダンプの大きさを制限していた。自分も今まではでかい Systrace の閲覧を諦めてきたのだが上司のバグを無視するわけにもいかず重い腰をあげて回避策を調べてみた。

結論としては  V8 の heap size を大きくするフラグをわたして Chrome を起動すればよい。--js-flags="--max-old-space-size=10000" みたいな。Chrome のバグ参照のこと。

追記

500MB くらいになるとこのトリックをもってしても Chrome が死んでしまう。みんなどうしてんのこれ・・・。

Cold Start and Busy Start on Android

|

Cold start 時のベンチマークが大きく下がっていることが発覚したので調べろと言われ、調査をする。しかし warm start 時と比べてこれといった違いがあるようには見えない。さて・・・。

改めて Systrace を睨むと、どうも自分たちのアプリではなく他のアプリが活発に動いている。なぜか。 Cold start を実現するためその測定はデバイスの再起動直後に行われていた。そしてデバイス起動の broadcast intent に反応し多くの pre-installed アプリがなんらかの準備運動を始めていたのだった。CPU が混雑するそんな起動直後でのベンチマーク。そりゃ遅くもなるわ・・・。

実際のところベンチマークは以前から起動直後に測定されているため、これだけが性能低下の原因だと言い切るのは勇み足の可能性がないではない。けどどうせ startup 時に色々やるアプリが増えたんでしょ... と shrug したい面はある。それに起動直後といういかにも non deterministic な環境で安定した測定ができるとも思えない。起動後はしばらくほっといてから測ってちょ、と伝える。測定のターンアラウンドが伸びてしまい気の毒だけれど・・・。

Cold start, 普通のアプリだったらプロセスを殺して page cache をクリアするくらいでだいたいいいと思うんだけど、センサーとかを使い始めるとまったく自明でないのだよなー・・・。一方でセンサーの cold start が遅くなったところでアプリにできることなんて大してない気もするのだが。しかし我々には platform に bug を file する、という責務があるのだった。

binder_driver

|

View を最適化する仕事は結局よくしらべてみるとうっかりさわっている API が binder call をしているのが原因ということがわかり、それを適当にキャッシュしたりなんだりすれば大体解決したのだった。

最初は binder_driver というタグを有効にすると Systrace で binder call  をトレースできることに気付かずだいぶ時間を無駄にした。しかも Systrace をみてもどの API が Systrace してるのかわからないし・・・。文字列渡して注釈してくれや・・・。まあそこは profiler を併用すればいいので致命傷ではないのだけれど、こいつらトレースを rich にしておく重要性をわかってないな、と思う。まあ八つ当たりだけれど。

 

 

Systrace Buffer size

|

Buffer size 増やしんたいんだけど、と隣の同僚に言われて調べたのだが、コードなどをみたところ結局 atrace コマンド (Systrace から呼ばれてデバイス側で動く) がカーネルパラメタをセットしてくれるので systrace の -b オプションを指定するだけで良いっぽい。そして atrace がしかるべき kernel parameter をセットしてくれるので自分でどうこうする必要はなさげ。自分は -t と -b は相反するコマンドだとおもってたけど、両方同時に指定してよいんだな。

Why Static Analysis Sucks

|

仕事のコードベースで静的解析が強化され、なにかと presubmit check が失敗するようになってしまった。静的解析、昔は憧れがあったし今もまあ有用ではあると思うけれども、すきになれないなあ。

静的解析は、言語のセマンティクスを変えている(狭めている、といっても良い。有効なコードの範囲が減るので。)しかしその変更は、言語のデザインとは別に追加されている。そして静的解析の都合で言語側のデザインが変わることはない。デザインを変えられないから外側のツールとして動いているわけで。

結果として静的解析を通すためのコードは cumbersome になりがちである。自分が一番嫌なのは、コンストラクタの中でメソッドを呼べない(呼びにくい)という制限。コンストラクタが終わるまでオブジェクトは未初期化かもしれないから、未初期化のものを触る際の制限を受ける。でもさー・・・たとえば Android の View とかってのはコンストラクタの中で色々やる前提でデザインされている。コンストラクタがシステムから呼ばれるので、たとえば factory method でコンストラクタをラップして factory method 側で初期化処理の仕上げをする、とかはできない。もっと端的にいえば、もし Android 本体に自分のチームが使っている静的解析をかけたらやまほどエラーができると思う。

結局のところ、言語機能になってるべきものを外側で誤魔化そうとするから歪みが出る。コンパイラの警告も静的解析に似た annoyance があるけれど、それほど理不尽には感じない。あれらは言語の機能と密着しているので、基本的にはその言語として良いコードを書けば直るようになっている。静的解析は違う。言語の外側に作られた静的解析用の DSL, というか annotation を頼るわけだけれど、こいつは言語の思想とは必ずしも会ってないので歪みが生まれる。

なので静的解析のようなものは言語処理系が(言語のデザインも含めて)提供すべきであって、あとづけ、そとづけの静的解析はいまいちだと思う。ま、言語を直せないから仕方ないんだけど。誰か Safe Java みたいのをつくってほしいもんです。というか Kotlin があればいいです。

Services on Android

|

Android 入門してすぐの頃、自分は Android の Service についてまったくよくわかっていなかった。色々誤解していた。その誤解をといたプロセスを思い出しつつ書いてみる。

まず、最初は Service はアプリ (Activity) とは別のプロセスだと思っていた. これは完全に間違いではない (別プロセスにもできる) が、大抵の場合 Activity と Service は同じプロセスで動く。VM がわかれたりもしない。自分がなそんな誤解をしていたのは, Android Chrome が renderer のためにこのプロセス分離 Service を使っているのを見ていたからだと思う。

プロセスモデルの誤解がとけたあとの思い込みは、Service の API 定義には AIDL (かインテント) を使わなければいけない、というもの。同一の VM 内にいるのだから参照を取れば呼べるのだけれど、参照の取得はすごく難しくしてあると信じていた。が、これも誤解で、普通に サービスオブジェクトのインスタンスを持ってきて Java のオブジェクトとしてメソッドコールできる。Local Service とか呼ばれている。プロセスをわけないなら AIDL を使うよりは Java として使ったほうが全然ラク。特に状態の変更を push するみたいな非同期な奴は AIDL だと大変。

プロセスはわかれていないし AIDL も必要ない。でもそれならなぜわざわざ Service なんて仕組みを使うのか。というと、Activity のライフサイクルとは別にプロセスの寿命を指示する仕組みが欲しかったからだよな。たぶん。これはすごい narrow な見方ではあるけれど、多くの実用をカバーしている見方だとも思う。

もちろん他のプロセスから Intents を受け取りたいという要求を満たすための機能でもあるというかそれがメインだし、Intent-based のコミュニケーションはプロセス内でも便利ではある。それはそれ。

Android Notification API

|

自分は Android platform の API をおおむねダメだと思っているが、notification はそんななかよくできているなと思う。

Notification API のよさそのいちは、Android platform のインフラを最大限に活かしているところ。PendingIntent を渡しておくだけでアクションを定義できるのも, RemoteViews で見た目を定義できるのも、Binder や Parceleable といった基盤があってのことだ。普通に View とか Activity とかで UI をつくったりしているだけだと platform の制約ばかりを感じるけれど, notificationn API を使うと platform に助けてもらっている感じがする。

Notification API のよさその 2 は、標準の外観が定義されており、それが毎バージョンよくなっていくこと。Notification の概観は RemoteViews でも定義できるけれど現実にその必要に迫られることは少なく、普通は notification の API 越しにもうしこし高位の情報を指定すると platform が見た目を決めてくれる。おかげで notification drawer には複数のアプリがデータを突っ込んでいるにもかかわらず、割と一貫した UX を提供できている。 毎バージョンその UI がちょっとずつ良くなってるのもえらい. Oreo から入った home screen の notification dot とかも、アプリは特に何もしなくても勝手に表示されるし。まああれが良いアイデアどうかについてはまだ判断できないけれども、platform が空気を読んで色々やってくれるのは良い。

Notification の UI と platform の関係は, Home screen widget のそれと対象的だ。Widget, Android のカスタマイザビリティの象徴みたいに見られていた時期もあるけれど, notification と比べるとそんなに流行ってない。理由は色々あるとおもうけれども、ひとつは Widget アプリと Home アプリ / launcher に UI を任せすぎたせいだと個人的には思う。特に launcher が platform の一部ではないせいで, Widget の UI は OS の進化にあわせて育っていくことができなかった。Home アプリが notification のように platform の一部なら, もっとバンバン API を生やして便利に出来たかもしれないし, UX のいまいちなところも試行錯誤して直せたかもしれない。

たとえば Widget は screen real estate を使いすぎる欠点を克服できてない。Widget のホストが iPhone の dashboard みたいにスクロール可能になっていたらもうちょっと出番があった気がする。まあこの特定のアイデアの是非はともかく、Home アプリが platform の一部になっていないせいで Widget に対して API を提供するのが難しく, fine tuning が進まなかったのは事実だと思う。なんでも customizable にすればいいってもんでもないな、と思うのだった。 Launcher が part of platform だった未来が本当に良いものかといわれるとまったく自信ないけど。

Under the Radar など iOS 系の podcast を聞いていると, アプリ開発者として platform の進化についていく態度について学ぶところがある。Android は fragmentation とかもあっていまいち platform の力を頑張って引き出そうという気が起きにくいけれども, notification をみると少しは platform に bet する人の気持もわかるね。

Cache vs. Sync (Is a False Conflict)

|

むかし、スマホアプリのオフラインデータモデルは Cache based (サーバが source of truth でクライアントはそれを部分的に cache すると考える) がよいのか Sync based (データはそれぞれがもっていてオンラインの時に同期する) がよいのかとぼんやり考えていた時期があった。

が、いまおもうと空想上の問題だったね。このとき考えていた素朴な答えのひとつは「アプリによる」だったけれど、どちらかというと「どちらも使う」という方が正しいと思う。

オフライン対応のための sync というのは強力なアイデアだけれども、データモデルによっては正しく実装するのが割と難しい。ファイルシステムみたいにネストしたデータ構造 (tree) もけっこう難しいし、一部の TODO アプリみたいな ordered list には別の難しさがある。"最強の sync framework" みたいのを開発し、その上にアプリを作ろうとして失敗するのを遠目に見たことがある。最強のフレームワークが想定するデータモデルの複雑さは大抵のアプリにとって過剰すぎたのだろう。Chrome の profile data sync も、最初はすごい汎用的なフレームワークから始まったものの機能開発者が誰も使いこなせず簡単なモデルに回帰していった。というのは 5 年くらい前の話で、今はどうなってるのかわからんけれど。

その点 cache は比較的簡単。サーバが source of truth だとオフラインでデータを書き換える操作はできないけれど、それでいいものは沢山ある。たとえば shopping アプリなら top selling item list とかは cache でいい。Twitter の timeline みたいのも一見すると自分が参加できるから sync と考えたくなるけれど、投稿された timeline の cache + 投稿前 draft の sync (あるいはクライアントに閉じたコピー)  ともみなせる。Immutable objects + Pending operations として表現すればよい、とも言える。

Twitter みたいな social media は本質的にオンラインなので sync の出番がないのは自然。もうちょっとクライアント中心のアプリだと sync したいものが色々でてくる。けれど、その色々 sync したいものをひとつのデータモデルとして sync しようとするとふたたびデータモデルの複雑さあらわれる。個々の機能がそれぞれ勝手に自分のデータを sync する方が、データ種別毎の単純さをあてにできてよい気がする。ぱっと見ると同じようなコードがあちこちに現れて無駄っぽいし、サーバとの API も工夫しないと round trip が増えてしまうけれど。そこは小細工でがんばる。

なので immutable とみなせるものは cache で済ませ、それ以外はぼちぼち sync する, くらいが現実的には安全。

Sync はなんとなくかっこいい。Offline でも online でも同じように操作ができ、データが透過的にクラウドに保存される。理想的じゃん?でもユーザが offline/online の透過性を求めていないこともよくある。同期の有無やタイミングをコントロールしたいユーザは多い。そしてこの flexibility を許すと透過的な sync という夢は終わる。一方で product design として automagical な sync を実現したい、という意見はあるだろうし、それが望ましいアプリも多いだろう。だから Sync か Cache かは engineering だけでなく product の design でもある。そして答えはアプリ単位でなく機能単位で変わる。だからどちらも必要。

というのは今になってみると当たり前だけれど、何年か前の自分はわかってなかった気がする。そして数年後にはまた気が変わり「やはり最強の sync framework があればいい」とか言ってるかもしれない。

AutoFactory

|

AutoFactory という factory コードを自動生成するライブラリがあり、仕事で使っている。

AutoFactory は Dagger や Guice などとセットで使う。DI したいけれど Dagger のコンテクストが必要な依存オブジェクトをすべて揃えられないとき、手元にあるオブジェクトだけを inject した factory をつくっておく。そして後から残りの引数を factory に渡し、最終的に欲しいインスタンスを create() する。

要するに関数としての constructor というか new に partially applied な関数としての factory のコードを生成するのが AutoFactory の役割。コードベースが Dagger heavy になるにつれこういうものが欲しくなるのはわかるしまあ便利なのだが Java のダメさを象徴してもいる。

Java はむかしよく FactoryFactoryFactory と馬鹿にされた。Functional POV だと FactoryFactoryFactory は単なる高階関数である。高階関数にある程度の認知的負荷があるのは仕方ない。FactoryFactoryFactoryを笑うものは関数型の奴らも笑えるのか。

と以前はおもっていたけれど FactoryFactoryFactory のダメさはまさに関数でない点だなあ。関数でないがゆえに、というか Java が関数的な道具立てを欠くがゆえに、コンストラクタに引数を partial application して Factory をつくったり、そこから更に FactoryFactory を作ったりすることが出来ない。逆に FactoryFactoryFactory を uncurry して必要な引数をまとめて渡す、とかもできない。だから仕方なくコード生成に頼る。この functional composability の欠落が Java の残念さだと今はわかる。

一方で今 Java を書くのは 15 年前に C を書くようなもので、そういう残念さは所与のものとしてつきあっていかないと精神衛生を保てない。だから AutoFactory も conceptual backporting だと思って付き合えば良い。

Architecture Components

|

I/O で発表された Android の Architecture Components. サポートライブラリの一味。ベストプラクティスを支援するためのライブラリらしい。

サブモジュールは四つ。Lifecycle, Live Data, ViewModel, Room ORM. ドキュメントなどを読んでみた今のところの自分の結論は: Lifecycle 以外いらない。

Lifecycle

今まで Android は Activity の lifecycle を外部から listen する良い方法がなく、開発者はみなそれぞれ Activity を継承してアドホックに(あるいはオレオレライブラリとして) lifecycle をフックしていた。ActivityLifecycleCallbacks というのがあるにはあるのだけれどこれはグローバルなフックなので大げさすぎた。初期の RxAndroid も、このせいで Activity の lifecycle のための Observable を提供する方法に合意がとれずに終わった。

Lifecycle モジュールはそのフックを提供する。そして Lifecycle をコールバックする Activity および Fragment の base class も用意している。API が安定したら support library の base class に直接同じ機能を入れる予定という。

このレイヤはしょうじき出来の良し悪しは割とどうでもよく、誰かが方法を決めて皆がそれに従うのが重要。なので support library がそれをやってくれるのは良いと思う。

Activity を継承しないと lifecycle がフックできないとかほんとにクソだなと誰もが思っていたのに今まで何もしてこなかったのはひどい話だと思うけど、Better late than never ではある。OSS の世界に複数の方法が乱立して辛くなる前だったのはよかった。

LiveData

RxJava 的なもののしょぼい再実装。既存の実装と比べて唯一の利点は Lifecycle aware なことだけれど、Lifecycle API が定義された今なら既存のライブラリたちを Lifecycle aware にするのは簡単なので、別に再実装しなくてよくね、と思う。

Support Library のような "official" なサードパーティライブラリしか使えない辛い立場の人にとっては better than nothing かもしれないけれど、どうせなら既存の出来の良いライブラリを endorse してほしかった。

ViewModel

使う必要なし、というかこれが best practice という点にまったく同意できないので積極的に避けていきたいかんじ。なお世間が MVVM の文脈で ViewModel と呼ぶものとは若干違うと思う。

Room Persistence Library

世の中もう ORM は足りてると思うのだがなぜまた作ったのだろうな・・・。


Lifecycle を別にすると既存のオープンソースのライブラリたちと比べて特に出来が良い様子もなく、そもそもコードが公開されておらず(support library の流儀を考えるとそのうち AOSP の一部として公開されるだろうけれど)、なんなのだろうな・・・。あなたたちに今更でてきてアーキテクチャどうこういわれる筋合いないですよ、と思う人が多いのではなかろうか。

コミュニティベースの Android 系カンファレンスではここ数年アーキテクチャや保守性に関する話題が延々と議論され、それなりに合意が生まれライブラリとかも揃ってきた。一方 Google I/O の Android 関連の talk はこれまで新機能や性能の話ばかりで開発者の生産性やコードの保守性に関する議論はなかった。Android は本体のコードもあんなだし参考実装を名乗る例年の I/O アプリも割とべったりした実装。少なくともアプリレベルのアーキテクチャやコードの品質についてはコミュニティからの credibility がないと思う。それを突然でてきてエラそうな顔されてもね・・・。

やっぱり Lifecycle だけ作ってあとはコミュニティを endorse したり support する方が良いのではないかなあ。

Kotlin and Conceptual Backporting

|

Kotlin on Android!

いやーめでたいね。諸事情で当分仕事では使えなそうな気配だが、それでもめでたい。どのくらいめでたいかというと Steve Yegee が思わず blog を書いてしまうくらいめでたい。

余暇で Kotlin をかいたあと仕事の古い Java に戻ると色々なフラストレーションを感じる。これは必要な痛みだと思う。以前に感じていたよりはっきりとした形でダメさを識別している。昔々 C++ が一番得意だった頃に C 言語をやらされたときのことを思い出す。あるいは元気だった頃の Java をしばらくさわってから C++ をさわったとき。最近だと Haskell 入門してから Rx をさわるとかも。

相対的に新しいものをさわってから古い世界に戻ると、古い世界に足りないものがはっきりとわかる。だからできる範囲で新しい世界のツールを backport しようとする。(古い例: C 言語でオブジェクト志向、Emacs でリファクタリング) そうした conceptual backports には不自然なものも多いけれど、古いテクノロジを新しい世代に引っ張っていくのは基本的にこうした backports でもある。

保守的なチームで働いていると新しいテクノロジを仕事で使う機会がなかなか来ないけれど、くじけずたまには新しいものを触っておかねばなあ。そして新しい概念をちまちまこっそり backport していきたい。


ソーシャルメディアなどをみているとなぜ Go なり Swift なりを採用しなかったのか企業政治残念、みたいなことを言ってる人がちょこっといるけれども、おまえら Android アプリのコード書いたことあんのかあるいは Android 本体のコード読んだことあんのか、と思う。なぜ Scala, Groovy じゃないのか、というなら百歩譲ってわからなくもないけれど。I/O 中はついウェブをしてしまったせいで無駄に精神衛生を損ね反省。

なおなぜ Scala や Groovy や Clojure じゃないのかは、まあ一通り試せばわかります・・・自分も Groovy でいいのではと思っていた時期がありました・・・ NYTimes が一時期 Groovy を使っていた(2014)けど, 遅くてやめた(2016)という話を思い出した。わかるよ。Java とっくに飽きたけど Groovy も思ったより厳しかったよな・・・。

Android Rendering Profiler

|

なんかすごいツールの話ではなく、画面の上に出せるアレ

実際に遅さを調べる段になると結構良い。自分の場合、普段さわっていない画面の何かが遅いから直せをバグをよこされ、まずどのくらい遅いのかを調べるのに使った。Systrace の方が詳しいけれど、この on screen profiler にも良い所がある。リアルタイムで結果が見えるし、ずっと動かしっぱなしにできる。ダンプした結果をホスト側で見ないといけないし、一回あたり数秒しかトレースできない Systrace と比べ、探索的な調査に向いている。なんとなくアプリをつつきながらグラフを眺め、平均的な挙動のトレンドを眺めたり、スパイクの出るタイミングを探したりする。

件のバグはタッチイベントがくるたび全ての View が rebind されるというもので、この rendering profiler で眺めたら UI thread の負荷が一目でわかる異常を示していた。でもこれって肉眼で見てるだけだと案外気づかないのだよね。デバイスが速かったり自分の目が節穴だったりして。

表示されているバーのうち、寒色系の部分は UI thread, 暖色系の部分は Render thread での経過時間を示している。二つのスレッドの結果を一つのバーに押し込んでいいのか疑問だったが、コードをみると所定のチェックポイントでのクロックを記録し描画の際にチェックポイントの間隔を長さとして描いていた。つまり並列に動いたぶんは本筋の処理にマスクされ、バー表示には計上されない。正しい。

別の見方をすると、Render thread  は UI thread で view tree の drawing traversal がおわるまで仕事ができない。その順序は直列化されている。当たり前だけど UI thread  がさっさとdraw を終わらせないと Render thread を utilize できない。各スレッドが素朴に 16ms まるごと使えるわけではない.

GapWorker

Draw のおわった UI thread はもうそのフレームでやることがない。だとしたら Render thread に描画を切り出すのって微妙に無駄じゃね?そう思いつつアプリの Systrace を眺めていたら面白いことに気づいた: RecyclerView は draw 後にある UI Thread の空白時間を使い、先のフレームで描くことになるであろう View を前もってレイアウトしている (GapWorker.java など参照。) これなら次のフレームの view traversal でレイアウトを計算せずに済み、Render thread を待たせない。UI thread の時間を最大限ににつかっている。賢い。

必要な View を事前に予測できる RecyclerView ならではのテクニックだけれど、そもそも UI を jank-free にしたい場面の多くは RecyclerView 的なスクロールなので、この頑張りはまったく正しい。RecyclerView の評価があがった。

Render Thread vs UI Thread

Rendering profiler を眺めていると、フレームの大半の時間は暖色系の render thread で使われいることがわかる。なにかと非難されがちな UI thread, 案外遅くない。とはいえ多くのアプリはぽろりぽろりとフレームを落とす。そしてフレームを落とす原因の多くは UI thread の spike にある。つまり UI thread には大体速いが時々遅い。

UI thread を spike される原因はアプリ次第だけれど、ありがちなのはどういうものだろう。

しょぼい理由: 本来バックグランドでできるものをさぼって UI thread でやっている。ちょっとしたサービスのメソッドを呼んだら実は遅かったとか、サーバからきた JSON をもとに view-model 的な UI 寄りのオブジェクトを作る部分が UI thread だったとか。特に API なりデータベースなりから帰ってきたデータがでかいと UI thread  を spike させがち。

もうちょっと本質的に大変なもの: View の遅さ。Inflation や layout といった処理は基本的に off-thread にはできないので、たまに遅い。Recycler View みたいに必要な差分だけ処理するならまに合うけれど, たとえば ViewPager とか Drawer みたいに大量の View を一度に新しく表示しようすると、inflate/layout/draw にはどうしても時間がかかりがち。事前に inflate しておいたり、逆に最初は placeholder を出しておき後から段階的に中身を埋めていくなどの小細工が必要になる。が、めんどい。理想的にはがんばって view を flatten し絶対的な計算量を減らせるのが一番。でもそれは更に大変。

UI thread 以外での計算も影響する。Sync や fetch なんかが典型だけど、あとは GC もけっこう影響がある。Stop the world は随分少なくなった最近の Android GC だけれど、別スレッドで動く collector や finalizer みたいなやつらもけっこう CPU を使う。あとは他のアプリのサービスとか。

結局コアが 4 つとかしかないところで並列に動く計算がそれ以上あるとメモリアクセスやロックの競合みたいな細かい話をするまでもなく UI thread は遅くなる。そしてコア以上に仕事があることはぼちぼちある。なにしろ UI thread と render thread だけで 1.5 コアくらいつかっているから、他の仕事のための余裕それほど多くない。

Systrace はどの software thread がどの CPU で動いていたかを記録してくれる。CPU時間不足が一目でわかってよい。

Render Thread の内訳

Rendering profiler では、Render Thread の仕事を 1, Bitmap の upload, 2. Display list の traversal, 3. GPU の計算待ち の三段階に分類している。で、特別な場合を除くとだいたいは 2 が支配的。というか、UI thread を含めても 2 の display list traversal (と、それにともなう GL API 呼び出し) が一番遅い。バーのうち赤い部分。

伝統的な 3DCG の視点で見ると、これはちょっと面白い。GPU に計算をさせるというとき、3DCG だと vertex buffer とかに描画に必要なデータを詰めこんでまとめて GPU に送り、粗粒度で GPU に計算を任せる。テクスチャとかも atlas とかにまとめる。Android の render thread はそういうことはしない。というかまあ、できない。アプリの UI はバッチ描画向けに作られていないから。描画順序の reordering で状態の flush を最小化するような工夫はあるらしいけれど、基本的には毎フレームベタベタと描いていく。

Core Graphics みたいに描画結果をテクスチャとしてキャッシュすることもない。いちおう Surface を使えば似たようことができなくもないけれど、すくなくとも RecyclerView はそういう作りにはなってない。毎フレーム律儀に GL で順番にビューを描いて、それで 60fps を目指す。険しい道のり。現実的でもある。

Display list のトラバースが遅いのを見ると GPU 化も台無しじゃん、とか一瞬思ってしまうけれど、よく考えると描画を GPU に offload できたおかげで display list の遅さだけが残ったとも言える。そのうち Vulkan の command buffer  を cache するとかいいはじめるとかっこいいんだけど、どうかねえ。

あわせてよみたい: Android Graphics Architecture

What Was DI, or What Is It, Still?

|

仕事のコードベースに Dagger が入っていた。休暇前には入れたい派と入れたくない派が口論していたけれど、入れたい派たる TL が説得しきったぽい。がんばった。

自分は DI に mixed feeling で、ないよりはいいけど今更騒ぐようなものでもないと感じる。たぶんそれは自分が DI 世代の洗礼をうけており, framework があろうがなかろうが DI 的なコードを書くからかもしれない。依存はコンストラクタの引数で渡すし単体テストがいるなら重いクラスはインターフェイスで隠すでしょ? あたりまえじゃん? という。

少し前に The myth of using Scala as a better Java という記事があった。この記事では Scala では Java と違って DI framework もクールなんだよ!みたいな話をしている。Cake pattern で framework を使わないのが Scala の DI に対する態度だと思っていたけれど、今は違うんだな。まあ手書きだと interceptor とかできないよね。たしかに。

などと話題を目にすることが続いたこともあり、少し DI について思いを馳せていた。

DI はもともと testability の文脈から生まれたけれど、DI framework は必ずしも testability のためだけにあるわけでもない。テストを書こうが書くまいが DI framework に有用性はある。とはいえテストのことを忘れて Dagger のような framework をみると中途半端に見えるのも事実。Android 上だと特に煮え切らない。

何が煮え切らないのか。自分は DI framework に何を期待しているのか。少し考えて見る。

DI framwork はオブジェクトグラフ(DAG)構築を支援するツールと言える。更に一歩さがると、Make をはじめとするビルドシステムにならぶ依存関係解決システムであり、つまり Spark や TensorFlow みたいなデータフローの実行系でもある。有向グラフを評価して末端の結果を計算する。その評価プロセスに伴う複雑さを利用者から隠してくれる。

Java の DI framework...というと一般化しすぎで Dagger の話だけれども、その第一のいまいちさは依存関係記述の冗長さだろう。初期 Spring の残念な XML は論外として、Dagger の annotation と Component/Module クラスを使う依存の記述も特段ぱっとしない。Java のクラスや annotation をたよっているせいで冗長。もうちょっとコンパクトにできないのかとおもってしまう。ただこれは DI の限界というよりまともな言語内 DSL を定義する力のない Java の限界かもしれない。Scala の MacWire なんかはちょっとだけマシに見える。

Dagger のいまいちさその 2 は、非同期性をまったく扱ってくれないところ。たとえば Android の Service はインスタンスを非同期にしか bind できない。だから Service を依存関係にもつグラフは Dagger では作れない。一方でこういう非同期性は Android のコードを辛くする原因の大きな部分を占めている。

こういう非同期性の問題を今は Rx が解決してくれているわけだけど、そのせいで依存の解決が Dagger と Rx にまたがってしまう。となると全部 Rx でよくね、という気分になる。でも本来なら Dagger の builder は必要に応じて build() の戻り値を Promise 的なオブジェクトで返してほしいし、値を provide する module 達も同様に依存関係を非同期で解決させてほしかった。あと 3 年くらいしたら開発者もやることなくなってそういう機能をつけはじめそうだけど、今欲しいんだよ!今くれ!せめてどこかのガッツある子が勢いでハックできるように Dagger にはプラグインを機構つけてほしいです。

Dagger のいまいちさその 3 は, 実行時の引数を依存関係に組み込めないこと。たとえばデータベースにクエリを投げるとかってデータベースのクライアントと SQL を引数にとって依存関係を解決するプロセスというか、ある意味で pure function なわけじゃん。それを扱えないとデータフローエンジンとしてはいまいち。

オブジェクトを inject するという DI 本来の原則によるとアプリケーション側からフレームワークに何かを渡せないのは自然だけれど、物事を宣言的にしていきたいデータフロー処理系の立場からみると DI のコンセプトに固執されてもつまらない。もう副作用のないところは全部ひきとってくれよ。要するにモナドだろ(いってみただけです)。

Dagger のいまいちさその 4 は、寿命の始まりの面倒は見てくれるのに寿命の終わりは放置していること。Android では Activity などライフサイクルの終わりに必要な後始末の処理を忘れて error prone にしがち。そこは lifecycle を manage するツールすなわち DI framework が面倒をみるのが筋じゃね?

最近の Dagger には Android 向けの機能が入るという話を聞いたので喜んで見に行くと、メモリプレッシャーが高くなったときに解放する参照を選べるとか書いてあってがっかり。そうじゃなくて! Component を捨てるタイミングで! destroy() を呼んで欲しいんだよ!そういうコードを生成しろ! @Destroy みたいなアノテーションが必要か・・・

Android ライフサイクルの辛さ, マルチコア化に伴う非同期性の蔓延やデータフロー・プログラミングの隆盛など昨今の身辺事情を鑑みつつ改めて DI を見直すと、かつてはすごくキラキラして見えたものが解いていて欲しい問題の 2 割くらいしか片付けてくれない現状にがっかりする。だから盛り上がれないのだろうな。ないよりはあったほうがいいんだけど。DI よりはもっと今時の何かに期待すべきなのかもしれない。

追記

その後 Dagger には Producer というのがあることを学ぶ。これはまさに非同期にオブジェクトを解決する仕組み!だが・・・ ListenenableFuture なんてつかってんじゃえーーー!RxJava に対応しろ!今すぐだ!

いやいいんですよ Guava. でも Guava だけ特別扱いとかおかしくね?今時みんな Andorid で Guava つかってないんでしょ...  Netflix の若者とかが PR 送りつけたりして欲しいもんです。はい。

How WebView Sucks

|

WebView のダメさというのは広く語られて久しいが、自分も仕事でさわった WebView を通じて理解が深まった。周回遅れは承知で記録しておく。なお自分の仕事アプリは電子書籍リーダであり電子書籍の標準フォーマットは HTML なので WebView を使うのは仕方ない。以下は仕事に文句を言ってるわけではない。

モバイル向けのサービスを作りにあたり、数年前まではアプリを WebView で実装しようとする人々がいた。そして滅びた。彼らは Web テクノロジのダメさを非難したけれど、Web のダメさは理由の半分くらいだと思う。残りの半分は WebView のダメさではないか。少なくとも Android では。

Android の WebView は Web と Android のダメなところを集めた代物といえる。

まず WebView は View のサブクラスなので Activity のライフサイクルに引きずられて作られたり破棄されたりする。しかし WebView の中からライフサイクルイベントを知ることはできないし状態も直列化されない。結果としてライフサイクルが切り替わるたびにページが再構築される。状態がふっとんでしまうし、遅い。様々なものが View の寿命にひきずられるのはほんとにキツイ。ライフサイクルの遷移による WebView の再構築はウェブページのリロード相当ではない。どちらかというとブラウザの再起動みたいなもの。レンダリングエンジンの使う OS の資源は一旦破棄したあと確保しなおされる。キャッシュも全部消える。超遅い。

同じモバイルでも WebView でなくブラウザのウェブならこうした問題はおきない。ブラウザはページを Service にホストするため Activity ライフサイクルの影響を受けない。仮にブラウザが背後のタブにいるページを一旦 purge したとしてもブラウザ本体は生き続けている。だからタブの開き直しは WebView つくりなおしより速い。

画面の描画もきびしい。最近の WebView は別スレッドの GL でページを書いて結果を SurfaceView としてアプリ本体の View ツリーに composite するけれど、メインスレッドを自分でもっていないせいでブラウザがやっている小細工がまったくできない。遅い。

スレッドといえば、アプリのスレッドと WebView のスレッドがわかれておりイベントループが二つあるのもつらい。普通の WebView のユースケースでウェブのイベントループとアプリのイベントループを混ぜたくないのはわかる。ランダムなウェブサイトの遅さがアプリを止めてしまうから。でもその理不尽な環境下にある WebView を主体としてアプリをつくるとか、無理でしょ。

このスレッドモデルのせいで WebView と Java のブリッジコールは超遅い。ブリッジは WebView のスレッドでもなくメインループでもない謎のスレッドで呼ばれる。スレッドホップが挟まるせいで JS が 1ms 近く止まってしまう。しかもブロッキングせずに ブリッジを呼ぶ方法がない。最近は postMessage() できるようになったけど。いずれにせよ JS の世界と Java の世界は大きく隔たっている。これは Node を自由に使える Electron とはだいぶ違うし、ActiveX の使えた Windows の IE component よりもしょぼい。

こういう技術的な問題のせいで WebView ベースのアプリは Android の力をまったく引き出せない。そのくせウェブのよさも全部捨ててしまっている。ブラウザの資源を使えない。URL もない。Hyperlink もない。柔軟なデプロイもできない。検索エンジンからもソーシャルメディアからも traffic を呼び込めない。そりゃダメだろ・・・

今となってはブラウザもマシになったし JS で書きたいだけなら React Native もある。WebView で頑張ろうとする人はいない。だからこの話は歴史的な回顧でしかない。Lesson Learned があるとすれば何か。Web やその他のテクノロジをプレットホーム抽象化レイヤとして使ってはいけないということだろう。Web の良さはプラットフォーム中立であること(だけ)ではない。Web は Web というプラットホームであり、独立したプラットホームとしての良さがある。その力を引き出さないと良いものは作れない。WebView はそれを反例として示した。

Users of an Orphan App

|

二年前に Android の勉強がてらつくったまま存在を忘れていた自分のデビュー作アプリ Hiccup, いまふと developer console をみると総インストール数 400, アクティブデバイスへのインストールが 15 となっていた。そしてレビューが2件ついていた...

アプリがゴミであることを考えると結構多い。サーチしてもこのアプリにたどり着くのはかなり困難なはずだが、一体どうやって発見したんだろうな。これが二年という年月の力なのか。

一方で自分がむかし作ったウェブサイト, たとえば gisted.in とか、一切 analytics とってないけど誰も使ってないと思うんだよね。(とおもったら GA が入っていた。そしてやはりユーザはいなかった。)2015 というのは趣味の Android  アプリを出すにはまだ遅すぎない時期だったのかもしれない。今はもう Apple の App Store みたいな勢いで血の海のような気がする。

その数少ないユーザたちはヨーロッパ、中東、東南アジアにいるらしい。無駄に Next Billion にリーチしていた。ははは・・・。

なおこの Hiccup というオーディオプレイヤ、巻き戻しをラクにするアイデアはわるくなかったと思うけど使う API の選定を間違えたせいでやたら buggy, かつオーディオデータを自分の在庫 (Play Music) から取り出すのが大変なためあまり実用にならなかったのだった。残念。アルバムとかプレイリストみたいのを真面目に作らないと使い物にならないと思うので、作り直すこともないでしょう。

MTTF and Apps

|

休暇に入る少し前、自分のチームの TL が「アプリの MTTF 取りたいなー」とバグをファイルした。すでに集めているメトリクスから適当に計算できるのでは、という期待があったようだけれど、ちょっと考えてみるとまあ無理。というか Android  アプリの MTTF はどう計算すればいいのだろうか。

Android の文脈を離れると、MTTF はクラッシュ間隔の平均値を取れば良い。そして Play Services のようなシステムに近いサービスやメッセージングやチャットみたいなほぼ常駐する性質のアプリならこの計算方法で意味がある。けれどたとえば一日一回くらい起動してコンテンツを眺めるようなのアプリだと、単純にクラッシュ間隔を求めても仕方ない。

そこでまずアプリが前面にいるときの時間を記録しておくことにした。そして前回のクラッシュから次のクラッシュまでどれだけ前面にいたかを TTF, time-to-failure とみなす。この指標は悪くない。ユーザからみてどれだけ頻繁にクラッシュするかを表した指標だから。

けれど細かいところに疑問はのこる。具体的には sync など何らかのバックグランド処理中、画面を見せていないときにおきたクラッシュはどう扱うべきか。ユーザの体感クラッシュ頻度という観点だけを考えると、これら background crash は無視してよい。けれどアプリの安定性を気にするならこの crash も指標に加えてあげたい。

そこで厳しい側に倒すべく、バックグラウンドだろうがなんだろうがクラッシュのタイミングで TTF を計上することにしてみる。もし background crash がおきたら、前回の crash からその時点までの foreground 時間の合計を計上する。この方法の問題は、画面を表示することなく二回つづけて background crash がおきると二回目の crash の TTF がゼロになってしまうこと。厳しすぎじゃね?

とはいえ厳しい側に倒す方針なのでまずはこれでよい。厳し目に計算した MTTF をモニタリングしておき、値が下がったら詳しくデータを調べればいいから。そうした breakdown につかうべく TTF event の付加情報として crash した時点でアプリが前面にいたかどうかの状態も送っておく。

というかんじで MTTF が計算できるようになった。のだけれど、休暇に入ってしまったので出荷されたコードがほんとに正しく動いてるかどうかはまだ調べてない。