old profiler files

This commit is contained in:
mischa 2024-06-06 12:08:26 +02:00
parent 1721b5601b
commit 21115a3785
33 changed files with 2356 additions and 0 deletions

View File

@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: 82fe9954a7d514fb098fd578f6c89acc
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: fda3d426dcfd94e53ba1cc2e77522cd0
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -0,0 +1,366 @@
using System;
using System.Linq;
using UnityEngine;
using UnityEditor;
using System.Collections.Generic;
namespace Mirror.Profiler.Chart
{
public class ChartView
{
Material mat;
public int MaxFrames = 1000;
private int selectedFrame = -1;
public int SelectedFrame
{
get => selectedFrame;
set
{
this.selectedFrame = value;
OnSelectFrame?.Invoke(value);
}
}
public event Action<int> OnSelectFrame;
private static readonly int[] Scales = { 4, 6, 8, 12, 16, 20, 28 };
private static readonly Color[] SeriesPalette = {
ToColor(0xCC7000),
ToColor(0x5AB2BC),
ToColor(0xfdc086),
ToColor(0xffff99),
ToColor(0x386cb0),
ToColor(0xf0027f),
ToColor(0xbf5b17),
ToColor(0x666666)};
private static Color ToColor(int hex)
{
float r = ((hex & 0xff0000) >> 0x10) / 255f;
float g = ((hex & 0xff00) >> 8) / 255f;
float b = (hex & 0xff) / 255f;
return new Color(r, g, b);
}
public List<ISeries> Series = new List<ISeries>();
public ChartView(params ISeries [] series)
{
Series.AddRange(series);
}
public void OnGUI(Rect rect)
{
if (mat == null)
{
Shader shader = Shader.Find("Hidden/Internal-Colored");
mat = new Material(shader);
}
if (Event.current.type == EventType.MouseDown)
{
OnMouseDown(rect, Event.current);
}
if (Event.current.type == EventType.KeyDown && Event.current.keyCode == KeyCode.LeftArrow)
{
SelectedFrame -= 1;
Event.current.Use();
}
if (Event.current.type == EventType.KeyDown && Event.current.keyCode == KeyCode.RightArrow)
{
SelectedFrame += 1;
Event.current.Use();
}
if (Event.current.type == EventType.Repaint)
{
RectInt axisBounds = AxisBounds();
GUI.BeginClip(rect);
GL.PushMatrix();
try
{
GL.Clear(true, false, Color.black);
mat.SetPass(0);
ClearBackground(rect);
acccumulatedValues.Clear();
for (int i = 0; i < Series.Count; i++)
{
DrawSeries(rect, axisBounds, Series[i], SeriesPalette[i % SeriesPalette.Count()]);
}
DrawGrid(rect, axisBounds);
DrawSelectedFrame(rect, axisBounds);
DrawLegend(rect);
}
finally
{
GL.PopMatrix();
GUI.EndClip();
}
}
}
private void OnMouseDown(Rect rect, Event current)
{
Vector2 position = current.mousePosition;
// was the click for us?
if (!rect.Contains(position))
return;
// need to map to axis
RectInt axisBounds = AxisBounds();
var (x, _) = ScreenToDataSpace(rect, axisBounds, position);
SelectedFrame = Mathf.RoundToInt(x);
}
private (float, float) ScreenToDataSpace(Rect rect, RectInt axisBounds, Vector2 position)
{
Vector2 normalized = (position - rect.min) / rect.size ;
float x = axisBounds.width * normalized.x + axisBounds.xMin;
float y = axisBounds.yMax - axisBounds.height * normalized.y ;
return (x, y);
}
private void DrawSelectedFrame( Rect rect, RectInt axisBounds)
{
int selected = SelectedFrame;
if (selected >= 0)
{
GL.Begin(GL.LINES);
Vector2 selectedPosition = Project(selected, 0, axisBounds, rect);
GL.Color(Color.yellow);
GL.Vertex3(selectedPosition.x, 0, 0);
GL.Vertex3(selectedPosition.x, rect.height, 0);
GL.End();
}
}
private RectInt AxisBounds()
{
Rect dataBounds = DataBounds();
int ymin = 0;
int ymax = PrettyScale(dataBounds.yMax);
int xmax = Mathf.RoundToInt(dataBounds.xMax);
int xmin = xmax - MaxFrames;
return new RectInt(xmin, ymin, xmax - xmin, ymax - ymin);
}
private int PrettyScale(float max)
{
int baseScale = 1;
while (true)
{
foreach (int scale in Scales)
{
if (scale * baseScale > max)
return scale * baseScale;
}
baseScale *= 10;
}
}
List<float> stackedValues = new List<float>();
private Rect DataBounds()
{
Vector2 min = new Vector2Int(int.MaxValue, 0);
Vector2 max = new Vector2Int(int.MinValue, int.MinValue);
stackedValues.Clear();
foreach (ISeries serie in Series)
{
int index = 0;
foreach (var (x,y) in serie.Data)
{
if (stackedValues.Count <= index)
{
stackedValues.Add(0);
}
float newvalue = stackedValues[index] + y;
max.y = Math.Max(max.y, newvalue);
max.x = Math.Max(max.x, x);
min.x = Math.Min(min.x, x);
stackedValues[index] = newvalue;
index++;
}
}
if (min.x > max.x)
min = max = new Vector2Int(0, 0);
return new Rect(min.x, min.y, max.x - min.x, max.y - min.y);
}
List<float> acccumulatedValues = new List<float>();
private void DrawSeries(Rect rect, RectInt axisBounds, ISeries series, Color color)
{
GL.Begin(GL.TRIANGLE_STRIP);
GL.Color(color);
int pframe = -1;
float pLow = 0;
float pHigh = 0;
float low = 0;
float high = 0;
int index = 0;
foreach ((int frame, float value) in series.Data)
{
if (pframe < frame - 1)
{
// there were empty frames, need to draw zeros
DrawDataPoint(rect, axisBounds, pframe, pLow, pHigh, pframe+1, 0, 0);
DrawDataPoint(rect, axisBounds, pframe+1, 0, 0, frame - 1, 0, 0);
pLow = 0;
pHigh = 0;
pframe = frame - 1;
}
if (acccumulatedValues.Count <= index)
{
acccumulatedValues.Add(0);
}
low = acccumulatedValues[index];
high = low + value;
DrawDataPoint(rect, axisBounds, pframe, pLow, pHigh, frame, low, high);
acccumulatedValues[index] = high;
pLow = low;
pHigh = high;
pframe = frame;
index++;
}
GL.End();
}
private void DrawDataPoint(Rect rect, RectInt axisBounds, int pframe, float pLow, float pHigh, int frame, float low, float high)
{
// assume we ended the strip in ph
if (frame >= axisBounds.xMin && frame <= axisBounds.xMax)
{
Vector2 pl = Project(pframe, pLow, axisBounds, rect);
Vector2 ph = Project(pframe, pHigh, axisBounds, rect);
Vector2 l = Project(frame, low, axisBounds, rect);
Vector2 h = Project(frame, high, axisBounds, rect);
GL.Vertex3(h.x, h.y, 0);
GL.Vertex3(pl.x, pl.y, 0);
GL.Vertex3(l.x, l.y, 0);
GL.Vertex3(h.x, h.y, 0);
}
}
private Vector2 Project(int x, float y, RectInt axisBounds, Rect rect)
{
float px = rect.width * (x - axisBounds.xMin) / axisBounds.width;
float py = rect.height * (y - axisBounds.yMin) / axisBounds.height;
return new Vector2(px, rect.height - py);
}
private void DrawGrid(Rect rect, RectInt axisBounds)
{
Vector2 labelSize = Vector2.zero;
for (int i = 1; i < 4; i++)
{
float f = 0.4f;
int lineValue = i * axisBounds.height / 4;
string labelTxt = lineValue.ToString();
GUIStyle labelStyle = new GUIStyle();
labelStyle.normal.textColor = new Color(f, f, f, 1);
labelStyle.alignment = TextAnchor.MiddleLeft;
Rect labelPosition = new Rect(2, rect.height * (4 - i) / 4 - 20, rect.width, 40);
GUI.Label(labelPosition, labelTxt, labelStyle);
Vector2 size = labelStyle.CalcSize(new GUIContent(labelTxt));
labelSize = Vector2.Max(labelSize, size);
}
mat.SetPass(0);
GL.Begin(GL.LINES);
// 4 lines
for (int i = 1; i < 4; i++)
{
float f = 0.2f;
GL.Color(new Color(f, f, f, 1));
GL.Vertex3(2 + 2 + labelSize.x, i * rect.height / 4, 0);
GL.Vertex3(rect.width, i * rect.height / 4, 0);
}
GL.End();
}
private static void ClearBackground(Rect rect)
{
GL.Begin(GL.QUADS);
GL.Color(Color.black);
GL.Vertex3(0, 0, 0);
GL.Vertex3(rect.width, 0, 0);
GL.Vertex3(rect.width, rect.height, 0);
GL.Vertex3(0, rect.height, 0);
GL.End();
}
public void DrawLegend(Rect rect)
{
Rect areaRect = new Rect(4, 4, rect.width, rect.height);
for (int i = 0; i< Series.Count; i++)
{
GUIStyle style = new GUIStyle();
style.normal.textColor = SeriesPalette[i % SeriesPalette.Count()];
style.contentOffset = new Vector2(0, (Series.Count - i - 1) * 15);
style.alignment = TextAnchor.UpperLeft;
GUI.Label(areaRect, Series[i].Name, style);
}
}
}
}

View File

@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: cce5c930d77ae429d90b86cbfb2d0a01
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -0,0 +1,14 @@
using System;
using System.Collections.Generic;
using UnityEngine;
namespace Mirror.Profiler.Chart
{
public interface ISeries
{
IEnumerable<(int, float)> Data { get; }
string Name { get; }
}
}

View File

@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: d7a6aa3d0128c4140a5dee1ad948f993
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -0,0 +1,49 @@
using System.Collections.Generic;
using UnityEngine;
using System.Linq;
namespace Mirror.Profiler.Chart
{
public class Series : ISeries
{
public string Name { get; }
public IEnumerable<(int, float)> Data { get; }
public Series(string name, IEnumerable<(int, float)> data)
{
Name = name;
Data = data;
}
public Rect Bounds
{
get
{
int minx = int.MaxValue;
float miny = int.MaxValue;
int maxx = int.MinValue;
float maxy = int.MinValue;
foreach ((int x, float y) in Data)
{
minx = Mathf.Min(minx, x);
miny = Mathf.Min(miny, y);
maxx = Mathf.Max(maxx, x);
maxy = Mathf.Max(maxy, y);
}
if (minx > maxx)
{
minx = maxx = 0;
miny = maxy = 0;
}
return new Rect(minx, miny, maxx - minx, maxy - miny);
}
}
}
}

View File

@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 22e1ccfd8e2634bc4b60f1a482ec4c54
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -0,0 +1,16 @@
{
"name": "Mirror.Profiler",
"references": [
"Mirror"
],
"includePlatforms": [
"Editor"
],
"excludePlatforms": [],
"allowUnsafeCode": false,
"overrideReferences": true,
"precompiledReferences": [],
"autoReferenced": false,
"defineConstraints": [],
"versionDefines": []
}

View File

@ -0,0 +1,7 @@
fileFormatVersion: 2
guid: 4f6360625eae34006b0fbd08a678325c
AssemblyDefinitionImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -0,0 +1,300 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using UnityEngine;
namespace Mirror.Profiler
{
public class NetworkProfiler
{
internal List<NetworkProfileTick> ticks;
public int MaxFrames { get; set; }
public int MaxTicks { get; set; }
/// <summary>
///
/// </summary>
/// <param name="maxFrames"> How many frames should the profiler save</param>
public NetworkProfiler(int maxFrames = 600)
{
ticks = new List<NetworkProfileTick>(maxFrames);
MaxFrames = maxFrames;
}
bool isRecording;
public IList<NetworkProfileTick> Ticks
{
get => ticks;
}
public bool IsRecording
{
get => isRecording;
set
{
if (value && !isRecording)
{
NetworkDiagnostics.InMessageEvent += OnInMessage;
NetworkDiagnostics.OutMessageEvent += OnOutMessage;
}
else if (!value && isRecording)
{
NetworkDiagnostics.InMessageEvent -= OnInMessage;
NetworkDiagnostics.OutMessageEvent -= OnOutMessage;
}
isRecording = value;
}
}
private void OnInMessage(NetworkDiagnostics.MessageInfo messageInfo)
{
AddMessage(NetworkDirection.Incoming, messageInfo);
}
private void OnOutMessage(NetworkDiagnostics.MessageInfo messageInfo)
{
AddMessage(NetworkDirection.Outgoing, messageInfo);
}
private void AddMessage(NetworkDirection direction, NetworkDiagnostics.MessageInfo messageInfo)
{
NetworkProfileMessage profilerMessage = new NetworkProfileMessage
{
Direction = direction,
Type = messageInfo.message.GetType().Name,
Name = GetMethodName(messageInfo.message),
Channel = messageInfo.channel,
Size = messageInfo.bytes,
Count = messageInfo.count,
GameObject = GetGameObject(messageInfo.message)
};
// add to the tick
NetworkProfileTick tick = AddCurrentTick();
tick.RecordMessage(profilerMessage);
ticks[ticks.Count - 1] = tick;
DropOldTicks();
}
private NetworkProfileTick AddCurrentTick()
{
NetworkProfileTick lastTick = ticks.Count > 0 ? ticks[ticks.Count - 1] : new NetworkProfileTick();
if (ticks.Count == 0 || lastTick.frameCount != Time.frameCount)
{
NetworkProfileTick newTick = new NetworkProfileTick
{
frameCount = Time.frameCount,
time = Time.time,
};
ticks.Add(newTick);
return newTick;
}
return lastTick;
}
public NetworkProfileTick CurrentTick()
{
NetworkProfileTick lastTick = ticks.Count > 0 ? ticks[ticks.Count - 1] : new NetworkProfileTick();
if (ticks.Count == 0 || lastTick.frameCount < Time.frameCount)
{
NetworkProfileTick newTick = new NetworkProfileTick
{
frameCount = Time.frameCount
};
return newTick;
}
return lastTick;
}
private void DropOldTicks()
{
while (ticks.Count > 0)
{
if (ticks[0].frameCount < Time.frameCount - MaxFrames)
ticks.RemoveAt(0);
else
break;
}
while (ticks.Count > MaxTicks)
{
ticks.RemoveAt(0);
}
}
private string GetMethodName(IMessageBase message)
{
switch (message)
{
case CommandMessage msg:
return GetMethodName(msg.functionHash, "InvokeCmd");
case RpcMessage msg:
return GetMethodName(msg.functionHash, "InvokeRpc");
case SyncEventMessage msg:
return GetMethodName(msg.functionHash, "InvokeSyncEvent");
}
return null;
}
private string GetMethodName(int functionHash, string prefix)
{
string fullMethodName = NetworkBehaviour.GetRpcHandler(functionHash).Method.Name;
if (fullMethodName.StartsWith(prefix, StringComparison.Ordinal))
return fullMethodName.Substring(prefix.Length);
return fullMethodName;
}
private GameObject GetGameObject(IMessageBase message)
{
uint netId = 0;
switch (message)
{
case CommandMessage msg:
netId = msg.netId;
break;
case UpdateVarsMessage msg:
netId = msg.netId;
break;
case RpcMessage msg:
netId = msg.netId;
break;
case SyncEventMessage msg:
netId = msg.netId;
break;
case ObjectDestroyMessage msg:
netId = msg.netId;
break;
case SpawnMessage msg:
return msg.sceneId != 0 ? GetSceneObject(msg.sceneId) : GetPrefab(msg.assetId);
default:
return null;
}
if (NetworkIdentity.spawned.TryGetValue(netId, out NetworkIdentity id))
{
return id.gameObject;
}
return null;
}
private GameObject GetSceneObject(ulong sceneId)
{
try
{
NetworkIdentity[] ids = Resources.FindObjectsOfTypeAll<NetworkIdentity>();
foreach (var id in ids)
{
if (id.sceneId == sceneId)
return id.gameObject;
}
return null;
}
catch (Exception)
{
return null;
}
}
private GameObject GetPrefab(Guid assetId)
{
var networkManager = NetworkManager.singleton;
if (networkManager == null)
return null;
GameObject playerPrefab = networkManager.playerPrefab;
if (playerPrefab != null)
{
NetworkIdentity id = playerPrefab.GetComponent<NetworkIdentity>();
if (id != null && id.assetId == assetId)
{
return playerPrefab;
}
}
foreach (var prefab in networkManager.spawnPrefabs)
{
NetworkIdentity id = prefab.GetComponent<NetworkIdentity>();
if (id != null && id.assetId == assetId)
{
return prefab;
}
}
return null;
}
/// <summary>
/// Saves the current tick array to the specified file relative to the executing assembly
/// </summary>
/// <param name="filename">The filename to save to</param>
public void Save(string filename)
{
using (MemoryStream stream = new MemoryStream())
{
var formatter = new System.Runtime.Serialization.Formatters.Binary.BinaryFormatter();
formatter.Serialize(stream, this.Ticks);
File.WriteAllBytes(filename, stream.GetBuffer());
}
}
/// <summary>
/// Loads the ticks from the specified filename
/// </summary>
/// <param name="filename">The filename of the capture to load</param>
public void Load(string filename)
{
FileStream stream = File.OpenRead(filename);
var formatter = new System.Runtime.Serialization.Formatters.Binary.BinaryFormatter();
this.ticks = (List<NetworkProfileTick>)formatter.Deserialize(stream);
}
/// <summary>
/// Clears out all the ticks
/// </summary>
public void Clear()
{
this.ticks.Clear();
}
internal NetworkProfileTick GetTick(int frame)
{
var tick = Ticks.FirstOrDefault(t => t.frameCount == frame);
if (tick.frameCount != frame)
tick.frameCount = frame;
return tick;
}
public NetworkProfileTick GetNextMessageTick(int frame)
{
var tick = Ticks.FirstOrDefault(t => t.frameCount > frame);
if (tick.frameCount == 0)
tick.frameCount = frame + 1;
return tick;
}
public NetworkProfileTick GetPrevMessageTick(int frame)
{
var tick = Ticks.LastOrDefault(t => t.frameCount < frame);
if (tick.frameCount == 0)
tick.frameCount = frame - 1;
return tick;
}
}
}

View File

@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: eb842af19bf94bc4e83b08a766963df9
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -0,0 +1,114 @@
using System;
using System.Collections.Generic;
using UnityEngine;
namespace Mirror.Profiler
{
/// <summary>
/// The direction of network traffic
/// </summary>
[Serializable]
public enum NetworkDirection
{
/// <summary>
/// Data/Message is coming from a remote host
/// </summary>
Incoming = 0,
/// <summary>
/// Data/Message going to a remote host
/// </summary>
Outgoing = 1
}
/// <summary>
/// Stores a network tick
/// </summary>
[Serializable]
public struct NetworkProfileTick
{
/// <summary>
/// The Time.time at the moment tick collection ended
/// </summary>
public int frameCount;
private List<NetworkProfileMessage> messages;
internal float time;
/// <summary>
/// The summary of messages captured during the tick
/// </summary>
public List<NetworkProfileMessage> Messages
{
get
{
messages = messages ?? new List<NetworkProfileMessage>();
return messages;
}
}
/// <summary>
/// Records the message to the current tick
/// </summary>
public void RecordMessage(NetworkProfileMessage networkProfileMessage)
{
Messages.Add(networkProfileMessage);
}
public int Count(NetworkDirection direction)
{
int total = 0;
foreach (var message in Messages)
if (message.Direction == direction)
total += message.Count;
return total;
}
public int Bytes(NetworkDirection direction)
{
int total = 0;
foreach (var message in Messages)
if (message.Direction == direction)
total += message.Count * message.Size;
return total;
}
public int TotalMessages()
{
int total = 0;
foreach (var message in Messages)
total +=message.Count;
return total;
}
}
/// <summary>
/// Stores a network profile message
/// </summary>
[Serializable]
public struct NetworkProfileMessage
{
public NetworkDirection Direction;
public string Type;
public string Name;
public int Channel;
public int Size;
public int Count;
[NonSerialized]
private GameObject gameObject;
public GameObject GameObject
{
get => gameObject;
set
{
gameObject = value;
Object = value == null ? "" : value.name;
}
}
public string Object { get; private set; }
}
}

View File

@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: a08bddf54468b4432912e474dabc9457
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -0,0 +1,436 @@
using System;
using System.Collections.Generic;
using UnityEditor;
using UnityEditor.IMGUI.Controls;
using UnityEngine;
using Mirror.Profiler.Chart;
using Mirror.Profiler.Table;
using System.Linq;
namespace Mirror.Profiler
{
public class NetworkProfilerWindow : EditorWindow
{
public enum ChartSeries {
MessageCount,
TotalBytes,
Bandwidth
}
public const int MaxFrames = 1000;
public const int MaxTicks = 300;
private const string SaveEditorPrefKey = "Mirror.NetworkProfilerWindow.Record";
private const string CaptureFileExtension = "netdata";
private const float EstimatedOverheadPerMessage = 70f;
public int activeConfigIndex;
private GUIStyle headerStyle;
private Vector2 leftScrollPosition;
private readonly NetworkProfiler networkProfiler = new NetworkProfiler(MaxFrames);
internal NetworkProfileTick selectedTick;
TreeModel<MyTreeElement> treeModel;
private readonly ChartView chart;
public GUIStyle columnHeaderStyle { get; private set; }
public NetworkProfilerWindow()
{
var inCount = GetCountSeriesByFrame("Inbound (count)", NetworkDirection.Incoming);
var outCount = GetCountSeriesByFrame("Outbound (count)", NetworkDirection.Outgoing);
chart = new ChartView(inCount,outCount)
{
MaxFrames = MaxTicks,
};
chart.OnSelectFrame += Chart_OnSelectFrame;
}
private void Chart_OnSelectFrame(int frame)
{
if (ShowAllFrames)
{
NetworkProfileTick t = networkProfiler.GetTick(frame);
Show(t);
}
else if (frame >= 0 && frame < networkProfiler.Ticks.Count)
{
NetworkProfileTick t = networkProfiler.Ticks[frame];
Show(t);
}
else
{
Show(new NetworkProfileTick());
}
}
#region Render window
[NonSerialized] bool m_Initialized;
[SerializeField] TreeViewState m_TreeViewState; // Serialized in the window layout file so it survives assembly reloading
[SerializeField] MultiColumnHeaderState m_MultiColumnHeaderState;
MultiColumnTreeView m_TreeView;
void InitIfNeeded()
{
if (!m_Initialized)
{
// Check if it already exists (deserialized from window layout file or scriptable object)
if (m_TreeViewState == null)
m_TreeViewState = new TreeViewState();
bool firstInit = m_MultiColumnHeaderState == null;
MultiColumnHeaderState headerState = MultiColumnTreeView.CreateDefaultMultiColumnHeaderState();
if (MultiColumnHeaderState.CanOverwriteSerializedFields(m_MultiColumnHeaderState, headerState))
MultiColumnHeaderState.OverwriteSerializedFields(m_MultiColumnHeaderState, headerState);
m_MultiColumnHeaderState = headerState;
MultiColumnHeader multiColumnHeader = new MultiColumnHeader(headerState);
if (firstInit)
multiColumnHeader.ResizeToFit();
treeModel = new TreeModel<MyTreeElement>(GetData(selectedTick));
m_TreeView = new MultiColumnTreeView(m_TreeViewState, multiColumnHeader, treeModel);
m_Initialized = true;
}
}
IList<MyTreeElement> GetData(NetworkProfileTick tick)
{
// generate some test data
return MyTreeElementGenerator.GenerateTable(tick);
}
void DoTreeView(Rect rect)
{
InitIfNeeded();
m_TreeView.OnGUI(rect);
}
private void OnEnable()
{
this.networkProfiler.IsRecording = EditorPrefs.GetBool(SaveEditorPrefKey, false);
ConfigureProfiler();
}
public void OnGUI()
{
if (headerStyle == null)
{
headerStyle = new GUIStyle(GUI.skin.label) { alignment = TextAnchor.MiddleLeft, fontStyle = FontStyle.Bold, fontSize = 14 };
}
EditorGUILayout.BeginVertical(GUILayout.ExpandHeight(true), GUILayout.ExpandWidth(true));
this.DrawCommandBar();
Rect rect = GUILayoutUtility.GetRect(10, 1000, 200, 200);
this.chart.OnGUI(rect);
Rect treeRect = GUILayoutUtility.GetRect(new GUIContent(), new GUIStyle(), GUILayout.ExpandHeight(true), GUILayout.ExpandWidth(true));
this.DoTreeView(treeRect);
EditorGUILayout.EndVertical();
this.Repaint();
}
private void DrawCommandBar()
{
EditorGUILayout.BeginHorizontal(EditorStyles.toolbar, GUILayout.ExpandWidth(true));
bool newValue = GUILayout.Toggle(networkProfiler.IsRecording, "Record", EditorStyles.toolbarButton);
if (newValue != networkProfiler.IsRecording)
{
EditorPrefs.SetBool(SaveEditorPrefKey, newValue);
}
networkProfiler.IsRecording = newValue;
if (GUILayout.Button("Clear", EditorStyles.toolbarButton))
{
this.networkProfiler.Clear();
}
if (GUILayout.Button("Save", EditorStyles.toolbarButton))
{
string path = EditorUtility.SaveFilePanel("Save Network Profile", null, "Capture", CaptureFileExtension);
if (!string.IsNullOrEmpty(path))
{
this.networkProfiler.Save(path);
}
}
if (GUILayout.Button("Load", EditorStyles.toolbarButton))
{
string path = EditorUtility.OpenFilePanel("Open Network Profile", null, CaptureFileExtension);
if (!string.IsNullOrEmpty(path))
{
this.networkProfiler.Load(path);
if (this.networkProfiler.Ticks.Count > 0)
{
Show(networkProfiler.Ticks[0]);
this.ConfigureProfiler();
}
}
}
GUILayout.FlexibleSpace();
var leftIcon = EditorGUIUtility.TrIconContent("Animation.PrevKey", "Previous Messages");
if (GUILayout.Button(leftIcon, EditorStyles.toolbarButton))
{
if (ShowAllFrames)
{
NetworkProfileTick tick = networkProfiler.GetPrevMessageTick(chart.SelectedFrame);
chart.SelectedFrame = tick.frameCount;
}
else
{
chart.SelectedFrame -= 1;
}
}
var rightIcon = EditorGUIUtility.TrIconContent("Animation.NextKey", "Next Messages");
if (GUILayout.Button(rightIcon, EditorStyles.toolbarButton))
{
if (ShowAllFrames)
{
NetworkProfileTick tick = networkProfiler.GetNextMessageTick(chart.SelectedFrame);
chart.SelectedFrame = tick.frameCount;
}
else
{
chart.SelectedFrame += 1;
}
}
var optionsIcon = EditorGUIUtility.TrIconContent("_Popup", "Options");
if (EditorGUILayout.DropdownButton(optionsIcon , FocusType.Passive, EditorStyles.toolbarButton))
{
GenericMenu menu = new GenericMenu();
menu.AddItem(new GUIContent("Message count"), Series == ChartSeries.MessageCount, OnSeriesSelected, ChartSeries.MessageCount);
menu.AddItem(new GUIContent("Total bytes"), Series == ChartSeries.TotalBytes, OnSeriesSelected, ChartSeries.TotalBytes);
menu.AddItem(new GUIContent("Estimated Bandwidth"), Series == ChartSeries.Bandwidth, OnSeriesSelected, ChartSeries.Bandwidth);
menu.AddSeparator("");
menu.AddItem(new GUIContent("Show all frames"), ShowAllFrames, () =>
{
ShowAllFrames = !ShowAllFrames;
});
menu.ShowAsContext();
}
EditorGUILayout.EndHorizontal();
}
#region Chart options
private void OnSeriesSelected(object series)
{
EditorPrefs.SetString("Mirror.Profiler.Series", series.ToString());
ConfigureProfiler();
}
public ChartSeries Series =>
(ChartSeries)Enum.Parse(typeof(ChartSeries), EditorPrefs.GetString("Mirror.Profiler.Series", ChartSeries.MessageCount.ToString()));
public bool ShowAllFrames
{
get => EditorPrefs.GetBool("Mirror.Profiler.AllFrames", false);
set
{
EditorPrefs.SetBool("Mirror.Profiler.AllFrames", value);
ConfigureProfiler();
}
}
private void ConfigureProfiler()
{
ISeries inSeries;
ISeries outSeries;
if (ShowAllFrames)
{
networkProfiler.MaxFrames = MaxFrames;
networkProfiler.MaxTicks = int.MaxValue;
chart.MaxFrames = MaxFrames;
(inSeries, outSeries) = AllFrameSeries();
}
else
{
networkProfiler.MaxFrames = int.MaxValue;
networkProfiler.MaxTicks = MaxTicks;
chart.MaxFrames = MaxTicks;
(inSeries, outSeries) = AllTicksSeries();
}
chart.Series[0] = inSeries;
chart.Series[1] = outSeries;
}
private (ISeries, ISeries) AllFrameSeries()
{
switch (Series)
{
case ChartSeries.TotalBytes:
return (
GetByteSeriesByFrame("Inbound (bytes)", NetworkDirection.Incoming),
GetByteSeriesByFrame("Outbound (bytes)", NetworkDirection.Outgoing));
case ChartSeries.MessageCount:
return (
GetCountSeriesByFrame("Inbound (bytes)", NetworkDirection.Incoming),
GetCountSeriesByFrame("Outbound (bytes)", NetworkDirection.Outgoing));
case ChartSeries.Bandwidth:
return (
GetBandwidthSeriesByFrame("Inbound (bytes/s)", NetworkDirection.Incoming),
GetBandwidthSeriesByFrame("Outbound (bytes/s)", NetworkDirection.Outgoing));
default:
return (
GetByteSeriesByFrame("Inbound (bytes)", NetworkDirection.Incoming),
GetByteSeriesByFrame("Outbound (bytes)", NetworkDirection.Outgoing));
}
}
private (ISeries, ISeries) AllTicksSeries()
{
switch (Series)
{
case ChartSeries.TotalBytes:
return (
GetByteSeriesByIndex("Inbound (bytes)", NetworkDirection.Incoming),
GetByteSeriesByIndex("Outbound (bytes)", NetworkDirection.Outgoing));
case ChartSeries.MessageCount:
return (
GetCountSeriesByIndex("Inbound (bytes)", NetworkDirection.Incoming),
GetCountSeriesByIndex("Outbound (bytes)", NetworkDirection.Outgoing));
case ChartSeries.Bandwidth:
return (
GetBandwidthSeriesByIndex("Inbound (bytes/s)", NetworkDirection.Incoming),
GetBandwidthSeriesByIndex("Outbound (bytes/s)", NetworkDirection.Outgoing));
default:
throw new Exception("Invalid chart type");
}
}
private ISeries GetCountSeriesByFrame(string legend, NetworkDirection direction)
{
var data = networkProfiler.Ticks.Select(tick => (tick.frameCount, (float)tick.Count(direction)));
// append the current frame
var lastTick = Enumerable.Range(0, 1).Select(_ =>
{
var tick = networkProfiler.CurrentTick();
return (tick.frameCount, (float)tick.Count(direction));
});
return new Series(legend, data.Concat(lastTick));
}
private ISeries GetByteSeriesByFrame(string legend, NetworkDirection direction)
{
var data = networkProfiler.Ticks.Select(tick => (tick.frameCount, (float)tick.Bytes(direction)));
// append the current frame
var lastTick = Enumerable.Range(0, 1).Select(_ =>
{
var tick = networkProfiler.CurrentTick();
return (tick.frameCount, (float)tick.Bytes(direction));
});
return new Series(legend, data.Concat(lastTick));
}
private ISeries GetCountSeriesByIndex(string legend, NetworkDirection direction)
{
var data = networkProfiler.Ticks.Select((tick, i) => (i, (float)tick.Count(direction)));
return new Series(legend, data);
}
private ISeries GetByteSeriesByIndex(string legend, NetworkDirection direction)
{
var data = networkProfiler.Ticks.Select((tick, i) => (i, (float)tick.Bytes(direction)));
return new Series(legend, data);
}
private ISeries GetBandwidthSeriesByIndex(string legend, NetworkDirection direction)
{
var tickData = networkProfiler.Ticks;
var tick1 = new[]
{
new NetworkProfileTick
{
frameCount = 0,
time =0
}
};
var allTicks = tick1.Concat(tickData);
var data = allTicks
.Zip(tickData, (prevTick, newTick) => EstimateBandwidth(newTick, prevTick, direction))
.Select((value, i) => (i - 1, value));
return new Series(legend, data);
}
private float EstimateBandwidth(NetworkProfileTick tick, NetworkProfileTick prevTick, NetworkDirection direction)
{
float estimatedBytes = tick.Count(direction) * EstimatedOverheadPerMessage + tick.Bytes(direction);
if (prevTick.frameCount == 0 || tick.time <= prevTick.time)
return estimatedBytes;
return estimatedBytes / (tick.time - prevTick.time);
}
private ISeries GetBandwidthSeriesByFrame(string legend, NetworkDirection direction)
{
var data = networkProfiler.Ticks.Select(tick => (tick.frameCount, tick.Bytes(direction) + tick.Count(direction) * EstimatedOverheadPerMessage));
// append the current frame
var lastTick = Enumerable.Range(0, 1).Select(_ =>
{
var tick = networkProfiler.CurrentTick();
return (tick.frameCount, tick.Bytes(direction) + tick.Count(direction) * EstimatedOverheadPerMessage);
});
return new Series(legend, data.Concat(lastTick));
}
#endregion
[MenuItem("Window/Analysis/Mirror Network Profiler", false, 0)]
public static void ShowWindow()
{
GetWindow<NetworkProfilerWindow>("Mirror Network Profiler");
}
public void Show(NetworkProfileTick tick)
{
selectedTick = tick;
treeModel.SetData(GetData(selectedTick));
}
#endregion
}
}

View File

@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: a090c2f7eb0f55941be54dad139bc759
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -0,0 +1,14 @@
# Mirror profiler
The mirror profiler is a graphical tool for diagnosing network performance with mirror.
## Install
1) Clone mirror repository in https://github.com/vis2k/Mirror
2) Clone this repository inside Assets/Mirror/Profiler
3) Open whole project in Unity
4) Open the profiler in Window -> Analysis -> Mirror Network Profiler
5) Open the basic example scene
6) Hit play
7) click "Record" in the mirror network profiler
8) click on "Start Host"

View File

@ -0,0 +1,7 @@
fileFormatVersion: 2
guid: 41e0c9adc640542668992e114c4ed136
TextScriptImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: f6aecdbb5da6c4891b64f929e4a11ec7
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -0,0 +1,384 @@
using System;
using System.Collections.Generic;
using System.Linq;
using UnityEditor;
using UnityEditor.IMGUI.Controls;
using UnityEngine;
using UnityEngine.Assertions;
namespace Mirror.Profiler.Table
{
internal class MultiColumnTreeView : TreeViewWithTreeModel<MyTreeElement>
{
const float KRowHeights = 20f;
const float KToggleWidth = 18f;
public bool showControls = true;
// All columns
enum MyColumns
{
Direction,
Name,
Object,
Count,
Bytes,
TotalBytes,
Channel,
}
public enum SortOption
{
Direction,
Name,
Object,
Count,
Bytes,
TotalBytes,
Channel,
}
// Sort options per column
readonly SortOption[] m_SortOptions =
{
SortOption.Direction,
SortOption.Name,
SortOption.Object,
SortOption.Count,
SortOption.Bytes,
SortOption.TotalBytes,
SortOption.Channel
};
public static void TreeToList(TreeViewItem root, IList<TreeViewItem> result)
{
if (root == null)
throw new NullReferenceException("root");
if (result == null)
throw new NullReferenceException("result");
result.Clear();
if (root.children == null)
return;
Stack<TreeViewItem> stack = new Stack<TreeViewItem>();
for (int i = root.children.Count - 1; i >= 0; i--)
stack.Push(root.children[i]);
while (stack.Count > 0)
{
TreeViewItem current = stack.Pop();
result.Add(current);
if (current.hasChildren && current.children[0] != null)
{
for (int i = current.children.Count - 1; i >= 0; i--)
{
stack.Push(current.children[i]);
}
}
}
}
public MultiColumnTreeView(TreeViewState state, MultiColumnHeader multicolumnHeader, TreeModel<MyTreeElement> model) : base(state, multicolumnHeader, model)
{
Assert.AreEqual(m_SortOptions.Length, Enum.GetValues(typeof(MyColumns)).Length, "Ensure number of sort options are in sync with number of MyColumns enum values");
// Custom setup
rowHeight = KRowHeights;
columnIndexForTreeFoldouts = 1;
showAlternatingRowBackgrounds = true;
showBorder = true;
customFoldoutYOffset = (KRowHeights - EditorGUIUtility.singleLineHeight) * 0.5f; // center foldout in the row since we also center content. See RowGUI
extraSpaceBeforeIconAndLabel = KToggleWidth;
multicolumnHeader.sortingChanged += OnSortingChanged;
Reload();
}
// Note we We only build the visible rows, only the backend has the full tree information.
// The treeview only creates info for the row list.
protected override IList<TreeViewItem> BuildRows(TreeViewItem root)
{
IList<TreeViewItem> rows = base.BuildRows(root);
SortIfNeeded(root, rows);
return rows;
}
void OnSortingChanged(MultiColumnHeader _)
{
SortIfNeeded(rootItem, GetRows());
}
void SortIfNeeded(TreeViewItem root, IList<TreeViewItem> rows)
{
if (rows.Count <= 1)
return;
if (multiColumnHeader.sortedColumnIndex == -1)
{
return; // No column to sort for (just use the order the data are in)
}
// Sort the roots of the existing tree items
SortByMultipleColumns();
TreeToList(root, rows);
Repaint();
}
void SortByMultipleColumns()
{
int[] sortedColumns = multiColumnHeader.state.sortedColumns;
if (sortedColumns.Length == 0)
return;
IEnumerable<TreeViewItem<MyTreeElement>> myTypes = rootItem.children.Cast<TreeViewItem<MyTreeElement>>();
IOrderedEnumerable<TreeViewItem<MyTreeElement>> orderedQuery = InitialOrder(myTypes, sortedColumns);
for (int i = 1; i < sortedColumns.Length; i++)
{
SortOption sortOption = m_SortOptions[sortedColumns[i]];
bool ascending = multiColumnHeader.IsSortedAscending(sortedColumns[i]);
switch (sortOption)
{
case SortOption.Direction:
orderedQuery = orderedQuery.ThenBy(l => l.data.message.Direction, ascending);
break;
case SortOption.Name:
orderedQuery = orderedQuery.ThenBy(l => l.data.name, ascending);
break;
case SortOption.Object:
orderedQuery = orderedQuery.ThenBy(l => l.data.message.Object, ascending);
break;
case SortOption.Count:
orderedQuery = orderedQuery.ThenBy(l => l.data.message.Count, ascending);
break;
case SortOption.Bytes:
orderedQuery = orderedQuery.ThenBy(l => l.data.message.Size, ascending);
break;
case SortOption.TotalBytes:
orderedQuery = orderedQuery.ThenBy(l => l.data.message.Size * l.data.message.Count, ascending);
break;
case SortOption.Channel:
orderedQuery = orderedQuery.ThenBy(l => l.data.message.Channel, ascending);
break;
}
}
rootItem.children = orderedQuery.Cast<TreeViewItem>().ToList();
}
IOrderedEnumerable<TreeViewItem<MyTreeElement>> InitialOrder(IEnumerable<TreeViewItem<MyTreeElement>> myTypes, int[] history)
{
SortOption sortOption = m_SortOptions[history[0]];
bool ascending = multiColumnHeader.IsSortedAscending(history[0]);
switch (sortOption)
{
case SortOption.Direction:
return myTypes.Order(l => l.data.message.Direction, ascending);
case SortOption.Name:
return myTypes.Order(l => l.data.name, ascending);
case SortOption.Object:
return myTypes.Order(l => l.data.message.Object, ascending);
case SortOption.Count:
return myTypes.Order(l => l.data.message.Count, ascending);
case SortOption.Bytes:
return myTypes.Order(l => l.data.message.Size, ascending);
case SortOption.TotalBytes:
return myTypes.Order(l => l.data.message.Size * l.data.message.Count, ascending);
case SortOption.Channel:
return myTypes.Order(l => l.data.message.Channel, ascending);
default:
Assert.IsTrue(false, "Unhandled enum");
break;
}
// default
return myTypes.Order(l => l.data.name, ascending);
}
protected override void RowGUI(RowGUIArgs args)
{
TreeViewItem<MyTreeElement> item = (TreeViewItem<MyTreeElement>)args.item;
for (int i = 0; i < args.GetNumVisibleColumns(); ++i)
{
CellGUI(args.GetCellRect(i), item, (MyColumns)args.GetColumn(i), ref args);
}
}
void CellGUI(Rect cellRect, TreeViewItem<MyTreeElement> item, MyColumns column, ref RowGUIArgs args)
{
// Center cell rect vertically (makes it easier to place controls, icons etc in the cells)
CenterRectUsingSingleLineHeight(ref cellRect);
string value = "";
switch (column)
{
case MyColumns.Direction:
value = item.data.message.Direction == NetworkDirection.Incoming ? "In" : "Out";
break;
case MyColumns.Name:
value = item.data.name;
break;
case MyColumns.Object:
value = item.data.message.Object;
break;
case MyColumns.Count:
value = item.data.message.Count.ToString();
break;
case MyColumns.Bytes:
value = item.data.message.Size.ToString();
break;
case MyColumns.TotalBytes:
value = (item.data.message.Size * item.data.message.Count).ToString();
break;
case MyColumns.Channel:
value = item.data.message.Channel < 0 ? "" : item.data.message.Channel.ToString();
break;
}
DefaultGUI.Label(cellRect, value, args.selected, args.focused);
}
protected override void SingleClickedItem(int id)
{
base.SingleClickedItem(id);
IList<TreeViewItem> rows = GetRows();
TreeViewItem<MyTreeElement> item = (TreeViewItem<MyTreeElement>)rows[id - 1];
GameObject gameobject = item.data.message.GameObject;
if (gameobject != null)
{
Selection.activeGameObject = gameobject;
}
}
// Misc
//--------
protected override bool CanMultiSelect(TreeViewItem item)
{
return true;
}
public static MultiColumnHeaderState CreateDefaultMultiColumnHeaderState()
{
MultiColumnHeaderState.Column[] columns = {
new MultiColumnHeaderState.Column
{
headerContent = new GUIContent("In/Out"),
contextMenuText = "In/Out",
headerTextAlignment = TextAlignment.Left,
sortedAscending = true,
sortingArrowAlignment = TextAlignment.Right,
width = 50,
minWidth = 30,
maxWidth = 60,
autoResize = false,
allowToggleVisibility = true
},
new MultiColumnHeaderState.Column
{
headerContent = new GUIContent("Name"),
headerTextAlignment = TextAlignment.Left,
sortedAscending = true,
sortingArrowAlignment = TextAlignment.Right,
width = 110,
minWidth = 60,
autoResize = true,
allowToggleVisibility = false
},
new MultiColumnHeaderState.Column
{
headerContent = new GUIContent("Object"),
headerTextAlignment = TextAlignment.Left,
sortedAscending = true,
sortingArrowAlignment = TextAlignment.Right,
width = 110,
minWidth = 60,
autoResize = true,
allowToggleVisibility = false
},
new MultiColumnHeaderState.Column
{
headerContent = new GUIContent("Count", "How many observers received the message" ),
headerTextAlignment = TextAlignment.Left,
sortedAscending = true,
sortingArrowAlignment = TextAlignment.Right,
width = 70,
minWidth = 60,
autoResize = false
},
new MultiColumnHeaderState.Column
{
headerContent = new GUIContent("Bytes", "How big was the message. Not including transport headers"),
headerTextAlignment = TextAlignment.Left,
sortedAscending = true,
sortingArrowAlignment = TextAlignment.Right,
width = 70,
minWidth = 60,
autoResize = false,
allowToggleVisibility = true
},
new MultiColumnHeaderState.Column
{
headerContent = new GUIContent("Total Bytes", "Total amount of bytes sent (Count * Bytes) "),
headerTextAlignment = TextAlignment.Left,
sortedAscending = true,
sortingArrowAlignment = TextAlignment.Right,
width = 70,
minWidth = 60,
autoResize = false
},
new MultiColumnHeaderState.Column
{
headerContent = new GUIContent("Channel", "Channel through which the message was sent"),
headerTextAlignment = TextAlignment.Left,
sortedAscending = true,
sortingArrowAlignment = TextAlignment.Right,
width = 70,
minWidth = 60,
autoResize = false
}
};
Assert.AreEqual(columns.Length, Enum.GetValues(typeof(MyColumns)).Length, "Number of columns should match number of enum values: You probably forgot to update one of them.");
MultiColumnHeaderState state = new MultiColumnHeaderState(columns);
return state;
}
}
static class MyExtensionMethods
{
public static IOrderedEnumerable<T> Order<T, TKey>(this IEnumerable<T> source, Func<T, TKey> selector, bool ascending)
{
if (ascending)
{
return source.OrderBy(selector);
}
return source.OrderByDescending(selector);
}
public static IOrderedEnumerable<T> ThenBy<T, TKey>(this IOrderedEnumerable<T> source, Func<T, TKey> selector, bool ascending)
{
if (ascending)
{
return source.ThenBy(selector);
}
return source.ThenByDescending(selector);
}
}
}

View File

@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 1b0852662036646439b19f80d65f25c4
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -0,0 +1,21 @@
using System;
using UnityEngine;
using Random = UnityEngine.Random;
namespace Mirror.Profiler.Table
{
[Serializable]
internal class MyTreeElement : TreeElement
{
public NetworkProfileMessage message;
public MyTreeElement(NetworkProfileMessage message, int depth, int id) : base(depth, id)
{
this.message = message;
}
public override string name => message.Name ?? message.Type ?? "";
}
}

View File

@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 040ad8037c1ae49ab95def4bf2b9cea3
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -0,0 +1,29 @@
using System;
using System.Collections.Generic;
using Random = UnityEngine.Random;
namespace Mirror.Profiler.Table
{
static class MyTreeElementGenerator
{
internal static IList<MyTreeElement> GenerateTable(NetworkProfileTick tick)
{
int IDCounter = 0;
List<MyTreeElement> treeElements = new List<MyTreeElement>(tick.Messages.Count);
MyTreeElement root = new MyTreeElement(new NetworkProfileMessage(), -1, IDCounter++);
treeElements.Add(root);
foreach (var message in tick.Messages)
{
MyTreeElement child = new MyTreeElement(message, 0, IDCounter++);
treeElements.Add(child);
}
return treeElements;
}
}
}

View File

@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: b7c37deb0866a48539dc2183b612b598
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -0,0 +1,64 @@
using System;
using System.Collections.Generic;
using UnityEngine;
namespace Mirror.Profiler.Table
{
[Serializable]
public abstract class TreeElement
{
[SerializeField] int m_ID;
[SerializeField] int m_Depth;
[NonSerialized] TreeElement m_Parent;
[NonSerialized] List<TreeElement> m_Children;
public int depth
{
get { return m_Depth; }
set { m_Depth = value; }
}
public TreeElement parent
{
get { return m_Parent; }
set { m_Parent = value; }
}
public abstract string name
{
get;
}
public List<TreeElement> children
{
get { return m_Children; }
set { m_Children = value; }
}
public bool hasChildren
{
get { return children != null && children.Count > 0; }
}
public int id
{
get { return m_ID; }
set { m_ID = value; }
}
public TreeElement()
{
}
public TreeElement( int depth, int id)
{
m_ID = id;
m_Depth = depth;
}
}
}

View File

@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 6018fe3de1538447190001b95d052dad
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -0,0 +1,102 @@
using System;
using System.Collections.Generic;
using System.Linq;
using UnityEditor;
namespace Mirror.Profiler.Table
{
// TreeElementUtility and TreeElement are useful helper classes for backend tree data structures.
// See tests at the bottom for examples of how to use.
public static class TreeElementUtility
{
// Returns the root of the tree parsed from the list (always the first element).
// Important: the first item and is required to have a depth value of -1.
// The rest of the items should have depth >= 0.
public static T ListToTree<T>(IList<T> list) where T : TreeElement
{
// Validate input
ValidateDepthValues(list);
// Clear old states
foreach (var element in list)
{
element.parent = null;
element.children = null;
}
// Set child and parent references using depth info
for (int parentIndex = 0; parentIndex < list.Count; parentIndex++)
{
var parent = list[parentIndex];
bool alreadyHasValidChildren = parent.children != null;
if (alreadyHasValidChildren)
continue;
int parentDepth = parent.depth;
int childCount = 0;
// Count children based depth value, we are looking at children until it's the same depth as this object
for (int i = parentIndex + 1; i < list.Count; i++)
{
if (list[i].depth == parentDepth + 1)
childCount++;
if (list[i].depth <= parentDepth)
break;
}
// Fill child array
List<TreeElement> childList = null;
if (childCount != 0)
{
childList = new List<TreeElement>(childCount); // Allocate once
childCount = 0;
for (int i = parentIndex + 1; i < list.Count; i++)
{
if (list[i].depth == parentDepth + 1)
{
list[i].parent = parent;
childList.Add(list[i]);
childCount++;
}
if (list[i].depth <= parentDepth)
break;
}
}
parent.children = childList;
}
return list[0];
}
// Check state of input list
public static void ValidateDepthValues<T>(IList<T> list) where T : TreeElement
{
if (list.Count == 0)
throw new ArgumentException("list should have items, count is 0, check before calling ValidateDepthValues", "list");
if (list[0].depth != -1)
throw new ArgumentException("list item at index 0 should have a depth of -1 (since this should be the hidden root of the tree). Depth is: " + list[0].depth, "list");
for (int i = 0; i < list.Count - 1; i++)
{
int depth = list[i].depth;
int nextDepth = list[i + 1].depth;
if (nextDepth > depth && nextDepth - depth > 1)
throw new ArgumentException(string.Format("Invalid depth info in input list. Depth cannot increase more than 1 per row. Index {0} has depth {1} while index {2} has depth {3}", i, depth, i + 1, nextDepth));
}
for (int i = 1; i < list.Count; ++i)
if (list[i].depth < 0)
throw new ArgumentException("Invalid depth value for item at index " + i + ". Only the first item (the root) should have depth below 0.");
if (list.Count > 1 && list[1].depth != 0)
throw new ArgumentException("Input list item at index 1 is assumed to have a depth of 0", "list");
}
}
}

View File

@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: ac69cc547e5d04cd999f18d3002e9208
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -0,0 +1,114 @@
using System;
using System.Collections.Generic;
using System.Linq;
namespace Mirror.Profiler.Table
{
// The TreeModel is a utility class working on a list of serializable TreeElements where the order and the depth of each TreeElement define
// the tree structure. Note that the TreeModel itself is not serializable (in Unity we are currently limited to serializing lists/arrays) but the
// input list is.
// The tree representation (parent and children references) are then build internally using TreeElementUtility.ListToTree (using depth
// values of the elements).
// The first element of the input list is required to have depth == -1 (the hiddenroot) and the rest to have
// depth >= 0 (otherwise an exception will be thrown)
public class TreeModel<T> where T : TreeElement
{
IList<T> m_Data;
T m_Root;
int m_MaxID;
public T root { get { return m_Root; } set { m_Root = value; } }
public event Action modelChanged;
public int numberOfDataElements
{
get { return m_Data.Count; }
}
public TreeModel(IList<T> data)
{
SetData(data);
}
public T Find(int id)
{
return m_Data.FirstOrDefault(element => element.id == id);
}
public void SetData(IList<T> data)
{
Init(data);
}
void Init(IList<T> data)
{
m_Data = data ?? throw new ArgumentNullException(nameof(data), "Input data is null. Ensure input is a non-null list.");
if (m_Data.Count > 0)
m_Root = TreeElementUtility.ListToTree(data);
m_MaxID = m_Data.Max(e => e.id);
Changed();
}
public int GenerateUniqueID()
{
return ++m_MaxID;
}
public IList<int> GetAncestors(int id)
{
List<int> parents = new List<int>();
TreeElement T = Find(id);
if (T != null)
{
while (T.parent != null)
{
parents.Add(T.parent.id);
T = T.parent;
}
}
return parents;
}
public IList<int> GetDescendantsThatHaveChildren(int id)
{
T searchFromThis = Find(id);
if (searchFromThis != null)
{
return GetParentsBelowStackBased(searchFromThis);
}
return new List<int>();
}
IList<int> GetParentsBelowStackBased(TreeElement searchFromThis)
{
Stack<TreeElement> stack = new Stack<TreeElement>();
stack.Push(searchFromThis);
List<int> parentsBelow = new List<int>();
while (stack.Count > 0)
{
TreeElement current = stack.Pop();
if (current.hasChildren)
{
parentsBelow.Add(current.id);
foreach (var T in current.children)
{
stack.Push(T);
}
}
}
return parentsBelow;
}
void Changed()
{
if (modelChanged != null)
modelChanged();
}
}
}

View File

@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: d5dcc828380a0402b9d0e40a47005d6b
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -0,0 +1,152 @@
using System;
using System.Collections.Generic;
using System.Linq;
using UnityEditor;
using UnityEditor.IMGUI.Controls;
using UnityEngine;
namespace Mirror.Profiler.Table
{
internal class TreeViewItem<T> : TreeViewItem where T : TreeElement
{
public T data { get; set; }
public TreeViewItem(int id, int depth, string displayName, T data) : base(id, depth, displayName)
{
this.data = data;
}
}
internal class TreeViewWithTreeModel<T> : TreeView where T : TreeElement
{
readonly List<TreeViewItem> m_Rows = new List<TreeViewItem>(100);
public event Action treeChanged;
public TreeModel<T> treeModel { get; private set; }
public TreeViewWithTreeModel(TreeViewState state, TreeModel<T> model) : base(state)
{
Init(model);
}
public TreeViewWithTreeModel(TreeViewState state, MultiColumnHeader multiColumnHeader, TreeModel<T> model)
: base(state, multiColumnHeader)
{
Init(model);
}
void Init(TreeModel<T> model)
{
treeModel = model;
treeModel.modelChanged += ModelChanged;
}
void ModelChanged()
{
treeChanged?.Invoke();
Reload();
}
protected override TreeViewItem BuildRoot()
{
int depthForHiddenRoot = -1;
return new TreeViewItem<T>(treeModel.root.id, depthForHiddenRoot, treeModel.root.name, treeModel.root);
}
protected override IList<TreeViewItem> BuildRows(TreeViewItem root)
{
if (treeModel.root == null)
{
Debug.LogError("tree model root is null. did you call SetData()?");
}
m_Rows.Clear();
if (!string.IsNullOrEmpty(searchString))
{
Search(treeModel.root, searchString, m_Rows);
}
else
{
if (treeModel.root.hasChildren)
AddChildrenRecursive(treeModel.root, 0, m_Rows);
}
// We still need to setup the child parent information for the rows since this
// information is used by the TreeView internal logic (navigation, dragging etc)
SetupParentsAndChildrenFromDepths(root, m_Rows);
return m_Rows;
}
void AddChildrenRecursive(T parent, int depth, IList<TreeViewItem> newRows)
{
foreach (T child in parent.children)
{
var item = new TreeViewItem<T>(child.id, depth, child.name, child);
newRows.Add(item);
if (child.hasChildren)
{
if (IsExpanded(child.id))
{
AddChildrenRecursive(child, depth + 1, newRows);
}
else
{
item.children = CreateChildListForCollapsedParent();
}
}
}
}
void Search(T searchFromThis, string search, List<TreeViewItem> result)
{
if (string.IsNullOrEmpty(search))
throw new ArgumentException("Invalid search: cannot be null or empty", "search");
const int kItemDepth = 0; // tree is flattened when searching
Stack<T> stack = new Stack<T>();
foreach (var element in searchFromThis.children)
stack.Push((T)element);
while (stack.Count > 0)
{
T current = stack.Pop();
// Matches search?
if (current.name.IndexOf(search, StringComparison.OrdinalIgnoreCase) >= 0)
{
result.Add(new TreeViewItem<T>(current.id, kItemDepth, current.name, current));
}
if (current.children != null && current.children.Count > 0)
{
foreach (var element in current.children)
{
stack.Push((T)element);
}
}
}
SortSearchResult(result);
}
protected virtual void SortSearchResult(List<TreeViewItem> rows)
{
rows.Sort((x, y) => EditorUtility.NaturalCompare(x.displayName, y.displayName)); // sort by displayName by default, can be overriden for multicolumn solutions
}
protected override IList<int> GetAncestors(int id)
{
return treeModel.GetAncestors(id);
}
protected override IList<int> GetDescendantsThatHaveChildren(int id)
{
return treeModel.GetDescendantsThatHaveChildren(id);
}
}
}

View File

@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 4ddf5bd6d1c6d4724b7368e8b4814479
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant: