การติดตามในกระบวนการ (ทดลอง)

ไลบรารี androidx.tracing:tracing:2.0.0-alpha01 ใหม่เป็น Kotlin API ที่มีค่าใช้จ่ายต่ำ ซึ่งช่วยให้บันทึกเหตุการณ์การติดตามในกระบวนการได้ เหตุการณ์เหล่านี้จะ บันทึกช่วงเวลาและบริบทของช่วงเวลาดังกล่าวได้ นอกจากนี้ ไลบรารียังรองรับการส่งต่อบริบท สำหรับโครูทีนของ Kotlin ด้วย

ไลบรารีใช้รูปแบบแพ็กเก็ตการติดตาม Perfetto เดียวกันกับที่นักพัฒนาแอป Android คุ้นเคย นอกจากนี้ การติดตาม 2.0 (ต่างจาก API ของ 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 dependency ได้หาก กำหนดเป้าหมายเป็น JVM

การใช้งานพื้นฐาน

TraceSink จะกำหนดวิธีจัดรูปแบบแพ็กเก็ตการติดตาม การติดตาม 2.0.0 มาพร้อมกับการใช้งาน Sink ที่ใช้Perfettoรูปแบบแพ็กเก็ตการติดตาม A 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 พื้นฐาน

รูปที่ 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") {
        // ...
    }
}

ซึ่งจะสร้างการติดตามต่อไปนี้

ภาพหน้าจอของการติดตาม Perfetto ขั้นพื้นฐานที่มีส่วนที่ซ้อนกัน

รูปที่ 2 การจับภาพหน้าจอของร่องรอย Perfetto พื้นฐานที่มีส่วนที่ซ้อนกัน

คุณจะเห็นว่ามีเหตุการณ์ที่ทับซ้อนกันในแทร็กชุดข้อความหลัก processImageการเรียกใช้loadImageและsharpenในเธรดเดียวกัน นั้นชัดเจนมาก

เพิ่มข้อมูลเมตาเพิ่มเติมในส่วนการติดตาม

บางครั้งการแนบข้อมูลเมตาเชิงบริบทเพิ่มเติมกับ Trace Slice อาจมีประโยชน์เพื่อให้ได้รายละเอียดเพิ่มเติม ตัวอย่างของข้อมูลเมตาดังกล่าว ได้แก่ 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 ขั้นพื้นฐานที่มีข้อมูลเมตาเพิ่มเติม

รูปที่ 3 ภาพหน้าจอของการติดตาม Perfetto ขั้นพื้นฐานพร้อมข้อมูลเมตาเพิ่มเติม

การส่งต่อบริบท

เมื่อใช้ Kotlin Coroutines (หรือเฟรมเวิร์กอื่นๆ ที่คล้ายกันซึ่งช่วยจัดการภาระงานพร้อมกัน) 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 ที่มีการส่งต่อบริบท

รูปที่ 4 ภาพหน้าจอของร่องรอย Perfetto พื้นฐานที่มีการส่งต่อบริบท

การส่งต่อบริบทช่วยให้เห็นภาพโฟลว์ของการ ดำเนินการได้ง่ายขึ้นมาก คุณจะเห็นว่างานใดที่เกี่ยวข้อง (เชื่อมต่อกับงานอื่นๆ) และเวลาที่Threadsระงับและกลับมาทำงานต่อ

เช่น คุณจะเห็นว่าสไลซ์ main สร้าง taskOne และ taskTwo หลังจากนั้นทั้ง 2 เธรดก็ไม่มีการใช้งาน (เนื่องจากมีการระงับโครูทีนเพราะใช้ delay)

การเผยแพร่ด้วยตนเอง

บางครั้งเมื่อคุณผสมภาระงานพร้อมกันโดยใช้โคโรทีน Kotlin กับอินสแตนซ์ของ 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 {
        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 และต่อมาได้เปลี่ยนไปใช้ Executor ของ Java แต่เราก็ยังใช้การส่งต่อบริบทได้

รวมกับการติดตามระบบ

androidx.tracing ใหม่จะไม่บันทึกข้อมูล เช่น การจัดกำหนดการ CPU การใช้หน่วยความจำ และการโต้ตอบของแอปพลิเคชันกับระบบปฏิบัติการโดยทั่วไป เนื่องจากไลบรารีมีวิธีลด ค่าใช้จ่ายในการติดตามในกระบวนการ

อย่างไรก็ตาม การผสานรวมการติดตามของระบบกับการติดตามในกระบวนการ และแสดงภาพเป็นการติดตามเดียวหากจำเป็นนั้นเป็นเรื่องง่ายมาก เนื่องจาก Perfetto UI รองรับการแสดงภาพไฟล์การติดตามหลายไฟล์จากอุปกรณ์ในไทม์ไลน์แบบรวม

โดยคุณเริ่มเซสชันการติดตามระบบได้โดยใช้ Perfetto UI โดยทำตามวิธีการที่นี่

นอกจากนี้ คุณยังบันทึกเหตุการณ์การติดตามในกระบวนการได้โดยใช้ Tracing 2.0 API ขณะที่เปิดการติดตามระบบ เมื่อมีไฟล์การติดตามทั้ง 2 ไฟล์แล้ว คุณจะใช้ตัวเลือก Open Multiple Trace Files ใน Perfetto ได้

การเปิดไฟล์การติดตามหลายไฟล์ใน UI ของ Perfetto

รูปที่ 6 การเปิดไฟล์การติดตามหลายไฟล์ใน UI ของ Perfetto

เวิร์กโฟลว์ขั้นสูง

เชื่อมโยงชิ้นส่วน

บางครั้งการระบุแหล่งที่มาของ Slice ในการติดตามไปยังการดำเนินการของผู้ใช้ในระดับที่สูงขึ้นหรือเหตุการณ์ของระบบก็มีประโยชน์ ตัวอย่างเช่น หากต้องการระบุแหล่งที่มาของ Slice ทั้งหมดที่ สอดคล้องกับงานที่ทำอยู่เบื้องหลังบางอย่างเป็นส่วนหนึ่งของการแจ้งเตือน คุณอาจทำ ดังนี้

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 ที่มีสไลซ์ที่สัมพันธ์กัน

รูปที่ 7 ภาพหน้าจอของการติดตาม Perfetto ที่มี Slice ที่สัมพันธ์กัน

เพิ่มข้อมูลสแต็กการเรียกใช้

เครื่องมือฝั่งโฮสต์ (ปลั๊กอินคอมไพเลอร์ โปรแกรมประมวลผลคำอธิบายประกอบ ฯลฯ) สามารถเลือกฝังข้อมูลสแต็กการเรียกใช้ลงในร่องรอยเพิ่มเติมได้ เพื่อให้สะดวกในการค้นหาไฟล์ คลาส หรือเมธอดที่รับผิดชอบในการสร้างส่วนร่องรอยในร่องรอย

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 พร้อมข้อมูลสแต็กการเรียก

รูปที่ 8 ภาพหน้าจอของ Perfetto Trace พร้อมข้อมูล Call Stack