https://perfetto.dev/docs/getting-started/memory-profiling
Perfetto로 메모리 프로필 기록하기
이 가이드에서는 다음 내용을 다룹니다:
* Perfetto를 사용하여 native 및 Java heap profile 기록하기
* Perfetto UI에서 heap profile 시각화 및 분석하기
* 다양한 memory profiling 모드 이해 및 사용 시기 파악하기
프로세스의 메모리 사용량은 프로세스 성능과 전체 시스템 안정성에 중요한 역할을 합니다. 프로세스가 메모리를 어디에 어떻게 사용하는지 이해하면, 프로세스가 예상보다 느리게 실행되는 이유를 파악하거나 프로그램을 더 효율적으로 만드는 데 큰 통찰력을 얻을 수 있습니다.
앱과 메모리의 관점에서 프로세스가 메모리를 사용하는 방식은 크게 두 가지입니다:
1. Native C/C++/Rust Process: 일반적으로 libc의 `malloc`/`free` (또는 C++의 `new`/`delete`와 같은 래퍼)를 통해 메모리를 할당합니다. Java API를 사용하더라도 JNI를 통해 구현된 경우 네이티브 할당이 가능하며 꽤 빈번합니다. 전형적인 예로 `java.util.regex.Pattern`이 있는데, 이는 Java 힙의 관리되는 메모리와 네이티브 정규식 라이브러리 사용으로 인한 네이티브 메모리를 모두 소유합니다.
2. Java/Kotlin Apps: 앱의 메모리 공간 중 상당 부분은 관리되는 heap(Android의 경우 ART의 가비지 컬렉터가 관리)에 존재합니다. 모든 새로운 `X()` 객체가 이곳에 생성됩니다.
Perfetto는 위 두 가지 상황을 디버깅하기 위한 상호 보완적인 두 가지 기술을 제공합니다:
* Heap Profiling (네이티브 코드를 위한 힙 프로파일링): `malloc`/`free`가 발생할 때의 호출 스택(callstack)을 샘플링하여, 호출 지점별로 메모리 사용량을 분류한 집계된 플레임 그래프(flamegraph)를 보여줍니다.
* Heap Dumps (Java/관리형 코드를 위한 힙 덤프): 객체 간의 유지(retention) 의존성을 보여주는 힙 유지 그래프를 생성합니다(호출 지점은 보여주지 않음).
## 네이티브 (C/C++/Rust) 힙 프로파일링
C/C++/Rust와 같은 네이티브 언어는 일반적으로 libc 계열의 `malloc`/`free` 함수를 사용하여 가장 낮은 수준에서 메모리를 할당하고 해제합니다. 네이티브 힙 프로파일링은 이러한 함수 호출을 가로채고(intercept), 할당되었으나 아직 해제되지 않은 메모리의 호출 스택을 추적하는 코드를 주입하여 작동합니다. 이를 통해 각 할당의 "코드 원점(code origin)"을 추적할 수 있습니다. `malloc`/`free`는 힙 사용량이 많은 프로세스에서 성능 핫스팟이 될 수 있으므로, 메모리 프로파일러의 오버헤드를 줄이기 위해 정확도와 오버헤드를 절충하는 샘플링(sampling)을 지원합니다.
**참고**: Perfetto를 사용한 네이티브 힙 프로파일링은 **Android와 Linux에서만 작동**합니다. 이는 `malloc`과 `free`를 가로채는 기술이 이 운영체제들에서만 작동하기 때문입니다.
매우 중요한 점은 힙 프로파일링이 **소급 적용되지 않는다**는 것입니다. 트레이싱이 시작된 이후에 발생하는 할당만 보고할 수 있습니다. 트레이스 시작 전에 발생한 할당에 대한 통찰력은 제공할 수 없습니다. 프로세스 시작부터 메모리 사용량을 분석해야 한다면, 프로세스가 실행되기 전에 트레이싱을 시작해야 합니다.
"왜 이 프로세스가 지금 이렇게 큰가?"라는 질문에 대해, 과거에 일어난 일을 힙 프로파일링으로 답할 수는 없습니다. 하지만 경험상 메모리 누수를 쫓고 있다면, 누수가 시간이 지남에 따라 계속 발생할 가능성이 높으므로 미래의 증가분을 볼 수 있을 것입니다.
### 첫 번째 힙 프로필 수집하기
Android (Perfetto UI 사용)
Android에서 Perfetto 힙 프로파일링 후크(hooks)는 libc 구현에 매끄럽게 통합되어 있습니다.
Prerequisites
* Android 10 이상을 실행하는 기기.
* 'Profileable' 또는 'Debuggable' 앱. "user" 빌드 Android(userdebug나 eng가 아닌)를 실행 중인 경우, 앱의 매니페스트에 profileable 또는 debuggable로 표시되어 있어야 합니다. (자세한 내용은 heapprofd 문서 참조)
Instruction
1. [https://ui.perfetto.dev/\#\!/record](https://ui.perfetto.dev/#!/record) 를 엽니다.
2. 대상 기기로 **Android**를 선택하고 사용 가능한 전송 방식 중 하나를 사용합니다(잘 모르겠다면 **WebUSB**가 가장 쉽습니다).
3. 왼쪽의 **Memory** 프로브를 클릭한 다음 **Native Heap Profiling** 옵션을 켭니다.
4. **Names** 상자에 프로세스 이름을 입력합니다. 입력해야 할 프로세스 이름은 프로세스 cmdline의 첫 번째 인자입니다. (`adb shell ps -A`의 가장 오른쪽 `NAME` 열에 해당)
5. **Buffers and duration** 페이지에서 관찰 시간을 선택합니다. 이는 프로파일러가 `malloc`/`free` 호출을 가로채는 기간을 결정합니다.
6. 빨간색 버튼을 눌러 트레이스 기록을 시작합니다.
7. 트레이스가 기록되는 동안 프로파일링 중인 프로세스와 상호 작용합니다(사용자 여정 실행, 패턴 테스트 등).

Android (Command Line)
Prerequisites
* ADB 설치됨. (Windows 사용자는 `adb.exe`가 PATH에 있어야 함)
* Android 10 이상을 실행하는 기기.
* Profileable 또는 Debuggable 앱.
Instructions
| $ adb devices -l List of devices attached 24121FDH20006S device usb:2-2.4.2 product:panther model:Pixel_7 device:panther transport_id:1 |
여러 기기가 연결된 경우 시리얼 번호를 내보냅니다:
| export ANDROID_SERIAL=24121FDH20006S |
Download `tools/heap_profile` (Perfetto 체크아웃이 없는 경우):
| curl -LO https://raw.githubusercontent.com/google/perfetto/main/tools/heap_profile |
프로파일링 시작:
| python3 heap_profile -n cohttp://m.google.android.apps.nexuslauncher |
테스트 패턴을 실행하고 프로세스와 상호 작용한 후 완료되면 `Ctrl-C`를 누릅니다. (또는 `-d 10000`으로 시간 제한 설정)
`Ctrl-C`를 누르면 스크립트가 트레이스를 가져와 `/tmp/heap_profile-latest`에 저장하고 아래 메세지를 출력합니다.
| Wrote profiles to /tmp/53dace (symlink /tmp/heap_profile-latest) The raw-trace file can be viewed using https://ui.perfetto.dev |
Linux (Command Line)
Prerequisites
* Perfetto 체크아웃에서 `libheapprofd_glibc_preload.so` 라이브러리를 빌드해야 합니다.
Instructions
1. `tracebox`와 `heap_profile` 스크립트 다운로드:
| curl -LO https://get.perfetto.dev/tracebox curl -LO https://raw.githubusercontent.com/google/perfetto/main/tools/heap_profile chmod +x tracebox heap_profile |
2. Start the tracing service
| ./tracebox traced & |
3. heapprofd 설정을 생성하고 tracing session 시작
| # Replace trace_processor_shell with with the name of the process you want to # profile. ./heap_profile -n trace_processor_shell --print-config | \ ./tracebox perfetto --txt -c - -o ~/trace_processor_memory.pftrace |
4. 다른 터미널에서 heapprofd의 .so를 프리로드(preload)하여 대상 프로세스 시작:
| PERFETTO_HEAPPROFD_BLOCKING_INIT=1 \ LD_PRELOAD=out/lnx/libheapprofd_glibc_preload.so \ trace_processor_shell /dev/null # Typing this will cause a 40MB allocation. > CREATE TABLE x as SELECT randomblob(40000000) as data |
*참고*: `PERFETTO_HEAPPROFD_BLOCKING_INIT=1`은 heapprofd가 완전히 초기화될 때까지 첫 번째 malloc에서 프로그램을 차단하여 시작 시 할당이 누락되는 것을 방지합니다.
5. 완료되면 이전 단계의 `tracebox perfetto` 명령에서 `Ctrl-C`를 눌러 트레이스 파일을 씁니다.
6. `tracebox traced` 서비스도 종료합니다.
첫 번째 힙 프로필 시각화하기
Perfetto UI에서 트레이스 파일을 열고 "Heap profile"이라고 표시된 트랙의 쉐브론(v) 마커를 클릭합니다.

**Profile Diamond 및 Native Flamegraph**
기본적으로 집계된 플레임 그래프는 해제되지 않은 메모리(즉, `free()`되지 않은 메모리)를 호출 스택별로 보여줍니다. 맨 위 프레임은 호출 스택의 가장 초기 진입점(`main()` 등)이며, 아래로 갈수록 `malloc()`을 호출한 프레임에 가까워집니다.
**힙 프로파일링 모드**

* **Unreleased Malloc Size** (기본값): 해제되지 않은 메모리 바이트의 합계로 호출 스택을 집계합니다.
* **Unreleased Malloc Count**: 해제되지 않은 할당 횟수로 집계하며, 크기는 무시합니다. 작은 객체가 다수 누적되어 발생하는 누수를 찾는 데 유용합니다.
* **Total Malloc Size**: 해제 여부와 관계없이 `malloc()`을 통해 할당된 바이트로 집계합니다. 힙 변동(churn)이나 할당자에 압력을 주는 코드 경로를 조사할 때 유용합니다.
* **Total Malloc Count**: 위와 유사하지만, `malloc()` 호출 횟수로 집계합니다.
첫 번째 힙 프로필 쿼리하기 (SQL)
Perfetto UI의 "Query (SQL)" 탭을 사용하여 SQL로 트레이스를 쿼리할 수 있습니다.
예를 들어, 다음 쿼리를 실행하면 고유 호출 스택별로 할당된 메모리를 볼 수 있습니다:
1. Perfetto UI에서 왼쪽 사이드 메뉴의 "Query (SQL)" 을 클릭

2. 아래위로 나뉘어진 윈도우가 열리는데, 윗쪽에 PerfettoSQL query 를 작성하면 아래쪽에 결과가 나옵니다.

3. " Ctrl/Cmd + Enter" 로 query를 실행할 수 있습니다.
| INCLUDE PERFETTO MODULE android.memory.heap_graph.heap_graph_class_aggregation; SELECT -- Class name (deobfuscated if available) type_name, -- Count of class instances obj_count, -- Size of class instances size_bytes, -- Native size of class instances native_size_bytes, -- Count of reachable class instances reachable_obj_count, -- Size of reachable class instances reachable_size_bytes, -- Native size of reachable class instances reachable_native_size_bytes FROM android_heap_graph_class_aggregation; |
## Java/관리형 (Managed) 힙 덤프
Java나 Kotlin 같은 관리형 언어는 런타임 환경을 사용하여 메모리 관리와 가비지 컬렉션(GC)을 처리합니다. 이러한 언어에서는 (거의) 모든 객체가 힙 할당입니다. 객체는 참조를 통해 메모리에 유지되며, 도달할 수 없게 되면 GC에 의해 자동으로 회수됩니다. 수동 `free()` 호출은 없습니다.
따라서 관리형 언어를 위한 프로파일링 도구는 대부분 모든 라이브 객체와 그들의 유지 관계를 포함하는 \*\*전체 힙 덤프(full object graph)\*\*를 캡처하고 분석하는 방식으로 작동합니다.
이 방식은 사전 계측(instrumentation) 없이 전체 힙의 일관된 스냅샷을 제공하므로 **소급 분석이 가능**하다는 장점이 있습니다. 하지만 어떤 객체가 다른 객체를 살려두고 있는지는 볼 수 있지만, 해당 객체가 할당된 **정확한 호출 지점(call site)은 일반적으로 볼 수 없다**는 단점이 있습니다.
**참고**: Perfetto를 사용한 Java 힙 덤프는 **Android에서만 작동**합니다. 이는 프로세스 성능에 영향을 주지 않고 효율적으로 힙 덤프를 캡처하기 위해 Android 런타임(ART)과의 깊은 통합이 필요하기 때문입니다.
### 첫 번째 힙 덤프 수집하기
#### Android (Perfetto UI 사용)
**전제 조건**
* Android 10 이상 기기.
* Profileable 또는 Debuggable 앱.
**지침**
1. [https://ui.perfetto.dev/\#\!/record](https://ui.perfetto.dev/#!/record) 접속.
2. Android 기기 선택.
3. **Memory** 프로브에서 **Java heap dumps** 옵션을 켭니다.
4. **Names** 상자에 프로세스 이름을 입력합니다.
5. **Buffers and duration**에서 짧은 시간(10초 이하)을 선택합니다. (힙 덤프는 트레이스 끝에 한 번 전체를 내보내므로 긴 시간은 의미가 없습니다.)
6. 기록 시작 버튼을 누릅니다.
#### Android (명령줄 사용)
**전제 조건**
* ADB 설치 및 경로 설정.
* Android 10 이상 기기.
* Profileable/Debuggable 앱.
**지침**
1. `tools/java_heap_dump` 다운로드:
```bash
curl -LO https://raw.githubusercontent.com/google/perfetto/main/tools/java_heap_dump
```
2. 프로파일링 시작:
```bash
python3 java_heap_dump -n cohttp://m.google.android.apps.nexuslauncher
```
3. 스크립트가 힙 덤프가 포함된 트레이스를 기록하고 파일 경로를 출력합니다.
### 첫 번째 힙 덤프 시각화하기
Perfetto UI에서 파일을 열고 "Heap profile" 트랙을 확인합니다.
UI는 힙 그래프를 플레임 그래프 형태로 평탄화하여 보여줍니다. 두 가지 평탄화 전략이 있습니다:
* **Shortest path (최단 경로)**: `Object Size` 선택 시 기본값. 객체 간 거리를 최소화하는 휴리스틱을 기반으로 정렬합니다.
* **Dominator tree (지배자 트리)**: `Dominated Size` 선택 시 사용. 지배자 트리 알고리즘을 사용하여 그래프를 평탄화합니다.
### 첫 번째 힙 프로필 쿼리하기 (SQL)
Java 힙 덤프 데이터도 SQL로 쿼리할 수 있습니다.
예를 들어 다음 쿼리로 도달 가능한(reachable) 객체 크기와 개수 요약을 볼 수 있습니다:
```sql
INCLUDE PERFETTO MODULE android.memory.heap_graph.heap_graph_class_aggregation;
SELECT
type_name, obj_count, size_bytes, native_size_bytes,
reachable_obj_count, reachable_size_bytes, reachable_native_size_bytes
FROM android_heap_graph_class_aggregation;
```
-----
## 다른 유형의 메모리
기본적인 네이티브 및 Java 힙 외에도 다른 방식으로 메모리가 할당될 수 있습니다:
* **직접 mmap() 호출**: 애플리케이션이 `mmap()`을 사용하여 커널에서 직접 메모리를 요청할 수 있습니다(대형 할당 또는 파일 매핑 등). Perfetto는 현재 이를 자동으로 프로파일링하지 않습니다.
* **커스텀 할당자 (Custom allocators)**: 일부 앱은 성능을 위해 자체 메모리 할당자를 사용합니다. 이는 보통 `mmap()`으로 시스템 메모리를 가져와 내부적으로 관리합니다. heapprofd Custom Allocator API를 사용하여 계측하면 프로파일링이 가능합니다.
* **DMA 버퍼 (dmabuf)**: CPU, GPU, 카메라 등 하드웨어 구성 요소 간에 메모리를 공유하기 위한 특수 버퍼입니다. 그래픽 집약적인 앱에서 흔합니다. 트레이스 설정에서 `dmabuf_heap` 또는 `dma_heap_stat` ftrace 이벤트를 활성화하여 추적할 수 있습니다.
'지식 > Android' 카테고리의 다른 글
| Android - Dalvik (0) | 2025.08.13 |
|---|---|
| Android CTS - Platform intents (0) | 2025.08.13 |
댓글