Xcode로 메모리 성능 문제를 감지하고 진단하는 방법을 알아보자.
왜 우리가 애플리케이션의 메모리 사용량에 대해 알아야 할까? 핵심적인 이유는 사용자 경험과 관련있기 때문이다.
-
Faster application activations ARC는 메모리 사용을 모니터링하고 다른 곳에 메모리 할당이 필요한 경우 앱을 종료하여 메모리를 확보한다. 앱이 백그라운드 상태일 때 상태를 보존하여 메모리 사용공간을 작게 유지하면 ARC에 의해 메모리가 회수될 확률이 낮아져 앱의 활성화 속도가 빨라진다.
-
Responsive experience 메모리 사용량을 줄이면 응답성이 향상된다. 또한 사용자가 앱과 상호작용하면서 추가적인 메모리가 필요한 경우 이를 위해 대기하는 비용을 회피할 수 있다.
-
Complex features 메모리를 효과적으로 관리한다면 애니메이션과 같은 더 많은 메모리를 사용하는 기능을 추가할 수 있다.
-
Wider device compatibility 메모리 사용 공간을 줄이면 앱이 구형 디바이스에서도 문제 없이 실행되어 더 많은 사용자에게 제공할 수 있게 된다.
앱이 메모리 공간을 어떻게 구성하는지 알아보자.
앱의 메모리를 분류하면 다음과 같이 나눌 수 있다.
- Dirty
- 앱에서 사용하는 메모리
- 이미지 버퍼, 프레임워크 등 할당된 모든 힙 영역의 메모리
- Compressed
- 최근에 액세스되지 않은 압축된 Dirty 페이지를 의미함
- 이 페이지에 액세스하면 압축이 해제된다.
- Clean
- 아직 사용되지 않은 메모리 혹은 페이지 아웃될 수 있는 메모리
보통 사용 중인 메모리이라고 하면 Dirty memory + Compressed memory를 의미한다.
애플에서 제공하고 있는 메모리 사용량을 분석하는 도구들 중 이번 세션에서는 이 중에서 XCTest 프레임워크를 사용할 것이다.
XCTest를 통해 다음과 같은 정보를 얻을 수 있다.
- Memory Utilization
- CPU usage
- Disk writes
- Hitch rate
- Wall clock time
- Application launch times
예제 테스트로 함께 알아보자. 이 앱에서 사용자가 레시피를 다운로드할 수 있는 Save meal 기능의 메모리 사용량을 측정하고 싶다.
이에 대한 테스트 코드
measure(metrics:, options:, block:)
: 타겟 앱의 메모리 성능을 측정한다.startMeasuring()
: 측정 시작을 지시한다.- UI에 대한 동작을 지시하고 실행
5회 반복의 측정 결과와 평균값이 계산되어 표시된다.
이 결과값을 향후에 기준치(baseline
)로 사용할 수 있으며 baseline
보다 결과가 크면 테스트가 실패한다.
regression
: baseline
으로부터 발생한 편차. regression이 발생했다면 테스트를 통과하도록 코드를 수정할 필요가 있음을 알 수 있다.
Xcode 13에서 새롭게 추가된 regression
을 분석하는 방법
- Ktrace file
- Memory graphs
- 메모리 그래프는 visual debugger 또는 Command line Tool과 함께 사용 가능하다.
- 인스턴스의 프로세스 주소 공간에 대한 스냅샷을 제공
- 메모리 그래프는 가상 메모리 영역과 할당된 malloc 블록의 주소와 크기를 기록한다. 이를 통해 힙 공간의 연결된 데이터 구조를 볼 수 있다.
- XCTest는 새로 할당하는 개체에 대한 malloc 스택 로깅(MSL)을 자동으로 활성화한다.
수집을 활성화하려면 xcodebuild의 enablePerformanceTestsDiagnostics
플래그를 사용해야 한다.
이 플래그는 Ktrace와 Memgraph를 수집을 활성화한다.
성능 테스트가 실행되면 콘솔에 결과가 출력되며 테스트 결과와 실패 이유, xcresult 번들 경로를 알 수 있다.
Xcode에서 xcresult 번들을 열면 메모리 사용량과 memgraphs 파일을 다운받을 수 있다. MSL을 활성화하기 위해 추가 반복을 하므로 두 개의 memgraph 파일이 생성된다.
앱의 일반적인 메모리 문제는 크게 Leak과 Heap 문제로 나눌 수 있다.
- Leaks
- Heap size issues
- Heap allocation regressions
- Fragmentation
프로세스가 객체에 대한 메모리 할당 해제가 이루어지지 않고 참조를 잃을 때 발생한다.(대표적으로 Retain cycle)
- unsafe type과 같이 ARC에서 관리하지 않는 경우 참조를 잃기 전에 할당을 해제 해주어야 한다.
- ARC에 의해 관리되는 객체라 하더라도 강한 참조 사이클이 발생할 수 있으므로 Reference cycle이 발생할 위험이 있다면 약한 참조를 사용해야 한다.
memgraph 파일로부터 누수를 살펴보자.
leaks
로 memgraph 파일에 대한 누수 결과를 분석할 수 있다. (leaks ~.memgraph
)
결과 출력에서 누수가 있었음이 표시된다.
- 4 leaks for 240 bytes
- retain cycle을 다루는 ROOT CYCLE이 표시되며 어떤 객체와 참조가 문제인지 알 수 있다.
- 누수에 대한 할당 호출 스택이 표시되어 있으므로 어떤 객체로부터 누수가 발생했는지 알 수 있다.
힙은 동적으로 할당된 객체가 저장되는 프로세스 주소 공간이다. 힙에 많은 객체를 할당하면 따라서 사용하는 메모리 공간이 증가하게 된다. 이로 인해 Heap allocation regression이 발생하게 된다.
- 힙 사용량을 줄이기 위해 사용하지 않는 메모리 할당을 제거해야 한다.
- 또한 불필요하게 큰 메모리 할당을 줄여야 한다.
- 더 이상 사용하지 않는 메모리를 할당 해제해야 한다.
- 필요한 시점에만 메모리를 할당해야 한다.
실패한 XCTest에서 Heap regression을 확인해보자.
vmmap -summary
로 memgraph 파일에서 어떤 메모리 위치가 사용되는지 살펴볼 수 있다. (vmmap -summary ~.memgraph
)
Pre memgraph의 물리적인 사용량은 약 112MB.
Post memgraph의 물리적인 사용량은 125MB로 약 13MB
의 차이가 발생했다.
프로세스의 메모리 사용량이 지역별로 분류되어 표시된다. 힙 할당 문제라면 MALLOC_
으로 시작되는 영역을 살펴보아야 한다.
MALLOC_LARGE
영역이 13MB의 Dirty Memory를 보유하고 있는데, 이에 해당하는 값이 증가한 크기와 유사하기 때문에 어떤 객체가 연관되어 있는지 확인해보아야 한다.
heap -diffFrom
명령어를 통해 pre memgraph와 post memgraph를 열어본다.(heap -diffFrom pre_~.memgraph post_~.memgraph
)
이것은 post에는 존재하지만 pre에는 없는 객체를 보여준다.
출력에서 힙 메모리가 클래스별로 분류된 부분을 볼 수 있다. 여기에서 13MB에 해당하는 non-object
를 발견할 수 있다.
이곳의 힙 주소를 얻기 위해 heap -address=
명령어를 사용한다. address 옵션에 값을 넣어 크기가 500KB 이상인 non-object만 가져온다. (-address=non-object[500K-] post_~.memgraph
)
0x113800000
주소에 있는 non-object가 13MB를 점유하고 있는 용의자라는 것을 알아내었다.
알아낸 주소를 어떻게 활용할 수 있을까?
- leaks --traceTree=0x113800000
이 주소를 참조하는 객체들에 대한 트리를 제공한다.
자세한 정보를 얻고 싶은 특정 객체가 있을 때, MSL이 활성화되어있지 않을 때 유용하다.
- leaks --referenceTree
프로세스의 모든 메모리에 대한 하향식 참조 트리를 제공한다.
앱에 regression이 발생했지만 어떤 특정 객체가 원인인지 알 수 없는 경우에 유용하다.
- malloc_history -fullStack ~.memgraph 0x113800000
객체가 어떻게 할당되었는지 알아내는 방법이다. 해당 주소에 대한 할당 호출 스택들을 볼 수 있다.
- 페이지: 시스템이 프로세스에 부여하는 고정된 크기의 분할할 수 없는 메모리 청크
- 페이지는 나눌 수 없기 때문에 페이지의 일부를 사용할 땐 전체 페이지가 Dirty 메모리로 간주됨.
- 따라서 아주 극히 일부만 사용하더라도 페이지 전체에 대한 비용이 든다.
Fragmentation은 100% 활용되지 않는 Dirty 페이지가 있을 때 발생한다.
- 최대한 수명이 비슷한 객체들을 서로 가까운 공간에 할당할 것.
- 이렇게 하면 객체가 함께 해제되며 작업에 필요한 연속된 메모리 청크를 제공할 수 있게 된다.
- 일반적으로 fragmentation는 불가피한 문제이기 때문에, 목표는 최대 25%의 fragmentation를 목표로 한다.
- 또한 Autorelease Pool을 사용하여 Fragmentation을 줄일 수 있다. (autorelease pool: 범위를 벗어나는 즉시 내부에 할당된 모든 객체를 해제)
- fragmentation은 특히 오랜 시간 실행되는 프로세스일 경우에 취약하다.
vmmap -summary
를 실행하여 단편화 분석을 살펴볼 수 있다. 출력 결과의 맨 아래로 내려가보자. MALLOC ZONE
에는 각 영역이 어떤 할당으로 나뉘었는지 보여준다.
일반적으로는 DefaultMallocZone
에 대해서만 관심을 가지면 된다. 보통 힙 할당이 끝나는 곳이기 때문이다.
그러나 MSL이 활성화되어 있기 때문에 지금은 MallocStackLoggingLiteZone
을 봐야 한다. 이곳은 MSL이 활성화되어 있는 동안 힙 할당이 끝나는 곳이다.
% FRAG
열은 각 malloc 영역이 단편화로 인해 발생한 메모리 낭비 비율을 보여준다. 사이즈가 가장 큰 MallocStackLoggingLiteZone
이 최대 상한선인 25%보다 작은 19%이기 때문에, 현재 앱은 단편화 문제로부터 안전하다고 볼 수 있다.
만약 단편화 문제가 발생했다면, Xcode 도구 중 Allocations 트랙을 사용하여 자세히 진단할 수 있다.
메모리 문제 감지 과정
- 새 기능을 추가할 때마다 성능 테스트를 작성하여 메모리 지표를 모니터링한다.
- 각 테스트에 대한 기준선을 정한다.
- 테스트를 통해 regression을 파악한다.
- 수집된 ktrace, memgraph 파일을 사용하여 진단한다.
진단 절차
- 가장 먼저 누수 문제를 확인한다.
- 누수가 발견되지 않았으면 힙을 진단한다.
- 어떤 객체에서 문제가 발생했는지 파악한다.
- 문제가 있는 객체를 찾아냈다면 그 주소를, 아직 찾아내지 못했다면 더 많은 단서를 통해 정보를 수집한다.
- 마지막으로 해당 주소를 조사한다.