İşlem içi izleme (deneysel)

Yeni androidx.tracing:tracing:2.0.0-alpha01 kitaplığı, düşük ek yüke sahip bir Kotlin API'sidir ve işlem içi izleme etkinliklerinin yakalanmasına olanak tanır. Bu etkinlikler, zaman dilimlerini ve bunların bağlamını yakalayabilir. Kitaplık, Kotlin Coroutines için bağlam yayılımını da destekler.

Kitaplık, Android geliştiricilerinin aşina olduğu Perfetto izleme paketi biçimini kullanır. Ayrıca Tracing 2.0 (1.0.0-* API'lerinden farklı olarak) takılabilir izleme arka uçlarını ve havuzları destekler. Bu nedenle, diğer izleme kitaplıkları çıkış izleme biçimini ve bağlam yayılımının kendi uygulamalarında nasıl çalıştığını özelleştirebilir.

Bağımlılıklar

İzlemeye başlamak için build.gradle.kts dosyanızda aşağıdaki bağımlılıkları tanımlamanız gerekir.

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

Bir Android kitaplığını veya uygulamasını hedefliyorsanız androidx.tracing:tracing-wire-android:2.0.0-alpha01 bağımlılığını beyan edin. JVM'yi hedefliyorsanız androidx.tracing:tracing-wire-desktop:2.0.0-alpha01 bağımlılığını kullanabilirsiniz.

Temel kullanım

TraceSink, izleme paketlerinin nasıl serileştirileceğini tanımlar. Tracing 2.0.0, Perfetto izleme paketi biçimini kullanan bir Sink uygulamasıyla birlikte gelir. TraceDriver, Tracer için bir tutma yeri sağlar ve izlemeyi tamamlamak için kullanılabilir.

Bazı uygulama varyantlarında hiç izleme yapmamayı tercih ederseniz uygulamadaki tüm izleme noktalarını devre dışı bırakmak için TraceDriver simgesini de kullanabilirsiniz. TraceDriver'daki gelecekteki API'ler, geliştiricilerin hangi izleme kategorilerini yakalamak (veya bir kategori gürültülü olduğunda devre dışı bırakmak) istediklerini kontrol etmelerine de olanak tanıyacak.

Başlamak için TraceSink ve TraceDriver örneği oluşturun.

/**
 * 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 örneğini aldıktan sonra, tüm izleme API'lerinin giriş noktasını tanımlayan Tracer değerini alın.

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

Bu işlem aşağıdaki izi oluşturur.

Temel bir Perfetto izinin ekran görüntüsü

Şekil 1. Temel bir Perfetto izinin ekran görüntüsü.

Doğru işlem ve iş parçacığı izlerinin doldurulduğunu ve basic süresince çalışan tek bir izleme bölümü 100ms oluşturulduğunu görebilirsiniz.

İzleme bölümleri (veya dilimleri), çakışan etkinlikleri göstermek için aynı izde iç içe yerleştirilebilir. Bir örnek verelim.

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

Bu işlem aşağıdaki izi oluşturur.

İç içe geçmiş bölümler içeren temel bir Perfetto izinin ekran görüntüsü

Şekil 2. İç içe geçmiş bölümler içeren temel bir Perfetto izinin ekran görüntüsü.

Ana iş parçacığı izinde çakışan etkinlikler olduğunu görebilirsiniz. Aynı iş parçacığında processImage aramaları loadImage ve sharpen işlemlerinin yapıldığı çok açık.

İzleme bölümlerine ek meta veri ekleme

Bazen daha fazla ayrıntı almak için bir iz dilimine ek bağlamsal meta veriler eklemek yararlı olabilir. Bu tür meta verilere örnek olarak, kullanıcının bulunduğu nav destination veya bir işlevin ne kadar süreceğini belirleyebilecek input arguments verileri verilebilir.

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

Bu işlem aşağıdaki sonucu verir. Arguments bölümünde, slice oluşturulurken eklenen anahtar/değer çiftleri bulunur.

Ek meta veriler içeren temel bir Perfetto izinin ekran görüntüsü

Şekil 3. Ek meta veriler içeren temel bir Perfetto izinin ekran görüntüsü.

Bağlam yayma

Kotlin Coroutines (veya eşzamanlı iş yükleriyle ilgili yardımcı olan benzer çerçeveler) kullanılırken Tracing 2.0, bağlam yayma kavramını destekler. Bu durumu en iyi bir örnekle açıklayabiliriz.

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

Bu işlem aşağıdaki sonucu verir.

Bağlam yayılımı içeren bir Perfetto izinin ekran görüntüsü

Şekil 4. Bağlam yayılımı içeren temel bir Perfetto izinin ekran görüntüsü.

Bağlam yayma, yürütme akışını görselleştirmeyi çok daha kolay hale getirir. Hangi görevlerin ilişkili (diğerlerine bağlı) olduğunu ve Threads'nın ne zaman askıya alındığını ve devam ettirildiğini tam olarak görebilirsiniz.

Örneğin, main diliminin taskOne ve taskTwo oluşturduğunu görebilirsiniz. Bundan sonra her iki iş parçacığı da etkin değildi (coroutines, delay kullanıldığı için askıya alınmıştı).

Manuel yayma

Bazen Kotlin coroutines ile eşzamanlı iş yüklerini Java Executor örnekleriyle karıştırırken bağlamı birinden diğerine yaymak faydalı olabilir. Örnek:

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

Bu işlem aşağıdaki sonucu verir.

Manuel bağlam yayılımı içeren bir Perfetto izinin ekran görüntüsü

Şekil 5. Manuel bağlam yayılımı içeren temel bir Perfetto izinin ekran görüntüsü.

Yürütmenin CoroutineContext içinde başladığını ve daha sonra Java Executor'ya geçtiğini görebilirsiniz. Ancak bağlam yayılımını kullanmaya devam edebildik.

Sistem izleriyle birleştirme

Yeni androidx.tracing, CPU planlama, bellek kullanımı ve uygulamaların işletim sistemiyle genel etkileşimi gibi bilgileri yakalamaz. Bunun nedeni, kitaplığın çok düşük ek yükle işlem içi izleme gerçekleştirme olanağı sunmasıdır.

Ancak sistem izlerini işlem içi izlerle birleştirmek ve gerekirse tek bir iz olarak görselleştirmek son derece kolaydır. Bunun nedeni, Perfetto UI bir cihazdaki birden fazla izleme dosyasının birleşik bir zaman çizelgesinde görselleştirilmesini desteklemesidir.

Bunu yapmak için Perfetto UI kullanarak sistem izleme oturumu başlatabilir ve buradaki talimatları uygulayabilirsiniz.

Sistem izleme açıkken Tracing 2.0 API'yi kullanarak devam eden izleme etkinliklerini de kaydedebilirsiniz. Her iki izleme dosyasına da sahip olduğunuzda Perfetto'daki Open Multiple Trace Files seçeneğini kullanabilirsiniz.

Perfetto kullanıcı arayüzünde birden fazla izleme dosyası açma

Şekil 6. Perfetto kullanıcı arayüzünde birden fazla izleme dosyası açma

Gelişmiş iş akışları

Dilimleri ilişkilendirme

Bazen, bir izdeki dilimleri daha üst düzey bir kullanıcı işlemine veya bir sistem etkinliğine atfetmek yararlı olur. Örneğin, arka planda çalışan bazı görevlere karşılık gelen tüm dilimleri bir bildirimin parçası olarak ilişkilendirmek için şuna benzer bir işlem yapabilirsiniz:

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

Bu işlem aşağıdaki sonucu verir.

Korelasyonlu dilimler içeren bir Perfetto izinin ekran görüntüsü

Şekil 7. İlişkilendirilmiş dilimlerin bulunduğu bir Perfetto izinin ekran görüntüsü.

Çağrı yığını bilgileri ekleme

Ana makine tarafındaki araçlar (derleyici eklentileri, açıklama işlemcileri vb.), izleme bölümü oluşturmaktan sorumlu dosya, sınıf veya yöntemin izlemede kolayca bulunabilmesi için ek olarak çağrı yığını bilgilerini bir izlemeye yerleştirmeyi seçebilir.

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

Bu işlem aşağıdaki sonucu verir.

Arama yığını bilgilerini içeren bir Perfetto izinin ekran görüntüsü

Şekil 8. Arama yığını bilgilerini içeren bir Perfetto izinin ekran görüntüsü.