进程内跟踪(实验性)

新的 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 附带一个使用 Perfetto 轨迹数据包格式的 Sink 实现。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 轨迹的屏幕截图。

您可以看到,系统已填充正确的进程和线程轨迹,并生成了一个运行时间为 100ms 的轨迹部分 basic

轨迹部分(或切片)可以嵌套在同一轨道上,以表示重叠的事件。示例如下。

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 界面中打开多个轨迹文件

图 6. 在 Perfetto 界面中打开多个轨迹文件。

高级工作流程

关联切片

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

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 轨迹的屏幕截图。