প্রক্রিয়াধীন ট্রেসিং

androidx.tracing:tracing:2.0.0-beta01 লাইব্রেরিটি একটি স্বল্প খরচের কোটলিন এপিআই, যা আপনাকে চলমান ট্রেস ইভেন্টগুলো ক্যাপচার করতে দেয়। এই ইভেন্টগুলো টাইম স্লাইস এবং সেগুলোর কনটেক্সট ক্যাপচার করতে পারে। লাইব্রেরিটি কোটলিন কো-রুটিনের জন্য কনটেক্সট প্রোপাগেশনও সমর্থন করে।

এই লাইব্রেরিটি সেই একই পারফেটটো ট্রেস প্যাকেট ফরম্যাট ব্যবহার করে, যার সাথে অ্যান্ড্রয়েড ডেভেলপাররা পরিচিত। এছাড়াও, ট্রেসিং 2.0 ( 1.0.0-* এপিআইগুলোর মতো নয়) প্লাগেবল ট্রেসিং ব্যাকএন্ড এবং সিঙ্ক- এর ধারণা সমর্থন করে, ফলে অন্যান্য ট্রেসিং লাইব্রেরিগুলো তাদের নিজস্ব ইমপ্লিমেন্টেশনে আউটপুট ট্রেসিং ফরম্যাট এবং কনটেক্সট প্রোপাগেশন কীভাবে কাজ করে, তা কাস্টমাইজ করতে পারে।

নির্ভরশীলতা

ট্রেসিং শুরু করতে, আপনাকে আপনার build.gradle.kts ফাইলে ডিপেন্ডেন্সিগুলো নির্ধারণ করতে হবে।

কোটলিন মাল্টিপ্ল্যাটফর্ম প্রকল্প

যেসব লাইব্রেরির শুধু ট্রেস ইভেন্ট নির্গত করার প্রয়োজন, তাদের হালকা androidx.tracing:tracing API-এর উপর নির্ভর করা উচিত। যেসব অ্যাপ ট্রেসিং ব্যাকএন্ড কনফিগার করে, তাদেরও 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 লাইব্রেরি ব্যবহার করে ইনিশিয়ালাইজেশন স্বয়ংক্রিয়ভাবে সম্পন্ন হয়।

ডিফল্টরূপে, এই স্বয়ংক্রিয় প্রারম্ভিকীকরণ নিম্নলিখিত কাজগুলো করে থাকে:

  • একটি TraceSink সহ একটি TraceDriver তৈরি করে যা 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 ইমপ্লিমেন্ট করে এবং আপনার কাস্টম ড্রাইভার ফ্যাক্টরি ব্যবহার করে।

জেভিএম

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 চালু হয়ে গেলে, যা Android-এ স্বয়ংক্রিয়ভাবে অথবা JVM-এ ম্যানুয়ালি হতে পারে, ট্রেস ইভেন্ট নির্গত করার জন্য গ্লোবাল Tracer.global ইনস্ট্যান্সটি ব্যবহার করুন।

আপনি যদি অ্যাপ্লিকেশনের কিছু সংস্করণে একেবারেই ট্রেস করতে না চান, তবে TraceDriver ব্যবহার করে অ্যাপ্লিকেশনের সমস্ত ট্রেস পয়েন্ট নিষ্ক্রিয়ও করতে পারেন। TraceDriver এর একটি ইনস্ট্যান্স তৈরি করার সময় isCategoryEnabled জন্য একটি ইমপ্লিমেন্টেশন প্রদান করে আপনি ঐচ্ছিকভাবে একটি নির্দিষ্ট category জন্য ট্রেস পয়েন্ট সক্রিয় করতে পারেন।

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

ম্যানুয়াল সেটআপ সহ, JVM-এ Tracer.global ব্যবহার করে একটি ট্রেস ইভেন্ট নির্গত করার একটি সাধারণ উদাহরণ এখানে দেওয়া হলো:

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

এর ফলে নিম্নলিখিত ট্রেসটি তৈরি হয়।

একটি সাধারণ পারফেটটো ট্রেসের স্ক্রিনশট।

চিত্র ১. একটি সাধারণ পারফেটটো ট্রেসের স্ক্রিনশট।

আপনি দেখতে পাচ্ছেন যে সঠিক প্রসেস এবং থ্রেড ট্র্যাকগুলি পূরণ করা হয়েছে এবং সেগুলি একটি একক ট্রেস সেকশন 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") {
        // ...
    }
}

এর ফলে নিম্নলিখিত ট্রেসটি তৈরি হয়।

নেস্টেড সেকশন সহ একটি বেসিক পারফেটটো ট্রেসের স্ক্রিনশট।

চিত্র ২. নেস্টেড সেকশনসহ একটি বেসিক পারফেটটো ট্রেসের স্ক্রিনশট।

আপনি দেখতে পাচ্ছেন যে প্রধান থ্রেড ট্র্যাকে ওভারল্যাপিং ইভেন্ট রয়েছে। এটা খুব স্পষ্ট যে 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 তৈরি করার সময় যোগ করা কী-ভ্যালু পেয়ারগুলো রয়েছে।

অতিরিক্ত মেটাডেটা সহ একটি সাধারণ পারফেটটো ট্রেসের স্ক্রিনশট।

চিত্র ৩. অতিরিক্ত মেটাডেটা সহ একটি সাধারণ পারফেটটো ট্রেসের স্ক্রিনশট।

প্রসঙ্গ প্রচার

কোটলিন কোরাউটিন বা কনকারেন্ট ওয়ার্কলোডে সাহায্যকারী অন্যান্য অনুরূপ ফ্রেমওয়ার্ক ব্যবহার করার সময়, ট্রেসিং ২.০ কনটেক্সট প্রোপাগেশন ধারণাটিকে সমর্থন করে। একটি উদাহরণের মাধ্যমে এটি সবচেয়ে ভালোভাবে ব্যাখ্যা করা যায়।

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

এর ফলে নিম্নলিখিত ফলাফলটি পাওয়া যায়।

কনটেক্সট প্রোপাগেশন সহ একটি পারফেটটো ট্রেসের স্ক্রিনশট।

চিত্র ৪। কনটেক্সট প্রোপাগেশন সহ একটি বেসিক পারফেটটো ট্রেসের স্ক্রিনশট।

কন্টেক্সট প্রোপাগেশন এক্সিকিউশনের প্রবাহকে কল্পনা করা অনেক সহজ করে তোলে। আপনি স্পষ্টভাবে দেখতে পারেন কোন টাস্কগুলো একে অপরের সাথে সম্পর্কিত ছিল, এবং ঠিক কখন Threads সাসপেন্ডরিজুম করা হয়েছিল।

উদাহরণস্বরূপ, আপনি দেখতে পারেন যে স্লাইস main taskOne এবং taskTwo তৈরি করেছে। এরপর, delay ব্যবহারের কারণে কো-রুটিনগুলো সাসপেন্ড হয়ে যাওয়ায় উভয় থ্রেডই নিষ্ক্রিয় ছিল।

ম্যানুয়াল প্রচার

কখনও কখনও, যখন আপনি কোটলিন কো-রুটিন এবং জাভা Executor ইনস্ট্যান্স ব্যবহার করে সমান্তরাল ওয়ার্কলোড একসাথে চালান, তখন একটি থেকে অন্যটিতে কনটেক্সট স্থানান্তর করা দরকারি হতে পারে। এখানে একটি উদাহরণ দেওয়া হলো:

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

এর ফলে নিম্নলিখিত ফলাফলটি পাওয়া যায়।

ম্যানুয়াল কনটেক্সট প্রোপাগেশন সহ একটি পারফেটটো ট্রেসের স্ক্রিনশট।

চিত্র ৫. ম্যানুয়াল কনটেক্সট প্রোপাগেশন সহ একটি বেসিক পারফেটটো ট্রেসের স্ক্রিনশট।

আপনি দেখতে পাচ্ছেন যে এক্সিকিউশন একটি CoroutineContext এ শুরু হয়েছিল এবং পরবর্তীতে একটি Java Executor এ স্থানান্তরিত হয়েছিল, কিন্তু তারপরেও আমরা কনটেক্সট প্রোপাগেশন ব্যবহার করতে সক্ষম হয়েছিলাম।

সিস্টেম ট্রেসের সাথে একত্রিত করুন

androidx.tracing লাইব্রেরিটি সিপিইউ শিডিউলিং, মেমরি ব্যবহার এবং সাধারণভাবে অপারেটিং সিস্টেমের সাথে অ্যাপ্লিকেশনের মিথস্ক্রিয়ার মতো তথ্য সংগ্রহ করে না। এর কারণ হলো, লাইব্রেরিটি স্বল্প খরচে ইন-প্রসেস ট্রেসিং করার একটি উপায় প্রদান করে।

তবে, প্রয়োজনে সিস্টেম ট্রেস এবং ইন-প্রসেস ট্রেস একত্রিত করে একটি একক ট্রেস হিসাবে দেখা অত্যন্ত সহজ। এর কারণ হলো, Perfetto UI একটি ডিভাইস থেকে একাধিক ট্রেস ফাইলকে একটি সমন্বিত টাইমলাইনে দেখানোর সুবিধা দেয়।

এটি করার জন্য, আপনি এখানের নির্দেশাবলী অনুসরণ করে Perfetto UI ব্যবহার করে একটি সিস্টেম ট্রেসিং সেশন শুরু করতে পারেন।

সিস্টেম ট্রেসিং চালু থাকা অবস্থায়, আপনি Tracing 2.0 এপিআই ব্যবহার করে চলমান ট্রেস ইভেন্টগুলোও রেকর্ড করতে পারেন। দুটি ট্রেস ফাইলই পেয়ে গেলে আপনি পারফেটোতে থাকা ‘ Open Multiple Trace Files অপশনটি ব্যবহার করতে পারবেন।

পারফেটটো UI-তে একাধিক ট্রেস ফাইল খোলা

চিত্র ৬. পারফেটটো UI-তে একাধিক ট্রেস ফাইল খোলা।

উন্নত কর্মপ্রবাহ

এই বিভাগে ইন-প্রসেস ট্রেসিং লাইব্রেরি ব্যবহার করে বাস্তবায়নযোগ্য উন্নত ওয়ার্কফ্লো বর্ণনা করা হয়েছে।

স্লাইসগুলির মধ্যে সম্পর্ক স্থাপন করুন

কখনও কখনও, একটি ট্রেসের স্লাইসগুলিকে আরও উচ্চ-স্তরের ব্যবহারকারীর কার্যকলাপ বা সিস্টেম ইভেন্টের সাথে যুক্ত করা দরকারি হতে পারে। উদাহরণস্বরূপ, কোনো ব্যাকগ্রাউন্ড কাজের সাথে সম্পর্কিত সমস্ত স্লাইসকে একটি নোটিফিকেশনের অংশ হিসাবে যুক্ত করতে, আপনি এইরকম কিছু করতে পারেন:

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

এর ফলে নিম্নলিখিত ফলাফলটি পাওয়া যায়।

পারস্পরিক সম্পর্কযুক্ত স্লাইস সহ একটি পারফেটটো ট্রেসের স্ক্রিনশট

চিত্র ৭। পারস্পরিক সম্পর্কযুক্ত স্লাইসসহ একটি পারফেটটো ট্রেসের স্ক্রিনশট।

কল স্ট্যাক তথ্য যোগ করুন

হোস্ট-সাইড টুল, যেমন কম্পাইলার প্লাগইন এবং অ্যানোটেশন প্রসেসর, একটি ট্রেসের মধ্যে কল স্ট্যাকের তথ্য এম্বেড করতে পারে, যাতে ট্রেসের কোনো একটি অংশ তৈরির জন্য দায়ী ফাইল, ক্লাস বা মেথডটি সুবিধাজনকভাবে খুঁজে পাওয়া যায়।

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

এর ফলে নিম্নলিখিত ফলাফলটি পাওয়া যায়।

কল স্ট্যাকের তথ্যসহ পারফেটটো ট্রেসের স্ক্রিনশট।

চিত্র ৮. কল স্ট্যাকের তথ্যসহ পারফেটটো ট্রেসের স্ক্রিনশট।