Skip to content

Instantly share code, notes, and snippets.

@rthery
Last active May 23, 2024 06:21
Show Gist options
  • Save rthery/79702daf9a3814f492267287787b8e7f to your computer and use it in GitHub Desktop.
Save rthery/79702daf9a3814f492267287787b8e7f to your computer and use it in GitHub Desktop.
Unity component exporting the specified Profiler stats to a CSV file in the application persistent data path
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;
}
}
}
@rthery
Copy link
Author

rthery commented Dec 2, 2023

Update() has 0 allocation and an insignificant impact on CPU unless you start tracking a lot of stats

The output format can easily be imported in the spreadsheet editor of your choice to create graphs and compare them between versions

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment