Skip to content

Commit

Permalink
perf: allocation free send to observers
Browse files Browse the repository at this point in the history
  • Loading branch information
paulpach committed Mar 17, 2021
1 parent 502e798 commit e0e2c36
Show file tree
Hide file tree
Showing 3 changed files with 79 additions and 14 deletions.
10 changes: 10 additions & 0 deletions Assets/Mirage/Runtime/InterestManagement/GlobalInterestManager.cs
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,16 @@ namespace Mirage.InterestManagement
/// </summary>
public class GlobalInterestManager : InterestManager
{
public override void ForEach<T>(NetworkIdentity identity, T action)
{
// avoids allocations by looping directly on the HashSet
// which avoids boxing the IEnumerator
foreach (INetworkPlayer player in ServerObjectManager.Server.Players)
{
action.Run(player);
}
}

public override IReadOnlyCollection<INetworkPlayer> Observers(NetworkIdentity identity)
{
return ServerObjectManager.Server.Players;
Expand Down
32 changes: 32 additions & 0 deletions Assets/Mirage/Runtime/InterestManagement/InterestManager.cs
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,19 @@ namespace Mirage.InterestManagement
/// </summary>
public abstract class InterestManager : MonoBehaviour
{

/// <summary>
/// Action to execute per observer.
/// This is equivalent to <see cref="Action{T}"/>
/// but it allows for creating an allocation free "delegate"
/// This is used in the hot path.
/// </summary>
public interface PlayerAction
{
void Run(INetworkPlayer player);
}


public ServerObjectManager ServerObjectManager;

public void Start()
Expand All @@ -29,6 +42,25 @@ public void Start()
server.Authenticated.AddListener(OnAuthenticated);
}

/// <summary>
/// Executes an action for every player that can observe an object
/// allocation free.
/// </summary>
/// <typeparam name="T">The action type</typeparam>
/// <param name="identity">The object that we are observing</param>
/// <param name="action">The action to execute for every observer</param>
public virtual void ForEach<T>(NetworkIdentity identity, T action) where T : struct, PlayerAction
{
// this is the default implementation which works, but will allocate
// by boxing the IEnumerator.
// implementations of InterestManager should override this method
// and provide an allocation free alternative.
foreach (INetworkPlayer player in Observers(identity))
{
action.Run(player);
}
}

/// <summary>
/// Invoked when a player joins the server
/// It should show all objects relevant to that player
Expand Down
51 changes: 37 additions & 14 deletions Assets/Mirage/Runtime/NetworkIdentity.cs
Original file line number Diff line number Diff line change
Expand Up @@ -1040,6 +1040,28 @@ void SendUpdateVarsMessage()

static readonly List<INetworkPlayer> connectionsExcludeSelf = new List<INetworkPlayer>(100);


// this is basically a delegate for player.Send,
// but it is allocation free
readonly struct SendAction : InterestManager.PlayerAction
{
private readonly ArraySegment<byte> data;
private readonly int channel;
private readonly INetworkPlayer owner;

internal SendAction(ArraySegment<byte> data, int channel, INetworkPlayer owner)
{
this.data = data;
this.channel = channel;
this.owner = owner;
}
public void Run(INetworkPlayer player)
{
if (player != owner)
player.Send(data, channel);
}
}

/// <summary>
/// this is like SendToReady - but it doesn't check the ready flag on the connection.
/// this is used for ObjectDestroy messages.
Expand All @@ -1055,21 +1077,22 @@ internal void SendToObservers<T>(T msg, bool includeOwner = true, int channelId
if (Observers.Count == 0)
return;

if (includeOwner)
{
NetworkServer.SendToMany(Observers, msg, channelId);
}
else
using (PooledNetworkWriter writer = NetworkWriterPool.GetWriter())
{
connectionsExcludeSelf.Clear();
foreach (INetworkPlayer player in Observers)
{
if (ConnectionToClient != player)
{
connectionsExcludeSelf.Add(player);
}
}
NetworkServer.SendToMany(connectionsExcludeSelf, msg, channelId);
// pack message into byte[] once
MessagePacker.Pack(msg, writer);
var segment = writer.ToArraySegment();
int count = Observers.Count;

InterestManager interestManager = ServerObjectManager.InterestManager;

// just a delegate to call send, but allocation free
SendAction action = new SendAction(segment, channelId, includeOwner ? null : ConnectionToClient);


interestManager.ForEach(this, action);

NetworkDiagnostics.OnSend(msg, channelId, segment.Count, count);
}
}

Expand Down

0 comments on commit e0e2c36

Please sign in to comment.