Quick answer: Frame rate (FPS) is an average over time, while frame time measures how long each individual frame takes to render. A game can show 60fps average while having occasional 50ms frame time spikes that cause visible stuttering.

Learning how to track performance bugs in games is a common challenge for game developers. A performance bug is not a crash. It does not generate an error log or a stack trace. The game keeps running — it just runs badly. Frame rate drops, stuttering, hitching during combat, long loading times that test player patience. Performance bugs are some of the hardest defects to track because they exist on a spectrum rather than a binary. There is no clear boundary between "working" and "broken," only a gradual degradation that players feel before they can articulate it. Here is how to measure, track, and systematically eliminate performance problems in your game.

Frame Time Is Your Primary Metric

Stop thinking in frames per second. Start thinking in frame time — the number of milliseconds each individual frame takes to complete. FPS is an average that hides problems. A game running at "60fps average" might have individual frames that take 40ms, creating visible stuttering that the average obscures.

Your frame time budget depends on your target: 16.67ms for 60fps, 33.33ms for 30fps, or 6.94ms for 144fps. Every frame that exceeds its budget is a micro-stutter the player will notice, especially in fast-paced games where visual smoothness is critical for gameplay.

// C#: Frame time tracking with percentile calculation
public class FrameTimeTracker {
    private List<float> _frameTimes = new();
    private const float SAMPLE_WINDOW = 10f; // seconds
    private float _windowStart;

    public void RecordFrame(float deltaTime) {
        _frameTimes.Add(deltaTime * 1000f); // convert to ms

        if (Time.time - _windowStart >= SAMPLE_WINDOW) {
            ReportPercentiles();
            _frameTimes.Clear();
            _windowStart = Time.time;
        }
    }

    private void ReportPercentiles() {
        _frameTimes.Sort();
        int count = _frameTimes.Count;

        float p50 = _frameTimes[count / 2];
        float p95 = _frameTimes[(int)(count * 0.95f)];
        float p99 = _frameTimes[(int)(count * 0.99f)];

        Debug.Log($"Frame time P50: {p50:F1}ms | P95: {p95:F1}ms | P99: {p99:F1}ms");
    }
}

The 99th percentile (P99) frame time is the number you should care about most. It represents the worst frames your players will experience regularly. A game with a smooth P50 of 10ms but a P99 of 45ms will feel like it stutters constantly.

Identifying CPU vs GPU Bottlenecks

The first step in fixing any performance bug is determining whether the CPU or GPU is the bottleneck. Every frame involves work on both processors, and they operate in parallel. The frame is not complete until both finish. If the GPU finishes in 8ms but the CPU takes 20ms, you are CPU-bound; optimizing shaders will accomplish nothing.

Each engine provides tools to see this split. In Unity, the Profiler window shows CPU and GPU time in separate tracks. In Godot, enable the frame time debugger under Debug to see the split. In Unreal, type stat unit in the console to see Game time (CPU gameplay), Draw time (CPU rendering commands), and GPU time separately.

CPU-bound indicators: high script execution time, expensive physics calculations, AI pathfinding, too many draw calls (CPU prepares each one), garbage collection pauses (in managed languages).

GPU-bound indicators: complex shaders, high polygon count, overdraw from overlapping transparent objects, post-processing effects, high-resolution shadow maps, uncompressed textures consuming VRAM bandwidth.

The Draw Call Problem

Draw calls are the single most common CPU-side performance bottleneck in indie games. Each draw call is a command from the CPU to the GPU to render a batch of geometry with a specific material. The GPU handles each draw call quickly, but the CPU overhead of preparing and issuing thousands of draw calls per frame adds up fast.

# GDScript: Monitoring draw calls and object counts
func _process(delta: float) -> void:
    var draw_calls := Performance.get_monitor(
        Performance.RENDER_TOTAL_DRAW_CALLS_IN_FRAME
    )
    var objects := Performance.get_monitor(
        Performance.RENDER_TOTAL_OBJECTS_IN_FRAME
    )

    if draw_calls > 2000:
        push_warning("Draw calls excessive: %d (objects: %d)"
            % [draw_calls, objects])

Common causes of excessive draw calls: each unique material requires its own draw call, so a scene with 500 objects using 500 different materials produces 500 draw calls. UI elements with different textures break batching. Particle systems that render each particle individually instead of using GPU instancing. Small props scattered across the level without mesh merging or LOD (Level of Detail) systems.

Fixes: Use texture atlases to combine materials. Merge static meshes that share materials. Implement LOD so distant objects use simpler meshes with fewer draw calls. Use GPU instancing for repeated objects like trees, grass, and bullets.

Profiler-Guided Bug Tracking

When you find a performance hotspot in the profiler, turn it into a tracked bug with concrete data. A performance bug report should include the exact frame time impact (in milliseconds, not FPS), the scene or gameplay situation where it occurs, whether it is CPU or GPU bound, and the specific function or shader responsible.

// C#: Automated performance regression detection
public class PerfRegressionDetector {
    private Dictionary<string, float> _baselines = new();

    public void SetBaseline(string sceneName, float p95FrameTime) {
        _baselines[sceneName] = p95FrameTime;
    }

    public bool CheckForRegression(string sceneName, float currentP95) {
        if (!_baselines.TryGetValue(sceneName, out float baseline))
            return false;

        float regressionThreshold = baseline * 1.15f; // 15% worse
        if (currentP95 > regressionThreshold) {
            Debug.LogError(
                $"PERF REGRESSION in {sceneName}: " +
                $"P95 was {baseline:F1}ms, now {currentP95:F1}ms"
            );
            return true;
        }
        return false;
    }
}

Setting Up Performance Regression Detection

The best time to catch a performance bug is when it is introduced, not weeks later during manual QA. Performance regression detection works by running automated benchmarks for every build and comparing the results against established baselines.

Create dedicated benchmark scenes that stress-test specific systems: a combat scene with the maximum expected number of enemies, a scene with heavy particle effects, a scene with complex UI overlays, and a scene that exercises pathfinding with many agents. Run each scene for a fixed number of frames, record the P50, P95, and P99 frame times, and flag any build where these numbers regress beyond a threshold.

A 15% regression threshold works well in practice. Smaller regressions are often within measurement noise, while anything above 15% is almost certainly a real change that warrants investigation. When a regression is detected, the team reviews the commits since the last good build to identify the offending change.

"Players do not measure frame rate. They feel it. A sustained 55fps feels fine. A single 100ms hitch during a boss fight feels like the game is broken. Track the spikes, not the averages."

Performance Budgets by System

Break your frame time budget into allocations for each major system. For a 60fps game targeting 16.67ms per frame, a typical allocation might be: gameplay logic 3ms, physics 3ms, rendering 7ms, audio 1ms, UI 1ms, with 1.67ms of headroom for spikes. When any system consistently exceeds its budget, that becomes a performance bug to investigate.

This per-system budgeting approach makes it immediately clear where optimization effort should be directed. If rendering is consistently at 10ms in a scene that is supposed to run at 60fps, no amount of gameplay code optimization will solve the problem. Budgets turn vague complaints about "the game feels slow" into specific, actionable engineering tasks.

Related Issues

For tracking how performance varies across player hardware, see our guide on tracking performance issues across devices. To understand how performance relates to overall game health metrics, check game stability metrics and crash-free sessions. For ensuring your game runs well on all target platforms, our multi-platform testing guide covers the practical steps.

Measure in milliseconds, not frames per second. Milliseconds tell the truth that averages hide.