feat: integrate performance upgrade and agent inspection tooling
This commit is contained in:
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 2319a25c4ac4b0941824ffb1fbfadcf4
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -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:
|
||||
@@ -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:
|
||||
Binary file not shown.
@@ -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:
|
||||
Submodule Assets/Modules/ShrinkApp.Core updated: 59d1a6c085...5efc172bca
Submodule Assets/Modules/ShrinkApp.Starter.Basic updated: c210428cb0...fd3788e2ce
Submodule Assets/Modules/ShrinkCommand updated: 5414a747e5...174b9c6976
Submodule Assets/Modules/ShrinkCommand.Integration.App updated: 4b621a0ca2...25f4cfb82e
Submodule Assets/Modules/ShrinkCommand.Integration.EventBus updated: 2880c25888...82c0833a11
Submodule Assets/Modules/ShrinkCommand.Integration.Network updated: af97af183b...b1c42ea4ab
Submodule Assets/Modules/ShrinkContext.AppAdapter updated: 789aa2181b...7a5071217d
Submodule Assets/Modules/ShrinkContext.Core updated: b93f3e6122...d3eca1f155
Submodule Assets/Modules/ShrinkContext.EventBusAdapter updated: 254625e791...92f55f4bdf
Submodule Assets/Modules/ShrinkDataSaver updated: d26f1a94ef...cdb682f484
Submodule Assets/Modules/ShrinkDataSaver.Integration.App updated: 2b2b808aa3...fea752a8ff
Submodule Assets/Modules/ShrinkDataSaver.Integration.EventBus updated: 15b511feb6...d6b1940982
Submodule Assets/Modules/ShrinkEventBus updated: c44a7e6dda...a92c9d2cea
Submodule Assets/Modules/ShrinkEventBus.Entities updated: b888d8d247...c19d536582
Submodule Assets/Modules/ShrinkInstaller updated: 30a6a11344...2e03045423
Submodule Assets/Modules/ShrinkModFramework updated: af76458da0...91f0141b86
Submodule Assets/Modules/ShrinkNetwork updated: c7c45b26f8...93c2b8520b
Submodule Assets/Modules/ShrinkNetwork.Integration.App updated: fa77a9ea96...123bd170d9
Submodule Assets/Modules/ShrinkNetwork.Integration.EventBus updated: 66f17aae85...2dae3e3890
Submodule Assets/Modules/ShrinkRuntime.Abstractions updated: 55bc7a4e75...52b92d3657
Submodule Assets/Modules/ShrinkShared.CodeGen updated: ab6617eda4...c669ee5956
Submodule Assets/Modules/ShrinkTutorial updated: 1b638dbad5...51c941a017
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 13ee6beaa5cda4a4da34954ba6f00e7a
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,11 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<PropertyGroup><TargetFramework>netstandard2.1</TargetFramework><LangVersion>latest</LangVersion><Nullable>enable</Nullable><EnableDefaultCompileItems>false</EnableDefaultCompileItems><PackageId>ShrinkSDK.Inspection</PackageId><Version>0.1.0</Version><AssemblyName>ShrinkInspection.Runtime</AssemblyName></PropertyGroup>
|
||||
<ItemGroup>
|
||||
<Compile Include="../Runtime/**/*.cs" />
|
||||
<PackageReference Include="Newtonsoft.Json" Version="13.0.3" />
|
||||
<PackageReference Include="ShrinkSDK.Context.Core" Version="0.3.0" />
|
||||
<PackageReference Include="ShrinkSDK.EventBus" Version="2.2.2" />
|
||||
<PackageReference Include="ShrinkSDK.Network" Version="0.4.2" />
|
||||
<PackageReference Include="ShrinkSDK.Command" Version="0.4.2" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 2274a33f3eae9774db82c4e478f2f90d
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"name": "ShrinkInspection.Editor",
|
||||
"references": ["ShrinkInspection.Runtime", "ShrinkContext.Core.Runtime", "ShrinkEventBus.Runtime", "Newtonsoft.Json"],
|
||||
"includePlatforms": ["Editor"],
|
||||
"autoReferenced": true
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 8a20e065e3e35a94e9e3c1c29328219f
|
||||
AssemblyDefinitionImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,61 @@
|
||||
#nullable enable
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Collections.Generic;
|
||||
using Newtonsoft.Json;
|
||||
using UnityEditor;
|
||||
using UnityEditor.Compilation;
|
||||
using UnityEngine;
|
||||
using ShrinkContext;
|
||||
using ShrinkEventBus;
|
||||
|
||||
namespace ShrinkSDK.Inspection.Editor
|
||||
{
|
||||
public static class ShrinkInspectionExport
|
||||
{
|
||||
private const string DirectoryName = "Artifacts/Inspection";
|
||||
/// <summary>Host extensions add explicitly owned Network/Command/Context instances. Remove handlers when the host shuts down.</summary>
|
||||
public static event Action<ShrinkInspectionSnapshot>? CaptureHostState;
|
||||
|
||||
[MenuItem("Tools/ShrinkSDK/Inspection/Export compilation inputs")]
|
||||
public static void ExportCompilation()
|
||||
{
|
||||
RequireStableEditor();
|
||||
var root = Path.GetFullPath(".");
|
||||
var assemblies = CompilationPipeline.GetAssemblies(AssembliesType.Player);
|
||||
var loaded = AppDomain.CurrentDomain.GetAssemblies().Where(a => !a.IsDynamic).Select(a => a.GetName().Name).ToHashSet();
|
||||
var identityNames = assemblies.Select(a => a.name).Where(loaded.Contains).OrderBy(n => n, StringComparer.Ordinal).ToArray();
|
||||
var inputs = assemblies.Select(a => new { Name = a.name, Sources = a.sourceFiles.Select(Path.GetFullPath).ToArray(),
|
||||
References = a.allReferences.Select(Path.GetFullPath).Where(File.Exists).ToArray(), Defines = a.defines, Output = Path.GetFullPath(a.outputPath) }).ToArray();
|
||||
var ticks = inputs.SelectMany(a => a.Sources.Concat(a.References)).Distinct(StringComparer.OrdinalIgnoreCase)
|
||||
.ToDictionary(path => path, path => File.GetLastWriteTimeUtc(path).Ticks);
|
||||
Directory.CreateDirectory(DirectoryName);
|
||||
File.WriteAllText(DirectoryName + "/compilation.json", JsonConvert.SerializeObject(new { Root = root,
|
||||
BuildId = ShrinkInspectionSnapshot.BuildIdentity(identityNames), IdentityAssemblies = identityNames, Assemblies = inputs, SourceTicks = ticks }, Formatting.Indented));
|
||||
Debug.Log("Shrink inspection compilation exported: " + Path.GetFullPath(DirectoryName + "/compilation.json"));
|
||||
}
|
||||
|
||||
[MenuItem("Tools/ShrinkSDK/Inspection/Export runtime snapshot")]
|
||||
public static void ExportRuntime()
|
||||
{
|
||||
RequireStableEditor();
|
||||
var path = DirectoryName + "/compilation.json";
|
||||
if (!File.Exists(path)) throw new InvalidOperationException("Export compilation inputs first.");
|
||||
var manifest = JsonConvert.DeserializeObject<IdentityManifest>(File.ReadAllText(path))!;
|
||||
var snapshot = new ShrinkInspectionSnapshot(ShrinkInspectionSnapshot.BuildIdentity(manifest.IdentityAssemblies));
|
||||
snapshot.AddContext("default-context", ShrinkContextDefaults.Default);
|
||||
var ordinal = 0;
|
||||
foreach (var bus in EventBus.CaptureNamedBuses()) snapshot.AddEventBus("named-bus-" + ordinal++, bus);
|
||||
snapshot.Coverage.Add("Default Context and named EventBus instances only; host-owned instances require CaptureHostState registration.");
|
||||
CaptureHostState?.Invoke(snapshot);
|
||||
File.WriteAllText(DirectoryName + "/runtime.json", snapshot.ToJson());
|
||||
Debug.Log("Shrink inspection runtime exported: " + Path.GetFullPath(DirectoryName + "/runtime.json"));
|
||||
}
|
||||
private static void RequireStableEditor()
|
||||
{
|
||||
if (EditorApplication.isCompiling || EditorApplication.isUpdating) throw new InvalidOperationException("Wait for Editor compilation/import to finish.");
|
||||
}
|
||||
private sealed class IdentityManifest { public string[] IdentityAssemblies = Array.Empty<string>(); }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 35c6f0febfce34543a9efb401ce8b3b6
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,23 @@
|
||||
# Shrink Inspection
|
||||
|
||||
可选诊断接入包,提供 Unity 编译输入导出和跨 Unity/Godot/.NET 的按需绑定快照。它依赖被观察模块,不进入它们的热路径。
|
||||
|
||||
Unity 安装本目录的 UPM 包后,使用 `Tools/ShrinkSDK/Inspection` 导出编译输入和运行快照。结果位于项目的 `Artifacts/Inspection`。在命令行使用:
|
||||
|
||||
```text
|
||||
shrink inspect flow command:execute --compilation Artifacts/Inspection/compilation.json --snapshot Artifacts/Inspection/runtime.json
|
||||
```
|
||||
|
||||
编辑器默认只捕获默认 Context 与命名 EventBus。宿主持有的独立实例通过 `ShrinkInspectionExport.CaptureHostState` 按需加入;宿主退出时取消该委托,避免诊断代码持有已退役对象。`AddContext` 可同时传入 Loader,记录最近事务的阶段和恢复结果。
|
||||
|
||||
Godot/.NET 使用 `ShrinkSDK.Inspection` 和相同的 `ShrinkInspectionSnapshot` API。宿主在所属线程创建快照,调用 `AddContext`、`AddNetwork`、`AddEventBus`、`AddCommands`,再写入 `ToJson()`。构建标识由明确的程序集名集合计算;必须与编译输入的程序集集合和 MVID 一致。未知构建不能通过手写相同字符串伪装为已验证关联。
|
||||
|
||||
捕获时暂停同一实例的注册、退役和组合修改。快照只含符号、状态和计数;不含消息正文、会话令牌、存档内容及异常文本。它记录快照时刻的绑定,不提供完整调用时序。不同 scope 的绑定不能拼成已经确认的端到端调用。
|
||||
|
||||
<!-- shrink:capabilities -->
|
||||
Capability: 导出真实编译输入、Context/provider 状态、EventBus/Network/Command 当前绑定及队列指标
|
||||
Aliases: 导航 调试 诊断 快照 inspect snapshot 调用链
|
||||
Limits: 按需快照,不是调用时序追踪;只覆盖显式加入的实例;过期或不同构建不能合并
|
||||
Extension: 宿主在所属线程调用 AddContext/AddNetwork/AddEventBus/AddCommands,Unity 使用 CaptureHostState
|
||||
Evidence: [快照 API](Runtime/ShrinkInspectionSnapshot.cs); [Unity 导出](Editor/ShrinkInspectionExport.cs)
|
||||
<!-- /shrink:capabilities -->
|
||||
@@ -0,0 +1,7 @@
|
||||
fileFormatVersion: 2
|
||||
guid: f566b821eafe5fa498c7504ccf0d40aa
|
||||
TextScriptImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 87749fd61a2f5e44aab3896a7a873b38
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"name": "ShrinkInspection.Runtime",
|
||||
"references": ["ShrinkContext.Core.Runtime", "ShrinkEventBus.Runtime", "ShrinkNetwork.Runtime", "ShrinkCommand.Runtime", "Newtonsoft.Json"],
|
||||
"autoReferenced": true,
|
||||
"noEngineReferences": true
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 4210356b49bcdf240ad3c08659b7f720
|
||||
AssemblyDefinitionImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,102 @@
|
||||
#nullable enable
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Reflection;
|
||||
using Newtonsoft.Json;
|
||||
using ShrinkContext;
|
||||
using ShrinkEventBus;
|
||||
using ShrinkNetwork;
|
||||
using ShrinkCommand;
|
||||
|
||||
namespace ShrinkSDK.Inspection
|
||||
{
|
||||
public sealed class ShrinkInspectionRelation
|
||||
{
|
||||
public string From = "";
|
||||
public string To = "";
|
||||
public string Kind = "";
|
||||
public string Scope = "";
|
||||
}
|
||||
|
||||
/// <summary>Explicit, on-demand snapshot. Call from the owning host thread while registration/composition is stable.</summary>
|
||||
public sealed class ShrinkInspectionSnapshot
|
||||
{
|
||||
public int FormatVersion = 1;
|
||||
public string BuildId = "";
|
||||
public string CapturedAtUtc = DateTime.UtcNow.ToString("O");
|
||||
public readonly List<string> Coverage = new();
|
||||
public readonly List<ShrinkInspectionRelation> Relations = new();
|
||||
public readonly Dictionary<string, object> State = new();
|
||||
|
||||
public static string BuildIdentity(IEnumerable<string> assemblyNames)
|
||||
{
|
||||
var loaded = AppDomain.CurrentDomain.GetAssemblies().Where(a => !a.IsDynamic).ToArray();
|
||||
var identity = string.Join(";", assemblyNames.Distinct().OrderBy(n => n, StringComparer.Ordinal).Select(name =>
|
||||
{
|
||||
var candidates = loaded.Where(a => a.GetName().Name == name).ToArray();
|
||||
if (candidates.Length != 1) throw new InvalidOperationException("Cannot establish unique runtime assembly identity: " + name);
|
||||
return name + ":" + candidates[0].ManifestModule.ModuleVersionId.ToString("N");
|
||||
}));
|
||||
using var sha = System.Security.Cryptography.SHA256.Create();
|
||||
return BitConverter.ToString(sha.ComputeHash(System.Text.Encoding.UTF8.GetBytes(identity))).Replace("-", "").ToLowerInvariant();
|
||||
}
|
||||
|
||||
public ShrinkInspectionSnapshot(string buildId) { BuildId = buildId; }
|
||||
public string ToJson() => JsonConvert.SerializeObject(this, Formatting.Indented, new Newtonsoft.Json.Converters.StringEnumConverter());
|
||||
|
||||
public void AddContext(string scope, ShrinkContextRuntime runtime, ShrinkContextLoader? loader = null)
|
||||
{
|
||||
var snapshot = runtime.CaptureDiagnostic();
|
||||
State.Add(scope, new { snapshot.BindingCount, snapshot.Notifications,
|
||||
Fibers = snapshot.Fibers.Select(f => new { f.Uid, f.Name, f.Status, f.Retired, f.InTransition, f.Inject, f.Provide, f.Dependencies, f.ErrorType }).ToArray(),
|
||||
Transaction = loader?.LastTransaction is { } t ? new { t.Generation, t.Phase, t.CurrentEntryId, t.ErrorType, t.RestoreAttempted, t.PreviousCompositionRestored, t.RestoreErrorType } : null });
|
||||
foreach (var fiber in snapshot.Fibers.Where(f => !f.Retired))
|
||||
foreach (var dependency in fiber.Dependencies)
|
||||
{
|
||||
Add("fiber:" + scope + ":" + fiber.Uid, "key:" + dependency.Key, "requires@" + dependency.Realm, scope);
|
||||
if (dependency.CurrentProviderUid.HasValue)
|
||||
Add("fiber:" + scope + ":" + dependency.CurrentProviderUid, "fiber:" + scope + ":" + fiber.Uid, "provider:" + dependency.Key + "@" + dependency.Realm, scope);
|
||||
}
|
||||
}
|
||||
|
||||
public void AddNetwork(string scope, ShrinkNetworkService service)
|
||||
{
|
||||
State.Add(scope, new { Metrics = service.GetDiagnosticsSnapshot(), Queue = service.DispatchScheduler is ShrinkNetworkDispatchQueue queued ? queued.CaptureDiagnostics() : (service.DispatchScheduler as ShrinkNetworkInlineDispatchScheduler)?.CaptureDiagnostics() });
|
||||
foreach (var meta in service.MessageRegistry.Registrations)
|
||||
{
|
||||
Add("opcode:" + meta.Opcode, "type:" + TypeName(meta.MessageType), "message", scope);
|
||||
if (!string.IsNullOrEmpty(meta.Route)) Add("route:" + meta.Route, "type:" + TypeName(meta.MessageType), "message", scope);
|
||||
}
|
||||
foreach (var pair in service.Router.CaptureBindings()) Add("type:" + TypeName(pair.Key), MethodId(pair.Value), "handler", scope);
|
||||
}
|
||||
|
||||
public void AddEventBus(string scope, IShrinkEventBus bus)
|
||||
{
|
||||
try
|
||||
{
|
||||
var bindings = EventBus.CaptureBindings(bus);
|
||||
State.Add(scope, new { Bus = bus.Key.ToString(), Subscribers = bindings.Count });
|
||||
foreach (var binding in bindings) Add("type:" + TypeName(binding.EventType), MethodId(binding.Handler), "subscribes", scope);
|
||||
}
|
||||
catch (NotSupportedException) { Coverage.Add(scope + ": custom EventBus does not expose bindings."); }
|
||||
}
|
||||
|
||||
public void AddCommands(string scope, ShrinkCommandService service)
|
||||
{
|
||||
State.Add(scope, new { Registrations = service.Commands.Select(c => new { c.Path, c.Aliases, c.Permission, c.SourceKind }).ToArray() });
|
||||
foreach (var binding in service.CaptureBindings()) Add("command:" + binding.Path, MethodId(binding.Handler), "command", scope);
|
||||
}
|
||||
|
||||
private void Add(string from, string to, string kind, string scope) => Relations.Add(new ShrinkInspectionRelation { From = from, To = to, Kind = kind, Scope = scope });
|
||||
private static string TypeName(Type type) => (type.FullName ?? type.Name).Replace('+', '.');
|
||||
public static string MethodId(MethodInfo method)
|
||||
{
|
||||
// Generated wrappers and generic signatures remain runtime-only endpoints rather than guessed source matches.
|
||||
if (method.IsGenericMethod || method.DeclaringType?.IsGenericType == true || method.GetParameters().Any(p => p.ParameterType.IsGenericType || p.ParameterType.IsArray || p.ParameterType.IsByRef))
|
||||
return "runtime-method:" + method.Module.ModuleVersionId.ToString("N") + ":" + method.MetadataToken;
|
||||
var parameters = method.GetParameters();
|
||||
return "M:" + TypeName(method.DeclaringType!) + "." + method.Name + (parameters.Length == 0 ? "" : "(" + string.Join(",", parameters.Select(p => TypeName(p.ParameterType))) + ")");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: a412b2f6a531a33419d270e47853ef05
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,13 @@
|
||||
{
|
||||
"name": "com.cneicy.shrink-inspection",
|
||||
"version": "0.1.0",
|
||||
"displayName": "Shrink Inspection",
|
||||
"unity": "2022.3",
|
||||
"description": "On-demand build inputs and bounded runtime binding snapshots for shrink inspect.",
|
||||
"dependencies": {
|
||||
"com.cneicy.shrink-context-core": "0.3.0",
|
||||
"com.cneicy.shrink-eventbus": "2.2.2",
|
||||
"com.cneicy.shrink-network": "0.4.2",
|
||||
"com.cneicy.shrink-command": "0.4.2"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
fileFormatVersion: 2
|
||||
guid: fdb80de9a8588bc4392a94dd7352be58
|
||||
TextScriptImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
Reference in New Issue
Block a user