Biblioteka androidx.tracing:tracing:2.0.0-beta01 to lekki
interfejs Kotlin API, który umożliwia rejestrowanie zdarzeń śledzenia w procesie. Te zdarzenia mogą rejestrować przedziały czasu i ich kontekst. Biblioteka obsługuje też propagację kontekstu w przypadku korutyn Kotlin.
Biblioteka używa tego samego Perfetto formatu pakietów śledzenia, który jest znany deweloperom Androida. Ponadto śledzenie 2.0 (w przeciwieństwie do interfejsów API 1.0.0-*)
obsługuje koncepcję wtykowych backendów śledzenia i ujść, dzięki czemu inne
biblioteki śledzenia mogą dostosowywać format śledzenia wyjściowego oraz sposób działania propagacji kontekstu w swojej implementacji.
Zależności
Aby rozpocząć śledzenie, musisz zdefiniować zależności w pliku build.gradle.kts.
Projekty Kotlin Multiplatform
Biblioteki, które muszą tylko emitować zdarzenia śledzenia, powinny zależeć od lekkiego interfejsu androidx.tracing:tracing API. Aplikacje, które konfigurują backend śledzenia, powinny też zależeć od 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")
}
}
}
}
Projekty tylko na Androida
Jeśli kierujesz reklamy tylko na Androida, dodaj ten kod do pliku build.gradle.kts aplikacji lub biblioteki:
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")
}
Inicjowanie i wykrywanie
Zanim zaczniesz rejestrować zdarzenia śledzenia, musisz zainicjować infrastrukturę śledzenia. Obejmuje to utworzenie AbstractTraceDriver i zarejestrowanie jego Tracer globalnie.
Android
Jeśli na Androidzie uwzględnisz zależność androidx.tracing:tracing-wire, inicjowanie nastąpi automatycznie podczas uruchamiania aplikacji za pomocą biblioteki androidx.startup.
Domyślnie to automatyczne inicjowanie wykonuje te czynności:
Tworzy
TraceDriverzTraceSink, który zapisuje pliki śledzenia Perfetto wContext.noBackupFilesDir/perfetto_traces/.Rejestruje wynikowy
Tracerglobalnie.
Dostosowywanie instancji TraceDriver
Jeśli musisz dostosować konfigurację, np. zmienić miejsce zapisywania plików śledzenia lub użyć niestandardowego TraceSink, możesz podać własną instancję AbstractTraceDriver.
Aby dostosować konfigurację, spraw, aby klasa Application implementowała 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)
}
}
Automatyczny inicjator wykrywa, że podklasa Application implementuje Factory, i używa Twojej niestandardowej fabryki sterowników.
JVM
W JVM nie ma automatycznego mechanizmu bootstrapowania. Aplikacja jest odpowiedzialna za zainicjowanie TraceDriver i zarejestrowanie Tracer globalnie podczas uruchamiania, najczęściej w funkcji main.
Aby zarejestrować śledzenie, wywołaj 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()
})
}
Podstawowe użycie
TraceSink określa sposób serializacji pakietów śledzenia. Śledzenie 2.0.0 zawiera implementację ujścia, która używa formatu pakietów śledzenia Perfetto. TraceDriver udostępnia uchwyt do Tracer i może służyć do finalizowania śledzenia.
Gdy Tracer zostanie zainicjowany (automatycznie na Androidzie lub ręcznie w
JVM), użyj globalnej instancji Tracer.global, aby emitować zdarzenia śledzenia.
Możesz też użyć TraceDriver, aby wyłączyć wszystkie punkty śledzenia w aplikacji, jeśli w niektórych wariantach aplikacji nie chcesz w ogóle śledzić. Opcjonalnie możesz włączyć punkty śledzenia dla danej category, podając
implementację dla isCategoryEnabled podczas tworzenia instancji
TraceDriver.
val driver = TraceDriver(
sink = sink,
isCategoryEnabled = { category ->
// Only enable trace points in the "com.example" package
category.startsWith("com.example")
}
)
Oto podstawowy przykład emitowania zdarzenia śledzenia za pomocą Tracer.global w JVM, w tym konfiguracja ręczna:
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)
}
}
}
Spowoduje to wygenerowanie tego śledzenia.
Rysunek 1. Zrzut ekranu podstawowego śledzenia Perfetto.
Widać, że wypełnione są prawidłowe ścieżki procesu i wątku oraz że utworzyły one jedną sekcję śledzenia basic, która trwała 100 ms.
Sekcje śledzenia (lub paski) można zagnieżdżać na tej samej ścieżce, aby reprezentować nakładające się zdarzenia. Oto przykład.
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") {
// ...
}
}
Spowoduje to wygenerowanie tego śledzenia.
Rysunek 2. Zrzut ekranu podstawowego śledzenia Perfetto z zagnieżdżonymi sekcjami.
Widać, że na ścieżce głównego wątku występują nakładające się zdarzenia. Wyraźnie widać, że processImage wywołuje loadImage i sharpen w tym samym wątku.
Dodawanie dodatkowych metadanych w sekcjach śledzenia
Czasami warto dołączyć do paska śledzenia dodatkowe metadane kontekstowe, aby uzyskać więcej szczegółów. Przykłady takich metadanych to
nav destination w którym znajduje się użytkownik, lub input arguments które mogą ostatecznie
określić czas trwania funkcji.
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)
}
}
}
Spowoduje to wygenerowanie tego wyniku. Zwróć uwagę, że sekcja Arguments zawiera pary klucz
wartość dodane podczas tworzenia slice.
Rysunek 3. Zrzut ekranu podstawowego śledzenia Perfetto z dodatkowymi metadanymi.
Propagacja kontekstu
Podczas korzystania z korutyn Kotlin lub innych podobnych frameworków, które ułatwiają obsługę równoczesnych zadań, śledzenie 2.0 obsługuje koncepcję propagacji kontekstu. Najlepiej wyjaśnić to na przykładzie.
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")
}
}
Spowoduje to wygenerowanie tego wyniku.
Rysunek 4. Zrzut ekranu podstawowego śledzenia Perfetto z propagacją kontekstu.
Propagacja kontekstu znacznie ułatwia wizualizację przepływu wykonywania. Możesz dokładnie zobaczyć, które zadania były powiązane (połączone z innymi),
oraz kiedy dokładnie Threads zostały zawieszone i wznowione.
Możesz na przykład zobaczyć, że pasek main utworzył taskOne i taskTwo.
Następnie oba wątki były nieaktywne, ponieważ korutyny zostały zawieszone z powodu użycia delay.
Propagacja ręczna
Czasami, gdy łączysz równoczesne zadania za pomocą korutyn Kotlin z instancjami Java Executor, może się przydać propagacja kontekstu z jednego do drugiego. Oto przykład:
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()
}
}
Spowoduje to wygenerowanie tego wyniku.
Rysunek 5. Zrzut ekranu podstawowego śledzenia Perfetto z ręczną propagacją kontekstu.
Widać, że wykonywanie rozpoczęło się w CoroutineContext, a następnie przełączyło się na Java Executor, ale nadal mogliśmy używać propagacji kontekstu.
Łączenie ze śledzeniem systemu
Biblioteka androidx.tracing nie rejestruje informacji takich jak planowanie procesora, wykorzystanie pamięci i ogólna interakcja aplikacji z systemem operacyjnym. Dzieje się tak, ponieważ biblioteka umożliwia śledzenie w procesie z niskim narzutem.
W razie potrzeby można jednak bardzo łatwo połączyć śledzenie systemu ze śledzeniem w procesie i wizualizować je jako jedno śledzenie. Dzieje się tak, ponieważ Perfetto UI obsługuje wizualizację wielu plików śledzenia z urządzenia na ujednoliconej osi czasu.
Aby to zrobić, możesz rozpocząć sesję śledzenia systemu za pomocą Perfetto UI przez
postępowanie zgodnie z instrukcjami tutaj.
Możesz też rejestrować zdarzenia śledzenia w procesie za pomocą interfejsu Tracing 2.0 API, gdy śledzenie systemu jest włączone. Gdy masz oba pliki śledzenia, możesz użyć opcji Open Multiple Trace Files w Perfetto.
Rysunek 6. Otwieranie wielu plików śledzenia w Perfetto UI.
Zaawansowane przepływy pracy
W tej sekcji opisujemy zaawansowane przepływy pracy, które możesz zaimplementować za pomocą biblioteki śledzenia w procesie.
Korelacja pasków
Czasami warto przypisać paski w śledzeniu do działania użytkownika wyższego poziomu lub zdarzenia systemowego. Aby na przykład przypisać wszystkie paski odpowiadające pracy w tle w ramach powiadomienia, możesz zrobić coś takiego:
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)
}
}
Spowoduje to wygenerowanie tego wyniku.
Rysunek 7. Zrzut ekranu śledzenia Perfetto z powiązanymi paskami.
Dodawanie informacji o stosie wywołań
Narzędzia po stronie hosta, takie jak wtyczki kompilatora i procesory adnotacji, mogą też osadzać informacje o stosie wywołań w śledzeniu, aby ułatwić znajdowanie pliku, klasy lub metody odpowiedzialnej za utworzenie sekcji śledzenia.
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)
}
}
}
Spowoduje to wygenerowanie tego wyniku.
Rysunek 8. Zrzut ekranu śledzenia Perfetto z informacjami o stosie wywołań.