앱 성능 측정 개요

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

주요 성능 문제

앱의 성능 저하에 영향을 줄 수 있는 문제는 다양하지만 앱에서 찾을 수 있는 일반적인 문제는 다음과 같습니다.

시작 지연 시간

시작 지연 시간은 앱 아이콘, 알림 또는 기타 진입점을 탭한 후 사용자의 데이터가 화면에 표시되기까지 걸리는 시간입니다.

앱에서 다음과 같은 시작 목표를 세우세요.

  • 500ms 미만의 콜드 스타트. 콜드 스타트는 실행되는 앱이 시스템의 메모리에 없을 때 발생합니다. 재부팅하거나 사용자 또는 시스템이 앱 프로세스를 중지한 후 앱을 처음 시작할 때 발생합니다.

    반면에 웜 스타트는 앱이 이미 백그라운드에서 실행 중일 때 발생합니다. 콜드 스타트는 저장소에서 모든 항목을 로드하고 앱을 초기화해야 하므로 시스템에서 가장 많은 작업을 해야 합니다. 콜드 스타트가 500ms 이하가 되도록 해 보세요.

  • P95 및 P99 지연 시간은 지연 시간 중앙값에 매우 가깝습니다. 앱을 시작하는 데 시간이 오래 걸리면 사용자 환경이 저하됩니다. 앱 시작의 중요한 경로에서 프로세스 간 통신(IPC) 및 불필요한 I/O로 인해 잠금 경합이 발생하고 불일치가 초래될 수 있습니다.

스크롤 버벅거림

버벅거림은 시스템이 요청된 60hz 이상 주기로 화면에 그려지도록 제시간에 프레임을 빌드하고 제공할 수 없을 때 발생하는 시각적 문제를 설명하는 용어입니다. 버벅거림은 스크롤 시 부드럽게 이어지는 애니메이션된 흐름 대신 끊어지는 느낌이 있을 때 가장 두드러집니다. 앱이 시스템의 프레임 지속 시간보다 콘텐츠를 렌더링하는 데 더 오래 걸리기 때문에 하나 이상의 프레임에서 움직임이 일시중지되면 버벅거림이 나타납니다.

앱은 90Hz 새로고침 빈도를 타겟팅해야 합니다. 기존 렌더링 속도는 60Hz이지만, 대부분의 최신 기기는 스크롤과 같은 사용자 상호작용 중에 90Hz 모드로 작동합니다. 일부 기기는 최대 120Hz의 더 높은 속도를 지원합니다.

특정 시간에 기기에서 사용 중인 새로고침 빈도를 확인하려면 디버깅 섹션에서 개발자 옵션 > 새로고침 빈도 보기를 사용하여 오버레이를 사용 설정합니다.

전환이 원활하지 않음

이는 탭 간 전환이나 새 활동 로드와 같은 상호작용 중에 두드러집니다. 이러한 유형의 전환은 매끄러운 애니메이션이어야 하며 지연이나 시각적 깜박임이 없어야 합니다.

전력 비효율

작업을 하면 배터리 충전량이 줄고 불필요한 작업을 하면 배터리 수명이 줄어듭니다.

코드에서 새 객체를 생성할 때 발생하는 메모리 할당으로 인해 시스템에서 상당한 작업이 발생할 수 있습니다. 할당 자체에 Android 런타임(ART)의 작업이 필요할 뿐만 아니라 나중에 이러한 객체를 해제(가비지 컬렉션)하는 데도 시간과 노력이 필요하기 때문입니다. 할당과 수집은 특히 임시 객체의 경우 훨씬 빠르고 효율적입니다. 전에는 가능하면 객체를 할당하지 않는 것이 좋았지만, 이제는 앱과 아키텍처에 가장 적합한 방법을 사용하는 것이 좋습니다. ART의 기능을 고려할 때 유지보수할 수 없는 코드의 위험을 감수하며 할당을 줄이는 방법은 좋지 않습니다.

그러나 노력이 필요하므로 내부 루프에서 많은 객체를 할당할 경우 성능 문제가 발생할 수 있다는 점에 유의하세요.

문제 파악

성능 문제를 식별하고 해결하려면 다음 워크플로를 따르는 것이 좋습니다.

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

이러한 성능 문제를 이해하고 디버그하려면 개별 테스트 실행을 수동으로 디버그하는 것이 중요합니다. 집계된 데이터를 분석하여 이전 단계를 대체할 수는 없습니다. 하지만 사용자에게 실제로 표시되는 내용을 이해하고 회귀가 발생할 수 있는 시점을 식별하려면 자동 테스트 및 필드에서 측정항목 수집을 설정하는 것이 중요합니다.

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

앱 링크는 웹사이트에 속하는 것으로 확인된 웹사이트 URL을 기반으로 하는 딥 링크입니다. 다음은 앱 링크 인증에 실패할 수 있는 이유입니다.

  • 인텐트 필터 범위: 앱이 응답할 수 있는 URL의 인텐트 필터에만 autoVerify를 추가합니다.
  • 확인되지 않은 프로토콜 전환: 확인되지 않은 서버 측 및 하위 도메인 리디렉션은 보안 위험으로 간주되며 인증에 실패합니다. 이로 인해 모든 autoVerify 링크가 실패합니다. 예를 들어 HTTPS 링크를 확인하지 않고 HTTP 링크를 HTTPS 링크로 리디렉션하면(예: example.com에서 www.example.com으로) 인증에 실패할 수 있습니다. 인텐트 필터를 추가하여 앱 링크를 확인해야 합니다.
  • 확인할 수 없는 링크: 테스트 목적으로 확인할 수 없는 링크를 추가하면 시스템에서 앱의 앱 링크를 확인하지 않을 수 있습니다.
  • 안정적이지 않은 서버: 서버가 클라이언트 앱에 연결할 수 있는지 확인합니다.

성능 분석을 위한 앱 설정

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

Tracepoints

앱은 맞춤 트레이스 이벤트로 코드를 계측할 수 있습니다.

트레이스를 캡처하는 동안 섹션마다 약간의 오버헤드(약 5μs)가 발생하므로 모든 메서드에 트레이스를 포함하지는 않아야 합니다. 0.1ms를 초과하는 큰 작업 청크를 추적하면 병목 현상에 관한 중요한 정보를 얻을 수 있습니다.

APK 고려사항

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

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

컴파일

기기 내에서 앱을 알려진 상태(일반적으로 단순성을 위해 speed 또는 프로덕션 성능에 더 근접하게 맞추기 위해 speed-profile)로 컴파일합니다. 단, 이렇게 하려면 애플리케이션을 워밍업하고 프로필을 덤프하거나 앱의 기준 프로필을 컴파일해야 합니다.

speedspeed-profile는 모두 dex에서 해석된 실행 코드의 양을 줄여 상당한 간섭을 일으킬 수 있는 백그라운드 JIT(just-in-time) 컴파일의 양을 줄입니다. speed-profile만 Dex에서 런타임 클래스 로드의 영향을 줄입니다.

다음 명령어는 speed 모드를 사용하여 애플리케이션을 컴파일합니다.

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

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

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

시스템 고려사항

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

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

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

앱 실행, DoU 테스트, 버벅거림 테스트와 같은 사용자 환경 중심 테스트에는 lockClocks 스크립트를 사용하지 않는 것이 좋습니다. 하지만 Microbenchmark 테스트에서 노이즈를 줄이는 데는 필수적일 수 있습니다.

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

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

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

alt_text그림 1. 트램펄린 활동을 보여주는 트레이스

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

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

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

다음 예에서 장기 실행 작업 중 10초는 앱이 불필요하지만 시간이 지남에 따라 일관되게 할당할 수 있음을 나타냅니다.

alt_text그림 2. GC 이벤트 사이의 공간을 보여주는 트레이스

메모리 프로파일러를 사용하면 특정 호출 스택이 대부분의 할당을 실행하는 것을 확인할 수 있습니다. 코드를 유지 관리하기가 더 어려워질 수 있으므로 모든 할당을 적극적으로 삭제할 필요는 없습니다. 대신 할당의 핫스팟에서 작업을 시작하세요.

버벅거리는 프레임

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

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

alt_text그림 3. 빠르고 빈번한 프레임을 보여주는 트레이스

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

alt_text그림 4. 주기적인 작업 버스트가 발생하며 빠르고 빈번한 프레임을 보여주는 트레이스

이 정기적인 케이던스에 중단이 있으면 이는 그림 5와 같이 버벅거리는 프레임인 것입니다.

alt_text그림 5. 버벅거리는 프레임을 보여주는 트레이스

이를 식별하는 연습을 할 수 있습니다.

alt_text 그림 6. 많이 버벅거리는 프레임을 보여주는 트레이스

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

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

흔히 발생하는 RecyclerView 실수

불필요하게 RecyclerView의 전체 지원 데이터를 무효화하면 프레임 렌더링 시간이 길어지고 버벅거림이 발생할 수 있습니다. 대신 업데이트해야 하는 뷰의 수를 최소화하려면 변경되는 데이터만 무효화합니다.

콘텐츠를 완전히 대체하는 대신 업데이트하게 하는, 비용이 많이 드는 notifyDatasetChanged() 호출을 방지하는 방법은 동적 데이터 표시를 참고하세요.

모든 중첩된 RecyclerView를 제대로 지원하지 않으면 내부 RecyclerView가 매번 완전히 다시 만들어질 수 있습니다. 중첩된 모든 내부 RecyclerView에는 모든 내부 RecyclerView 간에 뷰를 재활용할 수 있도록 RecycledViewPool이 설정되어 있어야 합니다.

데이터를 충분히 미리 가져오지 않거나 적시에 미리 가져오지 않으면 사용자가 서버에서 더 많은 데이터를 기다려야 할 때 스크롤 목록 하단에 도달하기가 불편할 수 있습니다. 이는 프레임 기한이 누락되지는 않으므로 엄밀히 말하면 버벅거림은 아니지만 사용자가 데이터를 기다릴 필요가 없도록 미리 가져오기의 시기와 양을 수정하여 UX를 크게 개선할 수 있습니다.

앱 디버그

다음은 앱의 성능을 디버그하는 다양한 방법입니다. 시스템 추적 및 Android 스튜디오 프로파일러 사용에 관한 개요는 다음 동영상을 참고하세요.

Systrace로 앱 시작 디버그

앱 시작 프로세스의 개요는 앱 시작 시간을 참고하고 시스템 추적의 개요는 다음 동영상을 참고하세요.

다음 단계에서 시작 유형을 구분할 수 있습니다.

  • 콜드 스타트: 저장된 상태가 없는 새 프로세스를 만들면서 시작합니다.
  • 웜 스타트: 프로세스를 재사용하는 동안 활동을 다시 만들거나 저장된 상태로 프로세스를 다시 만듭니다.
  • 핫 스타트: 활동을 다시 시작하고 확장에서 시작합니다.

기기의 시스템 추적 앱을 사용하여 Systrace를 캡처하는 것이 좋습니다. Android 10 이상에서는 Perfetto를 사용하세요. Android 9 이하의 경우 Systrace를 사용합니다. 웹 기반 Perfetto 트레이스 뷰어를 사용하여 트레이스 파일을 보는 것도 좋습니다. 자세한 내용은 시스템 추적 개요를 참고하세요.

다음과 같은 문제를 찾아보세요.

  • 모니터 경합: 모니터로 보호되는 리소스 경쟁은 앱 시작을 크게 지연시킬 수 있습니다.
  • 동기 바인더 트랜잭션: 앱의 중요 경로에서 불필요한 트랜잭션을 찾습니다. 필요한 트랜잭션이 비용이 많이 드는 경우 관련 플랫폼팀과 협력하여 개선하는 것이 좋습니다.

  • 동시 GC: 일반적이고 영향도 비교적 적지만 자주 발생하는 경우 Android 스튜디오 메모리 프로파일러를 사용하여 조사하는 것이 좋습니다.

  • I/O: 시작 중에 실행된 I/O를 확인하고 긴 중단을 찾습니다.

  • 다른 스레드의 중요한 활동: UI 스레드를 방해할 수 있으므로 시작 시 백그라운드 작업에 주의합니다.

앱 시작 측정항목 보고를 개선하려면 앱 측면에서 시작이 완료될 때 reportFullyDrawn를 호출하는 것이 좋습니다. reportFullyDrawn 사용에 관한 자세한 내용은 전체 표시까지 걸리는 시간 섹션을 참고하세요. Perfetto 트레이스 프로세서를 통해 RFD 정의 시작 시간을 추출할 수 있으며 사용자에게 표시되는 트레이스 이벤트가 내보내집니다.

기기에서 시스템 추적 사용

시스템 추적이라는 시스템 수준 앱을 사용하여 기기에서 시스템 트레이스를 캡처할 수 있습니다. 이 앱을 사용하면 기기에 전원을 연결하거나 adb에 연결하지 않고도 기기에서 트레이스를 기록할 수 있습니다.

Android 스튜디오 메모리 프로파일러 사용

Android 스튜디오 메모리 프로파일러를 사용하여 메모리 누수나 잘못된 사용 패턴으로 인해 발생할 수 있는 메모리 압력을 검사할 수 있습니다. 객체 할당을 실시간으로 확인할 수 있습니다.

메모리 프로파일러를 사용하여 GC가 발생하는 이유와 빈도를 추적하는 정보를 따라 앱의 메모리 문제를 해결할 수 있습니다.

앱 메모리를 프로파일링하려면 다음 단계를 따르세요.

  1. 메모리 문제를 감지합니다.

    중점적으로 살펴볼 사용자 여정의 메모리 프로파일링 세션을 기록합니다. 그림 7과 같이 객체 수가 증가하는지 확인합니다. 이는 결국 그림 8과 같이 GC로 이어집니다.

    alt_text 그림 7. 객체 수가 증가합니다.

    alt_text 그림 8. 가비지 컬렉션

    메모리 압력을 추가하는 사용자 여정을 확인한 후 메모리 압력의 근본 원인을 분석합니다.

  2. 메모리 압력 핫스팟을 진단합니다.

    그림 9와 같이 타임라인에서 범위를 선택하여 할당Shallow Size를 모두 시각화합니다.

    alt_text 그림 9. 할당얕은 크기의 값입니다.

    이 데이터를 정렬하는 방법에는 여러 가지가 있습니다. 다음은 각 뷰를 통해 문제를 분석할 수 있는 방법에 관한 예입니다.

    • 클래스별 정렬: 다른 경우에는 메모리 풀에서 캐시되거나 재사용되는 객체를 생성하는 클래스를 찾으려고 할 때 유용합니다.

      예를 들어 'Vertex'라는 클래스의 객체를 초당 2,000개 만드는 앱이 있다고 가정해 보겠습니다. 그러면 초당 2,000씩 할당 수가 증가하고 클래스별로 정렬할 때 확인할 수 있습니다. 이러한 객체를 재사용하여 가비지 생성을 방지하려면 메모리 풀을 구현하세요.

    • callstack별 정렬: 루프 내부 또는 많은 할당 작업을 하는 특정 함수와 같이 메모리가 할당되는 핫 경로가 있는 위치를 찾으려는 경우에 유용합니다.

    • Shallow Size: 객체 자체의 메모리만 추적합니다. 주로 프리미티브 값으로만 구성된 간단한 클래스를 추적하는 데 유용합니다.

    • Retained Size: 객체로 인해 발생한 총 메모리와 객체에서만 참조하는 참조를 보여줍니다. 복잡한 객체로 인한 메모리 압력을 추적하는 데 유용합니다. 이 값을 가져오려면 그림 10과 같이 전체 메모리 덤프를 수행하고 그림 11과 같이 Retained Size를 열로 추가합니다.

      alt_text 그림 10. 전체 메모리 덤프

      Retained Size 열
      그림 11. Retained Size 열
  3. 최적화의 영향을 측정합니다.

    GC는 메모리 최적화의 영향을 더 명확하고 쉽게 측정할 수 있습니다. 최적화로 메모리 압력이 줄면 GC 횟수가 줄어듭니다.

    최적화의 영향을 측정하려면 프로파일러 타임라인에서 GC 간 시간을 측정하세요. 그러면 GC 간에 걸리는 시간이 더 길어지는 것을 확인할 수 있습니다.

    메모리 개선의 최종적인 영향은 다음과 같습니다.

    • 앱에 지속적인 메모리 압력이 가해지지 않으면 메모리 부족 종료가 줄어들 수 있습니다.
    • GC가 줄면 특히 P99에서 버벅거림 측정항목이 개선됩니다. GC가 CPU 경합을 유발하여 GC가 실행되는 동안 렌더링 작업이 지연될 수 있기 때문입니다.