diff --git a/Source/Editor/Windows/Profiler/CPU.cs b/Source/Editor/Windows/Profiler/CPU.cs
index 36fbf21f4..fd4061276 100644
--- a/Source/Editor/Windows/Profiler/CPU.cs
+++ b/Source/Editor/Windows/Profiler/CPU.cs
@@ -429,21 +429,8 @@ namespace FlaxEditor.Windows.Profiler
private void UpdateTable(ref ViewRange viewRange)
{
_table.IsLayoutLocked = true;
- int idx = 0;
- while (_table.Children.Count > idx)
- {
- var child = _table.Children[idx];
- if (child is Row row)
- {
- _tableRowsCache.Add(row);
- child.Parent = null;
- }
- else
- {
- idx++;
- }
- }
+ RecycleTableRows(_table, _tableRowsCache);
UpdateTableInner(ref viewRange);
_table.UnlockChildrenRecursive();
diff --git a/Source/Editor/Windows/Profiler/GPU.cs b/Source/Editor/Windows/Profiler/GPU.cs
index 2cf75a9aa..4ed18691a 100644
--- a/Source/Editor/Windows/Profiler/GPU.cs
+++ b/Source/Editor/Windows/Profiler/GPU.cs
@@ -298,21 +298,7 @@ namespace FlaxEditor.Windows.Profiler
private void UpdateTable()
{
_table.IsLayoutLocked = true;
- int idx = 0;
- while (_table.Children.Count > idx)
- {
- var child = _table.Children[idx];
- if (child is Row row)
- {
- _tableRowsCache.Add(row);
- child.Parent = null;
- }
- else
- {
- idx++;
- }
- }
- _table.LockChildrenRecursive();
+ RecycleTableRows(_table, _tableRowsCache);
UpdateTableInner();
diff --git a/Source/Editor/Windows/Profiler/Network.cs b/Source/Editor/Windows/Profiler/Network.cs
index f5bbb6471..dbee0e8e7 100644
--- a/Source/Editor/Windows/Profiler/Network.cs
+++ b/Source/Editor/Windows/Profiler/Network.cs
@@ -1,8 +1,32 @@
// Copyright (c) 2012-2023 Wojciech Figat. All rights reserved.
+using System;
+using System.Collections.Generic;
+using FlaxEditor.GUI;
using FlaxEngine;
using FlaxEngine.GUI;
+namespace FlaxEngine
+{
+ partial class ProfilingTools
+ {
+ partial struct NetworkEventStat
+ {
+ ///
+ /// Gets the event name.
+ ///
+ public unsafe string Name
+ {
+ get
+ {
+ fixed (byte* name = Name0)
+ return System.Runtime.InteropServices.Marshal.PtrToStringAnsi(new IntPtr(name));
+ }
+ }
+ }
+ }
+}
+
namespace FlaxEditor.Windows.Profiler
{
///
@@ -13,6 +37,10 @@ namespace FlaxEditor.Windows.Profiler
{
private readonly SingleChart _dataSentChart;
private readonly SingleChart _dataReceivedChart;
+ private readonly Table _tableRpc;
+ private readonly Table _tableRep;
+ private SamplesBuffer _events;
+ private List _tableRowsCache;
private FlaxEngine.Networking.NetworkDriverStats _prevStats;
public Network()
@@ -48,11 +76,10 @@ namespace FlaxEditor.Windows.Profiler
Parent = layout,
};
_dataReceivedChart.SelectedSampleChanged += OnSelectedSampleChanged;
- }
- private static string FormatSampleBytes(float v)
- {
- return Utilities.Utils.FormatBytesCount((ulong)v);
+ // Tables
+ _tableRpc = InitTable(layout, "RPC Name");
+ _tableRep = InitTable(layout, "Replication Name");
}
///
@@ -60,11 +87,13 @@ namespace FlaxEditor.Windows.Profiler
{
_dataSentChart.Clear();
_dataReceivedChart.Clear();
+ _events?.Clear();
}
///
public override void Update(ref SharedUpdateData sharedData)
{
+ // Gather peer stats
var peers = FlaxEngine.Networking.NetworkPeer.Peers;
var stats = new FlaxEngine.Networking.NetworkDriverStats();
foreach (var peer in peers)
@@ -76,6 +105,12 @@ namespace FlaxEditor.Windows.Profiler
_dataSentChart.AddSample(Mathf.Max((long)stats.TotalDataSent - (long)_prevStats.TotalDataSent, 0));
_dataReceivedChart.AddSample(Mathf.Max((long)stats.TotalDataReceived - (long)_prevStats.TotalDataReceived, 0));
_prevStats = stats;
+
+ // Gather network events
+ var events = ProfilingTools.EventsNetwork;
+ if (_events == null)
+ _events = new SamplesBuffer();
+ _events.Add(events);
}
///
@@ -83,6 +118,159 @@ namespace FlaxEditor.Windows.Profiler
{
_dataSentChart.SelectedSampleIndex = selectedFrame;
_dataReceivedChart.SelectedSampleIndex = selectedFrame;
+
+ // Update events tables
+ if (_events != null)
+ {
+ if (_tableRowsCache == null)
+ _tableRowsCache = new List();
+ _tableRpc.IsLayoutLocked = true;
+ _tableRep.IsLayoutLocked = true;
+ RecycleTableRows(_tableRpc, _tableRowsCache);
+ RecycleTableRows(_tableRep, _tableRowsCache);
+
+ var events = _events.Get(selectedFrame);
+ var rowCount = Int2.Zero;
+ if (events != null && events.Length != 0)
+ {
+ var rowColor2 = Style.Current.Background * 1.4f;
+ for (int i = 0; i < events.Length; i++)
+ {
+ var e = events[i];
+ var name = e.Name;
+ var isRpc = name.Contains("::", StringComparison.Ordinal);
+
+ Row row;
+ if (_tableRowsCache.Count != 0)
+ {
+ var last = _tableRowsCache.Count - 1;
+ row = _tableRowsCache[last];
+ _tableRowsCache.RemoveAt(last);
+ }
+ else
+ {
+ row = new Row
+ {
+ Values = new object[5],
+ };
+ }
+ {
+ // Name
+ row.Values[0] = name;
+
+ // Count
+ row.Values[1] = (int)e.Count;
+
+ // Data Size
+ row.Values[2] = (int)e.DataSize;
+
+ // Message Size
+ row.Values[3] = (int)e.MessageSize;
+
+ // Receivers
+ row.Values[4] = (float)e.Receivers / (float)e.Count;
+ }
+
+ var table = isRpc ? _tableRpc : _tableRep;
+ row.Width = table.Width;
+ row.BackgroundColor = rowCount[isRpc ? 0 : 1] % 2 == 0 ? rowColor2 : Color.Transparent;
+ row.Parent = table;
+ if (isRpc)
+ rowCount.X++;
+ else
+ rowCount.Y++;
+ }
+ }
+
+ _tableRpc.Visible = rowCount.X != 0;
+ _tableRep.Visible = rowCount.Y != 0;
+ _tableRpc.Children.Sort(SortRows);
+ _tableRep.Children.Sort(SortRows);
+
+ _tableRpc.UnlockChildrenRecursive();
+ _tableRpc.PerformLayout();
+ _tableRep.UnlockChildrenRecursive();
+ _tableRep.PerformLayout();
+ }
+ }
+
+ ///
+ public override void OnDestroy()
+ {
+ _tableRowsCache?.Clear();
+
+ base.OnDestroy();
+ }
+
+ private static Table InitTable(ContainerControl parent, string name)
+ {
+ var headerColor = Style.Current.LightBackground;
+ var table = new Table
+ {
+ Columns = new[]
+ {
+ new ColumnDefinition
+ {
+ UseExpandCollapseMode = true,
+ CellAlignment = TextAlignment.Near,
+ Title = name,
+ TitleBackgroundColor = headerColor,
+ },
+ new ColumnDefinition
+ {
+ Title = "Count",
+ TitleBackgroundColor = headerColor,
+ },
+ new ColumnDefinition
+ {
+ Title = "Data Size",
+ TitleBackgroundColor = headerColor,
+ FormatValue = FormatCellBytes,
+ },
+ new ColumnDefinition
+ {
+ Title = "Message Size",
+ TitleBackgroundColor = headerColor,
+ FormatValue = FormatCellBytes,
+ },
+ new ColumnDefinition
+ {
+ Title = "Receivers",
+ TitleBackgroundColor = headerColor,
+ },
+ },
+ Splits = new[]
+ {
+ 0.40f,
+ 0.15f,
+ 0.15f,
+ 0.15f,
+ 0.15f,
+ },
+ Parent = parent,
+ };
+ return table;
+ }
+
+ private static string FormatSampleBytes(float v)
+ {
+ return Utilities.Utils.FormatBytesCount((ulong)v);
+ }
+
+ private static string FormatCellBytes(object x)
+ {
+ return Utilities.Utils.FormatBytesCount((int)x);
+ }
+
+ private static int SortRows(Control x, Control y)
+ {
+ if (x is Row xRow && y is Row yRow)
+ {
+ var xDataSize = (int)xRow.Values[2];
+ var yDataSize = (int)yRow.Values[2];
+ return yDataSize - xDataSize;
+ }
+ return 0;
}
}
}
diff --git a/Source/Editor/Windows/Profiler/ProfilerMode.cs b/Source/Editor/Windows/Profiler/ProfilerMode.cs
index 0cc1a39ee..b8d2f0c4c 100644
--- a/Source/Editor/Windows/Profiler/ProfilerMode.cs
+++ b/Source/Editor/Windows/Profiler/ProfilerMode.cs
@@ -1,6 +1,8 @@
// Copyright (c) 2012-2023 Wojciech Figat. All rights reserved.
using System;
+using System.Collections.Generic;
+using FlaxEditor.GUI;
using FlaxEditor.GUI.Tabs;
using FlaxEngine;
@@ -135,5 +137,28 @@ namespace FlaxEditor.Windows.Profiler
{
SelectedSampleChanged?.Invoke(frameIndex);
}
+
+ ///
+ /// Recycles all table rows to be reused.
+ ///
+ /// The table.
+ /// The output cache.
+ protected static void RecycleTableRows(Table table, List rowsCache)
+ {
+ int idx = 0;
+ while (table.Children.Count > idx)
+ {
+ var child = table.Children[idx];
+ if (child is Row row)
+ {
+ rowsCache.Add(row);
+ child.Parent = null;
+ }
+ else
+ {
+ idx++;
+ }
+ }
+ }
}
}
diff --git a/Source/Editor/Windows/Profiler/SamplesBuffer.cs b/Source/Editor/Windows/Profiler/SamplesBuffer.cs
index abc99cfd5..999156dca 100644
--- a/Source/Editor/Windows/Profiler/SamplesBuffer.cs
+++ b/Source/Editor/Windows/Profiler/SamplesBuffer.cs
@@ -49,6 +49,8 @@ namespace FlaxEditor.Windows.Profiler
/// The sample value
public T Get(int index)
{
+ if (index >= _data.Length || _data.Length == 0)
+ return default;
return index == -1 ? _data[_count - 1] : _data[index];
}
diff --git a/Source/Engine/Networking/NetworkInternal.h b/Source/Engine/Networking/NetworkInternal.h
index 521e8a7a2..9484337d1 100644
--- a/Source/Engine/Networking/NetworkInternal.h
+++ b/Source/Engine/Networking/NetworkInternal.h
@@ -3,6 +3,9 @@
#pragma once
#include "Types.h"
+#if COMPILE_WITH_PROFILER
+#include "Engine/Core/Collections/Dictionary.h"
+#endif
enum class NetworkMessageIDs : uint8
{
@@ -35,4 +38,22 @@ public:
static void OnNetworkMessageObjectDespawn(NetworkEvent& event, NetworkClient* client, NetworkPeer* peer);
static void OnNetworkMessageObjectRole(NetworkEvent& event, NetworkClient* client, NetworkPeer* peer);
static void OnNetworkMessageObjectRpc(NetworkEvent& event, NetworkClient* client, NetworkPeer* peer);
+
+#if COMPILE_WITH_PROFILER
+
+ struct ProfilerEvent
+ {
+ uint16 Count = 0;
+ uint16 DataSize = 0;
+ uint16 MessageSize = 0;
+ uint16 Receivers = 0;
+ };
+
+ ///
+ /// Enables network usage profiling tools. Captures network objects replication and RPCs send statistics.
+ ///
+ static bool EnableProfiling;
+
+ static Dictionary, ProfilerEvent> ProfilerEvents;
+#endif
};
diff --git a/Source/Engine/Networking/NetworkReplicator.cpp b/Source/Engine/Networking/NetworkReplicator.cpp
index 8133b6cf2..316ac4ae7 100644
--- a/Source/Engine/Networking/NetworkReplicator.cpp
+++ b/Source/Engine/Networking/NetworkReplicator.cpp
@@ -40,6 +40,11 @@ bool NetworkReplicator::EnableLog = false;
#define NETWORK_REPLICATOR_LOG(messageType, format, ...)
#endif
+#if COMPILE_WITH_PROFILER
+bool NetworkInternal::EnableProfiling = false;
+Dictionary, NetworkInternal::ProfilerEvent> NetworkInternal::ProfilerEvents;
+#endif
+
PACK_STRUCT(struct NetworkMessageObjectReplicate
{
NetworkMessageIDs ID = NetworkMessageIDs::ObjectReplicate;
@@ -1806,6 +1811,7 @@ void NetworkInternal::NetworkReplicatorUpdate()
NetworkMessage msg = peer->BeginSendMessage();
msg.WriteStructure(msgData);
msg.WriteBytes(stream->GetBuffer(), msgDataSize);
+ uint32 dataSize = msgDataSize, messageSize = msg.Length;
if (isClient)
peer->EndSendMessage(NetworkChannelType::Unreliable, msg);
else
@@ -1824,6 +1830,8 @@ void NetworkInternal::NetworkReplicatorUpdate()
msg = peer->BeginSendMessage();
msg.WriteStructure(msgDataPart);
msg.WriteBytes(stream->GetBuffer() + msgDataPart.PartStart, msgDataPart.PartSize);
+ messageSize += msg.Length;
+ dataSize += msgDataPart.PartSize;
dataStart += msgDataPart.PartSize;
if (isClient)
peer->EndSendMessage(NetworkChannelType::Unreliable, msg);
@@ -1832,7 +1840,18 @@ void NetworkInternal::NetworkReplicatorUpdate()
}
ASSERT_LOW_LAYER(dataStart == size);
- // TODO: stats for bytes send per object type
+#if COMPILE_WITH_PROFILER
+ // Network stats recording
+ if (EnableProfiling)
+ {
+ const Pair name(obj->GetTypeHandle(), StringAnsiView::Empty);
+ auto& profileEvent = ProfilerEvents[name];
+ profileEvent.Count++;
+ profileEvent.DataSize += dataSize;
+ profileEvent.MessageSize += messageSize;
+ profileEvent.Receivers += isClient ? 1 : CachedTargets.Count();
+ }
+#endif
}
}
@@ -1873,6 +1892,7 @@ void NetworkInternal::NetworkReplicatorUpdate()
NetworkMessage msg = peer->BeginSendMessage();
msg.WriteStructure(msgData);
msg.WriteBytes(e.ArgsData.Get(), e.ArgsData.Length());
+ uint32 dataSize = e.ArgsData.Length(), messageSize = msg.Length, receivers = 0;
NetworkChannelType channel = (NetworkChannelType)e.Info.Channel;
if (e.Info.Server && isClient)
{
@@ -1882,13 +1902,27 @@ void NetworkInternal::NetworkReplicatorUpdate()
NETWORK_REPLICATOR_LOG(Error, "[NetworkReplicator] Server RPC '{}::{}' called with non-empty list of targets is not supported (only server will receive it)", e.Name.First.ToString(), e.Name.Second.ToString());
#endif
peer->EndSendMessage(channel, msg);
+ receivers = 1;
}
else if (e.Info.Client && (isServer || isHost))
{
// Server -> Client(s)
BuildCachedTargets(NetworkManager::Clients, item.TargetClientIds, e.Targets, NetworkManager::LocalClientId);
peer->EndSendMessage(channel, msg, CachedTargets);
+ receivers = CachedTargets.Count();
}
+
+#if COMPILE_WITH_PROFILER
+ // Network stats recording
+ if (EnableProfiling && receivers)
+ {
+ auto& profileEvent = ProfilerEvents[e.Name];
+ profileEvent.Count++;
+ profileEvent.DataSize += dataSize;
+ profileEvent.MessageSize += messageSize;
+ profileEvent.Receivers += receivers;
+ }
+#endif
}
RpcQueue.Clear();
}
diff --git a/Source/Engine/Profiler/ProfilingTools.cpp b/Source/Engine/Profiler/ProfilingTools.cpp
index 4c6a9e19e..61c0e2c33 100644
--- a/Source/Engine/Profiler/ProfilingTools.cpp
+++ b/Source/Engine/Profiler/ProfilingTools.cpp
@@ -3,14 +3,17 @@
#if COMPILE_WITH_PROFILER
#include "ProfilingTools.h"
+#include "Engine/Core/Types/Pair.h"
#include "Engine/Engine/Engine.h"
#include "Engine/Engine/Time.h"
#include "Engine/Engine/EngineService.h"
#include "Engine/Graphics/GPUDevice.h"
+#include "Engine/Networking/NetworkInternal.h"
ProfilingTools::MainStats ProfilingTools::Stats;
Array> ProfilingTools::EventsCPU;
Array ProfilingTools::EventsGPU;
+Array ProfilingTools::EventsNetwork;
class ProfilingToolsService : public EngineService
{
@@ -120,6 +123,40 @@ void ProfilingToolsService::Update()
frame.Extract(ProfilingTools::EventsGPU);
}
+ // Get the last events from networking runtime
+ {
+ auto& networkEvents = ProfilingTools::EventsNetwork;
+ networkEvents.Resize(NetworkInternal::ProfilerEvents.Count());
+ int32 i = 0;
+ for (const auto& e : NetworkInternal::ProfilerEvents)
+ {
+ const auto& src = e.Value;
+ auto& dst = networkEvents[i++];
+ dst.Count = src.Count;
+ dst.DataSize = src.DataSize;
+ dst.MessageSize = src.MessageSize;
+ dst.Receivers = src.Receivers;
+ const StringAnsiView& typeName = e.Key.First.GetType().Fullname;
+ uint64 len = Math::Min(typeName.Length(), ARRAY_COUNT(dst.Name) - 10);
+ Platform::MemoryCopy(dst.Name, typeName.Get(), len);
+ const StringAnsiView& name = e.Key.Second;
+ if (name.HasChars())
+ {
+ uint64 pos = len;
+ dst.Name[pos++] = ':';
+ dst.Name[pos++] = ':';
+ len = Math::Min(name.Length(), ARRAY_COUNT(dst.Name) - pos - 1);
+ Platform::MemoryCopy(dst.Name + pos, name.Get(), len);
+ dst.Name[pos + len] = 0;
+ }
+ else
+ {
+ dst.Name[len] = 0;
+ }
+ }
+ NetworkInternal::ProfilerEvents.Clear();
+ }
+
#if 0
// Print CPU events to the log
{
@@ -173,6 +210,7 @@ void ProfilingToolsService::Dispose()
ProfilingTools::EventsCPU.Clear();
ProfilingTools::EventsCPU.SetCapacity(0);
ProfilingTools::EventsGPU.SetCapacity(0);
+ ProfilingTools::EventsNetwork.SetCapacity(0);
}
bool ProfilingTools::GetEnabled()
@@ -184,6 +222,7 @@ void ProfilingTools::SetEnabled(bool enabled)
{
ProfilerCPU::Enabled = enabled;
ProfilerGPU::Enabled = enabled;
+ NetworkInternal::EnableProfiling = enabled;
}
#endif
diff --git a/Source/Engine/Profiler/ProfilingTools.h b/Source/Engine/Profiler/ProfilingTools.h
index af9b79d80..f4039472f 100644
--- a/Source/Engine/Profiler/ProfilingTools.h
+++ b/Source/Engine/Profiler/ProfilingTools.h
@@ -105,6 +105,24 @@ public:
API_FIELD() Array Events;
};
+ ///
+ /// The network stat.
+ ///
+ API_STRUCT(NoDefault) struct NetworkEventStat
+ {
+ DECLARE_SCRIPTING_TYPE_MINIMAL(NetworkEventStat);
+
+ // Amount of occurrences.
+ API_FIELD() uint16 Count;
+ // Transferred data size (in bytes).
+ API_FIELD() uint16 DataSize;
+ // Transferred message (data+header) size (in bytes).
+ API_FIELD() uint16 MessageSize;
+ // Amount of peers that will receive this message.
+ API_FIELD() uint16 Receivers;
+ API_FIELD(Private, NoArray) byte Name[120];
+ };
+
public:
///
/// Controls the engine profiler (CPU, GPU, etc.) usage.
@@ -130,6 +148,11 @@ public:
/// The GPU rendering profiler events.
///
API_FIELD(ReadOnly) static Array EventsGPU;
+
+ ///
+ /// The networking profiler events.
+ ///
+ API_FIELD(ReadOnly) static Array EventsNetwork;
};
#endif
diff --git a/Source/Tools/Flax.Build/Bindings/BindingsGenerator.CSharp.cs b/Source/Tools/Flax.Build/Bindings/BindingsGenerator.CSharp.cs
index 3685cb271..00ab0d4b2 100644
--- a/Source/Tools/Flax.Build/Bindings/BindingsGenerator.CSharp.cs
+++ b/Source/Tools/Flax.Build/Bindings/BindingsGenerator.CSharp.cs
@@ -1767,6 +1767,10 @@ namespace Flax.Build.Bindings
// char's are not blittable, store as short instead
contents.Append($"fixed short {fieldInfo.Name}0[{fieldInfo.Type.ArraySize}]; // {managedType}*").AppendLine();
}
+ else if (managedType == "byte")
+ {
+ contents.Append($"fixed byte {fieldInfo.Name}0[{fieldInfo.Type.ArraySize}]; // {managedType}*").AppendLine();
+ }
else
#endif
{