ספריית androidx.tracing:tracing:2.0.0-beta01 היא Kotlin API עם תקורה נמוכה, שמאפשרת לכם לתעד אירועי מעקב בתהליך. האירועים האלה יכולים לתעד פלחים של זמן והקשר שלהם. הספרייה תומכת גם בהעברת הקשר לשגרות המשנה של Kotlin.
הספרייה משתמשת באותו פורמט של מנות נתונים למעקב של Perfetto שמוכר למפתחי Android. בנוסף, Tracing 2.0 (בניגוד לממשקי ה-API של 1.0.0-*) תומך במושג של pluggable tracing backends וsinks, כך שספריות Tracing אחרות יכולות להתאים אישית את פורמט הפלט של Tracing ואת אופן הפצת ההקשר בהטמעה שלהן.
פניות קשורות
כדי להתחיל במעקב, צריך להגדיר את התלות בקובץ build.gradle.kts.
פרויקטים של Kotlin Multiplatform
ספריות שצריכות רק לפלוט אירועי מעקב צריכות להיות תלויות ב-API הקל androidx.tracing:tracing. אפליקציות שמגדירות את הקצה העורפי של המעקב צריכות להיות תלויות גם ב-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")
}
}
}
}
פרויקטים ל-Android בלבד
אם אתם מטargetים רק את Android, מוסיפים את השורה הבאה לקובץ build.gradle.kts של האפליקציה או של הספרייה:
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")
}
אתחול וגילוי
כדי שתוכלו לתעד אירועי מעקב, אתם צריכים לאתחל את תשתית המעקב. התהליך כולל יצירה של AbstractTraceDriver ורישום של Tracer שלו ברחבי העולם.
Android
ב-Android, אם כוללים את התלות androidx.tracing:tracing-wire, האתחול מתבצע באופן אוטומטי בהפעלת האפליקציה באמצעות הספרייה androidx.startup.
כברירת מחדל, האתחול האוטומטי הזה מבצע את הפעולות הבאות:
יוצר
TraceDriverעםTraceSinkשכותב קובצי מעקב של Perfetto אלContext.noBackupFilesDir/perfetto_traces/.הפונקציה רושמת את התוצאה
Tracerבאופן גלובלי.
התאמה אישית של מופע TraceDriver
אם אתם צריכים להתאים אישית את ההגדרה, למשל כדי לשנות את המיקום שבו נשמרים קובצי המעקב או כדי להשתמש ב-TraceSink בהתאמה אישית, אתם יכולים לספק מופע AbstractTraceDriver משלכם.
כדי להתאים אישית את ההגדרה, צריך לגרום למחלקה Application ליישם את 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)
}
}
המאחלים האוטומטי מזהה ש-Application subclass מיישם את Factory ומשתמש במפעל מנהלי ההתקנים המותאם אישית.
JVM
ב-JVM, אין מנגנון אתחול אוטומטי. האפליקציה אחראית לאתחול של TraceDriver ולרישום של Tracer באופן גלובלי במהלך ההפעלה, בדרך כלל בפונקציה main.
כדי לרשום את המכשיר לאיתור, מתקשרים למספר 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()
})
}
שימוש בסיסי
TraceSink מגדיר איך חבילות מעקב עוברות סריאליזציה. Tracing 2.0.0 כולל הטמעה של Sink שמשתמש בפורמט מנות המידע של Perfetto. TraceDriver מספק נקודת אחיזה ל-Tracer ואפשר להשתמש בו כדי להשלים את המעקב.
אחרי שהספרייה Tracer מאותחלת, באופן אוטומטי ב-Android או באופן ידני ב-JVM, משתמשים במופע הגלובלי Tracer.global כדי לשלוח אירועי מעקב.
אפשר גם להשתמש ב-TraceDriver כדי להשבית את כל נקודות המעקב באפליקציה, אם בוחרים לא לעקוב אחרי וריאציות מסוימות של האפליקציה. אפשר להפעיל נקודות מעקב עבור category מסוים על ידי מתן הטמעה עבור isCategoryEnabled כשיוצרים מופע של TraceDriver.
val driver = TraceDriver(
sink = sink,
isCategoryEnabled = { category ->
// Only enable trace points in the "com.example" package
category.startsWith("com.example")
}
)
דוגמה בסיסית לשליחת אירוע מעקב באמצעות Tracer.global ב-JVM, כולל הגדרה ידנית:
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)
}
}
}
הפעולה הזו יוצרת את ה-trace הבא.
איור 1. צילום מסך של נתוני מעקב בסיסיים של Perfetto.
אפשר לראות שהתהליך והתהליכון הנכונים מופיעים, ושנוצר קטע מעקב יחיד basic, שפעל למשך 100 אלפיות השנייה.
אפשר להציב חלקים (או פרוסות) של מעקב אחד בתוך מעקב אחר באותו מסלול כדי לייצג אירועים חופפים. כאן מוצגת דוגמה.
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") {
// ...
}
}
הפעולה הזו יוצרת את ה-trace הבא.
איור 2. צילום מסך של מעקב Perfetto בסיסי עם קטעים מוטמעים.
אפשר לראות שיש חפיפה בין האירועים ב-track של ה-thread הראשי. ברור מאוד ש-processImage קורא ל-loadImage ול-sharpen באותו השרשור.
הוספת מטא-נתונים נוספים בקטעי מעקב
לפעמים כדאי לצרף פרוסת נתונים של מעקב עם מטא-נתונים נוספים לפי הקשר, כדי לקבל פרטים נוספים. דוגמאות למטא-נתונים כאלה יכולות לכלול את nav destination שבו המשתמש נמצא, או input arguments שעשויים לקבוע את משך הזמן שלוקח לפונקציה לפעול.
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)
}
}
}
זו התוצאה שמתקבלת. שימו לב: בקטע Arguments מופיעים צמדים של מפתח/ערך שנוספו במהלך יצירת slice.
איור 3. צילום מסך של מעקב בסיסי ב-Perfetto עם מטא-נתונים נוספים.
העברת הקשר
כשמשתמשים ב-Kotlin coroutines או במסגרות דומות אחרות שעוזרות בניהול עומסי עבודה מקבילים, Tracing 2.0 תומך בהעברת הקשר. ההסבר הטוב ביותר הוא באמצעות דוגמה.
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")
}
}
זו התוצאה שמתקבלת.
איור 4. צילום מסך של מעקב Perfetto בסיסי עם העברת הקשר.
התכונה 'העברת הקשר' מאפשרת לראות את זרימת הביצוע בצורה פשוטה יותר. תוכלו לראות בדיוק אילו משימות היו קשורות (מקושרות למשימות אחרות),
ובדיוק מתי Threads הושעו וחודשו.
לדוגמה, אפשר לראות שהפלח main יצר את הפלחים taskOne ו-taskTwo.
אחרי זה, שני השרשורים היו לא פעילים כי הקורוטינות הושעו בגלל השימוש ב-delay.
הפצה ידנית
לפעמים, כשמשלבים עומסי עבודה מקבילים באמצעות קורוטינות של Kotlin עם מופעים של Java Executor, כדאי להעביר את ההקשר מאחד לשני. לדוגמה:
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()
}
}
זו התוצאה שמתקבלת.
איור 5. צילום מסך של מעקב Perfetto בסיסי עם העברת הקשר ידנית.
אפשר לראות שהביצוע התחיל ב-CoroutineContext, ואחר כך עבר ל-Executor ב-Java, אבל עדיין הצלחנו להשתמש בהעברת הקשר.
שילוב עם נתוני מעקב של המערכת
ספריית androidx.tracing לא מתעדת מידע כמו תזמון של המעבד (CPU), שימוש בזיכרון והאינטראקציה של האפליקציה עם מערכת ההפעלה באופן כללי. הסיבה לכך היא שהספרייה מספקת דרך לבצע מעקב בתוך התהליך עם תקורה נמוכה.
עם זאת, קל מאוד למזג בין עקבות מערכת לבין עקבות בתהליך ולהציג אותם כעקבה אחת, אם צריך. הסיבה לכך היא ש-Perfetto UI
תומך בהצגה חזותית של כמה קובצי מעקב ממכשיר בציר זמן מאוחד.
כדי לעשות זאת, אפשר להתחיל סשן של מעקב אחר המערכת באמצעות Perfetto UI על ידי ביצוע ההוראות שמופיעות כאן.
אפשר גם להקליט אירועי מעקב בתהליך באמצעות Tracing 2.0 API, בזמן שהמעקב אחר המערכת מופעל. אחרי שיהיו לכם שני קובצי trace, תוכלו להשתמש באפשרות Open Multiple Trace Files ב-Perfetto.
איור 6. פתיחה של כמה קובצי מעקב בממשק המשתמש של Perfetto.
תהליכי עבודה מתקדמים
בקטע הזה מתוארים תהליכי עבודה מתקדמים שאפשר ליישם באמצעות ספריית המעקב בתוך התהליך.
השוואה בין רצועות
לפעמים כדאי לשייך את הפרוסות ב-trace לפעולת משתמש ברמה גבוהה יותר או לאירוע מערכת. לדוגמה, כדי לשייך את כל הפרוסות שמתאימות לעבודה ברקע כחלק מהתראה, אפשר לעשות משהו כזה:
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)
}
}
זו התוצאה שמתקבלת.
איור 7. צילום מסך של עקבות Perfetto עם פרוסות מתואמות.
הוספת מידע על call stack
כלים בצד המארח, כמו תוספים של קומפיילר ומעבדי הערות, יכולים גם להטמיע מידע על מחסנית הקריאות במעקב, כדי שיהיה נוח לאתר את הקובץ, המחלקה או השיטה שאחראים ליצירת קטע מעקב במעקב.
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)
}
}
}
זו התוצאה שמתקבלת.
איור 8. צילום מסך של עקבות Perfetto עם מידע על מחסנית הקריאות.