跳到正文
cccskills
免费在 GitHub 公开

tools-unity-profiling

Unity profiling patterns including ProfilerMarkers, FrameTimingManager, memory profiling, and performance budgets.

中文简介正在准备中。当前显示原文说明。

查看安装方法

包含的文件(1)

  • SKILL.md15.6 KB

SKILL.md(原文)

安装前可以查看交给 AI 代理的指令内容。

Unity Profiling

Overview

Unity provides comprehensive profiling tools for CPU, GPU, memory, and frame timing analysis. This skill covers instrumentation patterns and performance optimization workflows.

When to Use

  • Identifying performance bottlenecks
  • Tracking memory allocations
  • Measuring frame times
  • Implementing performance budgets
  • Detecting hitches and stutters

ProfilerMarkers

Basic ProfilerMarker

using Unity.Profiling;

public class CombatSystem
{
    private static readonly ProfilerMarker s_UpdateMarker = new(
        ProfilerCategory.Scripts,
        "CombatSystem.Update"
    );
    
    public void Update()
    {
        s_UpdateMarker.Begin();
        try
        {
            ProcessCombat();
        }
        finally
        {
            s_UpdateMarker.End();
        }
    }
}

Auto-Dispose Pattern

public class AbilitySystem
{
    private static readonly ProfilerMarker s_ExecuteMarker = new("AbilitySystem.Execute");
    
    public void ExecuteAbility(Ability ability)
    {
        using (s_ExecuteMarker.Auto())
        {
            ability.Activate();
            ApplyEffects();
            PlayAnimation();
        }
    }
}

Nested Markers

public class GameLoop
{
    private static readonly ProfilerMarker s_UpdateMarker = new("GameLoop.Update");
    private static readonly ProfilerMarker s_PhysicsMarker = new("GameLoop.Physics");
    private static readonly ProfilerMarker s_AIMarker = new("GameLoop.AI");
    private static readonly ProfilerMarker s_RenderMarker = new("GameLoop.Render");
    
    public void Update()
    {
        using (s_UpdateMarker.Auto())
        {
            using (s_PhysicsMarker.Auto())
            {
                UpdatePhysics();
            }
            
            using (s_AIMarker.Auto())
            {
                UpdateAI();
            }
            
            using (s_RenderMarker.Auto())
            {
                PrepareRendering();
            }
        }
    }
}

ProfilerMarker with Metadata

private static readonly ProfilerMarker s_LoadAssetMarker = new(
    ProfilerCategory.Loading,
    "LoadAsset",
    new ProfilerMarkerDataUnit[]
    {
        ProfilerMarkerDataUnit.Bytes,
        ProfilerMarkerDataUnit.Count
    }
);

public void LoadAsset(string path)
{
    using (s_LoadAssetMarker.Auto())
    {
        // Marker metadata captures additional info
        var asset = Resources.Load(path);
    }
}

FrameTimingManager

Basic Frame Timing

public class FrameTimeTracker
{
    private readonly FrameTiming[] _frameTimings = new FrameTiming[1];
    
    public void CaptureFrameTiming()
    {
        if (!FrameTimingManager.IsFeatureEnabled())
        {
            Debug.LogWarning("FrameTimingManager not available");
            return;
        }
        
        FrameTimingManager.CaptureFrameTimings();
        var count = FrameTimingManager.GetLatestTimings(1, _frameTimings);
        
        if (count > 0)
        {
            var timing = _frameTimings[0];
            
            Debug.Log($"CPU Frame Time: {timing.cpuFrameTime:F2}ms");
            Debug.Log($"CPU Main Thread: {timing.cpuMainThreadFrameTime:F2}ms");
            Debug.Log($"CPU Render Thread: {timing.cpuRenderThreadFrameTime:F2}ms");
            Debug.Log($"GPU Frame Time: {timing.gpuFrameTime:F2}ms");
            Debug.Log($"Present Wait: {timing.cpuMainThreadPresentWaitTime:F2}ms");
        }
    }
}

Continuous Frame Monitoring

public class PerformanceMonitor : MonoBehaviour
{
    private readonly FrameTiming[] _frameTimings = new FrameTiming[30];
    
    private float _avgCpuTime;
    private float _avgGpuTime;
    private float _maxFrameTime;
    
    private void Update()
    {
        if (!FrameTimingManager.IsFeatureEnabled()) return;
        
        FrameTimingManager.CaptureFrameTimings();
        var count = FrameTimingManager.GetLatestTimings(_frameTimings.Length, _frameTimings);
        
        if (count > 0)
        {
            float totalCpu = 0, totalGpu = 0;
            _maxFrameTime = 0;
            int gpuCount = 0;
            
            for (int i = 0; i < count; i++)
            {
                var t = _frameTimings[i];
                totalCpu += t.cpuFrameTime;
                
                if (t.gpuFrameTime > 0)
                {
                    totalGpu += t.gpuFrameTime;
                    gpuCount++;
                }
                
                var frameTime = Mathf.Max(t.cpuFrameTime, t.gpuFrameTime);
                _maxFrameTime = Mathf.Max(_maxFrameTime, frameTime);
            }
            
            _avgCpuTime = totalCpu / count;
            _avgGpuTime = gpuCount > 0 ? totalGpu / gpuCount : 0;
        }
    }
    
    public (float cpu, float gpu, float max) GetFrameTimes()
    {
        return (_avgCpuTime, _avgGpuTime, _maxFrameTime);
    }
}

Memory Profiling

Memory Snapshot

using UnityEngine.Profiling;

public class MemoryTracker
{
    public MemorySnapshot CaptureSnapshot()
    {
        return new MemorySnapshot
        {
            TotalAllocated = Profiler.GetTotalAllocatedMemoryLong(),
            TotalReserved = Profiler.GetTotalReservedMemoryLong(),
            TotalUnused = Profiler.GetTotalUnusedReservedMemoryLong(),
            MonoUsed = Profiler.GetMonoUsedSizeLong(),
            MonoHeap = Profiler.GetMonoHeapSizeLong(),
            GfxDriver = Profiler.GetAllocatedMemoryForGraphicsDriver(),
            Timestamp = Time.realtimeSinceStartup
        };
    }
}

public struct MemorySnapshot
{
    public long TotalAllocated;
    public long TotalReserved;
    public long TotalUnused;
    public long MonoUsed;
    public long MonoHeap;
    public long GfxDriver;
    public float Timestamp;
    
    public long TotalMB => TotalAllocated / 1_000_000;
    public long MonoMB => MonoUsed / 1_000_000;
}

GC Allocation Tracking

public class GCAllocationTracker
{
    private long _lastGCMemory;
    private int _lastGCCount;
    
    public void BeginFrame()
    {
        _lastGCMemory = GC.GetTotalMemory(false);
        _lastGCCount = GC.CollectionCount(0);
    }
    
    public (long allocated, bool gcOccurred) EndFrame()
    {
        var currentMemory = GC.GetTotalMemory(false);
        var currentGCCount = GC.CollectionCount(0);
        
        var allocated = currentMemory - _lastGCMemory;
        var gcOccurred = currentGCCount > _lastGCCount;
        
        if (allocated > 1024) // Log allocations > 1KB
        {
            Debug.LogWarning($"Frame allocated {allocated} bytes");
        }
        
        return (allocated, gcOccurred);
    }
}

GC Notification

public class GCNotificationHandler
{
    private bool _isRegistered;
    
    public void Register()
    {
        if (_isRegistered) return;
        
        // Register for full GC notification (Gen2)
        GC.RegisterForFullGCNotification(10, 10);
        _isRegistered = true;
        
        // Start monitoring thread
        StartMonitoringThread();
    }
    
    private void StartMonitoringThread()
    {
        var thread = new Thread(() =>
        {
            while (_isRegistered)
            {
                var status = GC.WaitForFullGCApproach();
                if (status == GCNotificationStatus.Succeeded)
                {
                    // GC is approaching - prepare
                    OnGCApproaching();
                }
                
                status = GC.WaitForFullGCComplete();
                if (status == GCNotificationStatus.Succeeded)
                {
                    // GC completed
                    OnGCComplete();
                }
            }
        });
        thread.IsBackground = true;
        thread.Start();
    }
    
    private void OnGCApproaching()
    {
        Debug.Log("Full GC approaching");
    }
    
    private void OnGCComplete()
    {
        Debug.Log("Full GC complete");
    }
}

Hitch Detection

Simple Hitch Detector

public class HitchDetector : MonoBehaviour
{
    private const float MinorHitchMs = 33.33f;  // < 30 FPS
    private const float MajorHitchMs = 50f;     // < 20 FPS
    private const float SevereHitchMs = 100f;   // < 10 FPS
    
    public event Action<float, HitchSeverity> OnHitchDetected;
    
    private float _lastFrameTime;
    
    private void Update()
    {
        var frameTimeMs = Time.unscaledDeltaTime * 1000f;
        
        if (frameTimeMs >= SevereHitchMs)
        {
            OnHitchDetected?.Invoke(frameTimeMs, HitchSeverity.Severe);
        }
        else if (frameTimeMs >= MajorHitchMs)
        {
            OnHitchDetected?.Invoke(frameTimeMs, HitchSeverity.Major);
        }
        else if (frameTimeMs >= MinorHitchMs)
        {
            OnHitchDetected?.Invoke(frameTimeMs, HitchSeverity.Minor);
        }
        
        _lastFrameTime = frameTimeMs;
    }
}

public enum HitchSeverity { Minor, Major, Severe }

Advanced Hitch Tracker

public class HitchTracker
{
    private readonly Queue<float> _frameTimes = new();
    private readonly int _windowSize;
    
    private int _minorHitchCount;
    private int _majorHitchCount;
    private int _severeHitchCount;
    private int _totalFrames;
    
    public float HitchRate => _totalFrames > 0 
        ? (float)(_minorHitchCount + _majorHitchCount + _severeHitchCount) / _totalFrames 
        : 0f;
    
    public HitchTracker(int windowSize = 300)
    {
        _windowSize = windowSize;
    }
    
    public void RecordFrame(float frameTimeMs)
    {
        _frameTimes.Enqueue(frameTimeMs);
        _totalFrames++;
        
        // Classify hitch
        if (frameTimeMs >= 100f)
            _severeHitchCount++;
        else if (frameTimeMs >= 50f)
            _majorHitchCount++;
        else if (frameTimeMs >= 33.33f)
            _minorHitchCount++;
        
        // Maintain window
        while (_frameTimes.Count > _windowSize)
        {
            var removed = _frameTimes.Dequeue();
            _totalFrames--;
            
            // Adjust counts
            if (removed >= 100f)
                _severeHitchCount--;
            else if (removed >= 50f)
                _majorHitchCount--;
            else if (removed >= 33.33f)
                _minorHitchCount--;
        }
    }
    
    public HitchStats GetStats()
    {
        return new HitchStats
        {
            TotalFrames = _totalFrames,
            MinorHitches = _minorHitchCount,
            MajorHitches = _majorHitchCount,
            SevereHitches = _severeHitchCount,
            HitchRate = HitchRate,
            WorstFrameTime = _frameTimes.Count > 0 ? _frameTimes.Max() : 0f
        };
    }
}

public struct HitchStats
{
    public int TotalFrames;
    public int MinorHitches;
    public int MajorHitches;
    public int SevereHitches;
    public float HitchRate;
    public float WorstFrameTime;
}

Performance Budgets

Frame Budget System

public class FrameBudgetManager
{
    private readonly Dictionary<string, float> _budgets = new();
    private readonly Dictionary<string, float> _actuals = new();
    
    public float TotalBudgetMs { get; } = 16.67f; // 60 FPS target
    
    public void SetBudget(string system, float budgetMs)
    {
        _budgets[system] = budgetMs;
    }
    
    public void RecordActual(string system, float actualMs)
    {
        _actuals[system] = actualMs;
        
        if (_budgets.TryGetValue(system, out var budget) && actualMs > budget)
        {
            Debug.LogWarning($"{system} over budget: {actualMs:F2}ms / {budget:F2}ms");
        }
    }
    
    public bool IsWithinBudget()
    {
        var totalActual = _actuals.Values.Sum();
        return totalActual <= TotalBudgetMs;
    }
    
    public void LogBudgetReport()
    {
        Debug.Log("=== Frame Budget Report ===");
        
        foreach (var (system, budget) in _budgets)
        {
            var actual = _actuals.GetValueOrDefault(system, 0f);
            var status = actual <= budget ? "OK" : "OVER";
            Debug.Log($"{system}: {actual:F2}ms / {budget:F2}ms [{status}]");
        }
        
        var totalActual = _actuals.Values.Sum();
        Debug.Log($"Total: {totalActual:F2}ms / {TotalBudgetMs:F2}ms");
    }
}

// Usage
var budgetManager = new FrameBudgetManager();
budgetManager.SetBudget("Physics", 2f);
budgetManager.SetBudget("AI", 3f);
budgetManager.SetBudget("Animation", 2f);
budgetManager.SetBudget("Rendering", 8f);

Memory Budget System

public class MemoryBudgetManager
{
    public long TotalBudgetMB { get; private set; }
    public long TextureBudgetMB { get; private set; }
    public long AudioBudgetMB { get; private set; }
    public long MeshBudgetMB { get; private set; }
    
    public void SetBudgetsForDevice(DeviceTier tier)
    {
        switch (tier)
        {
            case DeviceTier.Low:
                TotalBudgetMB = 512;
                TextureBudgetMB = 256;
                AudioBudgetMB = 64;
                MeshBudgetMB = 128;
                break;
                
            case DeviceTier.Medium:
                TotalBudgetMB = 1024;
                TextureBudgetMB = 512;
                AudioBudgetMB = 128;
                MeshBudgetMB = 256;
                break;
                
            case DeviceTier.High:
                TotalBudgetMB = 2048;
                TextureBudgetMB = 1024;
                AudioBudgetMB = 256;
                MeshBudgetMB = 512;
                break;
        }
    }
    
    public bool IsOverBudget()
    {
        var currentMB = Profiler.GetTotalAllocatedMemoryLong() / 1_000_000;
        return currentMB > TotalBudgetMB;
    }
    
    public float GetBudgetUsagePercent()
    {
        var currentMB = Profiler.GetTotalAllocatedMemoryLong() / 1_000_000;
        return (float)currentMB / TotalBudgetMB * 100f;
    }
}

public enum DeviceTier { Low, Medium, High }

Profiler Counters (Custom)

ProfilerCounter

using Unity.Profiling;

public class CustomProfilerCounters
{
    public static readonly ProfilerCounter<int> EnemyCount = new(
        ProfilerCategory.Scripts,
        "Enemy Count",
        ProfilerMarkerDataUnit.Count
    );
    
    public static readonly ProfilerCounter<int> ActiveEffects = new(
        ProfilerCategory.Scripts,
        "Active Effects",
        ProfilerMarkerDataUnit.Count
    );
    
    public static readonly ProfilerCounter<float> MemoryUsageMB = new(
        ProfilerCategory.Memory,
        "Custom Memory MB",
        ProfilerMarkerDataUnit.Bytes
    );
}

// Usage in Update
CustomProfilerCounters.EnemyCount.Sample(_enemies.Count);
CustomProfilerCounters.ActiveEffects.Sample(_activeEffects.Count);

Best Practices

  1. Use ProfilerMarkers for custom code sections
  2. Sample FrameTimingManager for accurate timing
  3. Set performance budgets early in development
  4. Track allocations to prevent GC spikes
  5. Detect hitches in production builds
  6. Profile on target devices - Editor is not representative
  7. Use Deep Profile sparingly - Significant overhead
  8. Profile specific scenarios - Combat, loading, menus
  9. Automate profiling - CI performance tests
  10. Log outliers - Capture worst-case frames

Troubleshooting

IssueInvestigation
Low FPSCheck CPU vs GPU bound in Profiler
HitchesLook for GC, loading, or spikes
Memory growthTrack allocations over time
GC spikesFind allocation sources
GPU boundReduce draw calls, shader complexity
CPU boundOptimize scripts, reduce Updates

评价

还没有评价。欢迎分享你的使用感受。

评论

无需登录即可发表。欢迎分享使用感受、提问,或说明在哪些环境下无法运行。

还没有评论。来写第一条吧。

提交后会立即公开。请勿填写邮箱、电话等个人信息。URL 最多 2 个。

0 / 1000 字

此表单受 reCAPTCHA 保护,适用 Google 的隐私权政策和服务条款。

同一仓库的技能

adopt

免费

Brownfield onboarding — audits existing project artifacts for template format compliance (not just existence), classifies gaps by impact, and produces a numbered migration plan. Run this when joining an in-progress project or upgrading from an older template version. Distinct from /project-stage-detect (which checks what exists) — this checks whether what exists will actually work with the template's skills.

中文简介正在准备中。当前显示原文说明。

IdoCohen560/claude-unity-game-studio222026年7月8日 更新

Creates an Architecture Decision Record (ADR) documenting a significant technical decision, its context, alternatives considered, and consequences. Every major technical choice should have an ADR.

中文简介正在准备中。当前显示原文说明。

IdoCohen560/claude-unity-game-studio222026年7月8日 更新

Validates completeness and consistency of the project architecture against all GDDs. Builds a traceability matrix mapping every GDD technical requirement to ADRs, identifies coverage gaps, detects cross-ADR conflicts, verifies engine compatibility consistency across all decisions, and produces a PASS/CONCERNS/FAIL verdict. The architecture equivalent of /design-review.

中文简介正在准备中。当前显示原文说明。

IdoCohen560/claude-unity-game-studio222026年7月8日 更新

art-bible

免费

Guided, section-by-section Art Bible authoring. Creates the visual identity specification that gates all asset production. Run after /brainstorm is approved and before /map-systems or any GDD authoring begins.

中文简介正在准备中。当前显示原文说明。

IdoCohen560/claude-unity-game-studio222026年7月8日 更新

Audits game assets for compliance with naming conventions, file size budgets, format standards, and pipeline requirements. Identifies orphaned assets, missing references, and standard violations.

中文简介正在准备中。当前显示原文说明。

IdoCohen560/claude-unity-game-studio222026年7月8日 更新

Generate per-asset visual specifications and AI generation prompts from GDDs, level docs, or character profiles. Produces structured spec files and updates the master asset manifest. Run after art bible and GDD/level design are approved, before production begins.

中文简介正在准备中。当前显示原文说明。

IdoCohen560/claude-unity-game-studio222026年7月8日 更新

查看 IdoCohen560 的全部技能

报告此技能的问题