Skip to content

Instantly share code, notes, and snippets.

Embed
What would you like to do?
Unity3D: Tweaked version of @robotduck's CodeProfiler script.
// You can switch to a regular Update() loop
// if you comment out this line. (makes debugging easier)
#define COROUTINE
//
using System.Collections;
using System.Collections.Generic;
using System.Diagnostics;
using System.Text;
using UnityEngine;
using Debug = UnityEngine.Debug;
// Simple code profiler class for Unity projects
// @robotduck 2011
//
// usage: place on an empty gameobject in your scene
// then insert calls to CodeProfiler.Begin(id) and
// CodeProfiler.End(id) around the section you want to profile
//
// "id" should be string, unique to each code portion that you're timing
// for example, in your enemy update function, you might have:
//
// function Update {
// CodeProfiler.Begin("Enemy:Update");
// <the rest of your enemy update code here>
//
// CodeProfiler.Begin("Enemy:Targeting");
// <expensive inner code here>
// CodeProfiler.End();
//
// CodeProfiler.End();
// }
//
// the Begin id and the End id must match exactly.
public class CodeProfiler : MonoBehaviour {
// this is the ProfileRecording class which is simply included
// directly after the CodeProfiler class in the same file.
// The ProfileRecording class is basically for "internal use
// only" - you don't need to place it on a gameobject or interact
// with it in any way yourself, it's purely used by the
// CodeProfiler to do its job.
sealed class ProfilerRecording : Stopwatch {
// this class accumulates time for a single recording
public string name;
public int totalMilliseconds;
public int iterations;
public ProfilerRecording(string id) : base() {
name = id;
}
new public void Start() {
iterations++;
base.Start();
}
new public void Stop() {
// If code is really fast ElapsedMilliseconds will be 0.
// Technically this is impossible, so we round up.
totalMilliseconds += Mathf.Max(1, (int) ElapsedMilliseconds);
base.Reset();
}
new public void Reset() {
totalMilliseconds = 0;
iterations = 0;
base.Reset();
}
}
// column width for text display
const int COL_WIDTH = 10;
const int MS_PER_SAMPLE = 5000;
readonly static string headerFormat = string.Format("{{0,-{0}}}{{1,-{0}}}{{2,-{0}}}{{3,-{0}}}{{4,-{0}}}\n", COL_WIDTH);
readonly static string recordFormat = string.Format("{{0,-{0}}}{{1,{0}:0.000%}}{{2,{0}:0.000ms}}{{3,{0}:0.000}}{{4,{0}:0.000ms}}\n", COL_WIDTH);
readonly Rect displayRect = new Rect(10, 10, 400, 300);
static StringBuilder displayText;
static Dictionary<string, ProfilerRecording> recordings;
static Stack<ProfilerRecording> stack;
static bool running;
static int frameCount;
static CodeProfiler instance;
static Stopwatch stopwatch;
static void CreateInstance() {
if (instance != null) {
return;
}
GameObject profiler = new GameObject("__Profiler__");
SetInstance(profiler.AddComponent<CodeProfiler>());
}
static bool SetInstance(CodeProfiler inst) {
if (instance != null && instance != inst) {
return false;
} else if (instance == inst) {
return true;
}
recordings = new Dictionary<string, ProfilerRecording>(100);
displayText = new StringBuilder("\n\nTaking initial readings...");
stopwatch = new Stopwatch();
stack = new Stack<ProfilerRecording>(100);
running = true;
instance = inst;
return true;
}
static void ClearInstance(CodeProfiler inst) {
if (instance != inst) {
return;
}
recordings = null;
displayText = null;
stopwatch = null;
instance = null;
running = false;
}
void OnEnable() {
if (!SetInstance(this)) {
DestroyImmediate(this);
return;
}
#if COROUTINE
StartCoroutine(UpdateCoroutine());
#endif
}
void OnDisable() {
#if COROUTINE
StopAllCoroutines();
#endif
ClearInstance(this);
}
void OnGUI() {
GUI.Box(displayRect, "Code Profiler");
GUI.Label(displayRect, string.Format("\n\n{0}", displayText));
}
public static void Begin(string id) {
if (!running) {
CreateInstance();
}
if (string.IsNullOrEmpty(id)) {
Debug.LogWarning("Empty profiler id!");
id = "<null>";
}
ProfilerRecording record = null;
// create a new recording if not present in the list
if (!recordings.TryGetValue(id, out record)) {
record = new ProfilerRecording(id);
recordings[id] = record;
}
record.Start();
stack.Push(record);
}
public static void End() {
stack.Pop().Stop();
}
// just here for backwards compatibility
public static void End(string id) {
End();
}
#if COROUTINE
static IEnumerator UpdateCoroutine() {
#else
void Update() {
#endif
#if COROUTINE
while (running) {
#endif
if (!stopwatch.IsRunning) {
stopwatch.Start();
}
while (stopwatch.ElapsedMilliseconds < MS_PER_SAMPLE) {
if (stack.Count != 0) {
Debug.LogWarning(string.Format("Unmatched Begin() and End()? stack count={0}", stack.Count));
}
frameCount++;
#if COROUTINE
yield return null;
#else
return;
#endif
}
stopwatch.Stop();
// time to display the results
// the overall frame time and frames per second:
int totalMS = (int) stopwatch.ElapsedMilliseconds;
double avgMS = (double) totalMS / frameCount;
double fps = 1000.0 / avgMS;
displayText.Length = 0;
displayText.AppendFormat("Avg frame time: {0:0.#}ms, {1:0.#}fps\n", avgMS, fps);
// the column titles for the individual recordings:
displayText.AppendFormat(headerFormat, "Label", "Total", "MS/frame", "Calls/fra", "MS/call");
int recordedMS;
double percent;
double msPerFrame;
double msPerCall;
double timesPerFrame;
// now we loop through each individual recording
foreach (var recording in recordings.Values) {
// calculate the statistics for this recording:
recordedMS = recording.totalMilliseconds;
msPerFrame = (double) recordedMS / frameCount;
percent = (double) recordedMS / totalMS;
msPerCall = (double) recordedMS / recording.iterations;
timesPerFrame = (double) recording.iterations / frameCount;
displayText.AppendFormat(recordFormat, recording.name, percent, msPerFrame, timesPerFrame, msPerCall);
// and reset the recording
recording.Reset();
}
Debug.Log(displayText.ToString());
// reset & schedule the next time to display results:
frameCount = 0;
stopwatch.Reset();
#if COROUTINE
}
#endif
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment