进程内跟踪(实验性)

新的 androidx.tracing:tracing:2.0.0-alpha04 库是一个低开销 Kotlin API,可用于捕获进程内跟踪事件。这些事件可以捕获时间切片及其上下文。此外,该库还支持 Kotlin 协程的上下文传播。

该库使用 Android 开发者熟悉的 Perfetto 跟踪数据包格式。此外,跟踪 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 用于定义如何序列化跟踪数据包。跟踪 2.0.0 附带一个使用 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.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 的实例后,获取 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 {
        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 时添加的键值对。

包含其他元数据的基本 Perfetto 轨迹的屏幕截图

图 3. 包含其他元数据的基本 Perfetto 跟踪记录的屏幕截图。

上下文传播

使用 Kotlin 协程(或其他有助于处理并发工作负载的类似框架)时,跟踪 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 何时被 挂起恢复

例如,您可以看到切片 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 {
        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 记录进程内跟踪事件。获得这两个 跟踪文件后,您可以使用 Perfetto 中的 Open Multiple Trace Files 选项。

在 Perfetto 界面中打开多个轨迹文件

图 6. 在 Perfetto 界面中打开多个跟踪文件。

高级工作流程

关联切片

有时,将跟踪记录中的切片归因于更高级别的用户操作或系统事件会很有用。例如,如需将与通知中的某些后台工作对应的所有切片归因于通知,您可以执行以下操作:

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 跟踪记录的屏幕截图。