ردیابی در حین فرآیند

کتابخانه androidx.tracing:tracing:2.0.0-beta01 یک API کاتلین با سربار کم است که به شما امکان می‌دهد رویدادهای ردیابی در حین فرآیند را ثبت کنید. این رویدادها می‌توانند برش‌های زمانی و زمینه آنها را ثبت کنند. این کتابخانه همچنین از انتشار زمینه برای کوروتین‌های کاتلین پشتیبانی می‌کند.

این کتابخانه از همان فرمت بسته ردیابی Perfetto استفاده می‌کند که توسعه‌دهندگان اندروید با آن آشنا هستند. همچنین، Tracing 2.0 (برخلاف APIهای 1.0.0-* ) از مفهوم backendها و sinkهای ردیابی قابل اتصال پشتیبانی می‌کند، بنابراین سایر کتابخانه‌های ردیابی می‌توانند فرمت ردیابی خروجی و نحوه عملکرد انتشار زمینه را در پیاده‌سازی خود سفارشی کنند .

وابستگی‌ها

برای شروع ردیابی، باید وابستگی‌ها را در build.gradle.kts خود تعریف کنید.

پروژه‌های چند پلتفرمی کاتلین

کتابخانه‌هایی که فقط نیاز به انتشار رویدادهای ردیابی دارند، باید به API سبک androidx.tracing:tracing وابسته باشند. برنامه‌هایی که backend ردیابی را پیکربندی می‌کنند نیز باید به 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")
      }
    }
  }
}

پروژه‌های مخصوص اندروید

اگر فقط اندروید را هدف قرار می‌دهید، موارد زیر را به فایل build.gradle.kts برنامه یا کتابخانه خود اضافه کنید:

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

مقداردهی اولیه و کشف

قبل از اینکه بتوانید رویدادهای ردیابی را ثبت کنید، باید زیرساخت ردیابی را مقداردهی اولیه کنید. این شامل ایجاد یک AbstractTraceDriver و ثبت سراسری Tracer آن است.

اندروید

در اندروید، اگر وابستگی androidx.tracing:tracing-wire را اضافه کنید، مقداردهی اولیه به طور خودکار در هنگام راه‌اندازی برنامه با استفاده از کتابخانه androidx.startup انجام می‌شود.

به طور پیش‌فرض، این مقداردهی اولیه خودکار موارد زیر را انجام می‌دهد:

  • یک TraceDriver با TraceSink ایجاد می‌کند که فایل‌های ردیابی Perfetto را در Context.noBackupFilesDir/perfetto_traces/ می‌نویسد.

  • Tracer حاصل را به صورت سراسری ثبت می‌کند.

نمونه TraceDriver سفارشی کنید

اگر نیاز به سفارشی‌سازی پیکربندی دارید، برای مثال برای تغییر محل ذخیره فایل‌های ردیابی یا استفاده از یک TraceSink سفارشی، می‌توانید نمونه AbstractTraceDriver خود را ارائه دهید.

برای سفارشی‌سازی پیکربندی، کلاس Application خود را طوری پیاده‌سازی کنید که 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)
    }
}

مقداردهنده اولیه خودکار تشخیص می‌دهد که زیرکلاس Application شما Factory را پیاده‌سازی کرده و از درایور Factory سفارشی شما استفاده می‌کند.

ماشین مجازی جاوا (JVM)

در JVM، هیچ مکانیزم بوت‌استرپ خودکاری وجود ندارد. برنامه مسئول مقداردهی اولیه TraceDriver و ثبت Tracer به صورت سراسری در هنگام راه‌اندازی است، که معمولاً در تابع main شما انجام می‌شود.

برای ثبت ردیاب، 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()
    })
}

کاربرد اولیه

یک TraceSink نحوه سریال‌سازی بسته‌های ردیابی را تعریف می‌کند. Tracing 2.0.0 با پیاده‌سازی یک Sink ارائه می‌شود که از قالب بسته ردیابی Perfetto استفاده می‌کند. یک TraceDriver یک هندل برای Tracer فراهم می‌کند و می‌تواند برای نهایی کردن ردیابی مورد استفاده قرار گیرد.

پس از مقداردهی اولیه Tracer ، چه به صورت خودکار در اندروید و چه به صورت دستی در JVM، از نمونه سراسری Tracer.global برای انتشار رویدادهای ردیابی استفاده کنید.

همچنین می‌توانید از TraceDriver برای غیرفعال کردن تمام نقاط ردیابی در برنامه استفاده کنید، اگر تصمیم دارید در برخی از انواع برنامه اصلاً ردیابی نکنید. می‌توانید به صورت اختیاری با ارائه پیاده‌سازی برای isCategoryEnabled هنگام ایجاد یک نمونه از TraceDriver نقاط ردیابی را برای یک category خاص فعال کنید.

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

در اینجا یک مثال ساده از انتشار یک رویداد ردیابی با استفاده از Tracer.global در JVM، شامل تنظیمات دستی، آورده شده است:

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

این باعث ایجاد ردیابی زیر می‌شود.

ضبط صفحه نمایش از یک مسیر پایه Perfetto

شکل ۱. تصویر صفحه نمایش از یک مسیر اولیه Perfetto.

می‌توانید ببینید که مسیرهای صحیح فرآیند و نخ پر شده‌اند و یک بخش ردیابی واحد basic ایجاد کرده‌اند که به مدت ۱۰۰ میلی‌ثانیه اجرا شده است.

بخش‌های ردیابی (یا برش‌ها) را می‌توان در یک مسیر تو در تو قرار داد تا رویدادهای همپوشانی را نشان دهند. در اینجا یک مثال آورده شده است.

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

این باعث ایجاد ردیابی زیر می‌شود.

تصویر صفحه نمایش از یک مسیر Perfetto پایه با بخش‌های تو در تو

شکل ۲. تصویر صفحه نمایش از یک مسیر Perfetto پایه با بخش‌های تو در تو.

می‌توانید ببینید که رویدادهای همپوشانی در مسیر نخ اصلی وجود دارد. کاملاً واضح است که processImage توابع loadImage و sharpen در همان نخ فراخوانی می‌کند.

اضافه کردن فراداده‌های اضافی در بخش‌های ردیابی

گاهی اوقات، برای دریافت جزئیات بیشتر، می‌توان فراداده‌های زمینه‌ای اضافی را به یک برش ردیابی پیوست کرد. برخی از نمونه‌های چنین فراداده‌هایی می‌توانند شامل nav destination که کاربر در آن قرار دارد یا input arguments که ممکن است در نهایت تعیین کنند که یک تابع چقدر طول می‌کشد.

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

این نتیجه‌ی زیر را تولید می‌کند. توجه داشته باشید که بخش Arguments شامل جفت‌های کلید-مقدار اضافه شده هنگام تولید slice است.

ضبط صفحه نمایش از یک مسیر پایه Perfetto با فراداده‌های اضافی

شکل ۳. تصویر صفحه نمایش از یک مسیر اولیه Perfetto به همراه فراداده‌های اضافی.

انتشار متن

هنگام استفاده از کوروتین‌های کاتلین یا سایر چارچوب‌های مشابه که به بارهای کاری همزمان کمک می‌کنند، Tracing 2.0 از مفهوم انتشار متن پشتیبانی می‌کند. این موضوع با یک مثال بهتر توضیح داده می‌شود.

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

این نتیجه زیر را تولید می‌کند.

ضبط صفحه نمایش یک ردیابی Perfetto با انتشار متن

شکل ۴. تصویر صفحه نمایش از یک ردیابی اولیه Perfetto با انتشار زمینه.

انتشار زمینه، تجسم جریان اجرا را بسیار ساده‌تر می‌کند. می‌توانید دقیقاً ببینید کدام وظایف به هم مرتبط بوده‌اند (به یکدیگر متصل بوده‌اند) و دقیقاً چه زمانی Threads به حالت تعلیق درآمده و از سر گرفته شده‌اند .

برای مثال، می‌توانید ببینید که slice main taskOne و taskTwo را تولید کرده است. پس از آن، هر دو thread غیرفعال بودند زیرا coroutineها به دلیل استفاده از delay به حالت تعلیق درآمده بودند.

انتشار دستی

گاهی اوقات، وقتی بارهای کاری همزمان را با استفاده از کوروتین‌های کاتلین با نمونه‌هایی از Java Executor ترکیب می‌کنید، ممکن است مفید باشد که context را از یکی به دیگری منتقل کنید. در اینجا مثالی آورده شده است:

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

این نتیجه زیر را تولید می‌کند.

ضبط صفحه نمایش یک ردیابی Perfetto با انتشار دستی متن

شکل ۵. تصویر صفحه نمایش از یک مسیر Perfetto پایه با انتشار دستی متن.

می‌توانید ببینید که اجرا در یک CoroutineContext شروع شده و متعاقباً به یک Java Executor تغییر یافته است، اما ما همچنان توانستیم از انتشار متن استفاده کنیم.

ترکیب با ردیابی‌های سیستم

کتابخانه androidx.tracing اطلاعاتی مانند زمان‌بندی CPU، میزان استفاده از حافظه و تعامل برنامه با سیستم عامل را به طور کلی ثبت نمی‌کند. دلیل این امر این است که این کتابخانه روشی برای انجام ردیابی درون‌پردازشی با سربار کم ارائه می‌دهد.

با این حال، ادغام ردیابی‌های سیستم با ردیابی‌های درون فرآیند و در صورت نیاز، تجسم آنها به عنوان یک ردیابی واحد بسیار ساده است. دلیل این امر پشتیبانی Perfetto UI از تجسم چندین فایل ردیابی از یک دستگاه در یک جدول زمانی واحد است.

برای انجام این کار، می‌توانید با دنبال کردن دستورالعمل‌های اینجا ، یک جلسه ردیابی سیستم را با استفاده از Perfetto UI شروع کنید.

همچنین می‌توانید رویدادهای ردیابی در حال انجام را با استفاده از Tracing 2.0 API ضبط کنید، در حالی که ردیابی سیستم روشن است. هنگامی که هر دو فایل ردیابی را دارید، می‌توانید از گزینه Open Multiple Trace Files در Perfetto استفاده کنید.

باز کردن چندین فایل ردیابی در رابط کاربری Perfeto

شکل ۶. باز کردن چندین فایل ردیابی در رابط کاربری Perfetto.

گردش‌های کاری پیشرفته

این بخش، گردش‌های کاری پیشرفته‌ای را که می‌توانید با کتابخانه ردیابی در حین فرآیند پیاده‌سازی کنید، شرح می‌دهد.

برش‌های مرتبط

گاهی اوقات، مفید است که برش‌ها را در یک ردیابی به یک اقدام کاربر سطح بالاتر یا یک رویداد سیستمی نسبت دهید. به عنوان مثال، برای نسبت دادن تمام برش‌هایی که مربوط به برخی کارهای پس‌زمینه به عنوان بخشی از یک اعلان هستند، می‌توانید کاری مانند این انجام دهید:

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

این نتیجه زیر را تولید می‌کند.

ضبط صفحه نمایش از یک مسیر Perfetto با برش‌های همبسته

شکل ۷. تصویر صفحه نمایش از یک مسیر Perfetto با برش‌های همبسته.

اطلاعات پشته تماس را اضافه کنید

ابزارهای سمت میزبان، مانند افزونه‌های کامپایلر و پردازنده‌های حاشیه‌نویسی، می‌توانند اطلاعات پشته فراخوانی را نیز در یک ردیابی جاسازی کنند تا یافتن فایل، کلاس یا روشی که مسئول تولید بخش ردیابی در یک ردیابی است، آسان شود.

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

این نتیجه زیر را تولید می‌کند.

تصویر صفحه نمایش یک ردیابی Perfetto به همراه اطلاعات پشته فراخوانی

شکل ۸. تصویر صفحه نمایش یک ردیابی Perfetto به همراه اطلاعات پشته فراخوانی.