تتبُّع العمليات الجارية (ميزة تجريبية)

مكتبة androidx.tracing:tracing:2.0.0-alpha04 الجديدة هي واجهة برمجة تطبيقات Kotlin منخفضة النفقات تتيح تسجيل أحداث التتبُّع داخل العملية. يمكن لهذه الأحداث تسجيل الشرائح الزمنية والسياق الخاص بها. تتيح المكتبة أيضًا نشر السياق لـ 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:2.0.0-alpha04")
        // ...
      }
    }
    jvmMain {
      dependencies {
        api("androidx.tracing:tracing-wire:2.0.0-alpha04")
        // ...
      }
    }
  }
}

عليك الإعلان عن تبعية على androidx.tracing:tracing-wire:2.0.0-alpha04 إذا كنت تستهدف مكتبة Android أو تطبيق Android أو JVM.

الاستخدام الأساسي

يحدّد TraceSink كيفية تسلسل حِزم التتبُّع. يأتي الإصدار 2.0.0 من Tracing مع عملية تنفيذ لمصدر يستخدم تنسيق حزمة تتبُّع 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.wire.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 {
        it.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 {
        it.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 {
        it.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 (أو الأُطر المشابهة الأخرى التي تساعد في أحمال العمل المتزامنة)، يتيح الإصدار 2.0 من Tracing مفهوم نشر السياق. يمكن شرح ذلك بشكل أفضل من خلال مثال.

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 {
        it.tracer.traceCoroutine(category = CATEGORY_MAIN, name = "main") {
            coroutineScope {
                launch { taskOne(tracer) }
                launch { taskTwo(tracer) }
            }
        }
        println("All done")
    }
}

يؤدي ذلك إلى ظهور النتيجة التالية.

لقطة شاشة لتتبُّع Perfetto مع نقل السياق

الشكل 4: لقطة شاشة لتتبُّع Perfetto أساسي مع نشر السياق

يُسهّل نشر السياق إلى حد كبير تصوُّر تدفق التنفيذ. يمكنك الاطّلاع بالضبط على المهام ذات الصلة (المرتبطة بمهام أخرى)، ووقت Threads الذي تم فيه تعليق واستئناف.

على سبيل المثال، يمكنك ملاحظة أنّ الشريحة main أنشأت taskOne وtaskTwo. بعد ذلك، كانت كلتا سلسلتي المحادثات غير نشطتين (بما أنّ إجراءات coroutines تم تعليقها بسبب استخدام delay).

النشر اليدوي

في بعض الأحيان، عند مزج أحمال العمل المتزامنة باستخدام Kotlin coroutines مع مثيلات 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 {
        it.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 باتّباع التعليمات هنا.

يمكنك أيضًا تسجيل أحداث التتبُّع داخل العملية باستخدام واجهة برمجة التطبيقات Tracing 2.0، أثناء تفعيل تتبُّع النظام. بعد الحصول على كلتا ملفَي التتبُّع، يمكنك استخدام الخيار Open Multiple Trace Files في Perfetto.

فتح ملفات تتبُّع متعددة في واجهة مستخدم Perfetto

الشكل 6: فتح ملفات تتبُّع متعددة في Perfetto UI

عمليات سير العمل المتقدّمة

ربط الشرائح

في بعض الأحيان، يكون من المفيد إحالة الشرائح في عملية تتبُّع إلى إجراء مستخدِم أكثر مستوى أو حدث نظام. على سبيل المثال، لإحالة جميع الشرائح التي تتوافق مع بعض الأعمال في الخلفية كجزء من إشعار، يمكنك إجراء ما يلي:

fun main() {
    val driver = createTraceDriver()
    onEvent(driver, eventId = EVENT_ID)
}

fun onEvent(driver: TraceDriver, eventId: Long) {
    driver.use {
        it.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)
        }
    }
}

يؤدي ذلك إلى ظهور النتيجة التالية.

لقطة شاشة لسجلّ تتبُّع Perfetto يتضمّن شرائح مرتبطة

الشكل 7: لقطة شاشة لتتبُّع Perfetto مع شرائح مرتبطة

إضافة معلومات عن حزمة التنفيذ

يمكن للأدوات من جهة المضيف (مكوّنات برنامج التجميع الإضافية ومعالِجات التعليقات التوضيحية وما إلى ذلك) أيضًا اختيار تضمين معلومات حزمة التنفيذ في عملية تتبُّع، لتسهيل تحديد موقع الملف أو الفئة أو الطريقة المسؤولة عن إنشاء قسم تتبُّع في عملية تتبُّع.

fun main() {
    val driver = createTraceDriver()
    driver.use {
        it.tracer.trace(
            category = CATEGORY_MAIN,
            name = "callStackEntry",
            metadataBlock = {
                addCallStackEntry(
                    name = "main",
                    lineNumber = 14,
                    sourceFile = "Basic.kt"
                )
            }
        ) {
            Thread.sleep(100L)
        }
    }
}

يؤدي ذلك إلى ظهور النتيجة التالية.

لقطة شاشة لسجلّ تتبُّع Perfetto يتضمّن معلومات عن حزمة التنفيذ

الشكل 8: لقطة شاشة لتتبُّع Perfetto مع معلومات حزمة التنفيذ