첫 실행 셰이더 컴파일 스터터를 콜드 캐시에서 재현하는 성능 테스트 자동화

첫 실행 셰이더 컴파일 스터터를 콜드 캐시에서 재현하는 방법을 정리합니다. 엔진·드라이버 캐시를 격리하고 프레임 타임과 컴파일 이벤트를 수집해 빌드·패치 성능 회귀를 판정하는 CI 기준까지 다룹니다.

✓ 복사 완료!
챕터셰이더 프로그래밍 (HLSL/ShaderGraph)
난이도중급
상태● 검토 완료
대상 엔진
Engine Agnostic
최종 업데이트
첫 실행 셰이더 컴파일 스터터를 콜드 캐시에서 재현하는 성능 테스트 자동화

핵심 요약

첫 실행 셰이더 컴파일 스터터는 게임 실행 직후 사용되는 셰이더 변형, PSO, 드라이버 파이프라인 캐시가 준비되지 않아 발생할 수 있습니다. 재현하려면 Unity 2022 LTS 또는 Unreal Engine 5.x 빌드와 Windows 11, DirectX 12 또는 Vulkan, GPU 드라이버 버전을 고정하고 엔진·런타임·드라이버 캐시를 격리해야 합니다.

자동화 테스트는 동일한 입력 경로를 실행하면서 프레임 타임, 셰이더 컴파일 이벤트, PSO 생성 시간, 디스크 I/O를 함께 기록합니다. 콜드 런과 웜 런의 P95·P99 프레임 타임 및 첫 사용 시점의 hitch를 비교해 빌드와 패치의 성능 회귀를 판정합니다.

첫 실행 셰이더 컴파일 스터터는 왜 발생하는가?

셰이더 소스가 패키지에 포함되어 있어도 실행 시점에 모든 작업이 끝나는 것은 아닙니다. 일반적인 렌더링 경로에서는 다음 작업이 서로 다른 계층에서 발생합니다.

계층실행 시점 작업관찰 가능한 현상
셰이더 변형플랫폼, 기능 플래그, 머티리얼 속성에 맞는 variant 선택 또는 컴파일첫 머티리얼 사용 시 CPU 사용량 증가
PSO셰이더와 렌더 상태를 Pipeline State Object로 결합특정 패스 진입 순간 긴 프레임 발생
드라이버 JITHLSL 또는 SPIR-V를 GPU별 네이티브 코드로 변환GPU 드라이버 스레드와 파일 I/O 증가
파이프라인 캐시이전 실행의 결과를 디스크에 저장하고 다음 실행에서 재사용첫 실행만 느리고 두 번째 실행은 정상
동기화렌더 스레드가 컴파일 완료를 기다림프레임 타임 spike와 입력 지연

비동기 셰이더 컴파일을 사용해도 첫 사용 시점에 동기화가 남아 있으면 스터터가 발생할 수 있습니다. 따라서 로그에 셰이더 컴파일이라는 문자열이 있는지만 확인하지 말고 해당 이벤트의 시작·종료 시각과 프레임 타임 spike를 같은 타임라인에서 비교해야 합니다.

콜드 캐시 조건은 어떻게 정의해야 하는가?

콜드 캐시는 단순히 게임 폴더의 캐시 디렉터리 하나를 삭제한 상태가 아닙니다. 테스트 전에 어떤 캐시를 비우고 어떤 캐시는 유지하는지 명시한 콜드 캐시 계약이 필요합니다.

캐시 레이어예시권장 초기화 방법검증 방법
빌드 산출물 캐시Unity ShaderCache, Unreal Engine Derived Data Cache테스트 전용 빌드 이미지 또는 새 산출물 사용빌드 해시와 패키지 해시 기록
런타임 사용자 캐시PSO cache, pipeline cache, 게임별 shader cache전용 사용자 프로필을 삭제하거나 VM 스냅샷으로 복원실행 전 디렉터리 목록과 파일 크기 기록
드라이버 캐시DirectX 12 또는 Vulkan 드라이버 셰이더 캐시전용 테스트 이미지와 드라이버 정책 사용드라이버 버전 및 캐시 초기화 로그 기록
OS 파일 캐시메모리에 남아 있는 패키지 파일재부팅 또는 테스트 VM 스냅샷 복원테스트 시작 시각과 시스템 상태 기록

드라이버 캐시의 실제 위치와 삭제 정책은 GPU 제조사와 드라이버 버전에 따라 달라질 수 있습니다. 공유 개발 PC에서 무작정 시스템 디렉터리를 삭제하지 말고 전용 Windows 계정, 전용 VM 이미지 또는 GPU 테스트 장비를 사용해야 합니다.

또한 콜드 캐시는 절대적으로 아무것도 캐시되지 않은 상태라기보다 테스트에서 통제하기로 합의한 캐시 레이어가 비워진 상태입니다. 이 범위를 문서화해야 팀원이 다른 조건으로 실행한 결과를 같은 콜드 런으로 오해하지 않습니다.

재현 자동화는 어떤 순서로 구현할까?

1단계: 실행 환경과 빌드를 고정한다

다음 값을 테스트 결과에 반드시 포함합니다.

  • 게임 빌드 커밋 해시와 패키지 해시
  • Unity 또는 Unreal Engine 버전
  • 셰이더 컴파일러 버전과 컴파일 옵션
  • Windows 버전
  • GPU 모델, VRAM, 드라이버 버전
  • DirectX 12 또는 Vulkan과 해상도
  • VSync, 프레임 제한, 백그라운드 프로세스 상태
  • 테스트 맵, 시작 위치, 입력 시나리오

셰이더 컴파일 플래그, 최적화 옵션, 렌더링 API가 바뀌면 캐시가 달라질 수 있습니다. 그러므로 결과 파일의 이름에 빌드 해시만 넣지 말고 하드웨어와 렌더링 환경을 함께 기록하는 편이 안전합니다.

2단계: 캐시 초기화 결과를 검증한다

캐시 초기화 스크립트는 삭제 작업만 수행하지 말고 실행 전후의 상태를 출력해야 합니다. 검증 가능한 항목은 다음과 같습니다.

  1. 캐시 대상 경로가 테스트 전용 범위인지 확인합니다.
  2. 초기화 전 파일 수와 총 용량을 기록합니다.
  3. 초기화 후 남은 파일 수와 총 용량을 기록합니다.
  4. 게임 프로세스와 셰이더 컴파일러 프로세스가 종료됐는지 확인합니다.
  5. 초기화 매니페스트와 환경 정보를 결과 폴더에 보관합니다.

아래는 특정 엔진의 내부 경로를 강제하지 않는 PowerShell 오케스트레이터 예시입니다. reset-cache.ps1은 프로젝트와 장비에 맞는 캐시 레이어를 초기화하고 PresentMon 또는 엔진별 프로파일러는 프레임 타임을 수집합니다.

param(
  [string]$GameExe = 'D:/builds/Game/Game.exe',
  [string]$RunId = (Get-Date -Format 'yyyyMMdd-HHmmss')
)

$ResultDir = Join-Path 'D:/perf-results' $RunId
New-Item -ItemType Directory -Force -Path $ResultDir | Out-Null

& 'tools/reset-cache.ps1' -Profile 'cold' -Manifest 'configs/cache-layers.json' `
  -Report (Join-Path $ResultDir 'cache-reset.json')

$FramesPath = Join-Path $ResultDir 'frames.csv'
$PresentMon = Start-Process -FilePath 'tools/presentmon.exe' -PassThru -ArgumentList @(
  '--output_file', $FramesPath,
  '--process_name', (Split-Path -Leaf $GameExe),
  '--timed', '120',
  '--terminate_after_timed'
)
Start-Sleep -Milliseconds 500
$Game = Start-Process -FilePath $GameExe -PassThru -ArgumentList @(
  '-benchmark', '-scenario=shader-hitch', '-log=perf'
)
Wait-Process -Id $PresentMon.Id
if (-not $Game.HasExited) {
  Stop-Process -Id $Game.Id -Force
}
if ($PresentMon.ExitCode -ne 0) {
  throw "PresentMon capture failed with exit code $($PresentMon.ExitCode)."
}

& 'tools/export-engine-events.ps1' -Output (Join-Path $ResultDir 'compile-events.jsonl')
& 'tools/evaluate-run.ps1' -Frames (Join-Path $ResultDir 'frames.csv') `
  -Events (Join-Path $ResultDir 'compile-events.jsonl') `
  -Policy 'configs/stutter-policy.json' `
  -Report (Join-Path $ResultDir 'evaluation.json')

3단계: 결정론적인 게임플레이 경로를 만든다

셰이더 컴파일 스터터는 특정 머티리얼이나 렌더 패스가 처음 등장할 때 나타나는 경우가 많습니다. 자유 플레이로 테스트하면 카메라와 입력이 달라져 결과를 비교하기 어렵습니다.

테스트 시나리오는 다음 조건을 만족해야 합니다.

  • 고정된 시작 세이브와 시작 위치를 사용합니다.
  • 카메라 경로와 입력 타이밍을 녹화하거나 스크립트로 재생합니다.
  • 파티클, 그림자, 포스트 프로세싱, UI 등 주요 렌더 기능을 모두 포함합니다.
  • 로딩 완료 후 동일한 프레임 수 또는 동일한 시간 동안 실행합니다.
  • 첫 5~10초와 실제 플레이 구간을 별도 구간으로 표시합니다.

첫 실행에서만 발생하는 현상을 확인할 때는 초기 로딩 구간을 버리지 않아야 합니다. 반대로 로딩 화면의 의도적인 작업과 플레이 중 hitch를 구분하기 위해 구간별 라벨을 남겨야 합니다.

콜드 런과 웜 런에서 첫 셰이더 사용 시점과 프레임 타임 spike를 비교한 성능 타임라인

어떤 성능 지표를 수집해야 하는가?

프레임 타임 하나만 저장하면 원인 분석이 어렵습니다. 최소한 다음 이벤트와 측정값을 같은 실행 ID로 연결합니다.

측정값의미분석 용도
FrameTimeMs각 프레임의 CPU 또는 Present 기준 시간hitch, P95, P99 계산
ShaderCompileStart·End셰이더 컴파일 구간컴파일 시간과 spike의 상관관계 확인
PSOCreationStart·EndPSO 생성 구간첫 렌더 패스의 지연 확인
PipelineCacheHit캐시 적중 여부콜드·웜 조건 검증
AssetLoadMs에셋과 셰이더 파일 로딩 시간디스크 I/O와 컴파일 분리
GPU UtilizationGPU 사용률CPU 대기와 GPU 병목 구분
Process CPU 및 IO게임과 컴파일러의 자원 사용량외부 프로세스 영향 확인

이벤트에는 최소한 run_id, frame_index, timestamp_ms, thread, shader_key, pso_key, duration_ms 필드를 넣습니다. shader_key와 pso_key는 소스 코드 전문 대신 해시로 저장해 결과 파일 크기와 민감한 정보 노출을 줄일 수 있습니다.

콜드 런과 웜 런은 어떻게 비교할까?

동일한 장비에서 다음 순서로 실행하면 캐시 효과를 비교하기 쉽습니다.

  1. 콜드 캐시 초기화 후 첫 실행을 수행합니다.
  2. 프로세스를 종료하지 않고 동일 시나리오를 다시 실행해 런타임 조건을 확인합니다.
  3. 프로세스를 종료한 뒤 캐시를 유지한 상태로 웜 런을 수행합니다.
  4. 각 조건을 여러 번 반복하고 중앙값과 P95를 함께 저장합니다.

다음과 같은 지표를 사용할 수 있습니다.

StutterRate=N(frameTime>T)/N(allFrames)StutterRate = N(frameTime > T) / N(allFrames)

여기서 T는 팀이 정한 hitch 임계값입니다. 예를 들어 60 FPS 목표에서는 33.3 ms 또는 50 ms를 별도 기준으로 둘 수 있습니다. 특정 임계값 하나만 사용하면 짧은 spike와 지속적인 프레임 저하를 구분하기 어려우므로 P95, P99, 최대 프레임 타임을 함께 확인해야 합니다.

ColdWarmDelta=P95Cold−P95WarmColdWarmDelta = P95Cold - P95Warm

ColdWarmDelta가 크고 같은 프레임 구간에서 셰이더 또는 PSO 이벤트가 관찰되면 콜드 캐시 관련 회귀일 가능성이 커집니다. 다만 이벤트가 있어도 프레임 타임이 증가하지 않으면 사용자 체감 스터터로 판정하지 않고 별도 최적화 후보로 분류합니다.

자동 판정 기준은 어떻게 설정할까?

절대 기준보다 하드웨어별 기준선과 회귀 폭을 함께 사용하는 것이 좋습니다.

판정예시 조건
통과콜드 런의 P99가 기준선 대비 10% 미만 증가하고 50 ms 초과 프레임이 허용 범위 이내
경고P99 증가율이 10~20%이거나 특정 장면에서만 단일 hitch 발생
실패첫 사용 구간의 최대 프레임 타임이 기준선을 크게 초과하거나 반복 실행에서 동일 이벤트가 재현됨
무효GPU 드라이버, 빌드 해시, 해상도, 시나리오 또는 캐시 초기화 검증이 불일치함

수치는 프로젝트의 목표 프레임 레이트와 하드웨어 등급에 맞춰 조정해야 합니다. 30 FPS 콘솔 타이틀과 144 FPS PC 타이틀에 같은 임계값을 적용하면 잘못된 실패가 발생할 수 있습니다.

재현성도 별도 지표로 기록합니다.

ReproductionRate=N(runsmeetingcondition)/N(validruns)ReproductionRate = N(runs meeting condition) / N(valid runs)

재현율이 낮으면 단순히 통과시키지 말고 조건을 세분화합니다. 특정 셰이더 키, 특정 GPU, 특정 렌더 패스에서만 나타나는 확률적 문제일 수 있기 때문입니다.

성능 테스트 파이프라인의 전체 흐름

flowchart LR
  A[빌드와 장비 고정] --> B[캐시 레이어 초기화]
  B --> C[초기화 상태 검증]
  C --> D[결정론적 시나리오 실행]
  D --> E[프레임 타임 수집]
  D --> F[셰이더와 PSO 이벤트 수집]
  E --> G[콜드·웜 비교]
  F --> G
  G --> H[정책 기반 회귀 판정]
  H --> I[로그와 캡처 보관]

CI에서 모든 커밋마다 실제 드라이버 캐시까지 초기화하기 어렵다면 테스트를 두 계층으로 나눌 수 있습니다.

  • 빠른 PR 테스트: 고정된 캐시 매니페스트와 짧은 시나리오로 셰이더 이벤트와 주요 frame spike를 확인합니다.
  • 야간 또는 릴리스 테스트: 전용 GPU 장비나 VM 스냅샷을 사용해 완전한 콜드 런, 여러 반복, 드라이버 조합을 검증합니다.

실패한 실행은 frames.csv, 컴파일 이벤트, 캐시 초기화 보고서, 게임 로그, GPU·드라이버 정보, 평가 결과를 하나의 아티팩트로 묶습니다. 그래야 QA가 재현 조건을 다시 만들고 프로그래머가 특정 shader key나 PSO key까지 추적할 수 있습니다.

자주 발생하는 자동화 오류는 무엇인가?

게임 캐시만 지우고 드라이버 캐시를 유지하는 경우

두 번째 실행처럼 보이는 콜드 런이 만들어집니다. 캐시 레이어별 초기화 여부와 초기화 후 파일 상태를 결과에 남겨야 합니다.

첫 실행의 로딩 시간을 모두 스터터로 계산하는 경우

셰이더 컴파일, 에셋 로딩, 네트워크 초기화, 리소스 압축 해제를 분리하지 않으면 원인과 증상이 섞입니다. 이벤트 타임라인과 프레임 인덱스를 연결하세요.

테스트 장비에서 백그라운드 작업이 실행되는 경우

Windows Update, 바이러스 검사, 클라우드 동기화, GPU 오버레이는 디스크와 CPU를 점유할 수 있습니다. 장비를 전용화하고 실행 전 프로세스 목록과 CPU·GPU 점유율을 기록하는 편이 좋습니다.

동일한 입력을 재생했지만 렌더 결과가 달라지는 경우

난수 시드, 프레임 기반 입력, 네트워크 상태, 셰이더 variant 선택 조건을 고정해야 합니다. 완전한 결정성이 어렵다면 동일 조건을 여러 번 실행하고 재현율을 판정에 포함합니다.

자주 묻는 질문 FAQ

셰이더를 미리 모두 컴파일하면 테스트가 필요하지 않은가?

필요합니다. 사전 컴파일은 셰이더 variant 누락, PSO 생성, 드라이버 JIT, 런타임 캐시 적중 실패를 모두 제거하지 못합니다. 실제 GPU와 렌더링 API에서 첫 사용 경로를 검증해야 합니다.

캐시 디렉터리가 비어 있으면 콜드 캐시로 볼 수 있는가?

아닙니다. 드라이버 캐시, VM 상태, OS 파일 캐시가 남아 있을 수 있습니다. 테스트 범위를 선언하고 전용 이미지 또는 스냅샷 복원으로 동일한 시작 조건을 만들어야 합니다.

프레임 타임만 수집해도 충분한가?

회귀 감지에는 사용할 수 있지만 원인 분석에는 부족합니다. 셰이더 컴파일, PSO 생성, pipeline cache hit, 에셋 로딩 이벤트를 함께 수집해야 어떤 작업이 hitch와 겹쳤는지 확인할 수 있습니다.

정리

콜드 캐시에서 첫 실행 셰이더 컴파일 스터터를 자동 재현하려면 캐시 삭제보다 시작 조건의 표준화가 먼저입니다. 빌드와 장비를 고정하고 캐시 레이어별 초기화 결과를 검증한 다음 결정론적인 경로를 실행하세요. 마지막으로 프레임 타임과 셰이더·PSO 이벤트를 하나의 타임라인으로 묶으면 성능 회귀를 자동 판정하면서도 프로그래머가 실제 원인까지 추적할 수 있습니다.