Skip to content

Commit 3ad516f

Browse files
ShadauxCatandrews-unity
authored andcommitted
fix: Fixed a few memory leak cases when shutting down NetworkManager during Incoming Message Queue processing (#1323)
* fix: Fixed memory leak on shutdown * fix: Fixed a few memory leak cases when shutting down NetworkManager during Incoming Message Queue processing * standards check * Fixed test not cleaning up quite correctly. * -Reworked the way shutdown is handled so it can't be processed while handling incoming messages. -Added tests for both ways to shut down to make sure they both work correctly -Fixed an issue in the snapshot system that was causing the new tests to fail * standards * Fixed broken test, slight naming change to be more accurate.
1 parent 7f60010 commit 3ad516f

File tree

10 files changed

+302
-16
lines changed

10 files changed

+302
-16
lines changed

com.unity.netcode.gameobjects/Runtime/Core/NetworkManager.cs

Lines changed: 41 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,9 @@ public NetworkPrefabHandler PrefabHandler
7373
}
7474
}
7575

76+
private bool m_ShuttingDown;
77+
private bool m_StopProcessingMessages;
78+
7679
private class NetworkManagerHooks : INetworkHooks
7780
{
7881
private NetworkManager m_NetworkManager;
@@ -116,7 +119,7 @@ public void OnAfterReceiveBatch(ulong senderId, int messageCount, int batchSizeI
116119

117120
public bool OnVerifyCanSend(ulong destinationId, Type messageType, NetworkDelivery delivery)
118121
{
119-
return true;
122+
return !m_NetworkManager.m_StopProcessingMessages;
120123
}
121124

122125
public bool OnVerifyCanReceive(ulong senderId, Type messageType)
@@ -134,7 +137,7 @@ public bool OnVerifyCanReceive(ulong senderId, Type messageType)
134137
return false;
135138
}
136139

137-
return true;
140+
return !m_NetworkManager.m_StopProcessingMessages;
138141
}
139142
}
140143

@@ -976,7 +979,7 @@ private void OnApplicationQuit()
976979
// Note that this gets also called manually by OnSceneUnloaded and OnApplicationQuit
977980
private void OnDestroy()
978981
{
979-
Shutdown();
982+
ShutdownInternal();
980983

981984
UnityEngine.SceneManagement.SceneManager.sceneUnloaded -= OnSceneUnloaded;
982985

@@ -996,13 +999,30 @@ private void DisconnectRemoteClient(ulong clientId)
996999
/// Globally shuts down the library.
9971000
/// Disconnects clients if connected and stops server if running.
9981001
/// </summary>
999-
public void Shutdown()
1002+
/// <param name="discardMessageQueue">
1003+
/// If false, any messages that are currently in the incoming queue will be handled,
1004+
/// and any messages in the outgoing queue will be sent, before the shutdown is processed.
1005+
/// If true, NetworkManager will shut down immediately, and any unprocessed or unsent messages
1006+
/// will be discarded.
1007+
/// </param>
1008+
public void Shutdown(bool discardMessageQueue = false)
10001009
{
10011010
if (NetworkLog.CurrentLogLevel <= LogLevel.Developer)
10021011
{
10031012
NetworkLog.LogInfo(nameof(Shutdown));
10041013
}
10051014

1015+
m_ShuttingDown = true;
1016+
m_StopProcessingMessages = discardMessageQueue;
1017+
}
1018+
1019+
internal void ShutdownInternal()
1020+
{
1021+
if (NetworkLog.CurrentLogLevel <= LogLevel.Developer)
1022+
{
1023+
NetworkLog.LogInfo(nameof(ShutdownInternal));
1024+
}
1025+
10061026
if (IsServer)
10071027
{
10081028
// make sure all messages are flushed before transport disconnect clients
@@ -1079,6 +1099,7 @@ public void Shutdown()
10791099

10801100
if (SpawnManager != null)
10811101
{
1102+
SpawnManager.CleanupAllTriggers();
10821103
SpawnManager.DestroyNonSceneObjects();
10831104
SpawnManager.ServerResetShudownStateForSceneObjects();
10841105

@@ -1167,6 +1188,11 @@ private void OnNetworkPreUpdate()
11671188
return;
11681189
}
11691190

1191+
if (m_ShuttingDown && m_StopProcessingMessages)
1192+
{
1193+
return;
1194+
}
1195+
11701196
// Only update RTT here, server time is updated by time sync messages
11711197
var reset = NetworkTimeSystem.Advance(Time.deltaTime);
11721198
if (reset)
@@ -1183,9 +1209,18 @@ private void OnNetworkPreUpdate()
11831209

11841210
private void OnNetworkPostLateUpdate()
11851211
{
1186-
MessagingSystem.ProcessSendQueues();
1187-
NetworkMetrics.DispatchFrame();
1212+
1213+
if (!m_ShuttingDown || !m_StopProcessingMessages)
1214+
{
1215+
MessagingSystem.ProcessSendQueues();
1216+
NetworkMetrics.DispatchFrame();
1217+
}
11881218
SpawnManager.CleanupStaleTriggers();
1219+
1220+
if (m_ShuttingDown)
1221+
{
1222+
ShutdownInternal();
1223+
}
11891224
}
11901225

11911226
/// <summary>

com.unity.netcode.gameobjects/Runtime/Core/SnapshotSystem.cs

Lines changed: 48 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -417,7 +417,54 @@ internal void ReadIndex(in SnapshotDataMessage message)
417417
}
418418
}
419419

420-
internal void ReadSpawns(in SnapshotDataMessage message)
420+
internal void SpawnObject(SnapshotSpawnCommand spawnCommand, ulong srcClientId)
421+
{
422+
if (m_NetworkManager)
423+
{
424+
NetworkObject networkObject;
425+
if (spawnCommand.ParentNetworkId == spawnCommand.NetworkObjectId)
426+
{
427+
networkObject = m_NetworkManager.SpawnManager.CreateLocalNetworkObject(false,
428+
spawnCommand.GlobalObjectIdHash, spawnCommand.OwnerClientId, null, spawnCommand.ObjectPosition,
429+
spawnCommand.ObjectRotation);
430+
}
431+
else
432+
{
433+
networkObject = m_NetworkManager.SpawnManager.CreateLocalNetworkObject(false,
434+
spawnCommand.GlobalObjectIdHash, spawnCommand.OwnerClientId, spawnCommand.ParentNetworkId, spawnCommand.ObjectPosition,
435+
spawnCommand.ObjectRotation);
436+
}
437+
438+
m_NetworkManager.SpawnManager.SpawnNetworkObjectLocally(networkObject, spawnCommand.NetworkObjectId,
439+
true, spawnCommand.IsPlayerObject, spawnCommand.OwnerClientId, false);
440+
//todo: discuss with tools how to report shared bytes
441+
m_NetworkManager.NetworkMetrics.TrackObjectSpawnReceived(srcClientId, networkObject, 8);
442+
}
443+
else
444+
{
445+
MockSpawnObject(spawnCommand);
446+
}
447+
}
448+
449+
internal void DespawnObject(SnapshotDespawnCommand despawnCommand, ulong srcClientId)
450+
{
451+
if (m_NetworkManager)
452+
{
453+
m_NetworkManager.SpawnManager.SpawnedObjects.TryGetValue(despawnCommand.NetworkObjectId,
454+
out NetworkObject networkObject);
455+
456+
m_NetworkManager.SpawnManager.OnDespawnObject(networkObject, true);
457+
//todo: discuss with tools how to report shared bytes
458+
m_NetworkManager.NetworkMetrics.TrackObjectDestroyReceived(srcClientId, networkObject, 8);
459+
}
460+
else
461+
{
462+
MockDespawnObject(despawnCommand);
463+
}
464+
}
465+
466+
467+
internal void ReadSpawns(in SnapshotDataMessage message, ulong srcClientId)
421468
{
422469
SnapshotSpawnCommand spawnCommand;
423470
SnapshotDespawnCommand despawnCommand;

com.unity.netcode.gameobjects/Runtime/Messaging/MessagingSystem.cs

Lines changed: 20 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -100,7 +100,7 @@ public MessagingSystem(IMessageSender messageSender, object owner, IMessageProvi
100100
}
101101
}
102102

103-
public void Dispose()
103+
public unsafe void Dispose()
104104
{
105105
if (m_Disposed)
106106
{
@@ -113,6 +113,14 @@ public void Dispose()
113113
{
114114
CleanupDisconnectedClient(kvp.Key);
115115
}
116+
117+
for (var queueIndex = 0; queueIndex < m_IncomingMessageQueue.Length; ++queueIndex)
118+
{
119+
// Avoid copies...
120+
ref var item = ref m_IncomingMessageQueue.GetUnsafeList()->ElementAt(queueIndex);
121+
item.Reader.Dispose();
122+
}
123+
116124
m_IncomingMessageQueue.Dispose();
117125
m_Disposed = true;
118126
}
@@ -253,11 +261,15 @@ public void HandleMessage(in MessageHeader header, FastBufferReader reader, ulon
253261

254262
internal unsafe void ProcessIncomingMessageQueue()
255263
{
256-
for (var i = 0; i < m_IncomingMessageQueue.Length; ++i)
264+
for (var index = 0; index < m_IncomingMessageQueue.Length; ++index)
257265
{
258266
// Avoid copies...
259-
ref var item = ref m_IncomingMessageQueue.GetUnsafeList()->ElementAt(i);
267+
ref var item = ref m_IncomingMessageQueue.GetUnsafeList()->ElementAt(index);
260268
HandleMessage(item.Header, item.Reader, item.SenderId, item.Timestamp);
269+
if (m_Disposed)
270+
{
271+
return;
272+
}
261273
}
262274

263275
m_IncomingMessageQueue.Clear();
@@ -461,16 +473,16 @@ internal unsafe void ProcessSendQueues()
461473
try
462474
{
463475
m_MessageSender.Send(clientId, queueItem.NetworkDelivery, queueItem.Writer);
476+
477+
for (var hookIdx = 0; hookIdx < m_Hooks.Count; ++hookIdx)
478+
{
479+
m_Hooks[hookIdx].OnAfterSendBatch(clientId, queueItem.BatchHeader.BatchSize, queueItem.Writer.Length, queueItem.NetworkDelivery);
480+
}
464481
}
465482
finally
466483
{
467484
queueItem.Writer.Dispose();
468485
}
469-
470-
for (var hookIdx = 0; hookIdx < m_Hooks.Count; ++hookIdx)
471-
{
472-
m_Hooks[hookIdx].OnAfterSendBatch(clientId, queueItem.BatchHeader.BatchSize, queueItem.Writer.Length, queueItem.NetworkDelivery);
473-
}
474486
}
475487
sendQueueItem.Clear();
476488
}

com.unity.netcode.gameobjects/Runtime/Spawning/NetworkSpawnManager.cs

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -154,6 +154,24 @@ internal unsafe void CleanupStaleTriggers()
154154
m_Triggers.Remove(staleKeys[i]);
155155
}
156156
}
157+
/// <summary>
158+
/// Cleans up any trigger that's existed for more than a second.
159+
/// These triggers were probably for situations where a request was received after a despawn rather than before a spawn.
160+
/// </summary>
161+
internal void CleanupAllTriggers()
162+
{
163+
foreach (var kvp in m_Triggers)
164+
{
165+
foreach (var data in kvp.Value.TriggerData)
166+
{
167+
data.Reader.Dispose();
168+
}
169+
170+
kvp.Value.TriggerData.Dispose();
171+
}
172+
173+
m_Triggers.Clear();
174+
}
157175

158176
internal void RemoveOwnership(NetworkObject networkObject)
159177
{

com.unity.netcode.gameobjects/Tests/Editor/StartStopTests.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,7 @@ public void TestStartupServerState()
4545
public void TestFlagShutdown()
4646
{
4747
m_NetworkManager.StartServer();
48-
m_NetworkManager.Shutdown();
48+
m_NetworkManager.ShutdownInternal();
4949

5050
Assert.False(m_NetworkManager.IsServer);
5151
Assert.False(m_NetworkManager.IsClient);
Lines changed: 125 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,125 @@
1+
using System.Collections;
2+
using Unity.Netcode;
3+
using Unity.Netcode.RuntimeTests;
4+
using NUnit.Framework;
5+
using TestProject.RuntimeTests.Support;
6+
using UnityEngine;
7+
using UnityEngine.TestTools;
8+
9+
namespace TestProject.RuntimeTests
10+
{
11+
public class NoMemoryLeakOnNetworkManagerShutdownTest
12+
{
13+
private GameObject m_Prefab;
14+
15+
[SetUp]
16+
public void Setup()
17+
{
18+
ShutdownDuringOnNetworkSpawnBehaviour.SpawnCount = 0;
19+
ShutdownDuringOnNetworkSpawnBehaviour.ClientRpcsCalled = 0;
20+
ShutdownDuringOnNetworkSpawnBehaviour.ServerRpcsCalled = 0;
21+
ShutdownDuringOnNetworkSpawnBehaviour.ShutdownImmediately = false;
22+
}
23+
24+
[UnityTearDown]
25+
public IEnumerator Teardown()
26+
{
27+
MultiInstanceHelpers.Destroy();
28+
// Shutdown and clean up both of our NetworkManager instances
29+
if (m_Prefab)
30+
{
31+
Object.Destroy(m_Prefab);
32+
}
33+
yield break;
34+
}
35+
36+
public IEnumerator RunTest()
37+
{
38+
// Must be 1 for this test.
39+
const int numClients = 1;
40+
Assert.True(MultiInstanceHelpers.Create(numClients, out NetworkManager server, out NetworkManager[] clients));
41+
m_Prefab = new GameObject("Object");
42+
m_Prefab.AddComponent<ShutdownDuringOnNetworkSpawnBehaviour>();
43+
var networkObject = m_Prefab.AddComponent<NetworkObject>();
44+
45+
// Make it a prefab
46+
MultiInstanceHelpers.MakeNetworkObjectTestPrefab(networkObject);
47+
48+
var validNetworkPrefab = new NetworkPrefab();
49+
validNetworkPrefab.Prefab = m_Prefab;
50+
server.NetworkConfig.NetworkPrefabs.Add(validNetworkPrefab);
51+
foreach (var client in clients)
52+
{
53+
client.NetworkConfig.NetworkPrefabs.Add(validNetworkPrefab);
54+
}
55+
56+
// Start the instances
57+
if (!MultiInstanceHelpers.Start(false, server, clients))
58+
{
59+
Debug.LogError("Failed to start instances");
60+
Assert.Fail("Failed to start instances");
61+
}
62+
63+
// [Client-Side] Wait for a connection to the server
64+
yield return MultiInstanceHelpers.Run(MultiInstanceHelpers.WaitForClientsConnected(clients, null, 512));
65+
66+
// [Host-Side] Check to make sure all clients are connected
67+
yield return MultiInstanceHelpers.Run(
68+
MultiInstanceHelpers.WaitForClientsConnectedToServer(server, clients.Length, null, 512));
69+
70+
var serverObject = Object.Instantiate(m_Prefab, Vector3.zero, Quaternion.identity);
71+
NetworkObject serverNetworkObject = serverObject.GetComponent<NetworkObject>();
72+
serverNetworkObject.NetworkManagerOwner = server;
73+
serverNetworkObject.Spawn();
74+
75+
// Wait until all objects have spawned.
76+
const int maxFrames = 240;
77+
var doubleCheckTime = Time.realtimeSinceStartup + 5.0f;
78+
while (ShutdownDuringOnNetworkSpawnBehaviour.SpawnCount < clients.Length + 1)
79+
{
80+
if (Time.frameCount > maxFrames)
81+
{
82+
// This is here in the event a platform is running at a higher
83+
// frame rate than expected
84+
if (doubleCheckTime < Time.realtimeSinceStartup)
85+
{
86+
Assert.Fail("Did not successfully call all expected client RPCs");
87+
break;
88+
}
89+
}
90+
var nextFrameNumber = Time.frameCount + 1;
91+
yield return new WaitUntil(() => Time.frameCount >= nextFrameNumber);
92+
}
93+
94+
Assert.AreEqual(ShutdownDuringOnNetworkSpawnBehaviour.SpawnCount, clients.Length + 1);
95+
// Extra frames to catch Native Container memory leak log message
96+
var lastFrameNumber = Time.frameCount + 10;
97+
yield return new WaitUntil(() => Time.frameCount >= lastFrameNumber);
98+
Object.Destroy(serverObject);
99+
}
100+
101+
[UnityTest]
102+
public IEnumerator WhenNetworkManagerShutsDownWhileTriggeredMessagesArePending_MemoryDoesNotLeak()
103+
{
104+
yield return RunTest();
105+
LogAssert.NoUnexpectedReceived();
106+
}
107+
108+
[UnityTest]
109+
public IEnumerator WhenNetworkManagerShutsDownWhileTriggeredMessagesArePending_MessagesAreStillProcessed()
110+
{
111+
yield return RunTest();
112+
Assert.AreEqual(1, ShutdownDuringOnNetworkSpawnBehaviour.ClientRpcsCalled);
113+
Assert.AreEqual(1, ShutdownDuringOnNetworkSpawnBehaviour.ServerRpcsCalled);
114+
}
115+
116+
[UnityTest]
117+
public IEnumerator WhenNetworkManagerShutsDownImmediatelyWhileTriggeredMessagesArePending_MessagesAreNotProcessed()
118+
{
119+
ShutdownDuringOnNetworkSpawnBehaviour.ShutdownImmediately = true;
120+
yield return RunTest();
121+
Assert.AreEqual(0, ShutdownDuringOnNetworkSpawnBehaviour.ClientRpcsCalled);
122+
Assert.AreEqual(0, ShutdownDuringOnNetworkSpawnBehaviour.ServerRpcsCalled);
123+
}
124+
}
125+
}

testproject/Assets/Tests/Runtime/NoMemoryLeakOnNetworkManagerShutdownTest.cs.meta

Lines changed: 3 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)