test(content): give the loader concurrency tests real concurrency
Fixing the two failures that were stopping portable-headless earlier let the job reach AcDream.Content.Tests for the first time on either operating system - the test loop exits on the first failing project, so the windows leg had never got past the apt step and the ubuntu leg had never got past Core.Net. Two RetailDatLoaderTests cases were waiting there, and they failed on both. Both assert on RawDatabase.MaxConcurrentReads after issuing two Task.Run reads that each block 40 ms in Thread.Sleep. A pair of pool work items is not a guarantee of two workers in flight: on a low-core or saturated pool the second queues behind the first, the reads run back to back, MaxConcurrentReads stays 1, and the assertion fails for a reason that has nothing to do with the loader. Pinning the suite to two CPUs on Ubuntu reproduces it 5 times in 6; Windows is clean 6 of 6 at sixteen cores, which is why nobody had seen it. The pairs now start with TaskCreationOptions.LongRunning on the default scheduler, which asks for a thread each. No assertion is changed - they still fail if the loader serialises. The two coalescing cases moved onto the same helper on purpose: two callers genuinely in flight is the situation coalescing exists for, and a sequential pair was only ever exercising a cache hit. Ten of ten clean under the same pin. Release build green. App tests 4,152 / 3 skipped. Content 124 / 124. Filed as #255. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
parent
777f60708d
commit
c7861020e1
3 changed files with 102 additions and 12 deletions
|
|
@ -97,6 +97,45 @@ Copy this block when adding a new issue:
|
|||
|
||||
---
|
||||
|
||||
## #255 — Two RetailDatLoader concurrency tests measured the thread pool, not the loader
|
||||
|
||||
**Status:** DONE — 2026-07-28; the pair now runs on dedicated threads
|
||||
**Severity:** LOW (test infrastructure only; no production defect)
|
||||
**Filed:** 2026-07-28
|
||||
**Component:** tests / xUnit parallelism, content loaders
|
||||
|
||||
**Description:** `AcDream.Content.Tests.Vfx.RetailDatLoaderTests.AnimationCache_CoalescesSameDidAndAllowsUnrelatedReadsInParallel`
|
||||
and `.PhysicsScriptLoader_AllowsConcurrentFirstReads` failed on **both** legs of
|
||||
`portable-headless` in run 30392764012. They were previously invisible: the
|
||||
ubuntu leg died in Core.Net (#254) before reaching `AcDream.Content.Tests`, and
|
||||
the windows leg died at an even earlier step (the misplaced `sudo apt-get`), and
|
||||
the workflow's test loop exits on the first failing project.
|
||||
|
||||
**Root cause:** both assert on `RawDatabase.MaxConcurrentReads` after issuing two
|
||||
`Task.Run` reads, each of which blocks 40 ms in `Thread.Sleep`. A pair of pool
|
||||
work items does not guarantee two workers are ever in flight: on a low-core or
|
||||
saturated pool the second queues behind the first, the two reads run back to
|
||||
back, `MaxConcurrentReads` stays 1, and the assertion fails for a reason that has
|
||||
nothing to do with the loader. Same family as #252 and #254 — a test asserting a
|
||||
real-time/parallel property under an unbounded-parallelism test host.
|
||||
|
||||
**Evidence:** clean on Windows locally (6/6 full-suite runs, 124/124 each).
|
||||
Reproduced by pinning the suite to two CPUs on Ubuntu 24.04 (`taskset -c 0,1`):
|
||||
**5 failures in 6 runs**, the same two tests every time.
|
||||
|
||||
**Fix:** a private `InParallel` helper starts both callbacks with
|
||||
`TaskCreationOptions.LongRunning` on `TaskScheduler.Default`, which asks for a
|
||||
thread each, so the concurrency the assertions measure is actually offered. **No
|
||||
assertion changed** — they still fail if the loader serialises. The two
|
||||
coalescing tests were moved onto the same helper deliberately: two callers
|
||||
genuinely in flight is the situation coalescing exists for, where a sequential
|
||||
pair only ever exercised a cache hit. 10/10 clean under the same two-CPU pin
|
||||
afterwards.
|
||||
|
||||
**Files:** `tests/AcDream.Content.Tests/Vfx/RetailDatLoaderTests.cs`.
|
||||
|
||||
---
|
||||
|
||||
## #254 — Logout confirmation wait overran its timeout on a starved thread pool
|
||||
|
||||
**Status:** DONE — 2026-07-28; monotonic deadline added to the synchronous drain
|
||||
|
|
|
|||
|
|
@ -3320,6 +3320,22 @@ structural `spirv-dis` compare. The pinned NuGet native is already the pin.
|
|||
the token, which still bounds the asynchronous wait. Ten of ten clean under
|
||||
the same two-CPU pin afterwards. The test was not touched.
|
||||
|
||||
**And one the fixes uncovered.** With (3) and (4) gone, `portable-headless`
|
||||
reached `AcDream.Content.Tests` for the first time on either operating system —
|
||||
the workflow's test loop exits on the first failing project, so the windows leg
|
||||
had never got past the apt step and the ubuntu leg had never got past Core.Net —
|
||||
and two `RetailDatLoaderTests` cases failed on both. Same family again, and
|
||||
again not the campaign's: they assert `MaxConcurrentReads` after issuing two
|
||||
`Task.Run` reads that each block 40 ms, and a pair of pool work items is not two
|
||||
workers in flight. Reproduced at 5 failures in 6 under the two-CPU pin, clean 6/6
|
||||
on Windows. Both pairs now start with `TaskCreationOptions.LongRunning` so the
|
||||
concurrency the assertions measure is actually offered; **no assertion changed**,
|
||||
and the two coalescing cases get stronger for it, since a sequential pair only
|
||||
ever exercised a cache hit. 10/10 clean under the pin. Filed as #255.
|
||||
|
||||
**Net effect on the V9 row:** the job is green, and so is the whole workflow —
|
||||
its first fully green run is the evidence the row was waiting for.
|
||||
|
||||
### 5.4 The null-target `BeginPass` divergence (V4c) — ✅ DISCHARGED at V6k
|
||||
|
||||
> **Closed 2026-07-28 by V6k commit 2 (`eb7e6b4e`); see §5.5.16.** The answer is
|
||||
|
|
|
|||
|
|
@ -15,6 +15,41 @@ namespace AcDream.Content.Tests.Vfx;
|
|||
|
||||
public sealed class RetailDatLoaderTests
|
||||
{
|
||||
/// <summary>
|
||||
/// Runs both callers on dedicated threads rather than thread-pool work
|
||||
/// items.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// These tests measure whether the LOADER serialises reads, and the
|
||||
/// interesting ones assert on <c>MaxConcurrentReads</c>. A pair of
|
||||
/// <see cref="Task.Run(Func{int})"/> work items does not guarantee two
|
||||
/// workers are ever in flight at once: on a saturated or low-core pool -
|
||||
/// four cores on a hosted CI runner - the second item simply queues behind
|
||||
/// the first, both 40 ms reads run back to back, and the assertion fails
|
||||
/// for a reason that has nothing to do with the loader. Observed exactly
|
||||
/// that way in CI on both operating systems, and reproduced locally by
|
||||
/// pinning the suite to two CPUs (5 failures in 6).
|
||||
/// <para><see cref="TaskCreationOptions.LongRunning"/> on the default
|
||||
/// scheduler asks for a thread per callback, which makes the concurrency
|
||||
/// the assertions measure actually available. The assertions themselves are
|
||||
/// unchanged and still fail if the loader serialises - and the coalescing
|
||||
/// cases get stronger, because two callers genuinely in flight is the
|
||||
/// situation coalescing exists for, where a sequential pair only ever
|
||||
/// exercised a cache hit.</para>
|
||||
/// </remarks>
|
||||
private static Task<T[]> InParallel<T>(Func<T> first, Func<T> second) =>
|
||||
Task.WhenAll(
|
||||
Task.Factory.StartNew(
|
||||
first,
|
||||
CancellationToken.None,
|
||||
TaskCreationOptions.LongRunning,
|
||||
TaskScheduler.Default),
|
||||
Task.Factory.StartNew(
|
||||
second,
|
||||
CancellationToken.None,
|
||||
TaskCreationOptions.LongRunning,
|
||||
TaskScheduler.Default));
|
||||
|
||||
private sealed class RawDatabase : IDatDatabase
|
||||
{
|
||||
private readonly Dictionary<uint, byte[]> _entries = new();
|
||||
|
|
@ -263,15 +298,15 @@ public sealed class RetailDatLoaderTests
|
|||
portal.Add(secondDid, AnimationBytes(secondDid));
|
||||
var loader = new RetailAnimationLoader(portal);
|
||||
|
||||
DatAnimation?[] firstPair = await Task.WhenAll(
|
||||
Task.Run(() => loader.LoadAnimation(firstDid)),
|
||||
Task.Run(() => loader.LoadAnimation(firstDid)));
|
||||
DatAnimation?[] firstPair = await InParallel(
|
||||
() => loader.LoadAnimation(firstDid),
|
||||
() => loader.LoadAnimation(firstDid));
|
||||
Assert.Same(firstPair[0], firstPair[1]);
|
||||
Assert.Equal(1, portal.TotalReads);
|
||||
|
||||
await Task.WhenAll(
|
||||
Task.Run(() => loader.LoadAnimation(secondDid)),
|
||||
Task.Run(() => loader.LoadAnimation(0x03010026u)));
|
||||
await InParallel(
|
||||
() => loader.LoadAnimation(secondDid),
|
||||
() => loader.LoadAnimation(0x03010026u));
|
||||
|
||||
Assert.True(portal.MaxConcurrentReads >= 2);
|
||||
Assert.Equal(3, portal.TotalReads);
|
||||
|
|
@ -291,9 +326,9 @@ public sealed class RetailDatLoaderTests
|
|||
portal.Add(secondDid, second);
|
||||
var loader = new RetailPhysicsScriptLoader(portal);
|
||||
|
||||
await Task.WhenAll(
|
||||
Task.Run(() => loader.LoadPhysicsScript(firstDid)),
|
||||
Task.Run(() => loader.LoadPhysicsScript(secondDid)));
|
||||
await InParallel(
|
||||
() => loader.LoadPhysicsScript(firstDid),
|
||||
() => loader.LoadPhysicsScript(secondDid));
|
||||
|
||||
Assert.Equal(2, portal.MaxConcurrentReads);
|
||||
Assert.Equal(2, portal.TotalReads);
|
||||
|
|
@ -319,9 +354,9 @@ public sealed class RetailDatLoaderTests
|
|||
portal.Add(scriptDid, bytes);
|
||||
var loader = new RetailPhysicsScriptLoader(portal);
|
||||
|
||||
DatPhysicsScript?[] loaded = await Task.WhenAll(
|
||||
Task.Run(() => loader.LoadPhysicsScript(scriptDid)),
|
||||
Task.Run(() => loader.LoadPhysicsScript(scriptDid)));
|
||||
DatPhysicsScript?[] loaded = await InParallel(
|
||||
() => loader.LoadPhysicsScript(scriptDid),
|
||||
() => loader.LoadPhysicsScript(scriptDid));
|
||||
|
||||
Assert.All(loaded, Assert.NotNull);
|
||||
Assert.Same(loaded[0], loaded[1]);
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue