앱 성능 측정 개요

이 주제는 앱의 주요 성능 문제를 식별하고 해결하는 데 도움이 됩니다.

주요 성능 문제

앱의 성능 저하에 영향을 줄 수 있는 문제는 여러 가지가 있지만 앱에서 주의해야 하는 일반적인 상황은 다음과 같습니다.

  • 스크롤 버벅거림
    • '버벅거림'은 요청된 케이던스(60hz 이상)로 화면에 그려지는 시간 내에 프레임을 빌드하고 제공할 수 없는 경우 발생하는 시각적 오류를 설명하기 위해 사용되는 용어입니다. 버벅거림은 스크롤할 때, 부드러운 애니메이션 흐름으로 나타나야 하는 화면에 오류가 있을 때, 시스템의 프레임 시간에 비해 앱에서 콘텐츠를 렌더링하는 데 더 시간이 오래 걸려서 프레임 하나 이상의 움직임이 일시중지될 때 가장 두드러집니다.
    • 앱은 90Hz 새로고침 빈도를 타겟팅해야 합니다. 기존의 렌더링 속도는 60Hz였지만, 많은 최신 기기가 스크롤과 같은 사용자 상호작용 중에 90Hz 모드로 작동하며, 일부 기기는 최대 120Hz의 더 높은 속도도 지원합니다.
      • 특정 시간에 기기에서 사용 중인 새로고침 빈도를 확인하려면 디버깅 섹션에서 개발자 옵션 > 새로고침 빈도 보기를 사용하여 오버레이를 사용 설정합니다.
  • 시작 지연 시간

    • 시작 지연 시간은 앱 아이콘, 알림 또는 기타 진입점을 탭한 후 사용자의 데이터가 화면에 표시되기까지 걸리는 시간입니다.
    • 앱에서는 다음 두 가지 시작 목표를 달성해야 합니다.

      • 콜드 스타트 500ms 미만: '콜드 스타트'는 시작된 앱이 시스템의 메모리에 존재하지 않을 때 나타납니다. 재부팅하거나 사용자 또는 시스템이 앱 프로세스를 종료한 후 앱을 처음 시작할 때 발생합니다.

        반면 '웜 스타트'는 앱이 이미 백그라운드에서 실행 중일 때 발생합니다. 콜드 스타트는 모든 것을 저장용량에서 로드하고 앱을 초기화해야 하므로 시스템에서 대부분의 작업을 해야 합니다. 콜드 스타트는 500ms 이하를 목표로 합니다.

      • P95/P99 지연 시간이 중간 지연 시간에 매우 근접합니다. 앱을 시작하는 데 매우 오랜 시간이 걸리는 경우 사용자의 신뢰가 약화됩니다. 앱 시작의 중요한 경로에서 IPC와 불필요한 I/O에 잠금 경합이 일어나고 이러한 불일치가 초래될 수 있습니다.

  • 원활하지 않은 전환

    • 이러한 문제는 탭 간 전환, 새 활동 로드와 같은 상호작용 중에 발생합니다. 이러한 유형의 전환에서는 애니메이션이 원활히 이루어져야 하며 지연이나 시각적 깜박임이 없어야 합니다.
  • 전력 비효율

    • 작업을 하면 배터리가 소모되고 불필요한 작업은 배터리 수명을 줄입니다.
    • 코드에서 새 객체를 생성할 때 발생하는 메모리 할당으로 인해 시스템에서 상당한 작업이 발생할 수 있습니다. 이는 할당 그 자체에 Android 런타임의 노력이 필요할 뿐 아니라 나중에 이러한 객체('가비지 컬렉션')를 해제하는 데도 시간과 노력이 필요하기 때문입니다. 할당과 수집은 특히 임시 객체의 경우 전보다 훨씬 빠르고 효율적입니다. 따라서 예전에는 가급적 객체를 할당하지 않도록 안내했으나 현재는 앱과 아키텍처에 가장 적합한 방식을 따를 것을 권장합니다. 관리할 수 없는 코드의 위험을 감수하며 할당을 줄이는 방법은 ART의 기능을 고려했을 때 올바른 선택이 아닙니다.

      하지만 여전히 노력이 필요하므로 내부 루프에서 많은 객체를 할당할지를 염두에 두는 것이 좋으며, 이러한 문제는 성능 문제를 일으킬 수 있습니다.

문제 식별

성능 문제를 식별하고 해결하는 데 권장되는 워크플로는 다음과 같습니다.

  • 검사할 중요한 사용자 여정을 파악합니다. 여기에는 다음이 포함될 수 있습니다.
    • 런처, 알림 등의 일반적인 시작 흐름
    • 사용자가 데이터를 스크롤하는 모든 화면
    • 화면 간 전환
    • 탐색 또는 음악 재생과 같은 장기 실행 흐름
  • 디버깅 도구를 사용하여 이러한 흐름 중에 발생하는 상황을 검사합니다.
    • Systrace 또는 Perfetto: 정밀한 타이밍 데이터로 전체 기기에서 정확히 어떤 일이 일어나는지 확인할 수 있습니다.
    • 메모리 프로파일러: 힙에서 발생하는 메모리 할당을 확인할 수 있습니다.
    • Simpleperf: 특정 기간에 CPU를 가장 많이 차지하는 함수 호출의 flamegraph를 확인합니다. systrace에서 시간이 오래 걸리는 항목은 파악하지만 이유는 알 수 없는 경우 simpleperf를 통해 추가 정보를 확인할 수 있습니다.

개별 테스트 실행의 수동 디버깅은 이러한 성능 문제를 이해하고 디버깅하는 데 중요합니다. 위 단계는 합산 데이터를 분석하여 변경할 수 없습니다. 그러나 사용자가 실제로 확인하는 내용과 회귀가 발생할 수 있는 경우를 파악하기 위해서는 필드와 자동 테스팅에서 측정항목 컬렉션을 설정하는 것도 중요합니다.

  • 시작 흐름
  • 버벅거림
    • 필드 측정항목
      • Play Console 프레임 vitals: 보고된 모든 항목은 앱에서 발생한 전반적인 버벅거림이므로 Play Console 내에서는 측정항목을 특정 사용자 여정으로 좁힐 수 없습니다.
      • FrameMetricsAggregator를 사용한 맞춤 측정: 특정 워크플로 중에 FrameMetricsAggregator를 사용하여 버벅거림 측정항목을 기록할 수 있습니다.
    • 실험실 테스트
      • Jetpack Macrobenchmark: 스크롤
      • Macrobenchmark에서는 단일 사용자 여정을 괄호로 묶는 dumpsys gfxinfo 명령어를 사용하여 프레임 시간을 수집합니다. 이는 특정 사용자 여정에서 발생하는 버벅거림의 편차를 이해하는 합리적인 방법입니다. 회귀 또는 개선사항을 식별하기 위해서는 프레임을 그리는 데 걸리는 시간을 강조하는 RenderTime 측정항목이 버벅거리는 프레임 수보다 더 중요합니다.

성능 분석을 위한 앱 설정

애플리케이션에서 정확하고 반복 및 실행 가능한 벤치마크를 얻기 위해서는 적절한 설정이 필수입니다. 노이즈 소스를 억제하면서 최대한 프로덕션에 가까운 시스템에서 테스트하세요. 다음 섹션에는 테스트 설정을 준비하기 위해 따를 수 있는 여러 가지 APK 및 시스템별 단계가 나오며, 이 중 일부는 사용 사례별로 설명됩니다.

Tracepoints

애플리케이션은 맞춤 트레이스 이벤트로 코드를 계측할 수 있습니다.

트레이스를 캡처하는 동안 섹션마다 약간의 오버헤드(약 5μs)가 발생하므로 모든 메서드에 트레이스를 포함하지는 않아야 합니다. 0.1ms 이상인 대규모 작업 청크의 트레이스를 통해 병목 현상에 대한 매우 유용한 정보를 얻을 수 있습니다.

APK 고려사항

주의: 디버그 빌드에서는 성능을 측정하면 안 됩니다.

디버그 변형은 스택 샘플의 문제 해결 및 기호화에 유용할 수 있지만 성능에 심각한 비선형적 영향을 미칩니다. Android 10(API 수준 29) 이상을 실행하는 기기는 매니페스트에서 profileable android:shell="true"를 사용하여 출시 빌드에서 프로파일링을 사용 설정할 수 있습니다.

프로덕션 수준의 코드 축소 구성을 사용합니다. 이러한 구성은 애플리케이션에서 사용하는 리소스에 따라 성능에 상당한 영향을 줄 수 있습니다. 일부 ProGuard 구성은 tracepoint를 삭제하므로 테스트를 실행 중인 ProGuard 구성에서 이러한 규칙을 삭제하는 것이 좋습니다.

컴파일

기기에서 애플리케이션을 알려진 상태(일반적으로 속도 또는 속도 프로필)로 컴파일합니다. 백그라운드 JIT 활동으로 인해 상당한 성능 오버헤드가 발생할 수 있으며, 이는 테스트 실행 간에 APK를 재설치하는 경우 자주 발생합니다. 컴파일을 위한 명령어는 다음과 같습니다.

adb shell cmd package compile -m speed -f com.google.packagename

'speed' 컴파일 모드는 앱을 완전히 컴파일하며 'speed-profile' 모드는 앱 사용 중에 수집한 활용 코드 경로의 프로필에 따라 앱을 컴파일합니다. 일관되고 올바른 프로필 수집은 어려울 수 있으므로 프로필을 사용하기로 한 경우 프로필이 제대로 수집되는지 확인합니다. 프로필은 다음 위치에 있습니다.

/data/misc/profiles/ref/[package-name]/primary.prof

Macrobenchmark를 사용하면 직접 컴파일 모드를 지정할 수 있습니다.

시스템 고려사항

수준은 낮고 충실도는 높은 측정의 경우에는 기기를 보정하세요. 동일한 기기 및 동일한 OS 버전에서 A/B 비교를 실행합니다. 같은 기기 유형에서도 상당한 성능 편차가 있을 수 있습니다.

루팅된 기기에서는 마이크로 벤치마크에 lockClocks 스크립트를 사용하는 것이 좋습니다. 특히 lockClocks 스크립트는 다음을 실행합니다.

  • CPU의 빈도를 고정합니다.
  • GPU를 구성하는 작은 코어를 중지합니다.
  • 열 제한을 사용 중지합니다.

lockClocks 스크립트 사용은 사용자 환경 중심 테스트(예: 앱 실행, DoU 테스트, 버벅거림 테스트)에는 권장되지 않지만 마이크로 벤치마크 테스트에서 노이즈를 줄이는 데는 필수일 수 있습니다.

가능하면 측정 데이터의 노이즈를 줄이고 부정확한 측정을 방지할 수 있는 Macrobenchmark 같은 테스트 프레임워크를 사용해 보세요.

느린 앱 시작: 불필요한 트램펄린 활동

트램펄린 활동은 앱 시작 시간을 불필요하게 연장할 수 있으므로 앱이 트램펄린 중인지 인식하는 것이 중요합니다. 다음 트레이스 예시에서 볼 수 있듯이 첫 번째 활동에서 프레임을 그리지 않고 한 activityStart 뒤에 바로 다른 activityStart가 이어집니다.

alt_text

이는 알림 진입점과 일반 앱 시작 진입점 모두에서 발생 가능하며 주로 리팩터링으로 해결할 수 있습니다. 예를 들어 다른 활동을 실행하기 전에 특정 활동을 사용해 설정을 실행하는 경우 activityStart 코드를 재사용 가능한 구성요소 또는 라이브러리에 팩터링합니다.

GC를 자주 트리거하는 불필요한 할당

가비지 컬렉션(GC)이 systrace에서 예상보다 더 자주 발생하는 것을 발견할 수도 있습니다.

이 경우 장기 실행 작업 중 매 10초는 다음과 같이 시간이 지남에 따라 앱에서 불필요하지만 일정한 할당 작업을 지속할 가능성이 있다는 사실을 나타냅니다.

alt_text

아니면 메모리 프로파일러 사용 시 특정 호출 스택이 할당의 다수를 차지하는 현상이 나타날 수도 있습니다. 코드를 유지 관리하기가 더 어려워질 수 있으므로 모든 할당을 적극적으로 삭제할 필요는 없습니다. 대신 할당의 핫스팟을 사용하여 시작합니다.

버벅거리는 프레임

그래픽 파이프라인은 상대적으로 복잡하므로 누락된 프레임이 최종적으로 사용자에게 표시되었는지를 판단하는 데 다소 미묘한 차이가 발생하기도 합니다. 경우에 따라 플랫폼이 버퍼링을 사용하여 프레임을 '구조'할 수도 있습니다. 하지만 앱의 관점에서 볼 때 문제가 있는 프레임을 쉽게 식별하기 위해 이러한 미묘한 차이는 대부분 무시해도 됩니다.

앱에서 필요한 작업을 거의 진행하지 않고 프레임을 그릴 경우 Choreographer.doFrame() tracepoint가 16.7ms 케이던스(60FPS 기기 가정)로 발생합니다.

alt_text

트레이스를 축소하고 탐색하면 프레임이 완료되는 데 좀 더 오래 걸릴 수 있습니다. 하지만 할당된 16.7ms를 초과하지 않으므로 괜찮습니다.

alt_text

규칙적인 케이던스에 실제로 발생하는 중단이 바로 버벅거리는 프레임입니다.

alt_text

조금만 연습하면 쉽게 확인할 수 있습니다.

alt_text

경우에 따라 확장되는 뷰 또는 RecyclerView의 역할에 관해 자세히 확인하기 위해 tracepoint를 확대해야 할 수 있습니다. 다른 경우에는 추가 검사가 필요하기도 합니다.

버벅거리는 프레임을 식별하고 원인을 디버깅하는 방법에 관한 자세한 내용은 느린 렌더링을 참고하세요.

흔히 발생하는 RecyclerView 실수

  • 전체 RecyclerView의 백업 데이터를 불필요하게 무효화합니다. 이로 인해 프레임 렌더링 시간이 길어지고 버벅거림이 발생할 수 있습니다. 대신 변경된 데이터만 무효하여 업데이트해야 하는 뷰의 수를 최소화합니다.
    • 전체적으로 바꾸는 대신 콘텐츠 업데이트를 야기하여 비용이 많이 드는 notifyDatasetChanged() 호출을 방지하는 방법은 동적 데이터 표시를 참고하세요.
  • 중첩된 RecyclerView를 제대로 지원하지 않아 내부 RecyclerView가 매번 완전히 다시 생성됩니다.
    • 중첩된 모든 내부 RecyclerView에는 내부 RecyclerView 사이에 뷰를 재활용할 수 있도록 설정한 RecycledViewPool이 있어야 합니다.
  • 충분한 데이터를 미리 가져오거나 적시에 가져오지 않습니다. 스크롤 목록 하단으로 빠르게 내려가는 동작은 부자연스럽게 보일 수 있으며 서버에서 더 많은 데이터가 수신될 때까지 기다려야 할 수 있습니다. 프레임 기한을 놓치지 않았으므로 이러한 현상은 엄밀히 말해 '버벅거림'은 아니며, 사용자가 데이터를 기다릴 필요가 없도록 미리 가져오는 데이터의 분량과 타이밍을 수정하면 UX가 크게 개선될 수 있습니다.