Новая библиотека androidx.tracing:tracing:2.0.0-alpha01 представляет собой низкозатратный API для Kotlin, позволяющий захватывать события трассировки внутри процесса. Эти события могут фиксировать временные срезы и их контекст. Библиотека также поддерживает распространение контекста для сопрограмм Kotlin.
Библиотека использует тот же формат пакетов трассировки Perfetto , с которым знакомы разработчики Android. Кроме того, Tracing 2.0 (в отличие от API версии 1.0.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 реализован объект Sink, использующий формат пакетов трассировки Perfetto . Объект TraceDriver предоставляет дескриптор Tracer и может использоваться для завершения трассировки.
Также с помощью TraceDriver можно отключить все точки трассировки в приложении, если в некоторых вариантах приложения вы решили вообще не использовать трассировку. В будущих API TraceDriver разработчики также смогут управлять тем, какие категории трассировки они хотят отслеживать (или отключать, если категория содержит много шума).
Для начала создайте экземпляры 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.
Вы можете видеть, что заполнены правильные дорожки процесса и потока, и был создан один 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") {
// ...
}
}
В результате генерируется следующий трассировочный файл.

Рисунок 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 Coroutines (или других подобных фреймворков, помогающих с параллельными нагрузками) 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 не собирает информацию о планировании работы ЦП, использовании памяти и взаимодействии приложений с операционной системой в целом. Это связано с тем, что библиотека предоставляет способ выполнения трассировки внутри процесса с минимальными накладными расходами .
Однако объединить системные трассировки с трассировками процессов и при необходимости визуализировать их в виде единой трассировки крайне просто. Это связано с тем, что Perfetto UI поддерживает визуализацию нескольких файлов трассировки с устройства на единой временной шкале.
Для этого вы можете запустить сеанс трассировки системы с помощью Perfetto UI , следуя инструкциям здесь .
Также можно записывать события трассировки в процессе выполнения, используя API Tracing 2.0 , при включенной системной трассировке. После получения обоих файлов трассировки можно использовать опцию « 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)
}
}
}
В результате получается следующее.

Рисунок 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 с информацией о стеке вызовов.