Отслеживание процесса (экспериментальная версия)

Новая библиотека 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)
        }
    }
}

В результате генерируется следующий трассировочный файл.

Скриншот базовой трассировки Perfetto.

Рисунок 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") {
        // ...
    }
}

В результате генерируется следующий трассировочный файл.

Скриншот базовой трассировки Perfetto с вложенными секциями.

Рисунок 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 .

Скриншот базовой трассировки Perfetto с дополнительными метаданными.

Рисунок 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")
    }
}

В результате получается следующее.

Скриншот трассировки Perfetto с распространением контекста.

Рисунок 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()
    }
}

В результате получается следующее.

Скриншот трассировки Perfetto с ручным распространением контекста.

Рисунок 5. Скриншот базовой трассировки Perfetto с ручным распространением контекста.

Как видите, выполнение началось в CoroutineContext , а затем переключилось на Java Executor , но мы всё ещё смогли использовать распространение контекста.

Объединить с трассировкой системы

Новая библиотека androidx.tracing не собирает информацию о планировании работы ЦП, использовании памяти и взаимодействии приложений с операционной системой в целом. Это связано с тем, что библиотека предоставляет способ выполнения трассировки внутри процесса с минимальными накладными расходами .

Однако объединить системные трассировки с трассировками процессов и при необходимости визуализировать их в виде единой трассировки крайне просто. Это связано с тем, что Perfetto UI поддерживает визуализацию нескольких файлов трассировки с устройства на единой временной шкале.

Для этого вы можете запустить сеанс трассировки системы с помощью Perfetto UI , следуя инструкциям здесь .

Также можно записывать события трассировки в процессе выполнения, используя API Tracing 2.0 , при включенной системной трассировке. После получения обоих файлов трассировки можно использовать опцию « Open Multiple Trace Files в Perfetto.

Открытие нескольких файлов трассировки в пользовательском интерфейсе 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)
        }
    }
}

В результате получается следующее.

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

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