Skip to content

Commit

Permalink
perf: Adding dirty check before update sync var (#1702)
Browse files Browse the repository at this point in the history
* adding dirty check before update sync var

* Moving creating spawn payload to its own function

* updating tests

* adding tests for behaviour mask

* adding comment

* removing un-needed cast
  • Loading branch information
James-Frowen committed Apr 16, 2020
1 parent 17f8e97 commit 58219c8
Show file tree
Hide file tree
Showing 5 changed files with 176 additions and 67 deletions.
123 changes: 73 additions & 50 deletions Assets/Mirror/Runtime/NetworkIdentity.cs
Original file line number Diff line number Diff line change
Expand Up @@ -795,17 +795,16 @@ bool OnSerializeSafely(NetworkBehaviour comp, NetworkWriter writer, bool initial
return result;
}

// serialize all components (or only dirty ones if not initial state)
// serialize all components using dirtyComponentsMask
// -> check ownerWritten/observersWritten to know if anything was written
internal void OnSerializeAllSafely(bool initialState, NetworkWriter ownerWriter, out int ownerWritten, NetworkWriter observersWriter, out int observersWritten)
// We pass dirtyComponentsMask into this function so that we can check if any Components are dirty before creating writers
internal void OnSerializeAllSafely(bool initialState, ulong dirtyComponentsMask, NetworkWriter ownerWriter, out int ownerWritten, NetworkWriter observersWriter, out int observersWritten)
{
// clear 'written' variables
ownerWritten = observersWritten = 0;

ulong dirtyComponentsMask = GetDirtyMask(initialState);

if (dirtyComponentsMask == 0L)
return;
// dirtyComponentsMask should be changed before tyhis function is called
Debug.Assert(dirtyComponentsMask != 0UL, "OnSerializeAllSafely Should not be given a zero dirtyComponentsMask", this);

// calculate syncMode mask at runtime. this allows users to change
// component.syncMode while the game is running, which can be a huge
Expand Down Expand Up @@ -859,22 +858,34 @@ internal void OnSerializeAllSafely(bool initialState, NetworkWriter ownerWriter,
}
}

internal ulong GetDirtyMask(bool initialState)
internal ulong GetDirtyComponentsMask()
{
// loop through all components only once and then write dirty+payload into the writer afterwards
ulong dirtyComponentsMask = 0L;
NetworkBehaviour[] components = NetworkBehaviours;
for (int i = 0; i < components.Length; ++i)
{
NetworkBehaviour comp = components[i];
if (initialState || comp.IsDirty())
if (comp.IsDirty())
{
dirtyComponentsMask |= (ulong)(1L << i);
dirtyComponentsMask |= 1UL << i;
}
}

return dirtyComponentsMask;
}
internal ulong GetIntialComponentsMask()
{
// loop through all components only once and then write dirty+payload into the writer afterwards
ulong dirtyComponentsMask = 0UL;
for (int i = 0; i < NetworkBehaviours.Length; ++i)
{
dirtyComponentsMask |= 1UL << i;
}

return dirtyComponentsMask;
}


// a mask that contains all the components with SyncMode.Observers
internal ulong GetSyncModeObserversMask()
Expand Down Expand Up @@ -937,7 +948,7 @@ internal void OnDeserializeAllSafely(NetworkReader reader, bool initialState)
for (int i = 0; i < components.Length; ++i)
{
// is the dirty bit at position 'i' set to 1?
ulong dirtyBit = (ulong)(1L << i);
ulong dirtyBit = 1UL << i;
if ((dirtyComponentsMask & dirtyBit) != 0L)
{
OnDeserializeSafely(components[i], reader, initialState);
Expand Down Expand Up @@ -1273,47 +1284,12 @@ internal void ServerUpdate()
{
if (observers != null && observers.Count > 0)
{
// one writer for owner, one for observers
using (PooledNetworkWriter ownerWriter = NetworkWriterPool.GetWriter(), observersWriter = NetworkWriterPool.GetWriter())
ulong dirtyComponentsMask = GetDirtyComponentsMask();

// AnyComponentsDirty
if (dirtyComponentsMask != 0UL)
{
// serialize all the dirty components and send (if any were dirty)
OnSerializeAllSafely(false, ownerWriter, out int ownerWritten, observersWriter, out int observersWritten);
if (ownerWritten > 0 || observersWritten > 0)
{
UpdateVarsMessage varsMessage = new UpdateVarsMessage
{
netId = netId
};

// send ownerWriter to owner
// (only if we serialized anything for owner)
// (only if there is a connection (e.g. if not a monster),
// and if connection is ready because we use SendToReady
// below too)
if (ownerWritten > 0)
{
varsMessage.payload = ownerWriter.ToArraySegment();
if (connectionToClient != null && connectionToClient.isReady)
NetworkServer.SendToClientOfPlayer(this, varsMessage);
}

// send observersWriter to everyone but owner
// (only if we serialized anything for observers)
if (observersWritten > 0)
{
varsMessage.payload = observersWriter.ToArraySegment();
NetworkServer.SendToReady(this, varsMessage, false);
}

// clear dirty bits only for the components that we serialized
// DO NOT clean ALL component's dirty bits, because
// components can have different syncIntervals and we don't
// want to reset dirty bits for the ones that were not
// synced yet.
// (we serialized only the IsDirty() components, or all of
// them if initialState. clearing the dirty ones is enough.)
ClearDirtyComponentsDirtyBits();
}
SendUpdateVarsMessage(dirtyComponentsMask);
}
}
else
Expand All @@ -1324,6 +1300,53 @@ internal void ServerUpdate()
}
}

void SendUpdateVarsMessage(ulong dirtyComponentsMask)
{
// one writer for owner, one for observers
using (PooledNetworkWriter ownerWriter = NetworkWriterPool.GetWriter(), observersWriter = NetworkWriterPool.GetWriter())
{
// serialize all the dirty components and send
OnSerializeAllSafely(false, dirtyComponentsMask, ownerWriter, out int ownerWritten, observersWriter, out int observersWritten);
if (ownerWritten > 0 || observersWritten > 0)
{
UpdateVarsMessage varsMessage = new UpdateVarsMessage
{
netId = netId
};

// send ownerWriter to owner
// (only if we serialized anything for owner)
// (only if there is a connection (e.g. if not a monster),
// and if connection is ready because we use SendToReady
// below too)
if (ownerWritten > 0)
{
varsMessage.payload = ownerWriter.ToArraySegment();
if (connectionToClient != null && connectionToClient.isReady)
NetworkServer.SendToClientOfPlayer(this, varsMessage);
}

// send observersWriter to everyone but owner
// (only if we serialized anything for observers)
if (observersWritten > 0)
{
varsMessage.payload = observersWriter.ToArraySegment();
NetworkServer.SendToReady(this, varsMessage, false);
}

// clear dirty bits only for the components that we serialized
// DO NOT clean ALL component's dirty bits, because
// components can have different syncIntervals and we don't
// want to reset dirty bits for the ones that were not
// synced yet.
// (we serialized only the IsDirty() components, or all of
// them if initialState. clearing the dirty ones is enough.)
ClearDirtyComponentsDirtyBits();
}
}
}


// clear all component's dirty bits no matter what
internal void ClearAllComponentsDirtyBits()
{
Expand Down
43 changes: 31 additions & 12 deletions Assets/Mirror/Runtime/NetworkServer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -930,36 +930,55 @@ internal static void SendSpawnMessage(NetworkIdentity identity, NetworkConnectio
// one writer for owner, one for observers
using (PooledNetworkWriter ownerWriter = NetworkWriterPool.GetWriter(), observersWriter = NetworkWriterPool.GetWriter())
{
// serialize all components with initialState = true
// (can be null if has none)
identity.OnSerializeAllSafely(true, ownerWriter, out int ownerWritten, observersWriter, out int observersWritten);
bool isOwner = identity.connectionToClient == conn;

// convert to ArraySegment to avoid reader allocations
// (need to handle null case too)
ArraySegment<byte> ownerSegment = ownerWritten > 0 ? ownerWriter.ToArraySegment() : default;
ArraySegment<byte> observersSegment = observersWritten > 0 ? observersWriter.ToArraySegment() : default;
ArraySegment<byte> payload = CreateSpawnMessagePayload(isOwner, identity, ownerWriter, observersWriter);

SpawnMessage msg = new SpawnMessage
{
netId = identity.netId,
isLocalPlayer = conn.identity == identity,
isOwner = identity.connectionToClient == conn,
isOwner = isOwner,
sceneId = identity.sceneId,
assetId = identity.assetId,
// use local values for VR support
position = identity.transform.localPosition,
rotation = identity.transform.localRotation,
scale = identity.transform.localScale
scale = identity.transform.localScale,

payload = payload,
};

// use owner segment if 'conn' owns this identity, otherwise
// use observers segment
msg.payload = msg.isOwner ? ownerSegment : observersSegment;

conn.Send(msg);
}
}

static ArraySegment<byte> CreateSpawnMessagePayload(bool isOwner, NetworkIdentity identity, PooledNetworkWriter ownerWriter, PooledNetworkWriter observersWriter)
{
// Only call OnSerializeAllSafely if there are NetworkBehaviours
if (identity.NetworkBehaviours.Length == 0)
{
return default;
}

// serialize all components with initialState = true
// (can be null if has none)
ulong dirtyComponentsMask = identity.GetIntialComponentsMask();
identity.OnSerializeAllSafely(true, dirtyComponentsMask, ownerWriter, out int ownerWritten, observersWriter, out int observersWritten);

// convert to ArraySegment to avoid reader allocations
// (need to handle null case too)
ArraySegment<byte> ownerSegment = ownerWritten > 0 ? ownerWriter.ToArraySegment() : default;
ArraySegment<byte> observersSegment = observersWritten > 0 ? observersWriter.ToArraySegment() : default;

// use owner segment if 'conn' owns this identity, otherwise
// use observers segment
ArraySegment<byte> payload = isOwner ? ownerSegment : observersSegment;

return payload;
}

/// <summary>
/// This destroys all the player objects associated with a NetworkConnections on a server.
/// <para>This is used when a client disconnects, to remove the players for that client. This also destroys non-player objects that have client authority set for this connection.</para>
Expand Down
71 changes: 68 additions & 3 deletions Assets/Mirror/Tests/Editor/NetworkIdentityTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -861,9 +861,10 @@ public void OnSerializeAndDeserializeAllSafely()
// serialize all - should work even if compExc throws an exception
NetworkWriter ownerWriter = new NetworkWriter();
NetworkWriter observersWriter = new NetworkWriter();
ulong mask = identity.GetIntialComponentsMask();
// error log because of the exception is expected
LogAssert.ignoreFailingMessages = true;
identity.OnSerializeAllSafely(true, ownerWriter, out int ownerWritten, observersWriter, out int observersWritten);
identity.OnSerializeAllSafely(true, mask, ownerWriter, out int ownerWritten, observersWriter, out int observersWritten);
LogAssert.ignoreFailingMessages = false;

// owner should have written all components
Expand Down Expand Up @@ -921,7 +922,8 @@ public void OnSerializeAllSafelyShouldNotLogErrorsForTooManyComponents()
NetworkWriter ownerWriter = new NetworkWriter();
NetworkWriter observersWriter = new NetworkWriter();

identity.OnSerializeAllSafely(true, ownerWriter, out int ownerWritten, observersWriter, out int observersWritten);
ulong mask = identity.GetIntialComponentsMask();
identity.OnSerializeAllSafely(true, mask, ownerWriter, out int ownerWritten, observersWriter, out int observersWritten);

// Should still write with too mnay Components because NetworkBehavioursCache should handle the error
Assert.That(ownerWriter.Position, Is.GreaterThan(0));
Expand Down Expand Up @@ -964,7 +966,8 @@ public void OnDeserializeSafelyShouldDetectAndHandleDeSerializationMismatch()
// serialize
NetworkWriter ownerWriter = new NetworkWriter();
NetworkWriter observersWriter = new NetworkWriter();
identity.OnSerializeAllSafely(true, ownerWriter, out int ownerWritten, observersWriter, out int observersWritten);
ulong mask = identity.GetIntialComponentsMask();
identity.OnSerializeAllSafely(true, mask, ownerWriter, out int ownerWritten, observersWriter, out int observersWritten);

// reset component values
comp1.value = 0;
Expand Down Expand Up @@ -1675,5 +1678,67 @@ public void RebuildObserversReturnsIfNull()
identity.RebuildObservers(true);
Assert.That(identity.observers, Is.Null);
}

[Test]
public void GetIntialComponentsMaskShouldReturn1BitPerNetworkBehaviour()
{
gameObject.AddComponent<MyTestComponent>();
gameObject.AddComponent<SerializeTest1NetworkBehaviour>();
gameObject.AddComponent<SerializeTest2NetworkBehaviour>();

ulong mask = identity.GetIntialComponentsMask();

// 1 + 2 + 4 = 7
Assert.That(mask, Is.EqualTo(7UL));
}

[Test]
public void GetIntialComponentsMaskShouldReturnZeroWhenNoNetworkBehaviours()
{
ulong mask = identity.GetIntialComponentsMask();

Assert.That(mask, Is.EqualTo(0UL));
}

[Test]
public void GetDirtyComponentsMaskShouldReturn1BitOnlyForDirtyComponents()
{
MyTestComponent comp1 = gameObject.AddComponent<MyTestComponent>();
SerializeTest1NetworkBehaviour comp2 = gameObject.AddComponent<SerializeTest1NetworkBehaviour>();
SerializeTest2NetworkBehaviour comp3 = gameObject.AddComponent<SerializeTest2NetworkBehaviour>();


// mark comps 1 and 3 as dirty

comp1.syncInterval = 0;
comp3.syncInterval = 0;

comp1.SetDirtyBit(1UL);
comp2.ClearAllDirtyBits();
comp3.SetDirtyBit(1UL);


ulong mask = identity.GetDirtyComponentsMask();

// 1 + 4 = 5
Assert.That(mask, Is.EqualTo(5UL));
}

[Test]
public void GetDirtyComponentsMaskShouldReturnZeroWhenNoDirtyComponents()
{
MyTestComponent comp1 = gameObject.AddComponent<MyTestComponent>();
SerializeTest1NetworkBehaviour comp2 = gameObject.AddComponent<SerializeTest1NetworkBehaviour>();
SerializeTest2NetworkBehaviour comp3 = gameObject.AddComponent<SerializeTest2NetworkBehaviour>();

comp1.ClearAllDirtyBits();
comp2.ClearAllDirtyBits();
comp3.ClearAllDirtyBits();

ulong mask = identity.GetDirtyComponentsMask();

Assert.That(mask, Is.EqualTo(0UL));
}

}
}
3 changes: 2 additions & 1 deletion Assets/Mirror/Tests/Editor/SyncVarTest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -123,7 +123,8 @@ public void TestSynchronizingObjects()
NetworkWriter ownerWriter = new NetworkWriter();
// not really used in this Test
NetworkWriter observersWriter = new NetworkWriter();
identity1.OnSerializeAllSafely(true, ownerWriter, out int ownerWritten, observersWriter, out int observersWritten);
ulong mask = identity1.GetIntialComponentsMask();
identity1.OnSerializeAllSafely(true, mask, ownerWriter, out int ownerWritten, observersWriter, out int observersWritten);

// set up a "client" object
GameObject gameObject2 = new GameObject();
Expand Down
3 changes: 2 additions & 1 deletion Assets/Mirror/Tests/Editor/SyncVarVirtualTest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,8 @@ private void SyncValuesWithClient()
NetworkWriter ownerWriter = new NetworkWriter();
// not really used in this Test
NetworkWriter observersWriter = new NetworkWriter();
netIdServer.OnSerializeAllSafely(true, ownerWriter, out int ownerWritten, observersWriter, out int observersWritten);
ulong mask = netIdServer.GetIntialComponentsMask();
netIdServer.OnSerializeAllSafely(true, mask, ownerWriter, out int ownerWritten, observersWriter, out int observersWritten);


// apply all the data from the server object
Expand Down

0 comments on commit 58219c8

Please sign in to comment.