perf(streaming): quiesce retired generations and budget teardown
Publish the retail blocking-for-cells edge before deferred recenter work, freeze old-world presentation/simulation/audio, and advance full-window retirement from exact metered entity and owner cursors. This removes synchronous portal teardown without allowing retained owners to remain observable.
This commit is contained in:
parent
b8f6317fe1
commit
bb16f74fd4
38 changed files with 1691 additions and 170 deletions
|
|
@ -51,6 +51,101 @@ public sealed class LandblockRetirementCoordinatorTests
|
|||
Assert.Equal(1, calls[3]);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void BudgetedRetirement_AdvancesAtMostTheAdmittedAtomicEntityWork()
|
||||
{
|
||||
const uint landblockId = 0x2021FFFFu;
|
||||
WorldEntity[] entities =
|
||||
[
|
||||
Entity(1),
|
||||
Entity(2),
|
||||
Entity(3),
|
||||
Entity(4),
|
||||
];
|
||||
var state = StateWith(landblockId, entities);
|
||||
var lightingCalls = new Dictionary<uint, int>();
|
||||
LandblockRetirementCoordinator coordinator =
|
||||
LandblockRetirementCoordinator.CreateBudgeted(
|
||||
state,
|
||||
ticket => AdvancePresentationStep(
|
||||
ticket,
|
||||
entity => lightingCalls[entity.Id] =
|
||||
lightingCalls.GetValueOrDefault(entity.Id) + 1),
|
||||
ticket => CompletePresentation(
|
||||
ticket,
|
||||
lighting: entity => lightingCalls[entity.Id] =
|
||||
lightingCalls.GetValueOrDefault(entity.Id) + 1));
|
||||
coordinator.BeginFull(landblockId);
|
||||
|
||||
Assert.False(state.IsLoaded(landblockId));
|
||||
Assert.Equal(1, coordinator.PendingCount);
|
||||
|
||||
int frames = 0;
|
||||
while (coordinator.PendingCount != 0 && frames++ < 64)
|
||||
{
|
||||
var meter = new StreamingWorkMeter(Budget(maxEntityOperations: 2));
|
||||
coordinator.Advance(meter);
|
||||
meter.FinishFrame();
|
||||
Assert.InRange(meter.Snapshot.Used.EntityOperations, 0, 2);
|
||||
}
|
||||
|
||||
Assert.InRange(frames, 2, 64);
|
||||
Assert.Equal(0, coordinator.PendingCount);
|
||||
Assert.Equal(
|
||||
entities.Select(entity => entity.Id).Order(),
|
||||
lightingCalls.Keys.Order());
|
||||
Assert.All(lightingCalls.Values, count => Assert.Equal(1, count));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void BudgetedRetirement_FailedEntityRetriesWithoutReplayingCommittedPrefix()
|
||||
{
|
||||
const uint landblockId = 0x2022FFFFu;
|
||||
var state = StateWith(
|
||||
landblockId,
|
||||
Entity(1),
|
||||
Entity(2),
|
||||
Entity(3));
|
||||
var calls = new Dictionary<uint, int>();
|
||||
bool failSecond = true;
|
||||
LandblockRetirementCoordinator coordinator =
|
||||
LandblockRetirementCoordinator.CreateBudgeted(
|
||||
state,
|
||||
ticket => AdvancePresentationStep(
|
||||
ticket,
|
||||
entity =>
|
||||
{
|
||||
calls[entity.Id] = calls.GetValueOrDefault(entity.Id) + 1;
|
||||
if (entity.Id == 2 && failSecond)
|
||||
{
|
||||
failSecond = false;
|
||||
throw new InvalidOperationException(
|
||||
"injected metered entity failure");
|
||||
}
|
||||
}),
|
||||
ticket => CompletePresentation(ticket));
|
||||
coordinator.BeginFull(landblockId);
|
||||
|
||||
var firstMeter = new StreamingWorkMeter(Budget(maxEntityOperations: 64));
|
||||
coordinator.Advance(firstMeter);
|
||||
firstMeter.FinishFrame();
|
||||
|
||||
Assert.Equal(1, coordinator.PendingCount);
|
||||
Assert.Equal(1, calls[1]);
|
||||
Assert.Equal(1, calls[2]);
|
||||
Assert.False(calls.ContainsKey(3));
|
||||
Assert.Equal(1, firstMeter.Snapshot.FailureCount);
|
||||
|
||||
var retryMeter = new StreamingWorkMeter(Budget(maxEntityOperations: 64));
|
||||
coordinator.Advance(retryMeter);
|
||||
retryMeter.FinishFrame();
|
||||
|
||||
Assert.Equal(0, coordinator.PendingCount);
|
||||
Assert.Equal(1, calls[1]);
|
||||
Assert.Equal(2, calls[2]);
|
||||
Assert.Equal(1, calls[3]);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ForceReload_RetiresEveryLoadedId_WhenOneOwnerRemainsPending()
|
||||
{
|
||||
|
|
@ -59,31 +154,88 @@ public sealed class LandblockRetirementCoordinatorTests
|
|||
foreach (uint id in ids)
|
||||
state.AddLandblock(new LoadedLandblock(id, new LandBlock(), Array.Empty<WorldEntity>()));
|
||||
|
||||
bool failOnce = true;
|
||||
bool failRetirement = true;
|
||||
var coordinator = new LandblockRetirementCoordinator(
|
||||
state,
|
||||
ticket => CompletePresentation(
|
||||
ticket,
|
||||
terrain: () =>
|
||||
{
|
||||
if (ticket.LandblockId == ids[1] && failOnce)
|
||||
if (ticket.LandblockId == ids[1] && failRetirement)
|
||||
{
|
||||
failOnce = false;
|
||||
throw new InvalidOperationException("injected terrain failure");
|
||||
}
|
||||
}));
|
||||
var controller = Controller(state, coordinator);
|
||||
|
||||
controller.ForceReloadWindow();
|
||||
for (int frame = 0;
|
||||
frame < 16 && ids.Any(state.IsLoaded);
|
||||
frame++)
|
||||
{
|
||||
controller.Tick(0x20, 0x20);
|
||||
}
|
||||
|
||||
foreach (uint id in ids)
|
||||
Assert.False(state.IsLoaded(id));
|
||||
Assert.Equal(1, coordinator.PendingCount);
|
||||
|
||||
failRetirement = false;
|
||||
coordinator.Advance();
|
||||
Assert.Equal(0, coordinator.PendingCount);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ForceReload_CapturesAndDetachesThroughTheSharedEntityBudget()
|
||||
{
|
||||
uint[] ids =
|
||||
[
|
||||
0x2323FFFFu,
|
||||
0x2424FFFFu,
|
||||
0x2525FFFFu,
|
||||
];
|
||||
var state = new GpuWorldState();
|
||||
foreach (uint id in ids)
|
||||
{
|
||||
state.AddLandblock(new LoadedLandblock(
|
||||
id,
|
||||
new LandBlock(),
|
||||
Array.Empty<WorldEntity>()));
|
||||
}
|
||||
|
||||
var coordinator = new LandblockRetirementCoordinator(
|
||||
state,
|
||||
ticket => CompletePresentation(ticket));
|
||||
var controller = Controller(
|
||||
state,
|
||||
coordinator,
|
||||
workBudgetOptions: new StreamingWorkBudgetOptions(
|
||||
MaxUpdateMilliseconds: 1000,
|
||||
MaxCompletionAdmissions: 64,
|
||||
MaxAdoptedCpuBytes: 64 * StreamingWorkBudgetOptions.MiB,
|
||||
MaxEntityOperations: 1,
|
||||
MaxGpuUploadBytes: 64 * StreamingWorkBudgetOptions.MiB,
|
||||
MaxGlRetireOperations: 64,
|
||||
DestinationReserveFraction: 0.75f));
|
||||
controller.ForceReloadWindow();
|
||||
|
||||
int priorLoaded = ids.Length;
|
||||
for (int frame = 0; frame < 32 && priorLoaded != 0; frame++)
|
||||
{
|
||||
controller.Tick(0x23, 0x23);
|
||||
int loaded = ids.Count(state.IsLoaded);
|
||||
Assert.InRange(priorLoaded - loaded, 0, 1);
|
||||
Assert.InRange(
|
||||
controller.WorkDiagnostics.LastFrame.Used.EntityOperations,
|
||||
0,
|
||||
1);
|
||||
priorLoaded = loaded;
|
||||
}
|
||||
|
||||
Assert.Equal(0, priorLoaded);
|
||||
Assert.Equal(ids.Length, controller.LastFullWindowRetirementLandblockCount);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ReplacementPublication_WaitsForSameIdRetirementFence()
|
||||
{
|
||||
|
|
@ -126,12 +278,18 @@ public sealed class LandblockRetirementCoordinatorTests
|
|||
EmptyMesh(),
|
||||
generation: 1));
|
||||
|
||||
controller.Tick(0x32, 0x32); // second failure; publication stays deferred
|
||||
controller.Tick(0x32, 0x32); // first failure; publication stays at worker source
|
||||
Assert.Equal(0, applyCount);
|
||||
Assert.False(state.IsLoaded(landblockId));
|
||||
Assert.True(coordinator.IsPending(landblockId));
|
||||
|
||||
controller.Tick(0x32, 0x32); // third attempt completes, then publishes once
|
||||
controller.Tick(0x32, 0x32); // second failure
|
||||
Assert.Equal(0, applyCount);
|
||||
Assert.True(coordinator.IsPending(landblockId));
|
||||
|
||||
controller.Tick(0x32, 0x32); // third attempt completes the reload barrier
|
||||
Assert.Equal(0, applyCount);
|
||||
controller.Tick(0x32, 0x32); // replacement publishes on the next frame
|
||||
Assert.Equal(1, applyCount);
|
||||
Assert.True(state.IsLoaded(landblockId));
|
||||
Assert.False(coordinator.IsPending(landblockId));
|
||||
|
|
@ -338,7 +496,8 @@ public sealed class LandblockRetirementCoordinatorTests
|
|||
GpuWorldState state,
|
||||
LandblockRetirementCoordinator coordinator,
|
||||
Func<int, IReadOnlyList<LandblockStreamResult>>? drain = null,
|
||||
Action<LandblockBuild, LandblockMeshData>? apply = null) =>
|
||||
Action<LandblockBuild, LandblockMeshData>? apply = null,
|
||||
StreamingWorkBudgetOptions? workBudgetOptions = null) =>
|
||||
new(
|
||||
enqueueLoad: (_, _, _) => { },
|
||||
enqueueUnload: (_, _) => { },
|
||||
|
|
@ -347,7 +506,8 @@ public sealed class LandblockRetirementCoordinatorTests
|
|||
state: state,
|
||||
nearRadius: 0,
|
||||
farRadius: 0,
|
||||
retirementCoordinator: coordinator);
|
||||
retirementCoordinator: coordinator,
|
||||
workBudgetOptions: workBudgetOptions);
|
||||
|
||||
private static IReadOnlyList<LandblockStreamResult> Drain(
|
||||
Queue<LandblockStreamResult> pending,
|
||||
|
|
@ -359,6 +519,60 @@ public sealed class LandblockRetirementCoordinatorTests
|
|||
return result;
|
||||
}
|
||||
|
||||
private static LandblockRetirementOperationResult AdvancePresentationStep(
|
||||
LandblockRetirementTicket ticket,
|
||||
Action<WorldEntity>? lighting = null)
|
||||
{
|
||||
return ticket.NextIncompleteStage switch
|
||||
{
|
||||
LandblockRetirementStage.EntityLighting =>
|
||||
ticket.RunEntityStep(
|
||||
LandblockRetirementStage.EntityLighting,
|
||||
static _ => true,
|
||||
lighting ?? (_ => { })),
|
||||
LandblockRetirementStage.EntityTranslucency =>
|
||||
ticket.RunEntityStep(
|
||||
LandblockRetirementStage.EntityTranslucency,
|
||||
static _ => true,
|
||||
static _ => { }),
|
||||
LandblockRetirementStage.PluginProjection =>
|
||||
ticket.RunEntityStep(
|
||||
LandblockRetirementStage.PluginProjection,
|
||||
static entity => entity.ServerGuid == 0,
|
||||
static _ => { }),
|
||||
LandblockRetirementStage.Terrain =>
|
||||
ticket.RunOnceStep(
|
||||
LandblockRetirementStage.Terrain,
|
||||
static () => { }),
|
||||
LandblockRetirementStage.Physics =>
|
||||
ticket.RunOnceStep(
|
||||
LandblockRetirementStage.Physics,
|
||||
static () => { }),
|
||||
LandblockRetirementStage.CellVisibility =>
|
||||
ticket.RunOnceStep(
|
||||
LandblockRetirementStage.CellVisibility,
|
||||
static () => { }),
|
||||
LandblockRetirementStage.BuildingRegistry =>
|
||||
ticket.RunOnceStep(
|
||||
LandblockRetirementStage.BuildingRegistry,
|
||||
static () => { }),
|
||||
LandblockRetirementStage.EnvironmentCells =>
|
||||
ticket.RunOnceStep(
|
||||
LandblockRetirementStage.EnvironmentCells,
|
||||
static () => { }),
|
||||
_ => LandblockRetirementOperationResult.NoWork,
|
||||
};
|
||||
}
|
||||
|
||||
private static StreamingWorkBudget Budget(int maxEntityOperations) => new(
|
||||
maxUpdateTime: TimeSpan.FromSeconds(1),
|
||||
maxCompletionAdmissions: 64,
|
||||
maxAdoptedCpuBytes: 64 * 1024 * 1024,
|
||||
maxEntityOperations,
|
||||
maxGpuUploadBytes: 64 * 1024 * 1024,
|
||||
maxGlRetireOperations: 64,
|
||||
destinationReserveFraction: 0.75f);
|
||||
|
||||
private static void CompletePresentation(
|
||||
LandblockRetirementTicket ticket,
|
||||
Action<WorldEntity>? lighting = null,
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue