Slide 1

Slide 1 text

DroidKaigi 2025 Performance for Conversion! 分散トレーシングでボトルネックを 特定せよ Andrey Chernov

Slide 2

Slide 2 text

Andrey Chernov andousan Android Engineer @ pixiv Android歴9年 KMPの信者 o11y ❤ 2 inetand inetand

Slide 3

Slide 3 text

このセッションのゴール Performance for CVR!パフォーマンス管理の重要性 既存ツール (Firebase, Systrace) のスケールにおける限界 分散トレーシングの概念と有効性の説明 モバイルアプリでの分散トレーシング(OTel)の導入方法 Otelの導入と運用の具体的なイメージ 3

Slide 4

Slide 4 text

パフォーマンス管理が重要な理由 4

Slide 5

Slide 5 text

パフォーマンス管理が重要な理由 ● アプリ起動時間が遅いと 離脱率が⤴ ● アプリの体験がスムーズ でないとCVRが⤵ 5 Madhavan, N. , 2024, Improving Mobile App Performance: A Comprehensive Approach

Slide 6

Slide 6 text

パフォーマンス管理が重要な理由 6

Slide 7

Slide 7 text

パフォーマンス管理が重要な理由 ● 遅いと離脱率が⤴ ● 遅いとは何か? アプリの起動時間が以下の閾値に達すると、長すぎると判断される ● コールド スタートに 5 秒以上かかっている。 ● ウォーム スタートに 2 秒以上かかっている。 ● ホットスタートに 1.5 秒以上かかっている。 指標の閾値がストア毎で異なる 7

Slide 8

Slide 8 text

パフォーマンス管理が重要な理由 ● ユーザーからのFBの時点で気づく のは遅い ● 様々な要因があるので継続的に パフォーマンス変動 の観測が必要 8 Environment

Slide 9

Slide 9 text

パフォーマンス指標 9 プロダクトのドメインに依存しない指標の例

Slide 10

Slide 10 text

画面初期表示までの時間 (TTID) 測定するイベント ● プロセスの開始 ● 画面の初期化 ● 初回フレームの描画(初回状態の描画) 10

Slide 11

Slide 11 text

画面完全表示までの時間 (TTFD) 11 測定するイベント ● プロセスの開始 ● 画面の初期化 ● データの読み込み ● レイアウト・描画

Slide 12

Slide 12 text

画像読み込み開始から描画完了までの時間 アプリ側の考慮 ● デコーディング ● Bitmapのキャッシュヒット ● Disk/Memory ● 画像フォーマット ● BitmapTransformation 12

Slide 13

Slide 13 text

リクエストにかかる時間 ネットワークの考慮 ● CDN経由でのDNS解決 ● 環境の問題 ● 端末の問題 13

Slide 14

Slide 14 text

アプリ起動時間(AppStartup) ● Cold ● Warm ● Hot 14

Slide 15

Slide 15 text

アプリ起動時間 15

Slide 16

Slide 16 text

このセッションのゴール Performance for CVR!パフォーマンス管理の重要性 既存ツール (Firebase, Systrace) のスケールにおける限界 分散トレーシングの概念と有効性の説明 モバイルアプリでの分散トレーシング(OTel)の導入方法 Otelの導入と運用の具体的なイメージ 16

Slide 17

Slide 17 text

Firebase Performance Custom traces 17

Slide 18

Slide 18 text

Firebase custom traces ● カスタムなパフォーマンス指標の 測定と管理が可能 ● ユーザーの環境ではコンテンツの 表示が重い場合の調査に有効 18

Slide 19

Slide 19 text

Firebase custom traces ● カスタムトレースに関連するユー ザーセッション詳細画面 ● 一部のトレースに限定して集計さ れる 19

Slide 20

Slide 20 text

実際の測定 アプリ起動時間   ネットワークリクエストの詳細トレース 20

Slide 21

Slide 21 text

アプリ起動時間(Cold) ● デフォルト指標! 21

Slide 22

Slide 22 text

アプリ起動時間 22

Slide 23

Slide 23 text

アプリ起動時間 ● あれ?コールドだけ? ● バックグラウンド起動判定フラグは? ● 結論:Cold起動以外の集計データが必要な場合は独自で 測定しなければいけない 23

Slide 24

Slide 24 text

実際の測定 アプリ起動時間   ネットワークリクエストの詳細トレース 24

Slide 25

Slide 25 text

リクエスト時間(詳細トレース) ● ケース ○ 海外ユーザーから「コンテンツの読み込みが遅い」 というFBを受ける ○ 手元では特に問題はないので、ユーザーの環境で測定する 25

Slide 26

Slide 26 text

ネットワークリクエストの測定 26 fun provideOkHttpClient(): OkHttpClient { return OkHttpClient.Builder() .eventListenerFactory { TimingEventListener() } .build() } Kotlin override fun dnsStart(call: Call, domainName: String) { super.dnsStart(call, domainName) dnsTrace = FirebasePerformance.startTrace("dns_lookup") } override fun dnsEnd(call: Call, domainName: String, inetAddressList: List) { super.dnsEnd(call, domainName, inetAddressList) dnsTrace?.stop() } Kotlin

Slide 27

Slide 27 text

リクエスト時間(詳細トレース) ● 各フェーズの測定ができた! 27

Slide 28

Slide 28 text

トレースが分散している問題 28

Slide 29

Slide 29 text

Firebase Custom Tracesでいける? 29

Slide 30

Slide 30 text

Firebase Performanceの限界 30

Slide 31

Slide 31 text

Firebase Performanceの制限 ① ● トレースの関係性がない。子のトレースも設定できない ● カスタムトレースのAPIが限定的 ○ Trace の開始時間として、任意の startTime 指定不可 31 FirebasePerformance.startTrace("").

Slide 32

Slide 32 text

Firebase Performanceの制限 ② ● BigQuery への集計データのエクスポートが24時間置きに 行われている ● ディレイがあるのでSLO設定ができない 32

Slide 33

Slide 33 text

サンプリング 33

Slide 34

Slide 34 text

サンプリングの種類 34 Head-based Sampling 一定の確率でTrace参照せずに判断する Tail-based Sampling Traceを参照した上で判断する ランダムにトレースを選択 遅いトレースやエラーを含んだトレースを選択

Slide 35

Slide 35 text

Head-based サンプリング ● 一定の確率・割合 で、スパンの欠損なく、 トレースを送信対象に含める ● どれを残せばいいかの選定は行わない ● エラーや比較的に遅いトレースも サンプリングされてしまう 35 Head-based Sampling 一定の確率でTrace参照せずに判断する ランダムにトレースを選択

Slide 36

Slide 36 text

Tail-based サンプリング ● トレースの内容を参照 した上で サンプリングする方法 ● エラーを含むトレース ● 全体的なレイテンシーに基づいて サンプリングする ● 特定の属性 があるトレースを含む など 36 Tail-based Sampling Traceを参照した上で判断する 遅いトレースやエラーを含んだトレースを選択

Slide 37

Slide 37 text

Firebase Performanceの制限 ③ サンプリングの制御不可 ● サンプリング方式は固定 (head-samplingのみ) ● カスタムトレースのセッションのサ ンプリング 37

Slide 38

Slide 38 text

従来の手法では問題箇所の特定が難しい ● Firebase Performanceで「ライトに導入 → 大枠の把握」し、結局 手元でプロファイルを行ったり、最適化を試みる ● トレースの文脈となる情報が見れない ● トレースの因果関係もわからない ● 情報が分散しているので、照らし合わせることが難しい 38

Slide 39

Slide 39 text

このセッションのゴール Performance for CVR!パフォーマンス管理の重要性 既存ツール (Firebase, Systrace) のスケールにおける限界 分散トレーシングの概念と有効性の説明 モバイルアプリでの分散トレーシング(OTel)の導入方法 Otelの運用の具体的なイメージ 39

Slide 40

Slide 40 text

分散トレーシング 因果関係をつけてボトルネックを特定する 40

Slide 41

Slide 41 text

分散トレーシングとは ● 元々はマイクロサービスを可視化するための手段である (network中心) ● 処理の流れを網羅的に記録する仕組み ● 複数サービスやアプリ内の処理を 1 本のトレースとして可視化 ● 遅延・エラーの原因を特定しやすい ● 分散トレーシングではトレースがパフォーマンス調査のルートである 41

Slide 42

Slide 42 text

バックエンドの例 42 *https://opentelemetry.io/ja/docs/concepts/observability-primer/ より ※バックエンドのトレースをアプリのトレースと一環して集計可能

Slide 43

Slide 43 text

トレースが分散している問題 43

Slide 44

Slide 44 text

トレースが分散している問題 44

Slide 45

Slide 45 text

OpenTelemetry 分散トレーシングを実現するための仕様と計装 45

Slide 46

Slide 46 text

OpenTelementry(OTel)とは ● CNCF*のOSS規格 1. メトリックス 2. ログ 3. トレース ● 三本柱のシグナルを提供する標準化された 仕様とOSSの計装SDK 46 *CNCF(Cloud Native Computing Foundation)プロジェクト

Slide 47

Slide 47 text

OpenTelementry(OTel)とは ● OTLP + SDK ● ベンダー非依存 ● ツール選定が柔軟 47 *https://opentelemetry.io/ja/docs/languages/#status-and-releases 言語API & SDK | OpenTelemetry

Slide 48

Slide 48 text

OpenTelementry(OTel)とは ● コレクター ● ベンダー非依存 ● ツール選定が柔軟 48 *https://opentelemetry.io/ja/docs/collector/ コレクター | OpenTelemetry

Slide 49

Slide 49 text

OpenTelemetryの主な概念 シグナルという概念について 49

Slide 50

Slide 50 text

● 収集・処理・エクスポートされるテレメトリデータのカテゴリ ● 主なシグナル ● トレース(Traces) ● メトリクス(Metrics) ● ログ(Logs) ● バゲッジ(Baggage) ● イベント(Event) OpenTelemetryのシグナルとは 50

Slide 51

Slide 51 text

Trace & Span ● トレース:アプリ内での処理の経 路を示す ● トレースは個別処理単位の スパンでできている ● span1.parent_idとして span2.idを設定すると親子 関係ができる 51

Slide 52

Slide 52 text

Metrics & Logs Metrics ● 数値ベースの集計データ ● 時系列で「状態の変化」を監視 ● CPU使用率、エラーレート、レイテンシー分布 などを計測 ● Counter, Gauge, Histogram など Logs ● テキスト/構造化ログ 52

Slide 53

Slide 53 text

OpenTelemetry Context 伝搬 53

Slide 54

Slide 54 text

このセッションのゴール Performance for CVR!パフォーマンス管理の重要性 既存ツール (Firebase, Systrace) のスケールにおける限界 分散トレーシングの概念と有効性の説明 モバイルアプリでの分散トレーシング( OTel)の導入方法 Otelの運用の具体的なイメージ 54

Slide 55

Slide 55 text

OpenTelemetry in Mobile モバイルアプリの環境には本当に適応可能なのか? 55

Slide 56

Slide 56 text

OpenTelemetry SDK ● otel-android SDK ● otel-java SDK ● Embrace Android SDK ● Embrace Kotlin SDK ● Datadog Tracing Android SDK ● Elastic OTel Android SDK 56

Slide 57

Slide 57 text

簡単なSDKを作ってみる 57 名付けて、Catracer SDK ※ Cat Racerじゃないよ

Slide 58

Slide 58 text

Catracer SDK とは ● 軽量版のOpenTelemetry Android SDK(独自SDK) ● Embrace Kotlin SDK を基盤実装として利用 ● 特定のSDKに依存しない設計になっている。KMPも対応可能 ● ゴール ○ アプリレベルでSpanを管理する(永続化を含む) ○ Dozeモードやバックグラウンド制限に対応した同期 ○ 特定OTel SDKに依存しないこと 58 *https://github.com/embrace-io/opentelemetry-kotlin/tree/main | OpenTelemetry Kotlin SDK by Embrace

Slide 59

Slide 59 text

独自SDKの仕様 定義対象:Span, Trace, Tracer のみ Tracer:Span の生成・終了管理 Span:属性、トレースContext伝搬 Trace:複数Spanの集合として処理 59

Slide 60

Slide 60 text

独自SDKの設計 60 expect class Catracer { fun startSpan( name: String, parent: CatSpan? = null, attributesMap: Map = emptyMap(), startTimeNanos: Long? = null ): CatSpan fun getOngoingSpan(spanDeclaration: AppSpanDeclaration): CatSpan? } Kotlin

Slide 61

Slide 61 text

独自SDKの設計 61 expect class CatSpan { val attributes: Map val name: String val traceId: String? val spanId: String? val parentSpan: CatSpan? val isRecording: Boolean fun start(startTimeMs: Long? = null): Boolean fun stop(stopTimeMs: Long? = null) fun putAttribute(key: String, value: String) } Kotlin

Slide 62

Slide 62 text

独自SDKの設計 62 interface AppSpanDeclaration { val spanName: String } enum class AppSpans(override val spanName: String) : AppSpanDeclaration { COLD_APP_STARTUP_SPAN("cold_app_startup"), HOME_SCREEN_FULLY_RENDERED("first_screen_fully_rendered"); } Kotlin

Slide 63

Slide 63 text

実際に測定してみよう! ユーザーの環境で指標を測定する 63

Slide 64

Slide 64 text

実際の測定 アプリ起動時間( Cold)   ネットワークリクエストの詳細トレース 64

Slide 65

Slide 65 text

アプリ起動時間(Cold) 65

Slide 66

Slide 66 text

class App : Application() { var processStartupTimestamp: Long? = null override fun attachBaseContext(base: Context?) { processStartupTimestamp = SystemClock.elapsedRealtimeNanos() super.attachBaseContext(base) } Kotlin 66

Slide 67

Slide 67 text

class App : Application() { override fun onCreate() { super.onCreate() val tracer = Catracer by inject() catracer.startSpan( AppSpans.COLD_APP_STARTUP_SPAN, startTimeNanos = processStartupTimestamp ) val handler = Handler(Looper.getMainLooper()) handler.post(DetectStartFromBackgroundRunnable(tracer)) //... Kotlin 67

Slide 68

Slide 68 text

registerActivityLifecycleCallbacks(object : ActivityLifecycleCallbacks { override fun onActivityCreated( activity: Activity, savedInstanceState: Bundle? ) { catracer.getOngoingSpan(AppSpans.COLD_APP_STARTUP) ?.putAttribute("has_been_started_by_user", "true") }   override fun onActivityResumed(activity: Activity) { catracer.getOngoingSpan(AppSpans.COLD_APP_STARTUP)?.stop() unregisterActivityLifecycleCallbacks(this) } //... Kotlin 68

Slide 69

Slide 69 text

registerActivityLifecycleCallbacks(object : ActivityLifecycleCallbacks { override fun onActivityCreated( activity: Activity, savedInstanceState: Bundle? ) { catracer.getOngoingSpan(AppSpans.COLD_APP_STARTUP) ?.putAttribute("has_been_started_by_user", "true") }   override fun onActivityResumed(activity: Activity) { catracer.getOngoingSpan(AppSpans.COLD_APP_STARTUP)?.stop() unregisterActivityLifecycleCallbacks(this) } //... Kotlin 69

Slide 70

Slide 70 text

/** * 仕組みの説明 * 1) この Runnable は、App#onCreate 内でメインスレッドにポストされる * 2) アプリプロセスがバックグラウンドから起動された場合、この Runnable は どの Activity#onCreate よりも先に実行される * 3) 一方、アプリがユーザーによって起動された場合は、必ず Activity#onCreate がこの Runnable より先にシステムによってスケジューリングされる */ class DetectStartFromBackgroundRunnable( private val tracer: Catracer ) : Runnable { override fun run() { val span = tracer.getOngoingSpan(AppSpans.COLD_APP_STARTUP_SPAN) ?: return if (!span.attributes.containsKey("has_been_started_by_user")) { // スパンをキャンセルするか、もしくはあえてバックグラウンドのケースを記録するか } } } Kotlin 70

Slide 71

Slide 71 text

アプリ起動時間 71

Slide 72

Slide 72 text

アプリ起動時間の測定結果 72

Slide 73

Slide 73 text

アプリ起動時間の測定結果 73

Slide 74

Slide 74 text

実際の測定 アプリ起動時間(Cold)   ネットワークリクエストの詳細トレース 74

Slide 75

Slide 75 text

ネットワークリクエストの測定 75 fun provideOkHttpClient(tracer: Catracer): OkHttpClient { return OkHttpClient.Builder() .eventListenerFactory { TimingEventListener(tracer) } .build() } Kotlin override fun dnsStart(call: Call, domainName: String) { super.dnsStart(call, domainName) dnsSpan = startSpanForMetric("dns_lookup") } override fun dnsEnd(call: Call, domainName: String, inetAddressList: List) { super.dnsEnd(call, domainName, inetAddressList) dnsSpan?.stop() } Kotlin

Slide 76

Slide 76 text

76 ネットワークリクエストの測定結果

Slide 77

Slide 77 text

このセッションのゴール Performance for CVR!パフォーマンス管理の重要性 既存ツール (Firebase, Systrace) のスケールにおける限界 分散トレーシングの概念と有効性の説明 モバイルアプリでの分散トレーシング(OTel)の導入方法 Otelの運用の具体的なイメージ 77

Slide 78

Slide 78 text

OpenTelemetryの可視化と運用 78 測定から観測まで

Slide 79

Slide 79 text

バックエンド 79

Slide 80

Slide 80 text

OpenTelemetry Collector 80

Slide 81

Slide 81 text

OpenTelemetry Collector 81

Slide 82

Slide 82 text

OpenTelemetry Collector 82

Slide 83

Slide 83 text

OTel Collector の正体 83 …

Slide 84

Slide 84 text

OpenTelemetry Collector 84 ● 非柔軟 ● 単一障害点 ● 帯域幅が狭い ● スケール不能 ● OTelを始めるのに最適 ● アプリとコレクターの 関係が明確 …

Slide 85

Slide 85 text

Gateway 設定パターン 85 1. アプリからSDK経由でOTLPデータをLoadBalancer(nginx)に送信 2. nginxは負荷を分散し、複数のコレクターに転送 3. コレクターはデータを複数のバックエンドに送信 * https://opentelemetry.io/docs/collector/deployment/gateway/ Gateway | OpenTelemetry …

Slide 86

Slide 86 text

OpenTelemetry を立ち上げる 86

Slide 87

Slide 87 text

Agent Collector + Jaegerの設定 87

Slide 88

Slide 88 text

Collector + Jaegerエクスポーターの設定 88 > andousan@andousan otel % docker compose -f docker-compose.yaml up bash

Slide 89

Slide 89 text

Collector + Jaegerエクスポーターの設定 89

Slide 90

Slide 90 text

Agent Collector (複数バックエンド) 90

Slide 91

Slide 91 text

Grafana (alloy) 91

Slide 92

Slide 92 text

Honeycomb 92

Slide 93

Slide 93 text

DataDog 93

Slide 94

Slide 94 text

結果考察 94

Slide 95

Slide 95 text

導入前後での観測プロセスの変化 95 Before After

Slide 96

Slide 96 text

パフォーマンス問題調査のフロー(FPM) ケース:画像表示までの時間が閾値を超える場合がある 1. Firebase Performanceでのパフォーマンスの異変に気づく 2. Android Studioのプロファイラーを利用して手元で調査する 3. ユーザー環境の問題の場合は調査が難航する 4. この先は手探り 96

Slide 97

Slide 97 text

Firebase Performance (before) ● 遅いのか ● 早いのか 97

Slide 98

Slide 98 text

パフォーマンス問題調査のフロー(Otel) ケース:画像表示までの時間が閾値を超える場合がある 1. 関連のログ・メトリックス等を参考にする 2. モニタリングの時点でボトルネックを特定する 3. パフォーマンスを改善する策を検討する 4. 対策を入れたらリリース(Feature Toggle利用可) 98

Slide 99

Slide 99 text

OpenTelemetry + Grafana (after) 99

Slide 100

Slide 100 text

OpenTelemetry + Grafana (after) 100

Slide 101

Slide 101 text

OpenTelemetry + Grafana (after) 101

Slide 102

Slide 102 text

Firebase Performance vs OTel 102 Firebase Performance OpenTelemetry SDK Open Source ❌ 🟢 Vendor-Lock Free ❌ 🟢 分散トレーシング対応 ❌ 🟢 自動計装 🟢 🔺(独自実装) サンプリング制御可否 ❌ 🟢 バックエンド・インフラの 分散トレース ❌ 🟢 Logの対応 ❌ 🟢

Slide 103

Slide 103 text

NEXT 103

Slide 104

Slide 104 text

OTel SDKのオート計装 一部の実装ではFirebase Performanceのようにオート計装 (Zero-code instrumentation)を利用可能 ○ ANR(応答停止): 約5秒以上 UI が応答しない場合に Tracing 発行 ○ クラッシュ(未処理例外): 例外の名前、メッセージ、スレッド情報など付きで Tracing 発行 ○ フレームの描画が >16ms(slow)または >700ms(frozen)において Tracing 発行 ○ Fragment ライフサイクルや UI インタラクションも自動追跡可能 104

Slide 105

Slide 105 text

AIと連携したモニタリングについて ● MCPのサーバーなどを利用すれば、任意のトレースに対して モニタリングの段階で以下のような改善の提案をしてくれる 105 In mobile apps, you can preload a connection before it’s actually needed: For example, as soon as the splash screen appears, fire an async HEAD or small request to the API to open the TCP/TLS connection. By the time the UI needs data, the connection is already live.

Slide 106

Slide 106 text

AIと連携したモニタリングについて ● 一部のバックエンドではすでに導入されている 106

Slide 107

Slide 107 text

KMP対応の OpenTelemetry SDK ● expect/actual で 各プラットフォームで実装を指定すれば KMPを 対応したSDKが実装可能 ○ Android: opentelemetry-kotlin SDK ○ iOS: opentelemetry-swift SDK 107

Slide 108

Slide 108 text

オミットしたテーマ ● アプリのバックエンドのテレメトリデータ連携 (Context propagation) ● Baggage ● 指標とメトリックスの関連付け(Examplar) ● ランタイムプロファイラ(α) ● などなど 108

Slide 109

Slide 109 text

ご清聴ありがとうございました 109