using System;
using System.Collections.Generic;
using Unity.Profiling;
using UnityEngine;
namespace Unity.Pipeline.Runtime.Telemetry
{
///
/// Samples per-frame timing on the main thread so the runtime telemetry command can report fps and
/// frame-time statistics over a rolling window.
///
/// A Player build has no Editor profiler window to read from and no EditorApplication.update
/// to drive sampling, so owns one shared instance and feeds it
/// every frame from its Update. The sampler is deliberately allocation-free on the hot path:
/// each frame time is written into a fixed ring buffer and only reduced to a
/// on demand when a telemetry request arrives (far rarer than the frame rate).
///
/// Sampling uses Time.unscaledDeltaTime so reported fps reflects real wall-clock frame pacing
/// independent of Time.timeScale — a paused or slow-motion game still reports its true fps.
///
public sealed class FrameStatsSampler : IDisposable
{
///
/// The process-wide sampler fed by the active . Null when no
/// manager is present (e.g. EditMode tests, or a scene without the component). Telemetry consumers
/// must treat a null sampler as "frame stats unavailable" rather than failing — memory and counter
/// data that do not depend on per-frame sampling can still be reported without it.
///
public static FrameStatsSampler Shared { get; set; }
private readonly float[] m_FrameTimesMs;
private int m_Head;
private int m_Count;
private float m_LastFrameTimeMs;
// ProfilerRecorders pull counters straight from the profiler stream. They are started lazily on
// the first snapshot (creation must happen on the main thread) and disposed with the sampler.
// Recorders that do not resolve on this platform/Unity version report Valid == false and are
// simply skipped when building a snapshot.
private readonly List m_Counters = new List();
private bool m_CountersStarted;
private struct CounterRecorder
{
public string Name;
public ProfilerRecorder Recorder;
}
/// Number of frames the rolling window can hold.
public int Capacity => m_FrameTimesMs.Length;
/// Number of frames currently recorded (ramps up to ).
public int SampleCount => m_Count;
public FrameStatsSampler(int windowFrames = 120)
{
if (windowFrames < 1)
windowFrames = 1;
m_FrameTimesMs = new float[windowFrames];
}
///
/// Record one completed frame. Call once per frame from the main thread.
/// is the unscaled delta time of the frame just completed
/// (typically Time.unscaledDeltaTime).
///
public void Sample(float deltaSeconds)
{
var ms = deltaSeconds * 1000f;
m_LastFrameTimeMs = ms;
m_FrameTimesMs[m_Head] = ms;
m_Head = (m_Head + 1) % m_FrameTimesMs.Length;
if (m_Count < m_FrameTimesMs.Length)
m_Count++;
// FrameTimingManager only yields data for frames in which timings were captured, so the
// capture call has to happen on the per-frame path, not when a snapshot is requested.
FrameTimingManager.CaptureFrameTimings();
}
///
/// Reduce the current window to an immutable snapshot. Must be called on the main thread (it reads
/// profiler recorders and the frame timing manager).
///
public FrameStatsSnapshot GetSnapshot()
{
var snap = new FrameStatsSnapshot();
if (m_Count > 0)
{
float sum = 0f, min = float.MaxValue, max = 0f;
for (int i = 0; i < m_Count; i++)
{
var v = m_FrameTimesMs[i];
sum += v;
if (v < min) min = v;
if (v > max) max = v;
}
var avg = sum / m_Count;
snap.Available = true;
snap.SampleWindow = m_Count;
snap.AverageFrameTimeMs = avg;
snap.MinFrameTimeMs = min;
snap.MaxFrameTimeMs = max;
snap.LastFrameTimeMs = m_LastFrameTimeMs;
snap.Fps = avg > 0f ? 1000f / avg : 0f;
}
ReadFrameTimingManager(snap);
ReadCounters(snap);
return snap;
}
// CPU/GPU frame time from FrameTimingManager. Only populated when the platform reports timings
// (requires "Frame Timing Stats" in Player settings / -enable-frame-timing-stats); otherwise the
// snapshot's GpuTimingAvailable stays false rather than reporting misleading zeros.
private static void ReadFrameTimingManager(FrameStatsSnapshot snap)
{
var timings = new FrameTiming[1];
var captured = FrameTimingManager.GetLatestTimings(1, timings);
if (captured > 0)
{
snap.GpuTimingAvailable = true;
snap.CpuFrameTimeMs = timings[0].cpuFrameTime;
snap.GpuFrameTimeMs = timings[0].gpuFrameTime;
}
}
private void ReadCounters(FrameStatsSnapshot snap)
{
EnsureCountersStarted();
var counters = new Dictionary();
foreach (var c in m_Counters)
{
if (c.Recorder.Valid)
counters[c.Name] = c.Recorder.LastValue;
}
snap.Counters = counters;
}
private void EnsureCountersStarted()
{
if (m_CountersStarted)
return;
m_CountersStarted = true;
// Common render/memory counters. Stat names that do not exist on the running platform resolve
// to invalid recorders and are filtered out at snapshot time.
TryAddCounter("DrawCalls", ProfilerCategory.Render, "Draw Calls Count");
TryAddCounter("SetPassCalls", ProfilerCategory.Render, "SetPass Calls Count");
TryAddCounter("Triangles", ProfilerCategory.Render, "Triangles Count");
TryAddCounter("Vertices", ProfilerCategory.Render, "Vertices Count");
TryAddCounter("GcAllocInFrame", ProfilerCategory.Memory, "GC Allocated In Frame");
}
private void TryAddCounter(string reportedName, ProfilerCategory category, string statName)
{
m_Counters.Add(new CounterRecorder
{
Name = reportedName,
Recorder = ProfilerRecorder.StartNew(category, statName)
});
}
public void Dispose()
{
foreach (var c in m_Counters)
{
if (c.Recorder.Valid)
c.Recorder.Dispose();
}
m_Counters.Clear();
m_CountersStarted = false;
}
}
///
/// Immutable point-in-time reduction of 's rolling window plus the
/// counters/frame-timing read at snapshot time. Serialized as the frame-stats portion of the runtime
/// telemetry response.
///
[Serializable]
public class FrameStatsSnapshot
{
/// True when at least one frame has been sampled (i.e. a manager is feeding the sampler).
public bool Available { get; set; }
/// Number of frames included in the averages.
public int SampleWindow { get; set; }
/// Frames per second derived from the average frame time over the window.
public float Fps { get; set; }
public float AverageFrameTimeMs { get; set; }
public float MinFrameTimeMs { get; set; }
public float MaxFrameTimeMs { get; set; }
public float LastFrameTimeMs { get; set; }
/// True when FrameTimingManager returned CPU/GPU timings for a recent frame.
public bool GpuTimingAvailable { get; set; }
public double CpuFrameTimeMs { get; set; }
public double GpuFrameTimeMs { get; set; }
/// Valid profiler counters by reported name (e.g. DrawCalls, Triangles).
public Dictionary Counters { get; set; }
}
}