using System; using System.Collections; using System.Collections.Generic; using System.Diagnostics; using Cysharp.Threading.Tasks; using NUnit.Framework; using ShrinkContext; using ShrinkEventBus; using ShrinkNetwork; using UnityEngine.TestTools; using Unity.Profiling; public sealed class PlayerPerformance { private static byte[] _allocationProbe; public static long AllocationEvents(Action action, int count) { using var recorder = ProfilerRecorder.StartNew(ProfilerCategory.Memory, "GC.Alloc", 65536, ProfilerRecorderOptions.StartImmediately | ProfilerRecorderOptions.CollectOnlyOnCurrentThread); if (!recorder.Valid) throw new InvalidOperationException("GC.Alloc recorder unavailable."); for (var i=0;i _allocationProbe=new byte[1024],10); GC.KeepAlive(_allocationProbe); Assert.That(events,Is.GreaterThanOrEqualTo(10)); UnityEngine.Debug.Log("PLAYER allocation calibration managedCounterDelta="+delta+" profilerEvents="+events+"; use profiler events, not uncalibrated bytes."); } private static void Measure(string scenario, int count, Action action) { for (var i = 0; i < Math.Min(count, 20); i++) action(); var latency = new double[count]; var started = Stopwatch.GetTimestamp(); for (var i = 0; i < count; i++) { var tick = Stopwatch.GetTimestamp(); action(); latency[i] = (Stopwatch.GetTimestamp() - tick) * 1e6 / Stopwatch.Frequency; } var elapsed = (Stopwatch.GetTimestamp() - started) / (double)Stopwatch.Frequency; Array.Sort(latency); var allocationSamples=Math.Min(count,10); var allocationEvents=AllocationEvents(action,allocationSamples); UnityEngine.Debug.Log(FormattableString.Invariant($"PLAYER PERF {scenario} n={count} ops={count/elapsed:F2} allocBytes=unsupported allocationSamples={allocationSamples} allocEventsPerOp={allocationEvents/(double)allocationSamples:F2} p50us={latency[(int)(count*.5)]:F2} p95us={latency[(int)(count*.95)]:F2} p99us={latency[(int)(count*.99)]:F2}")); } [UnityTest] public IEnumerator NetworkMatrix() => UniTask.ToCoroutine(async () => { foreach (var bytes in new[] {64, 1024, 32768}) foreach (var sessions in new[] {1, 32, 256}) { var client = new ShrinkNetworkService(); var server = new ShrinkNetworkService(); var a = new ShrinkLoopbackTransport(); var b = new ShrinkLoopbackTransport(); a.LinkPeer(b); foreach (var service in new[] {client, server}) { service.RegisterMessage(1); service.RegisterMessage(2); service.RegisterMessage(3); } server.RegisterRequestHandler((_, request) => UniTask.FromResult(new PlayerContracts.Response {Text=request.Text})); var delivered=0; server.RegisterHandler((_, message) => { delivered++; return UniTask.CompletedTask; }); client.BindTransport(a); server.BindTransport(b); for (var i=0;i client.CallAsync(client.Sessions[cursor++%sessions],request).GetAwaiter().GetResult()); var payload = new ShrinkJsonNetworkSerializer().Serialize(new PlayerContracts.Message {Text=request.Text}); Measure($"loopback-broadcast/bytes={bytes}/sessions={sessions}", 20, () => { foreach(var peer in client.Sessions.Values) client.SendSerializedAsync(peer,typeof(PlayerContracts.Message),payload).GetAwaiter().GetResult(); }); Assert.That(delivered,Is.EqualTo(50*sessions)); } finally {a.Stop();b.Stop();} await UniTask.Yield(); } }); [Test] public void ContextAndEventMatrix() { foreach(var count in new[] {10,100,1000}) { var runtime=new ShrinkContextRuntime(); try { for(var i=0;iEventBus.PostAsync(new PlayerContracts.Event()).GetAwaiter().GetResult()); Measure("event-bindings",1000,()=>EventBus.CaptureBindings(EventBus.Default)); } Measure("event-register-withdraw",10000,()=>ShrinkGeneratedBinding.Subscribe(ShrinkGeneratedBinding.RuntimeResolver,null,null,null,_=>{},ShrinkEventPriority.Normal,0,false).Dispose()); } [UnityTest] public IEnumerator SlowConsumerAndBurst() => UniTask.ToCoroutine(async () => { using var queue=new ShrinkNetworkWorkQueue(64,65536); var gate=new UniTaskCompletionSource(); var slow=queue.EnqueueAsync(1,"reliable",1024,()=>gate.Task); var pumping=queue.PumpAsync(64,65536,TimeSpan.FromMilliseconds(1)); var results=new List>(); for(var i=0;i<128;i++) results.Add(queue.EnqueueAsync(i%32,"state",1024,()=>UniTask.CompletedTask,"position")); var before=queue.CaptureDiagnostics(); Assert.That(before.Replaced,Is.EqualTo(96)); await UniTask.Delay(10); Assert.That(pumping.Status,Is.EqualTo(UniTaskStatus.Pending)); gate.TrySetResult(); await pumping; await slow; await queue.PumpAsync(64); foreach(var result in results) await result; Assert.That(queue.PendingCount,Is.Zero); UnityEngine.Debug.Log("PLAYER Queue pending="+before.PendingCount+" bytes="+before.PendingBytes+" replaced="+before.Replaced+" rejected="+before.Rejected); }); private sealed class Component : IShrinkComponent { private readonly string _key; public Component(string key,string dependency) { _key=key; Inject=dependency==null?Array.Empty():new[]{dependency}; Provide=new[]{key}; } public string Name=>_key; public IReadOnlyList Inject {get;} public IReadOnlyList Provide {get;} public UniTask ApplyAsync(ShrinkCtx ctx,object config) {ctx.Set(_key,this);return UniTask.CompletedTask;} } }