Overhaul client latency calculations.

The ping/pong orders are replaced with a dedicated
(and much smaller) Ping packet that is handled
directly in the client and server Connection wrappers.

This allows clients to respond when the orders are
processed, instead of queuing the pong order to be
sent in the next frame (which added an extra 120ms
of unwanted latency).

The ping frequency has been raised to 1Hz, and pings
are now routed through the server events queue in
preparation for the future dynamic latency system.

The raw ping numbers are no longer sent to clients,
the server instead evaluates a single ConnectionQuality
value that in the future may be based on more than
just the ping times.
This commit is contained in:
Paul Chote
2021-09-20 22:44:49 +01:00
committed by abcdefg30
parent 67face8cf0
commit df798fb620
12 changed files with 161 additions and 150 deletions

View File

@@ -12,6 +12,7 @@
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Diagnostics;
using System.Globalization;
using System.IO;
using System.Linq;
@@ -76,6 +77,7 @@ namespace OpenRA.Server
ReplayRecorder recorder;
GameInformation gameInfo;
readonly List<GameInformation.Player> worldPlayers = new List<GameInformation.Player>();
Stopwatch pingUpdated = Stopwatch.StartNew();
public ServerState State
{
@@ -321,6 +323,11 @@ namespace OpenRA.Server
events.Add(new ConnectionPacketEvent(conn, frame, data));
}
internal void OnConnectionPing(Connection conn, int[] pingHistory)
{
events.Add(new ConnectionPingEvent(conn, pingHistory));
}
internal void OnConnectionDisconnect(Connection conn)
{
events.Add(new ConnectionDisconnectEvent(conn));
@@ -469,9 +476,6 @@ namespace OpenRA.Server
LobbyInfo.Clients.Add(client);
newConn.Validated = true;
var clientPing = new Session.ClientPing { Index = client.Index };
LobbyInfo.ClientPings.Add(clientPing);
Log.Write("server", "Client {0}: Accepted connection from {1}.", newConn.PlayerIndex, newConn.EndPoint);
if (client.Fingerprint != null)
@@ -487,9 +491,6 @@ namespace OpenRA.Server
if (Type != ServerType.Local)
SendMessage($"{client.Name} has joined the game.");
// Send initial ping
SendOrderTo(newConn, "Ping", Game.RunTime.ToString(CultureInfo.InvariantCulture));
if (Type == ServerType.Dedicated)
{
var motdFile = Path.Combine(Platform.SupportDir, "motd.txt");
@@ -893,37 +894,6 @@ namespace OpenRA.Server
case "Chat":
DispatchOrdersToClients(conn, 0, o.Serialize());
break;
case "Pong":
{
if (!OpenRA.Exts.TryParseInt64Invariant(o.TargetString, out var pingSent))
{
Log.Write("server", "Invalid order pong payload: {0}", o.TargetString);
break;
}
var client = GetClient(conn);
if (client == null)
return;
var pingFromClient = LobbyInfo.PingFromClient(client);
if (pingFromClient == null)
return;
var history = pingFromClient.LatencyHistory.ToList();
history.Add(Game.RunTime - pingSent);
// Cap ping history at 5 values (25 seconds)
if (history.Count > 5)
history.RemoveRange(0, history.Count - 5);
pingFromClient.Latency = history.Sum() / history.Count;
pingFromClient.LatencyJitter = (history.Max() - history.Min()) / 2;
pingFromClient.LatencyHistory = history.ToArray();
SyncClientPing();
break;
}
case "GameSaveTraitData":
{
@@ -995,12 +965,7 @@ namespace OpenRA.Server
foreach (var c in LobbyInfo.Clients)
{
if (c.Bot != null)
{
LobbyInfo.Clients.Remove(c);
var ping = LobbyInfo.PingFromClient(c);
if (ping != null)
LobbyInfo.ClientPings.Remove(ping);
}
else
c.Slot = null;
}
@@ -1033,7 +998,6 @@ namespace OpenRA.Server
SyncLobbyInfo();
SyncLobbyClients();
SyncClientPing();
break;
}
@@ -1041,6 +1005,36 @@ namespace OpenRA.Server
}
}
public void ReceivePing(Connection conn, int[] pingHistory)
{
// Levels set relative to the default order lag of 3 net ticks (360ms)
// TODO: Adjust this once dynamic lag is implemented
var latency = pingHistory.Sum() / pingHistory.Length;
var quality = latency < 240 ? Session.ConnectionQuality.Good :
latency < 360 ? Session.ConnectionQuality.Moderate :
Session.ConnectionQuality.Poor;
lock (LobbyInfo)
{
foreach (var c in LobbyInfo.Clients)
if (c.Index == conn.PlayerIndex || (c.Bot != null && c.BotControllerClientIndex == conn.PlayerIndex))
c.ConnectionQuality = quality;
// Update ping without forcing a full update
// Note that syncing pings doesn't trigger INotifySyncLobbyInfo
if (pingUpdated.ElapsedMilliseconds > 5000)
{
var nodes = new List<MiniYamlNode>();
foreach (var c in LobbyInfo.Clients)
nodes.Add(new MiniYamlNode($"ConnectionQuality@{c.Index}", FieldSaver.FormatValue(c.ConnectionQuality)));
DispatchServerOrdersToClients(Order.FromTargetString("SyncConnectionQuality", nodes.WriteToString(), true));
pingUpdated.Restart();
}
}
}
public Session.Client GetClient(Connection conn)
{
if (conn == null)
@@ -1068,7 +1062,6 @@ namespace OpenRA.Server
SendMessage($"{dropClient.Name}{suffix} has disconnected.");
LobbyInfo.Clients.RemoveAll(c => c.Index == toDrop.PlayerIndex);
LobbyInfo.ClientPings.RemoveAll(p => p.Index == toDrop.PlayerIndex);
// Client was the server admin
// TODO: Reassign admin for game in progress via an order
@@ -1137,6 +1130,10 @@ namespace OpenRA.Server
foreach (var t in serverTraits.WithInterface<INotifySyncLobbyInfo>())
t.LobbyInfoSynced(this);
// The full LobbyInfo includes ping info, so we can delay the next partial ping update
// TODO: Replace the special-case ping updates with more general LobbyInfo delta updates
pingUpdated.Restart();
}
}
@@ -1173,18 +1170,6 @@ namespace OpenRA.Server
}
}
public void SyncClientPing()
{
lock (LobbyInfo)
{
// TODO: Split this further into per client ping orders
var clientPings = LobbyInfo.ClientPings.Select(ping => ping.Serialize()).ToList();
// Note that syncing pings doesn't trigger INotifySyncLobbyInfo
DispatchServerOrdersToClients(Order.FromTargetString("SyncClientPings", clientPings.WriteToString(), true));
}
}
public void StartGame()
{
lock (LobbyInfo)
@@ -1362,6 +1347,23 @@ namespace OpenRA.Server
}
}
class ConnectionPingEvent : IServerEvent
{
readonly Connection connection;
readonly int[] pingHistory;
public ConnectionPingEvent(Connection connection, int[] pingHistory)
{
this.connection = connection;
this.pingHistory = pingHistory;
}
void IServerEvent.Invoke(Server server)
{
server.ReceivePing(connection, pingHistory);
}
}
class CallbackEvent : IServerEvent
{
readonly Action action;