Skip to content

Commit 11750a9

Browse files
author
vis2k
authored
perf: Avoid allocation when reading message payload (#912)
* so far * syntax * add test * more tests * more tests * convert messages * use arraysegment in serialization and handle null case too
1 parent dd758ca commit 11750a9

7 files changed

+114
-29
lines changed

Assets/Mirror/Runtime/ClientScene.cs

+8-5
Original file line numberDiff line numberDiff line change
@@ -330,18 +330,21 @@ public static GameObject FindLocalObject(uint netId)
330330
return null;
331331
}
332332

333-
static void ApplySpawnPayload(NetworkIdentity identity, Vector3 position, Quaternion rotation, Vector3 scale, byte[] payload, uint netId)
333+
static void ApplySpawnPayload(NetworkIdentity identity, Vector3 position, Quaternion rotation, Vector3 scale, ArraySegment<byte> payload, uint netId)
334334
{
335335
if (!identity.gameObject.activeSelf)
336336
{
337337
identity.gameObject.SetActive(true);
338338
}
339-
339+
340340
// apply local values for VR support
341341
identity.transform.localPosition = position;
342342
identity.transform.localRotation = rotation;
343-
identity.transform.localScale = scale;
344-
if (payload != null && payload.Length > 0)
343+
identity.transform.localScale = scale;
344+
345+
// deserialize components if any payload
346+
// (Count is 0 if there were no components)
347+
if (payload.Count > 0)
345348
{
346349
NetworkReader payloadReader = new NetworkReader(payload);
347350
identity.OnUpdateVars(payloadReader, true);
@@ -453,7 +456,7 @@ internal static void OnSpawnSceneObject(NetworkConnection conn, SpawnSceneObject
453456
foreach (KeyValuePair<ulong, NetworkIdentity> kvp in spawnableObjects)
454457
Debug.Log("Spawnable: SceneId=" + kvp.Key + " name=" + kvp.Value.name);
455458
}
456-
459+
457460
return;
458461
}
459462

Assets/Mirror/Runtime/Messages.cs

+20-12
Original file line numberDiff line numberDiff line change
@@ -183,22 +183,24 @@ class RemoteCallMessage : MessageBase
183183
public uint netId;
184184
public int componentIndex;
185185
public int functionHash;
186-
public byte[] payload; // the parameters for the Cmd function
186+
// the parameters for the Cmd function
187+
// -> ArraySegment to avoid unnecessary allocations
188+
public ArraySegment<byte> payload;
187189

188190
public override void Deserialize(NetworkReader reader)
189191
{
190192
netId = reader.ReadPackedUInt32();
191193
componentIndex = (int)reader.ReadPackedUInt32();
192194
functionHash = reader.ReadInt32(); // hash is always 4 full bytes, WritePackedInt would send 1 extra byte here
193-
payload = reader.ReadBytesAndSize();
195+
payload = reader.ReadBytesAndSizeSegment();
194196
}
195197

196198
public override void Serialize(NetworkWriter writer)
197199
{
198200
writer.WritePackedUInt32(netId);
199201
writer.WritePackedUInt32((uint)componentIndex);
200202
writer.Write(functionHash);
201-
writer.WriteBytesAndSize(payload);
203+
writer.WriteBytesAndSizeSegment(payload);
202204
}
203205
}
204206

@@ -218,7 +220,9 @@ class SpawnPrefabMessage : MessageBase
218220
public Vector3 position;
219221
public Quaternion rotation;
220222
public Vector3 scale;
221-
public byte[] payload;
223+
// the serialized component data
224+
// -> ArraySegment to avoid unnecessary allocations
225+
public ArraySegment<byte> payload;
222226

223227
public override void Deserialize(NetworkReader reader)
224228
{
@@ -228,7 +232,7 @@ public override void Deserialize(NetworkReader reader)
228232
position = reader.ReadVector3();
229233
rotation = reader.ReadQuaternion();
230234
scale = reader.ReadVector3();
231-
payload = reader.ReadBytesAndSize();
235+
payload = reader.ReadBytesAndSizeSegment();
232236
}
233237

234238
public override void Serialize(NetworkWriter writer)
@@ -239,7 +243,7 @@ public override void Serialize(NetworkWriter writer)
239243
writer.Write(position);
240244
writer.Write(rotation);
241245
writer.Write(scale);
242-
writer.WriteBytesAndSize(payload);
246+
writer.WriteBytesAndSizeSegment(payload);
243247
}
244248
}
245249

@@ -251,7 +255,9 @@ class SpawnSceneObjectMessage : MessageBase
251255
public Vector3 position;
252256
public Quaternion rotation;
253257
public Vector3 scale;
254-
public byte[] payload;
258+
// the serialized component data
259+
// -> ArraySegment to avoid unnecessary allocations
260+
public ArraySegment<byte> payload;
255261

256262
public override void Deserialize(NetworkReader reader)
257263
{
@@ -261,7 +267,7 @@ public override void Deserialize(NetworkReader reader)
261267
position = reader.ReadVector3();
262268
rotation = reader.ReadQuaternion();
263269
scale = reader.ReadVector3();
264-
payload = reader.ReadBytesAndSize();
270+
payload = reader.ReadBytesAndSizeSegment();
265271
}
266272

267273
public override void Serialize(NetworkWriter writer)
@@ -272,7 +278,7 @@ public override void Serialize(NetworkWriter writer)
272278
writer.Write(position);
273279
writer.Write(rotation);
274280
writer.Write(scale);
275-
writer.WriteBytesAndSize(payload);
281+
writer.WriteBytesAndSizeSegment(payload);
276282
}
277283
}
278284

@@ -331,18 +337,20 @@ public override void Serialize(NetworkWriter writer)
331337
class UpdateVarsMessage : MessageBase
332338
{
333339
public uint netId;
334-
public byte[] payload;
340+
// the serialized component data
341+
// -> ArraySegment to avoid unnecessary allocations
342+
public ArraySegment<byte> payload;
335343

336344
public override void Deserialize(NetworkReader reader)
337345
{
338346
netId = reader.ReadPackedUInt32();
339-
payload = reader.ReadBytesAndSize();
347+
payload = reader.ReadBytesAndSizeSegment();
340348
}
341349

342350
public override void Serialize(NetworkWriter writer)
343351
{
344352
writer.WritePackedUInt32(netId);
345-
writer.WriteBytesAndSize(payload);
353+
writer.WriteBytesAndSizeSegment(payload);
346354
}
347355
}
348356

Assets/Mirror/Runtime/NetworkBehaviour.cs

+4-4
Original file line numberDiff line numberDiff line change
@@ -121,7 +121,7 @@ protected void SendCommandInternal(Type invokeClass, string cmdName, NetworkWrit
121121
netId = netId,
122122
componentIndex = ComponentIndex,
123123
functionHash = GetMethodHash(invokeClass, cmdName), // type+func so Inventory.RpcUse != Equipment.RpcUse
124-
payload = writer.ToArray()
124+
payload = new ArraySegment<byte>(writer.ToArray()) // segment to avoid reader allocations
125125
};
126126

127127
ClientScene.readyConnection.Send(message, channelId);
@@ -157,7 +157,7 @@ protected void SendRPCInternal(Type invokeClass, string rpcName, NetworkWriter w
157157
netId = netId,
158158
componentIndex = ComponentIndex,
159159
functionHash = GetMethodHash(invokeClass, rpcName), // type+func so Inventory.RpcUse != Equipment.RpcUse
160-
payload = writer.ToArray()
160+
payload = new ArraySegment<byte>(writer.ToArray()) // segment to avoid reader allocations
161161
};
162162

163163
NetworkServer.SendToReady(netIdentity, message, channelId);
@@ -196,7 +196,7 @@ protected void SendTargetRPCInternal(NetworkConnection conn, Type invokeClass, s
196196
netId = netId,
197197
componentIndex = ComponentIndex,
198198
functionHash = GetMethodHash(invokeClass, rpcName), // type+func so Inventory.RpcUse != Equipment.RpcUse
199-
payload = writer.ToArray()
199+
payload = new ArraySegment<byte>(writer.ToArray()) // segment to avoid reader allocations
200200
};
201201

202202
conn.Send(message, channelId);
@@ -225,7 +225,7 @@ protected void SendEventInternal(Type invokeClass, string eventName, NetworkWrit
225225
netId = netId,
226226
componentIndex = ComponentIndex,
227227
functionHash = GetMethodHash(invokeClass, eventName), // type+func so Inventory.RpcUse != Equipment.RpcUse
228-
payload = writer.ToArray()
228+
payload = new ArraySegment<byte>(writer.ToArray()) // segment to avoid reader allocations
229229
};
230230

231231
NetworkServer.SendToReady(netIdentity,message, channelId);

Assets/Mirror/Runtime/NetworkIdentity.cs

+3-1
Original file line numberDiff line numberDiff line change
@@ -1043,7 +1043,9 @@ internal void MirrorUpdate()
10431043
{
10441044
// populate cached UpdateVarsMessage and send
10451045
varsMessage.netId = netId;
1046-
varsMessage.payload = payload;
1046+
// segment to avoid reader allocations.
1047+
// (never null because of our above check)
1048+
varsMessage.payload = new ArraySegment<byte>(payload);
10471049
NetworkServer.SendToReady(this, varsMessage);
10481050
}
10491051
}

Assets/Mirror/Runtime/NetworkReader.cs

+24-1
Original file line numberDiff line numberDiff line change
@@ -167,6 +167,21 @@ public byte[] ReadBytes(int count)
167167
return bytes;
168168
}
169169

170+
// useful to parse payloads etc. without allocating
171+
public ArraySegment<byte> ReadBytesSegment(int count)
172+
{
173+
// check if within buffer limits
174+
if (Position + count > buffer.Count)
175+
{
176+
throw new EndOfStreamException("ReadBytesSegment can't read " + count + " bytes because it would read past the end of the stream. " + ToString());
177+
}
178+
179+
// return the segment
180+
ArraySegment<byte> result = new ArraySegment<byte>(buffer.Array, buffer.Offset + Position, count);
181+
Position += count;
182+
return result;
183+
}
184+
170185
// Use checked() to force it to throw OverflowException if data is invalid
171186
// null support, see NetworkWriter
172187
public byte[] ReadBytesAndSize()
@@ -177,7 +192,15 @@ public byte[] ReadBytesAndSize()
177192
}
178193
return null;
179194
}
180-
public ArraySegment<byte> ReadBytesAndSizeSegment() => new ArraySegment<byte>(ReadBytesAndSize());
195+
196+
public ArraySegment<byte> ReadBytesAndSizeSegment()
197+
{
198+
if (ReadBoolean())
199+
{
200+
return ReadBytesSegment((int)ReadPackedUInt32());
201+
}
202+
return default;
203+
}
181204

182205
// zigzag decoding https://gist.github.com/mfuerstenau/ba870a29e16536fdbaba
183206
public int ReadPackedInt32()

Assets/Mirror/Runtime/NetworkServer.cs

+10-6
Original file line numberDiff line numberDiff line change
@@ -836,6 +836,14 @@ internal static void SendSpawnMessage(NetworkIdentity identity, NetworkConnectio
836836

837837
if (LogFilter.Debug) Debug.Log("Server SendSpawnMessage: name=" + identity.name + " sceneId=" + identity.sceneId.ToString("X") + " netid=" + identity.netId); // for easier debugging
838838

839+
// serialize all components with initialState = true
840+
// (can be null if has none)
841+
byte[] serialized = identity.OnSerializeAllSafely(true);
842+
843+
// convert to ArraySegment to avoid reader allocations
844+
// (need to handle null case too)
845+
ArraySegment<byte> segment = serialized != null ? new ArraySegment<byte>(serialized) : default;
846+
839847
// 'identity' is a prefab that should be spawned
840848
if (identity.sceneId == 0)
841849
{
@@ -848,9 +856,7 @@ internal static void SendSpawnMessage(NetworkIdentity identity, NetworkConnectio
848856
position = identity.transform.localPosition,
849857
rotation = identity.transform.localRotation,
850858
scale = identity.transform.localScale,
851-
852-
// serialize all components with initialState = true
853-
payload = identity.OnSerializeAllSafely(true)
859+
payload = segment
854860
};
855861

856862
// conn is != null when spawning it for a client
@@ -876,9 +882,7 @@ internal static void SendSpawnMessage(NetworkIdentity identity, NetworkConnectio
876882
position = identity.transform.localPosition,
877883
rotation = identity.transform.localRotation,
878884
scale = identity.transform.localScale,
879-
880-
// include synch data
881-
payload = identity.OnSerializeAllSafely(true)
885+
payload = segment
882886
};
883887

884888
// conn is != null when spawning it for a client

Assets/Mirror/Tests/NetworkWriterTest.cs

+45
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,51 @@ public void TestWritingHugeArray()
4040
Assert.That(deserialized.Length, Is.EqualTo(100000));
4141
}
4242

43+
[Test]
44+
public void TestWritingBytesSegment()
45+
{
46+
byte[] data = {1, 2, 3};
47+
NetworkWriter writer = new NetworkWriter();
48+
writer.Write(data, 0, data.Length);
49+
50+
NetworkReader reader = new NetworkReader(writer.ToArray());
51+
ArraySegment<byte> deserialized = reader.ReadBytesSegment(data.Length);
52+
Assert.That(deserialized.Count, Is.EqualTo(data.Length));
53+
for (int i = 0; i < data.Length; ++i)
54+
Assert.That(deserialized.Array[deserialized.Offset + i], Is.EqualTo(data[i]));
55+
}
56+
57+
// write byte[], read segment
58+
[Test]
59+
public void TestWritingBytesAndReadingSegment()
60+
{
61+
byte[] data = {1, 2, 3};
62+
NetworkWriter writer = new NetworkWriter();
63+
writer.WriteBytesAndSize(data);
64+
65+
NetworkReader reader = new NetworkReader(writer.ToArray());
66+
ArraySegment<byte> deserialized = reader.ReadBytesAndSizeSegment();
67+
Assert.That(deserialized.Count, Is.EqualTo(data.Length));
68+
for (int i = 0; i < data.Length; ++i)
69+
Assert.That(deserialized.Array[deserialized.Offset + i], Is.EqualTo(data[i]));
70+
}
71+
72+
// write segment, read segment
73+
[Test]
74+
public void TestWritingSegmentAndReadingSegment()
75+
{
76+
byte[] data = {1, 2, 3, 4};
77+
ArraySegment<byte> segment = new ArraySegment<byte>(data, 1, 1); // [2, 3]
78+
NetworkWriter writer = new NetworkWriter();
79+
writer.WriteBytesAndSizeSegment(segment);
80+
81+
NetworkReader reader = new NetworkReader(writer.ToArray());
82+
ArraySegment<byte> deserialized = reader.ReadBytesAndSizeSegment();
83+
Assert.That(deserialized.Count, Is.EqualTo(segment.Count));
84+
for (int i = 0; i < segment.Count; ++i)
85+
Assert.That(deserialized.Array[deserialized.Offset + i], Is.EqualTo(segment.Array[segment.Offset + i]));
86+
}
87+
4388
[Test]
4489
public void TestOverwritingData()
4590
{

0 commit comments

Comments
 (0)