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

Thư viện androidx.tracing:tracing:2.0.2 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. Những sự kiện này có thể ghi lại các lát 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 ngữ cảnh cho coroutine Kotlin.

Thư viện này sử dụng cùng một định dạng gói dấu vết Perfetto mà các 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 nhậ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 hoạt động của tính năng truyền bá bối 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 trong build.gradle.kts.

Dự án Kotlin Multiplatform

Những 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 có kích thước nhỏ. Các ứng dụng định cấu hình phần phụ trợ theo dõi cũng phải phụ thuộc vào androidx.tracing:tracing-wire.

kotlin {
  sourceSets {
    commonMain {
      dependencies {
        // API definition
        implementation("androidx.tracing:tracing:2.0.2")
      }
    }
    androidMain {
      dependencies {
        // Android implementation (includes the Perfetto Sink and automatic initialization)
        implementation("androidx.tracing:tracing-wire:2.0.2")
      }
    }
    jvmMain {
      dependencies {
        // JVM implementation
        implementation("androidx.tracing:tracing-wire:2.0.2")
      }
    }
  }
}

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.2")

    // For applications to configure the tracing backend
    implementation("androidx.tracing:tracing-wire:2.0.2")
}

Khởi tạo 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. Thao tác này bao gồm việc tạo một AbstractTraceDriver và đăng ký Tracer của 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, thì 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, quy trình khởi chạy tự động này sẽ thực hiện những việc sau:

  • Tạo một TraceDriverTraceSink ghi các tệp dấu vết Perfetto vào Context.noBackupFilesDir/perfetto_traces/.

  • Đăng ký Tracer thu được trên toàn cầu.

Tuỳ chỉnh phiên bản 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 theo dõi 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 triển khai lớp Application của bạn 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 thấy lớp con Application của bạn triển khai Factory và sử dụng trình tạo trình điều khiển tuỳ chỉnh.

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 động 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 các gói dấu vết được chuyển đổi tuần tự. 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 handle cho Tracer và có thể dùng để hoàn tất một dấu vết.

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

Bạn cũng có thể dùng TraceDriver để tắt tất cả các điểm theo dõi trong ứng dụng, nếu 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 cung cấp một phương thức triển khai cho isCategoryEnabled khi tạo một phiên bản của 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 cách sử dụng Tracer.global trên JVM, bao gồm cả chế độ 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 quy trình và các dấu vết luồng chính xác được điền sẵn và chúng tạo ra một phần dấu vết duy nhất basic, chạy trong 100 mili giây.

Các phần (hoặc lát cắt) dấu vết có thể được lồng trên cùng một đường theo dõi để biểu thị các sự kiện trùng lặp. Sau đây là một 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 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 của một dấu vết Perfetto cơ bản có các phần lồng nhau.

Bạn có thể thấy có các sự kiện trùng lặp trong bản theo dõi chuỗi chính. 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 vào các phần dấu vết

Đôi khi, bạn nên đính kèm siêu dữ liệu theo ngữ cảnh bổ sung vào một lát 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()
    @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)
        }
    }
}

Điều này sẽ tạo ra kết quả sau. Xin 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 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ý các khối lượng công việc đồng thời, Tracing 2.0 hỗ trợ khái niệm về việc truyền ngữ cảnh. Cách tốt nhất để giải thích điều này là dùng 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")
    }
}

Điều 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 bá bối cảnh.

Tính năng Truyền bối cảnh giúp bạn dễ dàng hình dung được quy trình thực thi. Bạn có thể biết chính xác những việc cần làm nào có liên quan (kết nối với những việc khác) và chính xác thời điểm Threads bị tạm dừngtiếp tục.

Ví dụ: bạn có thể thấy rằng lá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 tin theo cách thủ công

Đôi khi, khi kết hợp các khối lượng công việc đồng thời bằng cách sử dụng coroutine Kotlin với các thực thể Executor của Java, bạn nên truyền ngữ 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()
    }
}

Điều 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 với tính năng truyền bối cảnh theo cách thủ công.

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

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

Thư viện androidx.tracing không thu thập 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 hoạt động 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à trực quan hoá 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 dấu vết 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 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 trong khi tính năng theo dõi hệ thống đang bật. Sau khi có cả hai tệp theo dõi, bạn có thể sử dụng lựa chọn Open Multiple Trace Files trong Perfetto.

Mở nhiều tệp theo dõi trong giao diện người dùng Perfetto

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

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

Phần này mô tả các quy trình 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át

Đôi khi, việc phân bổ các lát cắt trong dấu vết cho một thao tác người dùng ở cấp độ cao hơn hoặc một sự kiện hệ thống sẽ rất hữu ích. Ví dụ: để phân bổ tất cả các lát cắt tương ứng với một số hoạt động trong 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)
    }
}

Điều 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át 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à trình xử lý chú giải, cũng có thể chọn nhúng thông tin ngăn xếp lệnh gọi vào một dấu vết để thuận tiện cho việc 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()
    @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)
        }
    }
}

Điều 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 về ngăn xếp lệnh gọi.