程序內追蹤 (實驗功能)

新版 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:2.0.0-alpha01")
        // ...
      }
    }
    jvmMain {
      dependencies {
        api("androidx.tracing:tracing-wire:2.0.0-alpha01")
        // ...
      }
    }
  }
}

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

基本用法

TraceSink 會定義追蹤封包的序列化方式。追蹤記錄 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 追蹤記錄的螢幕截圖,顯示呼叫堆疊資訊。