新的 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 还将允许开发者控制他们感兴趣的轨迹类别(或在某个类别嘈杂时将其停用)。
首先,创建 TraceSink 和 TraceDriver 的实例。
/**
* 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)
}
}
}
这会生成以下轨迹。
图 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") {
// ...
}
}
这会生成以下轨迹。
图 2. 包含嵌套部分的 Perfetto 基本轨迹的屏幕截图。
您可以看到,主线程轨道中存在重叠的事件。很明显,processImage 在同一线程上调用 loadImage 和 sharpen。
在轨迹部分中添加其他元数据
有时,将额外的上下文元数据附加到轨迹切片会很有用,这样可以获取更多详细信息。此类元数据的一些示例可能包括用户所处的 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 时添加的键值对。
图 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")
}
}
这会产生以下结果。
图 4。 包含上下文传播的基本 Perfetto 轨迹的屏幕截图。
借助上下文传播,您可以更轻松地直观呈现执行流程。您可以准确了解哪些任务相关联(与其他任务相关联),以及 Threads 何时暂停和恢复。
例如,您可以看到切片 main 派生了 taskOne 和 taskTwo。
之后,两个线程都处于非活跃状态(因为协程因使用 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()
}
}
这会产生以下结果。
图 5. 手动进行上下文传播的基本 Perfetto 轨迹的屏幕截图。
您可以看到,执行从 CoroutineContext 开始,随后切换到 Java Executor,但我们仍然能够使用上下文传播。
与系统轨迹相结合
新的 androidx.tracing 不会捕获 CPU 调度、内存使用情况以及应用与操作系统的一般交互等信息。这是因为该库提供了一种执行低开销进程内跟踪的方法。
不过,如果需要,将系统轨迹与进程内轨迹合并并将其可视化为单个轨迹非常简单。这是因为 Perfetto UI 支持在统一的时间轴上直观呈现设备中的多个轨迹文件。
为此,您可以按照此处的说明使用 Perfetto UI 启动系统轨迹记录会话。
您还可以在系统跟踪开启时使用 Tracing 2.0 API 记录进程内跟踪事件。获得两个轨迹文件后,您可以使用 Perfetto 中的 Open Multiple Trace Files 选项。
图 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)
}
}
}
这会产生以下结果。
图 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)
}
}
}
这会产生以下结果。
图 8. 包含调用栈信息的 Perfetto 轨迹的屏幕截图。