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:
@@ -54,13 +54,13 @@ namespace UnityEngine
|
||||
// dictionary that the resolution-path ctor then Instantiate()s + GetComponent()s
|
||||
// (e.g. Prefab/Game/UnityEventAgent). Return a cached no-op GameObject per path so that
|
||||
// chain yields a non-null object. Typed asset loads go through the generic Load<T> (null).
|
||||
private static readonly System.Collections.Generic.Dictionary<string, GameObject> _loaded
|
||||
= new System.Collections.Generic.Dictionary<string, GameObject>();
|
||||
// ConcurrentDictionary + GetOrAdd so concurrent battle setups don't race on first-miss.
|
||||
private static readonly System.Collections.Concurrent.ConcurrentDictionary<string, GameObject> _loaded
|
||||
= new System.Collections.Concurrent.ConcurrentDictionary<string, GameObject>();
|
||||
public static Object Load(string path)
|
||||
{
|
||||
if (string.IsNullOrEmpty(path)) return null;
|
||||
if (!_loaded.TryGetValue(path, out var go)) { go = new GameObject(path); _loaded[path] = go; }
|
||||
return go;
|
||||
return _loaded.GetOrAdd(path, static p => new GameObject(p));
|
||||
}
|
||||
public static Object Load(string path, Type t) => Load(path);
|
||||
public static T[] LoadAll<T>(string path) where T : Object => new T[0];
|
||||
|
||||
Reference in New Issue
Block a user