feat: integrate performance upgrade and agent inspection tooling
Validate ShrinkSDK Workspace / catalog (push) Successful in 5s
Validate ShrinkSDK Workspace / unity (push) Successful in 3m20s

This commit is contained in:
2026-09-29 10:16:02 +08:00
parent f92105c97d
commit 08246ddb01
114 changed files with 7135 additions and 87 deletions
@@ -0,0 +1,139 @@
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;}
}
}