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()
{
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
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>
/// </summary>
/// <param name="initialState"></param>
/// <param name="dirtyComponentsMask"></param>
/// <param name="ownerWriter"></param>
/// <param name="ownerWritten"></param>
/// <param name="observersWriter"></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
ownerWritten = observersWritten = 0;
// dirtyComponentsMask should be changed before tyhis function is called
Debug.Assert(dirtyComponentsMask != 0UL, "OnSerializeAllSafely Should not be given a zero dirtyComponentsMask", this);
// check if components are in byte.MaxRange just to be 100% sure
// 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
// component.syncMode while the game is running, which can be a huge
// 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)
// serialize all components
for (int i = 0; i < components.Length; ++i)
{
// is this component dirty?
// -> 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
NetworkBehaviour comp = components[i];
if (initialState || comp.IsDirty())
{
// 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
// (owner always gets everything!)
int startPosition = ownerWriter.Position;
OnSerializeSafely(comp, ownerWriter, initialState);
++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)
{
// 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)
{
// read component dirty mask
ulong dirtyComponentsMask = reader.ReadUInt64();
// deserialize all components that were received
NetworkBehaviour[] components = NetworkBehaviours;
// loop through all components and deserialize the dirty ones
for (int i = 0; i < components.Length; ++i)
while (reader.Remaining > 0)
{
// is the dirty bit at position 'i' set to 1?
ulong dirtyBit = 1UL << i;
if ((dirtyComponentsMask & dirtyBit) != 0L)
// read & check index [0..255]
byte index = reader.ReadByte();
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)
{
ulong dirtyComponentsMask = GetDirtyComponentsMask();
// AnyComponentsDirty
if (dirtyComponentsMask != 0UL)
{
SendUpdateVarsMessage(dirtyComponentsMask);
}
SendUpdateVarsMessage();
}
else
{
@ -1151,13 +1086,13 @@ internal void ServerUpdate()
}
}
void SendUpdateVarsMessage(ulong dirtyComponentsMask)
void SendUpdateVarsMessage()
{
// 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);
OnSerializeAllSafely(false, ownerWriter, out int ownerWritten, observersWriter, out int observersWritten);
if (ownerWritten > 0 || observersWritten > 0)
{
UpdateVarsMessage varsMessage = new UpdateVarsMessage

View File

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

View File

@ -472,10 +472,9 @@ public void ApplyPayload_SendsDataToNetworkBehaviourDeserialize()
serverPayloadBehaviour.direction = direction;
ulong dirtyMask = 1UL;
NetworkWriter ownerWriter = 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
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
NetworkWriter ownerWriter = new NetworkWriter(1024);
NetworkWriter observersWriter = new NetworkWriter(1024);
ulong mask = identity.GetInitialComponentsMask();
// error log because of the exception is expected
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;
// owner should have written all components
@ -645,8 +644,7 @@ public void OnSerializeAllSafelyShouldNotLogErrorsForTooManyComponents()
NetworkWriter ownerWriter = new NetworkWriter(1024);
NetworkWriter observersWriter = new NetworkWriter(1024);
ulong mask = identity.GetInitialComponentsMask();
identity.OnSerializeAllSafely(true, mask, ownerWriter, out int ownerWritten, observersWriter, out int observersWritten);
identity.OnSerializeAllSafely(true, 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));
@ -658,14 +656,14 @@ public void OnSerializeAllSafelyShouldNotLogErrorsForTooManyComponents()
[Test]
public void CreatingNetworkBehavioursCacheShouldLogErrorForTooComponents()
{
// add 65 components
for (int i = 0; i < 65; ++i)
// add byte.MaxValue+1 components
for (int i = 0; i < byte.MaxValue+1; ++i)
{
gameObject.AddComponent<SerializeTest1NetworkBehaviour>();
}
// 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;
}
@ -689,8 +687,7 @@ public void OnDeserializeSafelyShouldDetectAndHandleDeSerializationMismatch()
// serialize
NetworkWriter ownerWriter = new NetworkWriter(1024);
NetworkWriter observersWriter = new NetworkWriter(1024);
ulong mask = identity.GetInitialComponentsMask();
identity.OnSerializeAllSafely(true, mask, ownerWriter, out int ownerWritten, observersWriter, out int observersWritten);
identity.OnSerializeAllSafely(true, ownerWriter, out int ownerWritten, observersWriter, out int observersWritten);
// reset component values
comp1.value = 0;
@ -965,66 +962,5 @@ public void HandleRpc()
NetworkIdentity.spawned.Clear();
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);
// not really used in this Test
NetworkWriter observersWriter = new NetworkWriter(1024);
ulong mask = identity1.GetInitialComponentsMask();
identity1.OnSerializeAllSafely(true, mask, ownerWriter, out int ownerWritten, observersWriter, out int observersWritten);
identity1.OnSerializeAllSafely(true, ownerWriter, out int ownerWritten, observersWriter, out int observersWritten);
// set up a "client" object
GameObject gameObject2 = new GameObject();
@ -138,25 +137,5 @@ public void TestSynchronizingObjects()
// check that the syncvars got updated
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));
}
}
}