Thư viện androidx.tracing:tracing:2.0.0-alpha01 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 quy trình. Các sự kiện này có thể ghi lại các lát thời gian và ngữ cảnh của chúng. Thư viện này cũng hỗ trợ việc truyền ngữ cảnh cho Coroutine của 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, 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ắm và cá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 sau trong 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")
// ...
}
}
}
}
Khai báo một phần phụ thuộc trên androidx.tracing:tracing-wire-android:2.0.0-alpha01 nếu bạn đang nhắm đến một thư viện Android hoặc một ứng dụng. Bạn có thể sử dụng phần phụ thuộc androidx.tracing:tracing-wire-desktop:2.0.0-alpha01 nếu đang nhắm đến JVM.
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.
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 có nhiều dữ liệu không liên quan).
Để bắt đầu, hãy tạo một phiên bản của TraceSink và 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
}
Sau khi bạn có một thực thể 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 {
driver.tracer.trace(category = CATEGORY_MAIN, name = "basic") {
Thread.sleep(100L)
}
}
}
Thao tác này sẽ tạo ra dấu vết sau.
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à tạo ra một phần dấu vết duy nhất basic, chạy trong 100ms.
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 để biểu thị các sự kiện trùng lặp. Sau đâ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 {
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.
Hình 2. Ả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.
Bạn có thể thấy có các sự kiện trùng lặp trong dấu vết của luồng chính. Rõ ràng là processImage gọi loadImage và sharpen 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()
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)
}
}
}
Đ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.
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 ngữ cảnh
Khi sử dụng 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 sẽ hỗ trợ khái niệm truyền bá bối 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: 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")
}
}
Điều này sẽ tạo ra kết quả sau.
Hình 4. Ảnh chụp màn hình của một 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 luồng thực thi hơn rất nhiều. 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 ngưng và tiếp tục.
Ví dụ: bạn có thể thấy rằng lát cắt main đã tạo ra taskOne và taskTwo.
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 dữ liệu 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 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(
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()
}
}
Điều này sẽ tạo ra kết quả sau.
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
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 hoạt động truy vết trong quy trình có mức hao tổn rất thấp.
Tuy nhiên, bạn có thể 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 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.
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
Tương quan các lát
Đôi khi, bạn nên phân bổ các lát cắt trong một 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á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()
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)
}
}
}
Điều này sẽ tạo ra kết quả sau.
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ủ (trình bổ trợ trình biên dịch, trình xử lý chú thích, v.v.) 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()
driver.use {
driver.tracer.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.
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.