Pelacakan dalam proses (Eksperimental)

Library androidx.tracing:tracing:2.0.0-alpha01 baru adalah API Kotlin dengan overhead rendah yang memungkinkan pengambilan peristiwa pelacakan dalam proses. Peristiwa ini dapat merekam irisan waktu dan konteksnya. Library ini juga mendukung propagasi konteks untuk Coroutine Kotlin.

Library ini menggunakan format paket rekaman aktivitas Perfetto yang sama dengan yang sudah dikenal oleh developer Android. Selain itu, Perekaman Aktivitas 2.0 (tidak seperti API 1.0.0-*) mendukung konsep backend perekaman aktivitas yang dapat di-plug dan sink, sehingga library perekaman aktivitas lainnya dapat menyesuaikan format perekaman aktivitas output, dan cara kerja propagasi konteks dalam implementasinya.

Dependensi

Untuk memulai pelacakan, Anda harus menentukan dependensi berikut di 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")
        // ...
      }
    }
  }
}

Deklarasikan dependensi pada androidx.tracing:tracing-wire-android:2.0.0-alpha01 jika Anda menargetkan library atau aplikasi Android. Anda dapat menggunakan dependensi androidx.tracing:tracing-wire-desktop:2.0.0-alpha01 jika Anda menargetkan JVM.

Penggunaan dasar

TraceSink menentukan cara paket rekaman aktivitas diserialisasi. Tracing 2.0.0 hadir dengan implementasi Sink yang menggunakan format paket rekaman aktivitas Perfetto. TraceDriver menyediakan handle ke Tracer dan dapat digunakan untuk menyelesaikan rekaman aktivitas.

Anda juga dapat menggunakan TraceDriver untuk menonaktifkan semua titik rekaman aktivitas di aplikasi, jika Anda memilih untuk tidak merekam aktivitas sama sekali di beberapa varian aplikasi. API mendatang di TraceDriver juga akan memungkinkan developer mengontrol kategori rekaman aktivitas yang ingin mereka ambil (atau menonaktifkan saat kategori berisik).

Untuk memulai, buat instance TraceSink dan 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
}

Setelah Anda memiliki instance TraceDriver, dapatkan Tracer yang menentukan titik entri untuk semua API rekaman aktivitas.

// 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)
        }
    }
}

Tindakan ini akan menghasilkan rekaman aktivitas berikut.

Screenshot rekaman aktivitas Perfetto dasar

Gambar 1. Screenshot rekaman aktivitas Perfetto dasar.

Anda dapat melihat bahwa proses dan thread yang benar dilacak, dan menghasilkan satu bagian rekaman aktivitas basic, yang berjalan selama 100ms.

Bagian (atau irisan) rekaman aktivitas dapat disarangkan di jalur yang sama untuk merepresentasikan peristiwa yang tumpang-tindih. Berikut contohnya.

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") {
        // ...
    }
}

Tindakan ini menghasilkan rekaman aktivitas berikut.

Screenshot rekaman aktivitas Perfetto dasar dengan bagian bertingkat

Gambar 2. Screenshot rekaman aktivitas Perfetto dasar dengan bagian bertingkat.

Anda dapat melihat bahwa ada peristiwa yang tumpang-tindih di jalur thread utama. Sangat jelas bahwa processImage memanggil loadImage dan sharpen di thread yang sama.

Menambahkan metadata tambahan di bagian rekaman aktivitas

Terkadang, Anda dapat melampirkan metadata kontekstual tambahan ke slice rekaman aktivitas untuk mendapatkan detail selengkapnya. Beberapa contoh metadata tersebut dapat mencakup nav destination yang digunakan pengguna, atau input arguments yang mungkin akan menentukan durasi fungsi.

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)
        }
    }
}

Tindakan ini menghasilkan hasil berikut. Perhatikan bahwa bagian Arguments berisi pasangan nilai kunci yang ditambahkan saat membuat slice.

Screenshot rekaman aktivitas Perfetto dasar dengan metadata tambahan

Gambar 3. Screenshot rekaman aktivitas Perfetto dasar dengan metadata tambahan.

Penerapan konteks

Saat menggunakan Kotlin Coroutines (atau framework serupa lainnya yang membantu workload serentak), Tracing 2.0 mendukung konsep propagasi konteks. Hal ini paling baik dijelaskan dengan contoh.

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")
    }
}

Tindakan ini menghasilkan hasil berikut.

Screenshot rekaman aktivitas Perfetto dengan propagasi konteks

Gambar 4. Screenshot rekaman aktivitas Perfetto dasar dengan propagasi konteks.

Propagasi Konteks mempermudah visualisasi alur eksekusi. Anda dapat melihat secara persis tugas mana yang terkait (terhubung dengan tugas lain), dan secara persis kapan Threads ditangguhkan dan dilanjutkan.

Misalnya, Anda dapat melihat bahwa irisan main memunculkan taskOne dan taskTwo. Setelah itu, kedua thread tidak aktif (mengingat coroutine ditangguhkan - karena penggunaan delay).

Penyebaran manual

Terkadang saat Anda mencampur workload serentak menggunakan coroutine Kotlin dengan instance Java Executor, akan berguna untuk menyebarkan konteks dari satu ke yang lain. Berikut ini contohnya:

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()
    }
}

Tindakan ini menghasilkan hasil berikut.

Screenshot rekaman aktivitas Perfetto dengan propagasi konteks manual

Gambar 5. Screenshot rekaman aktivitas Perfetto dasar dengan propagasi konteks manual.

Anda dapat melihat bahwa eksekusi dimulai di CoroutineContext, dan selanjutnya beralih ke Executor Java, tetapi kita masih dapat menggunakan propagasi konteks.

Gabungkan dengan rekaman aktivitas sistem

androidx.tracing baru tidak mengambil informasi seperti penjadwalan CPU, penggunaan Memori, dan interaksi aplikasi dengan sistem operasi secara umum. Hal ini karena library menyediakan cara untuk melakukan pelacakan dalam proses dengan overhead rendah.

Namun, sangat mudah untuk menggabungkan rekaman aktivitas sistem dengan rekaman aktivitas dalam proses dan memvisualisasikannya sebagai satu rekaman aktivitas jika diperlukan. Hal ini karena Perfetto UI mendukung visualisasi beberapa file rekaman aktivitas dari perangkat pada linimasa terpadu.

Untuk melakukannya, Anda dapat memulai sesi perekaman aktivitas sistem menggunakan Perfetto UI dengan mengikuti petunjuk di sini.

Anda juga dapat merekam peristiwa rekaman aktivitas dalam proses menggunakan Tracing 2.0 API, saat perekaman aktivitas sistem diaktifkan. Setelah memiliki kedua file rekaman aktivitas, Anda dapat menggunakan opsi Open Multiple Trace Files di Perfetto.

Membuka beberapa file rekaman aktivitas di UI Perfetto

Gambar 6. Membuka beberapa file rekaman aktivitas di UI Perfetto.

Alur kerja lanjutan

Mengorelasikan irisan

Terkadang, ada baiknya mengatribusikan irisan dalam rekaman aktivitas ke tindakan pengguna tingkat yang lebih tinggi atau peristiwa sistem. Misalnya, untuk mengatribusikan semua slice yang sesuai dengan beberapa tugas latar belakang sebagai bagian dari notifikasi, Anda dapat melakukan sesuatu seperti:

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)
        }
    }
}

Tindakan ini menghasilkan hasil berikut.

Screenshot rekaman aktivitas Perfetto dengan slice yang dikorelasikan

Gambar 7. Screenshot rekaman aktivitas Perfetto dengan slice yang dikorelasikan.

Menambahkan informasi stack panggilan

Alat sisi host (plugin compiler, pemroses anotasi, dll.) juga dapat memilih untuk menyematkan informasi stack panggilan ke dalam rekaman aktivitas, agar mudah menemukan file, class, atau metode yang bertanggung jawab untuk menghasilkan bagian rekaman aktivitas dalam rekaman aktivitas.

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)
        }
    }
}

Hal ini akan menghasilkan hasil berikut.

Screenshot rekaman aktivitas Perfetto dengan informasi stack panggilan

Gambar 8. Screenshot rekaman aktivitas Perfetto dengan informasi stack panggilan.