Библиотека androidx.tracing:tracing:2.0.0-beta01 — это низкозатратный API для Kotlin, позволяющий захватывать события трассировки внутри процесса. Эти события могут фиксировать временные срезы и их контекст. Библиотека также поддерживает распространение контекста для сопрограмм Kotlin.
Библиотека использует тот же формат пакетов трассировки Perfetto , с которым знакомы разработчики Android. Кроме того, Tracing 2.0 (в отличие от API версии 1.0.0-* ) поддерживает концепцию подключаемых бэкендов и приемников трассировки , поэтому другие библиотеки трассировки могут настраивать формат выходных данных трассировки и способ распространения контекста в своей реализации.
Зависимости
Для начала трассировки необходимо определить зависимости в файле build.gradle.kts .
Многоплатформенные проекты на Kotlin
Библиотеки, которым нужно только генерировать события трассировки, должны зависеть от легковесного API androidx.tracing:tracing . Приложения, которые настраивают бэкенд трассировки, также должны зависеть от androidx.tracing:tracing-wire .
kotlin {
sourceSets {
commonMain {
dependencies {
// API definition
implementation("androidx.tracing:tracing:2.0.0-beta01")
}
}
androidMain {
dependencies {
// Android implementation (includes the Perfetto Sink and automatic initialization)
implementation("androidx.tracing:tracing-wire:2.0.0-beta01")
}
}
jvmMain {
dependencies {
// JVM implementation
implementation("androidx.tracing:tracing-wire:2.0.0-beta01")
}
}
}
}
Проекты, предназначенные исключительно для Android
Если вы ориентируетесь только на Android, добавьте следующее в файл build.gradle.kts вашего приложения или библиотеки:
dependencies {
// For libraries and applications to emit events
implementation("androidx.tracing:tracing:2.0.0-beta01")
// For applications to configure the tracing backend
implementation("androidx.tracing:tracing-wire:2.0.0-beta01")
}
Инициализация и обнаружение
Прежде чем записывать события трассировки, необходимо инициализировать инфраструктуру трассировки. Это включает в себя создание объекта AbstractTraceDriver и глобальную регистрацию его Tracer .
Android
На Android, если добавить зависимость androidx.tracing:tracing-wire , инициализация происходит автоматически при запуске приложения с использованием библиотеки androidx.startup .
По умолчанию эта автоматическая инициализация выполняет следующие действия:
Создает
TraceDriverсTraceSink, который записывает файлы трассировки Perfetto вContext.noBackupFilesDir/perfetto_traces/.Регистрирует полученный
Tracerглобально.
Настройка экземпляра TraceDriver
Если вам необходимо настроить конфигурацию, например, изменить место сохранения файлов трассировки или использовать пользовательский TraceSink , вы можете предоставить собственный экземпляр AbstractTraceDriver .
Для настройки параметров необходимо, чтобы ваш класс Application реализовывал интерфейс AbstractTraceDriver.Factory :
import android.app.Application
import androidx.tracing.AbstractTraceDriver
import androidx.tracing.wire.TraceDriver
import androidx.tracing.wire.TraceSink
import java.io.File
class App : Application(), AbstractTraceDriver.Factory {
override fun create(): AbstractTraceDriver {
val sink = TraceSink(
context = this,
fileProvider = { File(noBackupFilesDir, "traces") },
)
// Return the custom TraceDriver
// You can also fully customize the instance of Tracer
return TraceDriver(context = this, sink = sink)
}
}
Автоматический инициализатор определяет, что ваш подкласс Application реализует интерфейс Factory , и использует вашу пользовательскую фабрику драйверов.
JVM
В JVM отсутствует механизм автоматической загрузки. Приложение само отвечает за инициализацию TraceDriver и глобальную регистрацию Tracer при запуске, чаще всего в main функции.
Для регистрации трассировщика вызовите Tracer.setGlobalTracer() .
import androidx.tracing.Tracer
import androidx.tracing.DelicateTracingApi
import androidx.tracing.wire.TraceDriver
import androidx.tracing.wire.TraceSink
import java.io.File
fun main() {
// Create the TraceSink, and the `TraceDriver`
val outputDirectory = File("/tmp/perfetto")
val sink = TraceSink(directory = outputDirectory)
val driver = TraceDriver(sink = sink, isEnabled = true)
// Register the tracer
@OptIn(DelicateTracingApi::class)
Tracer.setGlobalTracer(driver.tracer)
// Call driver.close() as a result of the process shutdown hook.
Runtime.getRuntime().addShutdownHook(Thread {
driver.close()
})
}
Основное использование
Объект TraceSink определяет способ сериализации пакетов трассировки. В Tracing 2.0.0 реализован объект Sink, использующий формат пакетов трассировки Perfetto . Объект TraceDriver предоставляет дескриптор Tracer и может использоваться для завершения трассировки.
После инициализации Tracer (автоматической на Android или ручной на JVM) используйте глобальный экземпляр Tracer.global для генерации событий трассировки.
Вы также можете использовать TraceDriver для отключения всех точек трассировки в приложении, если в некоторых вариантах приложения вы решили вообще не использовать трассировку. При желании вы можете включить точки трассировки для определенной category , предоставив реализацию для isCategoryEnabled при создании экземпляра TraceDriver .
val driver = TraceDriver(
sink = sink,
isCategoryEnabled = { category ->
// Only enable trace points in the "com.example" package
category.startsWith("com.example")
}
)
Вот простой пример генерации события трассировки с помощью Tracer.global в JVM, включая ручную настройку:
import androidx.tracing.Tracer
import androidx.tracing.DelicateTracingApi
import androidx.tracing.wire.TraceDriver
import androidx.tracing.wire.TraceSink
import java.io.File
// Category names should also follow the same convention used for package names
// on Android and Java. This makes them easier to identify and filter.
internal const val CATEGORY_MAIN = "com.example"
fun createSink(): TraceSink {
val outputDirectory = File("/tmp/perfetto")
if (!outputDirectory.exists()) {
outputDirectory.mkdirs()
}
return TraceSink(directory = outputDirectory)
}
fun createTraceDriver(): TraceDriver {
return TraceDriver(sink = createSink(), isCategoryEnabled = {true})
}
fun main() {
val driver = createTraceDriver()
@OptIn(DelicateTracingApi::class)
Tracer.setGlobalTracer(driver.tracer)
driver.use {
Tracer.global.trace(category = CATEGORY_MAIN, name = "basic") {
// The block of code that needs to be traced.
Thread.sleep(100L)
}
}
}
В результате генерируется следующий трассировочный файл.

Рисунок 1. Скриншот базовой трассировки Perfetto.
Вы можете видеть, что заполнены правильные треки процесса и потока, и что они создали один basic раздел трассировки, который длился 100 мс.
Участки (или срезы) трассировки могут быть вложены друг в друга на одной и той же дорожке для представления перекрывающихся событий. Вот пример.
fun main() {
val driver = createTraceDriver()
@OptIn(DelicateTracingApi::class)
Tracer.setGlobalTracer(driver.tracer)
driver.use {
Tracer.global.trace(
category = CATEGORY_MAIN,
name = "processImage",
) {
// Load the data first, then apply the sharpen filter
sharpen(output = loadImage())
}
}
}
internal fun loadImage(): ByteArray {
return Tracer.global.trace(CATEGORY_MAIN, "loadImage") {
// Loads an image
// ...
// A placeholder
ByteArray(0)
}
}
internal fun sharpen(output: ByteArray) {
// ...
Tracer.global.trace(CATEGORY_MAIN, "sharpen") {
// ...
}
}
В результате генерируется следующий трассировочный файл.

Рисунок 2. Скриншот базовой трассировки Perfetto с вложенными секциями.
Вы можете заметить, что в треке основного потока происходят перекрывающиеся события. Совершенно очевидно, что вызовы processImage loadImage и sharpen в одном и том же потоке.
Добавьте дополнительные метаданные в разделы трассировки.
Иногда бывает полезно добавить к срезу трассировки дополнительные контекстные метаданные, чтобы получить более подробную информацию. Примерами таких метаданных могут служить nav destination , на котором находится пользователь, или input arguments , которые могут определять время выполнения функции.
fun main() {
val driver = createTraceDriver()
@OptIn(DelicateTracingApi::class)
Tracer.setGlobalTracer(driver.tracer)
driver.use {
Tracer.global.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.global.traceCoroutine(category = CATEGORY_MAIN, "taskOne") {
delay(timeMillis = 100L)
}
}
suspend fun taskTwo() {
Tracer.global.traceCoroutine(category = CATEGORY_MAIN, "taskTwo") {
delay(timeMillis = 50L)
}
}
fun main() = runBlocking(context = Dispatchers.Default) {
val driver = createTraceDriver()
@OptIn(DelicateTracingApi::class)
Tracer.setGlobalTracer(driver.tracer)
driver.use {
Tracer.global.traceCoroutine(category = CATEGORY_MAIN, name = "main") {
taskOne()
taskTwo()
}
}
println("All done")
}
}
В результате получается следующее.

Рисунок 4. Скриншот базовой трассировки Perfetto с распространением контекста.
Распространение контекста значительно упрощает визуализацию потока выполнения . Вы можете точно увидеть, какие задачи были связаны (соединены с другими), и точно, когда Threads были приостановлены и возобновлены .
Например, вы можете видеть, что main поток среза породил taskOne и taskTwo . После этого оба потока стали неактивными, поскольку сопрограммы были приостановлены из-за использования delay .
Ручное распространение
Иногда, при одновременном выполнении нескольких задач с использованием корутин Kotlin и экземпляров Java Executor может быть полезно передавать контекст от одного объекта к другому. Вот пример:
fun executorTask(
token: PropagationToken,
executor: Executor,
callback: () -> Unit
) {
executor.execute {
Tracer.global.trace(
category = CATEGORY_MAIN,
name = "executeTask",
token = token,
) {
// Do something
Thread.sleep(100)
callback()
}
}
}
fun main() = runBlocking(context = Dispatchers.Default) {
val driver = createTraceDriver()
@OptIn(DelicateTracingApi::class)
Tracer.setGlobalTracer(driver.tracer)
val executor = Executors.newSingleThreadExecutor()
driver.use {
Tracer.global.traceCoroutine(category = CATEGORY_MAIN, name = "main") {
coroutineScope {
val deferred = CompletableDeferred<Unit>()
executorTask(
// Obtain the propagation token from the CoroutineContext
token = Tracer.global.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()
@OptIn(DelicateTracingApi::class)
Tracer.setGlobalTracer(driver.tracer)
driver.use {
onEvent(eventId = EVENT_ID)
}
}
fun onEvent(eventId: Long) {
Tracer.global.trace(
category = CATEGORY_MAIN,
name = "step-1",
metadataBlock = {
addCorrelationId(eventId)
}
) {
Thread.sleep(100L)
}
Thread.sleep(20)
Tracer.global.trace(
category = CATEGORY_MAIN,
name = "step-2",
metadataBlock = {
addCorrelationId(eventId)
}
) {
Thread.sleep(180)
}
}
В результате получается следующее.

Рисунок 7. Скриншот трассировки Perfetto с коррелированными срезами.
Добавить информацию о стеке вызовов
Инструменты на стороне хоста, такие как плагины компилятора и обработчики аннотаций, также могут встраивать информацию о стеке вызовов в трассировку, чтобы упростить поиск файла, класса или метода, ответственного за создание участка трассировки.
fun main() {
val driver = createTraceDriver()
@OptIn(DelicateTracingApi::class)
Tracer.setGlobalTracer(driver.tracer)
driver.use {
Tracer.global.trace(
category = CATEGORY_MAIN,
name = "callStackEntry",
metadataBlock = {
addCallStackEntry(
name = "main",
lineNumber = 14,
sourceFile = "Basic.kt"
)
}
) {
Thread.sleep(100L)
}
}
}
В результате получается следующее.

Рисунок 8. Скриншот трассировки Perfetto с информацией о стеке вызовов.