diff --git a/com.unity.multiplayer.mlapi/Runtime/Core/NetworkManager.cs b/com.unity.multiplayer.mlapi/Runtime/Core/NetworkManager.cs index 7b0ddf8239..6aa2affa9f 100644 --- a/com.unity.multiplayer.mlapi/Runtime/Core/NetworkManager.cs +++ b/com.unity.multiplayer.mlapi/Runtime/Core/NetworkManager.cs @@ -29,7 +29,7 @@ namespace MLAPI /// The main component of the library /// [AddComponentMenu("MLAPI/NetworkManager", -100)] - public class NetworkManager : MonoBehaviour, INetworkUpdateSystem + public class NetworkManager : MonoBehaviour, INetworkUpdateSystem, IProfilableTransportProvider { [Browsable(false)] [EditorBrowsable(EditorBrowsableState.Never)] @@ -56,10 +56,6 @@ public class NetworkManager : MonoBehaviour, INetworkUpdateSystem internal RpcQueueContainer RpcQueueContainer { get; private set; } internal NetworkTickSystem NetworkTickSystem { get; private set; } - public delegate void PerformanceDataEventHandler(PerformanceTickData profilerData); - - public static event PerformanceDataEventHandler OnPerformanceDataEvent; - /// /// A synchronized time, represents the time in seconds since the server application started. Is replicated across all clients /// @@ -386,6 +382,8 @@ private void Init(bool server) NetworkConfig.NetworkTransport.ResetChannelCache(); NetworkConfig.NetworkTransport.Init(); + + ProfilerNotifier.Initialize(this); } /// @@ -1483,34 +1481,17 @@ internal void HandleApproval(ulong clientId, bool createPlayerObject, ulong? pla private void ProfilerBeginTick() { - PerformanceDataManager.BeginNewTick(); - if (NetworkConfig.NetworkTransport is ITransportProfilerData profileTransport) - { - profileTransport.BeginNewTick(); - } + ProfilerNotifier.ProfilerBeginTick(); } private void NotifyProfilerListeners() { - var data = PerformanceDataManager.GetData(); - var eventHandler = OnPerformanceDataEvent; - if (eventHandler != null) - { - if (data != null) - { - if (NetworkConfig.NetworkTransport is ITransportProfilerData profileTransport) - { - var transportProfilerData = profileTransport.GetTransportProfilerData(); - PerformanceDataManager.AddTransportData(transportProfilerData); - } + ProfilerNotifier.NotifyProfilerListeners(); + } - eventHandler.Invoke(data); - } - else - { - NetworkLog.LogWarning($"No data available. Did you forget to call {nameof(PerformanceDataManager)}.{nameof(PerformanceDataManager.BeginNewTick)}() first?"); - } - } + public ITransportProfilerData Transport + { + get { return NetworkConfig.NetworkTransport as ITransportProfilerData; } } } } diff --git a/com.unity.multiplayer.mlapi/Runtime/Profiling/IProfilableTransportProvider.cs b/com.unity.multiplayer.mlapi/Runtime/Profiling/IProfilableTransportProvider.cs new file mode 100644 index 0000000000..1776368e1d --- /dev/null +++ b/com.unity.multiplayer.mlapi/Runtime/Profiling/IProfilableTransportProvider.cs @@ -0,0 +1,7 @@ +namespace MLAPI.Profiling +{ + public interface IProfilableTransportProvider + { + ITransportProfilerData Transport { get; } + } +} diff --git a/com.unity.multiplayer.mlapi/Runtime/Profiling/IProfilableTransportProvider.cs.meta b/com.unity.multiplayer.mlapi/Runtime/Profiling/IProfilableTransportProvider.cs.meta new file mode 100644 index 0000000000..dbcd7976d2 --- /dev/null +++ b/com.unity.multiplayer.mlapi/Runtime/Profiling/IProfilableTransportProvider.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: 02cc48a5664f497ca5c792b68cb15ba0 +timeCreated: 1616427501 \ No newline at end of file diff --git a/com.unity.multiplayer.mlapi/Runtime/Profiling/PerformanceDataManager.cs b/com.unity.multiplayer.mlapi/Runtime/Profiling/PerformanceDataManager.cs index 2c9442e895..c94c19c962 100644 --- a/com.unity.multiplayer.mlapi/Runtime/Profiling/PerformanceDataManager.cs +++ b/com.unity.multiplayer.mlapi/Runtime/Profiling/PerformanceDataManager.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using UnityEngine; namespace MLAPI.Profiling { @@ -11,8 +12,9 @@ internal static class PerformanceDataManager internal static void BeginNewTick() { s_TickId = Math.Max(s_TickId, 0); + s_TickId++; s_ProfilerData.Reset(); - s_ProfilerData.TickId = s_TickId++; + s_ProfilerData.TickId = s_TickId; } internal static void Increment(string fieldName, int count = 1) diff --git a/com.unity.multiplayer.mlapi/Runtime/Profiling/ProfilerCountersInfo.cs b/com.unity.multiplayer.mlapi/Runtime/Profiling/ProfilerCountersInfo.cs index 8a9fd576bf..e4977db766 100644 --- a/com.unity.multiplayer.mlapi/Runtime/Profiling/ProfilerCountersInfo.cs +++ b/com.unity.multiplayer.mlapi/Runtime/Profiling/ProfilerCountersInfo.cs @@ -83,7 +83,13 @@ internal static class ProfilerCountersInfo private static void RegisterMLAPIPerformanceEvent() { InitializeCounters(); - NetworkManager.OnPerformanceDataEvent += OnPerformanceTickData; + ProfilerNotifier.OnPerformanceDataEvent += OnPerformanceTickData; + ProfilerNotifier.OnNoTickDataEvent += OnNoTickData; + } + + private static void OnNoTickData() + { + Debug.LogWarning("There was a profiler event that was not captured in a tick"); } private static void InitializeCounters() diff --git a/com.unity.multiplayer.mlapi/Runtime/Profiling/ProfilerNotifier.cs b/com.unity.multiplayer.mlapi/Runtime/Profiling/ProfilerNotifier.cs new file mode 100644 index 0000000000..4a20e0083b --- /dev/null +++ b/com.unity.multiplayer.mlapi/Runtime/Profiling/ProfilerNotifier.cs @@ -0,0 +1,76 @@ +using System; +using MLAPI.Logging; + +namespace MLAPI.Profiling +{ + public static class ProfilerNotifier + { + public delegate void PerformanceDataEventHandler(PerformanceTickData profilerData); + + public static event PerformanceDataEventHandler OnPerformanceDataEvent; + + public delegate void NoTickDataHandler(); + + public static event NoTickDataHandler OnNoTickDataEvent; + + private static IProfilableTransportProvider s_ProfilableTransportProvider; + private static bool s_FailsafeCheck; + + public static void Initialize(IProfilableTransportProvider profilableNetwork) + { + s_ProfilableTransportProvider = profilableNetwork + ?? throw new ArgumentNullException( + $"{nameof(profilableNetwork)} was not set"); + s_FailsafeCheck = false; + } + + public static void ProfilerBeginTick() + { + PerformanceDataManager.BeginNewTick(); + var transport = s_ProfilableTransportProvider.Transport; + transport?.BeginNewTick(); + s_FailsafeCheck = true; + } + + public static void NotifyProfilerListeners() + { + if (!s_FailsafeCheck) + return; + + s_FailsafeCheck = false; + + var data = PerformanceDataManager.GetData(); + var eventHandler = OnPerformanceDataEvent; + if (eventHandler != null) + { + if (data != null) + { + var transport = s_ProfilableTransportProvider.Transport; + if (transport != null) + { + var transportProfilerData = transport.GetTransportProfilerData(); + + PerformanceDataManager.AddTransportData(transportProfilerData); + } + + eventHandler.Invoke(data); + } + else + { + NetworkLog.LogWarning( + "No data available. Did you forget to call PerformanceDataManager.BeginNewTick() first?"); + } + } + } + + public static void Increment(string fieldName, int count = 1) + { + if (!s_FailsafeCheck) + { + OnNoTickDataEvent?.Invoke(); + } + + PerformanceDataManager.Increment(fieldName); + } + } +} diff --git a/com.unity.multiplayer.mlapi/Runtime/Profiling/ProfilerNotifier.cs.meta b/com.unity.multiplayer.mlapi/Runtime/Profiling/ProfilerNotifier.cs.meta new file mode 100644 index 0000000000..015d08aa4b --- /dev/null +++ b/com.unity.multiplayer.mlapi/Runtime/Profiling/ProfilerNotifier.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: 12af6f0ff6554caaab917445ae94d809 +timeCreated: 1616427513 \ No newline at end of file diff --git a/com.unity.multiplayer.mlapi/Tests/Editor/ProfilerTests.cs b/com.unity.multiplayer.mlapi/Tests/Editor/ProfilerTests.cs new file mode 100644 index 0000000000..77e0cee67e --- /dev/null +++ b/com.unity.multiplayer.mlapi/Tests/Editor/ProfilerTests.cs @@ -0,0 +1,202 @@ +using System; +using System.Collections.Generic; +using MLAPI.Profiling; +using NUnit.Framework; +using UnityEditor; + +namespace MLAPI.RuntimeTests +{ + public class TestTransport : ITransportProfilerData + { + internal static class ProfilerConstants + { + public const string TransportTestData = nameof(TransportTestData); + } + + private static readonly ProfilingDataStore TransportProfilerData = new ProfilingDataStore(); + + public void BeginNewTick() + { + TransportProfilerData.Clear(); + } + + public IReadOnlyDictionary GetTransportProfilerData() + { + return TransportProfilerData.GetReadonly(); + } + + public void Send(string testMessage) + { + PerformanceDataManager.Increment(ProfilerConstants.TransportTestData); + } + } + + public class TestProfiler : IProfilableTransportProvider + { + internal static class ProfilerConstants + { + public const string NetworkTestData = nameof(NetworkTestData); + } + + private TestTransport m_Transport; + private bool m_HasSentAnyData; + + public ITransportProfilerData Transport => m_Transport; + public bool HasSentAnyData => m_HasSentAnyData; + + public void Initialize(bool useNullTransport) + { + m_Transport = useNullTransport ? null : new TestTransport(); + m_HasSentAnyData = false; + ProfilerNotifier.Initialize(this); + } + + public static void ProfilerBeginTick() + { + ProfilerNotifier.ProfilerBeginTick(); + } + + public static void NotifyProfilerListeners() + { + ProfilerNotifier.NotifyProfilerListeners(); + } + + public void Send() + { + ProfilerNotifier.Increment(ProfilerConstants.NetworkTestData); + m_Transport?.Send("testMessage"); + m_HasSentAnyData = true; + } + } + + public class NoTickDataException : Exception + { + } + + public class ProfilerTests + { + private static TestProfiler SetupTestProfiler(bool useNullTransport) + { + var testProfiler = new TestProfiler(); + testProfiler.Initialize(useNullTransport); + return testProfiler; + } + + private static void RegisterStaticAsserts(bool useNullTransport) + { + ProfilerNotifier.OnNoTickDataEvent += RaiseExceptionNoTickDataEvent; + if (useNullTransport) + { + ProfilerNotifier.OnPerformanceDataEvent += AssertNetworkDataExists; + } + else + { + ProfilerNotifier.OnPerformanceDataEvent += AssertNetworkAndTransportDataExists; + } + } + + private static void AssertNetworkAndTransportDataExists(PerformanceTickData profilerData) + { + Assert.IsTrue(profilerData.HasData(TestProfiler.ProfilerConstants.NetworkTestData)); + Assert.IsTrue(profilerData.HasData(TestTransport.ProfilerConstants.TransportTestData)); + } + + private static void AssertNetworkDataExists(PerformanceTickData profilerData) + { + Assert.IsTrue(profilerData.HasData(TestProfiler.ProfilerConstants.NetworkTestData)); + Assert.IsFalse(profilerData.HasData(TestTransport.ProfilerConstants.TransportTestData)); + } + + private static void RaiseExceptionNoTickDataEvent() + { + throw new NoTickDataException(); + } + + [TearDown] + public void TearDown() + { + ProfilerNotifier.OnPerformanceDataEvent -= AssertNetworkAndTransportDataExists; + ProfilerNotifier.OnPerformanceDataEvent -= AssertNetworkDataExists; + ProfilerNotifier.OnNoTickDataEvent -= RaiseExceptionNoTickDataEvent; + } + + [Test] + public void TestSentNoData() + { + const bool useNullTransport = true; + TestProfiler testProfiler = SetupTestProfiler(useNullTransport); + + TestProfiler.ProfilerBeginTick(); + TestProfiler.NotifyProfilerListeners(); + + Assert.False(testProfiler.HasSentAnyData); + } + + [Test] + public void TestNormalRegisterAndNotifyFlow_NullTransport() + { + const bool useNullTransport = true; + TestProfiler testProfiler = SetupTestProfiler(useNullTransport); + + RegisterStaticAsserts(useNullTransport); + + TestProfiler.ProfilerBeginTick(); + testProfiler.Send(); + TestProfiler.NotifyProfilerListeners(); + + Assert.IsTrue(testProfiler.HasSentAnyData); + } + + [Test] + public void TestNormalRegisterAndNotifyFlow() + { + const bool useNullTransport = false; + TestProfiler testProfiler = SetupTestProfiler(useNullTransport); + + RegisterStaticAsserts(useNullTransport); + + TestProfiler.ProfilerBeginTick(); + testProfiler.Send(); + TestProfiler.NotifyProfilerListeners(); + + Assert.IsTrue(testProfiler.HasSentAnyData); + } + + [Test] + public void TestDroppedRegisterAndNotifyFlow() + { + const bool useNullTransport = false; + TestProfiler testProfiler = SetupTestProfiler(useNullTransport); + + RegisterStaticAsserts(useNullTransport); + + TestProfiler.ProfilerBeginTick(); + testProfiler.Send(); + TestProfiler.NotifyProfilerListeners(); + + // Capturing data after notifying listeners is bad + Assert.Catch(() => + { + testProfiler.Send(); + }); + + Assert.IsTrue(testProfiler.HasSentAnyData); + } + + [Test] + public void TestProperMatchRegisterAndNotifyFlow() + { + const bool useNullTransport = false; + TestProfiler testProfiler = SetupTestProfiler(useNullTransport); + + RegisterStaticAsserts(useNullTransport); + + TestProfiler.NotifyProfilerListeners(); + TestProfiler.ProfilerBeginTick(); + testProfiler.Send(); + TestProfiler.NotifyProfilerListeners(); + + Assert.IsTrue(testProfiler.HasSentAnyData); + } + } +} diff --git a/com.unity.multiplayer.mlapi/Tests/Editor/ProfilerTests.cs.meta b/com.unity.multiplayer.mlapi/Tests/Editor/ProfilerTests.cs.meta new file mode 100644 index 0000000000..ff71e12578 --- /dev/null +++ b/com.unity.multiplayer.mlapi/Tests/Editor/ProfilerTests.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 69750fc1f921f490fabad933b42ff9c5 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: