Skip to content

Commit c215d15

Browse files
authored
perf!: reduce linq, cleanup connectedclients (#1196)
This is really two separate things: refactoring how the NetworkManager,ConnectedClients stuff works reducing linq / gc allocs in the hotpath The reason why they are in the same PR is because it would have been really hard to make one change without the other. Refactoring ConnectedClients Before whenever ConnectedClientIds from NetworkManager was called we would allocate a new list and use a linq .where call to fill it :this_is_fine: (this was really problematic for instance sending a ClientRpc uses this property causing 2 allocs 🔥 ) The new code keeps an additinal id list cached. In addition on the client side ConnectedClients is no longer partially broken by just containing the local client. The new system works like the following: NetworkManager.ConnectedClients / ConnectedClientsList / ConnectedClientsIds can only be accessed by the server. If a non-server tries to access them an exception is thrown. These lists are now exposed as properties and they are IReadOnlyLists because they really should not be modifiable. There is a new NetworkManager.LocalClient property which can be used to access the local client's NetworkClient. This works on clients/host.
1 parent 8a1f1f8 commit c215d15

File tree

12 files changed

+117
-43
lines changed

12 files changed

+117
-43
lines changed

com.unity.netcode.adapter.utp/Runtime/UnityTransport.cs

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,7 @@
11
using System;
22
using System.Collections;
33
using System.Collections.Generic;
4-
using System.Linq;
54
using UnityEngine;
6-
75
using NetcodeNetworkEvent = Unity.Netcode.NetworkEvent;
86
using TransportNetworkEvent = Unity.Networking.Transport.NetworkEvent;
97
using Unity.Collections.LowLevel.Unsafe;
@@ -472,12 +470,21 @@ public override void DisconnectRemoteClient(ulong clientId)
472470
}
473471

474472
// we need to cleanup any SendQueues for this connectionID;
475-
var keys = m_SendQueue.Keys.Where(k => k.ClientId == clientId).ToList();
473+
var keys = new NativeList<SendTarget>(16, Allocator.Temp); // use nativelist and manual foreach to avoid allocations
474+
foreach (var key in m_SendQueue.Keys)
475+
{
476+
if (key.ClientId == clientId)
477+
{
478+
keys.Add(key);
479+
}
480+
}
481+
476482
foreach (var queue in keys)
477483
{
478484
m_SendQueue[queue].Dispose();
479485
m_SendQueue.Remove(queue);
480486
}
487+
keys.Dispose();
481488
}
482489
}
483490

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

Lines changed: 2 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
using System;
22
using System.Collections.Generic;
3+
using System.Linq;
34
using UnityEngine;
45
using System.Reflection;
5-
using System.Linq;
66
using Unity.Collections;
77

88
namespace Unity.Netcode
@@ -131,14 +131,7 @@ internal unsafe void __sendClientRpc(FastBufferWriter writer, uint rpcMethodId,
131131

132132
if (rpcParams.Send.TargetClientIds != null)
133133
{
134-
// Copy into a localArray because SendMessage doesn't take IEnumerable, only IReadOnlyList
135-
ulong* localArray = stackalloc ulong[rpcParams.Send.TargetClientIds.Count()];
136-
var index = 0;
137-
foreach (var clientId in rpcParams.Send.TargetClientIds)
138-
{
139-
localArray[index++] = clientId;
140-
}
141-
messageSize = NetworkManager.SendMessage(message, networkDelivery, localArray, index, true);
134+
messageSize = NetworkManager.SendMessage(message, networkDelivery, in rpcParams.Send.TargetClientIds, true);
142135
}
143136
else if (rpcParams.Send.TargetClientIdsNativeArray != null)
144137
{

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

Lines changed: 65 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
using System;
22
using System.Collections;
33
using System.Collections.Generic;
4-
using System.Linq;
54
using Unity.Collections;
65
using Unity.Collections.LowLevel.Unsafe;
76
using UnityEngine;
@@ -251,20 +250,61 @@ internal set
251250

252251
private ulong m_LocalClientId;
253252

253+
private Dictionary<ulong, NetworkClient> m_ConnectedClients = new Dictionary<ulong, NetworkClient>();
254+
255+
private List<NetworkClient> m_ConnectedClientsList = new List<NetworkClient>();
256+
257+
private List<ulong> m_ConnectedClientIds = new List<ulong>();
258+
254259
/// <summary>
255-
/// Gets a dictionary of connected clients and their clientId keys. This is only populated on the server.
260+
/// Gets a dictionary of connected clients and their clientId keys. This is only accessible on the server.
256261
/// </summary>
257-
public readonly Dictionary<ulong, NetworkClient> ConnectedClients = new Dictionary<ulong, NetworkClient>();
262+
public IReadOnlyDictionary<ulong, NetworkClient> ConnectedClients
263+
{
264+
get
265+
{
266+
if (IsServer == false)
267+
{
268+
throw new NotServerException($"{nameof(ConnectedClients)} should only be accessed on server.");
269+
}
270+
return m_ConnectedClients;
271+
}
272+
}
258273

259274
/// <summary>
260-
/// Gets a list of connected clients. This is only populated on the server.
275+
/// Gets a list of connected clients. This is only accessible on the server.
261276
/// </summary>
262-
public readonly List<NetworkClient> ConnectedClientsList = new List<NetworkClient>();
277+
public IReadOnlyList<NetworkClient> ConnectedClientsList
278+
{
279+
get
280+
{
281+
if (IsServer == false)
282+
{
283+
throw new NotServerException($"{nameof(ConnectedClientsList)} should only be accessed on server.");
284+
}
285+
return m_ConnectedClientsList;
286+
}
287+
}
263288

264289
/// <summary>
265-
/// Gets a list of just the IDs of all connected clients.
290+
/// Gets a list of just the IDs of all connected clients. This is only accessible on the server.
266291
/// </summary>
267-
public ulong[] ConnectedClientsIds => ConnectedClientsList.Select(c => c.ClientId).ToArray();
292+
public IReadOnlyList<ulong> ConnectedClientsIds
293+
{
294+
get
295+
{
296+
if (IsServer == false)
297+
{
298+
throw new NotServerException($"{nameof(m_ConnectedClientIds)} should only be accessed on server.");
299+
}
300+
return m_ConnectedClientIds;
301+
}
302+
}
303+
304+
/// <summary>
305+
/// Gets the local <see cref="NetworkClient"/> for this client.
306+
/// </summary>
307+
public NetworkClient LocalClient { get; internal set; }
268308

269309
/// <summary>
270310
/// Gets a dictionary of the clients that have been accepted by the transport but are still pending by the Netcode. This is only populated on the server.
@@ -468,8 +508,10 @@ private void Initialize(bool server)
468508
LocalClientId = ulong.MaxValue;
469509

470510
PendingClients.Clear();
471-
ConnectedClients.Clear();
472-
ConnectedClientsList.Clear();
511+
m_ConnectedClients.Clear();
512+
m_ConnectedClientsList.Clear();
513+
m_ConnectedClientIds.Clear();
514+
LocalClient = null;
473515
NetworkObject.OrphanChildren.Clear();
474516

475517
// Create spawn manager instance
@@ -1415,12 +1457,21 @@ private void OnClientDisconnectFromServer(ulong clientId)
14151457
{
14161458
if (ConnectedClientsList[i].ClientId == clientId)
14171459
{
1418-
ConnectedClientsList.RemoveAt(i);
1460+
m_ConnectedClientsList.RemoveAt(i);
1461+
break;
1462+
}
1463+
}
1464+
1465+
for (int i = 0; i < ConnectedClientsIds.Count; i++)
1466+
{
1467+
if (ConnectedClientsIds[i] == clientId)
1468+
{
1469+
m_ConnectedClientIds.RemoveAt(i);
14191470
break;
14201471
}
14211472
}
14221473

1423-
ConnectedClients.Remove(clientId);
1474+
m_ConnectedClients.Remove(clientId);
14241475
}
14251476
m_MessagingSystem.ClientDisconnected(clientId);
14261477
}
@@ -1462,8 +1513,9 @@ internal void HandleApproval(ulong ownerClientId, bool createPlayerObject, uint?
14621513
PendingClients.Remove(ownerClientId);
14631514

14641515
var client = new NetworkClient { ClientId = ownerClientId, };
1465-
ConnectedClients.Add(ownerClientId, client);
1466-
ConnectedClientsList.Add(client);
1516+
m_ConnectedClients.Add(ownerClientId, client);
1517+
m_ConnectedClientsList.Add(client);
1518+
m_ConnectedClientIds.Add(client.ClientId);
14671519

14681520
if (createPlayerObject)
14691521
{

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -694,7 +694,7 @@ private void OnTransformParentChanged()
694694

695695
unsafe
696696
{
697-
var maxCount = NetworkManager.ConnectedClientsIds.Length;
697+
var maxCount = NetworkManager.ConnectedClientsIds.Count;
698698
ulong* clientIds = stackalloc ulong[maxCount];
699699
int idx = 0;
700700
foreach (var clientId in NetworkManager.ConnectedClientsIds)

com.unity.netcode.gameobjects/Runtime/Messaging/Messages/ConnectionApprovedMessage.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -66,7 +66,7 @@ public void Handle(FastBufferReader reader, ulong clientId, NetworkManager netwo
6666
var time = new NetworkTime(networkManager.NetworkTickSystem.TickRate, NetworkTick);
6767
networkManager.NetworkTimeSystem.Reset(time.Time, 0.15f); // Start with a constant RTT of 150 until we receive values from the transport.
6868

69-
networkManager.ConnectedClients.Add(networkManager.LocalClientId, new NetworkClient { ClientId = networkManager.LocalClientId });
69+
networkManager.LocalClient = new NetworkClient() { ClientId = networkManager.LocalClientId };
7070

7171
// Only if scene management is disabled do we handle NetworkObject synchronization at this point
7272
if (!networkManager.NetworkConfig.EnableSceneManagement)

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ public struct ClientRpcSendParams
2525
/// Note: Even if you provide a value type such as NativeArray, enumerating it will cause boxing.
2626
/// If you want to avoid boxing, use TargetClientIdsNativeArray
2727
/// </summary>
28-
public IEnumerable<ulong> TargetClientIds;
28+
public IReadOnlyList<ulong> TargetClientIds;
2929

3030
/// <summary>
3131
/// NativeArray version of target id list - use either this OR TargetClientIds

com.unity.netcode.gameobjects/Runtime/Metrics/INetworkMetrics.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -81,7 +81,7 @@ void TrackRpcReceived(
8181

8282
void TrackServerLogReceived(ulong senderClientId, uint logType, long bytesCount);
8383

84-
void TrackSceneEventSent(ulong[] receiverClientIds, uint sceneEventType, string sceneName, long bytesCount);
84+
void TrackSceneEventSent(IReadOnlyList<ulong> receiverClientIds, uint sceneEventType, string sceneName, long bytesCount);
8585

8686
void TrackSceneEventSent(ulong receiverClientId, uint sceneEventType, string sceneName, long bytesCount);
8787

com.unity.netcode.gameobjects/Runtime/Metrics/NetworkMetrics.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -262,7 +262,7 @@ public void TrackServerLogReceived(ulong senderClientId, uint logType, long byte
262262
m_ServerLogReceivedEvent.Mark(new ServerLogEvent(new ConnectionInfo(senderClientId), (Unity.Multiplayer.Tools.MetricTypes.LogLevel)logType, bytesCount));
263263
}
264264

265-
public void TrackSceneEventSent(ulong[] receiverClientIds, uint sceneEventType, string sceneName, long bytesCount)
265+
public void TrackSceneEventSent(IReadOnlyList<ulong> receiverClientIds, uint sceneEventType, string sceneName, long bytesCount)
266266
{
267267
foreach (var receiverClientId in receiverClientIds)
268268
{

com.unity.netcode.gameobjects/Runtime/Metrics/NullNetworkMetrics.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -131,7 +131,7 @@ public void TrackServerLogReceived(ulong senderClientId, uint logType, long byte
131131
{
132132
}
133133

134-
public void TrackSceneEventSent(ulong[] receiverClientIds, uint sceneEventType, string sceneName, long bytesCount)
134+
public void TrackSceneEventSent(IReadOnlyList<ulong> receiverClientIds, uint sceneEventType, string sceneName, long bytesCount)
135135
{
136136
}
137137

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

Lines changed: 32 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,6 @@ public class NetworkSpawnManager
2020
/// </summary>
2121
public readonly HashSet<NetworkObject> SpawnedObjectsList = new HashSet<NetworkObject>();
2222

23-
2423
/// <summary>
2524
/// Gets the NetworkManager associated with this SpawnManager.
2625
/// </summary>
@@ -69,7 +68,7 @@ public NetworkObject GetPlayerNetworkObject(ulong clientId)
6968
throw new NotServerException("Only the server can find player objects from other clients.");
7069
}
7170

72-
if (NetworkManager.ConnectedClients.TryGetValue(clientId, out NetworkClient networkClient))
71+
if (TryGetNetworkClient(clientId, out NetworkClient networkClient))
7372
{
7473
return networkClient.PlayerObject;
7574
}
@@ -111,12 +110,37 @@ internal void RemoveOwnership(NetworkObject networkObject)
111110
foreach (var client in NetworkManager.ConnectedClients)
112111
{
113112
var bytesReported = NetworkManager.LocalClientId == client.Key
114-
? 0
115-
: size;
113+
? 0
114+
: size;
116115
NetworkManager.NetworkMetrics.TrackOwnershipChangeSent(client.Key, networkObject.NetworkObjectId, networkObject.name, bytesReported);
117116
}
118117
}
119118

119+
/// <summary>
120+
/// Helper function to get a network client for a clientId from the NetworkManager.
121+
/// On the server this will check the <see cref="NetworkManager.ConnectedClients"/> list.
122+
/// On a non-server this will check the <see cref="NetworkManager.LocalClient"/> only.
123+
/// </summary>
124+
/// <param name="clientId">The clientId for which to try getting the NetworkClient for.</param>
125+
/// <param name="networkClient">The found NetworkClient. Null if no client was found.</param>
126+
/// <returns>True if a NetworkClient with a matching id was found else false.</returns>
127+
private bool TryGetNetworkClient(ulong clientId, out NetworkClient networkClient)
128+
{
129+
if (NetworkManager.IsServer)
130+
{
131+
return NetworkManager.ConnectedClients.TryGetValue(clientId, out networkClient);
132+
}
133+
134+
if (clientId == NetworkManager.LocalClient.ClientId)
135+
{
136+
networkClient = NetworkManager.LocalClient;
137+
return true;
138+
}
139+
140+
networkClient = null;
141+
return false;
142+
}
143+
120144
internal void ChangeOwnership(NetworkObject networkObject, ulong clientId)
121145
{
122146
if (!NetworkManager.IsServer)
@@ -129,7 +153,7 @@ internal void ChangeOwnership(NetworkObject networkObject, ulong clientId)
129153
throw new SpawnStateException("Object is not spawned");
130154
}
131155

132-
if (NetworkManager.ConnectedClients.TryGetValue(networkObject.OwnerClientId, out NetworkClient networkClient))
156+
if (TryGetNetworkClient(networkObject.OwnerClientId, out NetworkClient networkClient))
133157
{
134158
for (int i = networkClient.OwnedObjects.Count - 1; i >= 0; i--)
135159
{
@@ -360,7 +384,7 @@ private void SpawnNetworkObjectLocallyCommon(NetworkObject networkObject, ulong
360384
}
361385
else if (playerObject && ownerClientId.Value == NetworkManager.LocalClientId)
362386
{
363-
NetworkManager.ConnectedClients[ownerClientId.Value].PlayerObject = networkObject;
387+
NetworkManager.LocalClient.PlayerObject = networkObject;
364388
}
365389
}
366390

@@ -524,7 +548,6 @@ internal void DestroySceneObjects()
524548
}
525549
}
526550

527-
528551
internal void ServerSpawnSceneObjectsOnStartSweep()
529552
{
530553
var networkObjects = UnityEngine.Object.FindObjectsOfType<NetworkObject>();
@@ -577,7 +600,7 @@ internal void OnDespawnObject(NetworkObject networkObject, bool destroyGameObjec
577600
}
578601
}
579602

580-
if (!networkObject.IsOwnedByServer && !networkObject.IsPlayerObject && NetworkManager.Singleton.ConnectedClients.TryGetValue(networkObject.OwnerClientId, out NetworkClient networkClient))
603+
if (!networkObject.IsOwnedByServer && !networkObject.IsPlayerObject && TryGetNetworkClient(networkObject.OwnerClientId, out NetworkClient networkClient))
581604
{
582605
//Someone owns it.
583606
for (int i = networkClient.OwnedObjects.Count - 1; i > -1; i--)
@@ -641,6 +664,7 @@ internal void OnDespawnObject(NetworkObject networkObject, bool destroyGameObjec
641664
}
642665
}
643666
}
667+
644668
networkObject.IsSpawned = false;
645669

646670
if (SpawnedObjects.Remove(networkObject.NetworkObjectId))

com.unity.netcode.gameobjects/Samples/Bootstrap/Scripts/BootstrapManager.cs

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -39,11 +39,10 @@ private void OnGUI()
3939
{
4040
if (GUILayout.Button("Random Teleport"))
4141
{
42-
// Get the local `NetworkClient` by `LocalClientId`
43-
if (networkManager.ConnectedClients.TryGetValue(networkManager.LocalClientId, out var networkedClient))
42+
if (networkManager.LocalClient != null)
4443
{
4544
// Get `BootstrapPlayer` component from the player's `PlayerObject`
46-
if (networkedClient.PlayerObject.TryGetComponent(out BootstrapPlayer bootstrapPlayer))
45+
if (networkManager.LocalClient.PlayerObject.TryGetComponent(out BootstrapPlayer bootstrapPlayer))
4746
{
4847
// Invoke a `ServerRpc` from client-side to teleport player to a random position on the server-side
4948
bootstrapPlayer.RandomTeleportServerRpc();

com.unity.netcode.gameobjects/Tests/Runtime/Helpers/NetworkManagerHelper.cs

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -217,7 +217,6 @@ public static void ShutdownNetworkManager()
217217

218218
if (NetworkManagerGameObject != null)
219219
{
220-
NetworkManagerObject.ConnectedClientsList.Clear();
221220
Debug.Log($"{nameof(NetworkManager)} shutdown.");
222221

223222
StopNetworkManagerMode();

0 commit comments

Comments
 (0)