オプションに「--profile」をつけて Run時間が1分から2秒になった話

開発中に何十回, 何百回と端末またはエミュレータで動かしてテストしていると思います.

回数が多い分, 数十秒でも大きく生産性に影響します.

Terminal からの選択しながらのビルドや Gradle コンソールで詳細を確認したりする方法は, まあ, あるっちゃああるけどもいちいちそんなの調べてやるのもなんだかめんどくさかったりして.

「ビルド時間の短縮」というようなキーワードでググればまあそれなりに似たような結果がみつかる.

Android お手軽なビルド時間の短縮メモ

今回, なんとなくいちいちHTMLで結果が出力されるという

「コマンドラインオプションに --profile をつける」

というのをなんとなくやってみたら, Runから実行までの時間が 1分から2秒 になったのでまあ書いてみるが.

Preferences

とするだけ.

「Run」するたびにHTMLで, build/reports/profile/ 以下 にそれぞれにかかった時間をまとめたHTMLファイルが出力される.

AnalyticsApplication_java_-_Android_Studio_2_0_Beta_6

閲覧するには, Run後出力されたHTMLから右クリックでブラウザを起動するといい.

Android_Studio_2_0_Beta_6

こんなかんじで出力された.

Profile_report

Profile_report

50秒のうち45秒が「Crashlystis」の処理に使われている.

デバッグ時には, このような処理は当然必要ない.

しかも謎なのは Fabric は使っているが Crashlytics は使っていないのだが.

この処理を殺す.

-_Android_Studio_2_0_Beta_6

その後結果.

Profile_report

Profile_report

いろいろ謎なことは多いが, オプション「--profile」をつけるだけで一回の「ビルド→実行」時間が数十倍に短縮された.

やみくもにググるよりか, いわゆる「ログ」のような「profile」を見ながらググるほうが効率的.

なぜなら, GUIダイアログから「--profile」と入力しておくだけだもの.


ソースディレクトリの構成時にもっておくべき「会社別」のイメージ

ひとつの部分を変更しようとして, 必死に複数のファイルをいったりきたりしながら更新してみると, 意図しないところが壊れている.

想像してみてください.

マネージャー会社, プログラマー会社, 人事会社, マーケティング会社がある.

プログラマー会社には, マネージャーやマーケティングや人事の人はいなくて, プログラマーしかいない.

いきなり src 直下に「activities」「fragments」「views」ディレクトリを作成してしまうことに似ていません?

「何をするか」ではなく「何であるか」でまとめられたものは, 探そうとすると何度も大きく移動しなければならない.

「会社の業種別」「会社別」のような「何をするか」を優先させたイメージでディレクトリを構成していくと以下のようになります.

1-A-m20R0Qve-eB4ishqZc_Q

良い点

- 高モジュール化.
- 見つけるのが分かりやすい.
- 高レベルな抽象化.
- 機能とレイヤー両方で分けられている
- 構成のメンテがやりやすい.
- 凝縮されている.
- スケール変更しやすい
- 関係ないクラスやファイルを変更するアクシデントが少なくなる.
- 機能の追加や削除がしやすい.
- 再利用しやすい.

Package by features, not layers — The linkedcare Engineering team — Medium

【2015-10-14 公開!!】Google I/O 2015 のソースコードに見るディレクトリ構成

ひとつの考え方として根底にあったほうがよいと思いました.

これに合わせてリソースディレクトリも小分けにできるという話は以下から.
res ディレクトリは小分けにしたほうがいいのではないか


どうやってアプリ起動時間を6秒から1.6秒に短縮したか

どこの開発チームでもやっているように, アプリの起動と読込み時間を改善することは わしら Android開発チームでもひとつの優先事項でした.

デバイスメーカーはさらに速く流れるような操作を提供し続けており, ユーザーは一層のスピード向上を期待しています.

最近, 新アプリを「Dependency Injection」や 「Reactive Programming」のようなモダンなパターンを利用して書き直しました. これによって メンテナンス性が向上し, よりモダン & モジュール化 されましたが, いくつかの調整が必要となりました.

リリース時, 起動時間は Nexus5 上で 5, 6 秒かかっていました. これはわしらの目標である「2秒以下」を超えていました. それでパフォーマンス改善を行うことになりました.

主な低速化の原因が 「リフレクション」であることが分かりました. これとその他の細かいことを改善することで, 起動時間を 1.6 秒に短縮することができました.

どのようなことを行ったか

まず, アプリの起動時間を Android のトレース機能を使って起動時間の計測です. Application クラスのコンストラクタから画面に表示されるプログレスインジケータの終了までを計測しました.

traceview_profile

Profiling with Traceview and dmtracedump | Android Developers

最大のパフォーマンス低下の原因を見つけるために出力された分析用トレースファイルたちを集め DDMS に読み込ませていましたが, 最終的には 簡単な方法でボトルネックを特定できトレースしながらパフォーマンスを比較できる「NimbleDroid」に切り替えました.

Home_-_NimbleDroid___Automated__Comprehensive_Performance_Analysis_for_Every_Build_of_Your_App

Home - NimbleDroid | Automated, Comprehensive Performance Analysis for Every Build of Your App

簡単な改善

最初に見つけた主な速度低下の原因は, 多量のクラス, メモリを大量に消費する jar リソースの読み込み JodaTime (特定済みの問題有り) でした.

Joda Time's Memory Issue in Android

Groovy のクロージャを使っていましたが 他にこれを使っていなかったので, 取り除くことにしました. 昔ながらの Java7 の構文に戻って. Groovy で作成されたコードを取り除きました. 他の選択肢を探していますが Java の 匿名クラスを見るための IDE補助機能は優先ではありません.

次に, RxJava が1秒程度 遅延させていることを見つけました. これについては, 次のリリースでの修正されます. ありがとうございます.

NewThreadWorker.tryEnableCancelPolicy doing costly reflection on Android · Issue #3119 · ReactiveX/RxJava

次に, アプリに組み込まれているいくつかの サードパーティの 統計クライアントが アプリの起動時にブロッキングしているコールがありました. どのようにインスタンス化され動作しているか ベンダーと協力をいただきながら起動時の機能改善を行いました.

また, 我々のコード自体にも技術的問題を見つけました.
md5の計算処理や 一般的な多すぎる処理で起動オブジェクトをブロックをしていました.

これらを改善したところで, 起動時間は以前の半分の 3.2 秒に短縮できました.

次は データフローの最適化です. これは, わしらのアプリのような高データ集約型のアプリには特に重要な意味を持ちます.

データストア処理

データ処理の全てが非同期であるため、我々は最初のプレゼンテーションとデータ層との間の抽象化として機能するように、単一のコンテンツマネージャとしていました。これによりカプセル化することはできましたが、徐々にデータが永続ストレージから必要とされる場合に必ずゴッドクラスをインスタンス化することになる、というスローダウンを引き起こしていました。

God object - Wikipedia, the free encyclopedia

この場合、Analytics の設定値は、アプリの起動時にディスクからの取得を必要としているのですが, SSL対応ネットワーククライアントのインスタンス化をを待たなければなりませんでした.

アプリに機能追加されるにつれ, さらに依存関係が加わり, コンテントマネージャーの起動時間は増加し, 初期インストール時と同じくらい時間がかかっていました.

単一のコンテントマネージャーとするよりも, 独立したデータストアとする方針にすることで, 素早くキャッシュされたデータを読み込むことが可能となりました.

これは, ディスクからUIへのデータの処理フローを最適化する FacebookのAndroidのチームによって行われた最近の研究と似ています.

Improving Facebook's performance on Android with FlatBuffers | Engineering Blog | Facebook Code

まず, 最初は, ディスクとネットワークDAOを利用して個々のシングルトンのデータ・ストアとしているコンテンツマネージャを壊すことにしました。こんなかんじで、コンテンツマネージャから ConfigNetworkDAO と ConfigDiskDAO を利用して ConfigStore を立ち上げていました.


@Singleton
public class ConfigStore extends Store {
   @Inject
   public ConfigStore(AppConfigParser parser,
                      final ConfigDiskDAO loader,
                      final Lazy<ConfigNetworkDAO> fetcher) {
       super(loader,fetcher,parser);
}

重要なオフライン時または最初のロード後に - 時間のかかるネットワーククライアントを利用し、私たちが実際にネットワークの操作を行うまでそれをインスタンス化しないために許可されている Dagger の時間のかかるインスタンス化を改善することにしました。このアプリでは, バックグラウンドサービスを使用してデータをダウンロードすることがメインで ここで処理するデータはほとんどなく、ネットワークを利用した通信よりもディスクストレージからUIにロードされます. 結果, UIに ディスクからのデータ処理経路を最適化することができ、大きくスピードを取り戻すことができました。

リフレクションの削除

トップストーリーのデータをパースする際に 永続ストレージとネットワークのどちらから取得しているかに関係なく 700ミリ秒以上かかっていることを発見しました.
このアプリのような大量のデータドリブンなアプリでのAndroid 標準の Gson 処理パフォーマンスがどれだけ貧弱であるかを知った時驚きました. 起動時のトレースにより「リフレクションタイプアダプター」のコールが原因であることが分かりました.

NYTimes - NimbleDroid | Automated, Comprehensive Performance Analysis for Every Build of Your App

Gsonからのリフレクションコールを最小限にしようとしましたが, 実行可能な方法は 苦労して独自でタイプアダプタを書くことだけでした. リフレクションを使わずかつ起動時間をかけずにシリアライズする技術をみつけることは長く無駄でした. モデルにコードを追加することしか選択肢が残っていなかったので, 簡単な「Gson with custon type adapter」に戻ることにしました.

TypeAdapter (Gson 2.3.1 API)

タイプアダプターを書くことで10倍のパースパフォーマンスの改善をすることができました.

開発者の負荷を最小限にするために以下の「Immutable library」を利用することにしました.

JSON serialization

これはコンパイル時にデータモデル用のカスタムタイプアダプターを提供してくれると同時に AutoValue 同様の不変性も提供してくれます.


@Value.Immutable
@Gson.TypeAdapters
public abstract class AppConfig {
  public abstract Optional<DeviceGroups> deviceGroups();
  public abstract Marketing marketing();
  public abstract List<OverrideCondition> overrides();
  public abstract LinkedHashMap<String, List<Integer>> imageCropMappings();
}

public AppConfigParser() {
  gson = new GsonBuilder()
    .registerTypeAdapterFactory(new GsonAdaptersAppConfig()) //auto generated adapter
    .create();
}

public void parse() {
  reader = new InputStreamReader(source.inputStream(), UTF_8);
  AppConfig appConfig = gson.fromJson(reader, AppConfig.class);
}

現在では, データフローは以下のようになっています:

バックグランドサービスが 起動時間のかかるネットワーククライアントである RxStore をコールして新しいデータをJSONでダウンロードします. これは, スケージュル, またはプッシュアラートから実行されます. その後, ディスクにJSONデータを流しこみます. 単一のオブジェクトとしてJSONを保存するのではなく, ストリーミングAPIを使います. なぜなら メモリー上での1M以上のインスタンス化を避けることができるからです.

UIがデータを必要とすれば、メモリ内のキャッシュ(guava)とディスクにバックアップされたデータストアから不変のデータを取得します。

CachesExplained - guava-libraries - Explanation for how to use Guava caches. - Guava: Google Core Libraries for Java 1.6+ - Google Project Hosting

ディスクにデータが存在しないとき, または最終保存したデータのフォーマットが変更したときのみネットワーククライアントを起動します. データは永続ストレージからUIに一方向で流れ, ネットワークから UI には決して流れません.
この一方向のデータフローは、データストアへの要求の99パーセントは、ディスク以外にアクセスする必要がないことを意味します。

私たちは、このようなディスクストレージのためのデータをシリアライズするFlatBuffersのような他の方法を模索していきますが、現在の結果に満足しています。リニューアル後は、ユーザーに 2秒未満でページの完全なビューを提供しています。

結論

Androidでは, リフレクションは 重大なパフォーマンス問題を引き起こすことがある (とくに大きいデータ・ドリブンなアプリ) . このことから, アプリ起動時に できるだけそれはさけるべきです.


こういう記事が日本でももっと増えるといいですね.

Improving Startup Time in the NYTimes Android App - The New York Times