プロセス内トレース(試験運用版)

新しい androidx.tracing:tracing:2.0.0-alpha04 ライブラリは、オーバーヘッドの少ない プロセス内トレース イベントのキャプチャを可能にする Kotlin API です。これらのイベントは、タイムスライスとそのコンテキストをキャプチャできます。このライブラリは、Kotlin コルーチンのコンテキスト伝播もサポートしています。

このライブラリは、Android デベロッパーが使い慣れている Perfettoトレース パケット形式を使用します。また、Tracing 2.0 は(1.0.0-* API とは異なり) **プラグ可能なトレース バックエンド** と**シンク** の概念をサポートしているため、他の トレース ライブラリは出力トレース形式と、実装でのコンテキスト 伝播の仕組みを**カスタマイズ** できます。

依存関係

トレースを開始するには、build.gradle.kts で次の依存関係を定義する必要があります。

kotlin {
  androidLibrary {
    namespace = "com.example.library"
    // ...
  }
  sourceSets {
    androidMain {
      dependencies {
        api("androidx.tracing:tracing-wire:2.0.0-alpha04")
        // ...
      }
    }
    jvmMain {
      dependencies {
        api("androidx.tracing:tracing-wire:2.0.0-alpha04")
        // ...
      }
    }
  }
}

Android ライブラリ、Android アプリケーションをターゲットとする場合、または JVM をターゲットとする場合は、androidx.tracing:tracing-wire:2.0.0-alpha04 への依存関係を宣言します。

基本的な使用方法

TraceSink は、トレース パケットのシリアル化方法を定義します。Tracing 2.0.0 には、Perfetto トレース パケット形式を使用するシンクの実装が付属しています。TraceDriverTracer へのハンドルを提供し、トレースを終了するために使用できます。

一部のアプリ バリアントでトレースしない場合は、TraceDriver を使用してアプリ内のすべてのトレースポイントを無効にすることもできます。 TraceDriver の今後の API では、デベロッパーはキャプチャするトレース カテゴリ(またはカテゴリがノイズの多い場合に無効にするカテゴリ)を制御することもできます。

まず、TraceSinkTraceDriver のインスタンスを作成します。

/**
 * A [TraceSink] defines how traces are serialized.
 *
 * [androidx.tracing.wire.TraceSink] uses the `Perfetto` trace packet format.
 */
fun createSink(): TraceSink {
    val outputDirectory = File(/* path = */ "/tmp/perfetto")
    if (!outputDirectory.exists()) {
        outputDirectory.mkdirs()
    }
    // We are using the factory function defined in androidx.tracing.wire
    return TraceSink(
        sequenceId = 1,
        directory = outputDirectory
    )
}
/**
 * Creates a new instance of [androidx.tracing.wire.TraceDriver].
 */
fun createTraceDriver(): TraceDriver {
    // We are using a factory function from androidx.tracing.wire here.
    // `isEnabled` controls whether tracing is enabled for the application.
    val driver = TraceDriver(sink = createSink(), isEnabled = true)
    return driver
}

TraceDriver のインスタンスを取得したら、すべてのトレース API のエントリ ポイントを定義する Tracer を取得します。

// Tracing Categories identify subsystems that are responsible
// in generating trace sections. Future APIs in `TraceDriver` will allow the
// application to specify which categories they are interested in tracing.
// This lets the application disable entire trace categories, without
// needing to disable trace instrumentation at the call sites for those
// categories.

internal const val CATEGORY_MAIN = "main"

fun main() {
    val driver = createTraceDriver()
    driver.use {
        it.tracer.trace(category = CATEGORY_MAIN, name = "basic") {
            Thread.sleep(100L)
        }
    }
}

これにより、次のトレースが生成されます。

基本的な Perfetto トレースのスクリーン キャプチャ

図 1. 基本的な Perfetto トレースの画面キャプチャ。

正しいプロセスとスレッドのトラックが入力され、100ms 実行された単一のトレース セクション basic が生成されていることがわかります。

トレース セクション(またはスライス)は、同じトラックにネストして、重複するイベントを表すことができます。次に例を示します。

fun main() {
    // Initialize the tracing infrastructure to monitor app performance
    val driver = createTraceDriver()
    val tracer = driver.tracer
    driver.use {
        it.tracer.trace(
            category = CATEGORY_MAIN,
            name = "processImage",
        ) {
            // Load the data first, then apply the sharpen filter
            sharpen(tracer = tracer, output = loadImage(tracer))
        }
    }
}

internal fun loadImage(tracer: Tracer): ByteArray {
    return tracer.trace(CATEGORY_MAIN, "loadImage") {
        // Loads an image
        // ...
        // A placeholder
        ByteArray(0)
    }
}

internal fun sharpen(tracer: Tracer, output: ByteArray) {
    // ...
    tracer.trace(CATEGORY_MAIN, "sharpen") {
        // ...
    }
}

これにより、次のトレースが生成されます。

ネストされたセクションを含む基本的な Perfetto トレースの画面キャプチャ

図 2. ネストされたセクションを含む基本的な Perfetto トレースの画面キャプチャ。

メインスレッド トラックに重複するイベントがあることがわかります。processImage が同じスレッドで loadImagesharpen を呼び出していることが明確にわかります。

トレース セクションに追加のメタデータを追加する

詳細情報を取得するために、トレース スライスに追加のコンテキスト メタデータをアタッチすると便利な場合があります。このようなメタデータの例としては、ユーザーが使用している nav destination や、関数の実行時間を決定する input arguments などがあります。

fun main() {
    val driver = createTraceDriver()
    driver.use {
        it.tracer.trace(
            category = CATEGORY_MAIN,
            name = "basicWithContext",
            // Add additional metadata
            metadataBlock = {
                // Add key value pairs.
                addMetadataEntry("key", "value")
                addMetadataEntry("count", 1L)
            }
        ) {
            Thread.sleep(100L)
        }
    }
}

これにより、次の結果が生成されます。Arguments セクションには、slice の生成時に追加された Key-Value ペアが含まれています。

追加のメタデータを含む基本的な Perfetto トレースのスクリーン キャプチャ

図 3. 追加のメタデータを含む基本的な Perfetto トレースの画面キャプチャ。

コンテキスト伝播

Kotlin コルーチン(または同時ワークロードに役立つ他の同様のフレームワーク)を使用する場合、Tracing 2.0 はコンテキスト伝播の概念をサポートします。 例で説明するのが最適です。

suspend fun taskOne(tracer: Tracer) {
    tracer.traceCoroutine(category = CATEGORY_MAIN, "taskOne") {
        delay(timeMillis = 100L)
    }
}

suspend fun taskTwo(tracer: Tracer) {
    tracer.traceCoroutine(category = CATEGORY_MAIN, "taskTwo") {
        delay(timeMillis = 50L)
    }
}

fun main() = runBlocking(context = Dispatchers.Default) {
    val driver = createTraceDriver()
    val tracer = driver.tracer
    driver.use {
        it.tracer.traceCoroutine(category = CATEGORY_MAIN, name = "main") {
            coroutineScope {
                launch { taskOne(tracer) }
                launch { taskTwo(tracer) }
            }
        }
        println("All done")
    }
}

これにより、次の結果が生成されます。

コンテキスト伝播を含む Perfetto トレースのスクリーン キャプチャ

図 4. コンテキスト伝播を含む基本的な Perfetto トレースの画面キャプチャ。

コンテキスト伝播により、実行フローを視覚化 することがはるかに簡単になります。どのタスクが関連していたか(他のタスクに接続されていたか)、Threads がいつ一時停止 および再開 されたかを正確に確認できます。

たとえば、スライス maintaskOnetaskTwo を生成したことがわかります。 その後、両方のスレッドが非アクティブになりました(コルーチンが一時停止したため - delay の使用による)。

手動伝播

Kotlin コルーチンを使用して同時ワークロードを Java Executor のインスタンスと混在させる場合は、コンテキストを一方から他方に伝播すると便利な場合があります。次に例を示します。

fun executorTask(
    tracer: Tracer,
    token: PropagationToken,
    executor: Executor,
    callback: () -> Unit
) {
    executor.execute {
        tracer.trace(
            category = CATEGORY_MAIN,
            name = "executeTask",
            token = token,
        ) {
            // Do something
            Thread.sleep(100)
            callback()
        }
    }
}

@OptIn(DelicateTracingApi::class)
fun main() = runBlocking(context = Dispatchers.Default) {
    val driver = createTraceDriver()
    val executor = Executors.newSingleThreadExecutor()
    val tracer = driver.tracer
    driver.use {
        it.tracer.traceCoroutine(category = CATEGORY_MAIN, name = "main") {
            coroutineScope {
                val deferred = CompletableDeferred<Unit>()
                executorTask(
                    tracer = tracer,
                    // Obtain the propagation token from the CoroutineContext
                    token = tracer.tokenFromCoroutineContext(),
                    executor = executor,
                    callback = {
                        deferred.complete(Unit)
                    }
                )
                deferred.await()
            }
        }
        executor.shutdownNow()
    }
}

これにより、次の結果が生成されます。

手動コンテキスト伝播を含む Perfetto トレースの画面キャプチャ

図 5. 手動コンテキスト伝播を含む基本的な Perfetto トレースの画面キャプチャ。

実行は CoroutineContext で開始され、その後 Java Executor に切り替わりましたが、コンテキスト伝播を使用できました。

システム トレースと組み合わせる

新しい androidx.tracing は、CPU スケジューリング、メモリ使用量、オペレーティング システムとのアプリのインタラクションなどの情報をキャプチャしません。これは、このライブラリがオーバーヘッドの少ないプロセス内トレース を実行する方法を提供するためです。

ただし、必要に応じて、システム トレースをプロセス内トレースとマージして、単一のトレースとして視覚化することは非常に簡単です。これは、Perfetto UI がデバイスから複数のトレースファイルを統合されたタイムラインで視覚化することをサポートしているためです。

これを行うには、こちらの手順に沿って Perfetto UI を使用して システム トレース セッションを開始します

システム トレースがオンになっているときに、Tracing 2.0 API を使用してプロセス内トレース イベントを記録することもできます。[both] のトレースファイルがある場合は、Perfetto の Open Multiple Trace Files オプションを使用できます。

Perfetto UI で複数のトレース ファイルを開く

図 6. Perfetto UI で複数のトレースファイルを開く。

高度なワークフロー

スライスを関連付ける

トレース内のスライスを、より上位のユーザー アクションまたはシステム イベントに関連付けると便利な場合があります。たとえば、バックグラウンド処理に対応するすべてのスライスを通知の一部として関連付けるには、次のようにします。

fun main() {
    val driver = createTraceDriver()
    onEvent(driver, eventId = EVENT_ID)
}

fun onEvent(driver: TraceDriver, eventId: Long) {
    driver.use {
        it.tracer.trace(
            category = CATEGORY_MAIN,
            name = "step-1",
            metadataBlock = {
                addCorrelationId(eventId)
            }
        ) {
            Thread.sleep(100L)
        }

        Thread.sleep(20)

        driver.tracer.trace(
            category = CATEGORY_MAIN,
            name = "step-2",
            metadataBlock = {
                addCorrelationId(eventId)
            }
        ) {
            Thread.sleep(180)
        }
    }
}

これにより、次の結果が生成されます。

関連付けられたスライスを含む Perfetto トレースの画面キャプチャ

図 7. 関連付けられたスライスを含む Perfetto トレースの画面キャプチャ。

コールスタック情報を追加する

ホスト側のツール(コンパイラ プラグイン、アノテーション プロセッサなど)では、コールスタック情報をトレースに埋め込むこともできます。これにより、トレース内でトレース セクションの生成を担当するファイル、クラス、メソッドを簡単に見つけることができます。

fun main() {
    val driver = createTraceDriver()
    driver.use {
        it.tracer.trace(
            category = CATEGORY_MAIN,
            name = "callStackEntry",
            metadataBlock = {
                addCallStackEntry(
                    name = "main",
                    lineNumber = 14,
                    sourceFile = "Basic.kt"
                )
            }
        ) {
            Thread.sleep(100L)
        }
    }
}

これにより、次の結果が生成されます。

コールスタック情報を含む Perfetto トレースのスクリーン キャプチャ

図 8. コールスタック情報を含む Perfetto トレースの画面キャプチャ。