مكتبة androidx.tracing:tracing:2.0.0-alpha01 الجديدة هي واجهة برمجة تطبيقات Kotlin ذات حمل منخفض
تتيح تسجيل أحداث التتبُّع أثناء المعالجة. يمكن لهذه الأحداث تسجيل شرائح زمنية وسياقها. تتيح المكتبة أيضًا نقل السياق إلى الكوروتينات في Kotlin.
تستخدِم المكتبة تنسيق حِزم التتبُّع Perfetto نفسه الذي يعرفه مطوّرو تطبيقات Android. بالإضافة إلى ذلك، تتيح ميزة "التتبُّع" 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")
// ...
}
}
}
}
عليك تحديد تبعية androidx.tracing:tracing-wire-android:2.0.0-alpha01 إذا كنت تستهدف مكتبة Android أو تطبيقًا. يمكنك استخدام
androidx.tracing:tracing-wire-desktop:2.0.0-alpha01 التبعية إذا كنت
تستهدف JVM.
الاستخدام الأساسي
تحدّد TraceSink كيفية تسلسل حِزم التتبُّع. يتضمّن الإصدار 2.0.0 من أداة Tracing عملية تنفيذ لـ Sink تستخدم تنسيق حزمة التتبُّع 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)
}
}
}
يؤدي ذلك إلى إنشاء التتبُّع التالي.
الشكل 1: لقطة شاشة لتتبُّع أساسي في 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") {
// ...
}
}
يؤدي ذلك إلى إنشاء التتبُّع التالي.
الشكل 2: لقطة شاشة لتتبُّع 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.
الشكل 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 {
tracer.traceCoroutine(category = CATEGORY_MAIN, name = "main") {
coroutineScope {
launch { taskOne(tracer) }
launch { taskTwo(tracer) }
}
}
println("All done")
}
}
يؤدي ذلك إلى ظهور النتيجة التالية.
الشكل 4. لقطة شاشة لتتبُّع Perfetto أساسي مع نقل السياق
تسهّل ميزة "نقل السياق" كثيرًا عملية تصوّر تسلسل التنفيذ. يمكنك الاطّلاع على المهام المرتبطة (المتصلة بمهام أخرى) ووقت Threads تعليقها واستئنافها.
على سبيل المثال، يمكنك الاطّلاع على أنّ الشريحة main أنشأت الشريحتَين taskOne وtaskTwo.
بعد ذلك، أصبح كلا مؤشري الترابط غير نشطَين (بما أنّ الروتينات الفرعية تم تعليقها بسبب استخدام delay).
النشر اليدوي
في بعض الأحيان، عند دمج أحمال العمل المتزامنة باستخدام إجراءات Kotlin الفرعية المتزامنة مع مثيلات Executor في Java، قد يكون من المفيد نقل السياق من أحدهما إلى الآخر. يُرجى الاطّلاع على المثال أدناه:
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()
}
}
يؤدي ذلك إلى ظهور النتيجة التالية.
الشكل 5. لقطة شاشة لتتبُّع Perfetto أساسي مع نقل السياق يدويًا
يمكنك ملاحظة أنّ التنفيذ بدأ في CoroutineContext، ثم تم التبديل إلى Executor في Java، ولكن ظلّ بإمكاننا استخدام ميزة نقل السياق.
الدمج مع عمليات تتبُّع النظام
لا تسجّل androidx.tracing الجديدة معلومات مثل جدولة وحدة المعالجة المركزية واستخدام الذاكرة وتفاعل التطبيقات مع نظام التشغيل بشكل عام. ويرجع ذلك إلى أنّ المكتبة توفّر طريقة لإجراء تتبُّع داخل العملية مع تكلفة منخفضة جدًا.
ومع ذلك، من السهل جدًا دمج عمليات تتبُّع النظام مع عمليات التتبُّع داخل العملية
وعرضها كعملية تتبُّع واحدة إذا لزم الأمر. ويرجع ذلك إلى أنّ Perfetto UI
يتيح عرض ملفات تتبُّع متعددة من جهاز على مخطط زمني موحّد.
لإجراء ذلك، يمكنك بدء جلسة تتبُّع النظام باستخدام Perfetto UI باتّباع التعليمات هنا.
يمكنك أيضًا تسجيل أحداث التتبُّع الجاري تنفيذها باستخدام واجهة برمجة التطبيقات Tracing 2.0، وذلك أثناء تفعيل "تتبُّع نشاط النظام". بعد الحصول على كلتا ملفَي التتبُّع، يمكنك استخدام الخيار Open Multiple Trace Files في Perfetto.
الشكل 6. فتح ملفات تتبُّع متعددة في واجهة مستخدم 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)
}
}
}
يؤدي ذلك إلى ظهور النتيجة التالية.
الشكل 7. لقطة شاشة لتتبُّع Perfetto مع شرائح مرتبطة
إضافة معلومات عن تسلسل استدعاء الدوال البرمجية
يمكن لأدوات جهة المضيف (مثل مكوّنات المترجم الإضافية ومعالجات التعليقات التوضيحية وما إلى ذلك) أيضًا اختيار تضمين معلومات سلسلة استدعاء الدوال البرمجية في عملية تتبُّع، وذلك لتسهيل تحديد موقع الملف أو الفئة أو الطريقة المسؤولة عن إنشاء قسم عملية تتبُّع في عملية التتبُّع.
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)
}
}
}
يؤدي ذلك إلى ظهور النتيجة التالية.
الشكل 8. لقطة شاشة لتتبُّع Perfetto تتضمّن معلومات عن حزمة استدعاء الدوال البرمجية