실기기의 프로파일은 누군가 필요하다고 생각했을 때만 취득되는 경우가 대부분입니다. 출시 전 막바지에 누군가 Profiler를 연결해 스파이크나 메모리 증가를 발견하고 원인 커밋을 찾는 흐름에서는, 언제부터 나빠졌는지를 찾는 범위가 몇 주 분량의 차이가 되기 쉽습니다. 이 장에서는 데일리 빌드마다 실기기에서 계측을 자동으로 수행하기 위한 구성을 다룹니다.
구성은 세 가지 부품으로 나뉩니다. 실기기를 대여하는 디바이스 팜, 앱에 내장되어 외부로부터 조작을 받는 원격 조작 서버, 계측 데이터를 서버로 보내는 계측 SDK입니다. 디바이스 팜에서 원격으로 실기기를 빌리고, 원격 조작으로 게임 화면을 차례로 순회하면서, 계측 SDK로 메모리 스냅샷과 Profiler 캡처를 전송합니다. 이를 데일리 빌드의 후속 잡으로 CI에 올리면 빌드가 나올 때마다 커밋 단위의 계측 데이터가 쌓입니다. 쌓인 데이터는 커맨드 라인에서 가져올 수 있으므로, AI 에이전트에게 차이를 읽게 하여 수정안까지 진행하는 입구가 되기도 합니다.
이 장의 내용은 사내 프로젝트에서 운용하고 있는 데일리 순회 워크플로와, 거기에 이르기까지 궁리한 점을 해설합니다. 집필 시점에 대상으로 하는 것은 Android 기기뿐이며, iOS 대응은 앞으로 예정하고 있습니다.
파이프라인은 그림 2.1의 순서로 진행됩니다. 기기 확보, 앱 조작, 계측 데이터 전송은 각각 별도의 도구가 담당합니다. 어느 하나를 다른 도구로 교체하더라도 이 역할 분담은 그대로 사용할 수 있습니다.
그림 2.1: 데일리 계측 파이프라인의 단계
데일리 빌드 자체는 건드리지 않습니다. 데일리 빌드에 순회용 시나리오 내장이나 사전 설정된 계측 코드는 필요하지 않으며, 이러한 계측을 염두에 둔 코드를 두지 않음으로써 내부 구성이 크게 변하는 개발 단계에서도 순회가 깨지지 않고 안정성과 유연성을 유지할 수 있었습니다. 그 방법은 아래에서 자세히 설명합니다.
데일리 빌드가 완료되면 빌드 완료 트리거를 다른 워크플로가 구독하고, 성공했을 때만 순회를 기동합니다. 이때 후속 워크플로에 전달하는 것은 APK를 빌드한 run의 ID입니다.
기동된 워크플로는 run의 ID로 빌드 아티팩트를 특정하여 다운로드합니다. 그 후 순회를 담당하는 LLM 에이전트를 기동합니다. 집필 시점에는 모델로 Claude Opus 5.5를 사용하고, 추론의 깊이를 정하는 effort는 medium으로 설정하고 있습니다.
이 장에서 자동화하는 것은 Unity 측의 계측입니다. 어떤 에셋이 메모리를 차지하고 있는지, 어떤 Profiler 마커가 무거운지 같은 Unity 내부의 내역을 매일 같은 형태로 취득해 비교하는 것이 목적입니다.
기기 외부에서 사용할 수 있는 계측 수단에도 물론 유용한 것이 많습니다. iOS라면 Instruments의 Time Profiler로 콜 스택 단위의 병목을 추적할 수 있습니다. Android에서도 simpleperf나 이를 사용하는 Android Studio의 CPU Profiler로 IL2CPP가 생성한 네이티브 함수 단위까지 샘플링할 수 있습니다. 이러한 도구를 자동화에 활용하는 것도 충분히 생각할 수 있지만, 이번 범위에서는 다루지 않습니다.
Unity의 내역은 평소 Unity 에디터의 Profiler나 Memory Profiler를 실기기에 연결하여 취득합니다. 다만 CI 순회에 에디터를 끼우면 고려할 것이 단번에 늘어납니다. 러너에서 프로젝트를 열 때마다 Library 폴더의 임포트를 기다려야 하고, 러너마다 Unity 라이선스도 필요합니다. 게다가 에디터와 실기기의 연결에 더해 에이전트가 에디터를 조작하는 Unity MCP와 같은 경로도 준비해야 하므로 구성이 복잡해집니다.
그래서 CI에서는 에디터를 사용하지 않고 런타임 API만 사용하여 애플리케이션 안에서 계측을 완결하기로 했습니다.
디바이스 팜에 사용하는 것은 브라우저에서 실기기를 조작할 수 있는 STF라는 기반입니다*1. 다만 이 장에서 사용하는 것은 화면을 통한 디바이스 팜 조작이 아니라 API를 통한 기기 확보뿐이며, 기기를 예약하면 원격 adb 엔드포인트가 반환되어 CI 러너에서는 USB 연결과 동일하게 다룰 수 있습니다. 예약부터 APK 설치까지를 스크립트 하나로 묶어 CI에서는 한 단계로 호출합니다.
순회만이 목적이라면 Android 10 이상의 비어 있는 기기가 한 대 있으면 충분하지만, 계측을 날짜별로 비교하고 싶다면 기기가 매일 바뀌어서는 비교가 되지 않습니다. 계측용으로는 기종을 고정하고, 빈 기기가 없으면 그날은 건너뛰는 운용이 비교할 수 없는 데이터를 쌓지 않을 수 있어 좋습니다.
앱 조작에는 앱에 내장한 원격 조작 서버를 사용합니다. 앱이 시작되면 기기 안에서만 명령을 대기하고, CI 러너에서는 adb를 경유해 연결하여 명령을 보냅니다. 앱이 동작 중인지 확인, 씬 상의 오브젝트 구성 취득, 스크린샷 촬영, 로그 취득과 같은 정해진 명령 외에, JavaScript로 작성한 임의의 처리를 앱 안에서 실행할 수 있습니다.
이 JavaScript는 Jint*2로 해석되어 C#의 타입에 직접 접근합니다. UI 버튼을 이름으로 찾아 누르는 것도, 내비게이션의 현재 노드를 읽는 것도, 계측 SDK를 시작하는 것도 모두 같은 경로로 수행할 수 있습니다. 실기기 위에서 C# 코드를 즉석에서 작성해 시험할 수 있는 환경에 가까우며, 계측 SDK의 API도 다시 빌드하지 않고 외부에서 호출할 수 있습니다. 임계값이나 대상 화면을 바꿀 때마다 APK를 다시 만들 필요가 없습니다.
계측을 매일 비교하려면 매일 같은 화면을 같은 순서로 지나야 합니다. 고정 좌표 탭은 기기의 해상도나 레이아웃이 바뀌면 누르는 위치가 어긋나므로, 런타임 UI 오브젝트를 이름으로 취득하여 UI 프레임워크의 이벤트를 직접 발생시킵니다. 현재 어느 화면에 있는지는 내비게이션 상태를 읽어 판정합니다. 프로젝트 측에 화면 전이 그래프의 정의가 있다면 그것을 그대로 순회 계획의 입력으로 사용할 수 있습니다.
계측 데이터 전송에는 필자의 소속 조직에서 개발하고 있는 계측 기반을 사용합니다. 앱에 내장한 SDK가 계측 데이터를 서버로 보내고, 서버 측에서 축적하여 대시보드에서 날짜별 비교나 악화 시의 알림을 내는 구조입니다.
SDK가 보낼 수 있는 것은 Unity의 메모리 스냅샷이나 Profiler 캡처만이 아닙니다. CPU 시간의 내역이나 드로 콜 수 등의 렌더링 부하, 코드 중 임의 구간의 처리 시간, 앱의 시작 시간, 일정 간격으로 취득한 메모리 사용량의 추이 등도 보낼 수 있으므로, 순회에 포함하면 마찬가지로 정점 관측의 대상으로 삼을 수 있습니다. 이 장에서는 그중 표 2.1의 두 종류로 좁혀 설명합니다.
표 2.1: 데일리 순회에서 전송하는 계측 데이터
| 종류 | 취득 시점 | 전제 | 전송량에 대한 기준 |
|---|---|---|---|
| 메모리 스냅샷 | 각 화면에 도달해 안정된 후 | Development Build | 화면마다 1장, 모두 전송 |
| Profiler 캡처 | 순회 시작부터 종료까지 1개 | Profiler가 활성화된 빌드 | 임곗값을 넘은 프레임만 전송 |
메모리 스냅샷은 Unity의 Memory Profiler와 같은 형식이며, Unity의 사양상 Development Build에서만 취득할 수 있습니다*3. 데일리 빌드의 순회용 APK는 Development Build로 만들어야 하며, 릴리스 설정의 빌드를 계측에 전용할 수는 없습니다.
스냅샷은 순회가 화면에 도달하여 로드가 안정된 시점에 한 장 취득합니다. 이름에는 화면 이름과 커밋 해시를 넣어, 나중에 같은 화면의 다른 날 데이터를 나란히 놓을 수 있게 합니다.
Profiler 캡처는 Unity가 출력하는 원시 Profiler 로그를 SDK가 기기 위에서 읽어 나가며, 조건에 맞는 프레임만 별도 파일로 출력하여 업로드하는 것입니다. 전체 프레임을 그대로 남기지 않는 것은 Profiler가 장시간 기록에 적합하지 않기 때문입니다. 에디터의 Profiler 창이 유지할 수 있는 것은 최대 2,000프레임이며, 60fps라면 30초 남짓입니다. 오래된 프레임부터 버려지므로 수십 분의 순회를 에디터에 연결해 기록해도 남는 것은 마지막 30초 남짓입니다. 그때그때 파일로 출력하면 프레임 수의 상한은 없어지지만, 이번에는 파일이 순회 1회분에 수십GB로 부풀어 오릅니다. CI 아티팩트로 남기기에도, 매일 쌓기에도 너무 큽니다.
캡처는 순회 처음에 시작하고, 마지막에 정지와 업로드를 한꺼번에 수행합니다. 시작 시에 정하는 것은 어떤 프레임을 남길지의 임계값이며, 프레임 시간과 GC 할당량의 두 가지로 지정합니다. 정지는 원시 로그의 읽다 남은 부분을 소화한 후에 완료됩니다. 걸러내기는 계측 중에도 병행하여 진행되므로, Deep Profiling처럼 출력이 빠른 경우에도 읽다 남은 부분은 거의 쌓이지 않습니다. 그래도 정지가 끝나기 전에 기기를 반납하지 않도록, 정지는 순회 마감으로부터 여유를 두고 시작합니다.
원시 로그는 스레드별 버퍼를 블록 단위로 차례로 덧붙여 쓴 것입니다. 어느 블록이 어느 프레임의 것인지 알려면 프레임의 구분을 찾아야 합니다. 단서가 되는 것은 Profiler의 출력 방식입니다. Profiler는 프레임 구분을 쓰기 직전과 직후에 스레드 버퍼를 전환하므로, 구분은 십여 바이트뿐인 단독 블록으로 도착합니다. 그래서 걸러내기 스레드는 이 길이 이하의 블록만 열어 구분인지 확인합니다. 그보다 긴 블록은 내용을 읽지 않고 그 스레드의 현재 프레임에 속하는 것으로 취급합니다. 메인 스레드에서 다음 프레임의 구분이 도착하면 프레임의 길이가 정해지므로, 거기서 남길지 여부를 판정하고, 남길 프레임의 블록만 원래 순서대로 출력합니다. 샘플러 이름이나 스레드 이름처럼 어느 프레임에도 속하지 않는 정의 블록은 모두 출력하므로, 출력은 Unity의 Profiler 창에서 그대로 열 수 있습니다.
GC 할당량은 원시 로그에서 얻을 수 없습니다. 원시 로그의 프레임 통계에도 GC 항목이 있지만, Unity 2022.3부터 6000.5까지 확인한 어떤 캡처에서도 0이었습니다. 그래서 계측 중에는 메인 스레드에서 매 프레임 ProfilerRecorder로 GC Allocated In Frame 카운터를 읽습니다. ProfilerRecorder의 값에는 시각이 붙지 않으므로, 그 프레임 안에서 ProfilerUnsafeUtility.Timestamp을 읽어 값에 덧붙여 둡니다. 이 시각은 원시 로그의 프레임 구분에 기록되는 시각과 같은 시계입니다. 걸러내기 스레드는 어떤 프레임의 시작부터 다음 프레임의 시작 사이에 기록된 값을 그 프레임의 GC 할당량으로 대조합니다. 어떤 프레임의 값을 얻을 수 있는 것은 다음 프레임 안이므로, 값이 도착할 때까지 몇 프레임 분은 판정을 기다립니다.
기기 위에서 걸러내기를 수행하면 그 자체가 계측을 무겁게 하지 않을지 걱정될 수 있습니다. 걸러내기는 전용 스레드에서 수행하며, 메인 스레드에서 하는 것은 데이터 증가 상황 확인과 GC 카운터 읽기뿐입니다. 앞서 설명한 대로 대부분의 블록은 내용을 읽지 않으므로 걸러내기에 드는 처리는 극히 일부의 데이터로 한정됩니다. 걸러내기 스레드와 Profiler 데이터를 출력하는 Dispatcher 스레드는 스레드 이름으로 구별하여 출력에서 제외하고 있습니다. 또한 걸러내기 처리는 Deep Profiling 대상에서 제외하고 있습니다. 대상에 포함하면 걸러내기 스레드의 메서드 호출 하나하나가 샘플이 되어, 마침 읽고 있는 원시 로그에 기록되어 버리기 때문입니다. 다만 GC 카운터는 프로세스 전체의 값이므로, 걸러내기 스레드가 할당하는 프레임당 수 KB도 포함됩니다. 후술하는 임계값과 비교하면 충분히 작은 양이지만, Profiler를 활성화하는 것 자체의 부하도 포함하여 계측에 의한 추가 부담이 전혀 없다고는 할 수 없습니다.
필자의 순회에서는 현재 300ms・2MB를 임계값으로 하고 있습니다. 30fps 타이틀이라면 통상의 약 10배 프레임 시간이며, 플레이 중의 사소한 끊김이 아니라 화면 전환이나 로드에서 발생하는 큰 정지가 어제보다 늘지 않았는지를 보기 위한 설정입니다. 이 설정이라면 순회 전체 중 남는 것은 극히 일부 프레임뿐이며, 매일 계속 보낼 수 있는 양에 들어갑니다. 임계값을 낮추면 사소한 끊김도 잡을 수 있지만, 남는 프레임이 늘어 파일이 커지고 후술하는 출력 상한에도 빨리 도달합니다. 대상 타이틀의 프레임 레이트와 계측으로 해결하고 싶은 문제에 맞춰 데이터 양을 보면서 임계값을 조정하여 운용하시기 바랍니다.
순회가 길어지면 원시 로그의 저장 위치도 문제가 됩니다. SDK는 원시 로그의 출력 위치를 일정 크기마다 전환하여 연번 파일로 나누고, 걸러내기 스레드는 읽기를 마친 파일부터 삭제합니다. 이로써 기기의 스토리지 사용량은 몇 파일 분에서 상한에 도달합니다. 걸러내기가 늦어져 미처리 파일이 너무 쌓였을 때는 기록을 중지합니다. 파일을 전환하면 Unity는 새 파일의 선두에 그때까지 등장한 샘플러 이름이나 오브젝트 이름의 정의를 모두 다시 씁니다. 출력은 하나의 파일로 이어 붙이므로 두 번째 이후의 정의는 대부분 중복입니다. 그래서 직전에 쓴 것과 바이트 단위로 동일한 정의는 출력에서 생략합니다. 출력 파일에는 1.5GB의 상한을 두고, 다음에 남길 프레임에서 상한을 넘는 시점에 기록을 중지합니다. 업로드할 수 있는 것은 2GB까지이므로 그 직전에 멈추어, 상한까지 남긴 프레임은 확실히 보낼 수 있게 합니다.
캡처에는 각 프레임의 스크린샷도 포함시키고 있습니다. Profiler의 수치만으로는 스파이크가 발생했을 때 어느 화면에서 무엇이 표시되고 있었는지 알기 어렵기 때문입니다. 화면은 비동기 GPU 리드백으로 취득하므로 이미지를 얻는 것은 몇 프레임 후입니다. 그래서 마지막으로 얻은 이미지를 매 프레임 끝에 다시 포함시켜, 어느 프레임을 남기더라도 이미지가 붙도록 하고 있습니다. Unity 6.6부터는 Profiler의 표준 기능으로도 스크린샷을 연속으로 기록할 수 있게 되었고, 기본적으로 스크린샷 촬영 기능이 활성화되어 있습니다. 다만 촬영은 15프레임에 1회로 고정이며, 필자의 Android 기기에서 기록된 이미지는 256×112픽셀뿐이어서 UI 문자를 읽을 수 없었습니다. 또한 AsyncGPUReadback이 활성화되지 않은 기기에서는 선택지가 없고, 조용히 스크린샷이 비활성화되었습니다. 300ms・2MB로 걸러낸 어느 날의 캡처에서는 남은 71프레임 중 표준 스크린샷이 붙어 있던 것은 6프레임뿐이었으며, 남기는 프레임이 띄엄띄엄해지는 걸러내기와는 궁합이 좋지 않으므로 SDK 측의 촬영을 계속 사용하고 있습니다.
캡처 업로드 시 덧붙이는 메타데이터 외에, SDK에는 모든 계측 데이터에 공통되는 정보를 붙이는 구조가 있습니다. 빌드 시 포함한 커밋 해시, 빌드의 run ID, 기기 모델을 초기화 직후에 등록해 두면 스냅샷에도 캡처에도 같은 정보가 붙어, 조작 측에서 매번 전달할 필요도 없어집니다. 계측 결과를 커밋 순서로 나열할 수 있는 것은 이 등록이 있기 때문입니다.
순회는 매일 게임의 주요 화면을 한 차례 지나므로 셰이더 웜업에 사용할 데이터를 수집하는 자리가 되기도 합니다. GraphicsStateCollection은 실행 중에 사용된 셰이더 베리언트와 렌더링 상태의 조합을 기록해 두어, 다음번 이후 사전에 웜업할 수 있게 하는 에셋입니다.
수집에는 계측 SDK를 사용하지 않고 원격 조작에서 Unity API를 직접 호출합니다. 순회 시작 시에 기록을 시작하고, 평소처럼 화면을 순회한 뒤, 종료 시에 기록을 멈추고 기기 위 앱의 저장 영역에 파일로 출력합니다. 출력한 파일은 adb로 CI 러너에 꺼냅니다. 에디터나 Player 연결은 사용하지 않으므로 계측과 마찬가지로 CI 안에서 완결됩니다. 다만 IL2CPP 빌드에서는 코드 스트리핑으로 사용되지 않는 타입의 멤버가 제거되어 원격 조작에서 호출할 수 없게 되는 경우가 있습니다. 순회용 빌드에서는 link.xml로 GraphicsStateCollection 타입을 남겨 두어야 합니다.
꺼낸 파일을 그대로 프로젝트에 넣지는 않습니다. 순회가 오류로 멈추지 않고 끝나 파일 저장까지 마친 날에만, 같은 플랫폼과 품질 설정의 기존 에셋에 합산하고, 웜업 시 찾을 수 없다고 경고된 베리언트를 제거한 뒤 에셋 갱신 PR을 자동으로 만들어 그대로 자동 머지합니다. 매일의 순회에서 지난 화면의 기록이 이렇게 조금씩 쌓여 갑니다.
그렇다고 해서 이 방법만으로 필요한 베리언트를 모두 망라할 수 있는 것은 아닙니다. 기록되는 것은 순회가 실제로 지난 화면과 그때 렌더링된 것뿐입니다. 순회에서 지나지 않는 화면이나 특정 조건에서만 나타나는 이펙트 등은 기록에서 누락됩니다. 이러한 누락이나, 웜업했는데도 스터터가 남는 등의 함정에 대해서는 제1부 6장을 참조하시기 바랍니다. 이 책을 처음부터 순서대로 읽어 여기까지 오신 분은 이미 읽으셨을 수도 있습니다.
매일 데이터가 쌓여도 매일 아침 대시보드를 여는 사람이 없으면 쌓이기만 하고 끝납니다. 이 계측 기반에는 커맨드 라인에서 데이터를 취득하는 CLI도 있으며, 이 장의 파이프라인에서는 악화 확인부터 수정 PR 작성까지를 이 CLI를 사용하는 에이전트에게 맡기고 있습니다. 사람이 매번 의뢰를 작성하는 것이 아니라, 계측 뒤에 이어지는 공정으로서 매일 돌립니다.
순회의 계측이 끝나면 순회와는 별개의 비교용 에이전트가 기동됩니다. 이 에이전트는 CLI로 오늘과 전날의 계측 데이터를 취득하고, 같은 화면의 스냅샷이나 캡처에 남은 스파이크를 비교하여 악화된 부분이 없는지 확인합니다. 악화가 발견된 경우 어느 화면의 어느 값이 얼마나 변했는지, 비교에 사용한 데이터, 그 사이에 들어간 커밋의 범위를 정리하여 백로그에 이슈로 등록합니다. 악화가 없으면 아무것도 등록하지 않습니다.
등록된 이슈는 다시 별개의, 수정을 담당하는 에이전트가 가져갑니다. 이슈의 내용을 확인하고, 커밋 이력에서 원인을 좁힌 뒤 수정을 가하여 PR을 만듭니다. 사람이 관여하는 것은 그 PR의 리뷰부터입니다.
수정 확인은 데일리 순회와는 분리하여 수행합니다. 데일리 순회는 정해진 화면을 정해진 순서로 순회하는 것이며, 이슈마다 확인하고 싶은 화면이나 조건에 맞추기에는 적합하지 않기 때문입니다. 그래서 백로그에서 이슈가 수정 완료(확인 대기) 상태가 된 것을 계기로 수정 확인용 에이전트가 동작합니다. 이 에이전트는 디바이스 팜의 기기, 앱 내 원격 조작, 계측 SDK라는 데일리 순회와 같은 구조를 사용하여 수정을 반영한 빌드를 실기기에서 실행하고, 이슈에 남긴 비교 대상 데이터와 대조합니다. 악화가 해소되었음을 실기기에서 확인한 후에야 비로소 티켓을 QA에 넘깁니다. 수정이 효과가 있었는지 확인하기 위해 개발자가 실기기를 만질 필요는 없습니다.
이 확인 구조는 퍼포먼스 악화에 한정되지 않고 평소의 버그 수정에도 사용할 수 있습니다. 앞서 설명한 대로 원격 조작에서는 사실상 리플렉션과 마찬가지로 직접 접근할 수 있는 처리를 즉석에서 실행할 수 있으므로, 에이전트는 게임 안에서 일어나는 거의 모든 것을 조작하고 읽을 수 있습니다. 이슈에 재현 절차와 기대 결과가 적혀 있다면, 그 절차를 실기기 위에서 따라가 게임 내부 상태까지 포함하여 수정이 되었는지 확인할 수 있습니다.
이 장의 순회는 코딩 에이전트에게 화면 전이 그래프와 원격 조작 사용법을 전달하여 순회시키고 있습니다. 도달할 수 없는 화면을 회피하고, 새로운 화면이 늘면 계획을 다시 짤 수 있는 유연성이 있는 반면, 1회 순회에 90턴 안팎, 비용으로 5달러 안팎이 듭니다. 계측의 시작과 정지처럼 매번 같아야 하는 부분은 에이전트의 판단에 맡기지 않고 재사용 가능한 절차로 고정하여, 에이전트에게는 그것을 정해진 타이밍에 실행하게 할 뿐입니다. 순회의 학습 내용도 다음번 입력으로서 리포지토리에 PR로 남기고 있습니다.
순회 결과는 리포트, 스크린샷, 로그, 기계가 읽을 수 있는 결과 파일로 남기고, CI가 결과 파일을 읽어 Slack으로 알립니다. 계측 자체의 결과는 대시보드에 있으므로 알림에는 업로드의 성공 여부와 대시보드 링크가 있으면 충분합니다. 비교에서 악화가 발견된 날은 등록한 이슈 링크를 붙여 같은 채널에 알립니다. 반대로 업로드가 없는 날은 계측되지 않았다는 사실도 알림에 포함합니다. 계측이 모르는 사이에 멈춰 있는 상태가 가장 발견하기 어렵기 때문입니다.
스냅샷과 캡처를 같은 순회 안에서 취득하고 있으므로 두 데이터가 서로 섞입니다. 스냅샷 취득 중에는 몇 초간 프레임이 멈추므로, 동시에 실행 중인 캡처에는 스파이크로 기록됩니다. 반대로 캡처 중에는 Profiler 버퍼의 메모리가 스냅샷에 포함되므로, 데이터를 읽을 때는 주의가 필요합니다. 데이터를 정확하게 보고 싶은 경우 스냅샷용과 캡처용으로 순회를 나누는 것도 선택지이지만, 기기를 빌리는 시간이 대략 두 배가 되므로 빈도를 낮추는 등의 절충이 필요합니다.
Profiler 캡처의 입도에도 과제가 있습니다. 데일리 계측에서는 Deep Profiling을 끄고 있으므로 게임 측 스크립트는 MonoBehaviour의 Update과 같은 단위로만 보입니다. 그보다 세밀하게 보고 싶은 부분에는 ProfilerMarker를 수동으로 추가하거나 Deep Profiling을 활성화해야 합니다. 그래서 지정한 어셈블리의 메서드에 빌드 시 후처리로 ProfilerMarker를 자동 삽입하는 방법 등을 모색하고 있습니다. 대상을 좁힌 가벼운 Deep Profiling처럼 사용할 수 있으므로, 데이터 양과 부하를 억제한 채 내역을 세밀하게 취득할 수 있게 될 것으로 생각합니다.
데일리 빌드 뒤에, 디바이스 팜에서 기기를 빌리고, 앱 내 원격 조작으로 게임을 실행하며, 계측 SDK에 메모리 스냅샷과 필터링된 Profiler 캡처를 보내는 잡을 연결했습니다. 커밋 해시를 메타데이터에 덧붙임으로써 계측 결과는 커밋 단위의 이력이 되며, CLI로 꺼내면 악화 검출부터 이슈 등록, 수정 PR 작성까지를 AI 에이전트에게 맡길 수 있습니다.
이 장의 구성은 대규모로 보일 수 있지만, 모든 것을 한 번에 갖출 필요는 없습니다. 앞으로 계측 자동화를 계획하고 계신다면, 우선 빌드 머신에 실기기를 한 대 USB로 연결하고, adb 너머로 리플렉션으로 동작하는 인터프리터에서 메모리 스냅샷을 취득하는 것만으로도 훌륭한 메모리 정점 관측이 될 수 있습니다. 이 장이 여러분의 계측 자동화 첫걸음에 도움이 되기를 바랍니다.
[*1] DeviceFarmer/stf(OpenSTF의 후속 리포지토리) https://github.com/DeviceFarmer/stf
[*2] sebastienros/jint, JavaScript Interpreter for .NET https://github.com/sebastienros/jint
[*3] Unity Manual, Memory Profiler package https://docs.unity3d.com/Packages/com.unity.memoryprofiler@1.1/manual/index.html