사용자 인터페이스(UI) 성능 테스트를 실행하면 앱이 기능 요구사항을 충족할 뿐만 아니라 사용자가 앱과 매끄럽고 원활하게 상호작용하고, 생략되거나 지연된 프레임 없이 또는 전문 용어로 버벅거림(프레임이 넘어가는 현상)이 없이 초당 60프레임의 일관된 속도로(왜 60fps일까요?) 실행되게 보장할 수 있습니다. 이 문서에서는 UI 성능을 측정하는 데 사용할 수 있는 몇 가지 도구를 설명하고, 테스트 방법에 UI 성능 측정법을 통합하는 데 쓰이는 접근법을 보여드립니다.
UI 성능 측정
성능을 개선하려면 우선 시스템의 성능을 측정한 후 파이프라인의 다양한 부분에서 발생할 수 있는 문제를 진단하고 식별할 수 있어야 합니다.
dumpsys는 기기에서 실행되는 Android 도구로, 시스템 서비스의 상태에 관한 흥미로운 정보를 덤프합니다. gfxinfo 명령어를 dumpsys에 전달하면 기록 단계 중에 발생하는 애니메이션 프레임과 관련된 성능 정보가 담긴 logcat으로 출력됩니다.
> adb shell dumpsys gfxinfo <PACKAGE_NAME>
이 명령어는 프레임 타이밍 데이터의 서로 다른 변형을 여러 개 작성할 수 있습니다.
프레임 상태 집계
Android 6.0(API 레벨 23)에서 이 명령어는 프레임 데이터의 집계된 분석을 logcat으로 출력합니다. 이는 프로세스의 전 수명을 통틀어 수집한 것입니다. 예:
Stats since: 752958278148ns Total frames rendered: 82189 Janky frames: 35335 (42.99%) 90th percentile: 34ms 95th percentile: 42ms 99th percentile: 69ms Number Missed Vsync: 4706 Number High input latency: 142 Number Slow UI thread: 17270 Number Slow bitmap uploads: 1542 Number Slow draw: 23342
이와 같은 높은 수준의 통계는 수준이 높을 때 앱의 렌더링 성능을 나타내며, 이외에도 여러 프레임에 걸친 앱의 안정성을 나타내기도 합니다.
정확한 프레임 타이밍 정보
Android 6.0에서는 gfxinfo에 사용할 수 있는 새 명령어를 도입했습니다. 바로 framestats로, 이는 최근 프레임으로부터 극히 상세한 프레임 타이밍 정보를 제공하므로 문제를 더욱 정확하게 추적하고 디버그할 수 있습니다.
>adb shell dumpsys gfxinfo <PACKAGE_NAME> framestats
이 명령어는 프레임 타이밍 정보를 나노초 타임스탬프로 출력하며, 그 출처는 앱이 생성한 마지막 120개의 프레임입니다. 다음은 adb dumpsys gfxinfo <PACKAGE_NAME> framestats의 원시 출력 예입니다.
0,27965466202353,27965466202353,27965449758000,27965461202353,27965467153286,27965471442505,27965471925682,27965474025318,27965474588547,27965474860786,27965475078599,27965479796151,27965480589068, 0,27965482993342,27965482993342,27965465835000,27965477993342,27965483807401,27965486875630,27965487288443,27965489520682,27965490184380,27965490568703,27965491408078,27965496119641,27965496619641, 0,27965499784331,27965499784331,27965481404000,27965494784331,27965500785318,27965503736099,27965504201151,27965506776568,27965507298443,27965507515005,27965508405474,27965513495318,27965514061984, 0,27965516575320,27965516575320,27965497155000,27965511575320,27965517697349,27965521276151,27965521734797,27965524350474,27965524884536,27965525160578,27965526020891,27965531371203,27965532114484,
이 출력의 각 줄은 앱이 생성한 프레임을 나타냅니다. 각 줄에는 고정된 숫자의 열이 있으며 이 열은 프레임 생성 파이프라인의 각 단계에서 소요한 시간을 나타냅니다. 다음 섹션에서는 각 열이 무엇을 나타내는지를 포함하여 이 형식을 좀 더 자세히 다뤄보겠습니다.
Framestats 데이터 형식
데이터 블록은 CSV 형식의 출력이므로 이를 개발자가 선택한 스프레드시트 도구에 붙여넣거나 스크립트를 사용하여 수집하고 파싱하는 작업을 매우 간단하게 수행할 수 있습니다. 다음 표에 출력 데이터 열의 형식에 관한 설명이 나와 있습니다. 타임스탬프는 모두 나노초 단위입니다.
- FLAGS
- FLAGS 열에 '0'이 기재되어 있는 행의 경우, 총 프레임 시간을 계산하려면 FRAME_COMPLETED 열에서 INTENDED_VSYNC 열을 빼면 됩니다.
- 0이 아닌 값이 기재되어 있으면 그 행은 무시해야 합니다. 프레임이 정상 성능의 이상값인 것으로 판별되었기 때문이며, 이 경우 레이아웃과 그리기 작업에 16ms 이상이 걸릴 것으로 예상됩니다. 다음은 이러한 상황이 발생하는 몇 가지 이유입니다.
- 창 레이아웃이 변경되었습니다(예: 회전 이후 또는 애플리케이션의 첫 프레임).
- 프레임을 건너뛰었을 가능성도 있습니다. 이 경우 몇몇 값에 가비지 타임스탬프가 있을 것입니다. 프레임을 건너뛰는 것은 그 프레임이 60fps를 초과하는 경우 또는 화상에 나타난 것 중 아무것도 변한 것이 없을 때 등이며, 이는 반드시 앱에 문제가 있음을 나타내는 징후는 아닙니다.
- INTENDED_VSYNC
- 프레임의 의도된 시작 지점입니다. 이 값이 VSYNC와 다르면 UI 스레드가 vsync 신호에 적절한 시간 내에 응답하지 못하도록 하는 작업이 UI 스레드에 발생한 것입니다.
- VSYNC
- 모든 vsync 리스너 및 프레임의 그리기에서 사용된 시간 값입니다(Choreographer 프레임 콜백, 애니메이션, View.getDrawingTime() 등).
- VSYNC와 VSYNC가 애플리케이션에 미치는 영향에 관한 자세한 내용은 VSYNC 이해 동영상을 확인하세요.
- OLDEST_INPUT_EVENT
- 입력 대기열에서 가장 오래된 입력 이벤트의 타임스탬프이거나 프레임의 입력 이벤트가 없다면 Long.MAX_VALUE입니다.
- 이 값은 기본적으로 플랫폼 작업에 사용되며 앱 개발자에게 제한적으로 유용합니다.
- NEWEST_INPUT_EVENT
- 입력 대기열에서 가장 최근 입력 이벤트의 타임스탬프이거나 프레임의 입력 이벤트가 없다면 0입니다.
- 이 값은 기본적으로 플랫폼 작업에 사용되며 앱 개발자에게 제한적으로 유용합니다.
- 그러나 (FRAME_COMPLETED - NEWEST_INPUT_EVENT)를 살펴보면 앱이 추가하고 있는 지연 시간을 대략적으로 알아볼 수 있습니다.
- HANDLE_INPUT_START
- 입력 이벤트가 애플리케이션에 전달된 시점의 타임스탬프입니다.
- 이 값과 ANIMATION_START 사이의 시간을 보면 애플리케이션이 입력 이벤트를 처리하는 데 소요한 시간을 측정할 수 있습니다.
- 이 숫자가 크면(2ms 초과) 앱이 View.onTouchEvent()와 같은 입력 이벤트를 처리하는 데 비정상적으로 오랜 시간을 소요한다는 것을 나타냅니다. 이는 이 작업을 최적화하거나 다른 스레드로 오프로드해야 함을 의미할 수 있습니다. 다른 시나리오도 몇 가지 있을 수 있다는 점을 유의하세요. 예를 들어 새 활동을 시작하는 클릭 이벤트나 이와 비슷한 이벤트의 경우, 이 숫자가 클 것으로 예상하고 이를 허용할 수 있습니다.
- ANIMATION_START
- Choreographer에 등록된 애니메이션이 실행된 시점의 타임스탬프입니다.
- 이 값과 PERFORM_TRANVERSALS_START 사이의 시간을 보면 실행 중인 애니메이터 전체(ObjectAnimator, ViewPropertyAnimator 및 Transitions가 보편적인 것들임)를 평가하는 데 시간이 얼마나 걸렸는지 판단할 수 있습니다.
- 이 숫자가 크면(2ms 초과), 앱이 맞춤 애니메이터를 작성했는지 확인합니다. 아니면 ObjectAnimators가 애니메이션 효과를 실행하는 필드가 어느 것인지 알아보고 이들이 애니메이션에 적절한지 확인해야 합니다.
- Choreographer에 관해 자세히 알아보려면 더 좋거나 더 나쁜 동영상을 참조하세요.
- PERFORM_TRAVERSALS_START
- 이 값에서 DRAW_START를 빼면 레이아웃과 측정 단계를 완료하는 데 얼마나 걸렸는지 추출할 수 있습니다. 스크롤 또는 애니메이션 중에는 이 값이 0에 가까울 것이라는 예상이 나와야 한다는 점을 유의하세요.
- 렌더링 파이프라인의 측정 및 레이아웃 단계에 관해 자세히 알아보려면 무효화, 레이아웃 및 성능 동영상을 참조하세요.
- DRAW_START
- PerformTraversals의 그리기 단계가 시작된 시점입니다. 이 시점이 무효화된 모든 뷰의 표시 목록을 기록하는 시작 지점입니다.
- 이 값과 SYNC_START 사이의 시간이 트리의 모든 무효화된 뷰에서 View.draw()를 호출하는 데 걸린 시간입니다.
- 그리기 모델에 관한 자세한 내용은 하드웨어 가속 또는 무효화, 레이아웃 및 성능 동영상을 참조하세요.
- SYNC_QUEUED
- 동기화 요청이 RenderThread에 전송된 시간입니다.
- 동기화 단계 시작 메시지가 RenderThread에 전송된 시점을 표시합니다. 이 값과 SYNC_START 사이의 시간이 상당히 크면(약 0.1ms 이상), RenderThread가 다른 프레임에서 사용되고 있다는 뜻입니다. 내부적으로 이는 너무 많은 작업을 해서 16ms 제한을 초과하는 프레임과 16ms 제한을 초과하는 이전 프레임으로 인해 지연되는 프레임을 구분하는 데 사용합니다.
- SYNC_START
- 그리기의 동기화 단계가 시작된 시점입니다.
- 이 값과 ISSUE_DRAW_COMMANDS_START 사이의 시간이 상당히 큰 값이면(약 0.4ms 초과) 일반적으로 GPU에 업로드해야 하는 새로운 비트맵이 많이 그려졌음을 나타냅니다.
- 동기화 단계에 관한 자세한 내용은 프로필 GPU 렌더링 동영상을 참조하세요.
- ISSUE_DRAW_COMMANDS_START
- 하드웨어 렌더러가 GPU에 그리기 명령어를 발행하기 시작한 시점입니다.
- 이 값과 FRAME_COMPLETED 사이의 시간을 보면 앱이 얼마나 많은 GPU 작업을 생성하고 있는지 대략적으로 알 수 있습니다. 오버드로가 너무 많거나 렌더링 효과가 비효율적인 것 등의 문제가 여기에 나타납니다.
- SWAP_BUFFERS
- eglSwapBuffers가 호출된 시점으로, 플랫폼 작업 외에는 비교적 흥미롭지 않습니다.
- FRAME_COMPLETED
- 작업이 완료되었습니다. 이 프레임에서 작업하는 데 소요된 총 시간을 계산하려면 FRAME_COMPLETED - INTENDED_VSYNC를 실행하면 됩니다.
이 데이터는 여러 가지 방법으로 사용할 수 있습니다. 한 가지 단순하면서도 유용한 시각화로 프레임 시간 분포(FRAME_COMPLETED - INTENDED_VSYNC)를 여러 가지 지연 시간 버킷에서 표시한 히스토그램을 예로 들 수 있습니다. 아래 그림을 참조합니다. 이 그래프를 보면 한눈에 대부분의 프레임이 아주 양호했다는 것을 알 수 있습니다. 대부분 16ms 최종 기한(빨간색으로 표시)보다 한참 아래에 있지만, 최종 기한을 크게 넘어선 프레임도 몇 개 있습니다. 이 히스토그램에서 시간 경과에 따른 변화를 살펴보면 대규모 이동이나 새 이상값이 생성되는 것을 확인할 수 있습니다. 이외에도 데이터 내의 수많은 타임스탬프를 근거로 입력 지연 시간, 레이아웃에서 보낸 시간 또는 여타 비슷한 흥미로운 측정항목도 그래프로 표현할 수 있습니다.

단순한 프레임 타이밍 덤프
프로필 GPU 렌더링(또는 프로필 HWUI 렌더링)이 개발자 옵션에서 adb 셸의 dumpsys gfxinfo로 설정되었다면 adb shell dumpsys gfxinfo
명령어가 가장 최근의 120개 프레임에 관한 타이밍 정보를 출력하되, 몇 개의 서로 다른 카테고리로 나누어 탭으로 구분된 값을 담아 표현합니다. 이 데이터는 그리기 파이프라인의 어떤 부분이 수준이 높을 때 느려지는지 나타내는 데 유용할 수 있습니다.
위의 framestats와 마찬가지로, 이를 개발자가 선택한 스프레드시트 도구에 붙여넣거나 스크립트를 사용하여 수집하고 파싱하는 작업을 매우 간단하게 실행할 수 있습니다. 다음 그래프는 앱이 생성한 수많은 프레임이 어디에서 시간을 보내는지 분석한 내용을 표시한 것입니다.

gfxinfo를 실행하고, 그 출력을 복사하여 스프레드시트 애플리케이션에 붙여넣은 다음 데이터를 누적 가로 막대형 그래프로 나타낸 결과입니다.
각 세로 막대는 애니메이션 한 프레임을 나타냅니다. 막대의 높이가 그 애니메이션 프레임을 계산하는 데 걸린 밀리초 수를 나타냅니다. 막대에서 색이 지정된 각 세그먼트는 렌더링 파이프라인의 각기 다른 단계를 나타내므로, 개발자는 이것을 보고 애플리케이션의 어떤 부분이 병목 현상을 유발하고 있는지 확인할 수 있습니다. 렌더링 파이프라인을 이해하고 이에 맞게 최적화하는 방법에 관한 자세한 내용은 무효화, 레이아웃 및 성능 동영상을 참조하세요.
상태 수집 창 제어
framestats와 단순한 프레임 타이밍은 양쪽 모두 렌더링 약 2초에 상당하는 시간인 아주 짧은 시간 범위 동안 데이터를 수집합니다. 이 시간 범위를 정확하게 제어하려면(예: 데이터를 특정 애니메이션에 제한), 카운터를 모두 재설정한 다음 수집한 통계를 집계하면 됩니다.
>adb shell dumpsys gfxinfo <PACKAGE_NAME> reset
이 방법은 명령어 자체를 덤프하는 방법과 함께 써서 정기적인 간격을 두고 수집과 재설정을 반복하여 계속해서 2초 미만의 프레임 창을 캡처하도록 하는 데 쓰일 수도 있습니다.
성능 저하 진단
성능이 저하된 것을 알아내는 것은 문제를 추적하고 애플리케이션의 상태를 높은 수준으로 유지 관리하는 데 좋은 첫 단계입니다. 그러나 dumpsys는 문제의 존재 여부와 상대적인 심각도를 식별하는 데에만 그칩니다. 여전히 성능 문제와 관련된 특정 원인을 진단하고 이를 해결하기 위한 적절한 방법을 찾아야 합니다. 이를 위해 systrace 도구를 사용하는 것을 적극 권장합니다.
추가 리소스
Android의 렌더링 파이프라인의 작동 원리나 여기에서 마주칠 수 있는 보편적인 문제와 그 해결 방법에 관한 자세한 내용을 원하는 경우 다음 리소스 중 몇 가지가 유용할 수 있습니다.
UI 성능 테스트 자동화
UI 성능 테스트에 관한 한 가지 관점은 그저 인간 테스터가 대상 앱에서 일련의 사용자 작업을 수행하도록 하는 것입니다. 그러면서 육안으로 이상이 있는지 살펴보든가, 아니면 오랜 시간을 들여 도구 중심적인 관점으로 이상을 찾아내는 것입니다. 하지만 이와 같은 수동식 방법은 문제가 발생하기 쉽습니다. 프레임 속도 변화를 인지하는 사람의 능력에는 개인차가 극명하고, 시간도 오래 걸릴 뿐더러 지루하고 오류가 발생할 가능성이 크기 때문입니다.
보다 효율적인 접근 방식은 자동화된 UI 테스트로부터 가져온 주요 성능 측정항목을 기록하고 분석하는 것입니다. Android 6.0에는 새로운 로깅 기능이 포함되어 있어 애플리케이션 애니메이션에 존재하는 이상의 양과 심각도를 판별하기 쉽고, 이를 사용해 현재 성능을 판단하고 앞으로의 성능 목표를 추적하기 위해 철저한 프로세스를 빌드할 수도 있습니다.
추가 리소스
이 주제에 관해 자세히 알아보려면 다음 리소스를 참조하세요.
Codelab
- 자동 성능 테스트 Codelab: 앱 성능을 개선하는 방법을 파악할 수 있도록 자동 테스트를 작성하고 실행한 후 결과를 검토하는 방법을 알려줍니다.