mirror of
https://github.com/Empostor/Empostor.git
synced 2026-10-19 15:51:39 +00:00
Optimize
This commit is contained in:
1 parent
50a159f8cf
commit
a4de6a83d9
4 files changed
+65
-8
No files matched your search
@@ -9,7 +9,11 @@ namespace Empostor.Server
|
||||
{
|
||||
public static void AddEventPools(this IServiceCollection services)
|
||||
{
|
||||
services.TryAddSingleton<ObjectPoolProvider>(new DefaultObjectPoolProvider());
|
||||
// Larger object-pool retention so high-frequency packet allocation
|
||||
// (MessageReader, event objects) is served from the pool instead of
|
||||
// hammering the GC under load (e.g. 40+ players).
|
||||
services.TryAddSingleton<ObjectPoolProvider>(
|
||||
new DefaultObjectPoolProvider { MaximumRetained = 512 });
|
||||
|
||||
services.AddSingleton(serviceProvider =>
|
||||
{
|
||||
|
||||
@@ -4,6 +4,7 @@ using Empostor.Api;
|
||||
using Empostor.Api.Config;
|
||||
using Empostor.Api.Net;
|
||||
using Next.Hazel;
|
||||
using Next.Hazel.Udp;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Extensions.Options;
|
||||
|
||||
@@ -21,6 +22,16 @@ namespace Empostor.Server.Net.Hazel
|
||||
InnerConnection = innerConnection;
|
||||
innerConnection.DataReceived = ConnectionOnDataReceived;
|
||||
innerConnection.Disconnected = ConnectionOnDisconnected;
|
||||
|
||||
if (innerConnection is UdpConnection udp)
|
||||
{
|
||||
// Tolerate UDP packet loss under load (e.g. 40+ players). A few
|
||||
// dropped reliable packets or keepalive pings are retransmitted
|
||||
// for longer before the connection is declared dead, so a burst
|
||||
// of loss no longer disconnects players.
|
||||
udp.DisconnectTimeout = 10000;
|
||||
udp.MissingPingsUntilDisconnect = 12;
|
||||
}
|
||||
}
|
||||
|
||||
public Connection InnerConnection { get; }
|
||||
|
||||
@@ -33,10 +33,17 @@ public abstract partial class UdpConnection : NetworkConnection
|
||||
_readerPool = readerPool;
|
||||
_stoppingCts = new CancellationTokenSource();
|
||||
|
||||
Pipeline = Channel.CreateUnbounded<byte[]>(new UnboundedChannelOptions
|
||||
// Bound the inbound queue so a slow/overloaded connection cannot let
|
||||
// packets accumulate without limit. Once full, the oldest queued packet
|
||||
// is dropped (DropOldest) — reliable packets are retransmitted by the
|
||||
// sender anyway and unreliable ones carry fresh state each tick, so this
|
||||
// keeps memory flat under sustained load instead of letting it balloon
|
||||
// until the GC pressure starts disconnecting everyone.
|
||||
Pipeline = Channel.CreateBounded<byte[]>(new BoundedChannelOptions(1024)
|
||||
{
|
||||
SingleReader = true,
|
||||
SingleWriter = true
|
||||
SingleWriter = true,
|
||||
FullMode = BoundedChannelFullMode.DropOldest,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -46,8 +53,13 @@ public abstract partial class UdpConnection : NetworkConnection
|
||||
|
||||
public Task StartAsync()
|
||||
{
|
||||
// Store the task we're executing
|
||||
_executingTask = Task.Factory.StartNew(ReadAsync, TaskCreationOptions.LongRunning);
|
||||
// Run the read loop on the thread pool instead of a dedicated
|
||||
// long-running thread per connection. ReadAsync is fully async (it
|
||||
// awaits the pipeline channel), so it only occupies a thread-pool
|
||||
// thread while there is actually data to process. With hundreds of
|
||||
// connections this avoids hundreds of dedicated threads (1 MB stack
|
||||
// each) and the resulting context-switch overhead.
|
||||
_executingTask = Task.Run(ReadAsync);
|
||||
|
||||
// If the task is completed then return it, this will bubble cancellation and failure to the caller
|
||||
if (_executingTask.IsCompleted) return _executingTask;
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
using System;
|
||||
using System.Collections.Concurrent;
|
||||
using System.Collections.Generic;
|
||||
using System.Net;
|
||||
using System.Net.Sockets;
|
||||
using System.Threading;
|
||||
@@ -65,6 +66,20 @@ public class UdpConnectionListener : NetworkConnectionListener
|
||||
{
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
// Under high load (many players / high tick rate) the OS-level UDP
|
||||
// receive queue can overflow and silently drop packets; reliable
|
||||
// retransmission / keepalive then misreads that as a dead connection
|
||||
// and disconnects players. Use a large buffer so bursts are absorbed
|
||||
// instead of dropped.
|
||||
_socket.Client.ReceiveBufferSize = 4 * 1024 * 1024;
|
||||
_socket.Client.SendBufferSize = 4 * 1024 * 1024;
|
||||
}
|
||||
catch (SocketException)
|
||||
{
|
||||
}
|
||||
|
||||
_reliablePacketTimer = new Timer(ManageReliablePackets, null, 100, Timeout.Infinite);
|
||||
|
||||
_allConnections = new ConcurrentDictionary<EndPoint, UdpServerConnection>();
|
||||
@@ -79,10 +94,25 @@ public class UdpConnectionListener : NetworkConnectionListener
|
||||
|
||||
private async void ManageReliablePackets(object? state)
|
||||
{
|
||||
foreach (var kvp in _allConnections)
|
||||
// Resend reliably-sent packets for every connection. Run them
|
||||
// concurrently instead of serially awaiting each connection: with many
|
||||
// players one slow connection used to stall retransmission for everyone
|
||||
// else, which made packet loss snowball into mass disconnects.
|
||||
var connections = new List<UdpServerConnection>(_allConnections.Values);
|
||||
var tasks = new Task[connections.Count];
|
||||
for (var i = 0; i < connections.Count; i++)
|
||||
{
|
||||
var sock = kvp.Value;
|
||||
await sock.ManageReliablePackets();
|
||||
var conn = connections[i];
|
||||
tasks[i] = conn.ManageReliablePackets().AsTask();
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
await Task.WhenAll(tasks);
|
||||
}
|
||||
catch
|
||||
{
|
||||
// ignored
|
||||
}
|
||||
|
||||
try
|
||||
|
||||
Reference in new issue
Block a user