प्रोसेस में मौजूद ट्रेसिंग (एक्सपेरिमेंट के तौर पर उपलब्ध)

नई androidx.tracing:tracing:2.0.0-alpha01 लाइब्रेरी, कम ओवरहेड वाली Kotlin API है. इसकी मदद से, प्रोसेस में मौजूद ट्रेस इवेंट कैप्चर किए जा सकते हैं. इन इवेंट से, समय के स्लाइस और उनके कॉन्टेक्स्ट को कैप्चर किया जा सकता है. यह लाइब्रेरी, Kotlin Coroutines के लिए कॉन्टेक्स्ट प्रोपगेशन की सुविधा भी देती है.

यह लाइब्रेरी, Perfetto के उसी ट्रेस पैकेट फ़ॉर्मैट का इस्तेमाल करती है जिसके बारे में Android डेवलपर जानते हैं. इसके अलावा, Tracing 2.0 (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 में, सिंक को लागू करने की सुविधा मिलती है. यह Perfetto ट्रेस पैकेट फ़ॉर्मैट का इस्तेमाल करता है. TraceDriver, Tracer के लिए एक हैंडल उपलब्ध कराता है. इसका इस्तेमाल ट्रेस को पूरा करने के लिए किया जा सकता है.

अगर आपको ऐप्लिकेशन के कुछ वर्शन में ट्रेसिंग की सुविधा का इस्तेमाल नहीं करना है, तो TraceDriver का इस्तेमाल करके, ऐप्लिकेशन में मौजूद सभी ट्रेस पॉइंट बंद किए जा सकते हैं. 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 पाएं. यह सभी ट्रेसिंग एपीआई के लिए एंट्री पॉइंट तय करता है.

// 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 ट्रेस का स्क्रीन कैप्चर

पहली इमेज. इस इमेज में, 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 ट्रेस का स्क्रीन कैप्चर

दूसरी इमेज. नेस्ट किए गए सेक्शन के साथ, बुनियादी 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 ट्रेस का स्क्रीन कैप्चर

तीसरी इमेज. अतिरिक्त मेटाडेटा के साथ, सामान्य 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 ट्रेस का स्क्रीन कैप्चर

चौथी इमेज. कॉन्टेक्स्ट प्रोपगेशन के साथ बुनियादी 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 ट्रेस का स्क्रीन कैप्चर

पांचवीं इमेज. मैन्युअल कॉन्टेक्स्ट प्रोपगेशन के साथ बुनियादी Perfetto ट्रेस का स्क्रीन कैप्चर.

यहां देखा जा सकता है कि CoroutineContext में एक्ज़ीक्यूशन शुरू हुआ और बाद में Java Executor पर स्विच हो गया. हालांकि, हम अब भी कॉन्टेक्स्ट प्रोपगेशन का इस्तेमाल कर सकते हैं.

सिस्टम ट्रेस के साथ मिलाएं

नए androidx.tracing में सीपीयू शेड्यूलिंग, मेमोरी के इस्तेमाल, और ऑपरेटिंग सिस्टम के साथ ऐप्लिकेशन के इंटरैक्शन जैसी जानकारी शामिल नहीं होती. ऐसा इसलिए है, क्योंकि लाइब्रेरी प्रोसेस में बहुत कम ओवरहेड वाली ट्रेसिंग की सुविधा देती है.

हालांकि, सिस्टम ट्रेस को प्रोसेस में मौजूद ट्रेस के साथ मर्ज करना और ज़रूरत पड़ने पर उन्हें एक ही ट्रेस के तौर पर देखना बहुत आसान है. ऐसा इसलिए है, क्योंकि Perfetto UI एक ही टाइमलाइन पर, डिवाइस से कई ट्रेस फ़ाइलों को विज़ुअलाइज़ करने की सुविधा देता है.

इसके लिए, Perfetto UI का इस्तेमाल करके सिस्टम ट्रेसिंग सेशन शुरू किया जा सकता है. इसके लिए, यहां दिए गए निर्देशों का पालन करें.

सिस्टम ट्रेसिंग चालू होने पर, Tracing 2.0 API का इस्तेमाल करके, प्रोसेस में मौजूद ट्रेस इवेंट भी रिकॉर्ड किए जा सकते हैं. दोनों ट्रेस फ़ाइलें मिलने के बाद, Perfetto में Open Multiple Trace Files विकल्प का इस्तेमाल किया जा सकता है.

Perfetto यूज़र इंटरफ़ेस (यूआई) में एक से ज़्यादा ट्रेस फ़ाइलें खोलना

छठी इमेज. 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)
        }
    }
}

इससे यह नतीजा मिलता है.

परफ़ेट्टो ट्रेस का स्क्रीन कैप्चर, जिसमें एक-दूसरे से जुड़े स्लाइस दिख रहे हैं

सातवीं इमेज. परफ़ेट्टो ट्रेस का स्क्रीन कैप्चर, जिसमें एक-दूसरे से जुड़े स्लाइस दिख रहे हैं.

कॉल स्टैक की जानकारी जोड़ना

होस्ट साइड टूल (कंपाइलर प्लगिन, एनोटेशन प्रोसेसर वगैरह) चाहें, तो कॉल स्टैक की जानकारी को ट्रेस में एम्बेड कर सकते हैं. इससे, ट्रेस में किसी ट्रेस सेक्शन को जनरेट करने के लिए ज़िम्मेदार फ़ाइल, क्लास या तरीके का पता लगाना आसान हो जाता है.

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 ट्रेस का स्क्रीन कैप्चर

आठवीं इमेज. कॉल स्टैक की जानकारी के साथ Perfetto ट्रेस का स्क्रीन कैप्चर.