Frame time vs FPS: how to spot stutter an average hides
Convert FPS to milliseconds, interpret a percentile correctly and use a tiny reproducible trace to see why two equal averages can feel different.
Frames per second tells you how many frames fit in a second on average. Frame time tells you how long a particular frame took, or how long passed between presented frames, depending on the measurement. For a steady 60 fps the interval is about 16.67 ms. A game can average 60 fps while some individual frames take much longer.
Calculate throughput from the whole trace
For presentation intervals measured in milliseconds, throughput is 1,000 × the number of intervals divided by their sum. Averaging 1,000 divided by each individual interval instead overweights short frames and gives a different value. State which clock you measured: CPU work, GPU work or presentation intervals.
Two traces with the same average
Imagine 100 intervals. The even trace contains 100 frames at 16 ms. The uneven trace contains 95 at 14 ms and five at 54 ms. Both total 1,600 ms and average 62.5 fps. The second trace includes five intervals lasting over three times as long as the first trace’s typical interval.
from math import ceil
def percentile(samples, p):
values = sorted(samples)
return values[ceil(len(values) * p / 100) - 1]
def describe(samples):
return {
"fps": round(1000 * len(samples) / sum(samples), 2),
"p95_ms": percentile(samples, 95),
"p99_ms": percentile(samples, 99),
"over_60fps_budget": sum(ms > 1000 / 60 for ms in samples),
}
print("even", describe([16] * 100))
print("uneven", describe([14] * 95 + [54] * 5))Using nearest-rank percentiles, the uneven trace reports p95 at 14 ms and p99 at 54 ms. The p95 hides exactly the slowest five percent here. Report multiple tail percentiles and counts above the frame budget. Keep a timeline too, because percentile statistics cannot tell whether slow frames were evenly scattered or arrived together.
How to make your comparison reproducible
Record hardware, build, preset, resolution, cap and display sync. Choose a fixed route and repeat it. Compare first-run shader or asset loading separately from warmed runs. Use the same capture length and percentile definition. Retain the raw trace so someone can investigate spikes rather than trying to reverse-engineer a chart.