chore(engine-ambient): harden shim + LocalLog statics for fixture parallelism

Follow-up to the multi-instancing migration. Wraps the process-shared engine
statics that aren't ambient-fronted but race between concurrent battles:

- UnityEngine.Resources._loaded: Dictionary -> ConcurrentDictionary.GetOrAdd
  (the shared prefab cache keyed by path; concurrent first-misses produced
  duplicate GameObjects + Dictionary corruption)
- UnityEngine.GameObject._components: Dictionary -> ConcurrentDictionary with
  Interlocked.CompareExchange init (Resources.Load returns SHARED prefab
  GameObjects, so two engines' Setup() can race on the same _components map
  — surfaced as "Operations that change non-concurrent collections" crashes
  during BattleManagerBase ctor's GetComponent<T>() chain)
- Wizard.LocalLog: single static lock around all mutating entry points
  (StringBuilder _lastTraceLogStringBuilder + ~12 mutable string/bool/int
  scratch fields; serializing the trace-log surface is cheap since logging
  is not the hot path)

Flips SVSim.BattleEngine.Tests assembly Parallelizable scope from Self to
Fixtures and restructures MultiInstanceEngineTests.StressN_BaselineMatches so
Setup runs INSIDE Task.Run (was previously serialized as a workaround for the
LocalLog races). The fixture is also lifted to ParallelScope.All so the
two-engines and stress tests can run alongside each other.

Suite fully green under fixture parallelism (59/0/2 across 3 consecutive runs);
SVSim.UnitTests still 1054/0/0 — true multi-instance correctness is now proved
end-to-end in tests rather than gated behind a serial workaround.

Manifest sha refresh + new patch artifact for the LocalLog edit (decomp-origin);
the two shim files are authored, so no metadata update is needed for them.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
gamer147
2026-06-08 08:02:49 -04:00
parent 45344e6d83
commit fbac66fd0b
7 changed files with 360 additions and 174 deletions

View File

@@ -1,21 +1,7 @@
// Assembly-level parallelism policy.
//
// Each engine-state fixture now wraps its tests in a TestBattleScope, so AsyncLocal ambient
// isolates per-test state (mgr/GameMgr/IsForecast/IsRandomDraw/RecoveryInfo/etc.). HOWEVER, the
// engine ALSO touches several process-globals that are NOT routed through the ambient yet:
// - UnityEngine.Resources cache (Dictionary<string,object>, not concurrent)
// - PrefabMgr.Load cache
// - Wizard.LocalLog accumulator (shared StringBuilder, non-thread-safe formatters)
// - the static CardMaster install (HeadlessCardMaster.Load now locks internally)
// So enabling ParallelScope.Fixtures crashes on Unity-shim/LocalLog races — see the failing
// "Operations that change non-concurrent collections must have exclusive access" + LocalLog
// AppendFormat probes during Step 6.5.
//
// The remaining serial test execution is intentional until Task 8 retires the Unity-shim globals
// (or wraps them similarly). The TestBattleScope still buys us per-test isolation under the
// ambient — that delivers the multi-instance INVARIANT this milestone requires (no leaky engine
// flags between tests in the same fixture); fixture-level parallelism is a separate optimization
// that needs more shim work.
// Each engine-state fixture wraps its tests in a TestBattleScope, so AsyncLocal ambient
// isolates per-test state (mgr/GameMgr/IsForecast/IsRandomDraw/RecoveryInfo/etc.). The
// residual process-globals (Unity Resources shim cache, Wizard.LocalLog accumulators) are
// now thread-safe (ConcurrentDictionary / static lock), so fixtures can run in parallel.
using NUnit.Framework;
[assembly: Parallelizable(ParallelScope.Self)]
[assembly: Parallelizable(ParallelScope.Fixtures)]