Mac mini M4에서 모바일 CI를 돌리는 팀은 CPU 그래프가 여전히 비어 보일 때 병렬 xcodebuild 작업을 올리곤 한다—그러다 큐 p95가 치솟고 각 프로세스는 CPU가 40% 안팎이라고 보고한다. 이 2026 매트릭스는 컴파일 한계 레인과 시뮬레이터·IO 한계 레인을 분리하고, -destination 선택이 통합 메모리에서 어떻게 충돌하는지 정리하며, 오케스트레이터 YAML 옆에 붙일 수 있는 아홉 단계와 수치 가드레일을 제공한다.
병렬 시뮬레이터 worker·메모리 스로틀 매트릭스와 Mac mini M4 UI 테스트 샤딩을 함께 읽어 세 가지 “병렬 손잡이”가 같은 DRAM 예산을 놓고 싸우지 않게 한다. 디스크 산출물이 동시성보다 빨리 자라면 Xcode를 탓하기 전에 디스크 보존 매트릭스와 대조한다.
Apple Silicon CI에서 CPU 퍼센트가 거짓말하는 이유
통합 메모리는 GPU, Neural Engine, CPU 컨트롤러가 같은 물리 풀을 공유함을 뜻한다. CoreSimulator 프레임버퍼 합성이나 Metal 파이프라인 준비에 시간을 쓰는 레인은 CPU가 낮아도 Swift 모듈 컴파일에 필요한 메모리 대역을 형제 작업에서 빼앗는다. APFS copy-on-write는 착시를 키운다: 시뮬레이터 데이터셋을 반복 삭제·재생성하면 메타데이터 I/O가 치솟는데 “CPU 바쁨” 차트는 거의 움직이지 않는다.
- 공유 CoreServices 캐시: 여러 destination이 같은 헬퍼 데몬과 잠금 보호 캐시를 친다.
- 부팅 폭풍: 네 대 동시 기동은 겉으로 멀쩡해 보이다가 SpringBoard 워치독이 한 레인을 조용히 재시작한다.
- Metal 경합: UI 스냅샷과 가벼운 GPU 테스트도 같은 GPU 슬라이스의 컴파일 타임 셰이더 캐시와 경합한다.
운영 규칙: 두 destination이 같은 udid 계열과 같은 DerivedData 슬라이스를 공유한다면 격리가 아니라 레이스다.
매트릭스 A — 레인 유형과 M4 메모리 등급의 안전 병렬도
| 레인 유형 | 24 GB 등급 | 36–48 GB 등급 | 비고 |
|---|---|---|---|
| Swift 컴파일만 | 무거운 2 | 무거운 3 | 작업별 DerivedData 루트를 쌍으로 둔다. 컴파일러 프런트엔드 메모리 스파이크를 본다. |
| 단위 테스트(UI 없음) | 2 | 3 | 시뮬레이터 할당은 여전히 발생—순수 컴파일 대비 “공짜”가 아니다. |
| XCUITest + 스크린샷 | 1 | 2 | GPU와 디스크가 지배. 플릿 확장은 샤딩 가이드를 따른다. |
매트릭스 B — 증상, 유력 병목, 1차 완화
| 증상 | 유력 병목 | 완화 |
|---|---|---|
무작위 DTXProxy 끊김 |
시뮬레이터 IPC + 메모리 압력 | 병렬 UI 레인을 줄이고 스위트 사이에 종료 훅을 추가한다. |
| UI 작업과 동시에 컴파일이 느려짐 | 통합 메모리 대역폭 | 레이블로 컴파일·UI 풀을 나누고 UI는 비피크에 배치한다. |
| 여유 60%인데 빌드가 기어감 | APFS 단편화 / inode 요동 | 주간으로 작업 로컬 볼륨을 순환. 거대 공유 DerivedData를 피한다. |
오케스트레이터용 수치 가드레일
- 호스트당 UI 상한: 24 GB 통합 메모리 호스트에서 프로파일이 부팅 후 최소 6 GB 회수 가능 헤드룸을 보이지 않는 한 동시 XCUITest destination은 1.
- 컴파일 팬아웃: 시뮬레이터와 혼합 시
-parallel-testing-enabled워커 수를 물리 성능 코어의 절반으로 제한한다. - 디스크 워터마크: CI 볼륨 APFS 여유가 18% 아래로 떨어지면 새 병렬 destination을 멈춘다—그 아래에서는 CoreSimulator 생성이 불투명하게 실패한다.
용량 링크: 매트릭스가 한 호스트를 넘어설 때, 디스크만 키우기 전에 풀 용량 SLO에 동시성을 매핑한다.
코드 리뷰를 통과하는 호출 패턴
xcodebuild 인자를 API 계약처럼 다룬다: 작은 셸 함수나 YAML 앵커로 감싸 모든 레인이 -derivedDataPath, -clonedSourcePackagesDirPath, -resultBundlePath를 일관되게 설정하게 한다. 엔지니어가 CI에 일회성 명령을 붙여 넣으면 포화 회귀와 설정 드리프트의 상관이 사라진다. 금요일 밤과 월요일 아침에 다르게 풀리는 셸 글로빙보다 저장소에 명시적 -destination 블록을 둔다.
매트릭스 빌드는 destination 이름만이 아니라 테스트 클래스나 타깃으로 샤드해 각 샤드가 예측 가능한 GPU·IO 시그니처를 갖게 한다. 샤드 매니페스트와 아티팩트 업로드 정책을 짝지어 실패 샤드가 공격적 정리 전에 xcresult를 내도록 한다. 오케스트레이터가 유효 성능 코어 수와 열 상태를 노출하면 로그에 남긴다. 지속적인 병렬 UI 부하에서 macOS는 조용히 스로틀할 수 있다—팬이 잔잔해도.
중앙 캐시: 가속기인가 IO 폭탄인가
공유 DerivedData나 SwiftPM 캐시는 콜드 빌드를 크게 줄이지만 수십 개 작업이 같은 트리에 메타데이터 쓰기를 직렬화한다. 중앙화한다면 빠른 APFS와 넉넉한 inode, 작업별 하위 폴더 강제로 잠금 폭풍이 팀 간으로 퍼지지 않게 한다. 히트율과 명시적 퇴거 정책을 짝지어—오래된 module map은 미스보다 나쁘다가 수동 purge 전까지 미스터리하게 실패한다.
캐시가 네트워크 스토리지에 있으면 지연 스파이크가 “flaky 시뮬레이터”로 위장한다. 핫 경로는 로컬 NVMe를 우선하고 산출물은 비동기 복제. 어떤 레인이 공유 캐시를 읽고 쓸 수 있는지, 읽기 전용 미러인지 문서화해 보안 검토를 추적 가능하게 한다.
빌드 플랫폼 소유자를 위한 아홉 단계 롤아웃
- 레인 태깅: 오케스트레이터 메타데이터에 컴파일/단위/UI—마법 숫자보다 사람이 읽는 라벨.
- destination 고정: CI에서 시뮬레이터 UDID를 명시. “최신 iPhone” 이름만 쓰지 않는다.
- 측정: 레인 유형별 벽시계를 주간으로 비교. 회귀는 빨간 빌드보다 먼저 온다.
- DerivedData 격리: 레인 ID별. 매트릭스 작업 간 암묵적 공유 금지.
- Metal 부하 상한: UI 테스트가 동영상을 캡처할 때 필요하면 직렬화.
- 시뮬레이터 자동 종료: 샤드마다. 지우기 주기는 스로틀 가이드와 맞춘다.
- IO 지연 알람: 커널 카운터가 없으면 단계 시간 분산 등 대리 지표.
- 에스컬레이션 문서: 스위트 중간 TCC 프롬프트는 VNC로 스팟 확인.
- 지리 확장: 비호환 레인을 한 대의 히어로 러너에 쌓기 전에 홍콩·일본·한국·싱가포르·미국으로 분산한다.
FAQ
xcodebuild -parallelizeTargets는 CI에 도움이 되는가?
멀티 타깃 앱에서는 때로 유효하지만 컴파일러 프런트엔드를 쌓는다—공유 호스트에서 전역 활성화 전에 측정한다.
안정을 위해 시뮬레이터 런타임을 오래된 버전에 고정해야 하는가?
재현성을 위해 핀은 타당하지만 Apple 지원 종료 일정을 추적하고 Xcode 핀과 함께 업그레이드 창을 문서화한다.
병렬 xcodebuild destination을 맞추는 것이 Apple Silicon M4 CI를 정직하게 만드는 방법—칩은 빠르지만 통합 메모리와 APFS 메타데이터가 순진한 병렬을 숨은 큐로 바꾼다. 네이티브 macOS에서 SSH 우선 자동화와 막힌 시뮬레이터를 위한 선택적 VNC는 진지한 모바일 원격 랩 패턴과 맞는다. 홍콩·일본·한국·싱가포르·미국에서 전용 Mac mini M4를 렌탈하면 CapEx 없이 무거운 컴파일과 무거운 UI를 나누고, 한 디스크에 비호환 destination을 쌓는 것보다 물리적 격리가 낫다. 경합이 보이면 요금을 열어 호스트를 추가하고 병렬 손잡이를 맹신하지 않는다.