Skip to content

Instantly share code, notes, and snippets.

Show Gist options
  • Save bambom/70d5844f73eabe8214d7a8cbf1c148a2 to your computer and use it in GitHub Desktop.
Save bambom/70d5844f73eabe8214d7a8cbf1c148a2 to your computer and use it in GitHub Desktop.

Revisions

  1. @rthery rthery revised this gist Dec 2, 2023. 1 changed file with 1 addition and 1 deletion.
    2 changes: 1 addition & 1 deletion ProfilerStatsToCSVExporter.cs
    Original file line number Diff line number Diff line change
    @@ -33,7 +33,7 @@ public ProfilerRecorder ToProfilerRecorder()

    [SerializeField] [Tooltip("Input values found via ProfilerRecorderHandle.GetAvailable")]
    private ProfilerStatsEntry[] profilerStats = {
    new ("GC","GC.Collect"),
    new ("GC", "GC.Collect"),
    new ("Internal", "Main Thread"),
    new ("Memory", "Total Used Memory"),
    new ("Memory", "Audio Used Memory"),
  2. @rthery rthery created this gist Dec 2, 2023.
    197 changes: 197 additions & 0 deletions ProfilerStatsToCSVExporter.cs
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,197 @@
    using System;
    using System.IO;
    using UnityEngine;

    namespace Unity.Profiling
    {
    /// <summary>
    /// This component will export the specified Profiler stats to a CSV file in the application persistent data path
    /// cf. https://docs.unity3d.com/ScriptReference/Unity.Profiling.ProfilerRecorder.html
    /// </summary>
    public class ProfilerStatsToCSVExporter : MonoBehaviour
    {
    [Serializable]
    private sealed class ProfilerStatsEntry
    {
    public string Category;
    public string Name;

    public ProfilerStatsEntry(string category, string name)
    {
    Category = category;
    Name = name;
    }

    public ProfilerRecorder ToProfilerRecorder()
    {
    ProfilerCategory profilerCategory = new ProfilerCategory(Category);
    return ProfilerRecorder.StartNew(profilerCategory, Name);
    }
    }

    private const char OUTPUT_SEPARATOR = ',';

    [SerializeField] [Tooltip("Input values found via ProfilerRecorderHandle.GetAvailable")]
    private ProfilerStatsEntry[] profilerStats = {
    new ("GC","GC.Collect"),
    new ("Internal", "Main Thread"),
    new ("Memory", "Total Used Memory"),
    new ("Memory", "Audio Used Memory"),
    new ("Memory", "GC Used Memory"),
    new ("PlayerLoop", "PlayerLoop"),
    new ("Render", "Batches Count"),
    new ("Render", "CPU Main Thread Frame Time"),
    new ("Render", "CPU Render Thread Frame Time"),
    new ("Render", "CPU Total Frame Time"),
    new ("Render", "Draw Calls Count"),
    new ("Render", "FrameTime.GPU"),
    new ("Render", "GPU Frame Time"),
    new ("Render", "Gfx.WaitForPresentOnGfxThread"),
    new ("Render", "Render Textures Bytes"),
    new ("Render", "Render Textures Count"),
    new ("Render", "SetPass Calls Count"),
    new ("Render", "Shadow Casters Count"),
    new ("Render", "Triangles Count"),
    new ("Render", "Vertices Count"),
    new ("VSync", "WaitForTargetFPS")
    };

    private TextWriter _textWriter;
    private ProfilerRecorder[] _profilerRecorders;
    private float _lastFlushTime;

    private void OnEnable()
    {
    string outputFilePath = Path.Combine(Application.persistentDataPath, $"profiler_stats-{DateTime.Now:yyyy.MM.dd-HH.mm}.csv");
    _textWriter = new StreamWriter(outputFilePath, true);

    Debug.Log("Writing Profiler Stats to " + outputFilePath);

    _textWriter.Write("Frame");
    _textWriter.Write(OUTPUT_SEPARATOR);

    _profilerRecorders = new ProfilerRecorder[profilerStats.Length];
    for (int i = 0; i < profilerStats.Length; i++)
    {
    _profilerRecorders[i] = profilerStats[i].ToProfilerRecorder();

    if (_profilerRecorders[i].Valid == false)
    {
    Debug.LogError($"ProfilerRecorder for {profilerStats[i].Name} ({profilerStats[i].Category}) is not valid. Either there's a typo or this ProfilerRecorder is not available on this platform.");
    continue;
    }

    _textWriter.Write(profilerStats[i].Name);
    AppendStatUnitToText(_profilerRecorders[i], _textWriter);

    bool isLastColumn = i == profilerStats.Length - 1;
    AppendSeparatorToText(_textWriter, isLastColumn);
    }
    }

    private void OnDisable()
    {
    _textWriter.Flush();
    _textWriter.Dispose();

    foreach (ProfilerRecorder profilerRecorder in _profilerRecorders)
    {
    profilerRecorder.Dispose();
    }
    }

    private void Update()
    {
    _textWriter.Write(GetLongAsChars(Time.frameCount));
    _textWriter.Write(OUTPUT_SEPARATOR);

    for (int i = 0; i < _profilerRecorders.Length; i++)
    {
    ProfilerRecorder profilerRecorder = _profilerRecorders[i];
    _textWriter.Write(GetLongAsChars(profilerRecorder.LastValue));

    bool isLastColumn = i == _profilerRecorders.Length - 1;
    AppendSeparatorToText(_textWriter, isLastColumn);
    }

    if (_lastFlushTime + 1f < Time.realtimeSinceStartup)
    {
    _lastFlushTime = Time.realtimeSinceStartup;
    _textWriter.Flush();
    }
    }

    private static void AppendSeparatorToText(TextWriter textWriter, bool isLastColumn = false)
    {
    if (isLastColumn)
    {
    textWriter.WriteLine();
    }
    else
    {
    textWriter.Write(OUTPUT_SEPARATOR);
    }
    }

    private static void AppendStatUnitToText(ProfilerRecorder profilerRecorder, TextWriter textWriter)
    {
    switch (profilerRecorder.UnitType)
    {
    case ProfilerMarkerDataUnit.TimeNanoseconds:
    textWriter.Write(" (ns)");
    break;

    case ProfilerMarkerDataUnit.Bytes:
    textWriter.Write(" (bytes)");
    break;

    case ProfilerMarkerDataUnit.Percent:
    textWriter.Write(" (%)");
    break;

    case ProfilerMarkerDataUnit.FrequencyHz:
    textWriter.Write(" (Hz)");
    break;

    case ProfilerMarkerDataUnit.Undefined:
    case ProfilerMarkerDataUnit.Count:
    default:
    break;
    }
    }

    private static readonly char[] _longAsCharsBuffer = new char[20]; // 19 for long.MaxValue.ToString().Length + 1 for negative sign
    private static ReadOnlySpan<char> GetLongAsChars(long value)
    {
    int bufferIndex = 0;
    if (value == 0)
    {
    _longAsCharsBuffer[bufferIndex] = '0';
    return new Span<char>(_longAsCharsBuffer, bufferIndex, 1);
    }

    // For negative values, we need to add the '-' sign and invert the value
    if (value < 0)
    {
    _longAsCharsBuffer[bufferIndex] = '-';
    bufferIndex++;
    value = -value;
    }

    int length = 1;
    for (long r = value / 10; r > 0; r /= 10)
    {
    length++;
    }

    for (int i = length - 1; i >= 0; i--)
    {
    _longAsCharsBuffer[bufferIndex + i] = (char)('0' + (value % 10));
    value /= 10;
    }

    ReadOnlySpan<char> bufferSplice = new ReadOnlySpan<char>(_longAsCharsBuffer).Slice(bufferIndex, length);
    return bufferSplice;
    }
    }
    }