Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

perf: DirtyObjects (V2) #3577

Draft
wants to merge 5 commits into
base: master
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
16 changes: 16 additions & 0 deletions Assets/Mirror/Core/NetworkBehaviour.cs
Original file line number Diff line number Diff line change
Expand Up @@ -155,18 +155,28 @@ protected void SetSyncVarHookGuard(ulong dirtyBit, bool value)
syncVarHookGuard &= ~dirtyBit;
}

// callback for both SyncObject and SyncVar dirty bit setters.
// called once it becomes dirty, not called again while already dirty.
// we only want to follow the .netIdentity memory indirection once.
[MethodImpl(MethodImplOptions.AggressiveInlining)]
void OnBecameDirty() => netIdentity.OnBecameDirty();

[MethodImpl(MethodImplOptions.AggressiveInlining)]
void SetSyncObjectDirtyBit(ulong dirtyBit)
{
bool clean = syncObjectDirtyBits == 0;
syncObjectDirtyBits |= dirtyBit;
if (clean) OnBecameDirty();
}

/// <summary>Set as dirty so that it's synced to clients again.</summary>
// these are masks, not bit numbers, ie. 110011b not '2' for 2nd bit.
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void SetSyncVarDirtyBit(ulong dirtyBit)
{
bool clean = syncObjectDirtyBits == 0;
syncVarDirtyBits |= dirtyBit;
if (clean) OnBecameDirty();
}

/// <summary>Set as dirty to trigger OnSerialize & send. Dirty bits are cleared after the send.</summary>
Expand All @@ -190,6 +200,12 @@ public void SetSyncVarDirtyBit(ulong dirtyBit)
// only check time if bits were dirty. this is more expensive.
NetworkTime.localTime - lastSyncTime >= syncInterval;

// check only dirty bits, ignoring sync interval.
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public bool IsDirtyPending() =>
// check bits first. this is basically free.
(syncVarDirtyBits | syncObjectDirtyBits) != 0UL;

/// <summary>Clears all the dirty bits that were set by SetSyncVarDirtyBit() (formally SetDirtyBits)</summary>
// automatically invoked when an update is sent for this object, but can
// be called manually as well.
Expand Down
118 changes: 58 additions & 60 deletions Assets/Mirror/Core/NetworkIdentity.cs
Original file line number Diff line number Diff line change
Expand Up @@ -23,14 +23,6 @@ namespace Mirror
// to everyone etc.
public enum Visibility { Default, ForceHidden, ForceShown }

public struct NetworkIdentitySerialization
{
// IMPORTANT: int tick avoids floating point inaccuracy over days/weeks
public int tick;
public NetworkWriter ownerWriter;
public NetworkWriter observersWriter;
}

/// <summary>NetworkIdentity identifies objects across the network.</summary>
[DisallowMultipleComponent]
// NetworkIdentity.Awake initializes all NetworkComponents.
Expand Down Expand Up @@ -203,19 +195,6 @@ internal set
[Tooltip("Visibility can overwrite interest management. ForceHidden can be useful to hide monsters while they respawn. ForceShown can be useful for score NetworkIdentities that should always broadcast to everyone in the world.")]
public Visibility visible = Visibility.Default;

// broadcasting serializes all entities around a player for each player.
// we don't want to serialize one entity twice in the same tick.
// so we cache the last serialization and remember the timestamp so we
// know which Update it was serialized.
// (timestamp is the same while inside Update)
// => this way we don't need to pool thousands of writers either.
// => way easier to store them per object
NetworkIdentitySerialization lastSerialization = new NetworkIdentitySerialization
{
ownerWriter = new NetworkWriter(),
observersWriter = new NetworkWriter()
};

// Keep track of all sceneIds to detect scene duplicates
static readonly Dictionary<ulong, NetworkIdentity> sceneIds =
new Dictionary<ulong, NetworkIdentity>();
Expand Down Expand Up @@ -827,19 +806,57 @@ internal void OnStopLocalPlayer()
}
}

// NetworkBehaviour OnBecameDirty calls NetworkIdentity callback with index
bool addedToDirtySpawned = false;
internal void OnBecameDirty()
{
// ensure either isServer or isClient are set.
// ensures tests are obvious. without proper setup, it should throw.
if (!isClient && !isServer)
Debug.LogWarning("NetworkIdentity.OnBecameDirty(): neither isClient nor isServer are true. Improper setup?");


if (isServer)
{
// only add to dirty spawned once.
// don't run the insertion twice.
if (!addedToDirtySpawned)
{
// insert into server dirty objects if not inserted yet
// TODO keep a bool so we don't insert all the time?

// only add if observed.
// otherwise no point in adding + iterating from broadcast.
if (observers.Count > 0)
{
NetworkServer.dirtySpawned.Add(this);
addedToDirtySpawned = true;
}
}
}
}

// build dirty mask for server owner & observers (= all dirty components).
// faster to do it in one iteration instead of iterating separately.
(ulong, ulong) ServerDirtyMasks(bool initialState)
(ulong, ulong, ulong) ServerDirtyMasks(bool initialState)
{
ulong ownerMask = 0;
ulong observerMask = 0;

// are any dirty but not ready to be sent yet?
// we need to know this because then we don't remove
// the NetworkIdentity from dirtyObjects just yet.
// otherwise if we remove before it was synced, we would miss a sync.
ulong dirtyPending = 0;

NetworkBehaviour[] components = NetworkBehaviours;
for (int i = 0; i < components.Length; ++i)
{
NetworkBehaviour component = components[i];

bool dirty = component.IsDirty();
bool pending = !dirty && component.IsDirtyPending();

ulong nthBit = (1u << i);

// owner needs to be considered for both SyncModes, because
Expand All @@ -849,7 +866,10 @@ internal void OnStopLocalPlayer()
// for delta, only for ServerToClient and only if dirty.
// ClientToServer comes from the owner client.
if (initialState || (component.syncDirection == SyncDirection.ServerToClient && dirty))
{
ownerMask |= nthBit;
if (pending) dirtyPending |= nthBit;
}

// observers need to be considered only in Observers mode
//
Expand All @@ -858,10 +878,13 @@ internal void OnStopLocalPlayer()
// SyncDirection is irrelevant, as both are broadcast to
// observers which aren't the owner.
if (component.syncMode == SyncMode.Observers && (initialState || dirty))
{
observerMask |= nthBit;
if (pending) dirtyPending |= nthBit;
}
}

return (ownerMask, observerMask);
return (ownerMask, observerMask, dirtyPending);
}

// build dirty mask for client.
Expand Down Expand Up @@ -906,7 +929,7 @@ internal static bool IsDirty(ulong mask, int index)
// check ownerWritten/observersWritten to know if anything was written
// We pass dirtyComponentsMask into this function so that we can check
// if any Components are dirty before creating writers
internal void SerializeServer(bool initialState, NetworkWriter ownerWriter, NetworkWriter observersWriter)
internal void SerializeServer(bool initialState, NetworkWriter ownerWriter, NetworkWriter observersWriter, out bool pendingDirty)
{
// ensure NetworkBehaviours are valid before usage
ValidateComponents();
Expand All @@ -919,7 +942,7 @@ internal void SerializeServer(bool initialState, NetworkWriter ownerWriter, Netw
// instead of writing a 1 byte index per component,
// we limit components to 64 bits and write one ulong instead.
// the ulong is also varint compressed for minimum bandwidth.
(ulong ownerMask, ulong observerMask) = ServerDirtyMasks(initialState);
(ulong ownerMask, ulong observerMask, ulong pendingMask) = ServerDirtyMasks(initialState);

// if nothing dirty, then don't even write the mask.
// otherwise, every unchanged object would send a 1 byte dirty mask!
Expand Down Expand Up @@ -976,6 +999,16 @@ internal void SerializeServer(bool initialState, NetworkWriter ownerWriter, Netw
}
}
}

// are any dirty but not ready to be sent yet?
// we need to know this because then we don't remove
// the NetworkIdentity from dirtyObjects just yet.
// otherwise if we remove before it was synced, we would miss a sync.
pendingDirty = pendingMask != 0;

// if none are still pending, this will be removed from dirtyObjects.
// in that case, clear our flag (the flag is only for performance).
if (!pendingDirty) addedToDirtySpawned = false;
}

// serialize components into writer on the client.
Expand Down Expand Up @@ -1102,41 +1135,6 @@ internal void DeserializeClient(NetworkReader reader, bool initialState)
}
}

// get cached serialization for this tick (or serialize if none yet).
// IMPORTANT: int tick avoids floating point inaccuracy over days/weeks.
// calls SerializeServer, so this function is to be called on server.
internal NetworkIdentitySerialization GetServerSerializationAtTick(int tick)
{
// only rebuild serialization once per tick. reuse otherwise.
// except for tests, where Time.frameCount never increases.
// so during tests, we always rebuild.
// (otherwise [SyncVar] changes would never be serialized in tests)
//
// NOTE: != instead of < because int.max+1 overflows at some point.
if (lastSerialization.tick != tick
#if UNITY_EDITOR
|| !Application.isPlaying
#endif
)
{
// reset
lastSerialization.ownerWriter.Position = 0;
lastSerialization.observersWriter.Position = 0;

// serialize
SerializeServer(false,
lastSerialization.ownerWriter,
lastSerialization.observersWriter);

// set tick
lastSerialization.tick = tick;
//Debug.Log($"{name} (netId={netId}) serialized for tick={tickTimeStamp}");
}

// return it
return lastSerialization;
}

internal void AddObserver(NetworkConnectionToClient conn)
{
if (observers.ContainsKey(conn.connectionId))
Expand Down