처리 중 추적 (실험용)

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

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

종속 항목

트레이싱을 시작하려면 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")
        // ...
      }
    }
  }
}

Android 라이브러리나 애플리케이션을 타겟팅하는 경우 androidx.tracing:tracing-wire-android:2.0.0-alpha01에 대한 종속 항목을 선언합니다. JVM을 타겟팅하는 경우 androidx.tracing:tracing-wire-desktop:2.0.0-alpha01 종속 항목을 사용할 수 있습니다.

기본 사용법

TraceSink는 트레이스 패킷이 직렬화되는 방식을 정의합니다. 추적 2.0.0에는 Perfetto 추적 패킷 형식을 사용하는 싱크 구현이 함께 제공됩니다. TraceDriverTracer에 대한 핸들을 제공하며 트레이스를 종료하는 데 사용할 수 있습니다.

일부 애플리케이션 변형에서 전혀 추적하지 않으려는 경우 TraceDriver를 사용하여 애플리케이션의 모든 추적 포인트를 사용 중지할 수도 있습니다. TraceDriver의 향후 API를 사용하면 개발자가 캡처하려는 추적 카테고리 (또는 카테고리가 노이즈가 많은 경우 사용 중지)를 제어할 수도 있습니다.

시작하려면 TraceSinkTraceDriver 인스턴스를 만드세요.

/**
 * 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
}

TraceDriver 인스턴스가 있으면 모든 추적 API의 진입점을 정의하는 Tracer를 가져옵니다.

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

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

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

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

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

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

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

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

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

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

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

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

자세한 내용을 확인하기 위해 추가 컨텍스트 메타데이터를 트레이스 슬라이스에 첨부하는 것이 유용한 경우도 있습니다. 이러한 메타데이터의 예로는 사용자가 사용 중인 nav destination 또는 함수의 소요 시간을 결정할 수 있는 input arguments이 있습니다.

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

그러면 다음과 같은 결과가 생성됩니다. Arguments 섹션에는 slice를 생성할 때 추가된 키-값 쌍이 포함됩니다.

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

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

컨텍스트 전파

Kotlin 코루틴 (또는 동시 워크로드를 지원하는 기타 유사 프레임워크)을 사용하는 경우 추적 2.0은 컨텍스트 전파 개념을 지원합니다. 예를 통해 설명하는 것이 가장 좋습니다.

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

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

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

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

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

예를 들어 슬라이스 maintaskOnetaskTwo를 생성한 것을 확인할 수 있습니다. 그 후 두 스레드 모두 비활성 상태였습니다 (delay 사용으로 인해 코루틴이 일시 중단되었기 때문).

수동 전파

Kotlin 코루틴을 사용하는 동시 워크로드를 Java Executor 인스턴스와 혼합할 때 한쪽에서 다른 쪽으로 컨텍스트를 전파하는 것이 유용할 수 있습니다. 예를 들면 다음과 같습니다.

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

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

수동 컨텍스트 전파가 있는 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()
    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)
        }
    }
}

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

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

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

호출 스택 정보 추가

호스트 측 도구 (컴파일러 플러그인, 주석 프로세서 등)는 추적에서 추적 섹션을 생성하는 파일을 찾거나 클래스 또는 메서드를 쉽게 찾을 수 있도록 호출 스택 정보를 추적에 삽입할 수도 있습니다.

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

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

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

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