perf: NetworkIdentity DirtyComponentsMask code removed entirely. OnSerialize now includes the component index as byte before serializing each component. Faster because we avoid GetDirtyComponentsMask() and GetSyncModeObserversMask() calculations. Increases allowed NetworkBehaviour components from 64 to 255. Bandwidth is now smaller if <8 components, and larger if >8 components. Code is significantly more simple.

This commit is contained in:
vis2k 2020-10-12 19:12:24 +02:00
parent 4c6791852e
commit 47dd2ef663
5 changed files with 38 additions and 190 deletions

View File

@ -242,11 +242,11 @@ public NetworkBehaviour[] NetworkBehaviours
void CreateNetworkBehavioursCache() void CreateNetworkBehavioursCache()
{ {
networkBehavioursCache = GetComponents<NetworkBehaviour>(); networkBehavioursCache = GetComponents<NetworkBehaviour>();
if (networkBehavioursCache.Length > 64) if (networkBehavioursCache.Length > byte.MaxValue)
{ {
Debug.LogError($"Only 64 NetworkBehaviour components are allowed for NetworkIdentity: {name} because of the dirtyComponentMask", this); Debug.LogError($"Only {byte.MaxValue} NetworkBehaviour components are allowed for NetworkIdentity: {name} because we send the index as byte in order to save bandwidth.", this);
// Log error once then resize array so that NetworkIdentity does not throw exceptions later // Log error once then resize array so that NetworkIdentity does not throw exceptions later
Array.Resize(ref networkBehavioursCache, 64); Array.Resize(ref networkBehavioursCache, byte.MaxValue);
} }
} }
@ -869,48 +869,41 @@ bool OnSerializeSafely(NetworkBehaviour comp, NetworkWriter writer, bool initial
/// <para>We pass dirtyComponentsMask into this function so that we can check if any Components are dirty before creating writers</para> /// <para>We pass dirtyComponentsMask into this function so that we can check if any Components are dirty before creating writers</para>
/// </summary> /// </summary>
/// <param name="initialState"></param> /// <param name="initialState"></param>
/// <param name="dirtyComponentsMask"></param>
/// <param name="ownerWriter"></param> /// <param name="ownerWriter"></param>
/// <param name="ownerWritten"></param> /// <param name="ownerWritten"></param>
/// <param name="observersWriter"></param> /// <param name="observersWriter"></param>
/// <param name="observersWritten"></param> /// <param name="observersWritten"></param>
internal void OnSerializeAllSafely(bool initialState, ulong dirtyComponentsMask, NetworkWriter ownerWriter, out int ownerWritten, NetworkWriter observersWriter, out int observersWritten) internal void OnSerializeAllSafely(bool initialState, NetworkWriter ownerWriter, out int ownerWritten, NetworkWriter observersWriter, out int observersWritten)
{ {
// clear 'written' variables // clear 'written' variables
ownerWritten = observersWritten = 0; ownerWritten = observersWritten = 0;
// dirtyComponentsMask should be changed before tyhis function is called // check if components are in byte.MaxRange just to be 100% sure
Debug.Assert(dirtyComponentsMask != 0UL, "OnSerializeAllSafely Should not be given a zero dirtyComponentsMask", this); // that we avoid overflows
NetworkBehaviour[] components = NetworkBehaviours;
if (components.Length > byte.MaxValue)
throw new IndexOutOfRangeException($"{name} has more than {byte.MaxValue} components. This is not supported.");
// calculate syncMode mask at runtime. this allows users to change // serialize all components
// component.syncMode while the game is running, which can be a huge for (int i = 0; i < components.Length; ++i)
// advantage over syncvar-based sync modes. e.g. if a player decides
// to share or not share his inventory, or to go invisible, etc.
//
// (this also lets the TestSynchronizingObjects test pass because
// otherwise if we were to cache it in Awake, then we would call
// GetComponents<NetworkBehaviour> before all the test behaviours
// were added)
ulong syncModeObserversMask = GetSyncModeObserversMask();
// write regular dirty mask for owner,
// writer 'dirty mask & syncMode==Everyone' for everyone else
// (WritePacked64 so we don't write full 8 bytes if we don't have to)
ownerWriter.WriteUInt64(dirtyComponentsMask);
observersWriter.WriteUInt64(dirtyComponentsMask & syncModeObserversMask);
foreach (NetworkBehaviour comp in NetworkBehaviours)
{ {
// is this component dirty? // is this component dirty?
// -> always serialize if initialState so all components are included in spawn packet // -> always serialize if initialState so all components are included in spawn packet
// -> note: IsDirty() is false if the component isn't dirty or sendInterval isn't elapsed yet // -> note: IsDirty() is false if the component isn't dirty or sendInterval isn't elapsed yet
NetworkBehaviour comp = components[i];
if (initialState || comp.IsDirty()) if (initialState || comp.IsDirty())
{ {
// Debug.Log("OnSerializeAllSafely: " + name + " -> " + comp.GetType() + " initial=" + initialState); // Debug.Log("OnSerializeAllSafely: " + name + " -> " + comp.GetType() + " initial=" + initialState);
// remember start position in case we need to copy it into
// observers writer too
int startPosition = ownerWriter.Position;
// write index as byte [0..255]
ownerWriter.WriteByte((byte)i);
// serialize into ownerWriter first // serialize into ownerWriter first
// (owner always gets everything!) // (owner always gets everything!)
int startPosition = ownerWriter.Position;
OnSerializeSafely(comp, ownerWriter, initialState); OnSerializeSafely(comp, ownerWriter, initialState);
++ownerWritten; ++ownerWritten;
@ -934,56 +927,6 @@ internal void OnSerializeAllSafely(bool initialState, ulong dirtyComponentsMask,
} }
} }
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 (comp.IsDirty())
{
dirtyComponentsMask |= 1UL << i;
}
}
return dirtyComponentsMask;
}
internal ulong GetInitialComponentsMask()
{
// 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;
}
/// <summary>
/// a mask that contains all the components with SyncMode.Observers
/// </summary>
/// <returns></returns>
internal ulong GetSyncModeObserversMask()
{
// loop through all components
ulong mask = 0UL;
NetworkBehaviour[] components = NetworkBehaviours;
for (int i = 0; i < NetworkBehaviours.Length; ++i)
{
NetworkBehaviour comp = components[i];
if (comp.syncMode == SyncMode.Observers)
{
mask |= 1UL << i;
}
}
return mask;
}
void OnDeserializeSafely(NetworkBehaviour comp, NetworkReader reader, bool initialState) void OnDeserializeSafely(NetworkBehaviour comp, NetworkReader reader, bool initialState)
{ {
// read header as 4 bytes and calculate this chunk's start+end // read header as 4 bytes and calculate this chunk's start+end
@ -1024,18 +967,16 @@ void OnDeserializeSafely(NetworkBehaviour comp, NetworkReader reader, bool initi
internal void OnDeserializeAllSafely(NetworkReader reader, bool initialState) internal void OnDeserializeAllSafely(NetworkReader reader, bool initialState)
{ {
// read component dirty mask // deserialize all components that were received
ulong dirtyComponentsMask = reader.ReadUInt64();
NetworkBehaviour[] components = NetworkBehaviours; NetworkBehaviour[] components = NetworkBehaviours;
// loop through all components and deserialize the dirty ones while (reader.Remaining > 0)
for (int i = 0; i < components.Length; ++i)
{ {
// is the dirty bit at position 'i' set to 1? // read & check index [0..255]
ulong dirtyBit = 1UL << i; byte index = reader.ReadByte();
if ((dirtyComponentsMask & dirtyBit) != 0L) if (index < components.Length)
{ {
OnDeserializeSafely(components[i], reader, initialState); // deserialize this component
OnDeserializeSafely(components[index], reader, initialState);
} }
} }
} }
@ -1135,13 +1076,7 @@ internal void ServerUpdate()
{ {
if (observers.Count > 0) if (observers.Count > 0)
{ {
ulong dirtyComponentsMask = GetDirtyComponentsMask(); SendUpdateVarsMessage();
// AnyComponentsDirty
if (dirtyComponentsMask != 0UL)
{
SendUpdateVarsMessage(dirtyComponentsMask);
}
} }
else else
{ {
@ -1151,13 +1086,13 @@ internal void ServerUpdate()
} }
} }
void SendUpdateVarsMessage(ulong dirtyComponentsMask) void SendUpdateVarsMessage()
{ {
// one writer for owner, one for observers // one writer for owner, one for observers
using (PooledNetworkWriter ownerWriter = NetworkWriterPool.GetWriter(), observersWriter = NetworkWriterPool.GetWriter()) using (PooledNetworkWriter ownerWriter = NetworkWriterPool.GetWriter(), observersWriter = NetworkWriterPool.GetWriter())
{ {
// serialize all the dirty components and send // serialize all the dirty components and send
OnSerializeAllSafely(false, dirtyComponentsMask, ownerWriter, out int ownerWritten, observersWriter, out int observersWritten); OnSerializeAllSafely(false, ownerWriter, out int ownerWritten, observersWriter, out int observersWritten);
if (ownerWritten > 0 || observersWritten > 0) if (ownerWritten > 0 || observersWritten > 0)
{ {
UpdateVarsMessage varsMessage = new UpdateVarsMessage UpdateVarsMessage varsMessage = new UpdateVarsMessage

View File

@ -857,8 +857,7 @@ static ArraySegment<byte> CreateSpawnMessagePayload(bool isOwner, NetworkIdentit
// serialize all components with initialState = true // serialize all components with initialState = true
// (can be null if has none) // (can be null if has none)
ulong dirtyComponentsMask = identity.GetInitialComponentsMask(); identity.OnSerializeAllSafely(true, ownerWriter, out int ownerWritten, observersWriter, out int observersWritten);
identity.OnSerializeAllSafely(true, dirtyComponentsMask, ownerWriter, out int ownerWritten, observersWriter, out int observersWritten);
// convert to ArraySegment to avoid reader allocations // convert to ArraySegment to avoid reader allocations
// (need to handle null case too) // (need to handle null case too)

View File

@ -472,10 +472,9 @@ public void ApplyPayload_SendsDataToNetworkBehaviourDeserialize()
serverPayloadBehaviour.direction = direction; serverPayloadBehaviour.direction = direction;
ulong dirtyMask = 1UL;
NetworkWriter ownerWriter = new NetworkWriter(1024); NetworkWriter ownerWriter = new NetworkWriter(1024);
NetworkWriter observersWriter = new NetworkWriter(1024); NetworkWriter observersWriter = new NetworkWriter(1024);
serverIdentity.OnSerializeAllSafely(true, dirtyMask, ownerWriter, out int ownerWritten, observersWriter, out int observersWritten); serverIdentity.OnSerializeAllSafely(true, ownerWriter, out int ownerWritten, observersWriter, out int observersWritten);
// check that Serialize was called // check that Serialize was called
Assert.That(onSerializeCalled, Is.EqualTo(1)); Assert.That(onSerializeCalled, Is.EqualTo(1));

View File

@ -584,10 +584,9 @@ public void OnSerializeAndDeserializeAllSafely()
// serialize all - should work even if compExc throws an exception // serialize all - should work even if compExc throws an exception
NetworkWriter ownerWriter = new NetworkWriter(1024); NetworkWriter ownerWriter = new NetworkWriter(1024);
NetworkWriter observersWriter = new NetworkWriter(1024); NetworkWriter observersWriter = new NetworkWriter(1024);
ulong mask = identity.GetInitialComponentsMask();
// error log because of the exception is expected // error log because of the exception is expected
LogAssert.ignoreFailingMessages = true; LogAssert.ignoreFailingMessages = true;
identity.OnSerializeAllSafely(true, mask, ownerWriter, out int ownerWritten, observersWriter, out int observersWritten); identity.OnSerializeAllSafely(true, ownerWriter, out int ownerWritten, observersWriter, out int observersWritten);
LogAssert.ignoreFailingMessages = false; LogAssert.ignoreFailingMessages = false;
// owner should have written all components // owner should have written all components
@ -645,8 +644,7 @@ public void OnSerializeAllSafelyShouldNotLogErrorsForTooManyComponents()
NetworkWriter ownerWriter = new NetworkWriter(1024); NetworkWriter ownerWriter = new NetworkWriter(1024);
NetworkWriter observersWriter = new NetworkWriter(1024); NetworkWriter observersWriter = new NetworkWriter(1024);
ulong mask = identity.GetInitialComponentsMask(); identity.OnSerializeAllSafely(true, ownerWriter, out int ownerWritten, observersWriter, out int observersWritten);
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 // Should still write with too mnay Components because NetworkBehavioursCache should handle the error
Assert.That(ownerWriter.Position, Is.GreaterThan(0)); Assert.That(ownerWriter.Position, Is.GreaterThan(0));
@ -658,14 +656,14 @@ public void OnSerializeAllSafelyShouldNotLogErrorsForTooManyComponents()
[Test] [Test]
public void CreatingNetworkBehavioursCacheShouldLogErrorForTooComponents() public void CreatingNetworkBehavioursCacheShouldLogErrorForTooComponents()
{ {
// add 65 components // add byte.MaxValue+1 components
for (int i = 0; i < 65; ++i) for (int i = 0; i < byte.MaxValue+1; ++i)
{ {
gameObject.AddComponent<SerializeTest1NetworkBehaviour>(); gameObject.AddComponent<SerializeTest1NetworkBehaviour>();
} }
// call NetworkBehaviours property to create the cache // call NetworkBehaviours property to create the cache
LogAssert.Expect(LogType.Error, new Regex("Only 64 NetworkBehaviour components are allowed for NetworkIdentity.+")); LogAssert.Expect(LogType.Error, new Regex($"Only {byte.MaxValue} NetworkBehaviour components are allowed for NetworkIdentity.+"));
_ = identity.NetworkBehaviours; _ = identity.NetworkBehaviours;
} }
@ -689,8 +687,7 @@ public void OnDeserializeSafelyShouldDetectAndHandleDeSerializationMismatch()
// serialize // serialize
NetworkWriter ownerWriter = new NetworkWriter(1024); NetworkWriter ownerWriter = new NetworkWriter(1024);
NetworkWriter observersWriter = new NetworkWriter(1024); NetworkWriter observersWriter = new NetworkWriter(1024);
ulong mask = identity.GetInitialComponentsMask(); identity.OnSerializeAllSafely(true, ownerWriter, out int ownerWritten, observersWriter, out int observersWritten);
identity.OnSerializeAllSafely(true, mask, ownerWriter, out int ownerWritten, observersWriter, out int observersWritten);
// reset component values // reset component values
comp1.value = 0; comp1.value = 0;
@ -965,66 +962,5 @@ public void HandleRpc()
NetworkIdentity.spawned.Clear(); NetworkIdentity.spawned.Clear();
RemoteCallHelper.RemoveDelegate(registeredHash); RemoteCallHelper.RemoveDelegate(registeredHash);
} }
[Test]
public void GetInitialComponentsMaskShouldReturn1BitPerNetworkBehaviour()
{
gameObject.AddComponent<MyTestComponent>();
gameObject.AddComponent<SerializeTest1NetworkBehaviour>();
gameObject.AddComponent<SerializeTest2NetworkBehaviour>();
ulong mask = identity.GetInitialComponentsMask();
// 1 + 2 + 4 = 7
Assert.That(mask, Is.EqualTo(7UL));
}
[Test]
public void GetInitialComponentsMaskShouldReturnZeroWhenNoNetworkBehaviours()
{
ulong mask = identity.GetInitialComponentsMask();
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));
}
} }
} }

View File

@ -123,8 +123,7 @@ public void TestSynchronizingObjects()
NetworkWriter ownerWriter = new NetworkWriter(1024); NetworkWriter ownerWriter = new NetworkWriter(1024);
// not really used in this Test // not really used in this Test
NetworkWriter observersWriter = new NetworkWriter(1024); NetworkWriter observersWriter = new NetworkWriter(1024);
ulong mask = identity1.GetInitialComponentsMask(); identity1.OnSerializeAllSafely(true, ownerWriter, out int ownerWritten, observersWriter, out int observersWritten);
identity1.OnSerializeAllSafely(true, mask, ownerWriter, out int ownerWritten, observersWriter, out int observersWritten);
// set up a "client" object // set up a "client" object
GameObject gameObject2 = new GameObject(); GameObject gameObject2 = new GameObject();
@ -138,25 +137,5 @@ public void TestSynchronizingObjects()
// check that the syncvars got updated // check that the syncvars got updated
Assert.That(player2.guild.name, Is.EqualTo("Back street boys"), "Data should be synchronized"); Assert.That(player2.guild.name, Is.EqualTo("Back street boys"), "Data should be synchronized");
} }
[Test]
public void TestSyncModeObserversMask()
{
GameObject gameObject1 = new GameObject();
NetworkIdentity identity = gameObject1.AddComponent<NetworkIdentity>();
MockPlayer player1 = gameObject1.AddComponent<MockPlayer>();
player1.syncInterval = 0;
MockPlayer player2 = gameObject1.AddComponent<MockPlayer>();
player2.syncInterval = 0;
MockPlayer player3 = gameObject1.AddComponent<MockPlayer>();
player3.syncInterval = 0;
// sync mode
player1.syncMode = SyncMode.Observers;
player2.syncMode = SyncMode.Owner;
player3.syncMode = SyncMode.Observers;
Assert.That(identity.GetSyncModeObserversMask(), Is.EqualTo(0b101));
}
} }
} }