프로파일링
프로파일링은 앱의 성능, 리소스 사용량, 동작을 분석하여 잠재적인 병목 현상이나 비효율성을 찾아내는 과정이다. 다양한 기기와 환경에서 앱이 원활하게 작동하는지 확인하려면 프로파일링 도구를 활용하는 것이 중요하다.
iOS에서는 Instruments가 필수적인 도구이며, Android에서는 Android Studio Profiler를 사용하는 방법을 익혀야 한다.
하지만 먼저, 개발자 모드를 꺼야 한다! 앱 로그에서 __DEV__ === false, development-level warning are OFF, performance optimizations are ON
이라는 메시지를 확인해야 한다.
Android UI 성능 프로파일링: 시스템 트레이싱 활용
Android는 10,000개가 넘는 다양한 스마트폰을 지원하며 소프트웨어 렌더링을 일반화했다. 프레임워크 아키텍처와 다양한 하드웨어 대상을 지원해야 하는 필요성 때문에 iOS에 비해 기본적으로 제공되는 기능이 적은 편이다. 하지만 때로는 성능을 개선할 수 있는 부분이 있으며, 대부분의 경우 네이티브 코드의 문제가 아닌 경우가 많다.
이러한 지연 현상을 디버깅하는 첫 번째 단계는 각 16ms 프레임 동안 시간이 어디에서 소비되는지 근본적인 질문에 답하는 것이다. 이를 위해 Android Studio에 내장된 시스템 트레이싱 프로파일러를 사용한다.
1. 트레이스 수집하기
먼저, 분석하려는 버벅임 현상이 발생하는 기기를 USB로 컴퓨터에 연결한다. Android Studio에서 프로젝트의 android
폴더를 열고, 오른쪽 상단 패널에서 해당 기기를 선택한 후 프로파일링 가능한 상태로 프로젝트를 실행한다.
앱이 프로파일링 가능한 상태로 빌드되어 기기에서 실행 중일 때, 분석하려는 네비게이션 또는 애니메이션 직전까지 앱을 실행한다. 그런 다음 Android Studio Profiler 패널에서 "시스템 활동 캡처" 작업을 시작한다.
트레이스 수집이 시작되면, 분석하려는 애니메이션 또는 상호작용을 수행한다. 그런 다음 "녹화 중지"를 누른다. 이제 Android Studio에서 직접 트레이스를 확인할 수 있다. 또는 "과거 기록" 패널에서 해당 트레이스를 선택하고 "기록 내보내기"를 눌러 Perfetto와 같은 도구에서 열 수도 있다.
2. 트레이스 데이터 분석
Android Studio나 Perfetto에서 트레이스를 열면 다음과 같은 화면을 확인할 수 있다:
WASD 키를 사용해 이동 및 확대/축소를 할 수 있다.
사용하는 도구에 따라 UI가 약간 다를 수 있지만, 아래 설명은 모든 도구에 적용된다.
화면 오른쪽 상단에 있는 이 체크박스를 선택해 16ms 프레임 경계를 강조한다:
위 스크린샷처럼 줄무늬가 표시되어야 한다. 그렇지 않다면 다른 기기에서 프로파일링을 시도한다. 삼성 기기에서는 VSync 표시에 문제가 있는 반면, Nexus 시리즈는 일반적으로 안정적으로 동작한다.
3. 프로세스 찾기
패키지 이름의 일부가 보일 때까지 스크롤한다. 이 경우, com.facebook.adsmanager
를 프로파일링했는데, 커널의 스레드 이름 제한 때문에 book.adsmanager
로 표시되었다.
왼쪽에는 타임라인 행에 해당하는 여러 스레드가 표시된다. 우리가 주목해야 할 몇 가지 주요 스레드는 다음과 같다: UI 스레드(패키지 이름 또는 UI Thread로 표시), mqt_js
, 그리고 mqt_native_modules
. 안드로이드 5 이상에서 실행 중이라면 Render Thread도 중요하다.
-
UI 스레드. 여기서 안드로이드의 기본 측정(measure)/레이아웃(layout)/그리기(draw) 작업이 수행된다. 오른쪽의 스레드 이름은 패키지 이름(예:
book.adsmanager
) 또는 UI Thread로 표시된다. 이 스레드에서 볼 수 있는 이벤트는Choreographer
,traversals
,DispatchUI
와 관련된 내용이다. -
JS 스레드. 자바스크립트가 실행되는 곳이다. 스레드 이름은
mqt_js
또는<...>
로 표시되며, 기기의 커널 상태에 따라 다르다. 이름이 없을 경우JSCall
,Bridge.executeJSCall
등의 키워드로 식별할 수 있다. -
네이티브 모듈 스레드. 네이티브 모듈 호출(예:
UIManager
)이 실행되는 곳이다. 스레드 이름은mqt_native_modules
또는<...>
로 표시된다. 이름이 없을 경우NativeCall
,callJavaModuleMethod
,onBatchComplete
등의 키워드로 식별할 수 있다. -
추가: 렌더 스레드. 안드로이드 L(5.0) 이상을 사용 중이라면 애플리케이션에 렌더 스레드가 존재한다. 이 스레드는 UI를 그리기 위한 OpenGL 명령을 생성한다. 스레드 이름은
RenderThread
또는<...>
로 표시된다. 이름이 없을 경우DrawFrame
,queueBuffer
등의 키워드로 식별할 수 있다.
문제 원인 파악
부드러운 애니메이션은 다음과 같이 보여야 한다:
색상이 바뀔 때마다 하나의 프레임을 의미한다. 프레임을 화면에 표시하려면 모든 UI 작업이 16ms 이내에 완료되어야 한다. 어떤 스레드도 프레임 경계선 근처에서 작업을 수행하지 않는다. 이런 방식으로 렌더링하는 애플리케이션은 60 FPS로 동작한다.
하지만 애니메이션이 끊기는 현상을 발견했다면, 다음과 같은 상황을 볼 수 있다:
JS 스레드가 거의 항상 실행되고 있으며, 프레임 경계를 넘어 작업을 수행한다. 이 애플리케이션은 60 FPS로 렌더링되지 않는다. 이 경우, 문제는 JS에 있다.
또 다른 경우로 다음과 같은 상황을 볼 수 있다:
이 경우, UI와 렌더 스레드가 프레임 경계를 넘어 작업을 수행한다. 각 프레임에서 렌더링하려는 UI가 너무 많은 작업을 필요로 한다. 이 경우, 문제는 렌더링되는 네이티브 뷰에 있다.
이 시점에서 여러분은 다음 단계를 결정하는 데 매우 유용한 정보를 얻을 수 있다.
자바스크립트 문제 해결
자바스크립트 문제를 발견했다면, 실행 중인 특정 자바스크립트 코드에서 단서를 찾아보자. 위 시나리오에서는 RCTEventEmitter
가 프레임마다 여러 번 호출되는 것을 확인할 수 있다. 다음은 트레이스에서 자바스크립트 스레드를 확대한 이미지다:
이 상황은 정상적이지 않다. 왜 이렇게 자주 호출될까? 실제로 다른 이벤트인가? 이러한 질문에 대한 답은 여러분의 제품 코드에 따라 달라질 것이다. 많은 경우, shouldComponentUpdate를 살펴보는 것이 도움이 될 수 있다.
네이티브 UI 문제 해결
네이티브 UI 문제를 발견했다면 일반적으로 두 가지 상황을 고려해야 한다:
- 매 프레임마다 그리려는 UI가 GPU에 과도한 부하를 주는 경우
- 애니메이션이나 인터랙션 중에 새로운 UI를 생성하는 경우 (예: 스크롤 중에 새로운 콘텐츠를 로드하는 경우)
이 두 가지 상황은 각각 다른 접근 방식으로 문제를 해결해야 한다. 첫 번째 경우에는 GPU의 작업량을 줄이기 위해 최적화가 필요하며, 두 번째 경우에는 UI 생성 타이밍을 조정하거나 비동기 처리 방식을 고려해야 한다.
GPU 작업 과부하
첫 번째 시나리오에서 UI 스레드와 렌더 스레드가 다음과 같은 상태로 나타나는 트레이스를 확인할 수 있다:
DrawFrame
에서 프레임 경계를 넘어가는 긴 시간을 주목하라. 이는 GPU가 이전 프레임의 커맨드 버퍼를 비우기를 기다리는 시간이다.
이 문제를 완화하려면 다음 사항을 고려해야 한다:
- 복잡하고 정적인 콘텐츠가 애니메이션/변환되는 경우
renderToHardwareTextureAndroid
사용을 검토한다. (예:Navigator
슬라이드/알파 애니메이션) - 기본적으로 비활성화된
needsOffscreenAlphaCompositing
을 사용하지 않도록 주의한다. 대부분의 경우 GPU의 프레임당 부하를 크게 증가시키기 때문이다.
UI 스레드에서 새로운 뷰 생성하기
두 번째 시나리오에서는 다음과 같은 상황을 마주하게 된다:
먼저 JS 스레드가 잠시 생각을 하고, 그 다음 네이티브 모듈 스레드에서 작업이 진행되며, 마지막으로 UI 스레드에서 비용이 많이 드는 탐색 작업이 이루어지는 것을 확인할 수 있다.
이 문제를 빠르게 해결할 수 있는 방법은 없다. 상호작용이 끝난 후에 새로운 UI를 생성하도록 미루거나, 생성하려는 UI를 단순화하는 방법밖에 없다. 리액트 네이티브 팀은 이 문제를 해결하기 위해 인프라 수준의 솔루션을 개발 중이다. 이 솔루션은 메인 스레드가 아닌 다른 스레드에서 새로운 UI를 생성하고 설정할 수 있도록 하여, 상호작용이 원활하게 진행될 수 있게 한다.
네이티브 CPU 핫스팟 찾기
문제가 네이티브 쪽에 있는 것 같다면, CPU 핫스팟 프로파일러를 사용해 더 자세한 정보를 얻을 수 있다. Android Studio의 프로파일러 패널을 열고 "Find CPU Hotspots (Java/Kotlin Method Recording)"을 선택한다.
"Find CPU Hotspots (Java/Kotlin Recording)"을 선택해야 한다. "Find CPU Hotspots (Callstack Sample)"과 아이콘이 비슷하지만 기능이 다르다.
상호작용을 수행한 후 "Stop recording"을 누른다. 기록 작업은 리소스를 많이 사용하므로 상호작용 시간을 짧게 유지한다. 그런 다음 Android Studio에서 결과 트레이스를 확인하거나 Firefox Profiler와 같은 온라인 도구에서 열어볼 수 있다.
시스템 트레이스와 달리, CPU 핫스팟 프로파일링은 속도가 느려 정확한 측정값을 제공하지는 않는다. 하지만 어떤 네이티브 메서드가 호출되는지, 그리고 각 프레임 동안 시간이 어떻게 소비되는지 대략적인 정보를 제공한다.