プロセス内トレース

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

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

依存関係

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

Kotlin マルチプラットフォーム プロジェクト

トレース イベントの出力のみが必要なライブラリは、軽量の androidx.tracing:tracing API に依存する必要があります。トレース バックエンドを構成するアプリも androidx.tracing:tracing-wire に依存する必要があります。

kotlin {
  sourceSets {
    commonMain {
      dependencies {
        // API definition
        implementation("androidx.tracing:tracing:2.0.0-beta01")
      }
    }
    androidMain {
      dependencies {
        // Android implementation (includes the Perfetto Sink and automatic initialization)
        implementation("androidx.tracing:tracing-wire:2.0.0-beta01")
      }
    }
    jvmMain {
      dependencies {
        // JVM implementation
        implementation("androidx.tracing:tracing-wire:2.0.0-beta01")
      }
    }
  }
}

Android 専用プロジェクト

Android のみをターゲットとする場合は、アプリケーションまたはライブラリの build.gradle.kts ファイルに次のコードを追加します。

dependencies {
    // For libraries and applications to emit events
    implementation("androidx.tracing:tracing:2.0.0-beta01")

    // For applications to configure the tracing backend
    implementation("androidx.tracing:tracing-wire:2.0.0-beta01")
}

初期化と検出

トレース イベントを記録する前に、トレース インフラストラクチャを初期化する必要があります。これには、AbstractTraceDriver を作成し、その Tracer をグローバルに登録することが含まれます。

Android

Android では、androidx.tracing:tracing-wire 依存関係を含めると、androidx.startup ライブラリを使用してアプリケーションの起動時に初期化が自動的に行われます。

デフォルトでは、この自動初期化は次の処理を行います。

  • Perfetto トレースファイルを Context.noBackupFilesDir/perfetto_traces/ に書き込む TraceSink を含む TraceDriver を作成します。

  • 結果の Tracer をグローバルに登録します。

TraceDriver インスタンスをカスタマイズする

構成をカスタマイズする必要がある場合(トレース ファイルの保存場所を変更する、カスタム TraceSink を使用するなど)、独自の AbstractTraceDriver インスタンスを指定できます。

構成をカスタマイズするには、Application クラスに AbstractTraceDriver.Factory を実装します。

import android.app.Application
import androidx.tracing.AbstractTraceDriver
import androidx.tracing.wire.TraceDriver
import androidx.tracing.wire.TraceSink
import java.io.File

class App : Application(), AbstractTraceDriver.Factory {
    override fun create(): AbstractTraceDriver {
        val sink = TraceSink(
            context = this,
            fileProvider = { File(noBackupFilesDir, "traces") },
        )
        // Return the custom TraceDriver
        // You can also fully customize the instance of Tracer
        return TraceDriver(context = this, sink = sink)
    }
}

自動イニシャライザは、Application サブクラスが Factory を実装し、カスタム ドライバ ファクトリを使用していることを検出します。

JVM

JVM には自動ブートストラップ メカニズムはありません。アプリケーションは、起動時に TraceDriver を初期化し、Tracer をグローバルに登録する役割を担います。通常は main 関数で行います。

トレーサーを登録するには、Tracer.setGlobalTracer() を呼び出します。

import androidx.tracing.Tracer
import androidx.tracing.DelicateTracingApi
import androidx.tracing.wire.TraceDriver
import androidx.tracing.wire.TraceSink
import java.io.File

fun main() {
    // Create the TraceSink, and the `TraceDriver`
    val outputDirectory = File("/tmp/perfetto")
    val sink = TraceSink(directory = outputDirectory)
    val driver = TraceDriver(sink = sink, isEnabled = true)

    // Register the tracer
    @OptIn(DelicateTracingApi::class)
    Tracer.setGlobalTracer(driver.tracer)

    // Call driver.close() as a result of the process shutdown hook.
    Runtime.getRuntime().addShutdownHook(Thread {
        driver.close()
    })
}

基本的な使用方法

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

Tracer が初期化されたら(Android では自動、JVM では手動)、グローバル Tracer.global インスタンスを使用してトレース イベントを出力します。

また、一部のアプリ バリアントでトレースをまったく行わない場合は、TraceDriver を使用してアプリ内のすべてのトレースポイントを無効にすることもできます。TraceDriver のインスタンスを作成するときに isCategoryEnabled の実装を提供することで、特定の category のトレースポイントをオプションで有効にできます。

val driver = TraceDriver(
    sink = sink,
    isCategoryEnabled = { category ->
        // Only enable trace points in the "com.example" package
        category.startsWith("com.example")
    }
)

JVM で Tracer.global を使用してトレースイベントを送信する基本的な例を以下に示します(手動設定を含む)。

import androidx.tracing.Tracer
import androidx.tracing.DelicateTracingApi
import androidx.tracing.wire.TraceDriver
import androidx.tracing.wire.TraceSink
import java.io.File

// Category names should also follow the same convention used for package names
// on Android and Java. This makes them easier to identify and filter.
internal const val CATEGORY_MAIN = "com.example"

fun createSink(): TraceSink {
    val outputDirectory = File("/tmp/perfetto")
    if (!outputDirectory.exists()) {
        outputDirectory.mkdirs()
    }
    return TraceSink(directory = outputDirectory)
}

fun createTraceDriver(): TraceDriver {
    return TraceDriver(sink = createSink(), isCategoryEnabled = {true})
}

fun main() {
    val driver = createTraceDriver()
    @OptIn(DelicateTracingApi::class)
    Tracer.setGlobalTracer(driver.tracer)

    driver.use {
        Tracer.global.trace(category = CATEGORY_MAIN, name = "basic") {
            // The block of code that needs to be traced.
            Thread.sleep(100L)
        }
    }
}

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

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

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

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

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

fun main() {
    val driver = createTraceDriver()
    @OptIn(DelicateTracingApi::class)
    Tracer.setGlobalTracer(driver.tracer)

    driver.use {
        Tracer.global.trace(
            category = CATEGORY_MAIN,
            name = "processImage",
        ) {
            // Load the data first, then apply the sharpen filter
            sharpen(output = loadImage())
        }
    }
}

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

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

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

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

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

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

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

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

fun main() {
    val driver = createTraceDriver()
    @OptIn(DelicateTracingApi::class)
    Tracer.setGlobalTracer(driver.tracer)

    driver.use {
        Tracer.global.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.global.traceCoroutine(category = CATEGORY_MAIN, "taskOne") {
        delay(timeMillis = 100L)
    }
}

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

fun main() = runBlocking(context = Dispatchers.Default) {
    val driver = createTraceDriver()
    @OptIn(DelicateTracingApi::class)
    Tracer.setGlobalTracer(driver.tracer)

    driver.use {
        Tracer.global.traceCoroutine(category = CATEGORY_MAIN, name = "main") {
              taskOne()
              taskTwo()
            }
        }
        println("All done")
    }
}

結果は次のようになります。

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

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

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

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

手動伝播

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

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

fun main() = runBlocking(context = Dispatchers.Default) {
    val driver = createTraceDriver()
    @OptIn(DelicateTracingApi::class)
    Tracer.setGlobalTracer(driver.tracer)

    val executor = Executors.newSingleThreadExecutor()
    driver.use {
        Tracer.global.traceCoroutine(category = CATEGORY_MAIN, name = "main") {
            coroutineScope {
                val deferred = CompletableDeferred<Unit>()
                executorTask(
                    // Obtain the propagation token from the CoroutineContext
                    token = Tracer.global.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 を使用してインプロセス トレース イベントを記録することもできます。両方のトレース ファイルを取得したら、Perfetto の Open Multiple Trace Files オプションを使用できます。

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

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

高度なワークフロー

このセクションでは、インプロセス トレース ライブラリで実装できる高度なワークフローについて説明します。

スライスを関連付ける

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

fun main() {
    val driver = createTraceDriver()
    @OptIn(DelicateTracingApi::class)
    Tracer.setGlobalTracer(driver.tracer)

    driver.use {
        onEvent(eventId = EVENT_ID)
    }
}

fun onEvent(eventId: Long) {
    Tracer.global.trace(
        category = CATEGORY_MAIN,
        name = "step-1",
        metadataBlock = {
            addCorrelationId(eventId)
        }
    ) {
        Thread.sleep(100L)
    }

    Thread.sleep(20)

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

結果は次のようになります。

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

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

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

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

fun main() {
    val driver = createTraceDriver()
    @OptIn(DelicateTracingApi::class)
    Tracer.setGlobalTracer(driver.tracer)

    driver.use {
        Tracer.global.trace(
            category = CATEGORY_MAIN,
            name = "callStackEntry",
            metadataBlock = {
                addCallStackEntry(
                    name = "main",
                    lineNumber = 14,
                    sourceFile = "Basic.kt"
                )
            }
        ) {
            Thread.sleep(100L)
        }
    }
}

結果は次のようになります。

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

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