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
+8
View File
@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: 908affa5f1a95214fa21a165799e7e56
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,29 @@
using System.IO;
using UnityEditor;
using UnityEditor.TestTools.TestRunner.Api;
using UnityEngine;
[InitializeOnLoad]
public static class PlayerAcceptance
{
private static readonly TestRunnerApi Api;
static PlayerAcceptance() { Api = ScriptableObject.CreateInstance<TestRunnerApi>(); Api.RegisterCallbacks(new Results()); }
public static string Run()
{
SessionState.SetBool("Shrink.PlayerAcceptance.Active", true);
return Api.Execute(new ExecutionSettings(new Filter { testMode = TestMode.PlayMode, assemblyNames = new[] { "ShrinkSDK.AgentSupport.PlayerTests" }, targetPlatform = BuildTarget.StandaloneWindows64 }));
}
private sealed class Results : ICallbacks
{
public void RunStarted(ITestAdaptor tests) { }
public void TestStarted(ITestAdaptor test) { }
public void TestFinished(ITestResultAdaptor result) { }
public void RunFinished(ITestResultAdaptor result)
{
if (!SessionState.GetBool("Shrink.PlayerAcceptance.Active", false)) return;
SessionState.SetBool("Shrink.PlayerAcceptance.Active", false);
Directory.CreateDirectory("Artifacts/AgentSupport");
File.WriteAllText("Artifacts/AgentSupport/unity-player-results.xml", result.ToXml().OuterXml);
Debug.Log("Shrink Player acceptance: " + result.PassCount + " passed, " + result.FailCount + " failed.");
}
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 84cf42c70f18cd84b9cdca62969b6af8
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,11 @@
{
"name": "ShrinkSDK.AgentSupport.EditorTests",
"references": [],
"includePlatforms": [
"Editor"
],
"optionalUnityReferences": [
"TestAssemblies"
],
"autoReferenced": false
}
@@ -0,0 +1,7 @@
fileFormatVersion: 2
guid: c7d77cac6e21f93479774330dd7bda65
AssemblyDefinitionImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:
+8
View File
@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: 566d8dd60d1f29a42857bf011769527c
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,120 @@
using System;
using System.Collections;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using Cysharp.Threading.Tasks;
using NUnit.Framework;
using ShrinkContext;
using ShrinkNetwork;
using ShrinkEventBus;
using UnityEngine.TestTools;
public sealed class PlayerContracts
{
[Test]
public void DotNetGoldenFrameIsIdenticalInPlayer()
{
var bytes = UnityEngine.Resources.Load<UnityEngine.TextAsset>("ShrinkV2Golden").bytes;
var packet = ShrinkPacketCodec.Decode(bytes);
var codec = new ShrinkJsonNetworkSerializer();
var message = (Message)codec.Deserialize(packet.Payload, typeof(Message));
Assert.That(message.Text, Is.EqualTo("跨宿主-v2"));
using var encoded = ShrinkPacketCodec.Encode(new ShrinkNetworkPacket { Opcode = 42, Route = "跨宿主" }, codec, message);
Assert.That(encoded.WrittenMemory.ToArray(), Is.EqualTo(bytes));
}
[Test]
public void BinaryEnvelopeAndLegacyRejection()
{
var codec = new ShrinkJsonNetworkSerializer();
foreach (var size in new[] { 64, 1024, 32768 })
{
var message = new Message { Text = new string('x', size) };
using var frame = ShrinkPacketCodec.Encode(new ShrinkNetworkPacket { Opcode = 42, Route = "跨宿主" }, codec, message);
var packet = ShrinkPacketCodec.Decode(frame.WrittenMemory);
Assert.That(((Message)codec.Deserialize(packet.Payload, typeof(Message))).Text, Is.EqualTo(message.Text));
var copy = frame.WrittenMemory.ToArray(); copy[4] = 1;
Assert.Throws<ShrinkProtocolException>(() => ShrinkPacketCodec.Decode(copy));
}
}
[UnityTest]
public IEnumerator ContextTenThousandRetirements() => UniTask.ToCoroutine(async () =>
{
var runtime = new ShrinkContextRuntime(16);
var watch = Stopwatch.StartNew();
for (var i = 0; i < 10000; i++) await runtime.RetireAsync(runtime.Use(new Provider()));
Assert.That(runtime.Fibers.Count, Is.Zero);
Assert.That(runtime.CaptureDiagnostic().Fibers.Count, Is.EqualTo(16));
UnityEngine.Debug.Log("PLAYER Context 10000 retirements ms=" + watch.Elapsed.TotalMilliseconds);
await runtime.ShutdownAsync();
});
[Test]
public void EventDispatchAllocations()
{
using var binding = ShrinkGeneratedBinding.Subscribe<Event>(ShrinkGeneratedBinding.RuntimeResolver, null, null, null, _ => { }, ShrinkEventPriority.Normal, 0, false);
for (var i = 0; i < 100; i++) EventBus.Post(new Event());
var watch = Stopwatch.StartNew();
for (var i = 0; i < 10000; i++) EventBus.Post(new Event());
watch.Stop();
var events=PlayerPerformance.AllocationEvents(() => EventBus.Post(new Event()),10000);
UnityEngine.Debug.Log("PLAYER EventBus 10000 posts allocationEvents="+events+" ms="+watch.Elapsed.TotalMilliseconds);
using var host=new ShrinkEventBusHost();
var local=host.CreateBus(ShrinkBusKey.Game,ShrinkBusOptions.Inline());
using var localBinding=ShrinkGeneratedBinding.Subscribe<Event>(host,null,null,null,_=>{},ShrinkEventPriority.Normal,0,false);
for(var i=0;i<100;i++) local.Post(new Event());
var isolatedEvents=PlayerPerformance.AllocationEvents(()=>local.Post(new Event()),10000);
UnityEngine.Debug.Log("PLAYER EventBus isolated 10000 posts allocationEvents="+isolatedEvents);
Assert.That(isolatedEvents,Is.Zero);
Assert.Throws<ArgumentNullException>(()=>local.Post<ReferenceEvent>(null));
}
[UnityTest]
public IEnumerator LoopbackRpc() => UniTask.ToCoroutine(async () =>
{
var a = new ShrinkLoopbackTransport(); var b = new ShrinkLoopbackTransport(); a.LinkPeer(b);
var client = new ShrinkNetworkService(); var server = new ShrinkNetworkService();
foreach (var service in new[] { client, server }) { service.RegisterMessage<Request>(1); service.RegisterMessage<Response>(2); }
server.RegisterRequestHandler<Request, Response>((_, r) => UniTask.FromResult(new Response { Text = r.Text }));
client.BindTransport(a); server.BindTransport(b); a.OpenSession(1);
try { Assert.That((await client.CallAsync<Request, Response>(client.Sessions[1], new Request { Text = "Unity Player" })).Text, Is.EqualTo("Unity Player")); }
finally { a.Stop(); b.Stop(); }
});
[UnityTest]
public IEnumerator SavePhaseMatrix() => UniTask.ToCoroutine(async () =>
{
var root = System.IO.Path.Combine(System.IO.Path.GetTempPath(), "ShrinkPlayerSave-" + Guid.NewGuid().ToString("N"));
var storage = new ShrinkDataSaver.LocalStorageProvider(root);
var initialize = typeof(ShrinkDataSaver.ShrinkSave).GetMethod("Initialize", System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Static);
initialize.Invoke(null, new object[] { storage, System.IO.Path.Combine(root, "saves"), ".sav", 1 });
try
{
foreach (var size in new[] { 100 * 1024, 1024 * 1024, 10 * 1024 * 1024 })
foreach (var background in new[] { false, true })
{
var value = new string('s', size); var owner = System.Threading.Thread.CurrentThread.ManagedThreadId; var captureThread = -1;
ShrinkDataSaver.ShrinkSave.RegisterModule("player-benchmark", () => { captureThread = System.Threading.Thread.CurrentThread.ManagedThreadId; return value; }, _ => { });
var options = new ShrinkDataSaver.SaveOptions { EncodeInBackground = background, CollectTimings = true };
await ShrinkDataSaver.ShrinkSave.SaveSlotAsync(0, options);
Assert.That(captureThread, Is.EqualTo(owner));
Assert.That(options.LastTimings.EncodedBytes, Is.GreaterThan(size));
UnityEngine.Debug.Log("PLAYER Save " + size + " background=" + background + " " + "captureMs=" + options.LastTimings.CaptureMilliseconds + " encodeMs=" + options.LastTimings.EncodeMilliseconds + " encryptMs=" + options.LastTimings.EncryptMilliseconds + " writeMs=" + options.LastTimings.WriteMilliseconds + " bytes=" + options.LastTimings.EncodedBytes);
}
}
finally
{
ShrinkDataSaver.ShrinkSave.UnregisterModule("player-benchmark");
if (System.IO.Directory.Exists(root)) System.IO.Directory.Delete(root, true);
}
});
public sealed class Message : IShrinkNetworkMessage { public string Text { get; set; } }
public sealed class Request : IShrinkNetworkRequest { public string Text { get; set; } }
public sealed class Response : ShrinkRpcResponseBase { public string Text { get; set; } }
public readonly struct Event : IShrinkEvent { }
public sealed class ReferenceEvent : IShrinkEvent { }
private sealed class Provider : IShrinkComponent
{
public string Name => "test";
public IReadOnlyList<string> Inject => Array.Empty<string>();
public IReadOnlyList<string> Provide => new[] { "value" };
public UniTask ApplyAsync(ShrinkCtx ctx, object config) { ctx.Set("value", this); return UniTask.CompletedTask; }
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 531de603668484545b5993c8f4894027
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -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;}
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 1a11773fcfc6a5547942e45a40ce1c30
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: 04969664a502f284e94ccfd427fc052c
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,7 @@
fileFormatVersion: 2
guid: 7f13c1997933654439e4d4ffa612ae4c
TextScriptImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,17 @@
{
"name": "ShrinkSDK.AgentSupport.PlayerTests",
"references": [
"ShrinkContext.Core.Runtime",
"ShrinkEventBus.Runtime",
"ShrinkNetwork.Runtime",
"UniTask",
"ShrinkDataSaver.Runtime"
],
"optionalUnityReferences": [
"TestAssemblies"
],
"defineConstraints": [
"UNITY_INCLUDE_TESTS"
],
"autoReferenced": false
}
@@ -0,0 +1,7 @@
fileFormatVersion: 2
guid: ec966393c67534f4d9331d20dae2a555
AssemblyDefinitionImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant: