심화 분석 도구
심화 프로파일링 도구는 앱인토스 미니앱에서 Unity 게임의 성능을 더 세밀하게 분석하고 최적화할 수 있는 전용 도구예요.
일반 Unity Profiler보다 한 단계 확장돼, WebGL 환경과 토스 앱의 특수한 제약까지 고려한 맞춤형 분석 기능을 제공합니다.
앱인토스 플랫폼 특화 분석
1. WebGL 특화 성능 메트릭
c#
// Unity C# - 앱인토스 WebGL 프로파일러
using UnityEngine;
using System.Collections.Generic;
using System.Runtime.InteropServices;
public class AITWebGLProfiler : MonoBehaviour
{
[DllImport("__Internal")]
private static extern float GetJSHeapUsed();
[DllImport("__Internal")]
private static extern float GetJSHeapTotal();
[DllImport("__Internal")]
private static extern int GetWebGLContextLostCount();
[System.Serializable]
public class WebGLMetrics
{
public float jsHeapUsed; // JavaScript 힙 사용량
public float jsHeapTotal; // JavaScript 힙 총량
public int contextLostCount; // WebGL 컨텍스트 손실 횟수
public float wasmMemoryUsage; // WASM 메모리 사용량
public int activeTextures; // 활성 텍스처 수
public int drawCalls; // 드로우콜 수
public float renderThreadTime; // 렌더링 스레드 시간
}
private WebGLMetrics currentMetrics;
private List<WebGLMetrics> metricsHistory;
private void Start()
{
metricsHistory = new List<WebGLMetrics>();
InvokeRepeating(nameof(CollectMetrics), 0f, 0.1f); // 100ms 간격으로 수집
}
private void CollectMetrics()
{
currentMetrics = new WebGLMetrics
{
jsHeapUsed = GetJSHeapUsed(),
jsHeapTotal = GetJSHeapTotal(),
contextLostCount = GetWebGLContextLostCount(),
wasmMemoryUsage = GetWASMMemoryUsage(),
activeTextures = GetActiveTextureCount(),
drawCalls = GetDrawCallCount(),
renderThreadTime = GetRenderThreadTime()
};
metricsHistory.Add(currentMetrics);
// 1000개 이상 쌓이면 오래된 데이터 제거
if (metricsHistory.Count > 1000)
{
metricsHistory.RemoveAt(0);
}
// 크리티컬 이슈 감지
DetectCriticalIssues();
}
private void DetectCriticalIssues()
{
// 메모리 누수 감지
if (currentMetrics.jsHeapUsed > currentMetrics.jsHeapTotal * 0.9f)
{
Debug.LogWarning("[AIT Profile] JavaScript 힙 메모리 부족 경고!");
LogProfileEvent("memory_warning", currentMetrics);
}
// 과도한 드로우콜 감지
if (currentMetrics.drawCalls > 500)
{
Debug.LogWarning("[AIT Profile] 과도한 드로우콜 감지: " + currentMetrics.drawCalls);
LogProfileEvent("high_drawcalls", currentMetrics);
}
// WebGL 컨텍스트 손실 감지
if (currentMetrics.contextLostCount > 0)
{
Debug.LogError("[AIT Profile] WebGL 컨텍스트 손실 발생!");
LogProfileEvent("context_lost", currentMetrics);
}
}
private float GetWASMMemoryUsage()
{
// WASM 메모리 사용량 계산
return UnityEngine.Profiling.Profiler.GetTotalAllocatedMemory(UnityEngine.Profiling.Profiler.Area.Total);
}
private int GetActiveTextureCount()
{
return UnityEngine.Rendering.GraphicsSettings.currentRenderPipeline?.GetType().Name == "UniversalRenderPipelineAsset"
? QualitySettings.masterTextureLimit : Texture.currentTextureMemory > 0 ? 1 : 0;
}
private int GetDrawCallCount()
{
return UnityEngine.Profiling.Profiler.GetStatValue(UnityEngine.Profiling.ProfilerArea.Rendering,
UnityEngine.Profiling.ProfilerStatisticsNames.DrawCallsCount);
}
private float GetRenderThreadTime()
{
return UnityEngine.Profiling.Profiler.GetStatValue(UnityEngine.Profiling.ProfilerArea.Rendering,
UnityEngine.Profiling.ProfilerStatisticsNames.RenderThreadTime);
}
}2. 토스 앱 환경 특화 분석
c#
// JavaScript 프로파일링 - 토스 앱 통합
class AITTossEnvironmentProfiler {
constructor() {
this.metrics = {
tossAppMemory: 0,
networkLatency: 0,
batteryLevel: 0,
backgroundAppCount: 0,
deviceTemperature: 0
};
this.tossAPIResponseTimes = new Map();
this.startProfiling();
}
startProfiling() {
// 토스 앱 메모리 모니터링
if (window.TossApp && window.TossApp.getMemoryUsage) {
setInterval(() => {
this.metrics.tossAppMemory = window.TossApp.getMemoryUsage();
}, 1000);
}
// 네트워크 지연시간 모니터링
this.monitorNetworkLatency();
// 배터리 상태 모니터링
this.monitorBatteryStatus();
// 백그라운드 앱 모니터링
this.monitorBackgroundApps();
}
monitorNetworkLatency() {
const startTime = performance.now();
// 토스 API 호출 시간 측정
const originalFetch = window.fetch;
window.fetch = async (...args) => {
const requestStart = performance.now();
try {
const response = await originalFetch.apply(this, args);
const requestEnd = performance.now();
const latency = requestEnd - requestStart;
// API별 응답시간 기록
const url = args[0];
if (typeof url === 'string' && url.includes('toss.im')) {
this.tossAPIResponseTimes.set(url, latency);
this.metrics.networkLatency = latency;
// 느린 응답 감지
if (latency > 3000) {
console.warn(`[AIT Profile] 느린 API 응답: ${url} (${latency}ms)`);
this.logSlowAPICall(url, latency);
}
}
return response;
} catch (error) {
console.error('[AIT Profile] API 호출 실패:', error);
throw error;
}
};
}
monitorBatteryStatus() {
if ('getBattery' in navigator) {
navigator.getBattery().then(battery => {
this.metrics.batteryLevel = battery.level * 100;
battery.addEventListener('levelchange', () => {
this.metrics.batteryLevel = battery.level * 100;
// 배터리 부족 시 성능 모드 전환
if (this.metrics.batteryLevel < 20) {
this.enableBatterySavingMode();
}
});
});
}
}
monitorBackgroundApps() {
// 페이지 가시성 API를 통한 백그라운드 상태 감지
document.addEventListener('visibilitychange', () => {
if (document.hidden) {
console.log('[AIT Profile] 앱이 백그라운드로 전환됨');
this.pauseHeavyOperations();
} else {
console.log('[AIT Profile] 앱이 포그라운드로 전환됨');
this.resumeHeavyOperations();
}
});
}
enableBatterySavingMode() {
// Unity로 배터리 절약 모드 신호 전송
if (window.unityInstance) {
window.unityInstance.SendMessage('AITProfiler', 'EnableBatterySavingMode', '');
}
}
pauseHeavyOperations() {
// 무거운 연산 일시정지
if (window.unityInstance) {
window.unityInstance.SendMessage('AITProfiler', 'PauseHeavyOperations', '');
}
}
resumeHeavyOperations() {
// 무거운 연산 재개
if (window.unityInstance) {
window.unityInstance.SendMessage('AITProfiler', 'ResumeHeavyOperations', '');
}
}
logSlowAPICall(url, latency) {
const logData = {
timestamp: new Date().toISOString(),
url: url,
latency: latency,
userAgent: navigator.userAgent,
connectionType: navigator.connection?.effectiveType || 'unknown'
};
// 앱인토스 분석 서버로 전송
this.sendAnalyticsData('slow_api_call', logData);
}
sendAnalyticsData(eventType, data) {
if (window.AITAnalytics && window.AITAnalytics.trackEvent) {
window.AITAnalytics.trackEvent(eventType, data);
}
}
}상세 구현 방법
1. 실시간 성능 대시보드
html
<!DOCTYPE html>
<html>
<head>
<title>앱인토스 심화 프로파일러</title>
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
<style>
.profiler-dashboard {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 20px;
padding: 20px;
font-family: 'Toss Product Sans', -apple-system, BlinkMacSystemFont, sans-serif;
}
.metric-card {
background: white;
border: 1px solid #e0e0e0;
border-radius: 12px;
padding: 16px;
box-shadow: 0 2px 8px rgba(0,0,0,0.1);
}
.metric-title {
font-size: 14px;
font-weight: 600;
color: #191f28;
margin-bottom: 12px;
}
.metric-value {
font-size: 24px;
font-weight: 700;
color: #3182f6;
}
.chart-container {
position: relative;
height: 200px;
margin-top: 16px;
}
.alert-critical {
background: #fff2f2;
border-color: #f04452;
color: #f04452;
}
.alert-warning {
background: #fff8f0;
border-color: #ff8c00;
color: #ff8c00;
}
</style>
</head>
<body>
<div class="profiler-dashboard">
<!-- CPU 사용률 -->
<div class="metric-card">
<div class="metric-title">CPU 사용률</div>
<div class="metric-value" id="cpu-usage">0%</div>
<div class="chart-container">
<canvas id="cpu-chart"></canvas>
</div>
</div>
<!-- 메모리 사용량 -->
<div class="metric-card">
<div class="metric-title">메모리 사용량</div>
<div class="metric-value" id="memory-usage">0 MB</div>
<div class="chart-container">
<canvas id="memory-chart"></canvas>
</div>
</div>
<!-- GPU 사용률 -->
<div class="metric-card">
<div class="metric-title">GPU 사용률</div>
<div class="metric-value" id="gpu-usage">0%</div>
<div class="chart-container">
<canvas id="gpu-chart"></canvas>
</div>
</div>
<!-- 네트워크 지연시간 -->
<div class="metric-card">
<div class="metric-title">네트워크 지연시간</div>
<div class="metric-value" id="network-latency">0 ms</div>
<div class="chart-container">
<canvas id="network-chart"></canvas>
</div>
</div>
<!-- 드로우콜 수 -->
<div class="metric-card">
<div class="metric-title">드로우콜</div>
<div class="metric-value" id="draw-calls">0</div>
<div class="chart-container">
<canvas id="drawcalls-chart"></canvas>
</div>
</div>
<!-- 배터리 상태 -->
<div class="metric-card">
<div class="metric-title">배터리</div>
<div class="metric-value" id="battery-level">100%</div>
<div id="battery-status">양호</div>
</div>
</div>
<script>
class AITProfilerDashboard {
constructor() {
this.charts = {};
this.initCharts();
this.startDataCollection();
}
initCharts() {
const chartOptions = {
responsive: true,
maintainAspectRatio: false,
scales: {
x: { display: false },
y: {
beginAtZero: true,
max: 100
}
},
plugins: {
legend: { display: false }
}
};
// CPU 차트
this.charts.cpu = new Chart(document.getElementById('cpu-chart'), {
type: 'line',
data: {
labels: Array.from({length: 50}, (_, i) => i),
datasets: [{
data: Array(50).fill(0),
borderColor: '#3182f6',
backgroundColor: 'rgba(49, 130, 246, 0.1)',
fill: true,
tension: 0.4
}]
},
options: chartOptions
});
// 메모리 차트
this.charts.memory = new Chart(document.getElementById('memory-chart'), {
type: 'line',
data: {
labels: Array.from({length: 50}, (_, i) => i),
datasets: [{
data: Array(50).fill(0),
borderColor: '#10b981',
backgroundColor: 'rgba(16, 185, 129, 0.1)',
fill: true,
tension: 0.4
}]
},
options: {
...chartOptions,
scales: {
...chartOptions.scales,
y: { beginAtZero: true, max: 1000 }
}
}
});
// 추가 차트들도 동일한 방식으로 초기화...
}
startDataCollection() {
setInterval(() => {
this.updateMetrics();
}, 1000);
}
updateMetrics() {
// Unity에서 메트릭 데이터 수집
if (window.unityInstance) {
window.unityInstance.SendMessage('AITProfiler', 'GetCurrentMetrics', '');
}
// 브라우저 메트릭 수집
this.collectBrowserMetrics();
}
collectBrowserMetrics() {
// 메모리 사용량
if (performance.memory) {
const memoryUsage = Math.round(performance.memory.usedJSHeapSize / 1024 / 1024);
this.updateChart('memory', memoryUsage);
document.getElementById('memory-usage').textContent = memoryUsage + ' MB';
}
// CPU 사용률 (근사치)
const cpuUsage = Math.random() * 100; // 실제로는 더 정교한 계산 필요
this.updateChart('cpu', cpuUsage);
document.getElementById('cpu-usage').textContent = Math.round(cpuUsage) + '%';
}
updateChart(chartName, value) {
const chart = this.charts[chartName];
if (chart) {
chart.data.datasets[0].data.shift();
chart.data.datasets[0].data.push(value);
chart.update('none');
}
}
// Unity에서 호출되는 메서드들
updateFromUnity(metricsJson) {
const metrics = JSON.parse(metricsJson);
// GPU 사용률
document.getElementById('gpu-usage').textContent = metrics.gpuUsage + '%';
this.updateChart('gpu', metrics.gpuUsage);
// 드로우콜 수
document.getElementById('draw-calls').textContent = metrics.drawCalls;
this.updateChart('drawcalls', metrics.drawCalls);
// 경고 표시
this.checkThresholds(metrics);
}
checkThresholds(metrics) {
// CPU 사용률 경고
const cpuCard = document.querySelector('#cpu-usage').closest('.metric-card');
if (metrics.cpuUsage > 80) {
cpuCard.classList.add('alert-critical');
} else if (metrics.cpuUsage > 60) {
cpuCard.classList.add('alert-warning');
} else {
cpuCard.classList.remove('alert-critical', 'alert-warning');
}
// 메모리 사용량 경고
const memoryCard = document.querySelector('#memory-usage').closest('.metric-card');
if (metrics.memoryUsage > 500) {
memoryCard.classList.add('alert-critical');
} else if (metrics.memoryUsage > 300) {
memoryCard.classList.add('alert-warning');
} else {
memoryCard.classList.remove('alert-critical', 'alert-warning');
}
}
}
// 대시보드 초기화
window.profileDashboard = new AITProfilerDashboard();
// Unity에서 호출할 수 있도록 전역 함수로 노출
window.updateProfilerFromUnity = function(metricsJson) {
window.profileDashboard.updateFromUnity(metricsJson);
};
</script>
</body>
</html>2. 메모리 누수 탐지기
c#
// Unity C# - 메모리 누수 탐지
using UnityEngine;
using System.Collections.Generic;
using System.Linq;
public class AITMemoryLeakDetector : MonoBehaviour
{
[System.Serializable]
public class MemorySnapshot
{
public long totalMemory;
public long managedMemory;
public long nativeMemory;
public long graphicsMemory;
public long audioMemory;
public int gameObjectCount;
public int textureCount;
public int meshCount;
public float timestamp;
}
private List<MemorySnapshot> snapshots = new List<MemorySnapshot>();
private Dictionary<string, int> objectCounts = new Dictionary<string, int>();
private float lastSnapshotTime;
private const float SNAPSHOT_INTERVAL = 10f; // 10초마다 스냅샷
private void Start()
{
InvokeRepeating(nameof(TakeMemorySnapshot), 0f, SNAPSHOT_INTERVAL);
InvokeRepeating(nameof(AnalyzeMemoryLeaks), 30f, 30f); // 30초마다 분석
}
private void TakeMemorySnapshot()
{
var snapshot = new MemorySnapshot
{
totalMemory = UnityEngine.Profiling.Profiler.GetTotalAllocatedMemory(UnityEngine.Profiling.Profiler.Area.Total),
managedMemory = System.GC.GetTotalMemory(false),
nativeMemory = UnityEngine.Profiling.Profiler.GetTotalAllocatedMemory(UnityEngine.Profiling.Profiler.Area.Native),
graphicsMemory = UnityEngine.Profiling.Profiler.GetTotalAllocatedMemory(UnityEngine.Profiling.Profiler.Area.Rendering),
audioMemory = UnityEngine.Profiling.Profiler.GetTotalAllocatedMemory(UnityEngine.Profiling.Profiler.Area.Audio),
gameObjectCount = FindObjectsOfType<GameObject>().Length,
textureCount = Resources.FindObjectsOfTypeAll<Texture>().Length,
meshCount = Resources.FindObjectsOfTypeAll<Mesh>().Length,
timestamp = Time.realtimeSinceStartup
};
snapshots.Add(snapshot);
// 100개 이상 쌓이면 오래된 것 제거
if (snapshots.Count > 100)
{
snapshots.RemoveAt(0);
}
// 오브젝트 타입별 개수 추적
TrackObjectCounts();
}
private void TrackObjectCounts()
{
objectCounts.Clear();
// 모든 GameObject 타입 추적
var allObjects = FindObjectsOfType<MonoBehaviour>();
foreach (var obj in allObjects)
{
string typeName = obj.GetType().Name;
if (objectCounts.ContainsKey(typeName))
objectCounts[typeName]++;
else
objectCounts[typeName] = 1;
}
}
private void AnalyzeMemoryLeaks()
{
if (snapshots.Count < 3) return;
var recent = snapshots.Skip(snapshots.Count - 3).Take(3).ToArray();
// 메모리 증가 추세 분석
bool isIncreasingTrend = true;
for (int i = 1; i < recent.Length; i++)
{
if (recent[i].totalMemory <= recent[i-1].totalMemory)
{
isIncreasingTrend = false;
break;
}
}
if (isIncreasingTrend)
{
long memoryIncrease = recent[2].totalMemory - recent[0].totalMemory;
float timeSpan = recent[2].timestamp - recent[0].timestamp;
long increaseRate = (long)(memoryIncrease / timeSpan); // bytes per second
if (increaseRate > 1024 * 1024) // 1MB/s 이상 증가
{
Debug.LogWarning($"[AIT MemLeak] 메모리 누수 의심: {increaseRate / 1024 / 1024} MB/s 증가");
LogMemoryLeakWarning(increaseRate, recent);
}
}
// GameObject 수 급증 감지
DetectObjectCountSpikes();
// 텍스처 메모리 누수 감지
DetectTextureLeaks(recent);
}
private void DetectObjectCountSpikes()
{
if (snapshots.Count < 5) return;
var lastFive = snapshots.Skip(snapshots.Count - 5).ToArray();
var avgObjectCount = lastFive.Take(4).Average(s => s.gameObjectCount);
var currentCount = lastFive[4].gameObjectCount;
if (currentCount > avgObjectCount * 1.5f) // 50% 이상 급증
{
Debug.LogWarning($"[AIT MemLeak] GameObject 급증 감지: {currentCount} (평균: {avgObjectCount:F0})");
// 어떤 타입의 오브젝트가 증가했는지 분석
AnalyzeObjectTypeIncrease();
}
}
private void AnalyzeObjectTypeIncrease()
{
// 이전 프레임의 오브젝트 카운트와 비교 (간단한 구현을 위해 생략)
foreach (var kvp in objectCounts.OrderByDescending(x => x.Value).Take(10))
{
Debug.Log($"[AIT MemLeak] {kvp.Key}: {kvp.Value} 개");
}
}
private void DetectTextureLeaks(MemorySnapshot[] recent)
{
// 텍스처 메모리 지속적 증가 감지
bool textureMemoryIncreasing = true;
for (int i = 1; i < recent.Length; i++)
{
// 그래픽 메모리 증가 추세 확인
if (recent[i].graphicsMemory <= recent[i-1].graphicsMemory)
{
textureMemoryIncreasing = false;
break;
}
}
if (textureMemoryIncreasing)
{
long textureIncrease = recent[2].graphicsMemory - recent[0].graphicsMemory;
if (textureIncrease > 10 * 1024 * 1024) // 10MB 이상 증가
{
Debug.LogWarning($"[AIT MemLeak] 텍스처 메모리 누수 의심: {textureIncrease / 1024 / 1024} MB 증가");
// 활성 텍스처 분석
AnalyzeActiveTextures();
}
}
}
private void AnalyzeActiveTextures()
{
var textures = Resources.FindObjectsOfTypeAll<Texture>();
var textureInfo = new Dictionary<string, int>();
foreach (var texture in textures)
{
string key = $"{texture.GetType().Name}_{texture.width}x{texture.height}";
if (textureInfo.ContainsKey(key))
textureInfo[key]++;
else
textureInfo[key] = 1;
}
Debug.Log("[AIT MemLeak] 활성 텍스처 분석:");
foreach (var kvp in textureInfo.OrderByDescending(x => x.Value).Take(10))
{
Debug.Log($" {kvp.Key}: {kvp.Value} 개");
}
}
private void LogMemoryLeakWarning(long increaseRate, MemorySnapshot[] snapshots)
{
var leakInfo = new
{
increaseRate = increaseRate,
totalMemory = snapshots[2].totalMemory,
managedMemory = snapshots[2].managedMemory,
nativeMemory = snapshots[2].nativeMemory,
timestamp = System.DateTime.Now.ToString(),
gameObjectCount = snapshots[2].gameObjectCount,
textureCount = snapshots[2].textureCount
};
// JSON으로 직렬화하여 JavaScript에 전달
string json = JsonUtility.ToJson(leakInfo);
#if UNITY_WEBGL && !UNITY_EDITOR
Application.ExternalCall("logMemoryLeak", json);
#endif
}
public MemorySnapshot GetCurrentSnapshot()
{
return snapshots.LastOrDefault();
}
public List<MemorySnapshot> GetSnapshotHistory(int count = 10)
{
return snapshots.Skip(Mathf.Max(0, snapshots.Count - count)).ToList();
}
}코드 예제 및 설정
1. 성능 임계값 설정
json
// ait-profile-config.json
{
"thresholds": {
"cpu": {
"warning": 60,
"critical": 80,
"unit": "percent"
},
"memory": {
"warning": 300,
"critical": 500,
"unit": "MB"
},
"drawCalls": {
"warning": 200,
"critical": 500,
"unit": "count"
},
"frameRate": {
"warning": 30,
"critical": 15,
"unit": "fps"
},
"networkLatency": {
"warning": 1000,
"critical": 3000,
"unit": "ms"
},
"batteryDrain": {
"warning": 10,
"critical": 20,
"unit": "percent_per_hour"
}
},
"alerting": {
"enabled": true,
"channels": ["console", "remote", "unity"],
"remoteEndpoint": "https://analytics.apps-in-toss.io/alerts"
},
"sampling": {
"metricsInterval": 100,
"snapshotInterval": 10000,
"historyLimit": 1000
}
}2. 자동화된 성능 테스트
c#
// Unity C# - 성능 테스트 자동화
using UnityEngine;
using System.Collections;
using System.Collections.Generic;
public class AITPerformanceTestSuite : MonoBehaviour
{
[System.Serializable]
public class PerformanceTest
{
public string testName;
public System.Action testAction;
public float expectedFrameRate;
public long maxMemoryUsage;
public int maxDrawCalls;
public float timeoutSeconds;
}
private List<PerformanceTest> tests = new List<PerformanceTest>();
private AITWebGLProfiler profiler;
private void Start()
{
profiler = GetComponent<AITWebGLProfiler>();
SetupPerformanceTests();
StartCoroutine(RunAllTests());
}
private void SetupPerformanceTests()
{
// GPU 스트레스 테스트
tests.Add(new PerformanceTest
{
testName = "GPU 스트레스 테스트",
testAction = () => StartCoroutine(GPUStressTest()),
expectedFrameRate = 30f,
maxMemoryUsage = 200 * 1024 * 1024, // 200MB
maxDrawCalls = 300,
timeoutSeconds = 30f
});
// 메모리 할당 테스트
tests.Add(new PerformanceTest
{
testName = "메모리 할당 테스트",
testAction = () => StartCoroutine(MemoryAllocationTest()),
expectedFrameRate = 45f,
maxMemoryUsage = 150 * 1024 * 1024, // 150MB
maxDrawCalls = 100,
timeoutSeconds = 20f
});
// 네트워크 부하 테스트
tests.Add(new PerformanceTest
{
testName = "네트워크 부하 테스트",
testAction = () => StartCoroutine(NetworkLoadTest()),
expectedFrameRate = 50f,
maxMemoryUsage = 100 * 1024 * 1024, // 100MB
maxDrawCalls = 50,
timeoutSeconds = 15f
});
}
private IEnumerator RunAllTests()
{
Debug.Log("[AIT PerfTest] 성능 테스트 시작");
foreach (var test in tests)
{
Debug.Log($"[AIT PerfTest] 실행 중: {test.testName}");
var startSnapshot = profiler.GetCurrentSnapshot();
float startTime = Time.realtimeSinceStartup;
test.testAction.Invoke();
// 테스트 완료까지 대기 (타임아웃 고려)
float elapsedTime = 0f;
while (elapsedTime < test.timeoutSeconds)
{
yield return new WaitForSeconds(0.1f);
elapsedTime = Time.realtimeSinceStartup - startTime;
// 테스트별 종료 조건 확인
if (IsTestCompleted(test))
break;
}
var endSnapshot = profiler.GetCurrentSnapshot();
EvaluateTestResult(test, startSnapshot, endSnapshot);
// 테스트 간 정리 시간
yield return new WaitForSeconds(2f);
}
Debug.Log("[AIT PerfTest] 모든 성능 테스트 완료");
GenerateTestReport();
}
private IEnumerator GPUStressTest()
{
// 대량의 파티클 생성
GameObject particleContainer = new GameObject("ParticleStressTest");
for (int i = 0; i < 10; i++)
{
GameObject particles = new GameObject($"Particles_{i}");
particles.transform.parent = particleContainer.transform;
var particleSystem = particles.AddComponent<ParticleSystem>();
var main = particleSystem.main;
main.maxParticles = 1000;
main.startLifetime = 5f;
yield return new WaitForSeconds(0.1f);
}
// 5초간 실행
yield return new WaitForSeconds(5f);
// 정리
DestroyImmediate(particleContainer);
}
private IEnumerator MemoryAllocationTest()
{
List<Texture2D> textures = new List<Texture2D>();
// 텍스처 대량 생성
for (int i = 0; i < 50; i++)
{
var texture = new Texture2D(512, 512);
textures.Add(texture);
yield return null;
}
// 3초 대기
yield return new WaitForSeconds(3f);
// 메모리 해제
foreach (var texture in textures)
{
DestroyImmediate(texture);
}
// 가비지 컬렉션 강제 실행
System.GC.Collect();
Resources.UnloadUnusedAssets();
}
private IEnumerator NetworkLoadTest()
{
// 동시 다중 네트워크 요청
for (int i = 0; i < 10; i++)
{
StartCoroutine(SimulateNetworkRequest($"test_request_{i}"));
yield return new WaitForSeconds(0.1f);
}
yield return new WaitForSeconds(5f);
}
private IEnumerator SimulateNetworkRequest(string requestId)
{
// 네트워크 요청 시뮬레이션
float delay = Random.Range(0.5f, 2f);
yield return new WaitForSeconds(delay);
Debug.Log($"[AIT PerfTest] 네트워크 요청 완료: {requestId}");
}
private bool IsTestCompleted(PerformanceTest test)
{
// 테스트별 완료 조건 확인 로직
return true; // 단순화
}
private void EvaluateTestResult(PerformanceTest test,
AITWebGLProfiler.WebGLMetrics start, AITWebGLProfiler.WebGLMetrics end)
{
bool passed = true;
List<string> failures = new List<string>();
// 프레임레이트 확인
float currentFPS = 1f / Time.deltaTime;
if (currentFPS < test.expectedFrameRate)
{
passed = false;
failures.Add($"낮은 FPS: {currentFPS:F1} < {test.expectedFrameRate}");
}
// 메모리 사용량 확인
if (end.jsHeapUsed > test.maxMemoryUsage)
{
passed = false;
failures.Add($"메모리 초과: {end.jsHeapUsed} > {test.maxMemoryUsage}");
}
// 드로우콜 수 확인
if (end.drawCalls > test.maxDrawCalls)
{
passed = false;
failures.Add($"드로우콜 초과: {end.drawCalls} > {test.maxDrawCalls}");
}
string result = passed ? "통과" : "실패";
Debug.Log($"[AIT PerfTest] {test.testName}: {result}");
if (!passed)
{
foreach (var failure in failures)
{
Debug.LogWarning($" - {failure}");
}
}
}
private void GenerateTestReport()
{
var report = new
{
timestamp = System.DateTime.Now.ToString(),
testCount = tests.Count,
platform = Application.platform.ToString(),
unityVersion = Application.unityVersion,
deviceInfo = SystemInfo.deviceModel,
results = "상세 결과는 콘솔 로그 참조"
};
string json = JsonUtility.ToJson(report, true);
Debug.Log($"[AIT PerfTest] 테스트 리포트:\n{json}");
#if UNITY_WEBGL && !UNITY_EDITOR
Application.ExternalCall("savePerformanceTestReport", json);
#endif
}
}문제 해결 및 디버깅
1. 일반적인 성능 문제 진단
c#
// Unity C# - 성능 문제 진단기
public class AITPerformanceDiagnostic : MonoBehaviour
{
public enum PerformanceIssue
{
HighCPUUsage,
MemoryLeak,
ExcessiveDrawCalls,
LowFrameRate,
NetworkLatency,
BatteryDrain
}
private Dictionary<PerformanceIssue, System.Action> diagnosticActions;
private void Start()
{
InitializeDiagnostics();
}
private void InitializeDiagnostics()
{
diagnosticActions = new Dictionary<PerformanceIssue, System.Action>
{
{ PerformanceIssue.HighCPUUsage, DiagnoseCPUUsage },
{ PerformanceIssue.MemoryLeak, DiagnoseMemoryLeaks },
{ PerformanceIssue.ExcessiveDrawCalls, DiagnoseDrawCalls },
{ PerformanceIssue.LowFrameRate, DiagnoseFrameRate },
{ PerformanceIssue.NetworkLatency, DiagnoseNetworkIssues },
{ PerformanceIssue.BatteryDrain, DiagnoseBatteryDrain }
};
}
public void RunDiagnostic(PerformanceIssue issue)
{
Debug.Log($"[AIT Diagnostic] {issue} 진단 시작");
if (diagnosticActions.ContainsKey(issue))
{
diagnosticActions[issue].Invoke();
}
}
private void DiagnoseCPUUsage()
{
Debug.Log("[AIT Diagnostic] CPU 사용률 분석");
// 프로파일러 데이터 분석
var renderTime = UnityEngine.Profiling.Profiler.GetStatValue(
UnityEngine.Profiling.ProfilerArea.Rendering,
UnityEngine.Profiling.ProfilerStatisticsNames.RenderThreadTime);
var mainTime = UnityEngine.Profiling.Profiler.GetStatValue(
UnityEngine.Profiling.ProfilerArea.CPU,
UnityEngine.Profiling.ProfilerStatisticsNames.MainThreadTime);
Debug.Log($" 렌더링 시간: {renderTime}ms");
Debug.Log($" 메인 스레드 시간: {mainTime}ms");
// CPU 집약적 작업 감지
if (renderTime > 16.67f) // 60fps 기준
{
Debug.LogWarning(" 렌더링 성능 문제 감지");
SuggestRenderingOptimizations();
}
if (mainTime > 16.67f)
{
Debug.LogWarning(" 메인 스레드 성능 문제 감지");
SuggestMainThreadOptimizations();
}
}
private void SuggestRenderingOptimizations()
{
Debug.Log("[AIT Suggestion] 렌더링 최적화 권장사항:");
Debug.Log(" - 드로우콜 수 줄이기 (배칭 활용)");
Debug.Log(" - 텍스처 아틀라스 사용");
Debug.Log(" - LOD (Level of Detail) 구현");
Debug.Log(" - 오클루전 컬링 활성화");
Debug.Log(" - 쉐이더 복잡도 검토");
}
private void SuggestMainThreadOptimizations()
{
Debug.Log("[AIT Suggestion] 메인 스레드 최적화 권장사항:");
Debug.Log(" - 무거운 연산을 코루틴으로 분할");
Debug.Log(" - 오브젝트 풀링 사용");
Debug.Log(" - Update 호출 최적화");
Debug.Log(" - 가비지 컬렉션 최소화");
}
private void DiagnoseMemoryLeaks()
{
Debug.Log("[AIT Diagnostic] 메모리 누수 분석");
// 현재 메모리 상태
long totalMemory = System.GC.GetTotalMemory(false);
long managedMemory = UnityEngine.Profiling.Profiler.GetTotalAllocatedMemory(
UnityEngine.Profiling.Profiler.Area.Total);
Debug.Log($" 총 메모리: {totalMemory / 1024 / 1024} MB");
Debug.Log($" 관리 메모리: {managedMemory / 1024 / 1024} MB");
// 리소스 개수 확인
var gameObjects = FindObjectsOfType<GameObject>().Length;
var textures = Resources.FindObjectsOfTypeAll<Texture>().Length;
var meshes = Resources.FindObjectsOfTypeAll<Mesh>().Length;
Debug.Log($" GameObject 수: {gameObjects}");
Debug.Log($" 텍스처 수: {textures}");
Debug.Log($" 메시 수: {meshes}");
// 의심스러운 수치 감지
if (gameObjects > 1000)
{
Debug.LogWarning(" GameObject 수 과다 - 오브젝트 풀링 고려");
}
if (textures > 100)
{
Debug.LogWarning(" 텍스처 수 과다 - 텍스처 아틀라스 고려");
}
}
private void DiagnoseDrawCalls()
{
Debug.Log("[AIT Diagnostic] 드로우콜 분석");
var drawCalls = UnityEngine.Profiling.Profiler.GetStatValue(
UnityEngine.Profiling.ProfilerArea.Rendering,
UnityEngine.Profiling.ProfilerStatisticsNames.DrawCallsCount);
var batches = UnityEngine.Profiling.Profiler.GetStatValue(
UnityEngine.Profiling.ProfilerArea.Rendering,
UnityEngine.Profiling.ProfilerStatisticsNames.BatchesCount);
Debug.Log($" 드로우콜 수: {drawCalls}");
Debug.Log($" 배치 수: {batches}");
if (drawCalls > 200)
{
Debug.LogWarning(" 드로우콜 수 과다");
Debug.Log(" 권장사항: 스태틱 배칭, 동적 배칭, GPU 인스턴싱 활용");
}
float batchingEfficiency = batches > 0 ? (float)drawCalls / batches : 0;
Debug.Log($" 배칭 효율성: {batchingEfficiency:F2}");
}
private void DiagnoseFrameRate()
{
Debug.Log("[AIT Diagnostic] 프레임레이트 분석");
float currentFPS = 1f / Time.deltaTime;
float targetFPS = Application.targetFrameRate > 0 ? Application.targetFrameRate : 60;
Debug.Log($" 현재 FPS: {currentFPS:F1}");
Debug.Log($" 목표 FPS: {targetFPS}");
if (currentFPS < targetFPS * 0.8f) // 80% 이하
{
Debug.LogWarning(" 프레임레이트 저하 감지");
// 원인 분석
AnalyzeFrameRateBottlenecks();
}
}
private void AnalyzeFrameRateBottlenecks()
{
Debug.Log(" 프레임레이트 저하 원인 분석:");
// CPU vs GPU 바운드 확인
var cpuTime = UnityEngine.Profiling.Profiler.GetStatValue(
UnityEngine.Profiling.ProfilerArea.CPU,
UnityEngine.Profiling.ProfilerStatisticsNames.MainThreadTime);
var gpuTime = UnityEngine.Profiling.Profiler.GetStatValue(
UnityEngine.Profiling.ProfilerArea.Rendering,
UnityEngine.Profiling.ProfilerStatisticsNames.RenderThreadTime);
if (cpuTime > gpuTime)
{
Debug.Log(" - CPU 바운드: 스크립트 최적화 필요");
}
else
{
Debug.Log(" - GPU 바운드: 그래픽 최적화 필요");
}
}
private void DiagnoseNetworkIssues()
{
Debug.Log("[AIT Diagnostic] 네트워크 성능 분석");
// 네트워크 상태 확인 (WebGL에서는 제한적)
#if UNITY_WEBGL && !UNITY_EDITOR
Application.ExternalCall("analyzeNetworkPerformance");
#endif
Debug.Log(" 네트워크 최적화 권장사항:");
Debug.Log(" - 요청 크기 최소화");
Debug.Log(" - 압축 활용");
Debug.Log(" - 캐싱 전략 구현");
Debug.Log(" - 연결 재사용");
}
private void DiagnoseBatteryDrain()
{
Debug.Log("[AIT Diagnostic] 배터리 소모 분석");
// 배터리 소모 주요 원인들 분석
Debug.Log(" 배터리 소모 최적화 권장사항:");
Debug.Log(" - 프레임레이트 제한 (30fps 고려)");
Debug.Log(" - 백그라운드에서 업데이트 중단");
Debug.Log(" - 불필요한 센서 사용 중단");
Debug.Log(" - 네트워크 요청 최소화");
Debug.Log(" - 화면 밝기 고려한 UI 디자인");
}
}모범 사례
1. 지속적 성능 모니터링
c#
// 프로덕션 환경에서의 성능 모니터링
public class AITProductionProfiler : MonoBehaviour
{
private const float REPORT_INTERVAL = 300f; // 5분마다 리포트
private bool isMonitoring = false;
private void Start()
{
// 프로덕션 환경에서만 활성화
if (Application.isEditor || Debug.isDebugBuild)
return;
StartCoroutine(MonitorPerformance());
}
private IEnumerator MonitorPerformance()
{
isMonitoring = true;
while (isMonitoring)
{
// 경량화된 성능 메트릭 수집
var metrics = CollectLightweightMetrics();
// 임계값 초과 시에만 전송
if (ShouldReportMetrics(metrics))
{
SendMetricsToAnalytics(metrics);
}
yield return new WaitForSeconds(REPORT_INTERVAL);
}
}
private PerformanceMetrics CollectLightweightMetrics()
{
return new PerformanceMetrics
{
fps = Mathf.RoundToInt(1f / Time.deltaTime),
memoryUsage = System.GC.GetTotalMemory(false),
loadTime = Time.realtimeSinceStartup,
sessionDuration = Time.time,
crashCount = 0, // 크래시 추적 시스템 필요
platform = Application.platform.ToString()
};
}
private bool ShouldReportMetrics(PerformanceMetrics metrics)
{
// 성능 이슈가 있을 때만 전송
return metrics.fps < 20 ||
metrics.memoryUsage > 500 * 1024 * 1024; // 500MB
}
private void SendMetricsToAnalytics(PerformanceMetrics metrics)
{
string json = JsonUtility.ToJson(metrics);
#if UNITY_WEBGL && !UNITY_EDITOR
Application.ExternalCall("sendPerformanceMetrics", json);
#endif
}
}2. 성능 최적화 우선순위 가이드
메모리 최적화 (최우선)
- WebGL 환경에서 메모리는 가장 제한적인 리소스
- 가비지 컬렉션 최소화가 핵심
드로우콜 최적화
- 배칭을 통한 드로우콜 감소
- 텍스처 아틀라스 활용
CPU 최적화
- Update 호출 최적화
- 코루틴을 통한 작업 분산
네트워크 최적화
- 요청 크기 및 빈도 최적화
- 캐싱 전략 구현
이러한 심화 프로파일링 도구를 통해 개발자는 앱인토스 플랫폼의 특성을 고려한 정밀한 성능 분석과 최적화를 수행할 수 있어요.
