인프로세스 추적

androidx.tracing:tracing:2.0.0-beta01 라이브러리는 프로세스 내 트레이스 이벤트를 캡처할 수 있는 오버헤드가 낮은 Kotlin API입니다. 이러한 이벤트는 시간 슬라이스와 컨텍스트를 캡처할 수 있습니다. 이 라이브러리는 Kotlin 코루틴의 컨텍스트 전파도 지원합니다.

이 라이브러리는 Android 개발자에게 익숙한 동일한 Perfetto 추적 패킷 형식을 사용합니다. 또한 추적 2.01.0.0-* API와 달리 플러그형 추적 백엔드싱크의 개념을 지원하므로 다른 추적 라이브러리는 출력 추적 형식과 컨텍스트 전파가 구현에서 작동하는 방식을 맞춤설정할 수 있습니다.

종속 항목

추적을 시작하려면 build.gradle.kts에서 종속 항목을 정의해야 합니다.

Kotlin 멀티플랫폼 프로젝트

추적 이벤트만 내보내야 하는 라이브러리는 경량 androidx.tracing:tracing API에 종속되어야 합니다. 추적 백엔드를 구성하는 앱은 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 전용 프로젝트

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 라이브러리를 사용하여 애플리케이션 시작 시 초기화가 자동으로 이루어집니다.

기본적으로 이 자동 초기화는 다음을 실행합니다.

  • Context.noBackupFilesDir/perfetto_traces/에 Perfetto 추적 파일을 쓰는 TraceSink가 있는 TraceDriver를 만듭니다.

  • 결과 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 하위 클래스가 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는 트레이스 패킷이 직렬화되는 방식을 정의합니다. 추적 2.0.0에는 Perfetto 추적 패킷 형식을 사용하는 싱크 구현이 함께 제공됩니다. TraceDriverTracer 핸들을 제공하며 트레이스를 종료하는 데 사용할 수 있습니다.

Tracer가 Android에서 자동으로 또는 JVM에서 수동으로 초기화되면 전역 Tracer.global 인스턴스를 사용하여 트레이스 이벤트를 내보냅니다.

일부 애플리케이션 변형에서 전혀 추적하지 않으려는 경우 TraceDriver를 사용하여 애플리케이션의 모든 추적 포인트를 사용 중지할 수도 있습니다. TraceDriver의 인스턴스를 만들 때 isCategoryEnabled의 구현을 제공하여 특정 category의 추적 포인트를 선택적으로 사용 설정할 수 있습니다.

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

다음은 수동 설정을 포함하여 JVM에서 Tracer.global를 사용하여 추적 이벤트를 내보내는 기본 예입니다.

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

그러면 다음과 같은 트레이스가 생성됩니다.

기본 Perfetto 트레이스의 화면 캡처

그림 1. 기본 Perfetto 트레이스의 화면 캡처

올바른 프로세스 및 스레드 트랙이 채워져 있고 100ms 동안 실행된 단일 트레이스 섹션 basic가 생성되었음을 확인할 수 있습니다.

추적 섹션 (또는 슬라이스)은 중복되는 이벤트를 나타내기 위해 동일한 트랙에 중첩될 수 있습니다. 예를 들면 다음과 같습니다.

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

그러면 다음과 같은 트레이스가 생성됩니다.

중첩된 섹션이 있는 기본 Perfetto 트레이스의 화면 캡처

그림 2. 중첩된 섹션이 있는 기본 Perfetto 트레이스의 화면 캡처

기본 스레드 트랙에 중복된 이벤트가 있습니다. processImage가 동일한 스레드에서 loadImagesharpen을 호출하는 것이 매우 명확합니다.

트레이스 섹션에 추가 메타데이터 추가

자세한 내용을 확인하기 위해 추적 슬라이스에 추가 컨텍스트 메타데이터를 첨부하는 것이 유용한 경우도 있습니다. 이러한 메타데이터의 예로는 사용자가 속한 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를 생성할 때 추가된 키-값 쌍이 포함됩니다.

추가 메타데이터가 포함된 기본 Perfetto 트레이스의 화면 캡처

그림 3. 추가 메타데이터가 포함된 기본 Perfetto 트레이스의 화면 캡처

컨텍스트 전파

동시 워크로드를 지원하는 Kotlin 코루틴이나 기타 유사한 프레임워크를 사용하는 경우 추적 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")
    }
}

그러면 다음과 같은 결과가 생성됩니다.

컨텍스트 전파가 포함된 Perfetto 트레이스의 화면 캡처

그림 4. 컨텍스트 전파가 포함된 기본 Perfetto 트레이스의 화면 캡처

컨텍스트 전파를 사용하면 실행 흐름을 시각화하는 것이 훨씬 간단해집니다. 어떤 작업이 관련되어 있는지 (다른 작업과 연결되어 있는지)와 Threads일시중지되고 다시 시작된 시점을 정확하게 확인할 수 있습니다.

예를 들어 슬라이스 maintaskOnetaskTwo를 생성한 것을 확인할 수 있습니다. 그 후 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()
    }
}

그러면 다음과 같은 결과가 생성됩니다.

수동 컨텍스트 전파가 있는 Perfetto 트레이스의 화면 캡처

그림 5. 수동 컨텍스트 전파가 있는 기본 Perfetto 트레이스의 화면 캡처

실행이 CoroutineContext에서 시작된 후 Java Executor로 전환되었지만 컨텍스트 전파를 계속 사용할 수 있습니다.

시스템 트레이스와 결합

androidx.tracing 라이브러리는 CPU 스케줄링, 메모리 사용량, 애플리케이션과 운영체제의 일반적인 상호작용과 같은 정보를 캡처하지 않습니다. 라이브러리가 오버헤드가 낮은 인프로세스 추적을 실행하는 방법을 제공하기 때문입니다.

하지만 필요한 경우 시스템 트레이스를 인프로세스 트레이스와 병합하고 단일 트레이스로 시각화하는 것은 매우 간단합니다. 이는 Perfetto UI가 통합 타임라인에서 기기의 여러 트레이스 파일을 시각화하는 것을 지원하기 때문입니다.

이렇게 하려면 여기의 안내에 따라 Perfetto UI를 사용하여 시스템 추적 세션을 시작하면 됩니다.

시스템 트레이싱이 사용 설정된 동안 Tracing 2.0 API를 사용하여 인프로세스 트레이스 이벤트를 기록할 수도 있습니다. 두 개의 트레이스 파일이 있으면 Perfetto에서 Open Multiple Trace Files 옵션을 사용할 수 있습니다.

Perfetto UI에서 여러 트레이스 파일 열기

그림 6. Perfetto UI에서 여러 트레이스 파일을 엽니다.

고급 워크플로

이 섹션에서는 인프로세스 추적 라이브러리로 구현할 수 있는 고급 워크플로를 설명합니다.

슬라이드 상관관계 지정

트레이스의 슬라이스를 더 높은 수준의 사용자 작업이나 시스템 이벤트에 기여하는 것이 유용한 경우가 있습니다. 예를 들어 알림의 일부로 일부 백그라운드 작업에 해당하는 모든 슬라이스를 속성으로 지정하려면 다음과 같이 하면 됩니다.

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

그러면 다음과 같은 결과가 생성됩니다.

상관관계가 있는 슬라이스가 있는 Perfetto 트레이스의 화면 캡처

그림 7. 상관관계가 있는 슬라이스가 있는 Perfetto 트레이스의 화면 캡처

호출 스택 정보 추가

컴파일러 플러그인, 주석 프로세서와 같은 호스트 측 도구는 추적에서 추적 섹션을 생성하는 파일을 편리하게 찾을 수 있도록 호출 스택 정보를 추적에 삽입할 수도 있습니다.

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

그러면 다음과 같은 결과가 생성됩니다.

호출 스택 정보가 포함된 Perfetto 트레이스의 화면 캡처

그림 8. 호출 스택 정보가 포함된 Perfetto 트레이스의 화면 캡처