Theo dõi trong quá trình xử lý (Thử nghiệm)

Thư viện androidx.tracing:tracing:2.0.0-alpha04 mới là một API Kotlin có mức hao tổn thấp , cho phép ghi lại các sự kiện theo dõi trong quá trình xử lý. Các sự kiện này có thể ghi lại các khoảng thời gian và ngữ cảnh của chúng. Ngoài ra, thư viện này còn hỗ trợ việc truyền tải ngữ cảnh cho Coroutine của Kotlin.

Thư viện này sử dụng cùng một Perfetto định dạng gói dấu vết mà Android các nhà phát triển đã quen thuộc. Ngoài ra, tính năng Theo dõi 2.0 (không giống như các API 1.0.0-*) hỗ trợ khái niệm về các phần phụ trợ theo dõi có thể cắmcác đích, vì vậy, các thư viện theo dõi khác có thể tuỳ chỉnh định dạng theo dõi đầu ra và cách hoạt động của tính năng truyền tải ngữ cảnh trong quá trình triển khai.

Phần phụ thuộc

Để bắt đầu theo dõi, bạn cần xác định các phần phụ thuộc sau trong build.gradle.kts.

kotlin {
  androidLibrary {
    namespace = "com.example.library"
    // ...
  }
  sourceSets {
    androidMain {
      dependencies {
        api("androidx.tracing:tracing-wire:2.0.0-alpha04")
        // ...
      }
    }
    jvmMain {
      dependencies {
        api("androidx.tracing:tracing-wire:2.0.0-alpha04")
        // ...
      }
    }
  }
}

Khai báo phần phụ thuộc trên androidx.tracing:tracing-wire:2.0.0-alpha04 nếu bạn đang nhắm đến một thư viện Android, một ứng dụng Android hoặc nếu bạn đang nhắm đến JVM.

Cách sử dụng cơ bản

TraceSink xác định cách gói dấu vết được tuần tự hoá. Tính năng theo dõi 2.0.0 đi kèm với một cách triển khai Sink sử dụng định dạng gói dấu vết Perfetto. TraceDriver cung cấp một trình xử lý cho Tracer và có thể dùng để hoàn tất dấu vết.

Bạn cũng có thể sử dụng TraceDriver để tắt tất cả các điểm theo dõi trong ứng dụng, nếu bạn chọn không theo dõi trong một số biến thể ứng dụng. Các API trong tương lai trong TraceDriver cũng sẽ cho phép nhà phát triển kiểm soát những danh mục dấu vết mà họ muốn ghi lại (hoặc tắt khi một danh mục gây ra nhiều nhiễu).

Để bắt đầu, hãy tạo một thực thể của TraceSinkTraceDriver.

/**
 * 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.wire.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
}

Sau khi có một thực thể của TraceDriver, hãy lấy Tracer xác định điểm truy cập cho tất cả các API theo dõi.

// 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 {
        it.tracer.trace(category = CATEGORY_MAIN, name = "basic") {
            Thread.sleep(100L)
        }
    }
}

Thao tác này sẽ tạo ra dấu vết sau.

Ảnh chụp màn hình dấu vết Perfetto cơ bản

Hình 1. Ảnh chụp màn hình dấu vết Perfetto cơ bản.

Bạn có thể thấy rằng quá trình và các luồng chính xác được điền sẵn và tạo ra một phần dấu vết basic, chạy trong 100ms.

Các phần dấu vết (hoặc lớp cắt) có thể được lồng trên cùng một dấu vết để biểu thị các sự kiện chồng chéo. Dưới đây là một ví dụ.

fun main() {
    // Initialize the tracing infrastructure to monitor app performance
    val driver = createTraceDriver()
    val tracer = driver.tracer
    driver.use {
        it.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") {
        // ...
    }
}

Thao tác này sẽ tạo ra dấu vết sau.

Ảnh chụp màn hình dấu vết Perfetto cơ bản có các phần lồng nhau

Hình 2. Ảnh chụp màn hình dấu vết Perfetto cơ bản có các phần lồng nhau.

Bạn có thể thấy rằng có các sự kiện chồng chéo trong dấu vết luồng chính. Rất rõ ràng là processImage gọi loadImagesharpen trên cùng một luồng.

Thêm siêu dữ liệu bổ sung trong các phần dấu vết

Đôi khi, bạn có thể muốn đính kèm siêu dữ liệu theo ngữ cảnh bổ sung vào một lớp cắt dấu vết để biết thêm thông tin chi tiết. Một số ví dụ về siêu dữ liệu như vậy có thể bao gồm nav destination mà người dùng đang sử dụng hoặc input arguments có thể xác định thời gian thực hiện một hàm.

fun main() {
    val driver = createTraceDriver()
    driver.use {
        it.tracer.trace(
            category = CATEGORY_MAIN,
            name = "basicWithContext",
            // Add additional metadata
            metadataBlock = {
                // Add key value pairs.
                addMetadataEntry("key", "value")
                addMetadataEntry("count", 1L)
            }
        ) {
            Thread.sleep(100L)
        }
    }
}

Thao tác này sẽ tạo ra kết quả sau. Lưu ý rằng phần Arguments chứa các cặp khoá-giá trị được thêm khi tạo slice.

Ảnh chụp màn hình dấu vết Perfetto cơ bản có siêu dữ liệu bổ sung

Hình 3. Ảnh chụp màn hình dấu vết Perfetto cơ bản có siêu dữ liệu bổ sung.

Truyền tải ngữ cảnh

Khi sử dụng Coroutine của Kotlin (hoặc các khung tương tự khác giúp xử lý khối lượng công việc đồng thời), tính năng Theo dõi 2.0 hỗ trợ khái niệm về truyền tải ngữ cảnh. Cách tốt nhất để giải thích điều này là thông qua một ví dụ.

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 {
        it.tracer.traceCoroutine(category = CATEGORY_MAIN, name = "main") {
            coroutineScope {
                launch { taskOne(tracer) }
                launch { taskTwo(tracer) }
            }
        }
        println("All done")
    }
}

Thao tác này sẽ tạo ra kết quả sau.

Ảnh chụp màn hình dấu vết Perfetto có tính năng truyền ngữ cảnh

Hình 4. Ảnh chụp màn hình dấu vết Perfetto cơ bản có tính năng truyền tải ngữ cảnh.

Tính năng Truyền tải ngữ cảnh giúp bạn dễ dàng trực quan hoá luồng thực thi hơn rất nhiều. Bạn có thể thấy chính xác những tác vụ nào có liên quan (kết nối với các tác vụ khác) và chính xác thời điểm Threads bị tạm ngưngtiếp tục.

Ví dụ: bạn có thể thấy rằng lớp cắt main đã tạo ra taskOnetaskTwo. Sau đó, cả hai luồng đều không hoạt động (vì các coroutine bị tạm ngưng – do sử dụng delay).

Truyền tải thủ công

Đôi khi, khi bạn kết hợp khối lượng công việc đồng thời bằng cách sử dụng coroutine của Kotlin với các thực thể của Java Executor, bạn có thể muốn truyền tải ngữ cảnh từ thực thể này sang thực thể khác. Dưới đây là ví dụ:

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 {
        it.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()
    }
}

Thao tác này sẽ tạo ra kết quả sau.

Ảnh chụp màn hình dấu vết Perfetto với tính năng truyền bối cảnh theo cách thủ công

Hình 5. Ảnh chụp màn hình dấu vết Perfetto cơ bản có tính năng truyền tải ngữ cảnh thủ công.

Bạn có thể thấy rằng quá trình thực thi bắt đầu trong CoroutineContext, sau đó chuyển sang Executor của Java, nhưng chúng tôi vẫn có thể sử dụng tính năng truyền tải ngữ cảnh.

Kết hợp với dấu vết hệ thống

androidx.tracing mới không ghi lại thông tin như lập lịch CPU, mức sử dụng bộ nhớ và tương tác của ứng dụng với hệ điều hành nói chung. Điều này là do thư viện cung cấp một cách để thực hiện việc theo dõi trong quá trình xử lý với mức hao tổn rất thấp.

Tuy nhiên, việc hợp nhất dấu vết hệ thống với dấu vết trong quá trình xử lý và hình dung chúng dưới dạng một dấu vết duy nhất là cực kỳ đơn giản nếu cần. Điều này là do Perfetto UI hỗ trợ trực quan hoá nhiều tệp theo dõi từ một Thiết bị trên một dòng thời gian hợp nhất.

Để thực hiện việc này, bạn có thể bắt đầu một phiên theo dõi hệ thống bằng cách sử dụng Perfetto UI bằng cách làm theo hướng dẫn tại đây.

Bạn cũng có thể ghi lại các sự kiện theo dõi trong quá trình xử lý bằng API Tracing 2.0, trong khi tính năng theo dõi hệ thống được bật. Sau khi có cả hai tệp dấu vết, bạn có thể sử dụng tuỳ chọn Open Multiple Trace Files trong Perfetto.

Mở nhiều tệp dấu vết trong giao diện người dùng Perfetto

Hình 6. Mở nhiều tệp dấu vết trong giao diện người dùng Perfetto.

Quy trình công việc nâng cao

Tương quan các lớp cắt

Đôi khi, bạn nên phân bổ các lớp cắt trong dấu vết cho một hành động của người dùng ở cấp cao hơn hoặc một sự kiện hệ thống. Ví dụ: để phân bổ tất cả các lớp cắt tương ứng với một số công việc ở chế độ nền như một phần của thông báo, bạn có thể thực hiện như sau:

fun main() {
    val driver = createTraceDriver()
    onEvent(driver, eventId = EVENT_ID)
}

fun onEvent(driver: TraceDriver, eventId: Long) {
    driver.use {
        it.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)
        }
    }
}

Thao tác này sẽ tạo ra kết quả sau.

Ảnh chụp màn hình dấu vết Perfetto có các lát cắt tương quan

Hình 7. Ảnh chụp màn hình dấu vết Perfetto có các lớp cắt tương quan.

Thêm thông tin ngăn xếp lệnh gọi

Các công cụ phía máy chủ (trình bổ trợ trình biên dịch, trình xử lý chú giải, v.v.) cũng có thể chọn nhúng thông tin ngăn xếp lệnh gọi vào dấu vết để giúp bạn dễ dàng xác định vị trí tệp, lớp hoặc phương thức chịu trách nhiệm tạo ra một phần dấu vết trong dấu vết.

fun main() {
    val driver = createTraceDriver()
    driver.use {
        it.tracer.trace(
            category = CATEGORY_MAIN,
            name = "callStackEntry",
            metadataBlock = {
                addCallStackEntry(
                    name = "main",
                    lineNumber = 14,
                    sourceFile = "Basic.kt"
                )
            }
        ) {
            Thread.sleep(100L)
        }
    }
}

Thao tác này sẽ tạo ra kết quả sau.

Ảnh chụp màn hình dấu vết Perfetto có thông tin về ngăn xếp lệnh gọi

Hình 8. Ảnh chụp màn hình dấu vết Perfetto có thông tin ngăn xếp lệnh gọi.