Pelacakan dalam proses

Library androidx.tracing:tracing:2.0.0-beta01 adalah Kotlin API dengan overhead rendah yang memungkinkan Anda merekam peristiwa rekaman aktivitas dalam proses. Peristiwa ini dapat merekam rentang waktu dan konteksnya. Library ini juga mendukung propagasi konteks untuk coroutine Kotlin.

Library ini menggunakan format paket rekaman aktivitas Perfetto yang sama dengan yang biasa digunakan oleh developer Android. Selain itu, Tracing 2.0 (tidak seperti API 1.0.0-*) mendukung konsep backend rekaman aktivitas yang dapat dihubungkan dan sink, sehingga library rekaman aktivitas lainnya dapat menyesuaikan format rekaman aktivitas output, dan cara propagasi konteks berfungsi dalam implementasinya.

Dependensi

Untuk memulai rekaman aktivitas, Anda harus menentukan dependensi di build.gradle.kts.

Project Multiplatform Kotlin

Library yang hanya perlu memancarkan peristiwa rekaman aktivitas harus bergantung pada androidx.tracing:tracing API ringan. Aplikasi yang mengonfigurasi backend rekaman aktivitas juga harus bergantung pada 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")
      }
    }
  }
}

Project khusus Android

Jika Anda hanya menargetkan Android, tambahkan kode berikut ke file build.gradle.kts aplikasi atau library Anda:

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

Inisialisasi dan penemuan

Sebelum dapat merekam peristiwa rekaman aktivitas, Anda harus menginisialisasi infrastruktur rekaman aktivitas. Hal ini melibatkan pembuatan AbstractTraceDriver dan pendaftaran Tracer secara global.

Android

Di Android, jika Anda menyertakan dependensi androidx.tracing:tracing-wire, inisialisasi akan terjadi secara otomatis saat startup aplikasi menggunakan library androidx.startup.

Secara default, inisialisasi otomatis ini melakukan hal berikut:

  • Membuat TraceDriver dengan TraceSink yang menulis file rekaman aktivitas Perfetto ke Context.noBackupFilesDir/perfetto_traces/.

  • Mendaftarkan Tracer yang dihasilkan secara global.

Menyesuaikan instance TraceDriver

Jika perlu menyesuaikan konfigurasi, misalnya untuk mengubah tempat penyimpanan file rekaman aktivitas atau menggunakan TraceSink kustom, Anda dapat menyediakan instance AbstractTraceDriver Anda sendiri.

Untuk menyesuaikan konfigurasi, buat class Application Anda mengimplementasikan 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)
    }
}

Penginisialisasi otomatis mendeteksi bahwa subclass Application Anda mengimplementasikan Factory dan menggunakan factory driver kustom Anda.

JVM

Di JVM, tidak ada mekanisme bootstrapping otomatis. Aplikasi bertanggung jawab untuk menginisialisasi TraceDriver dan mendaftarkan Tracer secara global selama startup, yang paling umum dalam fungsi main Anda.

Untuk mendaftarkan tracer, panggil 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()
    })
}

Penggunaan dasar

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

Setelah Tracer diinisialisasi, baik secara otomatis di Android atau secara manual di JVM, gunakan instance Tracer.global global untuk memancarkan peristiwa 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. Anda dapat secara opsional mengaktifkan titik rekaman aktivitas untuk category tertentu dengan memberikan implementasi untuk isCategoryEnabled saat membuat instance TraceDriver.

val driver = TraceDriver(
    sink = sink,
    isCategoryEnabled = { category ->
        // Only enable trace points in the "com.example" package
        category.startsWith("com.example")
    }
)

Berikut adalah contoh dasar pemancaran peristiwa rekaman aktivitas menggunakan Tracer.global di JVM, termasuk penyiapan manual:

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

Tindakan ini menghasilkan rekaman aktivitas berikut.

Screenshot rekaman aktivitas Perfetto dasar

Gambar 1. Screenshot rekaman aktivitas Perfetto dasar.

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

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

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

Tindakan ini menghasilkan rekaman aktivitas berikut.

Screenshot rekaman aktivitas Perfetto dasar dengan bagian bertingkat

Gambar 2. Screenshot rekaman aktivitas Perfetto dasar dengan bagian yang disarangkan.

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, akan berguna untuk melampirkan metadata kontekstual tambahan ke irisan rekaman aktivitas, untuk mendapatkan detail selengkapnya. Beberapa contoh metadata tersebut dapat mencakup nav destination yang digunakan pengguna, atau input arguments yang mungkin menentukan durasi fungsi.

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

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

Screenshot rekaman aktivitas Perfetto dasar dengan metadata tambahan

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

Propagasi konteks

Saat menggunakan coroutine Kotlin, 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.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")
    }
}

Tindakan ini menghasilkan hasil berikut.

Screenshot rekaman aktivitas Perfetto dengan propagasi konteks

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

Propagasi Konteks membuat visualisasi alur eksekusi menjadi jauh lebih sederhana. Anda dapat melihat dengan tepat tugas mana yang terkait (terhubung dengan tugas lain), dan kapan tepatnya Threads ditangguhkan dan dilanjutkan.

Misalnya, Anda dapat melihat bahwa irisan main membuat taskOne dan taskTwo. Setelah itu, kedua thread tidak aktif karena coroutine ditangguhkan karena penggunaan delay.

Propagasi manual

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

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

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 kami masih dapat menggunakan propagasi konteks.

Menggabungkan dengan rekaman aktivitas sistem

Library androidx.tracing tidak merekam informasi seperti penjadwalan CPU, penggunaan memori, dan interaksi aplikasi dengan sistem operasi secara umum. Hal ini karena library menyediakan cara untuk melakukan rekaman aktivitas 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 rekaman 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 rekaman 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

Bagian ini menjelaskan alur kerja lanjutan yang dapat Anda terapkan dengan library rekaman aktivitas dalam proses.

Mengorelasikan irisan

Terkadang, akan berguna untuk mengaitkan irisan dalam rekaman aktivitas dengan tindakan pengguna tingkat yang lebih tinggi atau peristiwa sistem. Misalnya, untuk mengaitkan semua irisan yang sesuai dengan beberapa tugas latar belakang sebagai bagian dari notifikasi, Anda dapat melakukan hal berikut:

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

Tindakan ini menghasilkan hasil berikut.

Screenshot rekaman aktivitas Perfetto dengan slice yang dikorelasikan

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

Menambahkan informasi stack panggilan

Alat sisi host, seperti plugin compiler dan pemroses anotasi, juga dapat memilih untuk menyematkan informasi stack panggilan ke dalam rekaman aktivitas, sehingga memudahkan untuk menemukan file, class, atau metode yang bertanggung jawab untuk menghasilkan bagian rekaman aktivitas dalam rekaman aktivitas.

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

Tindakan ini menghasilkan hasil berikut.

Screenshot rekaman aktivitas Perfetto dengan informasi stack panggilan

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