
오늘은 Instruments Tutorials를 실습하며 학습해본 내용입니다.
튜토리얼 링크: https://developer.apple.com/tutorials/instruments/
해당 프로젝트는 Collection 탭을 누르면 첫 시작 시 UI가 버벅이는(프리징) 현상이 발생합니다.

참고: Hang이 무엇이고 유저 반응성(User Responsiveness)이 무엇인지는 다음에 자세히 다루어 보겠습니다.
프로파일링을 시작하기 위해 Profile (Command + I)을 눌러 Time Profiler를 실행합니다.

왜 Time Profiler로 먼저 시작하나요?
Time Profiler는 시간 관련 프로파일링에 사용되는 instrument들의 세트로 구성된 새로운 문서를 생성합니다. 앱에서 느린(sluggish) 동작을 느꼈지만 무엇을 찾아야 할지 아직 모르겠다면 좋은 시작점이 됩니다.

새로운 Instruments 문서를 생성하면 "Next Recording" 뷰에서 시작합니다. 여기서 어떤 데이터를 기록할지 instrument 세트를 선택하여 설정할 수 있습니다.
Tip
이전 단계에서 선택한 템플릿이 새 문서에 기본적으로 추가되는 instrument들을 결정합니다. 언제든지 나중에 변경할 수 있으며, 심지어 녹화 실행 후에도 "Next Recording" 뷰로 돌아와서 다음 녹화를 위한 변경을 할 수 있습니다.

오른쪽 위의 Record 버튼을 누르고 동일하게 Collection 탭을 누르면 프로파일링이 시작됩니다.
왼쪽 사이드바에는 실행(Run)할 때마다 문서가 늘어나며, 변경사항을 비교하며 분석할 수 있습니다.

상단의 Time Profiler 트랙은 CPU 사용량을 보여줍니다.
제가 사용하는 디바이스는 iPhone 15 모델입니다:

현재 CPU 사양을 보면:
Time Profiler는 전체 CPU의 총합을 나타내고 있는 것으로 보입니다.
상단의 CPUs 트랙을 눌러보겠습니다:

사양과 동일하게:
0번부터 시작하는 게 정말 개발자답네요!
프리징이 걸리는 시점에 성능 코어의 CPU를 매우 많이 사용하고 있는 것이 보입니다. 성능 CPU 2개를 동시에 사용하고 있네요.
Threads 트랙을 살펴보겠습니다:

관찰 결과:
궁금증: Concurrency 공부할 때 스레드 개수는 CPU 코어 수와 동일하게 간다고 했던 것 같은데... 뭐가 맞는 걸까요?
어쨌든 Main Thread가 굉장히 바쁜 상태입니다.
프리징(Hang)을 찾았을 때는 Main Thread의 CPU 사용량을 확인해야 합니다:
사용량이 너무 많은 경우
사용량이 너무 적은 경우
나머지 대부분의 스레드들은 일반적인 워커 스레드(generic worker threads)입니다. 여기서 윈도우 서버(Window Server)로부터 입력 이벤트를 가져오는 스레드를 찾아봐야 합니다.
스레드를 보면 ImageViewer 외에 다른 하나가 눈에 띕니다. com.apple...로 줄임말로 되어있는 부분을 확대해보겠습니다:

com.apple.uikit.eventfetch-thread라고 되어있네요!
이것이 윈도우 서버(화면)로부터 입력 이벤트를 가져오는 역할을 하는 스레드입니다. 사용자 입력을 윈도우 서버로부터 수신하여 앱의 메인 스레드에 전달하는 중요한 연결고리 역할을 합니다.
여기서 헷갈리는 점이 있습니다: 사용자 입력을 받는 건 Main Run Loop라고 했는데, 왜 다른 스레드에서 입력을 받아서 메인 스레드로 전달하는 걸까요?
저도 이번에 정확히 알게 된 내용입니다. 공식 문서에서는 찾지 못했지만, 다음과 같은 흐름으로 동작한다고 합니다:
Hardware (터치스크린)
↓
IOKit/IOHIDFamily (커널 레벨)
↓
com.apple.uikit.eventfetch-thread (이벤트 수신)
↓
Event Queue
↓
Main Thread (이벤트 처리)
즉, Main Thread는 이벤트의 "처리"만 담당합니다.
그 이유는 Main Thread가 바쁠 경우 입력에 대한 누락이 생길 수 있기 때문입니다.
틀린 부분이 있다면 댓글 남겨주세요!
❌ 만약 메인 스레드가 직접 받는다면:
// 메인 스레드가 바쁠 때
메인 스레드: UI 업데이트 중... (0.1초)
Window Server: "터치 이벤트 보낼게!"
메인 스레드: 바빠서 못 받음 ❌
→ 이벤트 유실 또는 지연
✅ eventfetch-thread를 사용하면:
eventfetch-thread: 항상 대기 중 ✓
Window Server: "터치 이벤트!"
eventfetch-thread: 즉시 받아서 큐에 저장 ✓
메인 스레드: UI 업데이트 끝나면 큐에서 꺼내서 처리

스레드 vs 프로세스 CPU 사용량:
하나의 프로세스는 여러 개의 병렬 스레드를 가질 수 있기 때문에, 프로세스 트랙에 표시되는 전체 CPU 사용량은 100%보다 훨씬 높을 수 있습니다.
예시: CPU 사용량이 200%라면, 해당 프로세스가 모든 스레드를 통해 CPU 코어 2개에 해당하는 자원을 사용하고 있다는 의미입니다.
이제 Main Thread가 바쁘다는 것을 알았으니, 구체적으로 무엇이 문제인지 파악해봅시다.
아래의 상세 영역을 봅시다:

이 영역은 Call Tree와 Extended Detail Area로 구성되어 있습니다.
이 영역은 어떤 트랙을 선택했는지에 따라 달라집니다.

엄청난 Call Tree입니다! 아마 여기서 많은 분들이 해석을 포기하실 텐데요... 왜냐하면 제가 그랬거든요. 😅
이렇게 많은 이유: Time Profiler가 앱의 실행 흐름 전체를 기록하기 때문입니다. 호출 트리의 최상단은 앱이 실행되는 가장 기본적인 시작 함수들로 구성됩니다.

'가장 무거운 스택 추적(The Heaviest Stack Trace)'은 성능 분석 도구(Instruments)의 핵심 기능입니다:
호출 트리의 요약을 보여주며, 수천 개의 노드를 포함하는 전체 호출 트리를 하나하나 걸러낼 필요 없이 관련 있는 프레임(함수 호출)을 식별해 줍니다.

Instruments는 함수를 색상으로 구분하여 개발자가 어디에 집중해야 할지 알려줍니다:
기본 텍스트 색상
옅은 회색
ThumbnailView.body.getter 행을 선택하면 관련 정보가 나타납니다:

분석 결과:
이 부분에서 성능 문제의 원인이 발생했음을 명확히 알 수 있습니다!
성능 개선 방법 및 구체적인 문제 파악은 다음 시간에 이어서 하겠습니다.