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

런타임 성능 모니터링

앱인토스 Unity 게임의 실시간 성능을 모니터링하고 분석하여 최적의 사용자 경험을 제공하는 시스템을 구현해요.


1. 실시간 성능 모니터링 시스템

통합 성능 모니터

c#
using UnityEngine;
using System.Collections.Generic;
using System.Linq;
using UnityEngine.Profiling;

public class AppsInTossPerformanceMonitor : MonoBehaviour
{
    public static AppsInTossPerformanceMonitor Instance { get; private set; }
    
    [System.Serializable]
    public class PerformanceThresholds
    {
        [Header("FPS 임계값")]
        public float criticalFPS = 15f;
        public float warningFPS = 25f;
        public float targetFPS = 30f;
        
        [Header("메모리 임계값 (MB)")]
        public float criticalMemoryMB = 250f;
        public float warningMemoryMB = 200f;
        public float targetMemoryMB = 150f;
        
        [Header("배터리 효율성")]
        public float batteryEfficiencyWarning = 1.3f; // 네이티브 앱 대비
        public float batteryEfficiencyCritical = 1.5f;
        
        [Header("네트워크 응답시간 (ms)")]
        public float networkWarningMs = 2000f;
        public float networkCriticalMs = 5000f;
    }
    
    [Header("모니터링 설정")]
    public bool enableRealTimeMonitoring = true;
    public bool enableTossAnalytics = true;
    public float monitoringInterval = 1f;
    public int maxDataPoints = 300; // 5분간 데이터
    
    [Header("성능 임계값")]
    public PerformanceThresholds thresholds;
    
    [Header("UI 표시")]
    public bool showDebugUI = false;
    public KeyCode toggleUIKey = KeyCode.F1;
    
    // 성능 데이터 구조
    [System.Serializable]
    public class PerformanceSnapshot
    {
        public float timestamp;
        public float fps;
        public float frameTime;
        public long memoryUsageMB;
        public int drawCalls;
        public int triangles;
        public float cpuTime;
        public float gpuTime;
        public float batteryLevel;
        public string deviceTemperature;
        public NetworkStatus networkStatus;
    }
    
    public enum NetworkStatus
    {
        Good,
        Slow,
        Poor,
        Disconnected
    }
    
    public enum PerformanceAlert
    {
        FPSCritical,
        FPSWarning,
        MemoryCritical,
        MemoryWarning,
        BatteryCritical,
        NetworkSlow,
        DeviceOverheating
    }
    
    // 데이터 저장
    private List<PerformanceSnapshot> performanceHistory = new List<PerformanceSnapshot>();
    private Queue<float> fpsHistory = new Queue<float>();
    private Dictionary<PerformanceAlert, float> lastAlertTime = new Dictionary<PerformanceAlert, float>();
    
    // UI 관련
    private GUIStyle debugUIStyle;
    private bool showUI = false;
    private Rect uiRect = new Rect(10, 10, 350, 400);
    
    // 이벤트
    public System.Action<PerformanceSnapshot> OnPerformanceUpdated;
    public System.Action<PerformanceAlert, float> OnPerformanceAlert;
    
    void Awake()
    {
        if (Instance == null)
        {
            Instance = this;
            DontDestroyOnLoad(gameObject);
            InitializeMonitoring();
        }
        else
        {
            Destroy(gameObject);
        }
    }
    
    void InitializeMonitoring()
    {
        Debug.Log("앱인토스 성능 모니터링 시작");
        
        // 알림 시간 초기화
        System.Enum.GetValues(typeof(PerformanceAlert)).Cast<PerformanceAlert>()
            .ToList().ForEach(alert => lastAlertTime[alert] = 0f);
        
        // 모니터링 시작
        if (enableRealTimeMonitoring)
        {
            InvokeRepeating(nameof(CollectPerformanceData), 0f, monitoringInterval);
        }
        
        // 앱인토스 특화 이벤트 리스너
        AppsInToss.OnApplicationPause += OnApplicationPause;
        AppsInToss.OnApplicationFocus += OnApplicationFocus;
        AppsInToss.OnLowMemoryWarning += OnLowMemoryWarning;
    }
    
    void Update()
    {
        // 디버그 UI 토글
        if (Input.GetKeyDown(toggleUIKey))
        {
            showUI = !showUI;
        }
    }
    
    void CollectPerformanceData()
    {
        var snapshot = new PerformanceSnapshot
        {
            timestamp = Time.realtimeSinceStartup,
            fps = CalculateCurrentFPS(),
            frameTime = Time.unscaledDeltaTime * 1000f,
            memoryUsageMB = Profiler.GetTotalAllocatedMemory(false) / (1024 * 1024),
            drawCalls = GetDrawCalls(),
            triangles = GetTriangles(),
            cpuTime = Time.deltaTime * 1000f,
            batteryLevel = SystemInfo.batteryLevel,
            deviceTemperature = GetDeviceTemperature(),
            networkStatus = GetNetworkStatus()
        };
        
        // 데이터 저장
        performanceHistory.Add(snapshot);
        
        // 데이터 크기 제한
        if (performanceHistory.Count > maxDataPoints)
        {
            performanceHistory.RemoveAt(0);
        }
        
        // FPS 히스토리 관리
        fpsHistory.Enqueue(snapshot.fps);
        if (fpsHistory.Count > 60) // 1분간 FPS
        {
            fpsHistory.Dequeue();
        }
        
        // 성능 분석 및 경고
        AnalyzePerformance(snapshot);
        
        // 이벤트 발생
        OnPerformanceUpdated?.Invoke(snapshot);
        
        // 앱인토스 분석 시스템에 전송
        if (enableTossAnalytics)
        {
            SendPerformanceAnalytics(snapshot);
        }
    }
    
    float CalculateCurrentFPS()
    {
        if (Time.unscaledDeltaTime > 0)
        {
            return 1.0f / Time.unscaledDeltaTime;
        }
        return 0f;
    }
    
    int GetDrawCalls()
    {
        // Unity 2019.3 이상에서는 다른 방법 사용 필요
        return UnityEngine.Rendering.FrameDebugger.enabled ? 
            UnityEngine.Rendering.FrameDebugger.GetFrameEventCount() : 0;
    }
    
    int GetTriangles()
    {
        // 현재 프레임의 삼각형 수 추정
        var renderers = FindObjectsOfType<MeshRenderer>();
        int totalTriangles = 0;
        
        foreach (var renderer in renderers)
        {
            if (renderer.isVisible)
            {
                var meshFilter = renderer.GetComponent<MeshFilter>();
                if (meshFilter != null && meshFilter.mesh != null)
                {
                    totalTriangles += meshFilter.mesh.triangles.Length / 3;
                }
            }
        }
        
        return totalTriangles;
    }
    
    string GetDeviceTemperature()
    {
        // 플랫폼별 온도 정보 (앱인토스 네이티브 API 사용)
        return AppsInToss.GetDeviceTemperature();
    }
    
    NetworkStatus GetNetworkStatus()
    {
        // 네트워크 상태 확인 (앱인토스 네이티브 API 사용)
        var pingTime = AppsInToss.GetNetworkPingTime();
        
        if (pingTime < 0) return NetworkStatus.Disconnected;
        else if (pingTime < 100) return NetworkStatus.Good;
        else if (pingTime < 500) return NetworkStatus.Slow;
        else return NetworkStatus.Poor;
    }
    
    void AnalyzePerformance(PerformanceSnapshot snapshot)
    {
        float currentTime = Time.realtimeSinceStartup;
        
        // FPS 체크
        if (snapshot.fps < thresholds.criticalFPS)
        {
            TriggerAlert(PerformanceAlert.FPSCritical, snapshot.fps, currentTime);
        }
        else if (snapshot.fps < thresholds.warningFPS)
        {
            TriggerAlert(PerformanceAlert.FPSWarning, snapshot.fps, currentTime);
        }
        
        // 메모리 체크
        if (snapshot.memoryUsageMB > thresholds.criticalMemoryMB)
        {
            TriggerAlert(PerformanceAlert.MemoryCritical, snapshot.memoryUsageMB, currentTime);
        }
        else if (snapshot.memoryUsageMB > thresholds.warningMemoryMB)
        {
            TriggerAlert(PerformanceAlert.MemoryWarning, snapshot.memoryUsageMB, currentTime);
        }
        
        // 배터리 효율성 체크
        float batteryDrain = CalculateBatteryDrainRate();
        if (batteryDrain > thresholds.batteryEfficiencyCritical)
        {
            TriggerAlert(PerformanceAlert.BatteryCritical, batteryDrain, currentTime);
        }
        
        // 네트워크 상태 체크
        if (snapshot.networkStatus == NetworkStatus.Poor || snapshot.networkStatus == NetworkStatus.Slow)
        {
            TriggerAlert(PerformanceAlert.NetworkSlow, 0f, currentTime);
        }
        
        // 기기 온도 체크
        if (snapshot.deviceTemperature.Contains("Hot") || snapshot.deviceTemperature.Contains("Overheating"))
        {
            TriggerAlert(PerformanceAlert.DeviceOverheating, 0f, currentTime);
        }
    }
    
    void TriggerAlert(PerformanceAlert alertType, float value, float currentTime)
    {
        // 중복 알림 방지 (30초 간격)
        if (currentTime - lastAlertTime[alertType] < 30f)
        {
            return;
        }
        
        lastAlertTime[alertType] = currentTime;
        
        Debug.LogWarning($"성능 알림: {alertType} - 값: {value:F2}");
        
        // 이벤트 발생
        OnPerformanceAlert?.Invoke(alertType, value);
        
        // 앱인토스에 성능 이슈 리포트
        ReportPerformanceIssue(alertType, value);
        
        // 자동 최적화 시도
        AttemptAutoOptimization(alertType, value);
    }
    
    float CalculateBatteryDrainRate()
    {
        // 배터리 소모율 계산 (단순화된 버전)
        if (performanceHistory.Count < 60) return 0f; // 1분 미만 데이터
        
        var recent = performanceHistory.Skip(performanceHistory.Count - 60).ToList();
        var startBattery = recent.First().batteryLevel;
        var endBattery = recent.Last().batteryLevel;
        
        if (startBattery <= 0 || endBattery <= 0) return 0f;
        
        float drainPerMinute = (startBattery - endBattery) * 60f;
        return drainPerMinute / 0.5f; // 네이티브 앱 기준 대비 비율
    }
    
    void ReportPerformanceIssue(PerformanceAlert alertType, float value)
    {
        var issueData = new Dictionary<string, object>
        {
            {"alert_type", alertType.ToString()},
            {"value", value},
            {"device_model", SystemInfo.deviceModel},
            {"memory_size", SystemInfo.systemMemorySize},
            {"graphics_device", SystemInfo.graphicsDeviceName},
            {"timestamp", System.DateTime.UtcNow.ToString("o")}
        };
        
        AppsInToss.ReportPerformanceIssue($"runtime_performance_{alertType.ToString().ToLower()}", issueData);
    }
    
    void AttemptAutoOptimization(PerformanceAlert alertType, float value)
    {
        switch (alertType)
        {
            case PerformanceAlert.FPSCritical:
            case PerformanceAlert.FPSWarning:
                OptimizeForFPS();
                break;
                
            case PerformanceAlert.MemoryCritical:
            case PerformanceAlert.MemoryWarning:
                OptimizeMemoryUsage();
                break;
                
            case PerformanceAlert.BatteryCritical:
                OptimizeForBattery();
                break;
                
            case PerformanceAlert.DeviceOverheating:
                ReducePerformanceLoad();
                break;
        }
    }
    
    void OptimizeForFPS()
    {
        // FPS 최적화
        QualitySettings.masterTextureLimit = Mathf.Min(QualitySettings.masterTextureLimit + 1, 3);
        QualitySettings.shadowDistance *= 0.8f;
        QualitySettings.lodBias *= 0.9f;
        
        Debug.Log("자동 FPS 최적화 적용");
    }
    
    void OptimizeMemoryUsage()
    {
        // 메모리 최적화
        System.GC.Collect();
        Resources.UnloadUnusedAssets();
        
        // 에셋 캐시 정리
        if (AppsInTossResourceManager.Instance != null)
        {
            AppsInTossResourceManager.Instance.ClearCache(ResourcePriority.Low);
        }
        
        Debug.Log("자동 메모리 최적화 적용");
    }
    
    void OptimizeForBattery()
    {
        // 배터리 최적화
        Application.targetFrameRate = Mathf.Min(Application.targetFrameRate, 30);
        QualitySettings.vSyncCount = 0;
        
        Debug.Log("자동 배터리 최적화 적용");
    }
    
    void ReducePerformanceLoad()
    {
        // 성능 부하 감소
        Application.targetFrameRate = 20;
        QualitySettings.masterTextureLimit = 2;
        QualitySettings.shadowDistance *= 0.5f;
        
        Debug.Log("기기 과열로 인한 성능 부하 감소");
    }
    
    void SendPerformanceAnalytics(PerformanceSnapshot snapshot)
    {
        // 10초마다 한 번씩만 전송 (데이터 절약)
        if (Mathf.FloorToInt(snapshot.timestamp) % 10 != 0) return;
        
        var analyticsData = new Dictionary<string, object>
        {
            {"fps", snapshot.fps},
            {"memory_mb", snapshot.memoryUsageMB},
            {"draw_calls", snapshot.drawCalls},
            {"triangles", snapshot.triangles},
            {"battery_level", snapshot.batteryLevel},
            {"network_status", snapshot.networkStatus.ToString()},
            {"device_temperature", snapshot.deviceTemperature},
            {"timestamp", System.DateTime.UtcNow.ToString("o")}
        };
        
        AppsInToss.SendAnalytics("runtime_performance", analyticsData);
    }
    
    // 이벤트 핸들러
    void OnApplicationPause(bool pauseStatus)
    {
        if (pauseStatus)
        {
            // 백그라운드 진입 시 성능 리포트 전송
            SendPerformanceSummary();
        }
        else
        {
            // 포그라운드 복귀 시 모니터링 재시작
            performanceHistory.Clear();
        }
    }
    
    void OnApplicationFocus(bool hasFocus)
    {
        if (hasFocus)
        {
            Debug.Log("앱 포커스 복귀 - 성능 모니터링 재시작");
        }
    }
    
    void OnLowMemoryWarning()
    {
        Debug.LogWarning("시스템 메모리 부족 경고");
        TriggerAlert(PerformanceAlert.MemoryCritical, GetCurrentMemoryUsage(), Time.realtimeSinceStartup);
        OptimizeMemoryUsage();
    }
    
    float GetCurrentMemoryUsage()
    {
        return Profiler.GetTotalAllocatedMemory(false) / (1024f * 1024f);
    }
    
    void SendPerformanceSummary()
    {
        if (performanceHistory.Count == 0) return;
        
        var summary = GeneratePerformanceSummary();
        
        var summaryData = new Dictionary<string, object>
        {
            {"session_duration", summary.sessionDuration},
            {"average_fps", summary.averageFPS},
            {"min_fps", summary.minFPS},
            {"max_memory_mb", summary.maxMemoryMB},
            {"average_memory_mb", summary.averageMemoryMB},
            {"total_alerts", summary.totalAlerts},
            {"timestamp", System.DateTime.UtcNow.ToString("o")}
        };
        
        AppsInToss.SendAnalytics("performance_session_summary", summaryData);
    }
    
    public PerformanceSummary GeneratePerformanceSummary()
    {
        if (performanceHistory.Count == 0)
        {
            return new PerformanceSummary();
        }
        
        var summary = new PerformanceSummary
        {
            sessionDuration = performanceHistory.Last().timestamp - performanceHistory.First().timestamp,
            averageFPS = performanceHistory.Average(p => p.fps),
            minFPS = performanceHistory.Min(p => p.fps),
            maxFPS = performanceHistory.Max(p => p.fps),
            averageMemoryMB = performanceHistory.Average(p => p.memoryUsageMB),
            maxMemoryMB = performanceHistory.Max(p => p.memoryUsageMB),
            totalAlerts = lastAlertTime.Values.Count(t => t > 0)
        };
        
        return summary;
    }
    
    // 디버그 UI
    void OnGUI()
    {
        if (!showUI || !showDebugUI) return;
        
        InitializeGUIStyle();
        
        uiRect = GUILayout.Window(0, uiRect, DrawDebugWindow, "앱인토스 성능 모니터", debugUIStyle);
    }
    
    void InitializeGUIStyle()
    {
        if (debugUIStyle == null)
        {
            debugUIStyle = new GUIStyle(GUI.skin.window);
            debugUIStyle.fontSize = 12;
        }
    }
    
    void DrawDebugWindow(int windowID)
    {
        if (performanceHistory.Count == 0)
        {
            GUILayout.Label("성능 데이터 수집 중...");
            GUI.DragWindow();
            return;
        }
        
        var latest = performanceHistory.Last();
        
        GUILayout.BeginVertical();
        
        // 현재 성능 정보
        GUILayout.Label($"FPS: {latest.fps:F1}", GetColorStyle(latest.fps, thresholds.warningFPS, thresholds.criticalFPS, false));
        GUILayout.Label($"메모리: {latest.memoryUsageMB}MB", GetColorStyle(latest.memoryUsageMB, thresholds.warningMemoryMB, thresholds.criticalMemoryMB, true));
        GUILayout.Label($"Draw Calls: {latest.drawCalls}");
        GUILayout.Label($"Triangles: {latest.triangles:N0}");
        GUILayout.Label($"배터리: {(latest.batteryLevel * 100):F0}%");
        GUILayout.Label($"네트워크: {latest.networkStatus}");
        GUILayout.Label($"온도: {latest.deviceTemperature}");
        
        GUILayout.Space(10);
        
        // 평균값
        if (performanceHistory.Count > 10)
        {
            float avgFPS = performanceHistory.Skip(performanceHistory.Count - 10).Average(p => p.fps);
            float avgMemory = performanceHistory.Skip(performanceHistory.Count - 10).Average(p => p.memoryUsageMB);
            
            GUILayout.Label($"10초 평균 FPS: {avgFPS:F1}");
            GUILayout.Label($"10초 평균 메모리: {avgMemory:F1}MB");
        }
        
        GUILayout.Space(10);
        
        // 제어 버튼
        if (GUILayout.Button("메모리 정리"))
        {
            OptimizeMemoryUsage();
        }
        
        if (GUILayout.Button("성능 리포트 전송"))
        {
            SendPerformanceSummary();
        }
        
        GUILayout.EndVertical();
        
        GUI.DragWindow();
    }
    
    GUIStyle GetColorStyle(float value, float warningThreshold, float criticalThreshold, bool higherIsWorse)
    {
        var style = new GUIStyle(GUI.skin.label);
        
        bool critical = higherIsWorse ? value > criticalThreshold : value < criticalThreshold;
        bool warning = higherIsWorse ? value > warningThreshold : value < warningThreshold;
        
        if (critical)
        {
            style.normal.textColor = Color.red;
        }
        else if (warning)
        {
            style.normal.textColor = Color.yellow;
        }
        else
        {
            style.normal.textColor = Color.green;
        }
        
        return style;
    }
    
    // 공개 API
    public PerformanceSnapshot GetCurrentPerformance()
    {
        return performanceHistory.LastOrDefault();
    }
    
    public List<PerformanceSnapshot> GetPerformanceHistory(int lastNSeconds = 60)
    {
        if (performanceHistory.Count == 0) return new List<PerformanceSnapshot>();
        
        float currentTime = performanceHistory.Last().timestamp;
        return performanceHistory.Where(p => currentTime - p.timestamp <= lastNSeconds).ToList();
    }
    
    public void SetMonitoringEnabled(bool enabled)
    {
        if (enabled && !IsInvoking(nameof(CollectPerformanceData)))
        {
            InvokeRepeating(nameof(CollectPerformanceData), 0f, monitoringInterval);
        }
        else if (!enabled && IsInvoking(nameof(CollectPerformanceData)))
        {
            CancelInvoke(nameof(CollectPerformanceData));
        }
    }
}

[System.Serializable]
public class PerformanceSummary
{
    public float sessionDuration;
    public float averageFPS;
    public float minFPS;
    public float maxFPS;
    public float averageMemoryMB;
    public float maxMemoryMB;
    public int totalAlerts;
}

실시간 성능 모니터링을 통해 사용자 경험을 지속적으로 개선하세요. 앱인토스 환경에서는 배터리 효율성과 메모리 사용량이 특히 중요해요.