앱인토스 개발자센터 로고
Skip to content

심화 분석 도구

심화 프로파일링 도구는 앱인토스 미니앱에서 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. 성능 최적화 우선순위 가이드

  1. 메모리 최적화 (최우선)

    • WebGL 환경에서 메모리는 가장 제한적인 리소스
    • 가비지 컬렉션 최소화가 핵심
  2. 드로우콜 최적화

    • 배칭을 통한 드로우콜 감소
    • 텍스처 아틀라스 활용
  3. CPU 최적화

    • Update 호출 최적화
    • 코루틴을 통한 작업 분산
  4. 네트워크 최적화

    • 요청 크기 및 빈도 최적화
    • 캐싱 전략 구현

이러한 심화 프로파일링 도구를 통해 개발자는 앱인토스 플랫폼의 특성을 고려한 정밀한 성능 분석과 최적화를 수행할 수 있어요.