mirror of
https://github.com/MirrorNetworking/Mirror.git
synced 2024-11-18 02:50:32 +00:00
old profiler files
This commit is contained in:
parent
1721b5601b
commit
21115a3785
8
Assets/Mirror/Profiler.meta
Normal file
8
Assets/Mirror/Profiler.meta
Normal file
@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 82fe9954a7d514fb098fd578f6c89acc
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
8
Assets/Mirror/Profiler/Chart.meta
Normal file
8
Assets/Mirror/Profiler/Chart.meta
Normal file
@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: fda3d426dcfd94e53ba1cc2e77522cd0
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
366
Assets/Mirror/Profiler/Chart/ChartView.cs
Normal file
366
Assets/Mirror/Profiler/Chart/ChartView.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
11
Assets/Mirror/Profiler/Chart/ChartView.cs.meta
Normal file
11
Assets/Mirror/Profiler/Chart/ChartView.cs.meta
Normal file
@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: cce5c930d77ae429d90b86cbfb2d0a01
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
14
Assets/Mirror/Profiler/Chart/ISeries.cs
Normal file
14
Assets/Mirror/Profiler/Chart/ISeries.cs
Normal 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; }
|
||||
}
|
||||
}
|
11
Assets/Mirror/Profiler/Chart/ISeries.cs.meta
Normal file
11
Assets/Mirror/Profiler/Chart/ISeries.cs.meta
Normal file
@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: d7a6aa3d0128c4140a5dee1ad948f993
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
49
Assets/Mirror/Profiler/Chart/Series.cs
Normal file
49
Assets/Mirror/Profiler/Chart/Series.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
11
Assets/Mirror/Profiler/Chart/Series.cs.meta
Normal file
11
Assets/Mirror/Profiler/Chart/Series.cs.meta
Normal file
@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 22e1ccfd8e2634bc4b60f1a482ec4c54
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
16
Assets/Mirror/Profiler/Mirror.Profiler.asmdef
Normal file
16
Assets/Mirror/Profiler/Mirror.Profiler.asmdef
Normal file
@ -0,0 +1,16 @@
|
||||
{
|
||||
"name": "Mirror.Profiler",
|
||||
"references": [
|
||||
"Mirror"
|
||||
],
|
||||
"includePlatforms": [
|
||||
"Editor"
|
||||
],
|
||||
"excludePlatforms": [],
|
||||
"allowUnsafeCode": false,
|
||||
"overrideReferences": true,
|
||||
"precompiledReferences": [],
|
||||
"autoReferenced": false,
|
||||
"defineConstraints": [],
|
||||
"versionDefines": []
|
||||
}
|
7
Assets/Mirror/Profiler/Mirror.Profiler.asmdef.meta
Normal file
7
Assets/Mirror/Profiler/Mirror.Profiler.asmdef.meta
Normal file
@ -0,0 +1,7 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 4f6360625eae34006b0fbd08a678325c
|
||||
AssemblyDefinitionImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
300
Assets/Mirror/Profiler/NetworkProfiler.cs
Normal file
300
Assets/Mirror/Profiler/NetworkProfiler.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
11
Assets/Mirror/Profiler/NetworkProfiler.cs.meta
Normal file
11
Assets/Mirror/Profiler/NetworkProfiler.cs.meta
Normal file
@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: eb842af19bf94bc4e83b08a766963df9
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
114
Assets/Mirror/Profiler/NetworkProfilerTick.cs
Normal file
114
Assets/Mirror/Profiler/NetworkProfilerTick.cs
Normal 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; }
|
||||
}
|
||||
|
||||
}
|
11
Assets/Mirror/Profiler/NetworkProfilerTick.cs.meta
Normal file
11
Assets/Mirror/Profiler/NetworkProfilerTick.cs.meta
Normal file
@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: a08bddf54468b4432912e474dabc9457
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
436
Assets/Mirror/Profiler/NetworkProfilerWindow.cs
Normal file
436
Assets/Mirror/Profiler/NetworkProfilerWindow.cs
Normal 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
|
||||
}
|
||||
}
|
11
Assets/Mirror/Profiler/NetworkProfilerWindow.cs.meta
Normal file
11
Assets/Mirror/Profiler/NetworkProfilerWindow.cs.meta
Normal file
@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: a090c2f7eb0f55941be54dad139bc759
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
14
Assets/Mirror/Profiler/README.md
Normal file
14
Assets/Mirror/Profiler/README.md
Normal 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"
|
7
Assets/Mirror/Profiler/README.md.meta
Normal file
7
Assets/Mirror/Profiler/README.md.meta
Normal file
@ -0,0 +1,7 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 41e0c9adc640542668992e114c4ed136
|
||||
TextScriptImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
8
Assets/Mirror/Profiler/Table.meta
Normal file
8
Assets/Mirror/Profiler/Table.meta
Normal file
@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: f6aecdbb5da6c4891b64f929e4a11ec7
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
384
Assets/Mirror/Profiler/Table/MultiColumnTreeView.cs
Normal file
384
Assets/Mirror/Profiler/Table/MultiColumnTreeView.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
11
Assets/Mirror/Profiler/Table/MultiColumnTreeView.cs.meta
Normal file
11
Assets/Mirror/Profiler/Table/MultiColumnTreeView.cs.meta
Normal file
@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 1b0852662036646439b19f80d65f25c4
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
21
Assets/Mirror/Profiler/Table/MyTreeElement.cs
Normal file
21
Assets/Mirror/Profiler/Table/MyTreeElement.cs
Normal 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 ?? "";
|
||||
}
|
||||
}
|
11
Assets/Mirror/Profiler/Table/MyTreeElement.cs.meta
Normal file
11
Assets/Mirror/Profiler/Table/MyTreeElement.cs.meta
Normal file
@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 040ad8037c1ae49ab95def4bf2b9cea3
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
29
Assets/Mirror/Profiler/Table/MyTreeElementGenerator.cs
Normal file
29
Assets/Mirror/Profiler/Table/MyTreeElementGenerator.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
11
Assets/Mirror/Profiler/Table/MyTreeElementGenerator.cs.meta
Normal file
11
Assets/Mirror/Profiler/Table/MyTreeElementGenerator.cs.meta
Normal file
@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: b7c37deb0866a48539dc2183b612b598
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
64
Assets/Mirror/Profiler/Table/TreeElement.cs
Normal file
64
Assets/Mirror/Profiler/Table/TreeElement.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
11
Assets/Mirror/Profiler/Table/TreeElement.cs.meta
Normal file
11
Assets/Mirror/Profiler/Table/TreeElement.cs.meta
Normal file
@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 6018fe3de1538447190001b95d052dad
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
102
Assets/Mirror/Profiler/Table/TreeElementUtility.cs
Normal file
102
Assets/Mirror/Profiler/Table/TreeElementUtility.cs
Normal 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");
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
11
Assets/Mirror/Profiler/Table/TreeElementUtility.cs.meta
Normal file
11
Assets/Mirror/Profiler/Table/TreeElementUtility.cs.meta
Normal file
@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: ac69cc547e5d04cd999f18d3002e9208
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
114
Assets/Mirror/Profiler/Table/TreeModel.cs
Normal file
114
Assets/Mirror/Profiler/Table/TreeModel.cs
Normal 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();
|
||||
}
|
||||
}
|
||||
|
||||
}
|
11
Assets/Mirror/Profiler/Table/TreeModel.cs.meta
Normal file
11
Assets/Mirror/Profiler/Table/TreeModel.cs.meta
Normal file
@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: d5dcc828380a0402b9d0e40a47005d6b
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
152
Assets/Mirror/Profiler/Table/TreeViewWithTreeModel.cs
Normal file
152
Assets/Mirror/Profiler/Table/TreeViewWithTreeModel.cs
Normal 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);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
11
Assets/Mirror/Profiler/Table/TreeViewWithTreeModel.cs.meta
Normal file
11
Assets/Mirror/Profiler/Table/TreeViewWithTreeModel.cs.meta
Normal file
@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 4ddf5bd6d1c6d4724b7368e8b4814479
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
Loading…
Reference in New Issue
Block a user