안드로이드에서 UI 버벅임(Jank) 추적하기
안드로이드에서 UI 버벅임(Jank) 추적하기
UI 버벅임(jank)은 메인 스레드가 한 프레임의 데드라인 내에 작업을 완료하지 못할 때 발생하며, 사용자에게 눈에 보이는 끊김이나 프레임 드롭으로 나타납니다. 최신 안드로이드 기기에서 60Hz, 90Hz, 120Hz 등 다양한 주사율을 지원하기 때문에, 한 프레임에 허용되는 시간은 최소 약 8밀리초까지 줄어들 수 있습니다. 버벅임을 정확히 진단하려면 Choreographer 콜백부터 Compose의 세 가지 페이즈(phase), 최종 Surface 제출(submission)에 이르기까지 전체 렌더링 파이프라인을 이해해야 하며, 어떤 프로파일링 도구가 어떤 병목 지점을 드러내는지 파악해야 합니다. 면접에서도 성능 최적화 관련 질문으로 자주 등장하는 주제이므로, 전반적인 원리와 도구 사용법을 반드시 숙지하시길 권장합니다.
이번 면접 질문을 통하여 아래 내용들을 학습하실 수 있습니다.
- 안드로이드 렌더링 파이프라인이
Choreographer와 VSYNC 신호를 통해 프레임 작업을 스케줄링하는 방식을 설명할 수 있습니다. - Perfetto에서 Frames 트랙,
doFrame슬라이스, Compose 페이즈 마커를 읽어 버벅이는 프레임을 추적할 수 있습니다. - CPU Profiler의 플레임 차트(flame chart)가 메인 스레드에서 비용이 큰 메서드 호출을 어떻게 격리하는지 설명할 수 있습니다.
- Layout Inspector와 Compose Compiler Metrics를 사용하여 불필요한 리컴포지션을 식별할 수 있습니다.
- 커스텀 트레이스 구간과 벤치마킹 전략을 적용하여 프로덕션 코드에서 버벅임을 측정하고 해결할 수 있습니다.
안드로이드 프레임 파이프라인과 Choreographer
안드로이드에서 모든 프레임은 디스플레이 서브시스템이 전달하는 VSYNC 신호로부터 시작됩니다. Choreographer 클래스가 이 신호를 수신하여 정해진 순서로 작업을 디스패치합니다. 입력 처리, 애니메이션 콜백, 순회(measure, layout, draw), 커밋 순서로 진행됩니다. Compose에서는 AndroidComposeView가 이 메커니즘에 자체적인 페이즈 순서를 연결하여 동작합니다.
Recomposer는 parentFrameClock.withFrameNanos를 통해 Choreographer 프레임 클럭과 컴포지션을 동기화합니다. 이 함수는 다음 VSYNC에 맞춰진 콜백이 올 때까지 일시 중단(suspend)되며, 콜백 내부에서 리컴포지션을 수행하고 변경 사항을 노드 트리에 적용합니다. 컴포지션이 완료되면 뷰 시스템이 AndroidComposeView.dispatchDraw()를 호출하고, 내부적으로 measureAndLayout()을 통한 레이아웃과 root.draw()를 통한 드로잉이 순차적으로 실행됩니다.
// AndroidComposeView 내부
override fun dispatchDraw(canvas: android.graphics.Canvas) {
measureAndLayout()
Snapshot.notifyObjectsInitialized()
isDrawingContent = true
canvasHolder.drawInto(canvas) {
root.draw(canvas = this, graphicsLayer = null)
}
}
VSYNC 시점부터 버퍼 제출(buffer submission)까지의 총 소요 시간이 프레임 데드라인을 초과하면 해당 프레임은 버벅임(janky)으로 판정됩니다. 60Hz에서는 약 16.6ms, 90Hz에서는 약 11.1ms, 120Hz에서는 약 8.3ms가 기준입니다. 리컴포지션, 레이아웃, 드로잉 어느 단계에서든 초과가 발생할 수 있으며, 프레임 콜백과 무관한 메인 스레드 작업이 시간을 소모하여 프레임 콜백이 실행되지 못하는 경우에도 동일하게 버벅임이 발생합니다.
Perfetto를 활용한 시스템 트레이싱
버벅임을 진단하는 핵심 도구는 시스템 트레이싱(system tracing)입니다. Android Studio의 Profiler 또는 기기 내 '시스템 트레이싱' 개발자 옵션을 통해 캡처하고, Perfetto UI에서 분석합니다. Perfetto UI는 모든 스레드 활동을 마이크로초 단위의 타임라인으로 보여 줍니다.
분석의 시작점은 애플리케이션 프로세스의 Frames 트랙입니다. 녹색 프레임은 데드라인을 충족한 것이고, 노란색 프레임은 근소하게 충족한 것이며, 빨간색 프레임은 데드라인을 초과한 것입니다. Frames 트랙 아래쪽에는 메인 스레드의 Choreographer doFrame 슬라이스가 프레임별 수행 작업을 보여 줍니다. Compose 애플리케이션에서는 런타임이 doFrame 내부에 Recompose, Layout, Draw 트레이스 마커를 중첩하여 기록합니다. 이 슬라이스들의 상대적 길이를 통해 병목이 어느 페이즈에 있는지 즉시 좁혀 나갈 수 있습니다.
트레이스에서는 RenderThread도 확인할 수 있습니다. RenderThread는 메인 스레드가 드로잉을 완료한 후 GPU 커맨드 제출을 처리합니다. 드로잉 페이즈에서 과도한 디스플레이 리스트 연산이나 대용량 비트맵을 생성하면, 메인 스레드가 제시간에 완료되더라도 RenderThread가 병목이 될 수 있습니다.
더 심층적인 조사를 위해 androidx.tracing 라이브러리를 사용하여 커스텀 트레이스 구간을 추가할 수 있습니다.
import androidx.tracing.trace
@Composable
fun MessageList(messages: List<Message>) {
trace("MessageList-Composition") {
LazyColumn {
items(messages, key = { it.id }) { message ->
trace("MessageBubble-${message.id}") {
MessageBubble(message)
}
}
}
}
}