fix(net): timeslice WorldSession.Tick inbound drain (#2 flood)
After a teleport ACE floods a town's CreateObjects; WorldSession.Tick drained the ENTIRE inbound queue every frame, each spawn hydrating mesh+textures under _datLock on the render thread — monopolizing the update loop for ~a minute and starving the streaming apply, so only the destination landblock loaded and neighbors trickled in (visible at high render-FPS because update/render are separate Silk.NET callbacks). Bound the per-frame drain to a ~4ms wall-clock budget once InWorld (handshake uses the blocking PumpOnce path, never Tick). A time budget self-adapts to the highly variable per-datagram cost; the tail stays queued (unbounded channel, FIFO) and drains over the next few frames. Acks are queued per packet BEFORE the heavy handler (WorldSession.cs:680), so deferring the tail only delays the tail's acks a few frames — within ACE's tolerance (holtburger defers acks on a flush cadence; verified in references/holtburger session/send.rs). Budget decision extracted as a pure testable static (4 unit tests). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
6e78fcd7f6
commit
76c7b1594b
2 changed files with 65 additions and 3 deletions
|
|
@ -0,0 +1,33 @@
|
|||
using AcDream.Core.Net;
|
||||
using Xunit;
|
||||
|
||||
namespace AcDream.Core.Net.Tests;
|
||||
|
||||
/// <summary>
|
||||
/// #2 flood-timeslice: the per-frame inbound drain is bounded to a wall-clock budget once
|
||||
/// in-world so a CreateObject flood can't monopolize the update loop. These cover the pure
|
||||
/// gate decision (<see cref="WorldSession.InboundBudgetExceeded"/>); the FIFO tail-drain is
|
||||
/// standard <c>Channel</c> behavior and is verified live.
|
||||
/// </summary>
|
||||
public sealed class WorldSessionInboundBudgetTests
|
||||
{
|
||||
[Fact]
|
||||
public void InWorld_overBudget_stops()
|
||||
=> Assert.True(WorldSession.InboundBudgetExceeded(
|
||||
WorldSession.State.InWorld, startTicks: 100, nowTicks: 250, budgetTicks: 100));
|
||||
|
||||
[Fact]
|
||||
public void InWorld_atBudgetBoundary_stops()
|
||||
=> Assert.True(WorldSession.InboundBudgetExceeded(
|
||||
WorldSession.State.InWorld, startTicks: 0, nowTicks: 100, budgetTicks: 100));
|
||||
|
||||
[Fact]
|
||||
public void InWorld_underBudget_continues()
|
||||
=> Assert.False(WorldSession.InboundBudgetExceeded(
|
||||
WorldSession.State.InWorld, startTicks: 0, nowTicks: 99, budgetTicks: 100));
|
||||
|
||||
[Fact]
|
||||
public void NotInWorld_neverBounds_soTheHandshakeDrainsFully()
|
||||
=> Assert.False(WorldSession.InboundBudgetExceeded(
|
||||
WorldSession.State.Disconnected, startTicks: 0, nowTicks: 1_000_000, budgetTicks: 100));
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue