Skip to content

Commit

Permalink
perf: faster component serialization (#430)
Browse files Browse the repository at this point in the history
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. Code is significantly more simple. (#2331)

* 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.

* Update Assets/Mirror/Runtime/NetworkIdentity.cs

Co-authored-by: James Frowen <jamesfrowendev@gmail.com>

Co-authored-by: James Frowen <jamesfrowendev@gmail.com>
  • Loading branch information
Lymdun and James-Frowen committed Oct 24, 2020
1 parent 24f870f commit b675027
Show file tree
Hide file tree
Showing 4 changed files with 28 additions and 135 deletions.
112 changes: 24 additions & 88 deletions Assets/Mirror/Runtime/NetworkIdentity.cs
Original file line number Diff line number Diff line change
Expand Up @@ -207,8 +207,8 @@ public NetworkBehaviour[] NetworkBehaviours

NetworkBehaviour[] components = GetComponentsInChildren<NetworkBehaviour>(true);

if (components.Length > 64)
throw new InvalidOperationException("Only 64 NetworkBehaviour per gameobject allowed");
if (components.Length > byte.MaxValue)
throw new InvalidOperationException("Only 255 NetworkBehaviour per gameobject allowed");

networkBehavioursCache = components;
return networkBehavioursCache;
Expand Down Expand Up @@ -817,45 +817,37 @@ void 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="observersWriter"></param>
internal (int ownerWritten, int observersWritten) OnSerializeAllSafely(bool initialState, NetworkWriter ownerWriter, NetworkWriter observersWriter)
{
ulong dirtyComponentsMask = initialState ? GetIntialComponentsMask() : GetDirtyComponentsMask();

// 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();

int ownerWritten = 0;
int observersWritten = 0;

// 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.WritePackedUInt64(dirtyComponentsMask);
observersWriter.WritePackedUInt64(dirtyComponentsMask & syncModeObserversMask);
// check if components are in byte.MaxRange just to be 100% sure
// that we avoid overflows
NetworkBehaviour[] components = NetworkBehaviours;

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())
{
if (logger.LogEnabled()) logger.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;

Expand All @@ -881,28 +873,8 @@ void OnSerializeSafely(NetworkBehaviour comp, NetworkWriter writer, bool initial
return (ownerWritten, observersWritten);
}

private 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;
}

/// <summary>
/// Determines if there are changes in any component that have not
/// been synchronized yet. Probably due to not meeting the syncInterval
/// </summary>
/// <returns></returns>
// Determines if there are changes in any component that have not
// been synchronized yet. Probably due to not meeting the syncInterval
internal bool StillDirty()
{
foreach (NetworkBehaviour behaviour in NetworkBehaviours)
Expand All @@ -913,35 +885,6 @@ internal bool StillDirty()
return false;
}

private ulong GetIntialComponentsMask()
{
// set a bit for every behaviour
return NetworkBehaviours.Length == 64
? ulong.MaxValue
: (1UL << NetworkBehaviours.Length) - 1;
}

/// <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)
{
// call OnDeserialize with a temporary reader, so that the
Expand All @@ -961,20 +904,17 @@ void OnDeserializeSafely(NetworkBehaviour comp, NetworkReader reader, bool initi
internal void OnDeserializeAllSafely(NetworkReader reader, bool initialState)
{
// hack needed so that we can deserialize gameobjects and NI

NetworkClient.Current = Client;
// read component dirty mask
ulong dirtyComponentsMask = reader.ReadPackedUInt64();

// 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.Position < reader.Length)
{
// 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);
}
}

Expand Down Expand Up @@ -1323,10 +1263,6 @@ internal void ServerUpdate()
}
}

/// <summary>
/// return true if the object is successfully synchronized
/// </summary>
/// <returns></returns>
void SendUpdateVarsMessage()
{
// one writer for owner, one for observers
Expand Down
2 changes: 1 addition & 1 deletion Assets/Mirror/Runtime/NetworkServer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -411,7 +411,7 @@ internal void Update()

if (identity.StillDirty())
DirtyObjectsTmp.Add(identity);
}
}
}

DirtyObjects.Clear();
Expand Down
29 changes: 3 additions & 26 deletions Assets/Tests/Editor/NetworkIdentityTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -559,7 +559,7 @@ public void OnCheckObserverFalse()
}

[Test]
public void OnSerializeAndDeserializeAllSafely()
public void OnSerializeAllSafely()
{
// create a networkidentity with our test components
SerializeTest1NetworkBehaviour comp1 = gameObject.AddComponent<SerializeTest1NetworkBehaviour>();
Expand All @@ -582,38 +582,15 @@ public void OnSerializeAndDeserializeAllSafely()
{
identity.OnSerializeAllSafely(true, ownerWriter, observersWriter);
});

// reset component values
comp1.value = 0;
comp2.value = null;

// deserialize all for owner - should work even if compExc throws an exception
var reader = new NetworkReader(ownerWriter.ToArray());

Assert.Throws<Exception>(() =>
{
identity.OnDeserializeAllSafely(reader, true);
});

// reset component values
comp1.value = 0;
comp2.value = null;

// deserialize all for observers - should propagate exceptions
reader = new NetworkReader(observersWriter.ToArray());
Assert.Throws<Exception>(() =>
{
identity.OnDeserializeAllSafely(reader, true);
});
}

// OnSerializeAllSafely supports at max 64 components, because our
// dirty mask is ulong and can only handle so many bits.
[Test]
public void NoMoreThan64Components()
{
// 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>();
}
Expand Down
20 changes: 0 additions & 20 deletions Assets/Tests/Editor/SyncVarTest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -139,26 +139,6 @@ public void TestSynchronizingObjects()
Assert.That(player2.guild.name, Is.EqualTo("Back street boys"), "Data should be synchronized");
}

[Test]
public void TestSyncModeObserversMask()
{
var gameObject1 = new GameObject("player", typeof(NetworkIdentity));
NetworkIdentity identity = gameObject1.GetComponent<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));
}

[Test]
public void SetNetworkIdentitySyncvar()
{
Expand Down

0 comments on commit b675027

Please sign in to comment.