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 {