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; } } }