AI 기반 실시간 프레임 드랍 원인 프로파일링 봇과 자동 캡처 시스템 구축
Unity 2022.3 LTS와 Unreal Engine 5.x에서 프레임 시간·CPU/GPU·GC 지표를 수집하고 이상 구간의 화면, 로그, 트레이스를 자동 캡처한 뒤 AI가 원인을 요약하는 방법을 설명합니다.

핵심 요약
프레임 드랍 프로파일링 봇은 매 프레임의 시간과 CPU·GPU·GC 지표를 수집해 문제 구간을 찾습니다. 임계값 기반 탐지와 p95·MAD 기반 탐지는 서로 다른 선택지이며, 아래 상태 머신 코드는 단순 임계값 예시입니다. 이상이 일정 시간 지속되면 제한된 큐에 저장한 직전 프레임 데이터와 현재 화면, 로그, 엔진 트레이스를 함께 캡처합니다.
AI는 원시 로그를 무작정 읽는 대신 정규화된 증거 묶음을 입력받아 CPU.MainThread, GPU.Pass, GC.Alloc, ShaderCompile, IO.Loading 같은 원인 후보와 근거를 요약합니다. 최종 판정은 규칙 기반 분류기가 담당하고 AI는 설명과 우선순위 지정에 집중시키는 것이 안전합니다.
어떤 환경에서 구현할까?
이 글의 예제는 Unity 2022.3 LTS 이상과 C# 스크립트를 기준으로 작성했습니다. Unreal Engine 5.x 프로젝트에서는 동일한 수집·판정·캡처 개념을 Unreal Insights Trace 시스템으로 연결할 수 있습니다.
| 구분 | Unity | Unreal Engine |
|---|---|---|
| 프레임 시간 | Time.unscaledDeltaTime, FrameTimingManager | Unreal Insights Timing Insights |
| CPU·GPU 지표 | FrameTiming, ProfilerRecorder | -trace=cpu,gpu,frame |
| 이벤트 표식 | ProfilerMarker | TRACE_BOOKMARK, CPU profiler trace |
| 화면 캡처 | ScreenCapture.CaptureScreenshot | 플랫폼별 캡처 모듈 또는 외부 캡처 도구 |
| 결과 파일 | JSON, PNG, 로그, 프로파일 데이터 | .utrace, 로그, 스크린샷 |
Unity의 ProfilerRecorder는 프로파일러 마커와 카운터를 런타임에서 읽는 API이며 FrameTimingManager는 CPU·GPU 프레임 타이밍을 수집하는 데 사용할 수 있습니다. FrameTimingManager는 계측 오버헤드를 만들고, 타이밍 결과가 지연되어 도착할 수 있으므로 실제 플레이어 성능을 그대로 대변한다고 가정하지 말고 개발 빌드에서 검증해야 합니다. Unreal Insights는 Trace Server가 런타임 이벤트를 기록하고 Timing Insights가 이를 분석하는 구조입니다. 참고: Unity ProfilerRecorder API, Unity Frame Timing Manager 2022.3, Unreal Insights
프레임 드랍은 왜 발생하는가?
목표 프레임레이트가 60 FPS라면 프레임 예산은 약 16.67ms입니다. 30 FPS와 120 FPS의 예산은 각각 약 33.33ms와 8.33ms입니다.
실제 프레임 시간은 CPU와 GPU 중 더 오래 걸리는 쪽의 영향을 크게 받습니다. 여기에 스레드 동기화와 Present 대기 시간이 추가됩니다.
따라서 평균 FPS만 보면 원인을 놓치기 쉽습니다. 평균은 짧은 스파이크를 숨길 수 있으므로 p95와 p99 프레임 시간, 연속으로 기준을 초과한 프레임 수를 함께 봐야 합니다.
| 관측 패턴 | 우선 조사할 원인 |
|---|---|
| 메인 스레드 시간이 급증 | 게임플레이 로직, 물리, 애니메이션, UI 레이아웃, 동기 로딩 |
| 렌더 스레드 시간이 급증 | 드로우콜, 렌더 패스, 스키닝, 커맨드 버퍼 제출 |
| GPU 시간이 프레임 예산 초과 | 셰이더, 그림자, 후처리, 오버드로, 해상도 |
| GC Alloc과 GC 시간이 함께 증가 | 매 프레임 할당, LINQ, 문자열 생성, 박싱 |
| CPU와 GPU 모두 낮지만 Present 대기 증가 | VSync, 프레임 페이싱, 플랫폼 스케줄링 |
| 특정 위치나 이펙트에서만 발생 | 스트리밍, 셰이더 컴파일, 파티클, 리소스 로딩 |
실시간 프로파일링 봇의 전체 구조는 어떻게 설계할까?
프로파일링 봇은 게임 클라이언트 안에서 데이터를 수집하는 에이전트와 외부에서 결과를 분석하는 AI 워커로 나누는 것이 좋습니다. 런타임에서는 짧은 링 버퍼만 유지하고 파일 저장과 AI 호출은 이상 구간이 발생했을 때만 수행합니다.
flowchart LR
A[런타임 지표 수집] --> B[링 버퍼 저장]
B --> C{이상 프레임 지속}
C -- 아니오 --> A
C -- 예 --> D[화면·로그·트레이스 캡처]
D --> E[특징 추출 및 원인 분류]
E --> F[AI 요약]
F --> G[리포트 발행]
권장 데이터 흐름은 다음과 같습니다.
FrameSample에 프레임 시간과 엔진 지표를 기록합니다.- 최근 2~5초의 샘플을 링 버퍼에 보관합니다.
- 기준 초과 프레임이 3~5회 연속 발생하면 이상 상태로 전환합니다.
- 이상 전후 구간의 화면, 로그, JSON, 프로파일 데이터를 저장합니다.
- 규칙 기반 분류 결과와 증거를 AI에 전달합니다.
- AI가 원인 후보, 신뢰도, 재현 조건, 다음 확인 항목을 정리합니다.
이상 프레임은 어떤 기준으로 탐지할까?
고정 임계값만 사용하면 저사양 기기와 고사양 기기에서 같은 기준을 적용하기 어렵습니다. 세션 초반에 정상 구간을 수집한 뒤 이동 윈도우의 중앙값과 MAD를 사용하는 방식이 실용적입니다.
여기서 W는 최근 정상 프레임 윈도우이고 MAD는 중앙값과 각 샘플의 차이에 대한 중앙값입니다. 예를 들어 다음 조건을 함께 사용합니다.
frameTimeMs >= budgetMs * 1.5- 또는
robustZ >= 3.5 - 위 조건이 4프레임 이상 연속 발생
- 마지막 캡처 이후 10초가 지나야 재캡처
단일 프레임 스파이크를 모두 저장하면 디스크와 네트워크 비용이 커집니다. 반대로 1초 이상 지속되는 문제만 잡으면 셰이더 컴파일이나 동기 로딩 같은 짧은 스파이크를 놓칠 수 있으므로 프로젝트 특성에 맞게 연속 프레임 수를 조정해야 합니다.
Unity에서 프레임 지표를 수집하는 방법
먼저 게임플레이 시스템에 의미 있는 프로파일러 마커를 추가합니다. 함수 이름만으로는 AI가 실제 기능을 이해하기 어렵기 때문에 Combat.ResolveHits, UI.Inventory.Rebuild, World.StreamChunk처럼 도메인과 동작을 함께 표현합니다.
using Unity.Profiling;
using UnityEngine;
public sealed class CombatSystem : MonoBehaviour
{
private static readonly ProfilerMarker ResolveHitsMarker =
new ProfilerMarker("Combat.ResolveHits");
public void ResolveHits()
{
using (ResolveHitsMarker.Auto())
{
// 충돌 판정과 피해량 계산
}
}
}
ProfilerRecorder로 커스텀 마커의 프레임별 누적 시간을 읽을 수 있습니다. 이 예제의 기본 옵션은 프레임 안의 샘플을 합산하고, LastValue는 마지막으로 완료된 프레임에서 수집한 값을 반환합니다. 시간 단위는 나노초이므로 1_000_000f로 나누어 밀리초로 변환합니다. Unity 2022.3 Memory 카테고리의 GC Allocated In Frame 카운터도 ProfilerRecorder로 읽어 프레임의 관리 힙 할당량을 얻을 수 있습니다. 마커 이름과 카운터 지원 여부는 실행 환경에 따라 확인해야 하므로 개발 빌드에서 실제 값을 검증합니다. 참고: Unity 2022.3 ProfilerRecorder.LastValue, ProfilerRecorderOptions, ProfilerMarkerDataUnit.TimeNanoseconds, Memory Profiler module
using System;
using System.Collections.Generic;
using Unity.Profiling;
using UnityEngine;
public sealed class RuntimeFrameProbe : MonoBehaviour
{
[SerializeField] private int ringCapacity = 300;
private FrameSample[] samples;
private int nextSample;
private int sampleCount;
private ProfilerRecorder resolveHitsRecorder;
private ProfilerRecorder gcAllocatedInFrameRecorder;
private void Awake()
{
samples = new FrameSample[Mathf.Max(1, ringCapacity)];
}
private void OnEnable()
{
resolveHitsRecorder = ProfilerRecorder.StartNew(
ProfilerCategory.Scripts,
"Combat.ResolveHits",
128);
gcAllocatedInFrameRecorder = ProfilerRecorder.StartNew(
ProfilerCategory.Memory,
"GC Allocated In Frame",
128);
}
private void OnDisable()
{
if (resolveHitsRecorder.Valid)
{
resolveHitsRecorder.Dispose();
}
if (gcAllocatedInFrameRecorder.Valid)
{
gcAllocatedInFrameRecorder.Dispose();
}
}
private void Update()
{
bool hasCombatMarkerSample =
resolveHitsRecorder.Valid && resolveHitsRecorder.Count > 0;
bool hasGcAllocationSample =
gcAllocatedInFrameRecorder.Valid && gcAllocatedInFrameRecorder.Count > 0;
var sample = new FrameSample
{
realtimeSeconds = Time.realtimeSinceStartupAsDouble,
frame = Time.frameCount,
frameTimeMs = Time.unscaledDeltaTime * 1000f,
combatMarkerMs = hasCombatMarkerSample
? resolveHitsRecorder.LastValue / 1_000_000f
: 0f,
hasCombatMarkerSample = hasCombatMarkerSample,
gcAllocatedBytes = hasGcAllocationSample
? gcAllocatedInFrameRecorder.LastValue
: 0,
hasGcAllocationSample = hasGcAllocationSample
};
samples[nextSample] = sample;
nextSample = (nextSample + 1) % samples.Length;
sampleCount = Mathf.Min(sampleCount + 1, samples.Length);
}
public List<FrameSample> CopySamples()
{
var copy = new List<FrameSample>(sampleCount);
int first = (nextSample - sampleCount + samples.Length) % samples.Length;
for (int i = 0; i < sampleCount; i++)
{
copy.Add(samples[(first + i) % samples.Length]);
}
return copy;
}
}
[Serializable]
public struct FrameSample
{
public double realtimeSeconds;
public int frame;
public float frameTimeMs;
public float combatMarkerMs;
public bool hasCombatMarkerSample;
public long gcAllocatedBytes;
public bool hasGcAllocationSample;
}
GC Allocated In Frame은 선택한 프레임에서 관리 힙에 할당된 바이트 수를 나타내는 프로파일러 카운터입니다. 예시는 카운터가 없는 환경의 0값과 실제 0바이트를 구분하도록 hasGcAllocationSample도 저장합니다. 메인 스레드, 렌더 스레드, GPU 프레임 시간은 FrameTimingManager.CaptureFrameTimings()와 GetLatestTimings()로 별도 수집할 수 있습니다. Development Player에서는 기능이 기본 활성화되며, Release 빌드와 에디터에서는 Player Settings의 Frame Timing Stats를 켜야 합니다. 결과에는 지연이 있으며 요청한 수보다 적은 프레임 자료만 반환될 수 있습니다. GPU 타임스탬프를 지원하지 않는 플랫폼에서는 GPU 시간이 유효하지 않을 수 있으므로 gpuFrameTimeMs에 유효성 플래그를 함께 저장해야 합니다. 참고: Unity 2022.3 FrameTimingManager 매뉴얼, Memory Profiler module
프레임 드랍이 발생하면 무엇을 자동 캡처할까?
캡처 묶음에는 원인을 설명할 수 있는 맥락이 포함되어야 합니다. 게임 화면만 담은 이미지에서는 어떤 오브젝트가 보였는지는 알 수 있지만, CPU와 GPU 중 어디가 병목이었는지 판정할 타이밍 자료는 얻을 수 없습니다.
권장 캡처 항목은 다음과 같습니다.
| 항목 | 권장 내용 |
|---|---|
| 화면 | 이상 시작 시점의 PNG 1장 또는 짧은 동영상 |
| 프레임 데이터 | 이상 전 120프레임과 후 60프레임 |
| 시스템 정보 | GPU, 해상도, 품질 설정, 플랫폼, 빌드 번호 |
| 게임 상태 | 씬, 스테이지, 플레이어 위치, 활성 적 수 |
| 프로파일 데이터 | Unity Profiler 데이터 또는 Unreal .utrace |
| 이벤트 로그 | 씬 전환, 리소스 로딩, 셰이더 컴파일, GC 발생 |
| 사용자 행동 | 입력 장치, 최근 명령, 카메라 상태 |

그림의 게임 화면, 지표, 로그, 트레이스, AI 진단 내용은 설명을 위한 가상 모형이며 실제 실행 기록이 아닙니다.
Unity의 스크린샷 저장은 ScreenCapture.CaptureScreenshot으로 구현할 수 있습니다. 해당 API는 PNG 파일을 저장하므로 파일명에 빌드 번호와 프레임 번호를 포함하고 캡처 완료 시점을 별도로 기록하는 것이 좋습니다. 참고: Unity 2022.3 ScreenCapture.CaptureScreenshot
using System.IO;
using UnityEngine;
public static class EvidenceCapture
{
public static string CaptureScreenshot(string incidentId)
{
string directory = Path.Combine(
Application.persistentDataPath,
"frame-incidents");
Directory.CreateDirectory(directory);
string fileName = $"{incidentId}_frame_{Time.frameCount}.png";
string path = Path.Combine(directory, fileName);
// Mobile CaptureScreenshot appends its filename to persistentDataPath.
string capturePath = Application.isMobilePlatform
? Path.Combine("frame-incidents", fileName)
: path;
ScreenCapture.CaptureScreenshot(capturePath, 1);
return path;
}
}
Unity 2022.3의 ScreenCapture.CaptureScreenshot은 모바일 플랫폼에서 전달된 파일명을 Application.persistentDataPath에 덧붙입니다. 그래서 예시는 모바일에는 상대 경로를 전달하고, Windows Editor·macOS Editor와 기타 비모바일 플랫폼에는 전체 경로를 전달합니다. 반환 경로는 실제 저장 위치를 가리킵니다. 스크린샷은 호출 즉시 파일이 완성된다고 가정하면 안 됩니다. 캡처 요청 후 파일이 실제로 생성되었는지 확인하고 파일 크기가 안정된 뒤 업로드 큐에 넣어야 합니다. 모바일에서는 저장 공간과 발열을 고려해 해상도와 캡처 빈도를 제한해야 합니다.
제한된 샘플 큐와 상태 머신은 어떻게 구현할까?
자동 캡처기는 Normal, Suspected, Capturing, Cooldown 상태를 갖는 간단한 상태 머신으로 관리하면 중복 저장을 막을 수 있습니다.
public enum CaptureState
{
Normal,
Suspected,
Capturing,
Cooldown
}
public sealed class FrameDropDetector
{
private readonly float budgetMs;
private readonly int requiredConsecutiveFrames;
private readonly float cooldownSeconds;
private int consecutiveBadFrames;
private float cooldownRemaining;
public CaptureState State { get; private set; } = CaptureState.Normal;
public FrameDropDetector(
float budgetMs,
int requiredConsecutiveFrames = 4,
float cooldownSeconds = 10f)
{
this.budgetMs = budgetMs;
this.requiredConsecutiveFrames = requiredConsecutiveFrames;
this.cooldownSeconds = cooldownSeconds;
}
public bool Push(float frameTimeMs, float deltaSeconds)
{
if (cooldownRemaining > 0f)
{
cooldownRemaining -= deltaSeconds;
State = CaptureState.Cooldown;
return false;
}
bool isBad = frameTimeMs >= budgetMs * 1.5f;
consecutiveBadFrames = isBad ? consecutiveBadFrames + 1 : 0;
if (consecutiveBadFrames >= requiredConsecutiveFrames)
{
State = CaptureState.Capturing;
consecutiveBadFrames = 0;
cooldownRemaining = cooldownSeconds;
return true;
}
State = isBad ? CaptureState.Suspected : CaptureState.Normal;
return false;
}
}
Push가 true를 반환한 프레임에서 캡처 작업을 시작합니다. JSON 직렬화, PNG 저장, 압축, 업로드는 메인 스레드를 오래 점유하지 않도록 코루틴이나 별도 작업 큐에서 처리해야 합니다. 단, Unity 객체와 렌더 텍스처 접근은 메인 스레드 제약을 지키고 순수 데이터 변환만 백그라운드에서 수행해야 합니다.
AI는 프레임 드랍 원인을 어떻게 분류해야 할까?
전체 로그를 그대로 보내면 입력량이 늘어 비용이 커질 수 있고, 민감한 내용이 외부 서비스로 전송될 위험도 있습니다. 먼저 클라이언트에서 특징을 추출하고 원인 후보를 좁힌 뒤 AI에는 구조화된 증거만 전달하는 방식을 고려하세요.
예를 들어 입력 데이터는 다음과 같이 구성할 수 있습니다. 사건 ID와 수치는 형식 설명을 위해 만든 가상 값이며 실제 프로파일링 기록이 아닙니다.
{
"incidentId": "run-20260824-0012",
"engine": "Unity 2022.3",
"platform": "Windows",
"targetFps": 60,
"frameTime": { "p95": 18.4, "p99": 41.7, "max": 58.2 },
"timing": {
"cpuMainThreadMs": 39.1,
"cpuRenderThreadMs": 8.2,
"gpuFrameMs": 7.9,
"presentWaitMs": 0.3
},
"signals": {
"gcAllocBytesPerFrame": 184320,
"shaderCompile": false,
"loadingOperation": "none"
},
"markers": [
{ "name": "UI.Inventory.Rebuild", "p99Ms": 22.4 },
{ "name": "Combat.ResolveHits", "p99Ms": 3.1 }
],
"context": {
"scene": "Dungeon_03",
"activeEnemies": 86,
"resolution": "1920x1080"
}
}
규칙 기반 분류 예시는 다음과 같습니다.
| 조건 | 원인 후보 | AI에 전달할 근거 |
|---|---|---|
cpuMainThreadMs > gpuFrameMs이고 특정 마커 급증 | CPU 메인 스레드 | 마커 p99, 활성 오브젝트 수, 게임 상태 |
gpuFrameMs > cpuMainThreadMs | GPU 병목 | GPU 시간, 해상도, 렌더 패스 정보 |
| GC 시간과 프레임별 할당량 동시 증가 | GC 스파이크 | 할당량, 호출 구간, 직전 입력 |
| 첫 진입 순간에만 발생 | 셰이더 또는 리소스 로딩 | 로딩 이벤트, 셰이더 컴파일 여부 |
presentWaitMs 증가 | VSync 또는 프레임 페이싱 | 동기화 시간, 화면 주사율, VSync 설정 |
AI의 출력 형식도 자유로운 문장보다 JSON 스키마로 제한하는 것이 좋습니다. 다음 응답의 신뢰도와 근거 문구는 형식 설명을 위한 가상 값입니다.
{
"primaryCause": "CPU.MainThread",
"confidence": 0.86,
"evidence": [
"메인 스레드 프레임 시간이 39.1ms로 GPU 프레임 시간 7.9ms보다 높음",
"UI.Inventory.Rebuild 마커 p99가 22.4ms로 증가함",
"해당 프레임의 관리 힙 할당량이 184320바이트임"
],
"nextChecks": [
"인벤토리 갱신 시 전체 레이아웃 재빌드 여부 확인",
"UI 아이템 생성과 문자열 포맷의 프레임별 할당 측정",
"가상화 목록 또는 변경 항목 기반 갱신 적용 검토"
]
}
AI의 confidence는 보정된 실제 정확도나 진실을 보장하는 값이 아닙니다. 모델이 낸 값은 추가 검토의 우선순위를 정하는 보조 신호로만 사용하고, 자동으로 버그 티켓을 생성할 때도 원본 증거 파일과 규칙 기반 판정 결과를 함께 첨부해야 사람이 재검증할 수 있습니다.
Unreal Engine 5에서는 어떻게 연결할까?
Unreal Engine 5 프로젝트에서는 런처에 Trace 옵션을 붙여 CPU·GPU·프레임 이벤트를 수집하고 Unreal Insights에서 .utrace 파일을 분석합니다. 예시는 다음과 같습니다.
MyGame.exe -trace=cpu,gpu,frame,bookmark -tracehost=127.0.0.1
게임플레이 코드에서는 Unreal의 MiscTrace.h에 정의된 TRACE_BOOKMARK로 재현에 중요한 순간에 북마크를 남깁니다. 다음은 기존 UInventoryComponent 구현 파일에 넣는 부분 예시이며, 프로젝트의 클래스 선언과 헤더는 생략했습니다.
#include "ProfilingDebugging/MiscTrace.h"
void UInventoryComponent::RebuildInventory()
{
TRACE_BOOKMARK(TEXT("Inventory.Rebuild Start"));
// 인벤토리 UI와 데이터 갱신
TRACE_BOOKMARK(TEXT("Inventory.Rebuild End"));
}
Trace 채널은 데이터량과 런타임 오버헤드를 증가시킬 수 있으므로 테스트 빌드와 QA 프로파일링 빌드에서만 활성화하는 편이 안전합니다. AI 워커는 .utrace 자체를 직접 해석하기보다 Unreal Insights에서 추출한 구간별 요약과 북마크, 시스템 메타데이터를 입력받도록 구성하면 처리 비용을 줄일 수 있습니다.
자동 캡처 시스템의 운영상 주의점은 무엇일까?
캡처 비용을 제한한다
이상 구간마다 고해상도 PNG와 전체 트레이스를 저장하면 디스크가 빠르게 증가합니다. 다음 정책을 기본값으로 두고 프로젝트에 맞게 조정합니다.
- 세션당 자동 캡처 최대 5건
- 캡처 사이 최소 10초 쿨다운
- 화면 캡처는 사건당 1장
- 원시 샘플은 최근 300프레임만 보관
- 업로드 실패 시 로컬 큐에 저장하고 재시도
- 저장 공간이 500MB 이하로 내려가면 오래된 사건부터 삭제
개발자 빌드와 사용자 빌드를 분리한다
개발자 빌드에서는 상세 마커와 트레이스를 활성화하고 사용자 빌드에서는 샘플링 비율과 데이터 필드를 줄이는 방식을 고려할 수 있습니다. 플레이어 이름, 계정 ID, 채팅 내용 등 개인정보는 목적에 필요한 최소한만 처리하고 보유 기간과 접근 권한을 제한해야 합니다. 수집이 불필요하면 저장하지 말고, 가명처리가 필요하더라도 단순 해시만으로 익명 정보가 된다고 간주하지 마세요. 구체적인 처리 근거와 보호 조치는 적용 법령 및 서비스 환경에 따라 확인해야 합니다. 참고: 개인정보 수집 제한에 관한 개인정보 보호법 제16조, 목적 달성 등 불필요해진 정보의 파기에 관한 제21조, 안전조치 의무에 관한 제29조, 그리고 개인정보보호위원회 가명정보 처리 안내를 확인하세요.
원인과 상관관계를 구분한다
프레임 드랍 직전에 GC가 발생했다고 해서 항상 GC가 근본 원인은 아닙니다. 대규모 오브젝트 생성이나 UI 재빌드가 GC를 유발했을 수 있습니다. 따라서 AI 리포트에는 다음 세 가지를 분리해 표시합니다.
- 시간적으로 먼저 발생한 이벤트
- 프레임 시간을 실제로 초과시킨 병목 지표
- 추가 측정이 필요한 가설
구현 순서: 1단계부터 6단계까지
1단계. 프레임 예산과 품질 기준 정의
60 FPS 목표라면 16.67ms를 기준으로 정합니다. 플랫폼별 해상도와 품질 설정에 따라 CPU·GPU 기준을 별도로 정의합니다.
2단계. 핵심 구간에 프로파일러 마커 추가
게임플레이, UI, 로딩, 애니메이션, 네트워크 처리처럼 비용 변동이 큰 구간에 도메인 기반 마커를 추가합니다.
3단계. 링 버퍼 기반 수집기 구현
프레임 시간, CPU·GPU 타이밍, GC, 활성 오브젝트 수, 씬, 해상도, 최근 이벤트를 2~5초 동안 보관합니다.
4단계. 이상 탐지와 캡처 상태 머신 연결
고정 임계값과 이동 통계값을 조합하고 연속 프레임 조건과 쿨다운을 적용합니다.
5단계. 증거 패키지와 AI 스키마 고정
incident.json, screenshot.png, profile.utrace 또는 Unity 프로파일 데이터, 이벤트 로그를 하나의 사건 ID로 묶습니다.
6단계. 회귀 테스트에 연결
동일한 시나리오를 반복 실행하고 p95·p99 프레임 시간과 캡처된 원인 분류를 이전 빌드와 비교합니다. AI 요약은 참고 정보로 사용하고 성능 통과 여부는 수치 기준으로 판정합니다.
자주 묻는 질문(FAQ)
프레임 드랍마다 스크린샷을 저장하면 안 되나요?
권장하지 않습니다. 스파이크가 잦은 구간에서는 저장 I/O 자체가 추가 프레임 드랍을 만들 수 있습니다. 링 버퍼를 유지하다가 연속 이상 조건을 만족한 사건만 캡처하세요.
AI가 프로파일러 파일만 보고 원인을 정확히 찾을 수 있나요?
보장할 수 없습니다. 프로파일러 파일은 분석에 필요한 근거를 제공하지만 재현 조건과 게임 상태가 없으면 원인 후보가 여러 개 남습니다. 규칙 기반 지표, 이벤트 북마크, 화면, 씬과 설정 정보를 함께 전달해야 합니다.
모바일에서도 같은 방식으로 사용할 수 있나요?
가능하지만 수집 주기, 마커 수, 화면 해상도, 업로드 빈도를 제한해야 합니다. 오프라인으로 사건을 저장한 뒤 Wi-Fi 환경에서만 업로드하는 방식도 고려할 수 있습니다.
정리
AI 기반 프레임 드랍 프로파일링의 핵심은 AI 모델 자체가 아니라 신뢰할 수 있는 증거를 자동으로 모으는 수집 파이프라인입니다. 프레임 예산을 기준으로 p95·p99와 연속 이상 프레임을 탐지하고 CPU·GPU·GC·로딩 신호를 분리하면 원인 후보를 좁힐 수 있습니다.
그 위에 링 버퍼와 자동 캡처를 연결하면 개발자가 문제를 다시 재현하지 않아도 당시 화면과 성능 데이터를 확인할 수 있습니다. 마지막으로 규칙 기반 판정과 AI 설명을 분리하면 과도한 확신을 줄이면서도 QA와 클라이언트 개발자가 바로 실행할 수 있는 프로파일링 리포트를 만들 수 있습니다.



