新的 androidx.tracing:tracing:2.0.0-alpha04 库是一个低开销的 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-alpha04")
// ...
}
}
jvmMain {
dependencies {
api("androidx.tracing:tracing-wire:2.0.0-alpha04")
// ...
}
}
}
}
如果您以 Android 库、Android 应用为目标平台,或者以 JVM 为目标平台,请声明对 androidx.tracing:tracing-wire:2.0.0-alpha04 的依赖关系。
基本用法
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.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)
}
}
}
这会生成以下轨迹。
图 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") {
// ...
}
}
这会生成以下轨迹。
图 2. 包含嵌套部分的 Perfetto 基本轨迹的屏幕截图。
您可以看到,主线程轨道中存在重叠的事件。很明显,processImage 在同一线程上调用 loadImage 和 sharpen。
在轨迹部分中添加其他元数据
有时,将额外的上下文元数据附加到轨迹切片会很有用,这样可以获取更多详细信息。此类元数据的示例可能包括用户所处的 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 时添加的键值对。
图 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 {
it.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 {
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()
}
}
这会产生以下结果。
图 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 {
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)
}
}
}
这会产生以下结果。
图 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)
}
}
}
这会产生以下结果。
图 8. 包含调用堆栈信息的 Perfetto 轨迹的屏幕截图。