Theo dõi trong quá trình xử lý

Thư viện androidx.tracing:tracing:2.0.0-beta01 là một API Kotlin có mức hao tổn thấp , cho phép bạn ghi lại các sự kiện theo dõi trong quy trình. Các sự kiện này có thể ghi lại các khoảng thời gian và bối cảnh của chúng. Thư viện này cũng hỗ trợ việc truyền tải bối cảnh cho các coroutine Kotlin.

Thư viện này sử dụng cùng một Perfetto định dạng gói dấu vết mà nhà phát triển Android đã quen thuộc. Ngoài ra, Tracing 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 đến, 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 truyền tải bối cảnh hoạt động 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 trong build.gradle.kts.

Dự án Kotlin Multiplatform

Các thư viện chỉ cần phát ra các sự kiện theo dõi sẽ phụ thuộc vào API androidx.tracing:tracing gọn nhẹ. Các ứng dụng định cấu hình phần phụ trợ theo dõi cũng sẽ phụ thuộc vào 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")
      }
    }
  }
}

Dự án chỉ dành cho Android

Nếu bạn chỉ nhắm đến Android, hãy thêm nội dung sau vào tệp build.gradle.kts của ứng dụng hoặc thư viện:

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

Khởi chạy và khám phá

Trước khi có thể ghi lại các sự kiện theo dõi, bạn phải khởi chạy cơ sở hạ tầng theo dõi. Việc này liên quan đến việc tạo AbstractTraceDriver và đăng ký Tracer trên toàn cầu.

Android

Trên Android, nếu bạn thêm phần phụ thuộc androidx.tracing:tracing-wire, quá trình khởi chạy sẽ tự động diễn ra khi khởi động ứng dụng bằng thư viện androidx.startup.

Theo mặc định, quá trình khởi chạy tự động này sẽ thực hiện những việc sau:

  • Tạo TraceDriver bằng TraceSink ghi các tệp dấu vết Perfetto vào Context.noBackupFilesDir/perfetto_traces/.

  • Đăng ký Tracer kết quả trên toàn cầu.

Tuỳ chỉnh thực thể TraceDriver

Nếu cần tuỳ chỉnh cấu hình, chẳng hạn như thay đổi vị trí lưu tệp dấu vết hoặc sử dụng TraceSink tuỳ chỉnh, bạn có thể cung cấp thực thể AbstractTraceDriver của riêng mình.

Để tuỳ chỉnh cấu hình, hãy làm cho lớp Application triển khai 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)
    }
}

Trình khởi chạy tự động phát hiện rằng lớp con Application của bạn triển khai Factory và sử dụng trình điều khiển tuỳ chỉnh của bạn.

JVM

Trên JVM, không có cơ chế khởi động tự động. Ứng dụng chịu trách nhiệm khởi chạy TraceDriver và đăng ký Tracer trên toàn cầu trong quá trình khởi động, thường là trong hàm main.

Để đăng ký trình theo dõi, hãy gọi 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()
    })
}

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

TraceSink xác định cách tuần tự hoá các gói dấu vết. Tracing 2.0.0 đi kèm với việc 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.

Sau khi Tracer được khởi chạy (tự động trên Android hoặc thủ công trên JVM), hãy sử dụng thực thể Tracer.global toàn cầu để phát ra các sự kiện theo dõi.

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. Bạn có thể tuỳ ý bật các điểm theo dõi cho một category nhất định bằng cách triển khai isCategoryEnabled khi tạo thực thể TraceDriver.

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

Dưới đây là ví dụ cơ bản về việc phát ra một sự kiện theo dõi bằng Tracer.global trên JVM, bao gồm cả thiết lập thủ công:

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

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 các quy trình và luồng theo dõi chính xác được điền sẵn và chúng tạo ra một phần theo dõi duy nhất basic, chạy trong 100 mili giây.

Các phần theo dõi (hoặc lớp cắt) có thể được lồng ghép 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à ví dụ.

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

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

Ảnh chụp màn hình một 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 ghép.

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 rằng processImage gọi loadImagesharpen trên cùng một luồng.

Thêm siêu dữ liệu bổ sung vào các phần theo dõi

Đôi khi, bạn có thể muốn đính kèm siêu dữ liệu theo bối cảnh bổ sung vào một lớp cắt dấu vết để biết thêm 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()
    @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)
        }
    }
}

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 bối cảnh

Khi sử dụng các coroutine 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, Tracing 2.0 hỗ trợ khái niệm về truyền tải bối 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.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")
    }
}

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ó truyền tải bối cảnh.

Truyền tải bối cảnh giúp bạn dễ dàng hình dung 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 những 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 các coroutine Kotlin với các thực thể của Java Executor, bạn có thể muốn truyền tải bối cảnh từ thực thể này sang thực thể khác. Dưới đây là ví dụ:

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

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ó truyền tải bối cảnh thủ công.

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

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

Thư viện androidx.tracing 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 cách thực hiện theo dõi trong quy trình có mức hao tổn thấp.

Tuy nhiên, việc hợp nhất các dấu vết hệ thống với các dấu vết trong quy trình và hình dung chúng dưới dạng một dấu vết duy nhất nếu cần là cực kỳ đơn giản. Điều này là do Perfetto UI hỗ trợ hình dung nhiều tệp dấu vết từ một thiết bị trên một tiến trình hợp nhất.

Để thực hiện việc này, bạn có thể bắt đầu phiên theo dõi hệ thống bằ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 quy trình bằng API Tracing 2.0 khi bật tính năng theo dõi hệ thống. 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 làm việc nâng cao

Phần này mô tả các quy trình làm việc nâng cao mà bạn có thể triển khai bằng thư viện theo dõi trong quy trình.

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ể làm như sau:

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

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ủ lưu trữ, chẳng hạn như trình bổ trợ trình biên dịch và bộ xử lý chú thích, 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 tìm thấy tệp, lớp hoặc phương thức chịu trách nhiệm tạo ra một phần theo dõi trong dấu vết.

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

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.