140 lines
7.3 KiB
C#
140 lines
7.3 KiB
C#
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<count;i++) action();
|
|
recorder.Stop();
|
|
if (recorder.Count == recorder.Capacity) throw new InvalidOperationException("GC.Alloc recorder saturated.");
|
|
return recorder.Count;
|
|
}
|
|
|
|
[Test]
|
|
public void AllocationCounterDetectsKnownAllocation()
|
|
{
|
|
var before=GC.GetAllocatedBytesForCurrentThread();
|
|
_allocationProbe=new byte[1024];
|
|
var delta=GC.GetAllocatedBytesForCurrentThread()-before;
|
|
var events=AllocationEvents(() => _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<PlayerContracts.Request>(1); service.RegisterMessage<PlayerContracts.Response>(2);
|
|
service.RegisterMessage<PlayerContracts.Message>(3);
|
|
}
|
|
server.RegisterRequestHandler<PlayerContracts.Request, PlayerContracts.Response>((_, request) => UniTask.FromResult(new PlayerContracts.Response {Text=request.Text}));
|
|
var delivered=0;
|
|
server.RegisterHandler<PlayerContracts.Message>((_, message) => { delivered++; return UniTask.CompletedTask; });
|
|
client.BindTransport(a); server.BindTransport(b);
|
|
for (var i=0;i<sessions;i++) a.OpenSession(i);
|
|
try
|
|
{
|
|
var request = new PlayerContracts.Request {Text=new string('x',bytes)}; var cursor=0;
|
|
Measure($"loopback-rpc/bytes={bytes}/sessions={sessions}", 128, () => client.CallAsync<PlayerContracts.Request,PlayerContracts.Response>(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;i<count;i++) runtime.Use(new Component("k"+i,i==0?null:"k"+(i-1)));
|
|
Measure("context-diagnostic/components="+count,100,()=>runtime.CaptureDiagnostic());
|
|
}
|
|
finally {runtime.ShutdownAsync().GetAwaiter().GetResult();}
|
|
}
|
|
using(var binding=ShrinkGeneratedBinding.SubscribeAsync<PlayerContracts.Event>(ShrinkGeneratedBinding.RuntimeResolver,null,null,null,(_,__)=>UniTask.CompletedTask,ShrinkEventPriority.Normal,0,false))
|
|
{
|
|
Measure("event-async-completed",10000,()=>EventBus.PostAsync(new PlayerContracts.Event()).GetAwaiter().GetResult());
|
|
Measure("event-bindings",1000,()=>EventBus.CaptureBindings(EventBus.Default));
|
|
}
|
|
Measure("event-register-withdraw",10000,()=>ShrinkGeneratedBinding.Subscribe<PlayerContracts.Event>(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<UniTask<ShrinkNetworkQueueResult>>();
|
|
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<string>():new[]{dependency}; Provide=new[]{key}; }
|
|
public string Name=>_key;
|
|
public IReadOnlyList<string> Inject {get;}
|
|
public IReadOnlyList<string> Provide {get;}
|
|
public UniTask ApplyAsync(ShrinkCtx ctx,object config) {ctx.Set(_key,this);return UniTask.CompletedTask;}
|
|
}
|
|
}
|