程序內追蹤 (實驗功能)

新版 androidx.tracing:tracing:2.0.0-alpha01 程式庫是低負載的 Kotlin API,可擷取程序內追蹤事件。這些事件可以擷取時間片段及其脈絡。此外,這個程式庫也支援 Kotlin 協同程式的內容傳播。

這個程式庫採用 Android 開發人員熟悉的 Perfetto 追蹤封包格式。此外,與 1.0.0-* API 不同,Tracing 2.0 支援可外掛的追蹤後端接收器的概念,因此其他追蹤程式庫可以自訂輸出追蹤格式,以及實作中的內容傳播方式。

依附元件

如要開始追蹤,您需要在 build.gradle.kts 中定義下列依附元件。

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

如果您是以 Android 程式庫或應用程式為目標,請宣告 androidx.tracing:tracing-wire-android:2.0.0-alpha01 的依附元件。如果您以 JVM 為目標,可以使用 androidx.tracing:tracing-wire-desktop:2.0.0-alpha01 依附元件。

基本用法

TraceSink 會定義追蹤封包的序列化方式。Tracing 2.0.0 隨附 Sink 的實作項目,可使用 Perfetto 追蹤記錄封包格式。TraceDriver 提供 Tracer 的控制代碼,可用於完成追蹤。

如果您選擇不在某些應用程式變體中追蹤,也可以使用 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.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 的執行個體後,請取得 Tracer,這個執行個體會定義所有追蹤 API 的進入點。

// 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 {
        driver.tracer.trace(category = CATEGORY_MAIN, name = "basic") {
            Thread.sleep(100L)
        }
    }
}

這會產生下列追蹤記錄。

基本 Perfetto 追蹤記錄的螢幕截圖

圖 1. 基本 Perfetto 追蹤記錄的螢幕截圖。

您會看到正確的程序和執行緒軌跡已填入,並產生單一追蹤記錄區段 basic,該區段執行了 100ms

追蹤區段 (或切片) 可在同一軌上巢狀排列,代表重疊事件。範例如下:

fun main() {
    // Initialize the tracing infrastructure to monitor app performance
    val driver = createTraceDriver()
    val tracer = driver.tracer
    driver.use {
        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 {
        driver.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 時新增的鍵/值組合。

含有額外中繼資料的基本 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 {
        tracer.traceCoroutine(category = CATEGORY_MAIN, name = "main") {
            coroutineScope {
                launch { taskOne(tracer) }
                launch { taskTwo(tracer) }
            }
        }
        println("All done")
    }
}

這會產生下列結果。

螢幕截圖:Perfetto 追蹤記錄,顯示情境傳播

圖 4. 基本 Perfetto 追蹤記錄的螢幕截圖,其中包含內容傳播。

脈絡傳播功能可大幅簡化執行流程的視覺化。您可以查看哪些工作相關 (與其他工作連結),以及工作Threads暫停繼續的時間。

舉例來說,您可以看到切片 main 衍生出 taskOnetaskTwo。之後兩個執行緒都會處於閒置狀態 (因為協同程式已暫停 - 這是使用 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 {
        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 記錄程序內追蹤記錄事件。取得這兩個追蹤記錄檔案後,即可在 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 {
        driver.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 {
        driver.tracer.trace(
            category = CATEGORY_MAIN,
            name = "callStackEntry",
            metadataBlock = {
                addCallStackEntry(
                    name = "main",
                    lineNumber = 14,
                    sourceFile = "Basic.kt"
                )
            }
        ) {
            Thread.sleep(100L)
        }
    }
}

這會產生下列結果。

螢幕截圖:Perfetto 追蹤記錄,內含呼叫堆疊資訊

圖 8. Perfetto 追蹤記錄的螢幕截圖,顯示呼叫堆疊資訊。