کتابخانه جدید androidx.tracing:tracing:2.0.0-alpha01 یک API کاتلین با سربار کم است که امکان ثبت رویدادهای ردیابی در حین فرآیند را فراهم میکند. این رویدادها میتوانند برشهای زمانی و زمینه آنها را ثبت کنند. این کتابخانه علاوه بر این از انتشار زمینه برای کوروتینهای کاتلین نیز پشتیبانی میکند.
این کتابخانه از همان فرمت بسته ردیابی Perfetto استفاده میکند که توسعهدهندگان اندروید با آن آشنا هستند. همچنین، Tracing 2.0 (برخلاف APIهای 1.0.0-* ) از مفهوم backendها و sinkهای ردیابی قابل اتصال پشتیبانی میکند، بنابراین سایر کتابخانههای ردیابی میتوانند فرمت ردیابی خروجی و نحوه عملکرد انتشار زمینه را در پیادهسازی خود سفارشی کنند .
وابستگیها
برای شروع ردیابی، باید وابستگیهای زیر را در 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 تعریف کنید. اگر هدف شما JVM است، میتوانید از وابستگی androidx.tracing:tracing-wire-desktop:2.0.0-alpha01 استفاده کنید.
کاربرد اولیه
یک TraceSink نحوه سریالسازی بستههای ردیابی را تعریف میکند. Tracing 2.0.0 با پیادهسازی یک Sink ارائه میشود که از قالب بسته ردیابی Perfetto استفاده میکند. یک TraceDriver یک هندل برای Tracer فراهم میکند و میتواند برای نهایی کردن ردیابی مورد استفاده قرار گیرد.
همچنین میتوانید از TraceDriver برای غیرفعال کردن تمام نقاط ردیابی در برنامه استفاده کنید، اگر تصمیم دارید در برخی از انواع برنامه اصلاً ردیابی نکنید. APIهای آینده در 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 را که نقطه ورود برای همه APIهای ردیابی را تعریف میکند، دریافت کنید.
// 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.
میتوانید ببینید که مسیرهای صحیح فرآیند و نخ پر شدهاند و یک بخش ردیابی واحد 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 پایه با بخشهای تو در تو.
میتوانید ببینید که رویدادهای همپوشانی در مسیر نخ اصلی وجود دارد. کاملاً واضح است که 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 به همراه فرادادههای اضافی.
انتشار متن
هنگام استفاده از کوروتینهای کاتلین (یا سایر چارچوبهای مشابه که به بارهای کاری همزمان کمک میکنند)، 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 با انتشار زمینه.
انتشار زمینه، تجسم جریان اجرا را بسیار سادهتر میکند. میتوانید دقیقاً ببینید کدام وظایف به هم مرتبط بودهاند (به یکدیگر متصل بودهاند) و دقیقاً چه زمانی Threads به حالت تعلیق درآمده و از سر گرفته شدهاند .
برای مثال، میتوانید ببینید که slice main taskOne و taskTwo را تولید کرده است. پس از آن هر دو thread غیرفعال بودند (با توجه به اینکه coroutineها به دلیل استفاده از delay به حالت تعلیق درآمده بودند).
انتشار دستی
گاهی اوقات وقتی بارهای کاری همزمان را با استفاده از کوروتینهای کاتلین با نمونههایی از Java Executor ترکیب میکنید، ممکن است مفید باشد که context را از یکی به دیگری منتقل کنید. در اینجا مثالی آورده شده است:
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 پایه با انتشار دستی متن.
میتوانید ببینید که اجرا در یک CoroutineContext شروع شده و متعاقباً به یک Java Executor تغییر یافته است، اما ما همچنان توانستیم از انتشار متن استفاده کنیم.
ترکیب با ردیابیهای سیستم
androidx.tracing جدید اطلاعاتی مانند زمانبندی CPU، میزان استفاده از حافظه و تعامل برنامهها با سیستم عامل را به طور کلی ثبت نمیکند. دلیل این امر این است که این کتابخانه روشی برای انجام ردیابی درونپردازشی با سربار بسیار کم ارائه میدهد.
با این حال، ادغام ردیابیهای سیستم با ردیابیهای درون فرآیند و در صورت نیاز، تجسم آنها به عنوان یک ردیابی واحد بسیار ساده است. دلیل این امر پشتیبانی Perfetto UI از تجسم چندین فایل ردیابی از یک دستگاه در یک جدول زمانی واحد است.
برای انجام این کار، میتوانید با دنبال کردن دستورالعملهای اینجا ، یک جلسه ردیابی سیستم را با استفاده از Perfetto UI شروع کنید.
همچنین میتوانید رویدادهای ردیابی در حال انجام را با استفاده از Tracing 2.0 API ضبط کنید، در حالی که ردیابی سیستم روشن است. هنگامی که هر دو فایل ردیابی را دارید، میتوانید از گزینه 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)
}
}
}
این نتیجه زیر را تولید میکند.

شکل ۷. تصویر صفحه نمایش از یک مسیر 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)
}
}
}
این نتیجه زیر را تولید میکند.

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