perf: Batching. Mirror will batch up to Transport.GetMaxPacketSize messages into one batch and then send them all at once. Reduces transport calls by a factor of ~1000x assuming average of 64 byte message with Telepathy 64kb MaxMessageSize.

This commit is contained in:
vis2k 2020-10-09 10:58:58 +02:00
parent fa53fa60f8
commit 4b9a731fc3
8 changed files with 117 additions and 26 deletions

View File

@ -214,18 +214,24 @@ internal void TransportReceive(ArraySegment<byte> buffer)
// unpack message
using (PooledNetworkReader networkReader = NetworkReaderPool.GetReader(buffer))
{
if (MessagePacker.UnpackMessage(networkReader, out int msgType))
// the other end might batch multiple messages into one packet.
// we need to try to unpack multiple times.
while (networkReader.Remaining > 0)
{
// logging
// Debug.Log("ConnectionRecv " + this + " msgType:" + msgType + " content:" + BitConverter.ToString(buffer.Array, buffer.Offset, buffer.Count));
if (MessagePacker.UnpackMessage(networkReader, out int msgType))
{
// logging
// Debug.Log("ConnectionRecv " + this + " msgType:" + msgType + " content:" + BitConverter.ToString(buffer.Array, buffer.Offset, buffer.Count));
// try to invoke the handler for that message
InvokeHandler(msgType, networkReader);
}
else
{
Debug.LogError("Closed connection: " + this + ". Invalid message header.");
Disconnect();
// try to invoke the handler for that message
InvokeHandler(msgType, networkReader);
}
else
{
Debug.LogError("Closed connection: " + this + ". Invalid message header.");
Disconnect();
break;
}
}
}
}

View File

@ -4,10 +4,40 @@ namespace Mirror
{
public class NetworkConnectionToClient : NetworkConnection
{
public NetworkConnectionToClient(int networkConnectionId) : base(networkConnectionId) { }
// batching from server to client.
// fewer transport calls give us significantly better performance/scale.
//
// for a 64KB max message transport and 64 bytes/message on average, we
// reduce transport calls by a factor of 1000.
//
// depending on the transport, this can give 10x performance.
NetworkWriter batch = new NetworkWriter();
// batch interval is 0 by default, meaning that we send immediately.
// (useful to run tests without waiting for intervals too)
float batchInterval;
double batchSendTime;
public NetworkConnectionToClient(int networkConnectionId, float batchInterval) : base(networkConnectionId)
{
this.batchInterval = batchInterval;
}
public override string address => Transport.activeTransport.ServerGetClientAddress(connectionId);
void SendBatch(int channelId)
{
// send batch
Transport.activeTransport.ServerSend(connectionId, channelId, batch.ToArraySegment());
// clear batch
batch.Position = 0;
// reset send time
// (use NetworkTime for maximum precision over days)
batchSendTime = NetworkTime.time;
}
internal override bool Send(ArraySegment<byte> segment, int channelId = Channels.DefaultReliable)
{
//Debug.Log("ConnectionSend " + this + " bytes:" + BitConverter.ToString(segment.Array, segment.Offset, segment.Count));
@ -15,11 +45,45 @@ internal override bool Send(ArraySegment<byte> segment, int channelId = Channels
// validate packet size first.
if (ValidatePacketSize(segment, channelId))
{
return Transport.activeTransport.ServerSend(connectionId, channelId, segment);
// batching?
if (batchInterval > 0)
{
// if batch would become bigger than MaxPacketSize then send
// out the previous batch first
// TODO respect channelId
int max = Transport.activeTransport.GetMaxPacketSize(Channels.DefaultReliable);
if (batch.Position + segment.Count > max)
{
// TODO respect channelId
//Debug.LogWarning($"sending batch {batch.Position} / {max} after full for segment={segment.Count} for connectionId={connectionId}");
SendBatch(Channels.DefaultReliable);
}
// now add segment to batch
batch.WriteBytes(segment.Array, segment.Offset, segment.Count);
return true;
}
// otherwise send directly (for tests etc.)
else return Transport.activeTransport.ServerSend(connectionId, channelId, segment);
}
return false;
}
// flush batched messages every batchInterval to make sure that they are
// sent out every now and then, even if the batch isn't full yet.
// (avoids 30s latency if batches would only get full every 30s)
internal void Update()
{
// enough time elapsed and anything batched?
if (NetworkTime.time > batchSendTime + batchInterval &&
batch.Position > 0)
{
// TODO respect channelId
//Debug.LogWarning($"sending batch of {batch.Position} bytes after time for connId= {connectionId}");
SendBatch(Channels.DefaultReliable);
}
}
/// <summary>
/// Disconnects this connection.
/// </summary>

View File

@ -62,6 +62,13 @@ public class NetworkManager : MonoBehaviour
[Tooltip("Server Update frequency, per second. Use around 60Hz for fast paced games like Counter-Strike to minimize latency. Use around 30Hz for games like WoW to minimize computations. Use around 1-10Hz for slow paced games like EVE.")]
public int serverTickRate = 30;
// batching from server to client.
// fewer transport calls give us significantly better performance/scale.
// batch interval is 0 by default, meaning that we send immediately.
// (useful to run tests without waiting for intervals too)
[Tooltip("Server can batch messages up to Transport.GetMaxPacketSize to significantly reduce transport calls and improve performance/scale./nNote that this increases latency./n0 means send immediately.")]
public float serverBatchInterval = 0.010f;
// transport layer
[Header("Network Info")]
[Tooltip("Transport component attached to this object that server and client will use to connect")]
@ -261,6 +268,8 @@ void SetupServer()
authenticator.OnServerAuthenticated.AddListener(OnServerAuthenticated);
}
NetworkServer.batchInterval = serverBatchInterval;
ConfigureServerFrameRate();
// start listening to network connections

View File

@ -46,6 +46,12 @@ public static class NetworkServer
/// </summary>
public static bool active { get; internal set; }
// batching from server to client.
// fewer transport calls give us significantly better performance/scale.
// batch interval is 0 by default, meaning that we send immediately.
// (useful to run tests without waiting for intervals too)
public static float batchInterval = 0;
/// <summary>
/// This shuts down the server and disconnects all clients.
/// </summary>
@ -382,6 +388,12 @@ public static void Update()
Debug.LogWarning("Found 'null' entry in spawned list for netId=" + kvp.Key + ". Please call NetworkServer.Destroy to destroy networked objects. Don't use GameObject.Destroy.");
}
}
// update all connections to send out batched messages in interval
foreach (NetworkConnectionToClient conn in connections.Values)
{
conn.Update();
}
}
static void OnConnected(int connectionId)
@ -412,7 +424,7 @@ static void OnConnected(int connectionId)
if (connections.Count < maxConnections)
{
// add connection
NetworkConnectionToClient conn = new NetworkConnectionToClient(connectionId);
NetworkConnectionToClient conn = new NetworkConnectionToClient(connectionId, batchInterval);
OnConnected(conn);
}
else

View File

@ -4,7 +4,7 @@ namespace Mirror.Tests
{
public class FakeNetworkConnection : NetworkConnectionToClient
{
public FakeNetworkConnection() : base(1)
public FakeNetworkConnection() : base(1, 0)
{
}

View File

@ -865,9 +865,9 @@ public void Reset()
// creates .observers and generates a netId
identity.OnStartServer();
uint netId = identity.netId;
identity.connectionToClient = new NetworkConnectionToClient(1);
identity.connectionToClient = new NetworkConnectionToClient(1, 0);
identity.connectionToServer = new NetworkConnectionToServer();
identity.observers.Add(new NetworkConnectionToClient(2));
identity.observers.Add(new NetworkConnectionToClient(2, 0));
// mark for reset and reset
identity.Reset();
@ -882,7 +882,7 @@ public void HandleCommand()
{
// add component
CommandTestNetworkBehaviour comp0 = gameObject.AddComponent<CommandTestNetworkBehaviour>();
NetworkConnectionToClient connection = new NetworkConnectionToClient(1);
NetworkConnectionToClient connection = new NetworkConnectionToClient(1, 0);
Assert.That(comp0.called, Is.EqualTo(0));
Assert.That(comp0.senderConnectionInCall, Is.Null);

View File

@ -256,7 +256,7 @@ public void AddConnectionTest()
Assert.That(NetworkServer.connections.Count, Is.EqualTo(0));
// add first connection
NetworkConnectionToClient conn42 = new NetworkConnectionToClient(42);
NetworkConnectionToClient conn42 = new NetworkConnectionToClient(42, 0);
bool result42 = NetworkServer.AddConnection(conn42);
Assert.That(result42, Is.True);
Assert.That(NetworkServer.connections.Count, Is.EqualTo(1));
@ -264,7 +264,7 @@ public void AddConnectionTest()
Assert.That(NetworkServer.connections[42], Is.EqualTo(conn42));
// add second connection
NetworkConnectionToClient conn43 = new NetworkConnectionToClient(43);
NetworkConnectionToClient conn43 = new NetworkConnectionToClient(43, 0);
bool result43 = NetworkServer.AddConnection(conn43);
Assert.That(result43, Is.True);
Assert.That(NetworkServer.connections.Count, Is.EqualTo(2));
@ -274,7 +274,7 @@ public void AddConnectionTest()
Assert.That(NetworkServer.connections[43], Is.EqualTo(conn43));
// add duplicate connectionId
NetworkConnectionToClient connDup = new NetworkConnectionToClient(42);
NetworkConnectionToClient connDup = new NetworkConnectionToClient(42, 0);
bool resultDup = NetworkServer.AddConnection(connDup);
Assert.That(resultDup, Is.False);
Assert.That(NetworkServer.connections.Count, Is.EqualTo(2));
@ -297,7 +297,7 @@ public void RemoveConnectionTest()
Assert.That(NetworkServer.connections.Count, Is.EqualTo(0));
// add connection
NetworkConnectionToClient conn42 = new NetworkConnectionToClient(42);
NetworkConnectionToClient conn42 = new NetworkConnectionToClient(42, 0);
bool result42 = NetworkServer.AddConnection(conn42);
Assert.That(result42, Is.True);
Assert.That(NetworkServer.connections.Count, Is.EqualTo(1));
@ -323,7 +323,7 @@ public void DisconnectAllConnectionsTest()
Assert.That(NetworkServer.connections.Count, Is.EqualTo(0));
// add connection
NetworkConnectionToClient conn42 = new NetworkConnectionToClient(42);
NetworkConnectionToClient conn42 = new NetworkConnectionToClient(42, 0);
NetworkServer.AddConnection(conn42);
Assert.That(NetworkServer.connections.Count, Is.EqualTo(1));
@ -356,7 +356,7 @@ public void OnDataReceivedTest()
Assert.That(NetworkServer.connections.Count, Is.EqualTo(0));
// add a connection
NetworkConnectionToClient connection = new NetworkConnectionToClient(42);
NetworkConnectionToClient connection = new NetworkConnectionToClient(42, 0);
NetworkServer.AddConnection(connection);
Assert.That(NetworkServer.connections.Count, Is.EqualTo(1));
@ -440,7 +440,7 @@ public void RegisterUnregisterClearHandlerTest()
Assert.That(NetworkServer.connections.Count, Is.EqualTo(0));
// add a connection
NetworkConnectionToClient connection = new NetworkConnectionToClient(42);
NetworkConnectionToClient connection = new NetworkConnectionToClient(42, 0);
NetworkServer.AddConnection(connection);
Assert.That(NetworkServer.connections.Count, Is.EqualTo(1));

View File

@ -36,7 +36,7 @@ public void TearDown()
public IEnumerator DestroyPlayerForConnectionTest()
{
GameObject player = new GameObject("testPlayer", typeof(NetworkIdentity));
NetworkConnectionToClient conn = new NetworkConnectionToClient(1);
NetworkConnectionToClient conn = new NetworkConnectionToClient(1, 0);
NetworkServer.AddPlayerForConnection(conn, player);
@ -55,7 +55,7 @@ public IEnumerator DestroyPlayerForConnectionTest()
public IEnumerator RemovePlayerForConnectionTest()
{
GameObject player = new GameObject("testPlayer", typeof(NetworkIdentity));
NetworkConnectionToClient conn = new NetworkConnectionToClient(1);
NetworkConnectionToClient conn = new NetworkConnectionToClient(1, 0);
NetworkServer.AddPlayerForConnection(conn, player);