test(ci): harden scheduling and allocation gates
This commit is contained in:
parent
c4608b1127
commit
d123c4b67c
11 changed files with 101 additions and 87 deletions
|
|
@ -71,10 +71,9 @@ public class CellViewDedupTests
|
|||
|
||||
// Warm the method/JIT before measuring. Duplicate portal emissions are the hot path.
|
||||
Assert.False(view.Add(polygon));
|
||||
long before = GC.GetAllocatedBytesForCurrentThread();
|
||||
for (int i = 0; i < 1_000; i++)
|
||||
Assert.False(view.Add(polygon));
|
||||
long allocated = GC.GetAllocatedBytesForCurrentThread() - before;
|
||||
long allocated = ZeroAllocationProbe.MeasureWarmed(
|
||||
() => _ = view.Add(polygon),
|
||||
batchSize: 1_000);
|
||||
|
||||
Assert.True(allocated <= 256, $"duplicate probes allocated {allocated:N0} bytes");
|
||||
}
|
||||
|
|
@ -91,13 +90,13 @@ public class CellViewDedupTests
|
|||
view.Reset();
|
||||
Assert.True(view.Add(polygon));
|
||||
|
||||
long before = GC.GetAllocatedBytesForCurrentThread();
|
||||
for (int i = 0; i < 1_000; i++)
|
||||
{
|
||||
view.Reset();
|
||||
Assert.True(view.Add(polygon));
|
||||
}
|
||||
long allocated = GC.GetAllocatedBytesForCurrentThread() - before;
|
||||
long allocated = ZeroAllocationProbe.MeasureWarmed(
|
||||
() =>
|
||||
{
|
||||
view.Reset();
|
||||
_ = view.Add(polygon);
|
||||
},
|
||||
batchSize: 1_000);
|
||||
|
||||
Assert.True(allocated <= 256, $"steady rebuilds allocated {allocated:N0} bytes");
|
||||
}
|
||||
|
|
|
|||
|
|
@ -138,20 +138,12 @@ public sealed class DirectionalShadowCascadeFitterTests
|
|||
DirectionalShadowCascadeFitInput input = CameraInput(
|
||||
Vector3.Zero,
|
||||
DirectionalShadowQuality.For(DirectionalShadowPreset.High));
|
||||
Span<DirectionalShadowCascade> cascades =
|
||||
stackalloc DirectionalShadowCascade[4];
|
||||
var cascades = new DirectionalShadowCascade[4];
|
||||
|
||||
// Cross the tiered-JIT promotion threshold before taking the thread's
|
||||
// allocation counter; measuring immediately after one call makes the
|
||||
// runtime's compilation bookkeeping look like renderer allocation.
|
||||
for (int i = 0; i < 128; i++)
|
||||
DirectionalShadowCascadeFitter.Fit(in input, cascades);
|
||||
long before = GC.GetAllocatedBytesForCurrentThread();
|
||||
for (int i = 0; i < 100; i++)
|
||||
DirectionalShadowCascadeFitter.Fit(in input, cascades);
|
||||
long after = GC.GetAllocatedBytesForCurrentThread();
|
||||
|
||||
Assert.Equal(0, after - before);
|
||||
ZeroAllocationProbe.AssertAllocatesNothing(
|
||||
"DirectionalShadowCascadeFitter.Fit",
|
||||
() => DirectionalShadowCascadeFitter.Fit(in input, cascades),
|
||||
batchSize: 100);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
|
|
|
|||
|
|
@ -465,12 +465,10 @@ public sealed class DirectionalShadowCasterFrameTests
|
|||
frame.Build(in query);
|
||||
int copies = source.IndexCopies;
|
||||
|
||||
long before = GC.GetAllocatedBytesForCurrentThread();
|
||||
for (int iteration = 0; iteration < 256; iteration++)
|
||||
frame.Build(in query);
|
||||
long allocated = GC.GetAllocatedBytesForCurrentThread() - before;
|
||||
|
||||
Assert.Equal(0, allocated);
|
||||
ZeroAllocationProbe.AssertAllocatesNothing(
|
||||
"DirectionalShadowCasterFrame.Build stable frame",
|
||||
() => frame.Build(in query),
|
||||
batchSize: 256);
|
||||
Assert.Equal(1ul, frame.BuildSequence);
|
||||
Assert.Equal(copies, source.IndexCopies);
|
||||
Assert.Equal(0, frame.Stats.Classifications);
|
||||
|
|
@ -508,16 +506,16 @@ public sealed class DirectionalShadowCasterFrameTests
|
|||
frame.Build(in query);
|
||||
}
|
||||
|
||||
long before = GC.GetAllocatedBytesForCurrentThread();
|
||||
for (int iteration = 0; iteration < 64; iteration++)
|
||||
void RefreshDenseFrame()
|
||||
{
|
||||
for (int index = 0; index < 75; index++)
|
||||
source.PublishTransformChanges(dynamics[index].Id, 1);
|
||||
frame.Build(in query);
|
||||
}
|
||||
long allocated = GC.GetAllocatedBytesForCurrentThread() - before;
|
||||
|
||||
Assert.Equal(0, allocated);
|
||||
ZeroAllocationProbe.AssertAllocatesNothing(
|
||||
"DirectionalShadowCasterFrame.Build dense changed frame",
|
||||
RefreshDenseFrame,
|
||||
batchSize: 64);
|
||||
Assert.True(frame.Stats.DensityBulkRefresh);
|
||||
Assert.Equal(75, frame.Stats.DynamicTransformRefreshes);
|
||||
Assert.Equal(0, source.ProjectionReads);
|
||||
|
|
|
|||
|
|
@ -54,18 +54,14 @@ public sealed class DirectionalShadowTransformBufferSetTests
|
|||
topologyBuildSequence: 7,
|
||||
transforms,
|
||||
ReadOnlySpan<int>.Empty);
|
||||
long before = GC.GetAllocatedBytesForCurrentThread();
|
||||
for (int iteration = 0; iteration < 256; iteration++)
|
||||
{
|
||||
buffers.Publish(
|
||||
ZeroAllocationProbe.AssertAllocatesNothing(
|
||||
"DirectionalShadowTransformBufferSet.Publish",
|
||||
() => buffers.Publish(
|
||||
warmed,
|
||||
topologyBuildSequence: 7,
|
||||
transforms,
|
||||
ReadOnlySpan<int>.Empty);
|
||||
}
|
||||
long allocated = GC.GetAllocatedBytesForCurrentThread() - before;
|
||||
|
||||
Assert.Equal(0, allocated);
|
||||
ReadOnlySpan<int>.Empty),
|
||||
batchSize: 256);
|
||||
Assert.Equal(buffersBefore, device.CreatedBuffers.Count);
|
||||
Assert.Equal(uploadsBefore, slotZero.UploadCount);
|
||||
Assert.Empty(device.OfKind<GpuRecordedHostStorageVisibility>());
|
||||
|
|
|
|||
|
|
@ -38,15 +38,11 @@ public sealed class VulkanDrawBindingStateTests
|
|||
var state = new VulkanDrawBindingState();
|
||||
state.MarkBound(pipelineLayout: 30, packGeneration: 8);
|
||||
|
||||
long before = GC.GetAllocatedBytesForCurrentThread();
|
||||
for (int iteration = 0; iteration < 10_000; iteration++)
|
||||
{
|
||||
Assert.False(state.RequiresBind(
|
||||
ZeroAllocationProbe.AssertAllocatesNothing(
|
||||
"VulkanDrawBindingState.RequiresBind",
|
||||
() => _ = state.RequiresBind(
|
||||
pipelineLayout: 30,
|
||||
packGeneration: 8));
|
||||
}
|
||||
long allocated = GC.GetAllocatedBytesForCurrentThread() - before;
|
||||
|
||||
Assert.Equal(0, allocated);
|
||||
packGeneration: 8),
|
||||
batchSize: 10_000);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,21 +2,38 @@ using AcDream.App.Rendering;
|
|||
|
||||
namespace AcDream.App.Tests.Rendering;
|
||||
|
||||
[Collection(AcDream.App.Tests.ThreadSchedulingCollection.Name)]
|
||||
public sealed class HostQuiescenceGateTests
|
||||
{
|
||||
[Fact]
|
||||
public async Task ExternalStopWaitsForAdmittedCallbackToReturn()
|
||||
public void ExternalStopWaitsForAdmittedCallbackToReturn()
|
||||
{
|
||||
var gate = new HostQuiescenceGate();
|
||||
using var callbackEntered = new ManualResetEventSlim(false);
|
||||
using var releaseCallback = new ManualResetEventSlim(false);
|
||||
int calls = 0;
|
||||
Task callback = Task.Run(() => gate.Invoke(() =>
|
||||
Exception? callbackError = null;
|
||||
var callbackThread = new Thread(() =>
|
||||
{
|
||||
callbackEntered.Set();
|
||||
Assert.True(releaseCallback.Wait(TimeSpan.FromSeconds(5)));
|
||||
calls++;
|
||||
}));
|
||||
try
|
||||
{
|
||||
gate.Invoke(() =>
|
||||
{
|
||||
callbackEntered.Set();
|
||||
Assert.True(releaseCallback.Wait(TimeSpan.FromSeconds(5)));
|
||||
calls++;
|
||||
});
|
||||
}
|
||||
catch (Exception error)
|
||||
{
|
||||
callbackError = error;
|
||||
}
|
||||
})
|
||||
{
|
||||
IsBackground = true,
|
||||
Name = "HostQuiescenceGate admitted callback contract",
|
||||
};
|
||||
callbackThread.Start();
|
||||
Assert.True(callbackEntered.Wait(TimeSpan.FromSeconds(5)));
|
||||
|
||||
using var stopStarted = new ManualResetEventSlim(false);
|
||||
|
|
@ -44,13 +61,15 @@ public sealed class HostQuiescenceGateTests
|
|||
TimeSpan.FromSeconds(5));
|
||||
|
||||
releaseCallback.Set();
|
||||
await callback;
|
||||
bool callbackJoined = callbackThread.Join(TimeSpan.FromSeconds(5));
|
||||
bool stopJoined = stopThread.Join(TimeSpan.FromSeconds(5));
|
||||
gate.Invoke(() => calls++);
|
||||
|
||||
Assert.True(stopStartedInTime, "the dedicated stop thread did not start");
|
||||
Assert.True(stopBlockedOnCallback, "stop never waited for the admitted callback");
|
||||
Assert.True(callbackJoined, "the admitted callback did not finish after release");
|
||||
Assert.True(stopJoined, "stop did not finish after the callback returned");
|
||||
Assert.Null(callbackError);
|
||||
Assert.Null(stopError);
|
||||
Assert.Equal(1, calls);
|
||||
Assert.False(gate.IsAccepting);
|
||||
|
|
|
|||
|
|
@ -45,7 +45,6 @@ public sealed class AtmosphericGpuTimerSamplingTests
|
|||
frameSerial: 1);
|
||||
|
||||
int measured = 0;
|
||||
long before = GC.GetAllocatedBytesForCurrentThread();
|
||||
for (long frame = 1; frame <= 10_000; frame++)
|
||||
{
|
||||
if (AtmosphericGpuTimerSampling.ShouldMeasure(
|
||||
|
|
@ -55,9 +54,12 @@ public sealed class AtmosphericGpuTimerSamplingTests
|
|||
measured++;
|
||||
}
|
||||
}
|
||||
long allocated = GC.GetAllocatedBytesForCurrentThread() - before;
|
||||
|
||||
Assert.Equal(2_500, measured);
|
||||
Assert.Equal(0, allocated);
|
||||
ZeroAllocationProbe.AssertAllocatesNothing(
|
||||
"AtmosphericGpuTimerSampling.ShouldMeasure",
|
||||
() => _ = AtmosphericGpuTimerSampling.ShouldMeasure(
|
||||
RenderQualitySemantic.Low,
|
||||
frameSerial: 4),
|
||||
batchSize: 10_000);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -383,12 +383,10 @@ public sealed class RenderPackAutoRuntimeTests
|
|||
for (int i = 0; i < 128; i++)
|
||||
fixture.Observe(1, stable: true);
|
||||
|
||||
long before = GC.GetAllocatedBytesForCurrentThread();
|
||||
for (int i = 0; i < 128; i++)
|
||||
fixture.Observe(1, stable: true);
|
||||
long after = GC.GetAllocatedBytesForCurrentThread();
|
||||
|
||||
Assert.Equal(0, after - before);
|
||||
ZeroAllocationProbe.AssertAllocatesNothing(
|
||||
"RenderPackAutoRuntime stable observation",
|
||||
() => fixture.Observe(1, stable: true),
|
||||
batchSize: 128);
|
||||
}
|
||||
|
||||
private sealed class Fixture : IDisposable
|
||||
|
|
|
|||
|
|
@ -371,11 +371,10 @@ public sealed class DirectionalShadowPreparedDrawTests
|
|||
Assert.Equal([0, 1], product.DynamicTransformSlots.ToArray());
|
||||
|
||||
product.RefreshDynamicTransforms(changed);
|
||||
long before = GC.GetAllocatedBytesForCurrentThread();
|
||||
for (int iteration = 0; iteration < 256; iteration++)
|
||||
product.RefreshDynamicTransforms(changed);
|
||||
long allocated = GC.GetAllocatedBytesForCurrentThread() - before;
|
||||
Assert.Equal(0, allocated);
|
||||
ZeroAllocationProbe.AssertAllocatesNothing(
|
||||
"DirectionalShadowPreparedDraws.RefreshDynamicTransforms",
|
||||
() => product.RefreshDynamicTransforms(changed),
|
||||
batchSize: 256);
|
||||
|
||||
RenderProjectionRecord wrongId = current with
|
||||
{
|
||||
|
|
@ -508,16 +507,12 @@ public sealed class DirectionalShadowPreparedDrawTests
|
|||
product.Transforms[0]);
|
||||
AssertMatrixBitsEqual(movedPart * movedRoot, product.Transforms[1]);
|
||||
|
||||
long beforeDense = GC.GetAllocatedBytesForCurrentThread();
|
||||
for (int iteration = 0; iteration < 256; iteration++)
|
||||
{
|
||||
product.RefreshDynamicTransforms(
|
||||
ZeroAllocationProbe.AssertAllocatesNothing(
|
||||
"DirectionalShadowPreparedDraws.RefreshDynamicTransforms dense",
|
||||
() => product.RefreshDynamicTransforms(
|
||||
reversedDenseChanges,
|
||||
denseRefresh: true);
|
||||
}
|
||||
long denseAllocated =
|
||||
GC.GetAllocatedBytesForCurrentThread() - beforeDense;
|
||||
Assert.Equal(0, denseAllocated);
|
||||
denseRefresh: true),
|
||||
batchSize: 256);
|
||||
|
||||
Assert.True(product.TryBegin(generation, 14, estimatedInstances: 0));
|
||||
product.Complete(generation, 14, in stats);
|
||||
|
|
@ -619,9 +614,14 @@ public sealed class DirectionalShadowPreparedDrawTests
|
|||
RenderSceneGeneration generation = RenderSceneGeneration.FromRaw(9);
|
||||
|
||||
BuildVariedTopology(product, generation, buildSequence: 1, drawCount);
|
||||
long before = GC.GetAllocatedBytesForCurrentThread();
|
||||
BuildVariedTopology(product, generation, buildSequence: 2, drawCount);
|
||||
long allocated = GC.GetAllocatedBytesForCurrentThread() - before;
|
||||
ulong buildSequence = 1;
|
||||
long allocated = ZeroAllocationProbe.MeasureWarmed(
|
||||
() => BuildVariedTopology(
|
||||
product,
|
||||
generation,
|
||||
++buildSequence,
|
||||
drawCount),
|
||||
batchSize: 1);
|
||||
|
||||
Assert.Equal(drawCount, product.Stats.PreparedInstances);
|
||||
// One small constant covers the sort's comparison-delegate wrapper.
|
||||
|
|
|
|||
|
|
@ -13,6 +13,7 @@ namespace AcDream.App.Tests.Streaming;
|
|||
/// before Far-tier ones within a lane, a pool of one reproduces the serial
|
||||
/// pre-#418 behavior, and disposal joins every worker.
|
||||
/// </summary>
|
||||
[Collection(AcDream.App.Tests.ThreadSchedulingCollection.Name)]
|
||||
public sealed class LandblockStreamerPoolTests
|
||||
{
|
||||
private const int SpinTimeoutMs = 10_000;
|
||||
|
|
|
|||
13
tests/AcDream.App.Tests/ThreadSchedulingCollection.cs
Normal file
13
tests/AcDream.App.Tests/ThreadSchedulingCollection.cs
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
namespace AcDream.App.Tests;
|
||||
|
||||
/// <summary>
|
||||
/// Dedicated-thread coordination tests must not compete with the thousands of
|
||||
/// parallel App tests for host scheduling time. They verify ordering and
|
||||
/// lifecycle contracts, not behavior under an intentionally saturated test
|
||||
/// runner.
|
||||
/// </summary>
|
||||
[CollectionDefinition(Name, DisableParallelization = true)]
|
||||
public sealed class ThreadSchedulingCollection
|
||||
{
|
||||
public const string Name = "Dedicated thread scheduling";
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue