diff --git a/.gitignore b/.gitignore index 526f409..a5adde4 100644 --- a/.gitignore +++ b/.gitignore @@ -45,6 +45,9 @@ ExportedObj/ *.unityproj *.sln *.csproj +!Tools/**/*.csproj +Tools/**/bin/ +Tools/**/obj/ *.suo *.tmp *.user @@ -146,3 +149,8 @@ GeneratedServers/ExampleGreedMod.Rules/* !GeneratedServers/ExampleGreedMod.Rules/pack-mod.ps1 GeneratedModSdk/ .mimosa/ + +!Assets/ShrinkInspection/DotNet~/*.csproj + +Assets/ShrinkInspection/DotNet~/bin/ +Assets/ShrinkInspection/DotNet~/obj/ diff --git a/Assets/AgentSupportTests.meta b/Assets/AgentSupportTests.meta new file mode 100644 index 0000000..04a47fd --- /dev/null +++ b/Assets/AgentSupportTests.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 2319a25c4ac4b0941824ffb1fbfadcf4 +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/AgentSupportTests/Editor.meta b/Assets/AgentSupportTests/Editor.meta new file mode 100644 index 0000000..2d4e267 --- /dev/null +++ b/Assets/AgentSupportTests/Editor.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 908affa5f1a95214fa21a165799e7e56 +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/AgentSupportTests/Editor/PlayerAcceptance.cs b/Assets/AgentSupportTests/Editor/PlayerAcceptance.cs new file mode 100644 index 0000000..77c200a --- /dev/null +++ b/Assets/AgentSupportTests/Editor/PlayerAcceptance.cs @@ -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(); 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."); + } + } +} diff --git a/Assets/AgentSupportTests/Editor/PlayerAcceptance.cs.meta b/Assets/AgentSupportTests/Editor/PlayerAcceptance.cs.meta new file mode 100644 index 0000000..2bec7b5 --- /dev/null +++ b/Assets/AgentSupportTests/Editor/PlayerAcceptance.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 84cf42c70f18cd84b9cdca62969b6af8 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/AgentSupportTests/Editor/ShrinkSDK.AgentSupport.EditorTests.asmdef b/Assets/AgentSupportTests/Editor/ShrinkSDK.AgentSupport.EditorTests.asmdef new file mode 100644 index 0000000..b30c4c5 --- /dev/null +++ b/Assets/AgentSupportTests/Editor/ShrinkSDK.AgentSupport.EditorTests.asmdef @@ -0,0 +1,11 @@ +{ + "name": "ShrinkSDK.AgentSupport.EditorTests", + "references": [], + "includePlatforms": [ + "Editor" + ], + "optionalUnityReferences": [ + "TestAssemblies" + ], + "autoReferenced": false +} \ No newline at end of file diff --git a/Assets/AgentSupportTests/Editor/ShrinkSDK.AgentSupport.EditorTests.asmdef.meta b/Assets/AgentSupportTests/Editor/ShrinkSDK.AgentSupport.EditorTests.asmdef.meta new file mode 100644 index 0000000..45bffdc --- /dev/null +++ b/Assets/AgentSupportTests/Editor/ShrinkSDK.AgentSupport.EditorTests.asmdef.meta @@ -0,0 +1,7 @@ +fileFormatVersion: 2 +guid: c7d77cac6e21f93479774330dd7bda65 +AssemblyDefinitionImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/AgentSupportTests/Runtime.meta b/Assets/AgentSupportTests/Runtime.meta new file mode 100644 index 0000000..0029724 --- /dev/null +++ b/Assets/AgentSupportTests/Runtime.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 566d8dd60d1f29a42857bf011769527c +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/AgentSupportTests/Runtime/PlayerContracts.cs b/Assets/AgentSupportTests/Runtime/PlayerContracts.cs new file mode 100644 index 0000000..59207ca --- /dev/null +++ b/Assets/AgentSupportTests/Runtime/PlayerContracts.cs @@ -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("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(() => 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(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(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(()=>local.Post(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(1); service.RegisterMessage(2); } + server.RegisterRequestHandler((_, r) => UniTask.FromResult(new Response { Text = r.Text })); + client.BindTransport(a); server.BindTransport(b); a.OpenSession(1); + try { Assert.That((await client.CallAsync(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 Inject => Array.Empty(); + public IReadOnlyList Provide => new[] { "value" }; + public UniTask ApplyAsync(ShrinkCtx ctx, object config) { ctx.Set("value", this); return UniTask.CompletedTask; } + } +} diff --git a/Assets/AgentSupportTests/Runtime/PlayerContracts.cs.meta b/Assets/AgentSupportTests/Runtime/PlayerContracts.cs.meta new file mode 100644 index 0000000..d71ba98 --- /dev/null +++ b/Assets/AgentSupportTests/Runtime/PlayerContracts.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 531de603668484545b5993c8f4894027 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/AgentSupportTests/Runtime/PlayerPerformance.cs b/Assets/AgentSupportTests/Runtime/PlayerPerformance.cs new file mode 100644 index 0000000..4e7348d --- /dev/null +++ b/Assets/AgentSupportTests/Runtime/PlayerPerformance.cs @@ -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 _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;} + } +} diff --git a/Assets/AgentSupportTests/Runtime/PlayerPerformance.cs.meta b/Assets/AgentSupportTests/Runtime/PlayerPerformance.cs.meta new file mode 100644 index 0000000..d7a6691 --- /dev/null +++ b/Assets/AgentSupportTests/Runtime/PlayerPerformance.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 1a11773fcfc6a5547942e45a40ce1c30 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/AgentSupportTests/Runtime/Resources.meta b/Assets/AgentSupportTests/Runtime/Resources.meta new file mode 100644 index 0000000..f114e50 --- /dev/null +++ b/Assets/AgentSupportTests/Runtime/Resources.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 04969664a502f284e94ccfd427fc052c +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/AgentSupportTests/Runtime/Resources/ShrinkV2Golden.bytes b/Assets/AgentSupportTests/Runtime/Resources/ShrinkV2Golden.bytes new file mode 100644 index 0000000..901517a Binary files /dev/null and b/Assets/AgentSupportTests/Runtime/Resources/ShrinkV2Golden.bytes differ diff --git a/Assets/AgentSupportTests/Runtime/Resources/ShrinkV2Golden.bytes.meta b/Assets/AgentSupportTests/Runtime/Resources/ShrinkV2Golden.bytes.meta new file mode 100644 index 0000000..d73c5c8 --- /dev/null +++ b/Assets/AgentSupportTests/Runtime/Resources/ShrinkV2Golden.bytes.meta @@ -0,0 +1,7 @@ +fileFormatVersion: 2 +guid: 7f13c1997933654439e4d4ffa612ae4c +TextScriptImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/AgentSupportTests/Runtime/ShrinkSDK.AgentSupport.PlayerTests.asmdef b/Assets/AgentSupportTests/Runtime/ShrinkSDK.AgentSupport.PlayerTests.asmdef new file mode 100644 index 0000000..a6fba42 --- /dev/null +++ b/Assets/AgentSupportTests/Runtime/ShrinkSDK.AgentSupport.PlayerTests.asmdef @@ -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 +} diff --git a/Assets/AgentSupportTests/Runtime/ShrinkSDK.AgentSupport.PlayerTests.asmdef.meta b/Assets/AgentSupportTests/Runtime/ShrinkSDK.AgentSupport.PlayerTests.asmdef.meta new file mode 100644 index 0000000..334593f --- /dev/null +++ b/Assets/AgentSupportTests/Runtime/ShrinkSDK.AgentSupport.PlayerTests.asmdef.meta @@ -0,0 +1,7 @@ +fileFormatVersion: 2 +guid: ec966393c67534f4d9331d20dae2a555 +AssemblyDefinitionImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Modules/ShrinkApp.Core b/Assets/Modules/ShrinkApp.Core index 59d1a6c..5efc172 160000 --- a/Assets/Modules/ShrinkApp.Core +++ b/Assets/Modules/ShrinkApp.Core @@ -1 +1 @@ -Subproject commit 59d1a6c0854e81b2d5142bcb17c6fac2c2992bcf +Subproject commit 5efc172bca69305259b4ea629d933613e0047798 diff --git a/Assets/Modules/ShrinkApp.Starter.Basic b/Assets/Modules/ShrinkApp.Starter.Basic index c210428..fd3788e 160000 --- a/Assets/Modules/ShrinkApp.Starter.Basic +++ b/Assets/Modules/ShrinkApp.Starter.Basic @@ -1 +1 @@ -Subproject commit c210428cb05f05de758f9d264671fe28d07fb6b5 +Subproject commit fd3788e2cec064c618eb77af719c0008ca387c8b diff --git a/Assets/Modules/ShrinkCommand b/Assets/Modules/ShrinkCommand index 5414a74..174b9c6 160000 --- a/Assets/Modules/ShrinkCommand +++ b/Assets/Modules/ShrinkCommand @@ -1 +1 @@ -Subproject commit 5414a747e518205e986255f98a546d14a2b144ef +Subproject commit 174b9c6976d448c535d29393d8ebead7b7a08b3a diff --git a/Assets/Modules/ShrinkCommand.Integration.App b/Assets/Modules/ShrinkCommand.Integration.App index 4b621a0..25f4cfb 160000 --- a/Assets/Modules/ShrinkCommand.Integration.App +++ b/Assets/Modules/ShrinkCommand.Integration.App @@ -1 +1 @@ -Subproject commit 4b621a0ca2c4ac048eaffe158afa8b5e35ca7cf8 +Subproject commit 25f4cfb82e601c70b0cbce64deeb706a7df307f8 diff --git a/Assets/Modules/ShrinkCommand.Integration.EventBus b/Assets/Modules/ShrinkCommand.Integration.EventBus index 2880c25..82c0833 160000 --- a/Assets/Modules/ShrinkCommand.Integration.EventBus +++ b/Assets/Modules/ShrinkCommand.Integration.EventBus @@ -1 +1 @@ -Subproject commit 2880c25888439bdb852fe042d2037ba24f307bdb +Subproject commit 82c0833a1130ab0161631a6867d458597e24d619 diff --git a/Assets/Modules/ShrinkCommand.Integration.Network b/Assets/Modules/ShrinkCommand.Integration.Network index af97af1..b1c42ea 160000 --- a/Assets/Modules/ShrinkCommand.Integration.Network +++ b/Assets/Modules/ShrinkCommand.Integration.Network @@ -1 +1 @@ -Subproject commit af97af183b48291b6f4c4d3bf7f05ce3991ca60a +Subproject commit b1c42ea4ab37479b745fbeeb3d7f2a0ba752ca0a diff --git a/Assets/Modules/ShrinkContext.AppAdapter b/Assets/Modules/ShrinkContext.AppAdapter index 789aa21..7a50712 160000 --- a/Assets/Modules/ShrinkContext.AppAdapter +++ b/Assets/Modules/ShrinkContext.AppAdapter @@ -1 +1 @@ -Subproject commit 789aa2181bfcbb7f4cb33eb1cbaf203f038ee25f +Subproject commit 7a5071217d774666b9cf866e362435e422d1dfd4 diff --git a/Assets/Modules/ShrinkContext.Core b/Assets/Modules/ShrinkContext.Core index b93f3e6..d3eca1f 160000 --- a/Assets/Modules/ShrinkContext.Core +++ b/Assets/Modules/ShrinkContext.Core @@ -1 +1 @@ -Subproject commit b93f3e61223e7572588b67c0d3935073817f0da6 +Subproject commit d3eca1f155bece2f4e1de2184579e655cf7b828e diff --git a/Assets/Modules/ShrinkContext.EventBusAdapter b/Assets/Modules/ShrinkContext.EventBusAdapter index 254625e..92f55f4 160000 --- a/Assets/Modules/ShrinkContext.EventBusAdapter +++ b/Assets/Modules/ShrinkContext.EventBusAdapter @@ -1 +1 @@ -Subproject commit 254625e7919102112a426d327e78445dd36371c2 +Subproject commit 92f55f4bdf5de701489d5ee0033dd7497da2a783 diff --git a/Assets/Modules/ShrinkDataSaver b/Assets/Modules/ShrinkDataSaver index d26f1a9..cdb682f 160000 --- a/Assets/Modules/ShrinkDataSaver +++ b/Assets/Modules/ShrinkDataSaver @@ -1 +1 @@ -Subproject commit d26f1a94ef5ce3a7439e504757f464a17469a4c5 +Subproject commit cdb682f484fd1d8451780de7d4d77f78e260ae13 diff --git a/Assets/Modules/ShrinkDataSaver.Integration.App b/Assets/Modules/ShrinkDataSaver.Integration.App index 2b2b808..fea752a 160000 --- a/Assets/Modules/ShrinkDataSaver.Integration.App +++ b/Assets/Modules/ShrinkDataSaver.Integration.App @@ -1 +1 @@ -Subproject commit 2b2b808aa391be970e9f75962145e94ad6129f8d +Subproject commit fea752a8ff5621c2119966cc2b8655fc38d8449e diff --git a/Assets/Modules/ShrinkDataSaver.Integration.EventBus b/Assets/Modules/ShrinkDataSaver.Integration.EventBus index 15b511f..d6b1940 160000 --- a/Assets/Modules/ShrinkDataSaver.Integration.EventBus +++ b/Assets/Modules/ShrinkDataSaver.Integration.EventBus @@ -1 +1 @@ -Subproject commit 15b511feb639fe248485e85aab6d6a4a2b5c0cab +Subproject commit d6b1940982de3f3a409c8fc0492871c1d10282fe diff --git a/Assets/Modules/ShrinkEventBus b/Assets/Modules/ShrinkEventBus index c44a7e6..a92c9d2 160000 --- a/Assets/Modules/ShrinkEventBus +++ b/Assets/Modules/ShrinkEventBus @@ -1 +1 @@ -Subproject commit c44a7e6ddaedd865b1956dc6d72211a624d22c70 +Subproject commit a92c9d2cea55577438a49906ed45754bfcff8c1b diff --git a/Assets/Modules/ShrinkEventBus.Entities b/Assets/Modules/ShrinkEventBus.Entities index b888d8d..c19d536 160000 --- a/Assets/Modules/ShrinkEventBus.Entities +++ b/Assets/Modules/ShrinkEventBus.Entities @@ -1 +1 @@ -Subproject commit b888d8d2478333793e9d7c941302d486fc054a5f +Subproject commit c19d5365826d25696fa63a7b5712a1335eee8b6c diff --git a/Assets/Modules/ShrinkInstaller b/Assets/Modules/ShrinkInstaller index 30a6a11..2e03045 160000 --- a/Assets/Modules/ShrinkInstaller +++ b/Assets/Modules/ShrinkInstaller @@ -1 +1 @@ -Subproject commit 30a6a11344b642fd4edc121af9e9ae1027a39eaf +Subproject commit 2e03045423d40007760aa9d23e5002c5b1a465d2 diff --git a/Assets/Modules/ShrinkModFramework b/Assets/Modules/ShrinkModFramework index af76458..91f0141 160000 --- a/Assets/Modules/ShrinkModFramework +++ b/Assets/Modules/ShrinkModFramework @@ -1 +1 @@ -Subproject commit af76458da030c05ff7fa89be8f266ea5e232e846 +Subproject commit 91f0141b865ea02fde063d055e54b892ab426e5e diff --git a/Assets/Modules/ShrinkNetwork b/Assets/Modules/ShrinkNetwork index c7c45b2..93c2b85 160000 --- a/Assets/Modules/ShrinkNetwork +++ b/Assets/Modules/ShrinkNetwork @@ -1 +1 @@ -Subproject commit c7c45b26f89616e6551546fef8e56c6c090515d7 +Subproject commit 93c2b8520baac7d698d2e03c839246b5fd4608f3 diff --git a/Assets/Modules/ShrinkNetwork.Integration.App b/Assets/Modules/ShrinkNetwork.Integration.App index fa77a9e..123bd17 160000 --- a/Assets/Modules/ShrinkNetwork.Integration.App +++ b/Assets/Modules/ShrinkNetwork.Integration.App @@ -1 +1 @@ -Subproject commit fa77a9ea96b4b0fff5ef39d78646fd865b58dfa3 +Subproject commit 123bd170d91ff6f704bb5d38c23ff6bd4a5f7acd diff --git a/Assets/Modules/ShrinkNetwork.Integration.EventBus b/Assets/Modules/ShrinkNetwork.Integration.EventBus index 66f17aa..2dae3e3 160000 --- a/Assets/Modules/ShrinkNetwork.Integration.EventBus +++ b/Assets/Modules/ShrinkNetwork.Integration.EventBus @@ -1 +1 @@ -Subproject commit 66f17aae852e65f63c970ffef5a6487af75f8368 +Subproject commit 2dae3e389026c93298688237d2988115f68b06ac diff --git a/Assets/Modules/ShrinkRuntime.Abstractions b/Assets/Modules/ShrinkRuntime.Abstractions index 55bc7a4..52b92d3 160000 --- a/Assets/Modules/ShrinkRuntime.Abstractions +++ b/Assets/Modules/ShrinkRuntime.Abstractions @@ -1 +1 @@ -Subproject commit 55bc7a4e75c8cdec811becfda42b5cf9af610bf7 +Subproject commit 52b92d365793a1192a2005bd0155d24fcb4df6fa diff --git a/Assets/Modules/ShrinkShared.CodeGen b/Assets/Modules/ShrinkShared.CodeGen index ab6617e..c669ee5 160000 --- a/Assets/Modules/ShrinkShared.CodeGen +++ b/Assets/Modules/ShrinkShared.CodeGen @@ -1 +1 @@ -Subproject commit ab6617eda4c67f18d98abfea1dc4aa9ae4d60b2a +Subproject commit c669ee5956c65a386e7fa185032b452a1ef3d986 diff --git a/Assets/Modules/ShrinkTutorial b/Assets/Modules/ShrinkTutorial index 1b638db..51c941a 160000 --- a/Assets/Modules/ShrinkTutorial +++ b/Assets/Modules/ShrinkTutorial @@ -1 +1 @@ -Subproject commit 1b638dbad57202aa926ee2d6d9a91e4b41c40dc2 +Subproject commit 51c941a0176046fac99ab2425862a3ea4fbcb876 diff --git a/Assets/ShrinkInspection.meta b/Assets/ShrinkInspection.meta new file mode 100644 index 0000000..6c0165b --- /dev/null +++ b/Assets/ShrinkInspection.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 13ee6beaa5cda4a4da34954ba6f00e7a +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/ShrinkInspection/DotNet~/ShrinkSDK.Inspection.csproj b/Assets/ShrinkInspection/DotNet~/ShrinkSDK.Inspection.csproj new file mode 100644 index 0000000..2b79593 --- /dev/null +++ b/Assets/ShrinkInspection/DotNet~/ShrinkSDK.Inspection.csproj @@ -0,0 +1,11 @@ + + netstandard2.1latestenablefalseShrinkSDK.Inspection0.1.0ShrinkInspection.Runtime + + + + + + + + + diff --git a/Assets/ShrinkInspection/Editor.meta b/Assets/ShrinkInspection/Editor.meta new file mode 100644 index 0000000..4f88071 --- /dev/null +++ b/Assets/ShrinkInspection/Editor.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 2274a33f3eae9774db82c4e478f2f90d +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/ShrinkInspection/Editor/ShrinkInspection.Editor.asmdef b/Assets/ShrinkInspection/Editor/ShrinkInspection.Editor.asmdef new file mode 100644 index 0000000..655db71 --- /dev/null +++ b/Assets/ShrinkInspection/Editor/ShrinkInspection.Editor.asmdef @@ -0,0 +1,6 @@ +{ + "name": "ShrinkInspection.Editor", + "references": ["ShrinkInspection.Runtime", "ShrinkContext.Core.Runtime", "ShrinkEventBus.Runtime", "Newtonsoft.Json"], + "includePlatforms": ["Editor"], + "autoReferenced": true +} diff --git a/Assets/ShrinkInspection/Editor/ShrinkInspection.Editor.asmdef.meta b/Assets/ShrinkInspection/Editor/ShrinkInspection.Editor.asmdef.meta new file mode 100644 index 0000000..f8c1a10 --- /dev/null +++ b/Assets/ShrinkInspection/Editor/ShrinkInspection.Editor.asmdef.meta @@ -0,0 +1,7 @@ +fileFormatVersion: 2 +guid: 8a20e065e3e35a94e9e3c1c29328219f +AssemblyDefinitionImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/ShrinkInspection/Editor/ShrinkInspectionExport.cs b/Assets/ShrinkInspection/Editor/ShrinkInspectionExport.cs new file mode 100644 index 0000000..8cda944 --- /dev/null +++ b/Assets/ShrinkInspection/Editor/ShrinkInspectionExport.cs @@ -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"; + /// Host extensions add explicitly owned Network/Command/Context instances. Remove handlers when the host shuts down. + public static event Action? 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(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(); } + } +} diff --git a/Assets/ShrinkInspection/Editor/ShrinkInspectionExport.cs.meta b/Assets/ShrinkInspection/Editor/ShrinkInspectionExport.cs.meta new file mode 100644 index 0000000..1af4cc9 --- /dev/null +++ b/Assets/ShrinkInspection/Editor/ShrinkInspectionExport.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 35c6f0febfce34543a9efb401ce8b3b6 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/ShrinkInspection/README.md b/Assets/ShrinkInspection/README.md new file mode 100644 index 0000000..4fe301f --- /dev/null +++ b/Assets/ShrinkInspection/README.md @@ -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 的绑定不能拼成已经确认的端到端调用。 + + +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) + diff --git a/Assets/ShrinkInspection/README.md.meta b/Assets/ShrinkInspection/README.md.meta new file mode 100644 index 0000000..9cf9445 --- /dev/null +++ b/Assets/ShrinkInspection/README.md.meta @@ -0,0 +1,7 @@ +fileFormatVersion: 2 +guid: f566b821eafe5fa498c7504ccf0d40aa +TextScriptImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/ShrinkInspection/Runtime.meta b/Assets/ShrinkInspection/Runtime.meta new file mode 100644 index 0000000..c93b3f8 --- /dev/null +++ b/Assets/ShrinkInspection/Runtime.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 87749fd61a2f5e44aab3896a7a873b38 +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/ShrinkInspection/Runtime/ShrinkInspection.Runtime.asmdef b/Assets/ShrinkInspection/Runtime/ShrinkInspection.Runtime.asmdef new file mode 100644 index 0000000..35d25ec --- /dev/null +++ b/Assets/ShrinkInspection/Runtime/ShrinkInspection.Runtime.asmdef @@ -0,0 +1,6 @@ +{ + "name": "ShrinkInspection.Runtime", + "references": ["ShrinkContext.Core.Runtime", "ShrinkEventBus.Runtime", "ShrinkNetwork.Runtime", "ShrinkCommand.Runtime", "Newtonsoft.Json"], + "autoReferenced": true, + "noEngineReferences": true +} diff --git a/Assets/ShrinkInspection/Runtime/ShrinkInspection.Runtime.asmdef.meta b/Assets/ShrinkInspection/Runtime/ShrinkInspection.Runtime.asmdef.meta new file mode 100644 index 0000000..16bf366 --- /dev/null +++ b/Assets/ShrinkInspection/Runtime/ShrinkInspection.Runtime.asmdef.meta @@ -0,0 +1,7 @@ +fileFormatVersion: 2 +guid: 4210356b49bcdf240ad3c08659b7f720 +AssemblyDefinitionImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/ShrinkInspection/Runtime/ShrinkInspectionSnapshot.cs b/Assets/ShrinkInspection/Runtime/ShrinkInspectionSnapshot.cs new file mode 100644 index 0000000..259370a --- /dev/null +++ b/Assets/ShrinkInspection/Runtime/ShrinkInspectionSnapshot.cs @@ -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 = ""; + } + + /// Explicit, on-demand snapshot. Call from the owning host thread while registration/composition is stable. + public sealed class ShrinkInspectionSnapshot + { + public int FormatVersion = 1; + public string BuildId = ""; + public string CapturedAtUtc = DateTime.UtcNow.ToString("O"); + public readonly List Coverage = new(); + public readonly List Relations = new(); + public readonly Dictionary State = new(); + + public static string BuildIdentity(IEnumerable 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))) + ")"); + } + } +} diff --git a/Assets/ShrinkInspection/Runtime/ShrinkInspectionSnapshot.cs.meta b/Assets/ShrinkInspection/Runtime/ShrinkInspectionSnapshot.cs.meta new file mode 100644 index 0000000..8fdd2e8 --- /dev/null +++ b/Assets/ShrinkInspection/Runtime/ShrinkInspectionSnapshot.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: a412b2f6a531a33419d270e47853ef05 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/ShrinkInspection/package.json b/Assets/ShrinkInspection/package.json new file mode 100644 index 0000000..36fddbe --- /dev/null +++ b/Assets/ShrinkInspection/package.json @@ -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" + } +} diff --git a/Assets/ShrinkInspection/package.json.meta b/Assets/ShrinkInspection/package.json.meta new file mode 100644 index 0000000..5138145 --- /dev/null +++ b/Assets/ShrinkInspection/package.json.meta @@ -0,0 +1,7 @@ +fileFormatVersion: 2 +guid: fdb80de9a8588bc4392a94dd7352be58 +TextScriptImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/DESIGN.md b/DESIGN.md index 72123d8..ba73f26 100644 --- a/DESIGN.md +++ b/DESIGN.md @@ -85,28 +85,28 @@ ShrinkSDK Workspace/ | 包 | 版本 | 职责 | |---|---:|---| -| `com.cneicy.shrink-eventbus` | 2.1.0 | 单一事件模型、多 Bus、生成特性订阅、可选 MonoBehaviour 生命周期织入、UniTask 调度与零 GC 热路径 | -| `com.cneicy.shrink-eventbus-entities` | 0.1.2 | ShrinkEventBus 的 ECS/Burst NativeQueue writer 与 playback 适配 | -| `com.cneicy.shrink-datasaver` | 2.3.0 | 多槽位存档、设置、迁移、加密、原子写入与备份 | -| `com.cneicy.shrink-command` | 0.3.0 | 路径式命令、权限与同步/异步执行 | -| `com.cneicy.shrink-network` | 0.3.0 | 消息、RPC、权限、诊断、TCP/KCP/Loopback 与服务器生成 | -| `com.cneicy.shrink-mod-framework` | 0.3.0 | 模组发现、依赖、可逆生命周期、命名空间内容覆盖、外部 DLL revision 与 Harmony lease | +| `com.cneicy.shrink-eventbus` | 2.2.2 | 单一事件模型、多 Bus、生成特性订阅、可选 MonoBehaviour 生命周期织入、UniTask 调度与零 GC 热路径 | +| `com.cneicy.shrink-eventbus-entities` | 0.1.5 | ShrinkEventBus 的 ECS/Burst NativeQueue writer 与 playback 适配 | +| `com.cneicy.shrink-datasaver` | 2.4.0 | 多槽位存档、设置、迁移、加密、原子写入与备份 | +| `com.cneicy.shrink-command` | 0.4.2 | 路径式命令、权限与同步/异步执行 | +| `com.cneicy.shrink-network` | 0.4.2 | 消息、RPC、权限、诊断、TCP/KCP/Loopback 与服务器生成 | +| `com.cneicy.shrink-mod-framework` | 0.3.6 | 模组发现、依赖、可逆生命周期、命名空间内容覆盖、外部 DLL revision 与 Harmony lease | | `com.cneicy.shrink-tutorial` | 0.2.0 | 数据驱动引导、遮罩、锚点、触发与持久化 | -| `com.cneicy.shrink-context-core` | 0.2.0 | 可逆效应、coeffect、fiber、声明式 loader 与诊断 | -| `com.cneicy.shrink-app-core` | 0.2.0 | App 设置、服务门面、ClassicHost 兼容面与宿主协议 | -| `com.cneicy.shrink-app-starter-basic` | 0.3.0 | 默认 Context 组合根、配置资产和示例入口 | -| `com.cneicy.shrink-context-app-adapter` | 0.2.0 | ContextLoader 宿主、Profile/JSON、诊断与基准 | -| `com.cneicy.shrink-context-eventbus-adapter` | 0.2.0 | EventBus 生成绑定的可逆 EffectAttach 包装 | -| `com.cneicy.shrink-datasaver-integration-eventbus` | 2.2.0 | DataSaver 事件桥 | -| `com.cneicy.shrink-datasaver-integration-app` | 0.2.0 | DataSaver App installer/原生 Context 组件 | -| `com.cneicy.shrink-command-integration-eventbus` | 0.2.0 | 命令请求与生命周期事件桥 | -| `com.cneicy.shrink-command-integration-network` | 0.2.0 | `command/execute` RPC 桥 | -| `com.cneicy.shrink-command-integration-app` | 0.2.0 | Command App installer/原生 Context 组件 | -| `com.cneicy.shrink-network-integration-eventbus` | 0.2.0 | 网络事件广播、裁决结果与 delta 去重 | -| `com.cneicy.shrink-network-integration-app` | 0.2.0 | Network App installer/原生 Context 组件 | -| `com.cneicy.shrink-shared-codegen` | 0.1.1 | App、Command、Network 共用的 Editor-only IL 后处理注册表生成器 | +| `com.cneicy.shrink-context-core` | 0.3.0 | 可逆效应、coeffect、fiber、声明式 loader 与诊断 | +| `com.cneicy.shrink-app-core` | 0.2.3 | App 设置、服务门面、ClassicHost 兼容面与宿主协议 | +| `com.cneicy.shrink-app-starter-basic` | 0.3.7 | 默认 Context 组合根、配置资产和示例入口 | +| `com.cneicy.shrink-context-app-adapter` | 0.2.4 | ContextLoader 宿主、Profile/JSON、诊断与基准 | +| `com.cneicy.shrink-context-eventbus-adapter` | 0.2.4 | EventBus 生成绑定的可逆 EffectAttach 包装 | +| `com.cneicy.shrink-datasaver-integration-eventbus` | 2.2.5 | DataSaver 事件桥 | +| `com.cneicy.shrink-datasaver-integration-app` | 0.2.5 | DataSaver App installer/原生 Context 组件 | +| `com.cneicy.shrink-command-integration-eventbus` | 0.2.5 | 命令请求与生命周期事件桥 | +| `com.cneicy.shrink-command-integration-network` | 0.2.5 | `command/execute` RPC 桥 | +| `com.cneicy.shrink-command-integration-app` | 0.2.5 | Command App installer/原生 Context 组件 | +| `com.cneicy.shrink-network-integration-eventbus` | 0.2.5 | 网络事件广播、裁决结果与 delta 去重 | +| `com.cneicy.shrink-network-integration-app` | 0.2.5 | Network App installer/原生 Context 组件 | +| `com.cneicy.shrink-shared-codegen` | 0.2.1 | App、Command、Network 共用的 Editor-only IL 后处理注册表生成器 | | `com.cneicy.shrink-runtime-abstractions` | 0.1.0 | Unity 与其他 .NET 宿主共用的平台服务合同 | -| `com.cneicy.shrink-installer` | 0.2.6 | UI Toolkit 包管理器:显示真实安装状态,按 Context 分层提供固定版本单包与推荐组合 | +| `com.cneicy.shrink-installer` | 0.2.13 | UI Toolkit 包管理器:显示真实安装状态,按 Context 分层提供固定版本单包与推荐组合 | `ShrinkShared.CodeGen` 不反向引用业务 asmdef,只按程序集名与类型全名读取 Cecil 元数据,因此业务包可以依赖它而不形成包循环。 @@ -202,7 +202,9 @@ Mono 中已加载程序集不能真正卸载。系统只回滚组件实例与效 ### 8.1 Unity 运行时注册 -`ShrinkEventBus/CodeGen/Editor/EventBusILPostProcessor.cs` 为引用方程序集生成实例直接调用绑定和静态模块初始化注册,不运行时枚举方法。`ShrinkShared.CodeGen/Editor/ShrinkRegistryILPostProcessor.cs` 继续负责 Command、Network 和 App 注册表。外部 Mod DLL 必须携带 EventBus 生成合同;没有合同的生产 DLL 不自动反射注册。 +Unity 由 ShrinkShared.CodeGen 的 ShrinkRegistryILPostProcessor 作为唯一 IL 后处理入口,统一编织 EventBus、Command、Network 与 App 生成合同;旧分散入口保留兼容类但不再重复处理。运行时仍是生成的强类型绑定,不枚举方法。外部 Mod DLL 必须携带生成合同。 + +Unity 加载兼容其 Roslyn 3.8 的预构建分析器 DLL;独立 CLI 使用自己的较新 Roslyn,不能把相同源代码直接编译为仅支持新 API 的 DLL 后交给旧 Unity。SHRINK001–004 检查订阅签名、协议 opcode/route、installer 和常量 Context key。编译器分析不代替异步生命周期行为验证。 ### 8.2 独立服务器生成 @@ -262,3 +264,16 @@ Mono 中已加载程序集不能真正卸载。系统只回滚组件实例与效 - `Docs/Archive/CORDIS_MIGRATION.completed.md` 保存迁移论证、阶段记录和历史验收数据。 - `Docs/Archive/codebase-map-2026-05-23/` 保存迁移前代码地图,其中关于 Git 状态、模块规模和测试覆盖的描述均已过期。 - `.planning/threads/` 只用于恢复工作上下文;其中历史 Notes 不得覆盖本文和当前源码。 + + +## 12. 本地导航与性能合同 + +- Tools/AgentSupport/Shrink.Inspect 是无模型 API 依赖的本地 Roslyn CLI;capabilities 用包级职责/入口/复用边界/不负责内容/验证点,symbol、flow、impact 使用语义边而不是把 grep 当调用图。 +- Unity 输入由 ShrinkInspection 导出实际源、引用、宏与构建标识;Godot/.NET 通过 MSBuildWorkspace 加载目标工程。static、candidate、runtime-bound、unresolved 分开显示;结果有数量、深度与分页上限,第三方引用和生成成员的缺口显式声明。 +- 运行快照只按需导出明确宿主的 Context、Network、EventBus 与 Command 绑定。fiber 标识带 scope;缺失 provider 是未满足依赖,不是假造绑定。快照不保存消息正文/令牌,不常驻连接生产环境。 +- Context 仅保留受管理的活跃/过渡 fiber;完全退役后移除运行时与父 effect 的实例引用,有界纯数据历史默认 64。依赖索引用于缩小诊断候选;同步深链转换以队列展开。 +- Network 使用 v2 SHK2 封装、消息体单次编码和明确池内存生命周期。自动派发为每服务有界串行队列;主线程手动泵送提供数量/字节/时间预算。可靠操作满队列明确失败,只有显式状态 key 可合并,RPC 响应独立完成以避免嵌套等待死锁。 +- MessagePack 为可选生成 resolver 适配器,不由核心反射发现 formatter。TCP 优化单次组合写入;没有把实验性 Pipelines 或 .NET 10 后端带入共享默认依赖。 +- DataSaver 主线程采集与后台快照编码分离;后台开关默认关闭,格式、原子替换与备份回退不变。阶段计时按需开启。 + +使用方式见 Tools/AgentSupport/README.md;新内容先执行 Docs/ReuseFirstDesign.md;跨组件可编译示例入口见 Docs/IntegrationExamples.md;性能结论与限制见 Docs/Performance/2026-09-28.md。本次本地交付未改变远程发布治理规则。 diff --git a/Docs/AgentSupportAcceptance.md b/Docs/AgentSupportAcceptance.md new file mode 100644 index 0000000..e817837 --- /dev/null +++ b/Docs/AgentSupportAcceptance.md @@ -0,0 +1,46 @@ +# Agent 导航与交付验收 + +验收基于实际本地代码、CLI、Unity/Godot/.NET 运行,不以文档关键词命中替代事实。CLI 版本 0.1.2,输出格式版本 1。未发布远程版本。 + +## 冷上下文 Agent + +仅提供需求、CLI 入口与允许的证据路径;只读执行,未让评估 agent 参与实现。 + +- 第一轮:26 次 CLI 查询、14 组源码读取,暴露模组证据路径错误、中文业务问法遗漏、route 链断点、Godot 输入选择和 impact 模块过滤缺口。 +- 第二轮:13 次查询复测 8 个导航任务与复用边界;修复上述入口问题,仍报告通用外部方法导致扩散、路由链不完整和真实 waiting 证据不足。 +- 第三轮限定复测:2 次 CLI+1 次既有 JSON 读取,无代码修改、无无关文件正文读取。route 默认深度返回 9 条未截断结果,包含 handler-forward 到 ShrinkNetworkCommandBridge.HandleExecuteAsync;事件 flow 从 155 条降至 52 条,默认页不再经 Dictionary 方法扩散到此前无关模块。并未逐条审核全部 52 条。 +- waiting fixture 返回 CurrentProviderUid=null、CommittedProviderUid=null、PotentialProviderUids=[]、IsSatisfied=false,没有 provider 绑定边。runtime-bound 修饰真实的 requires 关系,不代表依赖已经满足;该轮读取既有产物,不声称重新捕获实时状态。 + +## 8 个导航任务的落点 + +| 任务 | 证据/边界 | +|---|---| +| 启动应用 | Starter 组合、ContextLoaderHost;消费工程自己的组合根 README | +| 事件未触发 | publish/subscribe 候选与实际快照绑定分离;无快照不称已注册 | +| 远程命令 | route → 消息 → 注册/lambda → HandleExecuteAsync;继续调用 Command.ExecuteAsync 需展开深度 | +| provider 缺失 | key 查询、requires、候选 provider、waiting 依赖状态 | +| 存档入口 | ShrinkSave、模块采集、LocalStorageProvider;不假装已有任意介质注入 | +| 模组退役 | ShrinkModContextHost、fiber、effect lease 撤回 | +| Godot 主线程 | --project 加载 Godot 工程,而不是复用 Unity 编译清单 | +| 网络服务改动影响 | 直接调用、桥接、验证入口;不保证发现所有反射或动态间接调用 | + +4 个复用任务结论:每帧预算先用宿主已有配置;卸载订阅用 Context EventBus adapter;新存储介质实现 IStorageProvider 后仍需确认/窄扩展宿主注入入口;新怒气玩法没有现成业务模块,基础设施可复用但业务必须设计。不为满足“复用”而虚构能力。 + +## 验证记录 + +- 独立 .NET 核心回归 64 项、CLI/分析器回归 12 项通过。 +- Unity 全量 EditMode 226 项通过;实际 Windows Player 10 项通过,包含分配计数器校准、RPC/广播矩阵与存档阶段测试。 +- Godot 4.6.3 Mono 实际 headless 启动、事件/命令生成合同、存档 42 回读、Context 应用及 .NET JSON golden frame 互通通过。 +- 独立 NuGet 消费工程用新的隔离包缓存还原,实际运行输出 PASS;CodeGen 合同印记 0.1.0 是格式版本,不是 NuGet 包版本。 +- justanyproject 安装 22 个本地 SDK UPM 包,新增 Inspection 接入和生成式 MessagePack fixture;.NET 3.1.8 编码与项目既有 MessagePack 3.1.10 生成 formatter 的 Unity PlayMode 字节互通通过。 +- justanyproject 定向 EditMode 83 项:80 通过、3 失败。两项 ModSpellApi 测试仍调用已停用旧工厂重载;另一项 tooltip 断言期待“◆”,当前正文没有该字符。这些代码不是本次 SDK 迁移修改,未擅自重写玩法使其变绿,不能宣称消费项目全量测试通过。 + +从 Entry 启动后已进入 MainMenu,9 个应用 fiber 均 Active。宿主快照明确包含应用 Context、Network 与 Command scope,不把旧服务容器查询失败当作服务不存在。相同构建的 route 查询共 14 条未截断结果,其中 5 条为 runtime-bound;默认候选关系仍保留 candidate 标记。最终安装版本、实际运行状态、原始测试摘要保存在性能数据目录。编译通过与主场景启动不等于完整多人对局或所有内容的端到端验收。 + +## 保留的限制 + +- 静态 IDE 输入可能过期,buildId 为空时只提供候选证据;Unity 导出清单与运行快照须构建标识一致,源/引用时间戳变化会拒绝合并。 +- 第三方包只作引用;第三方源生成的成员可能 unresolved,不把覆盖不足当检查通过。 +- 自定义 Endless 联机协议不属于 SDK Network 运行快照覆盖;其现有协议版本 29 不变,不能与 SDK SHK2 版本 2 混同。 +- 快照按需生成;没有常驻采集、生产连接、消息正文或令牌导出。按模块、深度、数量与分页控制结果,当前仍每次重新建立索引。 +- 本次为本地安装和构建验收,没有验证远程 registry/Git tag/Installer 的正式发布路径,也没有发布这些版本。 diff --git a/Docs/Archive/ShrinkSDKUnity-thread-before-agent-support.md b/Docs/Archive/ShrinkSDKUnity-thread-before-agent-support.md new file mode 100644 index 0000000..3296b0c --- /dev/null +++ b/Docs/Archive/ShrinkSDKUnity-thread-before-agent-support.md @@ -0,0 +1,205 @@ +# Thread: ShrinkSDKUnity + +## Alias + +- `ShrinkSDKUnity` + +## Status: OPEN + +## Goal + +持续推进 `D:\UnityBuilds\ShrinkSDK` 的 SDK 基础设施,重点围绕: + +- `ShrinkModFramework` +- `ShrinkNetwork` +- 独立服务器能力 +- 权限、RPC、传输层 + +## Context + +*Created from conversation on 2026-04-05.* + +本线程当前已经落地: + +- `.planning/codebase/` 中文代码库地图 +- `Assets/Modules/ShrinkModFramework/` + - 模组声明、依赖声明、生命周期 + - `RuntimeInitializeOnLoadMethod(RuntimeInitializeLoadType.AfterAssembliesLoaded)` 自动启动 + - 外部 DLL 热插拔 + - Harmony 自动补丁接入 +- `Assets/Modules/ShrinkNetwork/` + - `[ShrinkNetworkMessage]` + - `[ShrinkNetworkSubscriber]` + - `[ShrinkNetworkSubscribe]` + - `RpcAsync` + - 权限模型 `ShrinkNetworkAuthority / ShrinkNetworkPeerKind / Permission` + - `ShrinkJsonNetworkSerializer` + - `ShrinkMessagePackNetworkSerializer` + - `ShrinkLoopbackTransport` + - `ShrinkTcpClientTransport` + - `ShrinkKcpClientTransport` + - `ShrinkKcpPeer` + - `ShrinkKcpTransportOptions` +- `GeneratedServers/ShrinkNetwork.ServerHost/` + - 独立 .NET 控制台服务器 + - `TcpServerTransport` + - `KcpServerTransport` + - 同时监听 TCP `17001` 与 KCP `17002` + +## This Round + +这一轮新增与修复: + +- 重构 `ShrinkNetwork` 目录结构: + - `Runtime/Core` + - `Runtime/Metadata` + - `Runtime/Routing` + - `Runtime/Serialization` + - `Runtime/Transport/*` + - `Editor/Scaffolding` +- 删除仓库内固定参考工程,改为插件模板 + 生成式独立服务器工程 +- 独立服务器生成器支持扫描: + - `[ShrinkNetworkMessage]` + - 显式 `RegisterMessage(opcode, route)` +- 新增显式状态同步范式: + - `ShrinkNetworkStateSyncAttribute` + - `ShrinkNetworkStateSyncRole` +- LAN 演示消息补齐: + - `[ShrinkNetworkMessage]` + - `[ShrinkNetworkStateSync("lan", role)]` +- `opcode` / `route` 冲突改为生成阶段直接报错,不由框架静默兜底 +- `ShrinkNetworkService.BindTransport(...)` 自动接入可选桥接: + - 自动注册 `ShrinkNetwork.Integration.EventBus` + - `BindTransport(null)` 自动解绑 +- `ShrinkNetwork.Integration.EventBus` 增加更易用扩展: + - `session.PublishEventAsync(...)` + - `session.RequestEventAsync(...)` + - `service.BroadcastEventAsync(...)` + - `service.UseEventBusBridge(...)` +- `ShrinkEmbeddedHostNetworkDemoSceneController` 去掉手工 `RegisterService(...)` +- 统一重写 README: + - `Assets/Modules/ShrinkNetwork/README.md` + - `Assets/Modules/ShrinkNetwork.Integration.EventBus/README.md` + - `Assets/Modules/ShrinkDataSaver.Integration.EventBus/README.md` + +## This Round Update + +继续补齐的内容: + +- `ShrinkNetwork` 的 RPC 关联 id 从裸 `int rpcId` 重构为强类型 `ShrinkRequestToken` +- `ShrinkRpcCallOptions` 增加: + - `RequestTokenOverride` + - `DebugLabel` +- `ShrinkNetworkPacket / ShrinkNetworkContext / ShrinkNetworkRouter / ShrinkNetworkService` 全链路改用 `RequestToken` +- `ShrinkNetwork.Integration.EventBus` 的请求结果调用默认带事件类型 `DebugLabel` +- TCP 客户端在“远端正常关闭连接”时不再刷异常堆栈,降级为 warning +- 独立服务器生成器继续升级: + - 不只识别 `[ShrinkNetworkEvent]` + - 还会区分: + - 广播型网络事件 + - 远端裁决网络事件 + - 增量网络事件 + - 对非状态同步范式接管的网络事件消息,自动生成 handler 模板并自动注册 `RegisterHandler(...)` +- 同步更新当前生成产物: + - `GeneratedServers/ShrinkNetwork.ServerHost/Generated/UnityGeneratedServerHandlers.g.cs` + - `GeneratedServers/ShrinkNetwork.ServerHost/Generated/UnityGeneratedServerModule.g.cs` + - `GeneratedServers/ShrinkNetwork.ServerHost/Generated/UNITY_GENERATED_SERVER_SCAFFOLD.md` +- 模板说明补齐: + - `Assets/Modules/ShrinkNetwork/Editor/Scaffolding/ServerProjectTemplate/README.md.txt` + - `Assets/Modules/ShrinkNetwork/README.md` +- 商业级可用性第一轮硬化: + - `ShrinkNetworkService` / `ShrinkNetworkSession` 改为并发安全集合,断线时自动失败对应的挂起 RPC + - TCP 客户端 / 服务端补齐最大包长保护,避免远端伪造长度直接触发大内存分配 + - 运行时 `ShrinkKcpServerTransport` 补齐远端地址校验,并把会话 `conversationId` 改为随机分配 + - 独立服务器模板与当前 `GeneratedServers` 宿主改为“默认拒绝匿名登录”,只有显式允许时才在缺少令牌时放行 + - 同步更新: + - `Assets/Modules/ShrinkNetwork/Editor/Scaffolding/ServerProjectTemplate/*` + - `GeneratedServers/ShrinkNetwork.ServerHost/*` +- 商业级可用性第二轮硬化: + - `ShrinkNetworkPacket` 增加 `ProtocolVersion / SchemaVersion` + - `ShrinkNetworkService` 增加协议版本窗口校验,必要时通过 `IShrinkNetworkSessionControlTransport` 主动断开违规会话 + - `ShrinkNetworkService` 增加基础指标快照: + - 会话连接/断开 + - 收发包与字节数 + - RPC 发起/完成/超时/取消/失败 + - 协议违规、权限拒绝、处理器异常、未知 opcode、分发 miss、反序列化失败 + - 独立服务器宿主新增: + - `ServerHostOptions.MinProtocolVersion / MaxProtocolVersion` + - `ServerHostOptions.MinSchemaVersion / MaxSchemaVersion` + - `ServerHostOptions.DisconnectOnProtocolViolation` + - `ServerHostOptions.DiagnosticsLogIntervalSeconds` + - `Program.cs` 会定期输出指标摘要,并在 `Ctrl+C` 前打印最后一份快照 + +## Verified + +已做实际验证: + +- `dotnet build .\\Assembly-CSharp.csproj /m:1 -p:BaseIntermediateOutputPath=Temp\\CliObj\\Assembly-CSharp\\` +- `dotnet build .\\Assembly-CSharp-Editor.csproj /m:1 -p:BaseIntermediateOutputPath=Temp\\CliObj\\Assembly-CSharp-Editor\\` +- `dotnet build .\\GeneratedServers\\ShrinkNetwork.ServerHost\\ShrinkNetwork.ServerHost.csproj /nr:false` +- `dotnet build .\\ShrinkNetwork.Runtime.csproj /m:1` +- `dotnet build .\\ShrinkSDK.sln /m:1` + +说明: + +- Unity 侧当前编译为 0 warning / 0 error +- 当前 `GeneratedServers\\ShrinkNetwork.ServerHost` 可编译通过,但在 `net8.0` 宿主侧仍有一批既有 nullable warnings +- 当前 `GeneratedServers\\ShrinkNetwork.ServerHost` 也已手工同步到新生成器语义 +- 这轮没有做 Unity 编辑器内按钮点击与 PlayMode 回归 +- 商业级硬化这一轮已额外验证: + - `dotnet build .\\Assembly-CSharp.csproj /m:1 -p:BaseIntermediateOutputPath=Temp\\CliObj\\Assembly-CSharp\\` + - `dotnet build .\\GeneratedServers\\ShrinkNetwork.ServerHost\\ShrinkNetwork.ServerHost.csproj /nr:false` + - `dotnet build .\\ShrinkSDK.sln /m:1` + +## Notes + +- `D:\UnityBuilds\ShrinkSDK` 是根 Git 仓库;EventBus、DataSaver 与 DataSaver.EventBus 已从 gitlink/嵌套仓库改为根仓库直接跟踪,干净克隆可恢复完整源码。 +- 当前唯一架构文档是根 `DESIGN.md`;Cordis 完成态和旧 `.planning/codebase` 地图已归档到 `Docs/Archive/`。 +- 独立服务器生成器已改为扫描 Unity 编译后的 Player 程序集语义,并用 SHA-256 清单保护模板托管文件。 +- 当前线程里“独立服务器模板仍有 nullable 警告”的记录已过期,至少现有生成工程与解决方案编译均已清零 +- PowerShell 查看 UTF-8 中文时会出现乱码,但文件本身已按 UTF-8 写入 +- `ShrinkNetwork.Integration.EventBus` 当前 `HasResult` 只回传: + - `EventResult` + - `IsCanceled` + - `ErrorCode / ErrorMessage` +- 还不会自动回传事件对象其它字段的最终改动 +- 当前生成器虽然已经把网络事件纳入一等公民,但生成结果仍以“可编译、可注册、可继续补业务”为目标,不会替你推断完整业务逻辑 +- 当前默认安全基线已明显提高,但仍未覆盖 TLS、正式身份体系、协议版本协商、压测与故障演练 +- 当前协议版本闸门与指标摘要已经具备最小生产辅助价值,但还没有 metrics 导出、trace、dashboard 或告警接入 + +## Next Steps + +- 在 Unity 编辑器中实际点击“生成完整独立服务器工程 / 刷新 Generated 合同”并补一轮运行时回归 +- 继续补商业级缺口时,优先排: + - TLS / 安全信道 + - 正式身份体系与令牌轮换 + - 更细粒度的协议兼容协商,而不是当前这种“窗口校验 + 直接断开” + - metrics 导出 / trace / dashboard / 告警 + - 压测、模糊包、断线与兼容回归 +- 后续修改服务器生成器时保持语义扫描、命名空间同名拒绝和模板哈希预检测试。 +- 如果要继续收口代码层细节,优先处理公开 API 中残留的 nullable 语义不一致,而不是重复消化已不存在的构建警告 +- 如果后续彻底放弃旧心智,可把公开 API 中残留的 `route = null` 一类签名继续做 nullable 收口 + +## Last Update + +- 2026-08-29: 完成第三方合规排查并新增根 `THIRD-PARTY-NOTICES.md`(README 已加"第三方与合规"入口)。结论:NeoForge / Minecraft Forge 与 MessagePipe 仅为概念参考(`Docs/JustAnyProjectArchitectureAudit.md` A.3 有既有声明;EventBus 的五级优先级、`receiveCanceled`、`EventResult{DEFAULT,ALLOW,DENY}`、`Attach(object)` 对应 Forge API 语义,实现为原创未复制代码,不触发 LGPL 衍生义务);MessagePipe 未引入依赖、API 形态不构成高度相似;AlicizaX(GitHub 组织)在代码、文档、git 历史中零引用,`ShrinkInstaller` 子模块历史为自研。真正触发 MIT 归属义务的是 `Assets/Modules/ShrinkNetwork/Plugins/Kcp-CSharp.dll`(Molth/Kcp-CSharp,上游 KCP)与 `System.Runtime.CompilerServices.Unsafe.dll`,两者版权行与 MIT 全文已收录进声明文件;UniTask / MessagePack / Newtonsoft.Json 为外挂包依赖已登记。遗留:根仓库本身尚无 LICENSE(EventBus 模块已有 MIT),且 `ShrinkNetwork` 模块独立发布时需把 DLL 声明同步进该模块仓库。 + +- 2026-04-10: 已新增 `Assets/Modules/ShrinkTutorial/` 独立包,落地一套可复用的互动式引导系统,包含 `ShrinkTutorialManager`、`ShrinkTutorialData / ShrinkTutorialStep / ShrinkTutorialDatabase`、默认 `PlayerPrefs` 持久化、`ShrinkTutorialAnchor / ShrinkTutorialTrigger`、遮罩挖洞与提示框 UI、以及 `ShrinkSDK/引导/*` / `ShrinkSDK/Tutorial/*` 编辑器入口。 +- 2026-04-10: 已新增示例场景 `Assets/Scenes/ShrinkTutorialSample.unity`,并配套生成 `Assets/Modules/ShrinkTutorial/Samples/Resources/ShrinkTutorialSettings.asset`、`Assets/Modules/ShrinkTutorial/Samples/ShrinkTutorialSampleDatabase.asset`、`Assets/Modules/ShrinkTutorial/Samples/ShrinkTutorialSampleTutorial.asset`。示例流程覆盖三类核心路径:静态目标点击、运行时动态锚点等待、`CompleteStep(customEventName)` 自定义事件完成。 +- 2026-04-10: 示例场景入口脚本为 `Assets/Modules/ShrinkTutorial/Runtime/Samples/ShrinkTutorialSampleSceneController.cs`,一键重建入口为 `Assets/Modules/ShrinkTutorial/Editor/Samples/ShrinkTutorialSampleSceneBuilder.cs`,菜单为 `ShrinkSDK/引导/创建示例场景` 与 ASCII 别名 `ShrinkSDK/Tutorial/Create Sample Scene`。后续如果示例场景被手改或损坏,可直接走该菜单重建。 +- 2026-04-10: 本轮已做命令行编译验证:`ShrinkTutorial.Runtime.csproj`、`ShrinkTutorial.Editor.csproj`、`ShrinkSDK.sln` 均为 `0 warning / 0 error`。Unity 侧也完成了一轮示例场景 PlayMode 烟测,并修复了 `ShrinkTutorialDialog.Initialize()` 在已有 `RectTransform` 上重复 `AddComponent()` 导致的运行时空引用。 +- 2026-04-10: 当前示例场景文案已改为英文,不是产品方向选择,而是临时规避 TMP 默认字体缺少中文字形导致的方块/警告。若后续要把示例或引导系统默认文案恢复成中文,优先补一个可用的中文 TMP 字体资产,再统一改回中文,避免再次出现 `LiberationSans SDF` 缺字警告。 + +- 2026-04-07: 已补上给外部模组开发者使用的 `Mod SDK` 导出器。新入口是 `ShrinkSDK/Mod/导出 Mod SDK 开发包`,会先构建 `ShrinkSDK.sln`,再导出 `Libs/ + Templates/ExternalMod/SampleShrinkMod/ + README + manifest.json`;其中外部模板已改成引用导出包里的 DLL,而不再直接引用本地仓库 `csproj`。 +- 2026-04-07: 因外部独立模组工程在用户 IDE 环境里仍会撞到 `Microsoft.NET.SDK.WorkloadAutoImportPropsLocator` 解析问题,已追加一条更省事的“仓库内模组模板”路径。新入口是 `ShrinkSDK/Mod/生成仓库内模组模板(推荐)`,会直接生成到当前 Unity 项目的 `Assets` 下并复用现有 `ShrinkSDK.sln` / asmdef 编译链,作为当前更推荐的模组开发入口。 +- 2026-04-07: 已给 `ShrinkModFramework` 补上外部 DLL 模组模板生成器。新入口是 `ShrinkSDK/Mod/生成外部模组模板`,实现位于 `Assets/Modules/ShrinkModFramework/Editor/Scaffolding/`,会生成带 `csproj + build.ps1 + 示例模组代码 + README` 的独立模组工程,方便模组开发者直接构建 DLL 并投放到 `Mods` 目录。 +- 2026-04-07: 已整理 `Assets/Modules/ShrinkModFramework/Runtime/` 目录结构,把平铺脚本拆成 `Bootstrap / Core / Metadata / Registry / Loading / Network / Integration` 七层;同时同步更新 `Assets/Modules/ShrinkModFramework/README.md` 与 `.planning/codebase/STRUCTURE.md`,把模组框架当前真实目录与推荐阅读顺序补齐。 +- 2026-04-07: 用户已在实际环境中完成本轮回归,反馈当前 `ShrinkCommand` / `ShrinkNetwork` / 独立服务器生成链路“测试过了没问题”。这意味着前面“还缺 Unity 编辑器内实际回归验证”的记录对当前这条链路已过期;当前可把状态视为:代码编译全绿,且已有用户侧真实验证通过。 +- 2026-04-07: 已继续把 `ShrinkCommand`/`ShrinkNetwork` 相关 nullable 收口做到当前全绿。先补齐 `Assets/Modules/ShrinkCommand.Integration.EventBus/` 与 `Assets/Modules/ShrinkCommand.Integration.Network/` 的 `#nullable enable`,再把 `Assets/Modules/ShrinkNetwork/Runtime` 的消息元数据、权限、注册器、路由器、会话、服务、序列化器、EventBus 桥接等公共类型按真实可空语义收口;同时把 `GeneratedServers/ShrinkNetwork.ServerHost/Generated/UnityGeneratedNetworkContracts.g.cs` 里 `LanJoinRoomResponse.Players` 的数组默认值补齐,并把源头生成器 `Assets/Modules/ShrinkNetwork/Editor/Scaffolding/ShrinkDedicatedServerScaffoldGenerator.cs` 的 `GetDefaultInitializer(...)` 改成对任意 `T[]` 都自动生成 `Array.Empty()`,避免后续重新生成时 warning 回弹。已实际验证:`dotnet build .\\ShrinkSDK.sln /m:1` 与 `dotnet build .\\GeneratedServers\\ShrinkNetwork.ServerHost\\ShrinkNetwork.ServerHost.csproj /nr:false` 现都为 `0 warning / 0 error`。 +- 2026-04-07: 已修复 `ShrinkCommand` 在 Unity 侧的兼容性编译断点。`Assets/Modules/ShrinkCommand/Runtime/Core/ShrinkCommandService.cs` 与 `Assets/Modules/ShrinkCommand/Runtime/Metadata/ShrinkCommandTypes.cs` 现已显式开启 `#nullable enable`,并把依赖 `init` 的对象初始化属性改成 Unity 2022 当前编译链可稳定通过的 `set`,避免再次触发 `System.Runtime.CompilerServices.IsExternalInit` 缺失;同时顺手收口了 `ShrinkCommandRegHelper.cs` 与 `ShrinkCommandService.cs` 内部的几处 nullable warning。已实际验证:`dotnet build .\\ShrinkCommand.Runtime.csproj /m:1`、`dotnet build .\\GeneratedServers\\ShrinkCommand.RuntimeSmoke\\ShrinkCommand.RuntimeSmoke.csproj /nr:false` 均为 `0 warning / 0 error`;`dotnet build .\\ShrinkSDK.sln /m:1` 与 `dotnet build .\\GeneratedServers\\ShrinkNetwork.ServerHost\\ShrinkNetwork.ServerHost.csproj /nr:false` 也都恢复到 `0 error`。当前残留 warning 已不在这两个修复点本体,而是在既有的 `ShrinkCommand.Integration.*` 与 `ShrinkNetwork` nullable 收口面。 +- 2026-04-06: 已补宿主文本配置文件能力。新增 `GeneratedServers/ShrinkNetwork.ServerHost/server.properties` 与模板 `server.properties.txt`,风格对齐 Minecraft 的 `key=value` 配置;`Program.cs` 启动改为先走 `ServerHostProperties.LoadOrCreate()`,首次启动缺文件时自动落默认配置,并打印配置文件路径、是否首次生成、配置告警与最终优先级。当前规则为“代码默认值 < server.properties < 环境变量”;同时补了 `SHRINK_SERVER_CONFIG_PATH` 自定义配置文件路径入口,并把 `UnityAssetsPath` / `ScanOutputDirectory` 的默认解析从易错的 `AppContext.BaseDirectory` 相对跳转改成基于宿主工程目录定位。 +- 2026-04-06: 已补上运行时烟测闭环。新增 `GeneratedServers/ShrinkNetwork.RuntimeSmoke/` 轻量控制台烟测工具,直接覆盖“登录拿令牌 -> 鉴权后业务 RPC -> `server/auth/refresh` 续期 -> 篡改令牌后被宿主断链”。同时把会话令牌关键配置补成环境变量:`SHRINK_SERVER_ENABLE_SESSION_TOKENS / SHRINK_SERVER_SESSION_TOKEN_TTL_SECONDS / SHRINK_SERVER_SESSION_TOKEN_REFRESH_WINDOW_SECONDS / SHRINK_SERVER_DISCONNECT_ON_INVALID_SESSION_TOKEN`。已实际跑通:宿主在 `SHRINK_SERVER_AUTH_TOKEN=smoke-token`、TTL/续期窗口都为 `120` 秒时,烟测日志确认 `Login ok`、`Join room ok`、`Refresh ok`、`Tampered token rejected with disconnect`、`PASS`;宿主日志也确认坏令牌请求被拒绝并主动踢线。 +- 2026-04-06: 已补上“正式身份/会话令牌体系”第一版。`ShrinkNetworkPacket` 新增 `SessionToken` 与到期时间戳;`ShrinkNetworkSession` 会自动持有并回传当前令牌;`ShrinkNetworkService` 新增 `IncomingPacketValidator` 与 `AuthRejectedCount` 指标。独立宿主与模板侧新增 `server/auth/refresh`,登录成功后签发内存态会话令牌,并按 `ServerHostOptions.EnableSessionTokens / SessionTokenTtlSeconds / SessionTokenRefreshWindowSeconds / DisconnectOnInvalidSessionToken` 做逐包校验与续期。已实际验证:`dotnet build .\\Assembly-CSharp.csproj /m:1 -p:BaseIntermediateOutputPath=Temp\\CliObj\\Assembly-CSharp\\`、`dotnet build .\\GeneratedServers\\ShrinkNetwork.ServerHost\\ShrinkNetwork.ServerHost.csproj /nr:false`、`dotnet build .\\ShrinkSDK.sln /m:1` 全部通过;Unity 侧与解决方案侧恢复到 `0 warning / 0 error`,宿主仍保留既有 nullable warnings;本轮尚未做真实登录-续期-断链运行时回归。 +- 2026-04-06: 已补完 TCP TLS 第一版收口。`ShrinkTcpClientTransport` / `ShrinkTcpServerTransport` 与当前 `GeneratedServers`、模板宿主都已接入可选 `ShrinkTcpTlsOptions`,TCP 读包路径统一改为 `Stream` 以兼容 `SslStream`。同时把 `ShrinkTcpTlsOptions` 的实际定义移到 `Runtime/Transport/Abstractions/IShrinkNetworkTransport.cs`,原 `Runtime/Transport/Tcp/ShrinkTcpTlsOptions.cs` 仅保留占位,避免 Unity 尚未刷新工程文件时再次出现“文件已在磁盘上但类型不可见”的构建断点。 +- 已补记商业级第二轮硬化:协议版本闸门、会话踢除接口、基础指标快照与宿主周期性摘要;Unity 侧命令行构建仍保持 0 warning / 0 error,独立服务器宿主仍存在既有 nullable warnings,下一续接点转向 TLS / 身份体系 / 真正的观测导出 +- 2026-04-06: 宸茶ˉ涓?Minecraft 椋庢牸鐨勭嫭绔嬪懡浠ょ郴缁熶笌妗ユ帴灞傘€傛柊澧?`Assets/Modules/ShrinkCommand/` 鐙珛鍛戒护杩愯鏃讹紝鏀寔璺緞寮忓懡浠ゃ€佽礊蹇冨弬鏁般€佹潈闄愩€佹潵婧愬垽瀹氥€佸睘鎬佸紡鑷姩娉ㄥ唽锛涘悓鏃舵柊澧?`Assets/Modules/ShrinkCommand.Integration.EventBus/` 涓?`Assets/Modules/ShrinkCommand.Integration.Network/`锛屽墠鑰呮敮鎸佺敤 `ShrinkCommandExecuteRequestEvent` 璇锋眰鎵ц鍛戒护骞跺彂甯?executing / executed / failed 浜嬩欢锛屽悗鑰呮敮鎸佺敤 `command/execute` RPC 鎵ц杩滅▼鍛戒护銆傚綋鍓?`GeneratedServers/ShrinkNetwork.ServerHost` 宸叉敼涓哄叡浜?`ShrinkCommandService`锛屾帶鍒跺彴涓庣綉缁滄ˉ鎺ュ叡鐢ㄥ悓涓€濂楀懡浠わ紱瀹夸富鍦?stdin EOF 鏃朵笉鍐嶇珛鍗抽€€鍑猴紝閫傞厤鍚庡彴杩涚▼鍚姩銆傚凡瀹為檯楠岃瘉锛?`dotnet run --project .\\GeneratedServers\\ShrinkCommand.RuntimeSmoke\\ShrinkCommand.RuntimeSmoke.csproj` PASS锛?`dotnet run --project .\\GeneratedServers\\ShrinkCommand.EventBusSmoke\\ShrinkCommand.EventBusSmoke.csproj` PASS锛屼互鍙婂悗鍙板惎鍔ㄥ涓?+ `dotnet run --project .\\GeneratedServers\\ShrinkNetwork.RuntimeSmoke\\ShrinkNetwork.RuntimeSmoke.csproj -- 127.0.0.1 17777 smoke-token runtime-smoke` PASS锛屽凡鐪熷疄瑕嗙洊鈥滅櫥褰?-> 涓氬姟 RPC -> 杩滅▼ `status` 鍛戒护 -> 绡℃敼浠ょ墝鏂摼鈥濄€? diff --git a/Docs/IntegrationExamples.md b/Docs/IntegrationExamples.md new file mode 100644 index 0000000..993b79a --- /dev/null +++ b/Docs/IntegrationExamples.md @@ -0,0 +1,13 @@ +# 接入示例 + +这些示例直接引用仓库中参与编译和测试的实现,不另存代码副本。 + +| 目标 | 实现入口 | 可运行验证 | +|---|---|---| +| Context 组件,声明依赖并注册可逆状态 | [测试组件](../Assets/Modules/ShrinkContext.Core/Tests/TestComponents.cs) | [Fiber 生命周期](../Assets/Modules/ShrinkContext.Core/Tests/FiberLifecycleTests.cs)、[退役保留](../Assets/Modules/ShrinkContext.Core/Tests/RetirementRetentionTests.cs) | +| Context 持有事件订阅并在卸载时撤回 | [EffectAttach](../Assets/Modules/ShrinkContext.EventBusAdapter/Runtime/ShrinkEventBusEffects.cs) | [事件接收和自动退订](../Assets/Modules/ShrinkContext.EventBusAdapter/Tests/EventBusEffectTests.cs) | +| RPC 到 Command 桥接 | [桥注册和执行](../Assets/Modules/ShrinkCommand.Integration.Network/ShrinkNetworkCommandBridge.cs) | [双 provider 就绪/退役/替换](../Assets/Modules/ShrinkCommand.Integration.Network/Tests/IntegrationComponentsTests.cs) | + +Unity Test Runner 的 EditMode 选择 `ShrinkContext.Core.Tests`、`ShrinkContext.EventBusAdapter.Tests`、`ShrinkCommand.Integration.Network.Tests`。Player 验证入口为 `Assets/AgentSupportTests/Editor/PlayerAcceptance.cs`,仅使用测试场景。独立共享运行时测试使用 `dotnet test Tools/AgentSupport/RuntimeTests`。 + +事件特性需要 CodeGen 完成实际绑定;静态索引里的订阅边只是候选。RPC 桥的权限由 `ShrinkNetworkCommandBridgeOptions` 和命令源共同决定,不能为了复用绕过原有权限。拥有者在组件退出时撤回它的 effect,不替他人释放借来的服务。 diff --git a/Docs/NetworkV2Migration.md b/Docs/NetworkV2Migration.md new file mode 100644 index 0000000..f7cdcfa --- /dev/null +++ b/Docs/NetworkV2Migration.md @@ -0,0 +1,58 @@ +# Network v2 与诊断接入迁移 + +网络消息体的编码器与包封装现已分离。所有 `ShrinkNetworkService` 使用 SHK2 二进制封装;协议版本为 2,旧 JSON/MessagePack 外层包会被拒绝。客户端与服务端必须一起升级。直接使用 TCP/KCP transport 传递自定义字节的项目保留自己的上层协议,不能把其版本号与 ShrinkNetworkService 的协议号混同。 + +## 消息编码与所有权 + +- JSON 消息编码仍可显式选择 `ShrinkJsonNetworkSerializer`,不再将字节消息体嵌入 JSON 外层包而产生 Base64。 +- 原反射式 `ShrinkMessagePackNetworkSerializer` 已移除。安装可选的 `ShrinkSDK.Network.MessagePack`/UPM 适配包,引用 `ShrinkNetwork.MessagePack`,传入生成的 resolver,并对每个消息类型调用 `Register()`。适配器只补充基础类型 formatter,不使用动态泛型反射回退。 +- `ShrinkNetworkPacket.Payload` 从 `byte[]` 改为 `ReadOnlyMemory`。只有需要独立所有权时调用 `ToArray()`;`ShrinkPacketCodec.Decode` 借用输入内存。 +- 实现 `IShrinkNetworkBufferSerializer` 可直接写入 `IBufferWriter`。旧消息序列化接口保留兼容路径,但会产生中间数组。 +- 实现 `IShrinkNetworkMemoryTransport` 时,发送任务完成、取消或失败之前必须保持输入有效;任务结束后不能继续访问。服务层负责在 await 结束后归还池内存。旧传输接口收到独立数组。 +- 广播可先编码消息体并使用 `SendSerializedAsync`,外层会话信息仍按收件人生成。调用方在所有发送完成前不得修改共享消息体。 + +## 排队行为 + +默认自动派发按序执行处理器;Unity/Godot 的主线程接入可使用 `ShrinkNetworkDispatchQueue`,按帧调用带数量、字节、时间预算的 `PumpAsync`。预算不会抢占正在执行的 handler;为避免饥饿,每轮允许首个合法包超过本轮字节预算。 + +可靠接收队列只允许 Reject。原 DropNewest/DropOldest 配置会明确报错。拒绝通过 `OnDispatchRejected` 与指标反馈;支持会话控制的 transport 会断开连接,以免继续使用缺失可靠操作的状态。 + +显式状态合并使用 `ShrinkNetworkWorkQueue.EnqueueAsync` 的 `stateKey`,只在相同 session/channel/key 内替换未执行项,替换后的项移至该分区队尾。返回值区分 Completed、Rejected、Replaced、Canceled;异常继续向调用方传播。RPC、可靠业务操作、存档事务和快照分片不能使用普通状态替换。多个分区轮转,但处理器仍串行执行。 + +RPC 响应单独完成挂起请求,避免嵌套调用等待当前处理队列而死锁。处理器的 async continuation 线程仍取决于其等待对象;访问引擎对象前由宿主保证线程切换。 + +## Context 与存档 + +`ShrinkContextRuntime.Fibers` 只保留仍被运行时管理的 fiber。退役历史改为有界纯数据诊断,构造参数 `retiredHistoryCapacity` 默认 64;需要审计时由上层保存诊断数据,不能继续依赖运行时永久持有所有历史组件。 + +生命周期转换使用同步队列展开深依赖链,保留确定性协调和依赖排空后撤回自身效应的规则。不要从多个线程同时修改同一 Context runtime。 + +`SaveOptions.EncodeInBackground` 默认关闭。开启后,模块采集和 JSON 快照物化仍在调用线程执行,后台只编码独立快照与加密。`CollectTimings` 提供采集、编码、加密、落盘阶段数据;存档格式、原子替换和备份回退保持兼容。 + +## 本地安装与消费工程迁移 + +本次未发布 registry 或 Git tag。UPM 通过工作区当前源码生成便携 tarball,NuGet 通过本地 feed 安装,不将相同版本的旧全局缓存当作新包验证。 + +~~~powershell +python -X utf8 Tools/AgentSupport/Install-Unity.py D:/UnityBuilds/justanyproject --messagepack +pwsh Tools/AgentSupport/Pack-Local.ps1 +~~~ + +安装器只更新 manifest 的 ShrinkSDK 依赖,包保存在消费工程 Packages/ShrinkSDK。首次原 manifest 备份在 SDK 的 Artifacts/AgentSupport/justanyproject-manifest-before.json。Unity 执行 PackageManager.Client.Resolve,再等编译稳定;最后核对已安装源码与工作区,不仅检查版本字符串。 + +justanyproject 本次安装 22 包,包括 Inspection 0.1.0 和可选 MessagePack adapter 0.1.0;Context 0.3.0、EventBus 2.2.2、Network 0.4.2、DataSaver 2.4.0、UPM CodeGen 0.2.1。项目已有 MessagePack 3.1.10 与生成器继续保留,adapter 已用其实际 formatter 与 .NET 3.1.8 golden frame 做字节互通验证。不要重新安装冲突的 MessagePack 二进制。 + +新增消费侧内容仅为 Assets/Editor/ShrinkInspection 的宿主快照接入、Assets/Tests/ShrinkSDK 的生成 codec fixture 和三个业务能力 README;没有借此次安装覆盖既有玩法、UI、场景或资源修改。自定义 Endless raw TCP 协议 29 不经过 ShrinkNetworkService v2,不需要为了 SDK 编号而改其协议。 + +进入 Entry 后先导出 compilation inputs,再导出 runtime snapshot。接入记录实际应用 Context/Loader 以及 Network/Command 服务;未注册宿主的实例不在默认快照覆盖内。 + +### 回退 + +1. 停止 Play Mode,备份当前工作区与本次 manifest;不要重置消费工程已有未提交工作。 +2. 将本次新增的 Inspection 编辑器接入、ShrinkSDK codec 测试源及其 asmdef/meta 一并从 Assets 移到工程外备份,避免旧依赖缺少新类型时阻断编译;不移动已有游戏文件。 +3. 恢复安装前 manifest,运行 PackageManager.Client.Resolve,由 Unity 重建对应 lock/cache;保留原 tarball 以便再次安装,不手工编辑 Library。 +4. 客户端和使用 ShrinkNetworkService 的服务端共同回退。v1/v2 不兼容,不可只回退单边。纯自定义 transport 协议按其独立策略处理。 + +新 manifest/lock、便携包和接入源应作为一个本地交付单元保存。消费工程 .gitignore 已对 Packages/ShrinkSDK 的 tarball 放行,避免 manifest 引用被忽略而没有一起保存的文件。版本目录仍以正式发布流程为准,Inspection/可选 MessagePack 的新增包图另做本地检查;旧发布检查器并未自动替这两个新增入口提供远程发布保证。 + +验证结果和三项既有业务测试失败详见 AgentSupportAcceptance.md;本地编译与主场景启动不替代完整多人业务回归。 diff --git a/Docs/Performance/2026-09-28.md b/Docs/Performance/2026-09-28.md new file mode 100644 index 0000000..b414bb5 --- /dev/null +++ b/Docs/Performance/2026-09-28.md @@ -0,0 +1,80 @@ +# 性能与跨宿主验收 — 2026-09-28 UTC + +本次为本地工作区验收,不代表远程发布、生产容量承诺或所有后端的性能保证。原始测量时间保留在 JSON/XML 中;Windows 本地显示日期可能为 9 月 29 日。 + +## 方法与证据边界 + +- Windows x64;Unity 2022.3.62f3 的实际 Standalone Player,而非用 EditMode 冒充 Player;另有 .NET 8.0.31、.NET 10 和 Godot 4.6.3 Mono 的实际运行。 +- 网络矩阵:64 B/1 KB/32 KB 负载参数,1/32/256 会话;RPC、广播、慢消费者、突发队列。Loopback 会话不是 OS socket;TCP 文件是实际本机 socket,且 RPC 采用串行往返采样,不等于高并发容量压测。 +- Context:10/100/1000 组件依赖链,10,000 次创建退役;旧实现来自 Context 子模块提交 b93f3e61223e7572588b67c0d3935073817f0da6,不是重新编写的假想基线。 +- 存档:100 KB/1 MB/10 MB,前台/后台编码;.NET 每组合 10 次并覆盖加密,Player 记录真实阶段耗时与采集线程。输入主要为字符串快照,不能推断复杂游戏对象的采集成本。 +- 单机开发环境、有限样本,非隔离实验机;p99 易受 JIT、GC、系统调度影响。没有可靠的多轮置信区间,不将小幅波动称为普遍提升。 +- Unity 的 GC.GetAllocatedBytesForCurrentThread 对已知分配返回零,已弃用该结果。Player 改用经已知数组分配校准的 GC.Alloc 事件计数;事件次数不是 B/op。独立 .NET 的字节计数与 Player 分开报告。 + +原始数据位于 [Data/2026-09-28](Data/2026-09-28/);Player 完整输出同时保存在 XML 与 player-output.txt。工具、测试源代码保留在 Tools/AgentSupport 与 Assets/AgentSupportTests。 + +## 已量化的核心变化 + +### Context(.NET 8) + +| 组件数 | 旧诊断 p95 µs | 新诊断 p95 µs | 旧 B/op | 新 B/op | +|---:|---:|---:|---:|---:| +| 10 | 9.4 | 11.7 | 10,456 | 7,312 | +| 100 | 538.2 | 116.9 | 108,016 | 73,456 | +| 1000 | 7,426.4 | 548.3 | 1,083,616 | 735,328 | + +大规模诊断通过按依赖键访问 provider 候选索引获益;10 个组件的 p95 本轮反而略高,不能宣称所有规模都更快。10,000 次退役后旧运行时持有 10,000 个 fiber,新运行时为 0,诊断历史有界 64。弱引用测试验证实例可回收;整进程 GC 差值可能为负,不将其解读为精确节省字节数。 + +深链生命周期改为迭代队列展开,避免 1000 层同步完成/撤回造成栈溢出;不改变依赖排空与效应撤回顺序。 + +### 网络封装(.NET 8) + +| 负载参数 | 旧 JSON 外封装 B/op | 新 v2 外封装 B/op | +|---:|---:|---:| +| 64 B | 3,504 | 32 | +| 1 KB | 10,560 | 32 | +| 32 KB | 230,328 | 32 | + +这里只比较已经编码的消息体如何封装,不是端到端零分配。新封装取消外层 JSON/Base64,消息直接写入池化 buffer;仍有 writer 对象、消息反序列化、排队和 transport 的独立成本。JSON 与生成式 MessagePack 的完整消息编码对照见原始 rows,不能套用此表的 32 B。 + +队列突发测试输入 10,000 个显式可替换状态,32 个 session 分区最终保留 32 项、32 KB,返回 9,968 次 Replaced;一次 4 KB budget 泵送完成 4 项。可靠操作不使用这种替换机制,满队列显式拒绝。 + +### 实际 TCP 与后端选择 + +256 个本机 TCP 连接、1 KB、256 次串行 RPC 往返: + +| 运行时 | p50 µs | p95 µs | p99 µs | ops/s | +|---|---:|---:|---:|---:| +| .NET 8 | 99.3 | 258.9 | 355.1 | 6,911 | +| .NET 10 | 88.4 | 250.8 | 384.3 | 7,392 | + +其他参数组合有升有降;本轮未证明目标场景稳定达到 15% 收益,因此不引入新的默认 .NET 10 专属服务端依赖。Pipelines 仅有内存 buffer roundtrip 微实验,未实现等价 socket 后端对照,不足以采纳。共享运行时继续 netstandard2.1。 + +TCP 慢消费者测试证明有界队列和拒绝反馈,不证明业务消费成功:该突发负载使用 RPC 消息的一次性发送路径,记录中存在 DispatchMissCount;不能将该行视为完整 handler 吞吐测试。可靠队列行为、会话隔离、取消和故障另由功能测试验证。KCP 未作独立吞吐结论。 + +### EventBus 与存档 + +.NET 8 同步 Post、同步完成的异步 Post 为 0 B/op;注册/撤回 224 B/op,按需绑定诊断 120 B/op。同步路径的捕获闭包已移出快路径;Unity 的结构体事件空值检查另外增加值类型短路,避免 Mono 装箱。 + +校准后的 Player 实测:独立 Bus 10,000 次同步发布为 0 次分配事件;当前默认 Bus 为 10,000 次分配事件。修复值类型空值检查前默认通道是 20,000 次,不能将早先字节 API 的零值作为基线。 + +默认全局 Bus 可能带网络桥或诊断观察器;接口观察器需要结构体装箱,不能把独立 Bus 的零分配结果扩大到所有配置。Player 同时测量默认与独立通道,并为引用类型空值保留异常检查。 + +存档采集仍在调用线程,后台开关仅移动脱离宿主对象的快照编码与加密。示例 .NET 8 的 10 MB 未加密平均编码为前台约 7.68 ms、后台约 8.21 ms;主要收益是主线程工作转移,而非宣称总保存时间必然缩短。Player 的冷启动与各阶段实测见 player-output.txt;未隐藏首轮高耗时。格式、原子落盘、取消时备份保留及损坏回退均有测试。 + +## 复现 + +在已还原依赖的仓库运行: + +~~~powershell +pwsh Tools/AgentSupport/Benchmarks/Prepare-Baseline.ps1 +dotnet run --project Tools/AgentSupport/Benchmarks -c Release -f net8.0 -- Artifacts/AgentSupport/benchmark-net8.json +dotnet run --project Tools/AgentSupport/Benchmarks -c Release -f net10.0 -- Artifacts/AgentSupport/benchmark-net10.json +dotnet run --project Tools/AgentSupport/Benchmarks -c Release -f net8.0 -- --tcp Artifacts/AgentSupport/tcp-net8.json +dotnet test Tools/AgentSupport/RuntimeTests +dotnet test Tools/AgentSupport/Shrink.Inspect.Tests +~~~ + +Unity 用 TestRunner 的 StandaloneWindows64 Player 运行 ShrinkSDK.AgentSupport.PlayerTests;Editor 辅助入口 PlayerAcceptance.Run() 将本次结果保存到 Artifacts/AgentSupport/unity-player-results.xml。基线脚本读取子模块 HEAD:以后 HEAD 前进时,应显式使用上述旧提交重建对照,不把新的 HEAD 当成本报告基线。 + +最终功能验收、消费工程已知失败和导航覆盖范围见 [AgentSupportAcceptance](../AgentSupportAcceptance.md);协议和回退见 [迁移说明](../NetworkV2Migration.md)。 diff --git a/Docs/Performance/Data/2026-09-28/README.md b/Docs/Performance/Data/2026-09-28/README.md new file mode 100644 index 0000000..f230695 --- /dev/null +++ b/Docs/Performance/Data/2026-09-28/README.md @@ -0,0 +1,18 @@ +# 本地验收原始证据 + +JSON/XML 内保留各次采集时间,未把编辑器微基准改名为 Player 结果。 + +- benchmark-net8/net10:封装、消息编码、Loopback、Context、EventBus、存档阶段和实验性 Pipelines。 +- tcp-net8/net10:实际本机 socket,串行 RPC 往返;慢消费者行的 DispatchMissCount 见上级报告限制。 +- unity-player-results.xml、player-output.txt:实际 Windows Player 的 10 项测试和已校准分配事件;不是字节分配计数。 +- unity-all-editmode-results.json:SDK 全量 226 项 EditMode 通过。 +- runtime-tests-final.log、cli-tests-final.log:64/12 项独立回归。 +- godot-run-final.log、nuget-consumer-final.log:实际跨宿主与隔离包消费运行。 +- justany-tests.json:83 项定向业务测试,80 通过/3 失败,保留失败原因。 +- justany-messagepack-results.json:Unity 生成 formatter 对 .NET golden frame 的互通。 +- justany-runtime.json、justany-installed.json:实际应用状态和已注册包路径/版本。 +- package-validation.json:24 包本地图、22 包安装比较;20 个 NuGet.Config.meta 由消费项目 NuGetForUnity 修改导入标签/平台设置,GUID 相同,其他 1053 个文件一致。 +- waiting-query.json:当时匹配构建的缺失 provider 查询产物;不是当前实时状态。 +- sha256.json:上述原始文件的摘要;用于校验证据文件,不充当各基准构建的代码提交标识。 + +consumer-runtime-query.json 是最后一次实际宿主导出后立即查询的 route 链,包含 5 条 runtime-bound 关系;justany-play-errors.json 和 justany-compilation.json 记录该次运行后的检查。 diff --git a/Docs/Performance/Data/2026-09-28/benchmark-net10.json b/Docs/Performance/Data/2026-09-28/benchmark-net10.json new file mode 100644 index 0000000..f9a1418 --- /dev/null +++ b/Docs/Performance/Data/2026-09-28/benchmark-net10.json @@ -0,0 +1,1506 @@ +{ + "runtime": ".NET 10.0.8", + "host": "standalone .NET; Loopback sessions are not OS connections; diagnostic microbenchmarks are not Player data", + "atUtc": "2026-09-28T21:30:12.1049948Z", + "rows": [ + { + "scenario": "network/legacy-json-envelope", + "parameters": { + "payloadBytes": 64 + }, + "iterations": 1000, + "throughputPerSecond": 275065.32801540365, + "allocatedBytesPerOperation": 3512, + "p50Microseconds": 2.7, + "p95Microseconds": 5.4, + "p99Microseconds": 7.1 + }, + { + "scenario": "network/v2-envelope", + "parameters": { + "payloadBytes": 64 + }, + "iterations": 1000, + "throughputPerSecond": 3179650.238473768, + "allocatedBytesPerOperation": 32.392, + "p50Microseconds": 0.3, + "p95Microseconds": 0.3, + "p99Microseconds": 0.4 + }, + { + "scenario": "network/v2-json-message", + "parameters": { + "payloadBytes": 64 + }, + "iterations": 1000, + "throughputPerSecond": 554139.421478444, + "allocatedBytesPerOperation": 3136.24, + "p50Microseconds": 1.6, + "p95Microseconds": 2.3, + "p99Microseconds": 3 + }, + { + "scenario": "network/v2-generated-messagepack", + "parameters": { + "payloadBytes": 64 + }, + "iterations": 1000, + "throughputPerSecond": 1911680.3670426304, + "allocatedBytesPerOperation": 112.368, + "p50Microseconds": 0.5, + "p95Microseconds": 0.5, + "p99Microseconds": 0.8 + }, + { + "scenario": "network/loopback-session-send", + "parameters": { + "payloadBytes": 64, + "connections": 1 + }, + "iterations": 1000, + "throughputPerSecond": 160261.54684445015, + "allocatedBytesPerOperation": 6944.24, + "p50Microseconds": 5.4, + "p95Microseconds": 8, + "p99Microseconds": 12.5 + }, + { + "scenario": "network/loopback-broadcast-shared-payload", + "parameters": { + "payloadBytes": 64, + "connections": 1 + }, + "iterations": 25, + "throughputPerSecond": 151423.37976983647, + "allocatedBytesPerOperation": 4015.04, + "p50Microseconds": 4.6, + "p95Microseconds": 7.2, + "p99Microseconds": 48.5 + }, + { + "scenario": "network/loopback-session-send", + "parameters": { + "payloadBytes": 64, + "connections": 32 + }, + "iterations": 1000, + "throughputPerSecond": 160523.95017336585, + "allocatedBytesPerOperation": 6944.24, + "p50Microseconds": 5.2, + "p95Microseconds": 9.1, + "p99Microseconds": 10.6 + }, + { + "scenario": "network/loopback-broadcast-shared-payload", + "parameters": { + "payloadBytes": 64, + "connections": 32 + }, + "iterations": 25, + "throughputPerSecond": 6599.7888067581835, + "allocatedBytesPerOperation": 125783.04, + "p50Microseconds": 125.1, + "p95Microseconds": 227.2, + "p99Microseconds": 290.2 + }, + { + "scenario": "network/loopback-session-send", + "parameters": { + "payloadBytes": 64, + "connections": 256 + }, + "iterations": 1000, + "throughputPerSecond": 175208.05957074027, + "allocatedBytesPerOperation": 6944.24, + "p50Microseconds": 5.3, + "p95Microseconds": 7.2, + "p99Microseconds": 9.4 + }, + { + "scenario": "network/loopback-broadcast-shared-payload", + "parameters": { + "payloadBytes": 64, + "connections": 256 + }, + "iterations": 25, + "throughputPerSecond": 870.6614937765116, + "allocatedBytesPerOperation": 1005651.52, + "p50Microseconds": 1092.4, + "p95Microseconds": 1434.7, + "p99Microseconds": 1663.9 + }, + { + "scenario": "network/legacy-json-envelope", + "parameters": { + "payloadBytes": 1024 + }, + "iterations": 1000, + "throughputPerSecond": 143323.9695006593, + "allocatedBytesPerOperation": 10568, + "p50Microseconds": 5.5, + "p95Microseconds": 7.9, + "p99Microseconds": 24.7 + }, + { + "scenario": "network/v2-envelope", + "parameters": { + "payloadBytes": 1024 + }, + "iterations": 1000, + "throughputPerSecond": 2974419.98810232, + "allocatedBytesPerOperation": 34.248, + "p50Microseconds": 0.3, + "p95Microseconds": 0.3, + "p99Microseconds": 0.3 + }, + { + "scenario": "network/v2-json-message", + "parameters": { + "payloadBytes": 1024 + }, + "iterations": 1000, + "throughputPerSecond": 101129.61783117423, + "allocatedBytesPerOperation": 6243.264, + "p50Microseconds": 5.3, + "p95Microseconds": 11.4, + "p99Microseconds": 35.2 + }, + { + "scenario": "network/v2-generated-messagepack", + "parameters": { + "payloadBytes": 1024 + }, + "iterations": 1000, + "throughputPerSecond": 1722949.6898690558, + "allocatedBytesPerOperation": 120.416, + "p50Microseconds": 0.5, + "p95Microseconds": 0.6, + "p99Microseconds": 0.9 + }, + { + "scenario": "network/loopback-session-send", + "parameters": { + "payloadBytes": 1024, + "connections": 1 + }, + "iterations": 1000, + "throughputPerSecond": 58379.160974698476, + "allocatedBytesPerOperation": 18971.264, + "p50Microseconds": 13, + "p95Microseconds": 28.5, + "p99Microseconds": 64.1 + }, + { + "scenario": "network/loopback-broadcast-shared-payload", + "parameters": { + "payloadBytes": 1024, + "connections": 1 + }, + "iterations": 25, + "throughputPerSecond": 94768.76421531463, + "allocatedBytesPerOperation": 13017.92, + "p50Microseconds": 8.7, + "p95Microseconds": 11.6, + "p99Microseconds": 51.1 + }, + { + "scenario": "network/loopback-session-send", + "parameters": { + "payloadBytes": 1024, + "connections": 32 + }, + "iterations": 1000, + "throughputPerSecond": 66881.13216380526, + "allocatedBytesPerOperation": 18972.4, + "p50Microseconds": 12.7, + "p95Microseconds": 19.7, + "p99Microseconds": 45.3 + }, + { + "scenario": "network/loopback-broadcast-shared-payload", + "parameters": { + "payloadBytes": 1024, + "connections": 32 + }, + "iterations": 25, + "throughputPerSecond": 2782.50804144824, + "allocatedBytesPerOperation": 411305.92, + "p50Microseconds": 334.7, + "p95Microseconds": 657.9, + "p99Microseconds": 699 + }, + { + "scenario": "network/loopback-session-send", + "parameters": { + "payloadBytes": 1024, + "connections": 256 + }, + "iterations": 1000, + "throughputPerSecond": 43279.73859037891, + "allocatedBytesPerOperation": 18971.264, + "p50Microseconds": 18.8, + "p95Microseconds": 42.6, + "p99Microseconds": 108.2 + }, + { + "scenario": "network/loopback-broadcast-shared-payload", + "parameters": { + "payloadBytes": 1024, + "connections": 256 + }, + "iterations": 25, + "throughputPerSecond": 331.20566812132193, + "allocatedBytesPerOperation": 3289254.4, + "p50Microseconds": 2852.9, + "p95Microseconds": 3688.7, + "p99Microseconds": 4192.6 + }, + { + "scenario": "network/legacy-json-envelope", + "parameters": { + "payloadBytes": 32768 + }, + "iterations": 1000, + "throughputPerSecond": 5821.6400840877695, + "allocatedBytesPerOperation": 230336, + "p50Microseconds": 106.9, + "p95Microseconds": 222.8, + "p99Microseconds": 1893.1 + }, + { + "scenario": "network/v2-envelope", + "parameters": { + "payloadBytes": 32768 + }, + "iterations": 1000, + "throughputPerSecond": 782044.2637053257, + "allocatedBytesPerOperation": 97.736, + "p50Microseconds": 1.2, + "p95Microseconds": 1.3, + "p99Microseconds": 1.6 + }, + { + "scenario": "network/v2-json-message", + "parameters": { + "payloadBytes": 32768 + }, + "iterations": 1000, + "throughputPerSecond": 31901.589975244362, + "allocatedBytesPerOperation": 6370.36, + "p50Microseconds": 28.1, + "p95Microseconds": 48.9, + "p99Microseconds": 82.6 + }, + { + "scenario": "network/v2-generated-messagepack", + "parameters": { + "payloadBytes": 32768 + }, + "iterations": 1000, + "throughputPerSecond": 370205.83444395085, + "allocatedBytesPerOperation": 243.184, + "p50Microseconds": 2.3, + "p95Microseconds": 3.9, + "p99Microseconds": 13.6 + }, + { + "scenario": "network/loopback-session-send", + "parameters": { + "payloadBytes": 32768, + "connections": 1 + }, + "iterations": 1000, + "throughputPerSecond": 5804.329100816553, + "allocatedBytesPerOperation": 435202.232, + "p50Microseconds": 104.6, + "p95Microseconds": 253.6, + "p99Microseconds": 1920 + }, + { + "scenario": "network/loopback-broadcast-shared-payload", + "parameters": { + "payloadBytes": 32768, + "connections": 1 + }, + "iterations": 25, + "throughputPerSecond": 10036.532980047374, + "allocatedBytesPerOperation": 428349.44, + "p50Microseconds": 76.6, + "p95Microseconds": 131.4, + "p99Microseconds": 652.6 + }, + { + "scenario": "network/loopback-session-send", + "parameters": { + "payloadBytes": 32768, + "connections": 32 + }, + "iterations": 1000, + "throughputPerSecond": 6689.553860273951, + "allocatedBytesPerOperation": 435922.512, + "p50Microseconds": 105.2, + "p95Microseconds": 260.9, + "p99Microseconds": 1218.7 + }, + { + "scenario": "network/loopback-broadcast-shared-payload", + "parameters": { + "payloadBytes": 32768, + "connections": 32 + }, + "iterations": 25, + "throughputPerSecond": 311.81167572583524, + "allocatedBytesPerOperation": 13633791.68, + "p50Microseconds": 3288.7, + "p95Microseconds": 3914.1, + "p99Microseconds": 4147.4 + }, + { + "scenario": "network/loopback-session-send", + "parameters": { + "payloadBytes": 32768, + "connections": 256 + }, + "iterations": 1000, + "throughputPerSecond": 6190.386577260976, + "allocatedBytesPerOperation": 435595.064, + "p50Microseconds": 86.6, + "p95Microseconds": 211.8, + "p99Microseconds": 1758.5 + }, + { + "scenario": "network/loopback-broadcast-shared-payload", + "parameters": { + "payloadBytes": 32768, + "connections": 256 + }, + "iterations": 25, + "throughputPerSecond": 28.569701328913943, + "allocatedBytesPerOperation": 109006786.56, + "p50Microseconds": 34834.4, + "p95Microseconds": 46626.8, + "p99Microseconds": 49805.5 + }, + { + "scenario": "context/current-diagnostics", + "parameters": { + "components": 10 + }, + "iterations": 100, + "throughputPerSecond": 102103.32856851132, + "allocatedBytesPerOperation": 7664, + "p50Microseconds": 9.1, + "p95Microseconds": 9.5, + "p99Microseconds": 64.5 + }, + { + "scenario": "context/baseline-diagnostics", + "parameters": { + "components": 10 + }, + "iterations": 100, + "throughputPerSecond": 47303.689687795646, + "allocatedBytesPerOperation": 8584, + "p50Microseconds": 20.3, + "p95Microseconds": 22.3, + "p99Microseconds": 101.5 + }, + { + "scenario": "context/current-diagnostics", + "parameters": { + "components": 100 + }, + "iterations": 100, + "throughputPerSecond": 6797.865470242344, + "allocatedBytesPerOperation": 76688, + "p50Microseconds": 106.5, + "p95Microseconds": 384.8, + "p99Microseconds": 619.9 + }, + { + "scenario": "context/baseline-diagnostics", + "parameters": { + "components": 100 + }, + "iterations": 100, + "throughputPerSecond": 1711.1801667373954, + "allocatedBytesPerOperation": 87424, + "p50Microseconds": 598.3, + "p95Microseconds": 717.1, + "p99Microseconds": 1314.9 + }, + { + "scenario": "context/current-diagnostics", + "parameters": { + "components": 1000 + }, + "iterations": 100, + "throughputPerSecond": 918.5300579776173, + "allocatedBytesPerOperation": 767360, + "p50Microseconds": 1076.7, + "p95Microseconds": 2006.4, + "p99Microseconds": 2218.9 + }, + { + "scenario": "context/baseline-diagnostics", + "parameters": { + "components": 1000 + }, + "iterations": 100, + "throughputPerSecond": 157.44704032628064, + "allocatedBytesPerOperation": 875824, + "p50Microseconds": 6156.6, + "p95Microseconds": 7668.6, + "p99Microseconds": 8162 + }, + { + "scenario": "context/10000-retirements", + "currentRetainedBytes": -2310080, + "baselineRetainedBytes": 8502328, + "currentFibers": 0, + "currentHistory": 64, + "baselineFibers": 10000 + }, + { + "scenario": "event/sync", + "parameters": null, + "iterations": 100000, + "throughputPerSecond": 7485534.20514855, + "allocatedBytesPerOperation": 0, + "p50Microseconds": 0.1, + "p95Microseconds": 0.1, + "p99Microseconds": 0.2 + }, + { + "scenario": "event/async-sync-subscriber", + "parameters": null, + "iterations": 10000, + "throughputPerSecond": 3772303.74589762, + "allocatedBytesPerOperation": 0, + "p50Microseconds": 0.2, + "p95Microseconds": 0.3, + "p99Microseconds": 0.3 + }, + { + "scenario": "event/binding-diagnostics", + "parameters": null, + "iterations": 1000, + "throughputPerSecond": 3656307.1297989036, + "allocatedBytesPerOperation": 120, + "p50Microseconds": 0.2, + "p95Microseconds": 0.2, + "p99Microseconds": 0.5 + }, + { + "scenario": "event/register-withdraw", + "parameters": null, + "iterations": 10000, + "throughputPerSecond": 1467093.1017282358, + "allocatedBytesPerOperation": 224, + "p50Microseconds": 0.5, + "p95Microseconds": 0.7, + "p99Microseconds": 3.7 + }, + { + "scenario": "event/async-subscriber", + "parameters": null, + "iterations": 10000, + "throughputPerSecond": 4033233.846898443, + "allocatedBytesPerOperation": 0, + "p50Microseconds": 0.2, + "p95Microseconds": 0.3, + "p99Microseconds": 0.3 + }, + { + "scenario": "queue/burst-state", + "before": { + "PendingCount": 32, + "PendingBytes": 32768, + "Rejected": 0, + "Replaced": 9968, + "Completed": 0, + "OldestWaitMilliseconds": 0.0095, + "LastWaitMilliseconds": 0 + }, + "after": { + "PendingCount": 28, + "PendingBytes": 28672, + "Rejected": 0, + "Replaced": 9968, + "Completed": 4, + "OldestWaitMilliseconds": 0.1673, + "LastWaitMilliseconds": 0.1616 + } + }, + { + "scenario": "save/phases", + "snapshotBytes": 102400, + "background": false, + "encrypted": false, + "elapsedMilliseconds": 492.5919, + "processAllocatedBytes": 5891920, + "samples": [ + { + "CaptureMilliseconds": 8.1006, + "EncodeMilliseconds": 33.0207, + "EncryptMilliseconds": 0, + "WriteMilliseconds": 37.4005, + "EncodedBytes": 102567 + }, + { + "CaptureMilliseconds": 0.091, + "EncodeMilliseconds": 0.3448, + "EncryptMilliseconds": 0, + "WriteMilliseconds": 13.1863, + "EncodedBytes": 102567 + }, + { + "CaptureMilliseconds": 0.0429, + "EncodeMilliseconds": 0.2747, + "EncryptMilliseconds": 0, + "WriteMilliseconds": 12.3583, + "EncodedBytes": 102567 + }, + { + "CaptureMilliseconds": 0.0405, + "EncodeMilliseconds": 0.2313, + "EncryptMilliseconds": 0, + "WriteMilliseconds": 9.252, + "EncodedBytes": 102567 + }, + { + "CaptureMilliseconds": 0.0407, + "EncodeMilliseconds": 0.2979, + "EncryptMilliseconds": 0, + "WriteMilliseconds": 10.0219, + "EncodedBytes": 102567 + }, + { + "CaptureMilliseconds": 0.0433, + "EncodeMilliseconds": 3.1242, + "EncryptMilliseconds": 0, + "WriteMilliseconds": 259.7361, + "EncodedBytes": 102567 + }, + { + "CaptureMilliseconds": 0.0518, + "EncodeMilliseconds": 0.2999, + "EncryptMilliseconds": 0, + "WriteMilliseconds": 70.4135, + "EncodedBytes": 102567 + }, + { + "CaptureMilliseconds": 0.044, + "EncodeMilliseconds": 0.3036, + "EncryptMilliseconds": 0, + "WriteMilliseconds": 9.265, + "EncodedBytes": 102567 + }, + { + "CaptureMilliseconds": 0.0491, + "EncodeMilliseconds": 0.2551, + "EncryptMilliseconds": 0, + "WriteMilliseconds": 9.9306, + "EncodedBytes": 102567 + }, + { + "CaptureMilliseconds": 0.0424, + "EncodeMilliseconds": 0.3005, + "EncryptMilliseconds": 0, + "WriteMilliseconds": 10.1206, + "EncodedBytes": 102567 + } + ] + }, + { + "scenario": "save/phases", + "snapshotBytes": 102400, + "background": false, + "encrypted": true, + "elapsedMilliseconds": 159.8585, + "processAllocatedBytes": 17221576, + "samples": [ + { + "CaptureMilliseconds": 0.0438, + "EncodeMilliseconds": 0.7538, + "EncryptMilliseconds": 16.5021, + "WriteMilliseconds": 10.3973, + "EncodedBytes": 136784 + }, + { + "CaptureMilliseconds": 0.0408, + "EncodeMilliseconds": 1.5252000000000001, + "EncryptMilliseconds": 3.6456, + "WriteMilliseconds": 10.1746, + "EncodedBytes": 136784 + }, + { + "CaptureMilliseconds": 0.0405, + "EncodeMilliseconds": 1.2368, + "EncryptMilliseconds": 3.4544, + "WriteMilliseconds": 11.639, + "EncodedBytes": 136784 + }, + { + "CaptureMilliseconds": 0.0409, + "EncodeMilliseconds": 0.5706, + "EncryptMilliseconds": 3.2193, + "WriteMilliseconds": 11.8822, + "EncodedBytes": 136784 + }, + { + "CaptureMilliseconds": 0.0436, + "EncodeMilliseconds": 0.42269999999999996, + "EncryptMilliseconds": 3.3107, + "WriteMilliseconds": 9.9851, + "EncodedBytes": 136784 + }, + { + "CaptureMilliseconds": 0.0545, + "EncodeMilliseconds": 0.7217, + "EncryptMilliseconds": 3.3503, + "WriteMilliseconds": 9.3812, + "EncodedBytes": 136784 + }, + { + "CaptureMilliseconds": 0.0415, + "EncodeMilliseconds": 0.374, + "EncryptMilliseconds": 3.2821, + "WriteMilliseconds": 10.5387, + "EncodedBytes": 136784 + }, + { + "CaptureMilliseconds": 0.0465, + "EncodeMilliseconds": 0.6067, + "EncryptMilliseconds": 3.2332, + "WriteMilliseconds": 9.6935, + "EncodedBytes": 136784 + }, + { + "CaptureMilliseconds": 0.0444, + "EncodeMilliseconds": 0.3421, + "EncryptMilliseconds": 3.7881, + "WriteMilliseconds": 9.7639, + "EncodedBytes": 136784 + }, + { + "CaptureMilliseconds": 0.0351, + "EncodeMilliseconds": 0.6562, + "EncryptMilliseconds": 3.3206, + "WriteMilliseconds": 10.4302, + "EncodedBytes": 136784 + } + ] + }, + { + "scenario": "save/phases", + "snapshotBytes": 102400, + "background": true, + "encrypted": false, + "elapsedMilliseconds": 107.3538, + "processAllocatedBytes": 5357408, + "samples": [ + { + "CaptureMilliseconds": 0.0519, + "EncodeMilliseconds": 0.1898, + "EncryptMilliseconds": 0, + "WriteMilliseconds": 10.8963, + "EncodedBytes": 102567 + }, + { + "CaptureMilliseconds": 0.042, + "EncodeMilliseconds": 0.1549, + "EncryptMilliseconds": 0, + "WriteMilliseconds": 11.4458, + "EncodedBytes": 102567 + }, + { + "CaptureMilliseconds": 0.0441, + "EncodeMilliseconds": 0.1975, + "EncryptMilliseconds": 0, + "WriteMilliseconds": 10.3143, + "EncodedBytes": 102567 + }, + { + "CaptureMilliseconds": 0.0424, + "EncodeMilliseconds": 0.1591, + "EncryptMilliseconds": 0, + "WriteMilliseconds": 9.7665, + "EncodedBytes": 102567 + }, + { + "CaptureMilliseconds": 0.0449, + "EncodeMilliseconds": 0.1591, + "EncryptMilliseconds": 0, + "WriteMilliseconds": 10.7366, + "EncodedBytes": 102567 + }, + { + "CaptureMilliseconds": 0.0369, + "EncodeMilliseconds": 0.3979, + "EncryptMilliseconds": 0, + "WriteMilliseconds": 10.4723, + "EncodedBytes": 102567 + }, + { + "CaptureMilliseconds": 0.0419, + "EncodeMilliseconds": 0.1694, + "EncryptMilliseconds": 0, + "WriteMilliseconds": 10.958, + "EncodedBytes": 102567 + }, + { + "CaptureMilliseconds": 0.042, + "EncodeMilliseconds": 0.1772, + "EncryptMilliseconds": 0, + "WriteMilliseconds": 8.6996, + "EncodedBytes": 102567 + }, + { + "CaptureMilliseconds": 0.0438, + "EncodeMilliseconds": 0.1683, + "EncryptMilliseconds": 0, + "WriteMilliseconds": 11.1525, + "EncodedBytes": 102567 + }, + { + "CaptureMilliseconds": 0.0407, + "EncodeMilliseconds": 0.1527, + "EncryptMilliseconds": 0, + "WriteMilliseconds": 9.7075, + "EncodedBytes": 102567 + } + ] + }, + { + "scenario": "save/phases", + "snapshotBytes": 102400, + "background": true, + "encrypted": true, + "elapsedMilliseconds": 163.2504, + "processAllocatedBytes": 17184672, + "samples": [ + { + "CaptureMilliseconds": 0.0454, + "EncodeMilliseconds": 1.5443, + "EncryptMilliseconds": 3.1719, + "WriteMilliseconds": 11.9392, + "EncodedBytes": 136784 + }, + { + "CaptureMilliseconds": 0.0431, + "EncodeMilliseconds": 0.3664, + "EncryptMilliseconds": 3.7874, + "WriteMilliseconds": 11.1521, + "EncodedBytes": 136784 + }, + { + "CaptureMilliseconds": 0.042, + "EncodeMilliseconds": 0.7787, + "EncryptMilliseconds": 3.3806, + "WriteMilliseconds": 12.6258, + "EncodedBytes": 136784 + }, + { + "CaptureMilliseconds": 0.0405, + "EncodeMilliseconds": 0.3501, + "EncryptMilliseconds": 3.2239, + "WriteMilliseconds": 11.9353, + "EncodedBytes": 136784 + }, + { + "CaptureMilliseconds": 0.0459, + "EncodeMilliseconds": 0.5949, + "EncryptMilliseconds": 3.856, + "WriteMilliseconds": 14.1356, + "EncodedBytes": 136784 + }, + { + "CaptureMilliseconds": 0.0811, + "EncodeMilliseconds": 0.38649999999999995, + "EncryptMilliseconds": 3.7922, + "WriteMilliseconds": 10.966, + "EncodedBytes": 136784 + }, + { + "CaptureMilliseconds": 0.0345, + "EncodeMilliseconds": 1.3151000000000002, + "EncryptMilliseconds": 4.559, + "WriteMilliseconds": 11.2043, + "EncodedBytes": 136784 + }, + { + "CaptureMilliseconds": 0.0437, + "EncodeMilliseconds": 0.2953, + "EncryptMilliseconds": 3.3202, + "WriteMilliseconds": 12.2786, + "EncodedBytes": 136784 + }, + { + "CaptureMilliseconds": 0.0358, + "EncodeMilliseconds": 0.5129, + "EncryptMilliseconds": 3.3678, + "WriteMilliseconds": 12.5374, + "EncodedBytes": 136784 + }, + { + "CaptureMilliseconds": 0.0383, + "EncodeMilliseconds": 0.2827, + "EncryptMilliseconds": 3.3328, + "WriteMilliseconds": 10.2607, + "EncodedBytes": 136784 + } + ] + }, + { + "scenario": "save/phases", + "snapshotBytes": 1048576, + "background": false, + "encrypted": false, + "elapsedMilliseconds": 135.3594, + "processAllocatedBytes": 52727296, + "samples": [ + { + "CaptureMilliseconds": 0.0404, + "EncodeMilliseconds": 2.1422, + "EncryptMilliseconds": 0, + "WriteMilliseconds": 10.726, + "EncodedBytes": 1048743 + }, + { + "CaptureMilliseconds": 0.0385, + "EncodeMilliseconds": 1.3005, + "EncryptMilliseconds": 0, + "WriteMilliseconds": 11.8459, + "EncodedBytes": 1048743 + }, + { + "CaptureMilliseconds": 0.0414, + "EncodeMilliseconds": 1.6967, + "EncryptMilliseconds": 0, + "WriteMilliseconds": 11.6083, + "EncodedBytes": 1048743 + }, + { + "CaptureMilliseconds": 0.0408, + "EncodeMilliseconds": 1.71, + "EncryptMilliseconds": 0, + "WriteMilliseconds": 12.2906, + "EncodedBytes": 1048743 + }, + { + "CaptureMilliseconds": 0.0389, + "EncodeMilliseconds": 1.7498, + "EncryptMilliseconds": 0, + "WriteMilliseconds": 12.9432, + "EncodedBytes": 1048743 + }, + { + "CaptureMilliseconds": 0.0448, + "EncodeMilliseconds": 1.588, + "EncryptMilliseconds": 0, + "WriteMilliseconds": 11.7309, + "EncodedBytes": 1048743 + }, + { + "CaptureMilliseconds": 0.0373, + "EncodeMilliseconds": 1.8777, + "EncryptMilliseconds": 0, + "WriteMilliseconds": 12.0149, + "EncodedBytes": 1048743 + }, + { + "CaptureMilliseconds": 0.0404, + "EncodeMilliseconds": 1.2343, + "EncryptMilliseconds": 0, + "WriteMilliseconds": 13.7337, + "EncodedBytes": 1048743 + }, + { + "CaptureMilliseconds": 0.0391, + "EncodeMilliseconds": 1.3233, + "EncryptMilliseconds": 0, + "WriteMilliseconds": 10.9884, + "EncodedBytes": 1048743 + }, + { + "CaptureMilliseconds": 0.0389, + "EncodeMilliseconds": 1.1755, + "EncryptMilliseconds": 0, + "WriteMilliseconds": 11.0014, + "EncodedBytes": 1048743 + } + ] + }, + { + "scenario": "save/phases", + "snapshotBytes": 1048576, + "background": false, + "encrypted": true, + "elapsedMilliseconds": 218.4906, + "processAllocatedBytes": 171734752, + "samples": [ + { + "CaptureMilliseconds": 0.0419, + "EncodeMilliseconds": 3.9634, + "EncryptMilliseconds": 5.5269, + "WriteMilliseconds": 11.8553, + "EncodedBytes": 1398352 + }, + { + "CaptureMilliseconds": 0.0385, + "EncodeMilliseconds": 2.879, + "EncryptMilliseconds": 4.5603, + "WriteMilliseconds": 11.1162, + "EncodedBytes": 1398352 + }, + { + "CaptureMilliseconds": 0.0386, + "EncodeMilliseconds": 3.5846, + "EncryptMilliseconds": 4.7762, + "WriteMilliseconds": 13.2829, + "EncodedBytes": 1398352 + }, + { + "CaptureMilliseconds": 0.0558, + "EncodeMilliseconds": 3.4905, + "EncryptMilliseconds": 4.5583, + "WriteMilliseconds": 13.4998, + "EncodedBytes": 1398352 + }, + { + "CaptureMilliseconds": 0.0483, + "EncodeMilliseconds": 2.6014, + "EncryptMilliseconds": 5.0912, + "WriteMilliseconds": 16.3448, + "EncodedBytes": 1398352 + }, + { + "CaptureMilliseconds": 0.043, + "EncodeMilliseconds": 2.6246, + "EncryptMilliseconds": 4.5423, + "WriteMilliseconds": 14.0758, + "EncodedBytes": 1398352 + }, + { + "CaptureMilliseconds": 0.0352, + "EncodeMilliseconds": 3.0539, + "EncryptMilliseconds": 5.1704, + "WriteMilliseconds": 12.6328, + "EncodedBytes": 1398352 + }, + { + "CaptureMilliseconds": 0.049, + "EncodeMilliseconds": 2.8352, + "EncryptMilliseconds": 7.4625, + "WriteMilliseconds": 12.4396, + "EncodedBytes": 1398352 + }, + { + "CaptureMilliseconds": 0.0437, + "EncodeMilliseconds": 2.4185, + "EncryptMilliseconds": 4.5391, + "WriteMilliseconds": 12.7969, + "EncodedBytes": 1398352 + }, + { + "CaptureMilliseconds": 0.0428, + "EncodeMilliseconds": 2.8889, + "EncryptMilliseconds": 4.6601, + "WriteMilliseconds": 11.9297, + "EncodedBytes": 1398352 + } + ] + }, + { + "scenario": "save/phases", + "snapshotBytes": 1048576, + "background": true, + "encrypted": false, + "elapsedMilliseconds": 153.1968, + "processAllocatedBytes": 52708192, + "samples": [ + { + "CaptureMilliseconds": 0.0359, + "EncodeMilliseconds": 1.4343, + "EncryptMilliseconds": 0, + "WriteMilliseconds": 11.7829, + "EncodedBytes": 1048743 + }, + { + "CaptureMilliseconds": 0.0491, + "EncodeMilliseconds": 1.2503, + "EncryptMilliseconds": 0, + "WriteMilliseconds": 12.6851, + "EncodedBytes": 1048743 + }, + { + "CaptureMilliseconds": 0.046, + "EncodeMilliseconds": 1.1868, + "EncryptMilliseconds": 0, + "WriteMilliseconds": 13.0668, + "EncodedBytes": 1048743 + }, + { + "CaptureMilliseconds": 0.0426, + "EncodeMilliseconds": 1.3036, + "EncryptMilliseconds": 0, + "WriteMilliseconds": 13.0289, + "EncodedBytes": 1048743 + }, + { + "CaptureMilliseconds": 0.0434, + "EncodeMilliseconds": 1.3727, + "EncryptMilliseconds": 0, + "WriteMilliseconds": 12.6408, + "EncodedBytes": 1048743 + }, + { + "CaptureMilliseconds": 0.0428, + "EncodeMilliseconds": 1.1718, + "EncryptMilliseconds": 0, + "WriteMilliseconds": 12.3451, + "EncodedBytes": 1048743 + }, + { + "CaptureMilliseconds": 0.0447, + "EncodeMilliseconds": 1.4653, + "EncryptMilliseconds": 0, + "WriteMilliseconds": 15.6111, + "EncodedBytes": 1048743 + }, + { + "CaptureMilliseconds": 0.0649, + "EncodeMilliseconds": 3.3023, + "EncryptMilliseconds": 0, + "WriteMilliseconds": 13.3589, + "EncodedBytes": 1048743 + }, + { + "CaptureMilliseconds": 0.042, + "EncodeMilliseconds": 1.9241, + "EncryptMilliseconds": 0, + "WriteMilliseconds": 14.8025, + "EncodedBytes": 1048743 + }, + { + "CaptureMilliseconds": 0.075, + "EncodeMilliseconds": 1.4431, + "EncryptMilliseconds": 0, + "WriteMilliseconds": 16.8658, + "EncodedBytes": 1048743 + } + ] + }, + { + "scenario": "save/phases", + "snapshotBytes": 1048576, + "background": true, + "encrypted": true, + "elapsedMilliseconds": 213.8634, + "processAllocatedBytes": 171728784, + "samples": [ + { + "CaptureMilliseconds": 0.0391, + "EncodeMilliseconds": 3.1345, + "EncryptMilliseconds": 5.5571, + "WriteMilliseconds": 14.472, + "EncodedBytes": 1398352 + }, + { + "CaptureMilliseconds": 0.0741, + "EncodeMilliseconds": 3.0989, + "EncryptMilliseconds": 4.8873, + "WriteMilliseconds": 15.4148, + "EncodedBytes": 1398352 + }, + { + "CaptureMilliseconds": 0.0426, + "EncodeMilliseconds": 3.2167, + "EncryptMilliseconds": 5.1552, + "WriteMilliseconds": 13.415, + "EncodedBytes": 1398352 + }, + { + "CaptureMilliseconds": 0.0443, + "EncodeMilliseconds": 2.6532999999999998, + "EncryptMilliseconds": 4.5273, + "WriteMilliseconds": 11.9668, + "EncodedBytes": 1398352 + }, + { + "CaptureMilliseconds": 0.0456, + "EncodeMilliseconds": 3.6863, + "EncryptMilliseconds": 4.9786, + "WriteMilliseconds": 13.3875, + "EncodedBytes": 1398352 + }, + { + "CaptureMilliseconds": 0.041, + "EncodeMilliseconds": 2.5318, + "EncryptMilliseconds": 4.6582, + "WriteMilliseconds": 12.8399, + "EncodedBytes": 1398352 + }, + { + "CaptureMilliseconds": 0.0475, + "EncodeMilliseconds": 2.3045999999999998, + "EncryptMilliseconds": 4.4072, + "WriteMilliseconds": 12.0248, + "EncodedBytes": 1398352 + }, + { + "CaptureMilliseconds": 0.0341, + "EncodeMilliseconds": 2.3112, + "EncryptMilliseconds": 4.2283, + "WriteMilliseconds": 13.1466, + "EncodedBytes": 1398352 + }, + { + "CaptureMilliseconds": 0.0619, + "EncodeMilliseconds": 2.5002, + "EncryptMilliseconds": 4.5946, + "WriteMilliseconds": 11.5307, + "EncodedBytes": 1398352 + }, + { + "CaptureMilliseconds": 0.0448, + "EncodeMilliseconds": 2.6482, + "EncryptMilliseconds": 4.8698, + "WriteMilliseconds": 14.5292, + "EncodedBytes": 1398352 + } + ] + }, + { + "scenario": "save/phases", + "snapshotBytes": 10485760, + "background": false, + "encrypted": false, + "elapsedMilliseconds": 329.6658, + "processAllocatedBytes": 524541704, + "samples": [ + { + "CaptureMilliseconds": 0.0482, + "EncodeMilliseconds": 17.0994, + "EncryptMilliseconds": 0, + "WriteMilliseconds": 19.4098, + "EncodedBytes": 10485927 + }, + { + "CaptureMilliseconds": 0.0453, + "EncodeMilliseconds": 12.6894, + "EncryptMilliseconds": 0, + "WriteMilliseconds": 18.8073, + "EncodedBytes": 10485927 + }, + { + "CaptureMilliseconds": 0.0441, + "EncodeMilliseconds": 12.5156, + "EncryptMilliseconds": 0, + "WriteMilliseconds": 22.0309, + "EncodedBytes": 10485927 + }, + { + "CaptureMilliseconds": 0.0482, + "EncodeMilliseconds": 12.6112, + "EncryptMilliseconds": 0, + "WriteMilliseconds": 19.757, + "EncodedBytes": 10485927 + }, + { + "CaptureMilliseconds": 0.0441, + "EncodeMilliseconds": 12.2356, + "EncryptMilliseconds": 0, + "WriteMilliseconds": 20.4103, + "EncodedBytes": 10485927 + }, + { + "CaptureMilliseconds": 0.0623, + "EncodeMilliseconds": 9.7949, + "EncryptMilliseconds": 0, + "WriteMilliseconds": 21.8181, + "EncodedBytes": 10485927 + }, + { + "CaptureMilliseconds": 0.036, + "EncodeMilliseconds": 11.454, + "EncryptMilliseconds": 0, + "WriteMilliseconds": 20.2002, + "EncodedBytes": 10485927 + }, + { + "CaptureMilliseconds": 0.0433, + "EncodeMilliseconds": 10.1147, + "EncryptMilliseconds": 0, + "WriteMilliseconds": 22.1706, + "EncodedBytes": 10485927 + }, + { + "CaptureMilliseconds": 0.0427, + "EncodeMilliseconds": 11.7147, + "EncryptMilliseconds": 0, + "WriteMilliseconds": 20.3321, + "EncodedBytes": 10485927 + }, + { + "CaptureMilliseconds": 0.0447, + "EncodeMilliseconds": 11.6348, + "EncryptMilliseconds": 0, + "WriteMilliseconds": 21.8113, + "EncodedBytes": 10485927 + } + ] + }, + { + "scenario": "save/phases", + "snapshotBytes": 10485760, + "background": false, + "encrypted": true, + "elapsedMilliseconds": 1078.4039, + "processAllocatedBytes": 1713112800, + "samples": [ + { + "CaptureMilliseconds": 0.044, + "EncodeMilliseconds": 28.8296, + "EncryptMilliseconds": 16.6704, + "WriteMilliseconds": 23.0414, + "EncodedBytes": 13981264 + }, + { + "CaptureMilliseconds": 0.0443, + "EncodeMilliseconds": 30.487099999999998, + "EncryptMilliseconds": 18.2286, + "WriteMilliseconds": 24.8151, + "EncodedBytes": 13981264 + }, + { + "CaptureMilliseconds": 0.04, + "EncodeMilliseconds": 34.6267, + "EncryptMilliseconds": 18.161, + "WriteMilliseconds": 24.9066, + "EncodedBytes": 13981264 + }, + { + "CaptureMilliseconds": 0.0379, + "EncodeMilliseconds": 27.775100000000002, + "EncryptMilliseconds": 16.2261, + "WriteMilliseconds": 25.6336, + "EncodedBytes": 13981264 + }, + { + "CaptureMilliseconds": 0.0348, + "EncodeMilliseconds": 30.782600000000002, + "EncryptMilliseconds": 17.8416, + "WriteMilliseconds": 22.8159, + "EncodedBytes": 13981264 + }, + { + "CaptureMilliseconds": 0.0409, + "EncodeMilliseconds": 30.037100000000002, + "EncryptMilliseconds": 19.0598, + "WriteMilliseconds": 25.7173, + "EncodedBytes": 13981264 + }, + { + "CaptureMilliseconds": 0.0375, + "EncodeMilliseconds": 26.2581, + "EncryptMilliseconds": 17.3837, + "WriteMilliseconds": 340.534, + "EncodedBytes": 13981264 + }, + { + "CaptureMilliseconds": 0.0364, + "EncodeMilliseconds": 25.908, + "EncryptMilliseconds": 18.5842, + "WriteMilliseconds": 26.5283, + "EncodedBytes": 13981264 + }, + { + "CaptureMilliseconds": 0.043, + "EncodeMilliseconds": 26.1494, + "EncryptMilliseconds": 17.55, + "WriteMilliseconds": 29.1655, + "EncodedBytes": 13981264 + }, + { + "CaptureMilliseconds": 0.05, + "EncodeMilliseconds": 24.607, + "EncryptMilliseconds": 18.0822, + "WriteMilliseconds": 25.4077, + "EncodedBytes": 13981264 + } + ] + }, + { + "scenario": "save/phases", + "snapshotBytes": 10485760, + "background": true, + "encrypted": false, + "elapsedMilliseconds": 345.5812, + "processAllocatedBytes": 524531368, + "samples": [ + { + "CaptureMilliseconds": 0.0534, + "EncodeMilliseconds": 11.6665, + "EncryptMilliseconds": 0, + "WriteMilliseconds": 20.4745, + "EncodedBytes": 10485927 + }, + { + "CaptureMilliseconds": 0.0295, + "EncodeMilliseconds": 23.0007, + "EncryptMilliseconds": 0, + "WriteMilliseconds": 21.5709, + "EncodedBytes": 10485927 + }, + { + "CaptureMilliseconds": 0.0275, + "EncodeMilliseconds": 12.2096, + "EncryptMilliseconds": 0, + "WriteMilliseconds": 20.5298, + "EncodedBytes": 10485927 + }, + { + "CaptureMilliseconds": 0.021, + "EncodeMilliseconds": 11.4892, + "EncryptMilliseconds": 0, + "WriteMilliseconds": 20.8939, + "EncodedBytes": 10485927 + }, + { + "CaptureMilliseconds": 0.0251, + "EncodeMilliseconds": 14.517, + "EncryptMilliseconds": 0, + "WriteMilliseconds": 21.1929, + "EncodedBytes": 10485927 + }, + { + "CaptureMilliseconds": 0.0361, + "EncodeMilliseconds": 11.4196, + "EncryptMilliseconds": 0, + "WriteMilliseconds": 21.2529, + "EncodedBytes": 10485927 + }, + { + "CaptureMilliseconds": 0.0258, + "EncodeMilliseconds": 17.5047, + "EncryptMilliseconds": 0, + "WriteMilliseconds": 19.9584, + "EncodedBytes": 10485927 + }, + { + "CaptureMilliseconds": 0.0252, + "EncodeMilliseconds": 15.2969, + "EncryptMilliseconds": 0, + "WriteMilliseconds": 20.1471, + "EncodedBytes": 10485927 + }, + { + "CaptureMilliseconds": 0.0213, + "EncodeMilliseconds": 11.3737, + "EncryptMilliseconds": 0, + "WriteMilliseconds": 21.4653, + "EncodedBytes": 10485927 + }, + { + "CaptureMilliseconds": 0.0196, + "EncodeMilliseconds": 9.4295, + "EncryptMilliseconds": 0, + "WriteMilliseconds": 19.4995, + "EncodedBytes": 10485927 + } + ] + }, + { + "scenario": "save/phases", + "snapshotBytes": 10485760, + "background": true, + "encrypted": true, + "elapsedMilliseconds": 783.8575, + "processAllocatedBytes": 1713149792, + "samples": [ + { + "CaptureMilliseconds": 0.0222, + "EncodeMilliseconds": 49.585, + "EncryptMilliseconds": 17.4144, + "WriteMilliseconds": 22.7226, + "EncodedBytes": 13981264 + }, + { + "CaptureMilliseconds": 0.024, + "EncodeMilliseconds": 31.7983, + "EncryptMilliseconds": 17.8468, + "WriteMilliseconds": 24.0622, + "EncodedBytes": 13981264 + }, + { + "CaptureMilliseconds": 0.0224, + "EncodeMilliseconds": 46.501400000000004, + "EncryptMilliseconds": 18.2564, + "WriteMilliseconds": 25.3939, + "EncodedBytes": 13981264 + }, + { + "CaptureMilliseconds": 0.0231, + "EncodeMilliseconds": 22.025399999999998, + "EncryptMilliseconds": 17.7059, + "WriteMilliseconds": 24.7711, + "EncodedBytes": 13981264 + }, + { + "CaptureMilliseconds": 0.0213, + "EncodeMilliseconds": 22.9529, + "EncryptMilliseconds": 16.7538, + "WriteMilliseconds": 25.2457, + "EncodedBytes": 13981264 + }, + { + "CaptureMilliseconds": 0.0218, + "EncodeMilliseconds": 29.400799999999997, + "EncryptMilliseconds": 19.6378, + "WriteMilliseconds": 23.7753, + "EncodedBytes": 13981264 + }, + { + "CaptureMilliseconds": 0.0244, + "EncodeMilliseconds": 22.6126, + "EncryptMilliseconds": 17.0682, + "WriteMilliseconds": 27.0039, + "EncodedBytes": 13981264 + }, + { + "CaptureMilliseconds": 0.0299, + "EncodeMilliseconds": 43.3661, + "EncryptMilliseconds": 18.6658, + "WriteMilliseconds": 26.3244, + "EncodedBytes": 13981264 + }, + { + "CaptureMilliseconds": 0.0227, + "EncodeMilliseconds": 26.6425, + "EncryptMilliseconds": 17.8585, + "WriteMilliseconds": 24.3419, + "EncodedBytes": 13981264 + }, + { + "CaptureMilliseconds": 0.0191, + "EncodeMilliseconds": 24.6259, + "EncryptMilliseconds": 17.997, + "WriteMilliseconds": 26.3308, + "EncodedBytes": 13981264 + } + ] + }, + { + "scenario": "experiment/pipelines-buffer-roundtrip", + "parameters": null, + "iterations": 10000, + "throughputPerSecond": 2022490.0897985601, + "allocatedBytesPerOperation": 0.412, + "p50Microseconds": 0.5, + "p95Microseconds": 0.6, + "p99Microseconds": 0.6 + } + ] +} \ No newline at end of file diff --git a/Docs/Performance/Data/2026-09-28/benchmark-net8.json b/Docs/Performance/Data/2026-09-28/benchmark-net8.json new file mode 100644 index 0000000..0187b54 --- /dev/null +++ b/Docs/Performance/Data/2026-09-28/benchmark-net8.json @@ -0,0 +1,1506 @@ +{ + "runtime": ".NET 8.0.31", + "host": "standalone .NET; Loopback sessions are not OS connections; diagnostic microbenchmarks are not Player data", + "atUtc": "2026-09-28T21:21:55.3766475Z", + "rows": [ + { + "scenario": "network/legacy-json-envelope", + "parameters": { + "payloadBytes": 64 + }, + "iterations": 1000, + "throughputPerSecond": 310732.7077248151, + "allocatedBytesPerOperation": 3504, + "p50Microseconds": 2.4, + "p95Microseconds": 5, + "p99Microseconds": 6.9 + }, + { + "scenario": "network/v2-envelope", + "parameters": { + "payloadBytes": 64 + }, + "iterations": 1000, + "throughputPerSecond": 4060089.321965083, + "allocatedBytesPerOperation": 32, + "p50Microseconds": 0.2, + "p95Microseconds": 0.3, + "p99Microseconds": 0.3 + }, + { + "scenario": "network/v2-json-message", + "parameters": { + "payloadBytes": 64 + }, + "iterations": 1000, + "throughputPerSecond": 662954.1235746486, + "allocatedBytesPerOperation": 3136, + "p50Microseconds": 1.4, + "p95Microseconds": 1.8, + "p99Microseconds": 2.3 + }, + { + "scenario": "network/v2-generated-messagepack", + "parameters": { + "payloadBytes": 64 + }, + "iterations": 1000, + "throughputPerSecond": 2257336.3431151244, + "allocatedBytesPerOperation": 112, + "p50Microseconds": 0.4, + "p95Microseconds": 0.4, + "p99Microseconds": 0.7 + }, + { + "scenario": "network/loopback-session-send", + "parameters": { + "payloadBytes": 64, + "connections": 1 + }, + "iterations": 1000, + "throughputPerSecond": 182711.80866419396, + "allocatedBytesPerOperation": 6976, + "p50Microseconds": 4.6, + "p95Microseconds": 8.7, + "p99Microseconds": 11.3 + }, + { + "scenario": "network/loopback-broadcast-shared-payload", + "parameters": { + "payloadBytes": 64, + "connections": 1 + }, + "iterations": 25, + "throughputPerSecond": 149521.53110047846, + "allocatedBytesPerOperation": 4040, + "p50Microseconds": 3.9, + "p95Microseconds": 6.2, + "p99Microseconds": 71.8 + }, + { + "scenario": "network/loopback-session-send", + "parameters": { + "payloadBytes": 64, + "connections": 32 + }, + "iterations": 1000, + "throughputPerSecond": 190465.3067443765, + "allocatedBytesPerOperation": 6976, + "p50Microseconds": 4.4, + "p95Microseconds": 8.6, + "p99Microseconds": 11.1 + }, + { + "scenario": "network/loopback-broadcast-shared-payload", + "parameters": { + "payloadBytes": 64, + "connections": 32 + }, + "iterations": 25, + "throughputPerSecond": 9878.68969059944, + "allocatedBytesPerOperation": 126800, + "p50Microseconds": 100.5, + "p95Microseconds": 106.1, + "p99Microseconds": 114.7 + }, + { + "scenario": "network/loopback-session-send", + "parameters": { + "payloadBytes": 64, + "connections": 256 + }, + "iterations": 1000, + "throughputPerSecond": 205389.41833716724, + "allocatedBytesPerOperation": 6976, + "p50Microseconds": 4.6, + "p95Microseconds": 5.1, + "p99Microseconds": 6.9 + }, + { + "scenario": "network/loopback-broadcast-shared-payload", + "parameters": { + "payloadBytes": 64, + "connections": 256 + }, + "iterations": 25, + "throughputPerSecond": 1052.0557168707655, + "allocatedBytesPerOperation": 1013840, + "p50Microseconds": 914.8, + "p95Microseconds": 1255.6, + "p99Microseconds": 1332.9 + }, + { + "scenario": "network/legacy-json-envelope", + "parameters": { + "payloadBytes": 1024 + }, + "iterations": 1000, + "throughputPerSecond": 194571.4563673509, + "allocatedBytesPerOperation": 10560, + "p50Microseconds": 4.7, + "p95Microseconds": 5.3, + "p99Microseconds": 7.5 + }, + { + "scenario": "network/v2-envelope", + "parameters": { + "payloadBytes": 1024 + }, + "iterations": 1000, + "throughputPerSecond": 3787878.7878787876, + "allocatedBytesPerOperation": 32, + "p50Microseconds": 0.2, + "p95Microseconds": 0.3, + "p99Microseconds": 0.3 + }, + { + "scenario": "network/v2-json-message", + "parameters": { + "payloadBytes": 1024 + }, + "iterations": 1000, + "throughputPerSecond": 203479.4994404314, + "allocatedBytesPerOperation": 6240, + "p50Microseconds": 3.9, + "p95Microseconds": 4.2, + "p99Microseconds": 6 + }, + { + "scenario": "network/v2-generated-messagepack", + "parameters": { + "payloadBytes": 1024 + }, + "iterations": 1000, + "throughputPerSecond": 1923446.8166955183, + "allocatedBytesPerOperation": 112, + "p50Microseconds": 0.4, + "p95Microseconds": 0.5, + "p99Microseconds": 0.9 + }, + { + "scenario": "network/loopback-session-send", + "parameters": { + "payloadBytes": 1024, + "connections": 1 + }, + "iterations": 1000, + "throughputPerSecond": 90451.07953363424, + "allocatedBytesPerOperation": 19000, + "p50Microseconds": 9.9, + "p95Microseconds": 11.8, + "p99Microseconds": 21.9 + }, + { + "scenario": "network/loopback-broadcast-shared-payload", + "parameters": { + "payloadBytes": 1024, + "connections": 1 + }, + "iterations": 25, + "throughputPerSecond": 100280.78620136382, + "allocatedBytesPerOperation": 12960, + "p50Microseconds": 7.2, + "p95Microseconds": 24.1, + "p99Microseconds": 29.1 + }, + { + "scenario": "network/loopback-session-send", + "parameters": { + "payloadBytes": 1024, + "connections": 32 + }, + "iterations": 1000, + "throughputPerSecond": 96683.74746205163, + "allocatedBytesPerOperation": 19000, + "p50Microseconds": 9.8, + "p95Microseconds": 10.7, + "p99Microseconds": 15.3 + }, + { + "scenario": "network/loopback-broadcast-shared-payload", + "parameters": { + "payloadBytes": 1024, + "connections": 32 + }, + "iterations": 25, + "throughputPerSecond": 4694.659355517163, + "allocatedBytesPerOperation": 412240, + "p50Microseconds": 198.6, + "p95Microseconds": 338.5, + "p99Microseconds": 345.2 + }, + { + "scenario": "network/loopback-session-send", + "parameters": { + "payloadBytes": 1024, + "connections": 256 + }, + "iterations": 1000, + "throughputPerSecond": 92279.86637875349, + "allocatedBytesPerOperation": 19000, + "p50Microseconds": 10, + "p95Microseconds": 11.8, + "p99Microseconds": 17.8 + }, + { + "scenario": "network/loopback-broadcast-shared-payload", + "parameters": { + "payloadBytes": 1024, + "connections": 256 + }, + "iterations": 25, + "throughputPerSecond": 583.7234551758759, + "allocatedBytesPerOperation": 3297360, + "p50Microseconds": 1692.5, + "p95Microseconds": 1882.3, + "p99Microseconds": 2170 + }, + { + "scenario": "network/legacy-json-envelope", + "parameters": { + "payloadBytes": 32768 + }, + "iterations": 1000, + "throughputPerSecond": 9403.1801555286, + "allocatedBytesPerOperation": 230328, + "p50Microseconds": 80.3, + "p95Microseconds": 126.9, + "p99Microseconds": 1072.6 + }, + { + "scenario": "network/v2-envelope", + "parameters": { + "payloadBytes": 32768 + }, + "iterations": 1000, + "throughputPerSecond": 1041666.6666666666, + "allocatedBytesPerOperation": 32, + "p50Microseconds": 0.9, + "p95Microseconds": 1, + "p99Microseconds": 1 + }, + { + "scenario": "network/v2-json-message", + "parameters": { + "payloadBytes": 32768 + }, + "iterations": 1000, + "throughputPerSecond": 45008.55162480871, + "allocatedBytesPerOperation": 6240, + "p50Microseconds": 19.6, + "p95Microseconds": 30.2, + "p99Microseconds": 37.2 + }, + { + "scenario": "network/v2-generated-messagepack", + "parameters": { + "payloadBytes": 32768 + }, + "iterations": 1000, + "throughputPerSecond": 504693.650953871, + "allocatedBytesPerOperation": 112, + "p50Microseconds": 1.8, + "p95Microseconds": 2.1, + "p99Microseconds": 2.8 + }, + { + "scenario": "network/loopback-session-send", + "parameters": { + "payloadBytes": 32768, + "connections": 1 + }, + "iterations": 1000, + "throughputPerSecond": 8491.061883708115, + "allocatedBytesPerOperation": 431792, + "p50Microseconds": 72.1, + "p95Microseconds": 185.3, + "p99Microseconds": 1144.4 + }, + { + "scenario": "network/loopback-broadcast-shared-payload", + "parameters": { + "payloadBytes": 32768, + "connections": 1 + }, + "iterations": 25, + "throughputPerSecond": 12688.423082779273, + "allocatedBytesPerOperation": 425752, + "p50Microseconds": 54.3, + "p95Microseconds": 79.4, + "p99Microseconds": 607.1 + }, + { + "scenario": "network/loopback-session-send", + "parameters": { + "payloadBytes": 32768, + "connections": 32 + }, + "iterations": 1000, + "throughputPerSecond": 10182.687598199294, + "allocatedBytesPerOperation": 431792, + "p50Microseconds": 69.9, + "p95Microseconds": 172.4, + "p99Microseconds": 677.8 + }, + { + "scenario": "network/loopback-broadcast-shared-payload", + "parameters": { + "payloadBytes": 32768, + "connections": 32 + }, + "iterations": 25, + "throughputPerSecond": 412.3174052370907, + "allocatedBytesPerOperation": 13621584, + "p50Microseconds": 2391.2, + "p95Microseconds": 2838.9, + "p99Microseconds": 2991.4 + }, + { + "scenario": "network/loopback-session-send", + "parameters": { + "payloadBytes": 32768, + "connections": 256 + }, + "iterations": 1000, + "throughputPerSecond": 8617.510091104317, + "allocatedBytesPerOperation": 431792, + "p50Microseconds": 71.4, + "p95Microseconds": 149.7, + "p99Microseconds": 1115.1 + }, + { + "scenario": "network/loopback-broadcast-shared-payload", + "parameters": { + "payloadBytes": 32768, + "connections": 256 + }, + "iterations": 25, + "throughputPerSecond": 51.40130230339516, + "allocatedBytesPerOperation": 108972112, + "p50Microseconds": 19440.8, + "p95Microseconds": 20351.4, + "p99Microseconds": 20604 + }, + { + "scenario": "context/current-diagnostics", + "parameters": { + "components": 10 + }, + "iterations": 100, + "throughputPerSecond": 128501.67052171678, + "allocatedBytesPerOperation": 7312, + "p50Microseconds": 5.7, + "p95Microseconds": 11.7, + "p99Microseconds": 60.8 + }, + { + "scenario": "context/baseline-diagnostics", + "parameters": { + "components": 10 + }, + "iterations": 100, + "throughputPerSecond": 110180.69634200087, + "allocatedBytesPerOperation": 10456, + "p50Microseconds": 8.8, + "p95Microseconds": 9.4, + "p99Microseconds": 22.2 + }, + { + "scenario": "context/current-diagnostics", + "parameters": { + "components": 100 + }, + "iterations": 100, + "throughputPerSecond": 14973.197975623634, + "allocatedBytesPerOperation": 73456, + "p50Microseconds": 61.1, + "p95Microseconds": 116.9, + "p99Microseconds": 120.1 + }, + { + "scenario": "context/baseline-diagnostics", + "parameters": { + "components": 100 + }, + "iterations": 100, + "throughputPerSecond": 2871.4426414975146, + "allocatedBytesPerOperation": 108016, + "p50Microseconds": 301.9, + "p95Microseconds": 538.2, + "p99Microseconds": 716.3 + }, + { + "scenario": "context/current-diagnostics", + "parameters": { + "components": 1000 + }, + "iterations": 100, + "throughputPerSecond": 2962.2695724556324, + "allocatedBytesPerOperation": 735328, + "p50Microseconds": 305.2, + "p95Microseconds": 548.3, + "p99Microseconds": 853.1 + }, + { + "scenario": "context/baseline-diagnostics", + "parameters": { + "components": 1000 + }, + "iterations": 100, + "throughputPerSecond": 157.03788853136595, + "allocatedBytesPerOperation": 1083616, + "p50Microseconds": 6206.3, + "p95Microseconds": 7426.4, + "p99Microseconds": 8613.4 + }, + { + "scenario": "context/10000-retirements", + "currentRetainedBytes": -2301952, + "baselineRetainedBytes": 8502328, + "currentFibers": 0, + "currentHistory": 64, + "baselineFibers": 10000 + }, + { + "scenario": "event/sync", + "parameters": null, + "iterations": 100000, + "throughputPerSecond": 14906017.559288686, + "allocatedBytesPerOperation": 0, + "p50Microseconds": 0, + "p95Microseconds": 0.1, + "p99Microseconds": 0.1 + }, + { + "scenario": "event/async-sync-subscriber", + "parameters": null, + "iterations": 10000, + "throughputPerSecond": 8157272.208173586, + "allocatedBytesPerOperation": 0, + "p50Microseconds": 0.1, + "p95Microseconds": 0.1, + "p99Microseconds": 0.2 + }, + { + "scenario": "event/binding-diagnostics", + "parameters": null, + "iterations": 1000, + "throughputPerSecond": 6963788.300835655, + "allocatedBytesPerOperation": 120, + "p50Microseconds": 0.1, + "p95Microseconds": 0.2, + "p99Microseconds": 0.3 + }, + { + "scenario": "event/register-withdraw", + "parameters": null, + "iterations": 10000, + "throughputPerSecond": 3009781.7908201655, + "allocatedBytesPerOperation": 224, + "p50Microseconds": 0.3, + "p95Microseconds": 0.3, + "p99Microseconds": 2.1 + }, + { + "scenario": "event/async-subscriber", + "parameters": null, + "iterations": 10000, + "throughputPerSecond": 7404116.688879017, + "allocatedBytesPerOperation": 0, + "p50Microseconds": 0.1, + "p95Microseconds": 0.2, + "p99Microseconds": 0.2 + }, + { + "scenario": "queue/burst-state", + "before": { + "PendingCount": 32, + "PendingBytes": 32768, + "Rejected": 0, + "Replaced": 9968, + "Completed": 0, + "OldestWaitMilliseconds": 0.0043, + "LastWaitMilliseconds": 0 + }, + "after": { + "PendingCount": 28, + "PendingBytes": 28672, + "Rejected": 0, + "Replaced": 9968, + "Completed": 4, + "OldestWaitMilliseconds": 0.0632, + "LastWaitMilliseconds": 0.0608 + } + }, + { + "scenario": "save/phases", + "snapshotBytes": 102400, + "background": false, + "encrypted": false, + "elapsedMilliseconds": 96.956, + "processAllocatedBytes": 5885672, + "samples": [ + { + "CaptureMilliseconds": 3.1233, + "EncodeMilliseconds": 6.865, + "EncryptMilliseconds": 0, + "WriteMilliseconds": 24.2279, + "EncodedBytes": 102567 + }, + { + "CaptureMilliseconds": 0.0675, + "EncodeMilliseconds": 0.1752, + "EncryptMilliseconds": 0, + "WriteMilliseconds": 8.236, + "EncodedBytes": 102567 + }, + { + "CaptureMilliseconds": 0.0365, + "EncodeMilliseconds": 0.1769, + "EncryptMilliseconds": 0, + "WriteMilliseconds": 7.3803, + "EncodedBytes": 102567 + }, + { + "CaptureMilliseconds": 0.0388, + "EncodeMilliseconds": 0.1625, + "EncryptMilliseconds": 0, + "WriteMilliseconds": 6.5416, + "EncodedBytes": 102567 + }, + { + "CaptureMilliseconds": 0.0349, + "EncodeMilliseconds": 0.1616, + "EncryptMilliseconds": 0, + "WriteMilliseconds": 6.6758, + "EncodedBytes": 102567 + }, + { + "CaptureMilliseconds": 0.0371, + "EncodeMilliseconds": 0.1676, + "EncryptMilliseconds": 0, + "WriteMilliseconds": 6.9452, + "EncodedBytes": 102567 + }, + { + "CaptureMilliseconds": 0.0359, + "EncodeMilliseconds": 0.1814, + "EncryptMilliseconds": 0, + "WriteMilliseconds": 4.9443, + "EncodedBytes": 102567 + }, + { + "CaptureMilliseconds": 0.0331, + "EncodeMilliseconds": 0.1497, + "EncryptMilliseconds": 0, + "WriteMilliseconds": 6.3617, + "EncodedBytes": 102567 + }, + { + "CaptureMilliseconds": 0.0361, + "EncodeMilliseconds": 0.571, + "EncryptMilliseconds": 0, + "WriteMilliseconds": 5.682, + "EncodedBytes": 102567 + }, + { + "CaptureMilliseconds": 0.036, + "EncodeMilliseconds": 0.1052, + "EncryptMilliseconds": 0, + "WriteMilliseconds": 5.9919, + "EncodedBytes": 102567 + } + ] + }, + { + "scenario": "save/phases", + "snapshotBytes": 102400, + "background": false, + "encrypted": true, + "elapsedMilliseconds": 92.316, + "processAllocatedBytes": 17204952, + "samples": [ + { + "CaptureMilliseconds": 0.0364, + "EncodeMilliseconds": 0.2097, + "EncryptMilliseconds": 5.6153, + "WriteMilliseconds": 9.0115, + "EncodedBytes": 136784 + }, + { + "CaptureMilliseconds": 0.042, + "EncodeMilliseconds": 0.1896, + "EncryptMilliseconds": 1.6223, + "WriteMilliseconds": 7.4026, + "EncodedBytes": 136784 + }, + { + "CaptureMilliseconds": 0.0361, + "EncodeMilliseconds": 0.5924, + "EncryptMilliseconds": 1.8733, + "WriteMilliseconds": 7.4121, + "EncodedBytes": 136784 + }, + { + "CaptureMilliseconds": 0.0375, + "EncodeMilliseconds": 0.1963, + "EncryptMilliseconds": 1.5104, + "WriteMilliseconds": 5.7556, + "EncodedBytes": 136784 + }, + { + "CaptureMilliseconds": 0.0354, + "EncodeMilliseconds": 0.41550000000000004, + "EncryptMilliseconds": 1.4954, + "WriteMilliseconds": 5.7419, + "EncodedBytes": 136784 + }, + { + "CaptureMilliseconds": 0.036, + "EncodeMilliseconds": 0.1918, + "EncryptMilliseconds": 1.4973, + "WriteMilliseconds": 8.5429, + "EncodedBytes": 136784 + }, + { + "CaptureMilliseconds": 0.036, + "EncodeMilliseconds": 0.48219999999999996, + "EncryptMilliseconds": 1.6586, + "WriteMilliseconds": 5.5383, + "EncodedBytes": 136784 + }, + { + "CaptureMilliseconds": 0.0359, + "EncodeMilliseconds": 0.1886, + "EncryptMilliseconds": 1.5087, + "WriteMilliseconds": 6.6723, + "EncodedBytes": 136784 + }, + { + "CaptureMilliseconds": 0.041, + "EncodeMilliseconds": 0.3231, + "EncryptMilliseconds": 1.4916, + "WriteMilliseconds": 5.3427, + "EncodedBytes": 136784 + }, + { + "CaptureMilliseconds": 0.0337, + "EncodeMilliseconds": 0.3246, + "EncryptMilliseconds": 1.5014, + "WriteMilliseconds": 7.1619, + "EncodedBytes": 136784 + } + ] + }, + { + "scenario": "save/phases", + "snapshotBytes": 102400, + "background": true, + "encrypted": false, + "elapsedMilliseconds": 63.4923, + "processAllocatedBytes": 5355208, + "samples": [ + { + "CaptureMilliseconds": 0.0399, + "EncodeMilliseconds": 0.1555, + "EncryptMilliseconds": 0, + "WriteMilliseconds": 7.2302, + "EncodedBytes": 102567 + }, + { + "CaptureMilliseconds": 0.0338, + "EncodeMilliseconds": 0.1878, + "EncryptMilliseconds": 0, + "WriteMilliseconds": 5.6046, + "EncodedBytes": 102567 + }, + { + "CaptureMilliseconds": 0.0345, + "EncodeMilliseconds": 0.2903, + "EncryptMilliseconds": 0, + "WriteMilliseconds": 4.9223, + "EncodedBytes": 102567 + }, + { + "CaptureMilliseconds": 0.0324, + "EncodeMilliseconds": 0.1057, + "EncryptMilliseconds": 0, + "WriteMilliseconds": 5.4714, + "EncodedBytes": 102567 + }, + { + "CaptureMilliseconds": 0.0328, + "EncodeMilliseconds": 0.1391, + "EncryptMilliseconds": 0, + "WriteMilliseconds": 6.3166, + "EncodedBytes": 102567 + }, + { + "CaptureMilliseconds": 0.0351, + "EncodeMilliseconds": 0.106, + "EncryptMilliseconds": 0, + "WriteMilliseconds": 5.8802, + "EncodedBytes": 102567 + }, + { + "CaptureMilliseconds": 0.0309, + "EncodeMilliseconds": 0.0977, + "EncryptMilliseconds": 0, + "WriteMilliseconds": 5.6458, + "EncodedBytes": 102567 + }, + { + "CaptureMilliseconds": 0.0311, + "EncodeMilliseconds": 0.1024, + "EncryptMilliseconds": 0, + "WriteMilliseconds": 6.5616, + "EncodedBytes": 102567 + }, + { + "CaptureMilliseconds": 0.0396, + "EncodeMilliseconds": 0.1061, + "EncryptMilliseconds": 0, + "WriteMilliseconds": 6.5712, + "EncodedBytes": 102567 + }, + { + "CaptureMilliseconds": 0.0364, + "EncodeMilliseconds": 1.0791, + "EncryptMilliseconds": 0, + "WriteMilliseconds": 6.1145, + "EncodedBytes": 102567 + } + ] + }, + { + "scenario": "save/phases", + "snapshotBytes": 102400, + "background": true, + "encrypted": true, + "elapsedMilliseconds": 81.9726, + "processAllocatedBytes": 17172720, + "samples": [ + { + "CaptureMilliseconds": 0.0369, + "EncodeMilliseconds": 0.1844, + "EncryptMilliseconds": 1.5083, + "WriteMilliseconds": 5.6385, + "EncodedBytes": 136784 + }, + { + "CaptureMilliseconds": 0.0349, + "EncodeMilliseconds": 0.6774, + "EncryptMilliseconds": 1.5047, + "WriteMilliseconds": 6.5934, + "EncodedBytes": 136784 + }, + { + "CaptureMilliseconds": 0.0417, + "EncodeMilliseconds": 0.1982, + "EncryptMilliseconds": 1.5526, + "WriteMilliseconds": 6.0437, + "EncodedBytes": 136784 + }, + { + "CaptureMilliseconds": 0.0364, + "EncodeMilliseconds": 0.3853, + "EncryptMilliseconds": 1.5582, + "WriteMilliseconds": 7.6947, + "EncodedBytes": 136784 + }, + { + "CaptureMilliseconds": 0.0369, + "EncodeMilliseconds": 0.1956, + "EncryptMilliseconds": 1.5091, + "WriteMilliseconds": 5.8586, + "EncodedBytes": 136784 + }, + { + "CaptureMilliseconds": 0.0324, + "EncodeMilliseconds": 0.2944, + "EncryptMilliseconds": 1.4879, + "WriteMilliseconds": 6.499, + "EncodedBytes": 136784 + }, + { + "CaptureMilliseconds": 0.0375, + "EncodeMilliseconds": 0.2364, + "EncryptMilliseconds": 1.5345, + "WriteMilliseconds": 5.7638, + "EncodedBytes": 136784 + }, + { + "CaptureMilliseconds": 0.0338, + "EncodeMilliseconds": 0.43610000000000004, + "EncryptMilliseconds": 1.5023, + "WriteMilliseconds": 6.1518, + "EncodedBytes": 136784 + }, + { + "CaptureMilliseconds": 0.0341, + "EncodeMilliseconds": 0.19929999999999998, + "EncryptMilliseconds": 1.5207, + "WriteMilliseconds": 6.1029, + "EncodedBytes": 136784 + }, + { + "CaptureMilliseconds": 0.0309, + "EncodeMilliseconds": 0.3601, + "EncryptMilliseconds": 1.5821, + "WriteMilliseconds": 6.1789, + "EncodedBytes": 136784 + } + ] + }, + { + "scenario": "save/phases", + "snapshotBytes": 1048576, + "background": false, + "encrypted": false, + "elapsedMilliseconds": 95.1374, + "processAllocatedBytes": 52734776, + "samples": [ + { + "CaptureMilliseconds": 0.0403, + "EncodeMilliseconds": 1.331, + "EncryptMilliseconds": 0, + "WriteMilliseconds": 8.1342, + "EncodedBytes": 1048743 + }, + { + "CaptureMilliseconds": 0.038, + "EncodeMilliseconds": 0.9023, + "EncryptMilliseconds": 0, + "WriteMilliseconds": 9.2404, + "EncodedBytes": 1048743 + }, + { + "CaptureMilliseconds": 0.0424, + "EncodeMilliseconds": 1.0221, + "EncryptMilliseconds": 0, + "WriteMilliseconds": 9.0316, + "EncodedBytes": 1048743 + }, + { + "CaptureMilliseconds": 0.0421, + "EncodeMilliseconds": 0.8347, + "EncryptMilliseconds": 0, + "WriteMilliseconds": 9.312, + "EncodedBytes": 1048743 + }, + { + "CaptureMilliseconds": 0.0394, + "EncodeMilliseconds": 1.064, + "EncryptMilliseconds": 0, + "WriteMilliseconds": 8.8081, + "EncodedBytes": 1048743 + }, + { + "CaptureMilliseconds": 0.0386, + "EncodeMilliseconds": 0.7535, + "EncryptMilliseconds": 0, + "WriteMilliseconds": 8.9509, + "EncodedBytes": 1048743 + }, + { + "CaptureMilliseconds": 0.0424, + "EncodeMilliseconds": 0.9133, + "EncryptMilliseconds": 0, + "WriteMilliseconds": 9.3709, + "EncodedBytes": 1048743 + }, + { + "CaptureMilliseconds": 0.0393, + "EncodeMilliseconds": 0.8236, + "EncryptMilliseconds": 0, + "WriteMilliseconds": 8.3293, + "EncodedBytes": 1048743 + }, + { + "CaptureMilliseconds": 0.0384, + "EncodeMilliseconds": 0.924, + "EncryptMilliseconds": 0, + "WriteMilliseconds": 7.0398, + "EncodedBytes": 1048743 + }, + { + "CaptureMilliseconds": 0.0374, + "EncodeMilliseconds": 0.788, + "EncryptMilliseconds": 0, + "WriteMilliseconds": 6.9633, + "EncodedBytes": 1048743 + } + ] + }, + { + "scenario": "save/phases", + "snapshotBytes": 1048576, + "background": false, + "encrypted": true, + "elapsedMilliseconds": 152.84, + "processAllocatedBytes": 171737408, + "samples": [ + { + "CaptureMilliseconds": 0.0379, + "EncodeMilliseconds": 2.2121, + "EncryptMilliseconds": 2.4214, + "WriteMilliseconds": 9.5574, + "EncodedBytes": 1398352 + }, + { + "CaptureMilliseconds": 0.0382, + "EncodeMilliseconds": 1.7246000000000001, + "EncryptMilliseconds": 2.4707, + "WriteMilliseconds": 8.8144, + "EncodedBytes": 1398352 + }, + { + "CaptureMilliseconds": 0.0557, + "EncodeMilliseconds": 1.9542000000000002, + "EncryptMilliseconds": 2.4186, + "WriteMilliseconds": 8.9148, + "EncodedBytes": 1398352 + }, + { + "CaptureMilliseconds": 0.0466, + "EncodeMilliseconds": 4.7713, + "EncryptMilliseconds": 2.5148, + "WriteMilliseconds": 8.9371, + "EncodedBytes": 1398352 + }, + { + "CaptureMilliseconds": 0.0428, + "EncodeMilliseconds": 1.5151, + "EncryptMilliseconds": 2.5436, + "WriteMilliseconds": 9.973, + "EncodedBytes": 1398352 + }, + { + "CaptureMilliseconds": 0.0418, + "EncodeMilliseconds": 2.0574000000000003, + "EncryptMilliseconds": 2.6034, + "WriteMilliseconds": 9.0099, + "EncodedBytes": 1398352 + }, + { + "CaptureMilliseconds": 0.0421, + "EncodeMilliseconds": 2.1558, + "EncryptMilliseconds": 2.8087, + "WriteMilliseconds": 9.676, + "EncodedBytes": 1398352 + }, + { + "CaptureMilliseconds": 0.043, + "EncodeMilliseconds": 1.5476, + "EncryptMilliseconds": 2.2712, + "WriteMilliseconds": 8.3233, + "EncodedBytes": 1398352 + }, + { + "CaptureMilliseconds": 0.046, + "EncodeMilliseconds": 1.4611, + "EncryptMilliseconds": 2.2757, + "WriteMilliseconds": 7.194, + "EncodedBytes": 1398352 + }, + { + "CaptureMilliseconds": 0.0354, + "EncodeMilliseconds": 14.5275, + "EncryptMilliseconds": 2.611, + "WriteMilliseconds": 9.349, + "EncodedBytes": 1398352 + } + ] + }, + { + "scenario": "save/phases", + "snapshotBytes": 1048576, + "background": true, + "encrypted": false, + "elapsedMilliseconds": 82.9009, + "processAllocatedBytes": 52673560, + "samples": [ + { + "CaptureMilliseconds": 0.0338, + "EncodeMilliseconds": 0.6007, + "EncryptMilliseconds": 0, + "WriteMilliseconds": 7.064, + "EncodedBytes": 1048743 + }, + { + "CaptureMilliseconds": 0.0366, + "EncodeMilliseconds": 0.7295, + "EncryptMilliseconds": 0, + "WriteMilliseconds": 8.7276, + "EncodedBytes": 1048743 + }, + { + "CaptureMilliseconds": 0.0434, + "EncodeMilliseconds": 0.6533, + "EncryptMilliseconds": 0, + "WriteMilliseconds": 7.289, + "EncodedBytes": 1048743 + }, + { + "CaptureMilliseconds": 0.0358, + "EncodeMilliseconds": 0.8061, + "EncryptMilliseconds": 0, + "WriteMilliseconds": 6.6027, + "EncodedBytes": 1048743 + }, + { + "CaptureMilliseconds": 0.0362, + "EncodeMilliseconds": 0.831, + "EncryptMilliseconds": 0, + "WriteMilliseconds": 7.7798, + "EncodedBytes": 1048743 + }, + { + "CaptureMilliseconds": 0.0395, + "EncodeMilliseconds": 0.8456, + "EncryptMilliseconds": 0, + "WriteMilliseconds": 7.057, + "EncodedBytes": 1048743 + }, + { + "CaptureMilliseconds": 0.0806, + "EncodeMilliseconds": 0.8413, + "EncryptMilliseconds": 0, + "WriteMilliseconds": 8.985, + "EncodedBytes": 1048743 + }, + { + "CaptureMilliseconds": 0.0619, + "EncodeMilliseconds": 1.1302, + "EncryptMilliseconds": 0, + "WriteMilliseconds": 6.3442, + "EncodedBytes": 1048743 + }, + { + "CaptureMilliseconds": 0.0321, + "EncodeMilliseconds": 0.8411, + "EncryptMilliseconds": 0, + "WriteMilliseconds": 7.4379, + "EncodedBytes": 1048743 + }, + { + "CaptureMilliseconds": 0.0397, + "EncodeMilliseconds": 0.8376, + "EncryptMilliseconds": 0, + "WriteMilliseconds": 6.5889, + "EncodedBytes": 1048743 + } + ] + }, + { + "scenario": "save/phases", + "snapshotBytes": 1048576, + "background": true, + "encrypted": true, + "elapsedMilliseconds": 146.4099, + "processAllocatedBytes": 171717440, + "samples": [ + { + "CaptureMilliseconds": 0.0452, + "EncodeMilliseconds": 2.2481999999999998, + "EncryptMilliseconds": 2.4795, + "WriteMilliseconds": 9.3868, + "EncodedBytes": 1398352 + }, + { + "CaptureMilliseconds": 0.0358, + "EncodeMilliseconds": 1.9322, + "EncryptMilliseconds": 2.447, + "WriteMilliseconds": 9.2873, + "EncodedBytes": 1398352 + }, + { + "CaptureMilliseconds": 0.0388, + "EncodeMilliseconds": 1.399, + "EncryptMilliseconds": 2.3029, + "WriteMilliseconds": 7.415, + "EncodedBytes": 1398352 + }, + { + "CaptureMilliseconds": 0.0376, + "EncodeMilliseconds": 3.9274, + "EncryptMilliseconds": 2.3132, + "WriteMilliseconds": 10.0161, + "EncodedBytes": 1398352 + }, + { + "CaptureMilliseconds": 0.0404, + "EncodeMilliseconds": 1.4548999999999999, + "EncryptMilliseconds": 2.3109, + "WriteMilliseconds": 7.8053, + "EncodedBytes": 1398352 + }, + { + "CaptureMilliseconds": 0.0362, + "EncodeMilliseconds": 2.2487, + "EncryptMilliseconds": 2.6949, + "WriteMilliseconds": 8.8699, + "EncodedBytes": 1398352 + }, + { + "CaptureMilliseconds": 0.0372, + "EncodeMilliseconds": 2.3531, + "EncryptMilliseconds": 2.8905, + "WriteMilliseconds": 10.1422, + "EncodedBytes": 1398352 + }, + { + "CaptureMilliseconds": 0.0466, + "EncodeMilliseconds": 1.5191, + "EncryptMilliseconds": 2.4375, + "WriteMilliseconds": 9.8297, + "EncodedBytes": 1398352 + }, + { + "CaptureMilliseconds": 0.0396, + "EncodeMilliseconds": 1.6644, + "EncryptMilliseconds": 2.623, + "WriteMilliseconds": 9.1452, + "EncodedBytes": 1398352 + }, + { + "CaptureMilliseconds": 0.0379, + "EncodeMilliseconds": 9.2456, + "EncryptMilliseconds": 2.5564, + "WriteMilliseconds": 7.5192, + "EncodedBytes": 1398352 + } + ] + }, + { + "scenario": "save/phases", + "snapshotBytes": 10485760, + "background": false, + "encrypted": false, + "elapsedMilliseconds": 235.5908, + "processAllocatedBytes": 524519872, + "samples": [ + { + "CaptureMilliseconds": 0.0423, + "EncodeMilliseconds": 8.8865, + "EncryptMilliseconds": 0, + "WriteMilliseconds": 15.1822, + "EncodedBytes": 10485927 + }, + { + "CaptureMilliseconds": 0.04, + "EncodeMilliseconds": 7.9581, + "EncryptMilliseconds": 0, + "WriteMilliseconds": 16.3275, + "EncodedBytes": 10485927 + }, + { + "CaptureMilliseconds": 0.0384, + "EncodeMilliseconds": 8.9705, + "EncryptMilliseconds": 0, + "WriteMilliseconds": 14.2821, + "EncodedBytes": 10485927 + }, + { + "CaptureMilliseconds": 0.0371, + "EncodeMilliseconds": 8.6078, + "EncryptMilliseconds": 0, + "WriteMilliseconds": 16.4507, + "EncodedBytes": 10485927 + }, + { + "CaptureMilliseconds": 0.0384, + "EncodeMilliseconds": 7.9348, + "EncryptMilliseconds": 0, + "WriteMilliseconds": 17.4569, + "EncodedBytes": 10485927 + }, + { + "CaptureMilliseconds": 0.0409, + "EncodeMilliseconds": 5.6066, + "EncryptMilliseconds": 0, + "WriteMilliseconds": 16.319, + "EncodedBytes": 10485927 + }, + { + "CaptureMilliseconds": 0.047, + "EncodeMilliseconds": 7.2851, + "EncryptMilliseconds": 0, + "WriteMilliseconds": 14.3258, + "EncodedBytes": 10485927 + }, + { + "CaptureMilliseconds": 0.0483, + "EncodeMilliseconds": 6.8769, + "EncryptMilliseconds": 0, + "WriteMilliseconds": 15.8296, + "EncodedBytes": 10485927 + }, + { + "CaptureMilliseconds": 0.043, + "EncodeMilliseconds": 6.8019, + "EncryptMilliseconds": 0, + "WriteMilliseconds": 17.4719, + "EncodedBytes": 10485927 + }, + { + "CaptureMilliseconds": 0.0524, + "EncodeMilliseconds": 7.8504, + "EncryptMilliseconds": 0, + "WriteMilliseconds": 14.2884, + "EncodedBytes": 10485927 + } + ] + }, + { + "scenario": "save/phases", + "snapshotBytes": 10485760, + "background": false, + "encrypted": true, + "elapsedMilliseconds": 721.024, + "processAllocatedBytes": 1713103856, + "samples": [ + { + "CaptureMilliseconds": 0.0379, + "EncodeMilliseconds": 19.2247, + "EncryptMilliseconds": 9.8912, + "WriteMilliseconds": 19.3142, + "EncodedBytes": 13981264 + }, + { + "CaptureMilliseconds": 0.0494, + "EncodeMilliseconds": 20.3263, + "EncryptMilliseconds": 10.4417, + "WriteMilliseconds": 22.555, + "EncodedBytes": 13981264 + }, + { + "CaptureMilliseconds": 0.0819, + "EncodeMilliseconds": 15.1159, + "EncryptMilliseconds": 11.5887, + "WriteMilliseconds": 18.8192, + "EncodedBytes": 13981264 + }, + { + "CaptureMilliseconds": 0.048, + "EncodeMilliseconds": 20.0489, + "EncryptMilliseconds": 9.6736, + "WriteMilliseconds": 18.8862, + "EncodedBytes": 13981264 + }, + { + "CaptureMilliseconds": 0.0389, + "EncodeMilliseconds": 21.2298, + "EncryptMilliseconds": 10.8562, + "WriteMilliseconds": 27.2163, + "EncodedBytes": 13981264 + }, + { + "CaptureMilliseconds": 0.052, + "EncodeMilliseconds": 45.3617, + "EncryptMilliseconds": 13.5929, + "WriteMilliseconds": 37.3237, + "EncodedBytes": 13981264 + }, + { + "CaptureMilliseconds": 0.0567, + "EncodeMilliseconds": 29.7656, + "EncryptMilliseconds": 12.7905, + "WriteMilliseconds": 36.0836, + "EncodedBytes": 13981264 + }, + { + "CaptureMilliseconds": 0.0527, + "EncodeMilliseconds": 31.802799999999998, + "EncryptMilliseconds": 13.8397, + "WriteMilliseconds": 46.4428, + "EncodedBytes": 13981264 + }, + { + "CaptureMilliseconds": 0.0489, + "EncodeMilliseconds": 36.0835, + "EncryptMilliseconds": 15.8013, + "WriteMilliseconds": 40.601, + "EncodedBytes": 13981264 + }, + { + "CaptureMilliseconds": 0.058, + "EncodeMilliseconds": 21.7598, + "EncryptMilliseconds": 13.4075, + "WriteMilliseconds": 22.6962, + "EncodedBytes": 13981264 + } + ] + }, + { + "scenario": "save/phases", + "snapshotBytes": 10485760, + "background": true, + "encrypted": false, + "elapsedMilliseconds": 255.7051, + "processAllocatedBytes": 524517240, + "samples": [ + { + "CaptureMilliseconds": 0.0359, + "EncodeMilliseconds": 7.289, + "EncryptMilliseconds": 0, + "WriteMilliseconds": 18.8855, + "EncodedBytes": 10485927 + }, + { + "CaptureMilliseconds": 0.0372, + "EncodeMilliseconds": 6.6096, + "EncryptMilliseconds": 0, + "WriteMilliseconds": 17.4669, + "EncodedBytes": 10485927 + }, + { + "CaptureMilliseconds": 0.0364, + "EncodeMilliseconds": 6.1187, + "EncryptMilliseconds": 0, + "WriteMilliseconds": 16.9843, + "EncodedBytes": 10485927 + }, + { + "CaptureMilliseconds": 0.0358, + "EncodeMilliseconds": 7.2911, + "EncryptMilliseconds": 0, + "WriteMilliseconds": 16.7205, + "EncodedBytes": 10485927 + }, + { + "CaptureMilliseconds": 0.0371, + "EncodeMilliseconds": 14.123, + "EncryptMilliseconds": 0, + "WriteMilliseconds": 17.6216, + "EncodedBytes": 10485927 + }, + { + "CaptureMilliseconds": 0.033, + "EncodeMilliseconds": 8.3647, + "EncryptMilliseconds": 0, + "WriteMilliseconds": 16.7194, + "EncodedBytes": 10485927 + }, + { + "CaptureMilliseconds": 0.0358, + "EncodeMilliseconds": 7.808, + "EncryptMilliseconds": 0, + "WriteMilliseconds": 16.9196, + "EncodedBytes": 10485927 + }, + { + "CaptureMilliseconds": 0.0396, + "EncodeMilliseconds": 6.4671, + "EncryptMilliseconds": 0, + "WriteMilliseconds": 17.6914, + "EncodedBytes": 10485927 + }, + { + "CaptureMilliseconds": 0.0344, + "EncodeMilliseconds": 6.3182, + "EncryptMilliseconds": 0, + "WriteMilliseconds": 16.7139, + "EncodedBytes": 10485927 + }, + { + "CaptureMilliseconds": 0.0397, + "EncodeMilliseconds": 11.7591, + "EncryptMilliseconds": 0, + "WriteMilliseconds": 17.0448, + "EncodedBytes": 10485927 + } + ] + }, + { + "scenario": "save/phases", + "snapshotBytes": 10485760, + "background": true, + "encrypted": true, + "elapsedMilliseconds": 509.435, + "processAllocatedBytes": 1713105072, + "samples": [ + { + "CaptureMilliseconds": 0.03, + "EncodeMilliseconds": 19.5715, + "EncryptMilliseconds": 11.2673, + "WriteMilliseconds": 18.1579, + "EncodedBytes": 13981264 + }, + { + "CaptureMilliseconds": 0.031, + "EncodeMilliseconds": 14.675, + "EncryptMilliseconds": 12.8596, + "WriteMilliseconds": 18.2182, + "EncodedBytes": 13981264 + }, + { + "CaptureMilliseconds": 0.0338, + "EncodeMilliseconds": 18.4144, + "EncryptMilliseconds": 15.7941, + "WriteMilliseconds": 18.4149, + "EncodedBytes": 13981264 + }, + { + "CaptureMilliseconds": 0.031, + "EncodeMilliseconds": 14.959499999999998, + "EncryptMilliseconds": 12.15, + "WriteMilliseconds": 20.1471, + "EncodedBytes": 13981264 + }, + { + "CaptureMilliseconds": 0.0341, + "EncodeMilliseconds": 14.4194, + "EncryptMilliseconds": 11.144, + "WriteMilliseconds": 18.9044, + "EncodedBytes": 13981264 + }, + { + "CaptureMilliseconds": 0.03, + "EncodeMilliseconds": 21.2929, + "EncryptMilliseconds": 11.2883, + "WriteMilliseconds": 18.6774, + "EncodedBytes": 13981264 + }, + { + "CaptureMilliseconds": 0.0289, + "EncodeMilliseconds": 17.1947, + "EncryptMilliseconds": 10.9435, + "WriteMilliseconds": 17.7606, + "EncodedBytes": 13981264 + }, + { + "CaptureMilliseconds": 0.0309, + "EncodeMilliseconds": 14.0913, + "EncryptMilliseconds": 11.0397, + "WriteMilliseconds": 19.9277, + "EncodedBytes": 13981264 + }, + { + "CaptureMilliseconds": 0.0625, + "EncodeMilliseconds": 21.0649, + "EncryptMilliseconds": 11.9086, + "WriteMilliseconds": 19.7946, + "EncodedBytes": 13981264 + }, + { + "CaptureMilliseconds": 0.0329, + "EncodeMilliseconds": 16.3758, + "EncryptMilliseconds": 11.1712, + "WriteMilliseconds": 21.9953, + "EncodedBytes": 13981264 + } + ] + }, + { + "scenario": "experiment/pipelines-buffer-roundtrip", + "parameters": null, + "iterations": 10000, + "throughputPerSecond": 2713630.5663346993, + "allocatedBytesPerOperation": 0, + "p50Microseconds": 0.3, + "p95Microseconds": 0.4, + "p99Microseconds": 0.5 + } + ] +} \ No newline at end of file diff --git a/Docs/Performance/Data/2026-09-28/cli-tests-final.log b/Docs/Performance/Data/2026-09-28/cli-tests-final.log new file mode 100644 index 0000000..920e38a --- /dev/null +++ b/Docs/Performance/Data/2026-09-28/cli-tests-final.log @@ -0,0 +1,4 @@ +D:\UnityBuilds\ShrinkSDK\Tools\AgentSupport\Shrink.Inspect.Tests\bin\Debug\net8.0\Shrink.Inspect.Tests.dll (.NETCoreApp,Version=v8.0)的测试运行 +总共 1 个测试文件与指定模式相匹配。 + +已通过! - 失败: 0,通过: 12,已跳过: 0,总计: 12,持续时间: 4 s - Shrink.Inspect.Tests.dll (net8.0) diff --git a/Docs/Performance/Data/2026-09-28/consumer-runtime-query.json b/Docs/Performance/Data/2026-09-28/consumer-runtime-query.json new file mode 100644 index 0000000..728bd7a --- /dev/null +++ b/Docs/Performance/Data/2026-09-28/consumer-runtime-query.json @@ -0,0 +1,174 @@ +{ + "success": true, + "formatVersion": 1, + "toolVersion": "0.1.2.0", + "command": "flow", + "query": "route:command/execute", + "buildId": "793d92f10d38ad8ae6f45cd7365d657b9bc73274c194f761250fb1b98e58d56f", + "offset": 0, + "limit": 20, + "nextOffset": null, + "coverage": [ + "SeedsKey.Game: incomplete compilation: CS0117 “EndlessSnapshotResolver”未包含“Instance”的定义", + "SeedsKey.ShrinkSDK.Tests: incomplete compilation: CS0117 “Resolver”未包含“Instance”的定义", + "Installed ShrinkSDK package sources are included; other external packages are reference-only.", + "Snapshot: Default Context and named EventBus instances only; host-owned instances require CaptureHostState registration.", + "Snapshot: Endless multiplayer uses its own session/protocol; SDK Network bindings do not describe that custom channel." + ], + "coverageTruncated": false, + "semanticCoverage": { + "incompleteCount": 10, + "examples": [ + "ShrinkInspection", + "com.cammin.ldtkunity", + "com.esotericsoftware.spine.spine-unity", + "vHierarchy", + "Scripts" + ] + }, + "validation": [], + "total": 14, + "truncated": false, + "items": [ + { + "From": "route:command/execute", + "To": "type:ShrinkCommand.Integration.ShrinkNetworkCommandRequest", + "Kind": "message", + "Evidence": "static", + "Location": { + "File": "D:\\UnityBuilds\\justanyproject\\Library\\PackageCache\\com.cneicy.shrink-command-integration-network@c9fc19bad75e\\ShrinkNetworkCommandContracts.cs", + "Line": 5 + } + }, + { + "From": "route:command/execute_response", + "To": "type:ShrinkCommand.Integration.ShrinkNetworkCommandResponse", + "Kind": "message", + "Evidence": "static", + "Location": { + "File": "D:\\UnityBuilds\\justanyproject\\Library\\PackageCache\\com.cneicy.shrink-command-integration-network@c9fc19bad75e\\ShrinkNetworkCommandContracts.cs", + "Line": 13 + } + }, + { + "From": "route:command/execute", + "To": "type:ShrinkCommand.Integration.ShrinkNetworkCommandRequest", + "Kind": "message [seedskey-network]", + "Evidence": "runtime-bound", + "Location": { + "File": "D:\\UnityBuilds\\justanyproject\\Artifacts\\Inspection\\runtime.json", + "Line": 1 + } + }, + { + "From": "route:command/execute_response", + "To": "type:ShrinkCommand.Integration.ShrinkNetworkCommandResponse", + "Kind": "message [seedskey-network]", + "Evidence": "runtime-bound", + "Location": { + "File": "D:\\UnityBuilds\\justanyproject\\Artifacts\\Inspection\\runtime.json", + "Line": 1 + } + }, + { + "From": "type:ShrinkCommand.Integration.ShrinkNetworkCommandRequest", + "To": "M:ShrinkCommand.Integration.ShrinkNetworkCommandBridge.RegisterService(ShrinkNetwork.ShrinkNetworkService,ShrinkCommand.ShrinkCommandService,ShrinkCommand.Integration.ShrinkNetworkCommandBridgeOptions)", + "Kind": "registers", + "Evidence": "candidate", + "Location": { + "File": "D:\\UnityBuilds\\justanyproject\\Library\\PackageCache\\com.cneicy.shrink-command-integration-network@c9fc19bad75e\\ShrinkNetworkCommandBridge.cs", + "Line": 38 + } + }, + { + "From": "type:ShrinkCommand.Integration.ShrinkNetworkCommandRequest", + "To": "M:ShrinkCommand.Integration.ShrinkNetworkCommandBridge.(ShrinkNetwork.ShrinkNetworkContext,ShrinkCommand.Integration.ShrinkNetworkCommandRequest)", + "Kind": "handler", + "Evidence": "candidate", + "Location": { + "File": "D:\\UnityBuilds\\justanyproject\\Library\\PackageCache\\com.cneicy.shrink-command-integration-network@c9fc19bad75e\\ShrinkNetworkCommandBridge.cs", + "Line": 39 + } + }, + { + "From": "type:ShrinkCommand.Integration.ShrinkNetworkCommandRequest", + "To": "M:ShrinkCommand.Integration.ShrinkNetworkCommandBridge.HandleExecuteAsync(ShrinkCommand.ShrinkCommandService,ShrinkCommand.Integration.ShrinkNetworkCommandBridgeOptions,ShrinkNetwork.ShrinkNetworkContext,ShrinkCommand.Integration.ShrinkNetworkCommandRequest)", + "Kind": "handler-forward", + "Evidence": "candidate", + "Location": { + "File": "D:\\UnityBuilds\\justanyproject\\Library\\PackageCache\\com.cneicy.shrink-command-integration-network@c9fc19bad75e\\ShrinkNetworkCommandBridge.cs", + "Line": 39 + } + }, + { + "From": "T:ShrinkCommand.Integration.ShrinkNetworkCommandRequest", + "To": "type:ShrinkCommand.Integration.ShrinkNetworkCommandRequest", + "Kind": "declares", + "Evidence": "static", + "Location": { + "File": "D:\\UnityBuilds\\justanyproject\\Library\\PackageCache\\com.cneicy.shrink-command-integration-network@c9fc19bad75e\\ShrinkNetworkCommandContracts.cs", + "Line": 5 + } + }, + { + "From": "opcode:1501", + "To": "type:ShrinkCommand.Integration.ShrinkNetworkCommandRequest", + "Kind": "message", + "Evidence": "static", + "Location": { + "File": "D:\\UnityBuilds\\justanyproject\\Library\\PackageCache\\com.cneicy.shrink-command-integration-network@c9fc19bad75e\\ShrinkNetworkCommandContracts.cs", + "Line": 5 + } + }, + { + "From": "T:ShrinkCommand.Integration.ShrinkNetworkCommandResponse", + "To": "type:ShrinkCommand.Integration.ShrinkNetworkCommandResponse", + "Kind": "declares", + "Evidence": "static", + "Location": { + "File": "D:\\UnityBuilds\\justanyproject\\Library\\PackageCache\\com.cneicy.shrink-command-integration-network@c9fc19bad75e\\ShrinkNetworkCommandContracts.cs", + "Line": 13 + } + }, + { + "From": "opcode:1502", + "To": "type:ShrinkCommand.Integration.ShrinkNetworkCommandResponse", + "Kind": "message", + "Evidence": "static", + "Location": { + "File": "D:\\UnityBuilds\\justanyproject\\Library\\PackageCache\\com.cneicy.shrink-command-integration-network@c9fc19bad75e\\ShrinkNetworkCommandContracts.cs", + "Line": 13 + } + }, + { + "From": "opcode:1501", + "To": "type:ShrinkCommand.Integration.ShrinkNetworkCommandRequest", + "Kind": "message [seedskey-network]", + "Evidence": "runtime-bound", + "Location": { + "File": "D:\\UnityBuilds\\justanyproject\\Artifacts\\Inspection\\runtime.json", + "Line": 1 + } + }, + { + "From": "opcode:1502", + "To": "type:ShrinkCommand.Integration.ShrinkNetworkCommandResponse", + "Kind": "message [seedskey-network]", + "Evidence": "runtime-bound", + "Location": { + "File": "D:\\UnityBuilds\\justanyproject\\Artifacts\\Inspection\\runtime.json", + "Line": 1 + } + }, + { + "From": "type:ShrinkCommand.Integration.ShrinkNetworkCommandRequest", + "To": "M:ShrinkCommand.Integration.ShrinkNetworkCommandBridge.<>c__DisplayClass3_0.b__0(ShrinkNetwork.ShrinkNetworkContext,ShrinkCommand.Integration.ShrinkNetworkCommandRequest)", + "Kind": "handler [seedskey-network]", + "Evidence": "runtime-bound", + "Location": { + "File": "D:\\UnityBuilds\\justanyproject\\Artifacts\\Inspection\\runtime.json", + "Line": 1 + } + } + ] +} diff --git a/Docs/Performance/Data/2026-09-28/godot-run-final.log b/Docs/Performance/Data/2026-09-28/godot-run-final.log new file mode 100644 index 0000000..e6c27d0 --- /dev/null +++ b/Docs/Performance/Data/2026-09-28/godot-run-final.log @@ -0,0 +1,6 @@ +Godot Engine v4.6.3.stable.mono.official.7d41c59c4 - https://godotengine.org + +[ShrinkDataSaver] Runtime initialized. Root: C:/Users/im/AppData/Roaming/Godot/app_userdata/ShrinkSDK Godot Sample/ +[ShrinkDataSaver] 槽位 0 已保存。(1 个模块) +[ShrinkDataSaver] 槽位 0 已加载。(v1, 1 个模块) +SHRINK_GODOT_SMOKE_PASS CodeGen=0.1.0 DataSaver=42 diff --git a/Docs/Performance/Data/2026-09-28/justany-compilation.json b/Docs/Performance/Data/2026-09-28/justany-compilation.json new file mode 100644 index 0000000..7f6d436 --- /dev/null +++ b/Docs/Performance/Data/2026-09-28/justany-compilation.json @@ -0,0 +1 @@ +{"success":true,"message":"No compilation errors detected."} diff --git a/Docs/Performance/Data/2026-09-28/justany-installed.json b/Docs/Performance/Data/2026-09-28/justany-installed.json new file mode 100644 index 0000000..509bc31 --- /dev/null +++ b/Docs/Performance/Data/2026-09-28/justany-installed.json @@ -0,0 +1 @@ +{"success":true,"message":"Command executed.","data":{"compiler":"Roslyn","logs":[],"created":[],"modified":[],"destroyed":[],"returnValue":{"root":"D:/UnityBuilds/justanyproject/Assets","packages":[{"name":"com.cneicy.shrink-app-starter-basic","version":"0.3.7","resolvedPath":"D:\\UnityBuilds\\justanyproject\\Library\\PackageCache\\com.cneicy.shrink-app-starter-basic@825861e850fb"},{"name":"com.cneicy.shrink-context-eventbus-adapter","version":"0.2.4","resolvedPath":"D:\\UnityBuilds\\justanyproject\\Library\\PackageCache\\com.cneicy.shrink-context-eventbus-adapter@d40b7bbb3332"},{"name":"com.cneicy.shrink-mod-framework","version":"0.3.6","resolvedPath":"D:\\UnityBuilds\\justanyproject\\Library\\PackageCache\\com.cneicy.shrink-mod-framework@bc48d592447c"},{"name":"com.cneicy.shrink-tutorial","version":"0.2.0","resolvedPath":"D:\\UnityBuilds\\justanyproject\\Library\\PackageCache\\com.cneicy.shrink-tutorial@6a0eb37e7cc8"},{"name":"com.cneicy.shrink-app-core","version":"0.2.3","resolvedPath":"D:\\UnityBuilds\\justanyproject\\Library\\PackageCache\\com.cneicy.shrink-app-core@5daf24a2b6e1"},{"name":"com.cneicy.shrink-command","version":"0.4.2","resolvedPath":"D:\\UnityBuilds\\justanyproject\\Library\\PackageCache\\com.cneicy.shrink-command@1de427034e91"},{"name":"com.cneicy.shrink-command-integration-app","version":"0.2.5","resolvedPath":"D:\\UnityBuilds\\justanyproject\\Library\\PackageCache\\com.cneicy.shrink-command-integration-app@9d3134f4b8d1"},{"name":"com.cneicy.shrink-command-integration-eventbus","version":"0.2.5","resolvedPath":"D:\\UnityBuilds\\justanyproject\\Library\\PackageCache\\com.cneicy.shrink-command-integration-eventbus@d9cc18d67d43"},{"name":"com.cneicy.shrink-command-integration-network","version":"0.2.5","resolvedPath":"D:\\UnityBuilds\\justanyproject\\Library\\PackageCache\\com.cneicy.shrink-command-integration-network@c9fc19bad75e"},{"name":"com.cneicy.shrink-context-app-adapter","version":"0.2.4","resolvedPath":"D:\\UnityBuilds\\justanyproject\\Library\\PackageCache\\com.cneicy.shrink-context-app-adapter@ec1aa00eb681"},{"name":"com.cneicy.shrink-context-core","version":"0.3.0","resolvedPath":"D:\\UnityBuilds\\justanyproject\\Library\\PackageCache\\com.cneicy.shrink-context-core@5fbc0d91d89d"},{"name":"com.cneicy.shrink-datasaver","version":"2.4.0","resolvedPath":"D:\\UnityBuilds\\justanyproject\\Library\\PackageCache\\com.cneicy.shrink-datasaver@d34bd599c3bf"},{"name":"com.cneicy.shrink-datasaver-integration-app","version":"0.2.5","resolvedPath":"D:\\UnityBuilds\\justanyproject\\Library\\PackageCache\\com.cneicy.shrink-datasaver-integration-app@a8252bd56933"},{"name":"com.cneicy.shrink-datasaver-integration-eventbus","version":"2.2.5","resolvedPath":"D:\\UnityBuilds\\justanyproject\\Library\\PackageCache\\com.cneicy.shrink-datasaver-integration-eventbus@580e3f7efbf8"},{"name":"com.cneicy.shrink-eventbus","version":"2.2.2","resolvedPath":"D:\\UnityBuilds\\justanyproject\\Library\\PackageCache\\com.cneicy.shrink-eventbus@d07f074f8df0"},{"name":"com.cneicy.shrink-inspection","version":"0.1.0","resolvedPath":"D:\\UnityBuilds\\justanyproject\\Library\\PackageCache\\com.cneicy.shrink-inspection@149d402d6a43"},{"name":"com.cneicy.shrink-network","version":"0.4.2","resolvedPath":"D:\\UnityBuilds\\justanyproject\\Library\\PackageCache\\com.cneicy.shrink-network@7ed4deb7650f"},{"name":"com.cneicy.shrink-network-integration-app","version":"0.2.5","resolvedPath":"D:\\UnityBuilds\\justanyproject\\Library\\PackageCache\\com.cneicy.shrink-network-integration-app@5988bc288b65"},{"name":"com.cneicy.shrink-network-integration-eventbus","version":"0.2.5","resolvedPath":"D:\\UnityBuilds\\justanyproject\\Library\\PackageCache\\com.cneicy.shrink-network-integration-eventbus@d41d8cd98f00"},{"name":"com.cneicy.shrink-runtime-abstractions","version":"0.1.0","resolvedPath":"D:\\UnityBuilds\\justanyproject\\Library\\PackageCache\\com.cneicy.shrink-runtime-abstractions@380b2a2dc79a"},{"name":"com.cneicy.shrink-shared-codegen","version":"0.2.1","resolvedPath":"D:\\UnityBuilds\\justanyproject\\Library\\PackageCache\\com.cneicy.shrink-shared-codegen@76b281b9f724"},{"name":"com.cneicy.shrink-network-messagepack","version":"0.1.0","resolvedPath":"D:\\UnityBuilds\\justanyproject\\Library\\PackageCache\\com.cneicy.shrink-network-messagepack@b1d0daaa690c"}]}}} diff --git a/Docs/Performance/Data/2026-09-28/justany-messagepack-results.json b/Docs/Performance/Data/2026-09-28/justany-messagepack-results.json new file mode 100644 index 0000000..ae3dbf6 --- /dev/null +++ b/Docs/Performance/Data/2026-09-28/justany-messagepack-results.json @@ -0,0 +1 @@ +{"success":true,"message":"Test job finished.","data":{"jobId":"7b421e25-0cda-4fda-b68e-ad7638a45f84","status":"finished","mode":"PlayMode","startedAt":"2026-09-29 07:04:55","lastActivityAt":"2026-09-29 07:05:03","testsCompleted":1,"hasFilters":true,"finishedAt":"2026-09-29 07:05:03","resultState":"Passed","passCount":1,"failCount":0,"skipCount":0,"inconclusiveCount":0,"totalTests":1,"durationSeconds":0.08,"failures":[]}} diff --git a/Docs/Performance/Data/2026-09-28/justany-play-errors.json b/Docs/Performance/Data/2026-09-28/justany-play-errors.json new file mode 100644 index 0000000..05d3fe9 --- /dev/null +++ b/Docs/Performance/Data/2026-09-28/justany-play-errors.json @@ -0,0 +1 @@ +{"success":true,"message":"No error entries found in cached logs from the last 120 second(s)"} diff --git a/Docs/Performance/Data/2026-09-28/justany-runtime.json b/Docs/Performance/Data/2026-09-28/justany-runtime.json new file mode 100644 index 0000000..e341292 --- /dev/null +++ b/Docs/Performance/Data/2026-09-28/justany-runtime.json @@ -0,0 +1 @@ +{"success":true,"message":"Command executed.","data":{"compiler":"Roslyn","logs":[],"created":[],"modified":[],"destroyed":[],"returnValue":{"root":"D:/UnityBuilds/justanyproject/Assets","playing":true,"scene":"Assets/Scenes/MainMenu.unity","fibers":[{"Name":"seedskey.app-services","Status":"Active","Retired":false,"ErrorType":null},{"Name":"seedskey.game-content","Status":"Active","Retired":false,"ErrorType":null},{"Name":"shrink.command","Status":"Active","Retired":false,"ErrorType":null},{"Name":"shrink.datasaver","Status":"Active","Retired":false,"ErrorType":null},{"Name":"shrink.integration.command-eventbus","Status":"Active","Retired":false,"ErrorType":null},{"Name":"shrink.integration.command-network","Status":"Active","Retired":false,"ErrorType":null},{"Name":"shrink.integration.datasaver-eventbus","Status":"Active","Retired":false,"ErrorType":null},{"Name":"shrink.integration.network-eventbus","Status":"Active","Retired":false,"ErrorType":null},{"Name":"shrink.network","Status":"Active","Retired":false,"ErrorType":null}]}}} diff --git a/Docs/Performance/Data/2026-09-28/justany-tests.json b/Docs/Performance/Data/2026-09-28/justany-tests.json new file mode 100644 index 0000000..cd3c842 --- /dev/null +++ b/Docs/Performance/Data/2026-09-28/justany-tests.json @@ -0,0 +1 @@ +{"success":true,"message":"Test job finished.","data":{"jobId":"27800c26-885f-4d33-81fe-2f8f79add11b","status":"finished","mode":"EditMode","startedAt":"2026-09-29 06:02:01","lastActivityAt":"2026-09-29 06:02:05","testsCompleted":83,"hasFilters":true,"finishedAt":"2026-09-29 06:02:05","resultState":"Failed(Child)","passCount":80,"failCount":3,"skipCount":0,"inconclusiveCount":0,"totalTests":83,"durationSeconds":1.5,"failures":[{"test":"Tests.EditMode.ModSpellApiEditModeTests.RegisteredCodeModSpellBecomesCatalogItemAndUsesCustomFactory","message":"System.NotSupportedException : 旧法术工厂 API 已停用,请注册组件并调用 SpellModApi.Register(ownerId, definition)。","stackTrace":" at Tests.EditMode.ModSpellApiEditModeTests.RegisteredCodeModSpellBecomesCatalogItemAndUsesCustomFactory () [0x00000] in D:\\UnityBuilds\\justanyproject\\Assets\\Tests\\EditMode\\ModSpellApiEditModeTests.cs:34 \r\n at (wrapper managed-to-native) System.Reflection.RuntimeMethodInfo.InternalInvoke(System.Reflection.RuntimeMethodInfo,object,object[],System.Exception&)\r\n at System.Reflection.RuntimeMethodInfo.Invoke (System.Object obj, System.Reflection.BindingFlags invokeAttr, System.Reflection.Binder b... (+127 chars)"},{"test":"Tests.EditMode.ModSpellApiEditModeTests.RegistrationRejectsSpellOutsideOwnerNamespace","message":" Expected: \r\n But was: c.b__3_0 () [0x00000] in D:\\UnityBuilds\\justanyproject\\Assets\\Tests\\EditMode\\ModSpellApiEditModeTests.cs:80 \r\n at NUnit.Framework.Assert.Throws (NUnit.Framework.Constraints.IResolveConstraint expression, NUnit.Framework.TestDelegate code, System.S... (+90 chars)","stackTrace":"at Tests.EditMode.ModSpellApiEditModeTests.RegistrationRejectsSpellOutsideOwnerNamespace () [0x00000] in D:\\UnityBuilds\\justanyproject\\Assets\\Tests\\EditMode\\ModSpellApiEditModeTests.cs:79\r\n"},{"test":"Tests.EditMode.RunModuleCatalogEditModeTests.DraftSpellOptionsShouldExposeCompleteSpellTooltips","message":" Spell_AccompliceMark\r\n Expected: String containing \"◆\"\r\n But was: \"目标标记\r\n品质:传说\r\n模块法力:5\r\n命中标记:命中标记时长 4\n当前及后续攻击\r\n命中标记时长:4\"\r\n","stackTrace":"at Tests.EditMode.RunModuleCatalogEditModeTests.DraftSpellOptionsShouldExposeCompleteSpellTooltips () [0x00020] in D:\\UnityBuilds\\justanyproject\\Assets\\Tests\\EditMode\\RunModuleCatalogEditModeTests.cs:167\r\n"}]}} diff --git a/Docs/Performance/Data/2026-09-28/nuget-consumer-final.log b/Docs/Performance/Data/2026-09-28/nuget-consumer-final.log new file mode 100644 index 0000000..16f4b4e --- /dev/null +++ b/Docs/Performance/Data/2026-09-28/nuget-consumer-final.log @@ -0,0 +1 @@ +PASS NuGet buildTransitive CodeGen 0.1.0 diff --git a/Docs/Performance/Data/2026-09-28/package-validation.json b/Docs/Performance/Data/2026-09-28/package-validation.json new file mode 100644 index 0000000..16eb49b --- /dev/null +++ b/Docs/Performance/Data/2026-09-28/package-validation.json @@ -0,0 +1,109 @@ +{ + "catalogPackages": 24, + "installedPackages": 22, + "comparedFiles": 1073, + "identicalFiles": 1053, + "hostImportMetadataDifferences": [ + [ + "com.cneicy.shrink-app-starter-basic", + "NuGet.Config.meta", + "consumer NuGetForUnity importer labels/platform flags" + ], + [ + "com.cneicy.shrink-context-eventbus-adapter", + "NuGet.Config.meta", + "consumer NuGetForUnity importer labels/platform flags" + ], + [ + "com.cneicy.shrink-mod-framework", + "NuGet.Config.meta", + "consumer NuGetForUnity importer labels/platform flags" + ], + [ + "com.cneicy.shrink-tutorial", + "NuGet.Config.meta", + "consumer NuGetForUnity importer labels/platform flags" + ], + [ + "com.cneicy.shrink-app-core", + "NuGet.Config.meta", + "consumer NuGetForUnity importer labels/platform flags" + ], + [ + "com.cneicy.shrink-command", + "NuGet.Config.meta", + "consumer NuGetForUnity importer labels/platform flags" + ], + [ + "com.cneicy.shrink-command-integration-app", + "NuGet.Config.meta", + "consumer NuGetForUnity importer labels/platform flags" + ], + [ + "com.cneicy.shrink-command-integration-eventbus", + "NuGet.Config.meta", + "consumer NuGetForUnity importer labels/platform flags" + ], + [ + "com.cneicy.shrink-command-integration-network", + "NuGet.Config.meta", + "consumer NuGetForUnity importer labels/platform flags" + ], + [ + "com.cneicy.shrink-context-app-adapter", + "NuGet.Config.meta", + "consumer NuGetForUnity importer labels/platform flags" + ], + [ + "com.cneicy.shrink-context-core", + "NuGet.Config.meta", + "consumer NuGetForUnity importer labels/platform flags" + ], + [ + "com.cneicy.shrink-datasaver", + "NuGet.Config.meta", + "consumer NuGetForUnity importer labels/platform flags" + ], + [ + "com.cneicy.shrink-datasaver-integration-app", + "NuGet.Config.meta", + "consumer NuGetForUnity importer labels/platform flags" + ], + [ + "com.cneicy.shrink-datasaver-integration-eventbus", + "NuGet.Config.meta", + "consumer NuGetForUnity importer labels/platform flags" + ], + [ + "com.cneicy.shrink-eventbus", + "NuGet.Config.meta", + "consumer NuGetForUnity importer labels/platform flags" + ], + [ + "com.cneicy.shrink-network", + "NuGet.Config.meta", + "consumer NuGetForUnity importer labels/platform flags" + ], + [ + "com.cneicy.shrink-network-integration-app", + "NuGet.Config.meta", + "consumer NuGetForUnity importer labels/platform flags" + ], + [ + "com.cneicy.shrink-network-integration-eventbus", + "NuGet.Config.meta", + "consumer NuGetForUnity importer labels/platform flags" + ], + [ + "com.cneicy.shrink-runtime-abstractions", + "NuGet.Config.meta", + "consumer NuGetForUnity importer labels/platform flags" + ], + [ + "com.cneicy.shrink-shared-codegen", + "NuGet.Config.meta", + "consumer NuGetForUnity importer labels/platform flags" + ] + ], + "errors": [] +} \ No newline at end of file diff --git a/Docs/Performance/Data/2026-09-28/player-output.txt b/Docs/Performance/Data/2026-09-28/player-output.txt new file mode 100644 index 0000000..63c4c52 --- /dev/null +++ b/Docs/Performance/Data/2026-09-28/player-output.txt @@ -0,0 +1,47 @@ +PLAYER Context 10000 retirements ms=123.6386 + +PLAYER EventBus 10000 posts allocationEvents=10000 ms=0.8038 +PLAYER EventBus isolated 10000 posts allocationEvents=0 + +[ShrinkDataSaver] 槽位 0 已保存。(2 个模块) +PLAYER Save 102400 background=False captureMs=11.8073 encodeMs=7.7169 encryptMs=0 writeMs=17.803 bytes=103171 +[ShrinkDataSaver] 槽位 0 已保存。(2 个模块) +PLAYER Save 102400 background=True captureMs=0.1186 encodeMs=0.2584 encryptMs=0 writeMs=29.4953 bytes=103171 +[ShrinkDataSaver] 槽位 0 已保存。(2 个模块) +PLAYER Save 1048576 background=False captureMs=1.3102 encodeMs=7.8701 encryptMs=0 writeMs=13.9663 bytes=1049347 +[ShrinkDataSaver] 槽位 0 已保存。(2 个模块) +PLAYER Save 1048576 background=True captureMs=0.1341 encodeMs=7.7226 encryptMs=0 writeMs=23.1749 bytes=1049347 +[ShrinkDataSaver] 槽位 0 已保存。(2 个模块) +PLAYER Save 10485760 background=False captureMs=0.1424 encodeMs=28.173 encryptMs=0 writeMs=32.9821 bytes=10486531 +[ShrinkDataSaver] 槽位 0 已保存。(2 个模块) +PLAYER Save 10485760 background=True captureMs=0.162 encodeMs=25.6417 encryptMs=0 writeMs=24.231 bytes=10486531 + +PLAYER allocation calibration managedCounterDelta=0 profilerEvents=10; use profiler events, not uncalibrated bytes. + +PLAYER PERF context-diagnostic/components=10 n=100 ops=71994.24 allocBytes=unsupported allocationSamples=10 allocEventsPerOp=135.00 p50us=13.00 p95us=20.20 p99us=25.60 +PLAYER PERF context-diagnostic/components=100 n=100 ops=6276.68 allocBytes=unsupported allocationSamples=10 allocEventsPerOp=1224.00 p50us=118.80 p95us=190.80 p99us=1980.20 +PLAYER PERF context-diagnostic/components=1000 n=100 ops=391.93 allocBytes=unsupported allocationSamples=10 allocEventsPerOp=12033.00 p50us=1841.90 p95us=5669.60 p99us=7090.60 +PLAYER PERF event-async-completed n=10000 ops=1531534.29 allocBytes=unsupported allocationSamples=10 allocEventsPerOp=3.00 p50us=0.30 p95us=0.60 p99us=1.10 +PLAYER PERF event-bindings n=1000 ops=3377237.42 allocBytes=unsupported allocationSamples=10 allocEventsPerOp=2.00 p50us=0.20 p95us=0.60 p99us=1.00 +PLAYER PERF event-register-withdraw n=10000 ops=725100.06 allocBytes=unsupported allocationSamples=10 allocEventsPerOp=10.00 p50us=0.80 p95us=1.70 p99us=2.40 + +PLAYER PERF loopback-rpc/bytes=64/sessions=1 n=128 ops=42139.92 allocBytes=unsupported allocationSamples=10 allocEventsPerOp=100.00 p50us=22.40 p95us=32.20 p99us=35.80 +PLAYER PERF loopback-broadcast/bytes=64/sessions=1 n=20 ops=135777.33 allocBytes=unsupported allocationSamples=10 allocEventsPerOp=37.00 p50us=7.20 p95us=9.20 p99us=9.20 +PLAYER PERF loopback-rpc/bytes=64/sessions=32 n=128 ops=22296.16 allocBytes=unsupported allocationSamples=10 allocEventsPerOp=100.00 p50us=24.60 p95us=46.10 p99us=96.20 +PLAYER PERF loopback-broadcast/bytes=64/sessions=32 n=20 ops=3283.21 allocBytes=unsupported allocationSamples=10 allocEventsPerOp=1060.00 p50us=215.80 p95us=1999.10 p99us=1999.10 +PLAYER PERF loopback-rpc/bytes=64/sessions=256 n=128 ops=16764.02 allocBytes=unsupported allocationSamples=10 allocEventsPerOp=100.00 p50us=33.90 p95us=42.50 p99us=115.00 +PLAYER PERF loopback-broadcast/bytes=64/sessions=256 n=20 ops=331.02 allocBytes=unsupported allocationSamples=10 allocEventsPerOp=8452.00 p50us=3052.70 p95us=5858.40 p99us=5858.40 +PLAYER PERF loopback-rpc/bytes=1024/sessions=1 n=128 ops=13102.67 allocBytes=unsupported allocationSamples=10 allocEventsPerOp=102.00 p50us=27.70 p95us=302.40 p99us=1201.80 +PLAYER PERF loopback-broadcast/bytes=1024/sessions=1 n=20 ops=117164.62 allocBytes=unsupported allocationSamples=10 allocEventsPerOp=38.00 p50us=8.40 p95us=10.40 p99us=10.40 +PLAYER PERF loopback-rpc/bytes=1024/sessions=32 n=128 ops=15497.49 allocBytes=unsupported allocationSamples=10 allocEventsPerOp=102.00 p50us=27.60 p95us=62.90 p99us=1200.70 +PLAYER PERF loopback-broadcast/bytes=1024/sessions=32 n=20 ops=1648.17 allocBytes=unsupported allocationSamples=10 allocEventsPerOp=1092.00 p50us=361.60 p95us=1577.50 p99us=1577.50 +PLAYER PERF loopback-rpc/bytes=1024/sessions=256 n=128 ops=12226.92 allocBytes=unsupported allocationSamples=10 allocEventsPerOp=102.00 p50us=29.00 p95us=305.10 p99us=1402.90 +PLAYER PERF loopback-broadcast/bytes=1024/sessions=256 n=20 ops=194.15 allocBytes=unsupported allocationSamples=10 allocEventsPerOp=8708.00 p50us=4612.30 p95us=10631.00 p99us=10631.00 +PLAYER PERF loopback-rpc/bytes=32768/sessions=1 n=128 ops=828.26 allocBytes=unsupported allocationSamples=10 allocEventsPerOp=112.60 p50us=1123.40 p95us=3500.10 p99us=3837.10 +PLAYER PERF loopback-broadcast/bytes=32768/sessions=1 n=20 ops=1815.71 allocBytes=unsupported allocationSamples=10 allocEventsPerOp=43.00 p50us=98.60 p95us=3193.40 p99us=3193.40 +PLAYER PERF loopback-rpc/bytes=32768/sessions=32 n=128 ops=842.40 allocBytes=unsupported allocationSamples=10 allocEventsPerOp=112.00 p50us=1163.90 p95us=3475.30 p99us=3500.30 +PLAYER PERF loopback-broadcast/bytes=32768/sessions=32 n=20 ops=65.08 allocBytes=unsupported allocationSamples=10 allocEventsPerOp=1252.90 p50us=15096.30 p95us=26400.40 p99us=26400.40 +PLAYER PERF loopback-rpc/bytes=32768/sessions=256 n=128 ops=830.36 allocBytes=unsupported allocationSamples=10 allocEventsPerOp=112.60 p50us=1230.40 p95us=3500.80 p99us=3549.90 +PLAYER PERF loopback-broadcast/bytes=32768/sessions=256 n=20 ops=8.67 allocBytes=unsupported allocationSamples=10 allocEventsPerOp=9995.60 p50us=114364.50 p95us=124433.00 p99us=124433.00 + +PLAYER Queue pending=32 bytes=32768 replaced=96 rejected=0 diff --git a/Docs/Performance/Data/2026-09-28/runtime-tests-final.log b/Docs/Performance/Data/2026-09-28/runtime-tests-final.log new file mode 100644 index 0000000..9b74c31 --- /dev/null +++ b/Docs/Performance/Data/2026-09-28/runtime-tests-final.log @@ -0,0 +1,69 @@ +D:\UnityBuilds\ShrinkSDK\Assets\Modules\ShrinkDataSaver\Runtime\LocalStorageProvider.cs(22,55): warning CS8625: 无法将 null 字面量转换为非 null 的引用类型。 [D:\UnityBuilds\ShrinkSDK\Tools\AgentSupport\RuntimeTests\RuntimeTests.csproj] +D:\UnityBuilds\ShrinkSDK\Assets\Modules\ShrinkDataSaver\Runtime\ShrinkSettings.cs(42,61): warning CS8601: 可能的 null 引用赋值。 [D:\UnityBuilds\ShrinkSDK\Tools\AgentSupport\RuntimeTests\RuntimeTests.csproj] +D:\UnityBuilds\ShrinkSDK\Assets\Modules\ShrinkDataSaver\Runtime\ShrinkSave.cs(59,85): warning CS8625: 无法将 null 字面量转换为非 null 的引用类型。 [D:\UnityBuilds\ShrinkSDK\Tools\AgentSupport\RuntimeTests\RuntimeTests.csproj] +D:\UnityBuilds\ShrinkSDK\Assets\Modules\ShrinkDataSaver\Runtime\ShrinkSave.cs(65,122): warning CS8625: 无法将 null 字面量转换为非 null 的引用类型。 [D:\UnityBuilds\ShrinkSDK\Tools\AgentSupport\RuntimeTests\RuntimeTests.csproj] +D:\UnityBuilds\ShrinkSDK\Assets\Modules\ShrinkDataSaver\Runtime\ShrinkSave.cs(184,88): warning CS8625: 无法将 null 字面量转换为非 null 的引用类型。 [D:\UnityBuilds\ShrinkSDK\Tools\AgentSupport\RuntimeTests\RuntimeTests.csproj] +D:\UnityBuilds\ShrinkSDK\Assets\Modules\ShrinkDataSaver\Runtime\ShrinkSave.cs(297,89): warning CS8625: 无法将 null 字面量转换为非 null 的引用类型。 [D:\UnityBuilds\ShrinkSDK\Tools\AgentSupport\RuntimeTests\RuntimeTests.csproj] +D:\UnityBuilds\ShrinkSDK\Assets\Modules\ShrinkContext.Core\Tests\RetirementRetentionTests.cs(40,28): warning CS8767: “UniTask ChainComponent.ApplyAsync(ShrinkCtx ctx, object config)”的参数“config”类型中引用类型的为 Null 性与隐式实现的成员“UniTask IShrinkComponent.ApplyAsync(ShrinkCtx ctx, object? config)”不匹配(可能是由于为 Null 性特性)。 [D:\UnityBuilds\ShrinkSDK\Tools\AgentSupport\RuntimeTests\RuntimeTests.csproj] +D:\UnityBuilds\ShrinkSDK\Assets\Modules\ShrinkDataSaver\Runtime\ShrinkSettings.cs(37,51): warning CS8604: “JToken JToken.FromObject(object o)”中的形参“o”可能传入 null 引用实参。 [D:\UnityBuilds\ShrinkSDK\Tools\AgentSupport\RuntimeTests\RuntimeTests.csproj] +D:\UnityBuilds\ShrinkSDK\Assets\Modules\ShrinkDataSaver\Runtime\ShrinkSettings.cs(46,30): warning CS8603: 可能返回 null 引用。 [D:\UnityBuilds\ShrinkSDK\Tools\AgentSupport\RuntimeTests\RuntimeTests.csproj] +D:\UnityBuilds\ShrinkSDK\Assets\Modules\ShrinkDataSaver\Runtime\ShrinkSettings.cs(59,34): warning CS8625: 无法将 null 字面量转换为非 null 的引用类型。 [D:\UnityBuilds\ShrinkSDK\Tools\AgentSupport\RuntimeTests\RuntimeTests.csproj] +D:\UnityBuilds\ShrinkSDK\Assets\Modules\ShrinkDataSaver\Runtime\ShrinkSettings.cs(104,28): warning CS8625: 无法将 null 字面量转换为非 null 的引用类型。 [D:\UnityBuilds\ShrinkSDK\Tools\AgentSupport\RuntimeTests\RuntimeTests.csproj] +D:\UnityBuilds\ShrinkSDK\Assets\Modules\ShrinkDataSaver\Runtime\ShrinkSettings.cs(106,25): warning CS8625: 无法将 null 字面量转换为非 null 的引用类型。 [D:\UnityBuilds\ShrinkSDK\Tools\AgentSupport\RuntimeTests\RuntimeTests.csproj] +D:\UnityBuilds\ShrinkSDK\Assets\Modules\ShrinkDataSaver\Runtime\DataSerializer.cs(20,16): warning CS8603: 可能返回 null 引用。 [D:\UnityBuilds\ShrinkSDK\Tools\AgentSupport\RuntimeTests\RuntimeTests.csproj] +D:\UnityBuilds\ShrinkSDK\Assets\Modules\ShrinkDataSaver\Runtime\ShrinkSettings.cs(137,35): warning CS8600: 将 null 文本或可能的 null 值转换为不可为 null 类型。 [D:\UnityBuilds\ShrinkSDK\Tools\AgentSupport\RuntimeTests\RuntimeTests.csproj] +D:\UnityBuilds\ShrinkSDK\Assets\Modules\ShrinkDataSaver\Runtime\ShrinkSettings.cs(22,41): warning CS8618: 在退出构造函数时,不可为 null 的 字段 "_storage" 必须包含非 null 值。请考虑添加 "required" 修饰符或将该 字段 声明为可为 null。 [D:\UnityBuilds\ShrinkSDK\Tools\AgentSupport\RuntimeTests\RuntimeTests.csproj] +D:\UnityBuilds\ShrinkSDK\Assets\Modules\ShrinkDataSaver\Runtime\ShrinkSettings.cs(23,31): warning CS8618: 在退出构造函数时,不可为 null 的 字段 "_filePath" 必须包含非 null 值。请考虑添加 "required" 修饰符或将该 字段 声明为可为 null。 [D:\UnityBuilds\ShrinkSDK\Tools\AgentSupport\RuntimeTests\RuntimeTests.csproj] +D:\UnityBuilds\ShrinkSDK\Assets\Modules\ShrinkDataSaver\Runtime\ShrinkSettings.cs(25,48): warning CS8618: 在退出构造函数时,不可为 null 的 字段 "_debounceCts" 必须包含非 null 值。请考虑添加 "required" 修饰符或将该 字段 声明为可为 null。 [D:\UnityBuilds\ShrinkSDK\Tools\AgentSupport\RuntimeTests\RuntimeTests.csproj] +D:\UnityBuilds\ShrinkSDK\Assets\Modules\ShrinkDataSaver\Runtime\ShrinkSettings.cs(28,52): warning CS8618: 在退出构造函数时,不可为 null 的 事件 "OnChanged" 必须包含非 null 值。请考虑添加 "required" 修饰符或将该 事件 声明为可为 null。 [D:\UnityBuilds\ShrinkSDK\Tools\AgentSupport\RuntimeTests\RuntimeTests.csproj] +D:\UnityBuilds\ShrinkSDK\Assets\Modules\ShrinkDataSaver\Runtime\SaveTypes.cs(16,23): warning CS8618: 在退出构造函数时,不可为 null 的 属性 "ScreenshotBase64" 必须包含非 null 值。请考虑添加 "required" 修饰符或将该 属性 声明为可为 null。 [D:\UnityBuilds\ShrinkSDK\Tools\AgentSupport\RuntimeTests\RuntimeTests.csproj] +D:\UnityBuilds\ShrinkSDK\Assets\Modules\ShrinkDataSaver\Runtime\ShrinkSave.cs(78,16): warning CS8603: 可能返回 null 引用。 [D:\UnityBuilds\ShrinkSDK\Tools\AgentSupport\RuntimeTests\RuntimeTests.csproj] +D:\UnityBuilds\ShrinkSDK\Assets\Modules\ShrinkDataSaver\Runtime\SaveTypes.cs(26,25): warning CS8618: 在退出构造函数时,不可为 null 的 属性 "Meta" 必须包含非 null 值。请考虑添加 "required" 修饰符或将该 属性 声明为可为 null。 [D:\UnityBuilds\ShrinkSDK\Tools\AgentSupport\RuntimeTests\RuntimeTests.csproj] +D:\UnityBuilds\ShrinkSDK\Assets\Modules\ShrinkDataSaver\Runtime\SaveTypes.cs(28,23): warning CS8618: 在退出构造函数时,不可为 null 的 属性 "EncryptedModules" 必须包含非 null 值。请考虑添加 "required" 修饰符或将该 属性 声明为可为 null。 [D:\UnityBuilds\ShrinkSDK\Tools\AgentSupport\RuntimeTests\RuntimeTests.csproj] +D:\UnityBuilds\ShrinkSDK\Assets\Modules\ShrinkDataSaver\Runtime\ShrinkSave.cs(86,24): warning CS8603: 可能返回 null 引用。 [D:\UnityBuilds\ShrinkSDK\Tools\AgentSupport\RuntimeTests\RuntimeTests.csproj] +D:\UnityBuilds\ShrinkSDK\Assets\Modules\ShrinkDataSaver\Runtime\ShrinkSave.cs(97,24): warning CS8603: 可能返回 null 引用。 [D:\UnityBuilds\ShrinkSDK\Tools\AgentSupport\RuntimeTests\RuntimeTests.csproj] +D:\UnityBuilds\ShrinkSDK\Assets\Modules\ShrinkDataSaver\Runtime\ShrinkSave.cs(101,24): warning CS8603: 可能返回 null 引用。 [D:\UnityBuilds\ShrinkSDK\Tools\AgentSupport\RuntimeTests\RuntimeTests.csproj] +D:\UnityBuilds\ShrinkSDK\Assets\Modules\ShrinkDataSaver\Runtime\SaveTypes.cs(43,46): warning CS8603: 可能返回 null 引用。 [D:\UnityBuilds\ShrinkSDK\Tools\AgentSupport\RuntimeTests\RuntimeTests.csproj] +D:\UnityBuilds\ShrinkSDK\Assets\Modules\ShrinkDataSaver\Runtime\SaveTypes.cs(44,69): warning CS8604: “void ISaveModule.Deserialize(T data)”中的形参“data”可能传入 null 引用实参。 [D:\UnityBuilds\ShrinkSDK\Tools\AgentSupport\RuntimeTests\RuntimeTests.csproj] +D:\UnityBuilds\ShrinkSDK\Assets\Modules\ShrinkDataSaver\Runtime\ShrinkSave.cs(164,24): warning CS8603: 可能返回 null 引用。 [D:\UnityBuilds\ShrinkSDK\Tools\AgentSupport\RuntimeTests\RuntimeTests.csproj] +D:\UnityBuilds\ShrinkSDK\Assets\Modules\ShrinkDataSaver\Runtime\ShrinkSave.cs(168,24): warning CS8603: 可能返回 null 引用。 [D:\UnityBuilds\ShrinkSDK\Tools\AgentSupport\RuntimeTests\RuntimeTests.csproj] +D:\UnityBuilds\ShrinkSDK\Assets\Modules\ShrinkContext.Core\Tests\RetirementRetentionTests.cs(16,33): warning CS8600: 将 null 文本或可能的 null 值转换为不可为 null 类型。 [D:\UnityBuilds\ShrinkSDK\Tools\AgentSupport\RuntimeTests\RuntimeTests.csproj] +D:\UnityBuilds\ShrinkSDK\Assets\Modules\ShrinkContext.Core\Tests\RetirementRetentionTests.cs(22,47): warning CS8604: “UniTask ShrinkContextRuntime.RetireAsync(ShrinkFiber fiber)”中的形参“fiber”可能传入 null 引用实参。 [D:\UnityBuilds\ShrinkSDK\Tools\AgentSupport\RuntimeTests\RuntimeTests.csproj] +D:\UnityBuilds\ShrinkSDK\Assets\Modules\ShrinkDataSaver\Runtime\SaveTypes.cs(85,39): warning CS8625: 无法将 null 字面量转换为非 null 的引用类型。 [D:\UnityBuilds\ShrinkSDK\Tools\AgentSupport\RuntimeTests\RuntimeTests.csproj] +D:\UnityBuilds\ShrinkSDK\Assets\Modules\ShrinkDataSaver\Runtime\SaveTypes.cs(76,33): warning CS8618: 在退出构造函数时,不可为 null 的 属性 "LastTimings" 必须包含非 null 值。请考虑添加 "required" 修饰符或将该 属性 声明为可为 null。 [D:\UnityBuilds\ShrinkSDK\Tools\AgentSupport\RuntimeTests\RuntimeTests.csproj] +D:\UnityBuilds\ShrinkSDK\Assets\Modules\ShrinkDataSaver\Runtime\SaveTypes.cs(82,23): warning CS8618: 在退出构造函数时,不可为 null 的 字段 "Screenshot" 必须包含非 null 值。请考虑添加 "required" 修饰符或将该 字段 声明为可为 null。 [D:\UnityBuilds\ShrinkSDK\Tools\AgentSupport\RuntimeTests\RuntimeTests.csproj] +D:\UnityBuilds\ShrinkSDK\Assets\Modules\ShrinkDataSaver\Runtime\SaveTypes.cs(111,25): warning CS8618: 在退出构造函数时,不可为 null 的 属性 "ModuleNames" 必须包含非 null 值。请考虑添加 "required" 修饰符或将该 属性 声明为可为 null。 [D:\UnityBuilds\ShrinkSDK\Tools\AgentSupport\RuntimeTests\RuntimeTests.csproj] +D:\UnityBuilds\ShrinkSDK\Assets\Modules\ShrinkDataSaver\Runtime\SaveTypes.cs(118,23): warning CS8618: 在退出构造函数时,不可为 null 的 属性 "ErrorMessage" 必须包含非 null 值。请考虑添加 "required" 修饰符或将该 属性 声明为可为 null。 [D:\UnityBuilds\ShrinkSDK\Tools\AgentSupport\RuntimeTests\RuntimeTests.csproj] +D:\UnityBuilds\ShrinkSDK\Assets\Modules\ShrinkDataSaver\Runtime\ShrinkSave.cs(190,35): warning CS8601: 可能的 null 引用赋值。 [D:\UnityBuilds\ShrinkSDK\Tools\AgentSupport\RuntimeTests\RuntimeTests.csproj] +D:\UnityBuilds\ShrinkSDK\Assets\Modules\ShrinkDataSaver\Runtime\SaveTypes.cs(130,25): warning CS8618: 在退出构造函数时,不可为 null 的 属性 "ModuleNames" 必须包含非 null 值。请考虑添加 "required" 修饰符或将该 属性 声明为可为 null。 [D:\UnityBuilds\ShrinkSDK\Tools\AgentSupport\RuntimeTests\RuntimeTests.csproj] +D:\UnityBuilds\ShrinkSDK\Assets\Modules\ShrinkDataSaver\Runtime\SaveTypes.cs(138,23): warning CS8618: 在退出构造函数时,不可为 null 的 属性 "ErrorMessage" 必须包含非 null 值。请考虑添加 "required" 修饰符或将该 属性 声明为可为 null。 [D:\UnityBuilds\ShrinkSDK\Tools\AgentSupport\RuntimeTests\RuntimeTests.csproj] +D:\UnityBuilds\ShrinkSDK\Assets\Modules\ShrinkDataSaver\Runtime\ShrinkSave.cs(365,37): warning CS8625: 无法将 null 字面量转换为非 null 的引用类型。 [D:\UnityBuilds\ShrinkSDK\Tools\AgentSupport\RuntimeTests\RuntimeTests.csproj] +D:\UnityBuilds\ShrinkSDK\Assets\Modules\ShrinkDataSaver\Runtime\ShrinkSave.cs(402,33): warning CS8625: 无法将 null 字面量转换为非 null 的引用类型。 [D:\UnityBuilds\ShrinkSDK\Tools\AgentSupport\RuntimeTests\RuntimeTests.csproj] +D:\UnityBuilds\ShrinkSDK\Assets\Modules\ShrinkDataSaver\Runtime\ShrinkSave.cs(403,29): warning CS8625: 无法将 null 字面量转换为非 null 的引用类型。 [D:\UnityBuilds\ShrinkSDK\Tools\AgentSupport\RuntimeTests\RuntimeTests.csproj] +D:\UnityBuilds\ShrinkSDK\Assets\Modules\ShrinkDataSaver\Runtime\ShrinkSave.cs(404,31): warning CS8625: 无法将 null 字面量转换为非 null 的引用类型。 [D:\UnityBuilds\ShrinkSDK\Tools\AgentSupport\RuntimeTests\RuntimeTests.csproj] +D:\UnityBuilds\ShrinkSDK\Assets\Modules\ShrinkDataSaver\Runtime\ShrinkSave.cs(405,28): warning CS8625: 无法将 null 字面量转换为非 null 的引用类型。 [D:\UnityBuilds\ShrinkSDK\Tools\AgentSupport\RuntimeTests\RuntimeTests.csproj] +D:\UnityBuilds\ShrinkSDK\Assets\Modules\ShrinkDataSaver\Runtime\ShrinkSave.cs(406,29): warning CS8625: 无法将 null 字面量转换为非 null 的引用类型。 [D:\UnityBuilds\ShrinkSDK\Tools\AgentSupport\RuntimeTests\RuntimeTests.csproj] +D:\UnityBuilds\ShrinkSDK\Assets\Modules\ShrinkDataSaver\Runtime\ShrinkSave.cs(407,31): warning CS8625: 无法将 null 字面量转换为非 null 的引用类型。 [D:\UnityBuilds\ShrinkSDK\Tools\AgentSupport\RuntimeTests\RuntimeTests.csproj] +D:\UnityBuilds\ShrinkSDK\Assets\Modules\ShrinkDataSaver\Runtime\ShrinkSave.cs(408,28): warning CS8625: 无法将 null 字面量转换为非 null 的引用类型。 [D:\UnityBuilds\ShrinkSDK\Tools\AgentSupport\RuntimeTests\RuntimeTests.csproj] +D:\UnityBuilds\ShrinkSDK\Assets\Modules\ShrinkDataSaver\Runtime\ShrinkSave.cs(409,36): warning CS8625: 无法将 null 字面量转换为非 null 的引用类型。 [D:\UnityBuilds\ShrinkSDK\Tools\AgentSupport\RuntimeTests\RuntimeTests.csproj] +D:\UnityBuilds\ShrinkSDK\Assets\Modules\ShrinkDataSaver\Runtime\ShrinkSave.cs(410,33): warning CS8625: 无法将 null 字面量转换为非 null 的引用类型。 [D:\UnityBuilds\ShrinkSDK\Tools\AgentSupport\RuntimeTests\RuntimeTests.csproj] +D:\UnityBuilds\ShrinkSDK\Assets\Modules\ShrinkDataSaver\Runtime\ShrinkSave.cs(520,29): warning CS8600: 将 null 文本或可能的 null 值转换为不可为 null 类型。 [D:\UnityBuilds\ShrinkSDK\Tools\AgentSupport\RuntimeTests\RuntimeTests.csproj] +D:\UnityBuilds\ShrinkSDK\Assets\Modules\ShrinkDataSaver\Runtime\ShrinkSave.cs(545,35): warning CS8600: 将 null 文本或可能的 null 值转换为不可为 null 类型。 [D:\UnityBuilds\ShrinkSDK\Tools\AgentSupport\RuntimeTests\RuntimeTests.csproj] +D:\UnityBuilds\ShrinkSDK\Assets\Modules\ShrinkDataSaver\Runtime\ShrinkSave.cs(592,35): warning CS8600: 将 null 文本或可能的 null 值转换为不可为 null 类型。 [D:\UnityBuilds\ShrinkSDK\Tools\AgentSupport\RuntimeTests\RuntimeTests.csproj] +D:\UnityBuilds\ShrinkSDK\Assets\Modules\ShrinkDataSaver\Runtime\ShrinkSave.cs(635,55): warning CS8601: 可能的 null 引用赋值。 [D:\UnityBuilds\ShrinkSDK\Tools\AgentSupport\RuntimeTests\RuntimeTests.csproj] +D:\UnityBuilds\ShrinkSDK\Assets\Modules\ShrinkDataSaver\Runtime\ShrinkSave.cs(22,41): warning CS8618: 在退出构造函数时,不可为 null 的 字段 "_storage" 必须包含非 null 值。请考虑添加 "required" 修饰符或将该 字段 声明为可为 null。 [D:\UnityBuilds\ShrinkSDK\Tools\AgentSupport\RuntimeTests\RuntimeTests.csproj] +D:\UnityBuilds\ShrinkSDK\Assets\Modules\ShrinkDataSaver\Runtime\ShrinkSave.cs(23,31): warning CS8618: 在退出构造函数时,不可为 null 的 字段 "_savesDir" 必须包含非 null 值。请考虑添加 "required" 修饰符或将该 字段 声明为可为 null。 [D:\UnityBuilds\ShrinkSDK\Tools\AgentSupport\RuntimeTests\RuntimeTests.csproj] +D:\UnityBuilds\ShrinkSDK\Assets\Modules\ShrinkDataSaver\Runtime\ShrinkSave.cs(24,31): warning CS8618: 在退出构造函数时,不可为 null 的 字段 "_fileExt" 必须包含非 null 值。请考虑添加 "required" 修饰符或将该 字段 声明为可为 null。 [D:\UnityBuilds\ShrinkSDK\Tools\AgentSupport\RuntimeTests\RuntimeTests.csproj] +D:\UnityBuilds\ShrinkSDK\Assets\Modules\ShrinkDataSaver\Runtime\ShrinkSave.cs(34,51): warning CS8618: 在退出构造函数时,不可为 null 的 字段 "_loadedModuleData" 必须包含非 null 值。请考虑添加 "required" 修饰符或将该 字段 声明为可为 null。 [D:\UnityBuilds\ShrinkSDK\Tools\AgentSupport\RuntimeTests\RuntimeTests.csproj] +D:\UnityBuilds\ShrinkSDK\Assets\Modules\ShrinkDataSaver\Runtime\ShrinkSave.cs(36,58): warning CS8618: 在退出构造函数时,不可为 null 的 事件 "OnSaveStarted" 必须包含非 null 值。请考虑添加 "required" 修饰符或将该 事件 声明为可为 null。 [D:\UnityBuilds\ShrinkSDK\Tools\AgentSupport\RuntimeTests\RuntimeTests.csproj] +D:\UnityBuilds\ShrinkSDK\Assets\Modules\ShrinkDataSaver\Runtime\ShrinkSave.cs(37,60): warning CS8618: 在退出构造函数时,不可为 null 的 事件 "OnSaveCompleted" 必须包含非 null 值。请考虑添加 "required" 修饰符或将该 事件 声明为可为 null。 [D:\UnityBuilds\ShrinkSDK\Tools\AgentSupport\RuntimeTests\RuntimeTests.csproj] +D:\UnityBuilds\ShrinkSDK\Assets\Modules\ShrinkDataSaver\Runtime\ShrinkSave.cs(38,57): warning CS8618: 在退出构造函数时,不可为 null 的 事件 "OnSaveFailed" 必须包含非 null 值。请考虑添加 "required" 修饰符或将该 事件 声明为可为 null。 [D:\UnityBuilds\ShrinkSDK\Tools\AgentSupport\RuntimeTests\RuntimeTests.csproj] +D:\UnityBuilds\ShrinkSDK\Assets\Modules\ShrinkDataSaver\Runtime\ShrinkSave.cs(39,58): warning CS8618: 在退出构造函数时,不可为 null 的 事件 "OnLoadStarted" 必须包含非 null 值。请考虑添加 "required" 修饰符或将该 事件 声明为可为 null。 [D:\UnityBuilds\ShrinkSDK\Tools\AgentSupport\RuntimeTests\RuntimeTests.csproj] +D:\UnityBuilds\ShrinkSDK\Assets\Modules\ShrinkDataSaver\Runtime\ShrinkSave.cs(40,60): warning CS8618: 在退出构造函数时,不可为 null 的 事件 "OnLoadCompleted" 必须包含非 null 值。请考虑添加 "required" 修饰符或将该 事件 声明为可为 null。 [D:\UnityBuilds\ShrinkSDK\Tools\AgentSupport\RuntimeTests\RuntimeTests.csproj] +D:\UnityBuilds\ShrinkSDK\Assets\Modules\ShrinkDataSaver\Runtime\ShrinkSave.cs(41,57): warning CS8618: 在退出构造函数时,不可为 null 的 事件 "OnLoadFailed" 必须包含非 null 值。请考虑添加 "required" 修饰符或将该 事件 声明为可为 null。 [D:\UnityBuilds\ShrinkSDK\Tools\AgentSupport\RuntimeTests\RuntimeTests.csproj] +D:\UnityBuilds\ShrinkSDK\Assets\Modules\ShrinkDataSaver\Runtime\ShrinkSave.cs(42,65): warning CS8618: 在退出构造函数时,不可为 null 的 事件 "OnMigrationCompleted" 必须包含非 null 值。请考虑添加 "required" 修饰符或将该 事件 声明为可为 null。 [D:\UnityBuilds\ShrinkSDK\Tools\AgentSupport\RuntimeTests\RuntimeTests.csproj] +D:\UnityBuilds\ShrinkSDK\Assets\Modules\ShrinkDataSaver\Runtime\ShrinkSave.cs(43,58): warning CS8618: 在退出构造函数时,不可为 null 的 事件 "OnDeleteCompleted" 必须包含非 null 值。请考虑添加 "required" 修饰符或将该 事件 声明为可为 null。 [D:\UnityBuilds\ShrinkSDK\Tools\AgentSupport\RuntimeTests\RuntimeTests.csproj] +D:\UnityBuilds\ShrinkSDK\Tools\AgentSupport\RuntimeTests\bin\Debug\net8.0\RuntimeTests.dll (.NETCoreApp,Version=v8.0)的测试运行 +总共 1 个测试文件与指定模式相匹配。 + +已通过! - 失败: 0,通过: 64,已跳过: 0,总计: 64,持续时间: 524 ms - RuntimeTests.dll (net8.0) diff --git a/Docs/Performance/Data/2026-09-28/sha256.json b/Docs/Performance/Data/2026-09-28/sha256.json new file mode 100644 index 0000000..36a00ec --- /dev/null +++ b/Docs/Performance/Data/2026-09-28/sha256.json @@ -0,0 +1,21 @@ +{ + "benchmark-net8.json": "8744d957712c35f98e761182eb6752c46f36788f1317832e4c15cbcfc2c82726", + "benchmark-net10.json": "54d7692db4c452e000d2418790b6d2a43d1ed9ef16c5b989fcf96e654a979eba", + "tcp-net8.json": "1b976e29eeaa7a1ae47f0a75f0e704425506ff05b7811a8528c8fcb4513d984a", + "tcp-net10.json": "dd92388a8b968ef810af3b73d4aa7a505185ebc7058a71e53cfebb392a29b5f5", + "unity-player-results.xml": "b6c590a299647799765f776a5ebdbfdf49a9a6da22c045ffdc26fd7de0812ff8", + "unity-all-editmode-results.json": "b847214ab6ebbffa03fd8938c3c207067ce599d3f7b258a328a9b9d9247af4e1", + "justany-tests.json": "171c1107fea5d8c7d555770676887098404eaab81ba7f0b9a89cd8c8b7e85112", + "justany-messagepack-results.json": "33c59e11dd0730b52d5c86f0c611649eb4c9c165afbafadeed9149de0a742dc6", + "justany-runtime.json": "d5b1217c46573980d9c8579c123b9899419638a8afa80a22e1e1130cb2ed97a7", + "justany-installed.json": "56f30aee832c2c0c9262ecbe517b0cc728b1de9120a43d5182734d8565424472", + "package-validation.json": "20847ac3e9322970fe8ba01495f5eb822e4467b633d4b5c59d462c6e4b02755d", + "waiting-query.json": "690436e0090247039a22fea002ad59ff5a86a340148bc8f4f80067687757c7f6", + "godot-run-final.log": "c2dab1e7ceadf955ea03cf6b44f7e65492ccf538d94053ac8d67786c743867a9", + "nuget-consumer-final.log": "10c7cb6dffcaa885c951796cda3beb4f33fb73a9f9ea26142c81fa8798743cbb", + "runtime-tests-final.log": "2b938f9c1331e8abb6272b197fd72caa6dc5054ec2b17d8b0f8c07d0c6f024c6", + "cli-tests-final.log": "14470b6b1ab750d073472feb6221ba1d8284b5a329ac131f07f2d3c5446f7482", + "consumer-runtime-query.json": "041ef46324bce87070b2df4dff28ba1416483d0506b88d7aa1169dd72e0454b2", + "justany-play-errors.json": "60353a33b56d949ecf0aa2fe18f0ba09f86e51d4fc21233a236a8030e7da4f2d", + "justany-compilation.json": "4e46d765370580ff07ee8b95ee04e3e6f3c4bf09aba25dfcec7ae3e6dfde0078" +} diff --git a/Docs/Performance/Data/2026-09-28/tcp-net10.json b/Docs/Performance/Data/2026-09-28/tcp-net10.json new file mode 100644 index 0000000..81c04b4 --- /dev/null +++ b/Docs/Performance/Data/2026-09-28/tcp-net10.json @@ -0,0 +1,303 @@ +{ + "runtime": ".NET 10.0.8", + "host": "Windows loopback TCP; sequential round-trip samples, multiple real connections", + "rows": [ + { + "scenario": "tcp/rpc", + "connections": 1, + "bytes": 64, + "operations": 256, + "throughputPerSecond": 2569.8786133898707, + "p50Microseconds": 59.1, + "p95Microseconds": 172, + "p99Microseconds": 302.8, + "processAllocatedBytes": 4181192 + }, + { + "scenario": "tcp/broadcast", + "connections": 1, + "bytes": 64, + "elapsedMilliseconds": 3.9542, + "delivered": 1 + }, + { + "scenario": "tcp/rpc", + "connections": 1, + "bytes": 1024, + "operations": 256, + "throughputPerSecond": 9339.622985687758, + "p50Microseconds": 76.8, + "p95Microseconds": 233.8, + "p99Microseconds": 413.7, + "processAllocatedBytes": 9931824 + }, + { + "scenario": "tcp/broadcast", + "connections": 1, + "bytes": 1024, + "elapsedMilliseconds": 4.1822, + "delivered": 1 + }, + { + "scenario": "tcp/rpc", + "connections": 1, + "bytes": 32768, + "operations": 256, + "throughputPerSecond": 2077.1380931710028, + "p50Microseconds": 403.3, + "p95Microseconds": 1068.7, + "p99Microseconds": 1287.4, + "processAllocatedBytes": 229642408 + }, + { + "scenario": "tcp/broadcast", + "connections": 1, + "bytes": 32768, + "elapsedMilliseconds": 0.8935, + "delivered": 1 + }, + { + "scenario": "tcp/slow-consumer-burst", + "connections": 1, + "attempts": 8, + "beforePump": { + "PendingCount": 2, + "PendingBytes": 2136, + "Rejected": 1, + "Replaced": 0, + "Completed": 0, + "OldestWaitMilliseconds": 108.5865, + "LastWaitMilliseconds": 0 + }, + "afterPump": { + "PendingCount": 0, + "PendingBytes": 0, + "Rejected": 1, + "Replaced": 0, + "Completed": 2, + "OldestWaitMilliseconds": 0, + "LastWaitMilliseconds": 108.3473 + }, + "metrics": { + "CurrentSessions": 1, + "SessionsConnected": 1, + "SessionsDisconnected": 0, + "PacketsSent": 771, + "PacketsReceived": 770, + "BytesSent": 8772548, + "BytesReceived": 8703064, + "RpcStarted": 0, + "RpcCompleted": 0, + "RpcTimedOut": 0, + "RpcCanceled": 0, + "RpcFailed": 0, + "ProtocolViolations": 0, + "AuthRejectedCount": 0, + "PermissionDeniedCount": 0, + "HandlerExceptionCount": 0, + "UnknownOpcodeCount": 0, + "DispatchMissCount": 2, + "SerializationErrorCount": 0, + "DispatchQueueRejectedCount": 1 + } + }, + { + "scenario": "tcp/rpc", + "connections": 32, + "bytes": 64, + "operations": 256, + "throughputPerSecond": 13928.182807399347, + "p50Microseconds": 58.6, + "p95Microseconds": 147.2, + "p99Microseconds": 235, + "processAllocatedBytes": 3788328 + }, + { + "scenario": "tcp/broadcast", + "connections": 32, + "bytes": 64, + "elapsedMilliseconds": 15.9674, + "delivered": 32 + }, + { + "scenario": "tcp/rpc", + "connections": 32, + "bytes": 1024, + "operations": 256, + "throughputPerSecond": 9348.968509314276, + "p50Microseconds": 79.3, + "p95Microseconds": 199.6, + "p99Microseconds": 551, + "processAllocatedBytes": 9913976 + }, + { + "scenario": "tcp/broadcast", + "connections": 32, + "bytes": 1024, + "elapsedMilliseconds": 3.5947, + "delivered": 32 + }, + { + "scenario": "tcp/rpc", + "connections": 32, + "bytes": 32768, + "operations": 256, + "throughputPerSecond": 1782.1179776023969, + "p50Microseconds": 446.9, + "p95Microseconds": 1466.1, + "p99Microseconds": 1839.8, + "processAllocatedBytes": 229509592 + }, + { + "scenario": "tcp/broadcast", + "connections": 32, + "bytes": 32768, + "elapsedMilliseconds": 13.9419, + "delivered": 32 + }, + { + "scenario": "tcp/slow-consumer-burst", + "connections": 32, + "attempts": 256, + "beforePump": { + "PendingCount": 30, + "PendingBytes": 32040, + "Rejected": 32, + "Replaced": 0, + "Completed": 0, + "OldestWaitMilliseconds": 109.5715, + "LastWaitMilliseconds": 0 + }, + "afterPump": { + "PendingCount": 23, + "PendingBytes": 24564, + "Rejected": 32, + "Replaced": 0, + "Completed": 7, + "OldestWaitMilliseconds": 109.1555, + "LastWaitMilliseconds": 109.1537 + }, + "metrics": { + "CurrentSessions": 32, + "SessionsConnected": 32, + "SessionsDisconnected": 0, + "PacketsSent": 864, + "PacketsReceived": 775, + "BytesSent": 9826176, + "BytesReceived": 8708404, + "RpcStarted": 0, + "RpcCompleted": 0, + "RpcTimedOut": 0, + "RpcCanceled": 0, + "RpcFailed": 0, + "ProtocolViolations": 0, + "AuthRejectedCount": 0, + "PermissionDeniedCount": 0, + "HandlerExceptionCount": 0, + "UnknownOpcodeCount": 0, + "DispatchMissCount": 7, + "SerializationErrorCount": 0, + "DispatchQueueRejectedCount": 32 + } + }, + { + "scenario": "tcp/rpc", + "connections": 256, + "bytes": 64, + "operations": 256, + "throughputPerSecond": 8559.783062997996, + "p50Microseconds": 74.8, + "p95Microseconds": 204, + "p99Microseconds": 313.2, + "processAllocatedBytes": 4045992 + }, + { + "scenario": "tcp/broadcast", + "connections": 256, + "bytes": 64, + "elapsedMilliseconds": 12.374, + "delivered": 256 + }, + { + "scenario": "tcp/rpc", + "connections": 256, + "bytes": 1024, + "operations": 256, + "throughputPerSecond": 7391.815297015266, + "p50Microseconds": 88.4, + "p95Microseconds": 250.8, + "p99Microseconds": 384.3, + "processAllocatedBytes": 9917520 + }, + { + "scenario": "tcp/broadcast", + "connections": 256, + "bytes": 1024, + "elapsedMilliseconds": 12.6405, + "delivered": 256 + }, + { + "scenario": "tcp/rpc", + "connections": 256, + "bytes": 32768, + "operations": 256, + "throughputPerSecond": 1195.502482302127, + "p50Microseconds": 418.6, + "p95Microseconds": 1033.3, + "p99Microseconds": 16401.9, + "processAllocatedBytes": 227807112 + }, + { + "scenario": "tcp/broadcast", + "connections": 256, + "bytes": 32768, + "elapsedMilliseconds": 47.4633, + "delivered": 256 + }, + { + "scenario": "tcp/slow-consumer-burst", + "connections": 256, + "attempts": 2048, + "beforePump": { + "PendingCount": 30, + "PendingBytes": 32040, + "Rejected": 256, + "Replaced": 0, + "Completed": 0, + "OldestWaitMilliseconds": 124.5254, + "LastWaitMilliseconds": 0 + }, + "afterPump": { + "PendingCount": 23, + "PendingBytes": 24564, + "Rejected": 256, + "Replaced": 0, + "Completed": 7, + "OldestWaitMilliseconds": 124.4757, + "LastWaitMilliseconds": 124.5044 + }, + "metrics": { + "CurrentSessions": 256, + "SessionsConnected": 256, + "SessionsDisconnected": 0, + "PacketsSent": 1536, + "PacketsReceived": 775, + "BytesSent": 17439488, + "BytesReceived": 8708404, + "RpcStarted": 0, + "RpcCompleted": 0, + "RpcTimedOut": 0, + "RpcCanceled": 0, + "RpcFailed": 0, + "ProtocolViolations": 0, + "AuthRejectedCount": 0, + "PermissionDeniedCount": 0, + "HandlerExceptionCount": 0, + "UnknownOpcodeCount": 0, + "DispatchMissCount": 7, + "SerializationErrorCount": 0, + "DispatchQueueRejectedCount": 256 + } + } + ] +} \ No newline at end of file diff --git a/Docs/Performance/Data/2026-09-28/tcp-net8.json b/Docs/Performance/Data/2026-09-28/tcp-net8.json new file mode 100644 index 0000000..8a631ba --- /dev/null +++ b/Docs/Performance/Data/2026-09-28/tcp-net8.json @@ -0,0 +1,303 @@ +{ + "runtime": ".NET 8.0.31", + "host": "Windows loopback TCP; sequential round-trip samples, multiple real connections", + "rows": [ + { + "scenario": "tcp/rpc", + "connections": 1, + "bytes": 64, + "operations": 256, + "throughputPerSecond": 2786.3308702603804, + "p50Microseconds": 61.2, + "p95Microseconds": 164.7, + "p99Microseconds": 395.7, + "processAllocatedBytes": 4198840 + }, + { + "scenario": "tcp/broadcast", + "connections": 1, + "bytes": 64, + "elapsedMilliseconds": 2.3583, + "delivered": 1 + }, + { + "scenario": "tcp/rpc", + "connections": 1, + "bytes": 1024, + "operations": 256, + "throughputPerSecond": 9602.940900650823, + "p50Microseconds": 75.7, + "p95Microseconds": 193.2, + "p99Microseconds": 496, + "processAllocatedBytes": 9938800 + }, + { + "scenario": "tcp/broadcast", + "connections": 1, + "bytes": 1024, + "elapsedMilliseconds": 4.7543, + "delivered": 1 + }, + { + "scenario": "tcp/rpc", + "connections": 1, + "bytes": 32768, + "operations": 256, + "throughputPerSecond": 2206.3658826885603, + "p50Microseconds": 384.2, + "p95Microseconds": 1002.5, + "p99Microseconds": 1126.9, + "processAllocatedBytes": 229867960 + }, + { + "scenario": "tcp/broadcast", + "connections": 1, + "bytes": 32768, + "elapsedMilliseconds": 10.4461, + "delivered": 1 + }, + { + "scenario": "tcp/slow-consumer-burst", + "connections": 1, + "attempts": 8, + "beforePump": { + "PendingCount": 2, + "PendingBytes": 2136, + "Rejected": 1, + "Replaced": 0, + "Completed": 0, + "OldestWaitMilliseconds": 109.4399, + "LastWaitMilliseconds": 0 + }, + "afterPump": { + "PendingCount": 0, + "PendingBytes": 0, + "Rejected": 1, + "Replaced": 0, + "Completed": 2, + "OldestWaitMilliseconds": 0, + "LastWaitMilliseconds": 109.0027 + }, + "metrics": { + "CurrentSessions": 1, + "SessionsConnected": 1, + "SessionsDisconnected": 0, + "PacketsSent": 771, + "PacketsReceived": 770, + "BytesSent": 8772548, + "BytesReceived": 8703064, + "RpcStarted": 0, + "RpcCompleted": 0, + "RpcTimedOut": 0, + "RpcCanceled": 0, + "RpcFailed": 0, + "ProtocolViolations": 0, + "AuthRejectedCount": 0, + "PermissionDeniedCount": 0, + "HandlerExceptionCount": 0, + "UnknownOpcodeCount": 0, + "DispatchMissCount": 2, + "SerializationErrorCount": 0, + "DispatchQueueRejectedCount": 1 + } + }, + { + "scenario": "tcp/rpc", + "connections": 32, + "bytes": 64, + "operations": 256, + "throughputPerSecond": 9747.14534288249, + "p50Microseconds": 78.5, + "p95Microseconds": 235.1, + "p99Microseconds": 304.2, + "processAllocatedBytes": 3791256 + }, + { + "scenario": "tcp/broadcast", + "connections": 32, + "bytes": 64, + "elapsedMilliseconds": 9.6954, + "delivered": 32 + }, + { + "scenario": "tcp/rpc", + "connections": 32, + "bytes": 1024, + "operations": 256, + "throughputPerSecond": 8234.052955253068, + "p50Microseconds": 86.6, + "p95Microseconds": 247.5, + "p99Microseconds": 494.8, + "processAllocatedBytes": 9908800 + }, + { + "scenario": "tcp/broadcast", + "connections": 32, + "bytes": 1024, + "elapsedMilliseconds": 15.3001, + "delivered": 32 + }, + { + "scenario": "tcp/rpc", + "connections": 32, + "bytes": 32768, + "operations": 256, + "throughputPerSecond": 1754.0157368099387, + "p50Microseconds": 419.6, + "p95Microseconds": 1319.5, + "p99Microseconds": 1647.1, + "processAllocatedBytes": 230263696 + }, + { + "scenario": "tcp/broadcast", + "connections": 32, + "bytes": 32768, + "elapsedMilliseconds": 11.5336, + "delivered": 32 + }, + { + "scenario": "tcp/slow-consumer-burst", + "connections": 32, + "attempts": 256, + "beforePump": { + "PendingCount": 30, + "PendingBytes": 32040, + "Rejected": 32, + "Replaced": 0, + "Completed": 0, + "OldestWaitMilliseconds": 126.3722, + "LastWaitMilliseconds": 0 + }, + "afterPump": { + "PendingCount": 23, + "PendingBytes": 24564, + "Rejected": 32, + "Replaced": 0, + "Completed": 7, + "OldestWaitMilliseconds": 126.2021, + "LastWaitMilliseconds": 126.1876 + }, + "metrics": { + "CurrentSessions": 32, + "SessionsConnected": 32, + "SessionsDisconnected": 0, + "PacketsSent": 864, + "PacketsReceived": 775, + "BytesSent": 9826176, + "BytesReceived": 8708404, + "RpcStarted": 0, + "RpcCompleted": 0, + "RpcTimedOut": 0, + "RpcCanceled": 0, + "RpcFailed": 0, + "ProtocolViolations": 0, + "AuthRejectedCount": 0, + "PermissionDeniedCount": 0, + "HandlerExceptionCount": 0, + "UnknownOpcodeCount": 0, + "DispatchMissCount": 7, + "SerializationErrorCount": 0, + "DispatchQueueRejectedCount": 32 + } + }, + { + "scenario": "tcp/rpc", + "connections": 256, + "bytes": 64, + "operations": 256, + "throughputPerSecond": 6808.96336192566, + "p50Microseconds": 104.3, + "p95Microseconds": 248.8, + "p99Microseconds": 376.4, + "processAllocatedBytes": 4047528 + }, + { + "scenario": "tcp/broadcast", + "connections": 256, + "bytes": 64, + "elapsedMilliseconds": 8.4403, + "delivered": 256 + }, + { + "scenario": "tcp/rpc", + "connections": 256, + "bytes": 1024, + "operations": 256, + "throughputPerSecond": 6911.465743698012, + "p50Microseconds": 99.3, + "p95Microseconds": 258.9, + "p99Microseconds": 355.1, + "processAllocatedBytes": 9916024 + }, + { + "scenario": "tcp/broadcast", + "connections": 256, + "bytes": 1024, + "elapsedMilliseconds": 9.0517, + "delivered": 256 + }, + { + "scenario": "tcp/rpc", + "connections": 256, + "bytes": 32768, + "operations": 256, + "throughputPerSecond": 1056.106917624073, + "p50Microseconds": 439.3, + "p95Microseconds": 1988.3, + "p99Microseconds": 14849.1, + "processAllocatedBytes": 228731112 + }, + { + "scenario": "tcp/broadcast", + "connections": 256, + "bytes": 32768, + "elapsedMilliseconds": 64.9767, + "delivered": 256 + }, + { + "scenario": "tcp/slow-consumer-burst", + "connections": 256, + "attempts": 2048, + "beforePump": { + "PendingCount": 30, + "PendingBytes": 32040, + "Rejected": 256, + "Replaced": 0, + "Completed": 0, + "OldestWaitMilliseconds": 123.9167, + "LastWaitMilliseconds": 0 + }, + "afterPump": { + "PendingCount": 23, + "PendingBytes": 24564, + "Rejected": 256, + "Replaced": 0, + "Completed": 7, + "OldestWaitMilliseconds": 123.3004, + "LastWaitMilliseconds": 123.418 + }, + "metrics": { + "CurrentSessions": 256, + "SessionsConnected": 256, + "SessionsDisconnected": 0, + "PacketsSent": 1536, + "PacketsReceived": 775, + "BytesSent": 17439488, + "BytesReceived": 8708404, + "RpcStarted": 0, + "RpcCompleted": 0, + "RpcTimedOut": 0, + "RpcCanceled": 0, + "RpcFailed": 0, + "ProtocolViolations": 0, + "AuthRejectedCount": 0, + "PermissionDeniedCount": 0, + "HandlerExceptionCount": 0, + "UnknownOpcodeCount": 0, + "DispatchMissCount": 7, + "SerializationErrorCount": 0, + "DispatchQueueRejectedCount": 256 + } + } + ] +} \ No newline at end of file diff --git a/Docs/Performance/Data/2026-09-28/unity-all-editmode-results.json b/Docs/Performance/Data/2026-09-28/unity-all-editmode-results.json new file mode 100644 index 0000000..15fabb7 --- /dev/null +++ b/Docs/Performance/Data/2026-09-28/unity-all-editmode-results.json @@ -0,0 +1 @@ +{"success":true,"message":"Test job finished.","data":{"jobId":"fbffc55a-232f-4e3c-b8bf-c37f45dffb39","status":"finished","mode":"EditMode","startedAt":"2026-09-29 07:04:55","lastActivityAt":"2026-09-29 07:05:19","testsCompleted":226,"hasFilters":false,"totalTests":226,"finishedAt":"2026-09-29 07:05:19","resultState":"Passed","passCount":226,"failCount":0,"skipCount":0,"inconclusiveCount":0,"durationSeconds":18.53,"failures":[],"task":{"task_id":"tests:fbffc55a-232f-4e3c-b8bf-c37f45dffb39","kind":"tests","id":"fbffc55a-232f-4e3c-b8bf-c37f45dffb39","status":"finished","phase":"finished","revision":"e8f3bd9db3325822b1334b053ac352179b5b7cd20a295878c2506105b3c0f9ca","changed":true,"wait_complete":true,"poll_after_ms":0,"wait_reason":"completed","waited_ms":0,"snapshot_at":"2026-09-28T23:06:00.8696005Z"}}} diff --git a/Docs/Performance/Data/2026-09-28/unity-player-results.xml b/Docs/Performance/Data/2026-09-28/unity-player-results.xml new file mode 100644 index 0000000..4d87aff --- /dev/null +++ b/Docs/Performance/Data/2026-09-28/unity-player-results.xml @@ -0,0 +1,206 @@ +PLAYER Context 10000 retirements ms=123.6386 +PLAYER EventBus 10000 posts allocationEvents=10000 ms=0.8038 +PLAYER EventBus isolated 10000 posts allocationEvents=0 +[ShrinkDataSaver] 槽位 0 已保存。(2 个模块) +PLAYER Save 102400 background=False captureMs=11.8073 encodeMs=7.7169 encryptMs=0 writeMs=17.803 bytes=103171 +[ShrinkDataSaver] 槽位 0 已保存。(2 个模块) +PLAYER Save 102400 background=True captureMs=0.1186 encodeMs=0.2584 encryptMs=0 writeMs=29.4953 bytes=103171 +[ShrinkDataSaver] 槽位 0 已保存。(2 个模块) +PLAYER Save 1048576 background=False captureMs=1.3102 encodeMs=7.8701 encryptMs=0 writeMs=13.9663 bytes=1049347 +[ShrinkDataSaver] 槽位 0 已保存。(2 个模块) +PLAYER Save 1048576 background=True captureMs=0.1341 encodeMs=7.7226 encryptMs=0 writeMs=23.1749 bytes=1049347 +[ShrinkDataSaver] 槽位 0 已保存。(2 个模块) +PLAYER Save 10485760 background=False captureMs=0.1424 encodeMs=28.173 encryptMs=0 writeMs=32.9821 bytes=10486531 +[ShrinkDataSaver] 槽位 0 已保存。(2 个模块) +PLAYER Save 10485760 background=True captureMs=0.162 encodeMs=25.6417 encryptMs=0 writeMs=24.231 bytes=10486531 +PLAYER allocation calibration managedCounterDelta=0 profilerEvents=10; use profiler events, not uncalibrated bytes. +PLAYER PERF context-diagnostic/components=10 n=100 ops=71994.24 allocBytes=unsupported allocationSamples=10 allocEventsPerOp=135.00 p50us=13.00 p95us=20.20 p99us=25.60 +PLAYER PERF context-diagnostic/components=100 n=100 ops=6276.68 allocBytes=unsupported allocationSamples=10 allocEventsPerOp=1224.00 p50us=118.80 p95us=190.80 p99us=1980.20 +PLAYER PERF context-diagnostic/components=1000 n=100 ops=391.93 allocBytes=unsupported allocationSamples=10 allocEventsPerOp=12033.00 p50us=1841.90 p95us=5669.60 p99us=7090.60 +PLAYER PERF event-async-completed n=10000 ops=1531534.29 allocBytes=unsupported allocationSamples=10 allocEventsPerOp=3.00 p50us=0.30 p95us=0.60 p99us=1.10 +PLAYER PERF event-bindings n=1000 ops=3377237.42 allocBytes=unsupported allocationSamples=10 allocEventsPerOp=2.00 p50us=0.20 p95us=0.60 p99us=1.00 +PLAYER PERF event-register-withdraw n=10000 ops=725100.06 allocBytes=unsupported allocationSamples=10 allocEventsPerOp=10.00 p50us=0.80 p95us=1.70 p99us=2.40 +PLAYER PERF loopback-rpc/bytes=64/sessions=1 n=128 ops=42139.92 allocBytes=unsupported allocationSamples=10 allocEventsPerOp=100.00 p50us=22.40 p95us=32.20 p99us=35.80 +PLAYER PERF loopback-broadcast/bytes=64/sessions=1 n=20 ops=135777.33 allocBytes=unsupported allocationSamples=10 allocEventsPerOp=37.00 p50us=7.20 p95us=9.20 p99us=9.20 +PLAYER PERF loopback-rpc/bytes=64/sessions=32 n=128 ops=22296.16 allocBytes=unsupported allocationSamples=10 allocEventsPerOp=100.00 p50us=24.60 p95us=46.10 p99us=96.20 +PLAYER PERF loopback-broadcast/bytes=64/sessions=32 n=20 ops=3283.21 allocBytes=unsupported allocationSamples=10 allocEventsPerOp=1060.00 p50us=215.80 p95us=1999.10 p99us=1999.10 +PLAYER PERF loopback-rpc/bytes=64/sessions=256 n=128 ops=16764.02 allocBytes=unsupported allocationSamples=10 allocEventsPerOp=100.00 p50us=33.90 p95us=42.50 p99us=115.00 +PLAYER PERF loopback-broadcast/bytes=64/sessions=256 n=20 ops=331.02 allocBytes=unsupported allocationSamples=10 allocEventsPerOp=8452.00 p50us=3052.70 p95us=5858.40 p99us=5858.40 +PLAYER PERF loopback-rpc/bytes=1024/sessions=1 n=128 ops=13102.67 allocBytes=unsupported allocationSamples=10 allocEventsPerOp=102.00 p50us=27.70 p95us=302.40 p99us=1201.80 +PLAYER PERF loopback-broadcast/bytes=1024/sessions=1 n=20 ops=117164.62 allocBytes=unsupported allocationSamples=10 allocEventsPerOp=38.00 p50us=8.40 p95us=10.40 p99us=10.40 +PLAYER PERF loopback-rpc/bytes=1024/sessions=32 n=128 ops=15497.49 allocBytes=unsupported allocationSamples=10 allocEventsPerOp=102.00 p50us=27.60 p95us=62.90 p99us=1200.70 +PLAYER PERF loopback-broadcast/bytes=1024/sessions=32 n=20 ops=1648.17 allocBytes=unsupported allocationSamples=10 allocEventsPerOp=1092.00 p50us=361.60 p95us=1577.50 p99us=1577.50 +PLAYER PERF loopback-rpc/bytes=1024/sessions=256 n=128 ops=12226.92 allocBytes=unsupported allocationSamples=10 allocEventsPerOp=102.00 p50us=29.00 p95us=305.10 p99us=1402.90 +PLAYER PERF loopback-broadcast/bytes=1024/sessions=256 n=20 ops=194.15 allocBytes=unsupported allocationSamples=10 allocEventsPerOp=8708.00 p50us=4612.30 p95us=10631.00 p99us=10631.00 +PLAYER PERF loopback-rpc/bytes=32768/sessions=1 n=128 ops=828.26 allocBytes=unsupported allocationSamples=10 allocEventsPerOp=112.60 p50us=1123.40 p95us=3500.10 p99us=3837.10 +PLAYER PERF loopback-broadcast/bytes=32768/sessions=1 n=20 ops=1815.71 allocBytes=unsupported allocationSamples=10 allocEventsPerOp=43.00 p50us=98.60 p95us=3193.40 p99us=3193.40 +PLAYER PERF loopback-rpc/bytes=32768/sessions=32 n=128 ops=842.40 allocBytes=unsupported allocationSamples=10 allocEventsPerOp=112.00 p50us=1163.90 p95us=3475.30 p99us=3500.30 +PLAYER PERF loopback-broadcast/bytes=32768/sessions=32 n=20 ops=65.08 allocBytes=unsupported allocationSamples=10 allocEventsPerOp=1252.90 p50us=15096.30 p95us=26400.40 p99us=26400.40 +PLAYER PERF loopback-rpc/bytes=32768/sessions=256 n=128 ops=830.36 allocBytes=unsupported allocationSamples=10 allocEventsPerOp=112.60 p50us=1230.40 p95us=3500.80 p99us=3549.90 +PLAYER PERF loopback-broadcast/bytes=32768/sessions=256 n=20 ops=8.67 allocBytes=unsupported allocationSamples=10 allocEventsPerOp=9995.60 p50us=114364.50 p95us=124433.00 p99us=124433.00 +PLAYER Queue pending=32 bytes=32768 replaced=96 rejected=0 +PLAYER Context 10000 retirements ms=123.6386 +PLAYER EventBus 10000 posts allocationEvents=10000 ms=0.8038 +PLAYER EventBus isolated 10000 posts allocationEvents=0 +[ShrinkDataSaver] 槽位 0 已保存。(2 个模块) +PLAYER Save 102400 background=False captureMs=11.8073 encodeMs=7.7169 encryptMs=0 writeMs=17.803 bytes=103171 +[ShrinkDataSaver] 槽位 0 已保存。(2 个模块) +PLAYER Save 102400 background=True captureMs=0.1186 encodeMs=0.2584 encryptMs=0 writeMs=29.4953 bytes=103171 +[ShrinkDataSaver] 槽位 0 已保存。(2 个模块) +PLAYER Save 1048576 background=False captureMs=1.3102 encodeMs=7.8701 encryptMs=0 writeMs=13.9663 bytes=1049347 +[ShrinkDataSaver] 槽位 0 已保存。(2 个模块) +PLAYER Save 1048576 background=True captureMs=0.1341 encodeMs=7.7226 encryptMs=0 writeMs=23.1749 bytes=1049347 +[ShrinkDataSaver] 槽位 0 已保存。(2 个模块) +PLAYER Save 10485760 background=False captureMs=0.1424 encodeMs=28.173 encryptMs=0 writeMs=32.9821 bytes=10486531 +[ShrinkDataSaver] 槽位 0 已保存。(2 个模块) +PLAYER Save 10485760 background=True captureMs=0.162 encodeMs=25.6417 encryptMs=0 writeMs=24.231 bytes=10486531 +PLAYER allocation calibration managedCounterDelta=0 profilerEvents=10; use profiler events, not uncalibrated bytes. +PLAYER PERF context-diagnostic/components=10 n=100 ops=71994.24 allocBytes=unsupported allocationSamples=10 allocEventsPerOp=135.00 p50us=13.00 p95us=20.20 p99us=25.60 +PLAYER PERF context-diagnostic/components=100 n=100 ops=6276.68 allocBytes=unsupported allocationSamples=10 allocEventsPerOp=1224.00 p50us=118.80 p95us=190.80 p99us=1980.20 +PLAYER PERF context-diagnostic/components=1000 n=100 ops=391.93 allocBytes=unsupported allocationSamples=10 allocEventsPerOp=12033.00 p50us=1841.90 p95us=5669.60 p99us=7090.60 +PLAYER PERF event-async-completed n=10000 ops=1531534.29 allocBytes=unsupported allocationSamples=10 allocEventsPerOp=3.00 p50us=0.30 p95us=0.60 p99us=1.10 +PLAYER PERF event-bindings n=1000 ops=3377237.42 allocBytes=unsupported allocationSamples=10 allocEventsPerOp=2.00 p50us=0.20 p95us=0.60 p99us=1.00 +PLAYER PERF event-register-withdraw n=10000 ops=725100.06 allocBytes=unsupported allocationSamples=10 allocEventsPerOp=10.00 p50us=0.80 p95us=1.70 p99us=2.40 +PLAYER PERF loopback-rpc/bytes=64/sessions=1 n=128 ops=42139.92 allocBytes=unsupported allocationSamples=10 allocEventsPerOp=100.00 p50us=22.40 p95us=32.20 p99us=35.80 +PLAYER PERF loopback-broadcast/bytes=64/sessions=1 n=20 ops=135777.33 allocBytes=unsupported allocationSamples=10 allocEventsPerOp=37.00 p50us=7.20 p95us=9.20 p99us=9.20 +PLAYER PERF loopback-rpc/bytes=64/sessions=32 n=128 ops=22296.16 allocBytes=unsupported allocationSamples=10 allocEventsPerOp=100.00 p50us=24.60 p95us=46.10 p99us=96.20 +PLAYER PERF loopback-broadcast/bytes=64/sessions=32 n=20 ops=3283.21 allocBytes=unsupported allocationSamples=10 allocEventsPerOp=1060.00 p50us=215.80 p95us=1999.10 p99us=1999.10 +PLAYER PERF loopback-rpc/bytes=64/sessions=256 n=128 ops=16764.02 allocBytes=unsupported allocationSamples=10 allocEventsPerOp=100.00 p50us=33.90 p95us=42.50 p99us=115.00 +PLAYER PERF loopback-broadcast/bytes=64/sessions=256 n=20 ops=331.02 allocBytes=unsupported allocationSamples=10 allocEventsPerOp=8452.00 p50us=3052.70 p95us=5858.40 p99us=5858.40 +PLAYER PERF loopback-rpc/bytes=1024/sessions=1 n=128 ops=13102.67 allocBytes=unsupported allocationSamples=10 allocEventsPerOp=102.00 p50us=27.70 p95us=302.40 p99us=1201.80 +PLAYER PERF loopback-broadcast/bytes=1024/sessions=1 n=20 ops=117164.62 allocBytes=unsupported allocationSamples=10 allocEventsPerOp=38.00 p50us=8.40 p95us=10.40 p99us=10.40 +PLAYER PERF loopback-rpc/bytes=1024/sessions=32 n=128 ops=15497.49 allocBytes=unsupported allocationSamples=10 allocEventsPerOp=102.00 p50us=27.60 p95us=62.90 p99us=1200.70 +PLAYER PERF loopback-broadcast/bytes=1024/sessions=32 n=20 ops=1648.17 allocBytes=unsupported allocationSamples=10 allocEventsPerOp=1092.00 p50us=361.60 p95us=1577.50 p99us=1577.50 +PLAYER PERF loopback-rpc/bytes=1024/sessions=256 n=128 ops=12226.92 allocBytes=unsupported allocationSamples=10 allocEventsPerOp=102.00 p50us=29.00 p95us=305.10 p99us=1402.90 +PLAYER PERF loopback-broadcast/bytes=1024/sessions=256 n=20 ops=194.15 allocBytes=unsupported allocationSamples=10 allocEventsPerOp=8708.00 p50us=4612.30 p95us=10631.00 p99us=10631.00 +PLAYER PERF loopback-rpc/bytes=32768/sessions=1 n=128 ops=828.26 allocBytes=unsupported allocationSamples=10 allocEventsPerOp=112.60 p50us=1123.40 p95us=3500.10 p99us=3837.10 +PLAYER PERF loopback-broadcast/bytes=32768/sessions=1 n=20 ops=1815.71 allocBytes=unsupported allocationSamples=10 allocEventsPerOp=43.00 p50us=98.60 p95us=3193.40 p99us=3193.40 +PLAYER PERF loopback-rpc/bytes=32768/sessions=32 n=128 ops=842.40 allocBytes=unsupported allocationSamples=10 allocEventsPerOp=112.00 p50us=1163.90 p95us=3475.30 p99us=3500.30 +PLAYER PERF loopback-broadcast/bytes=32768/sessions=32 n=20 ops=65.08 allocBytes=unsupported allocationSamples=10 allocEventsPerOp=1252.90 p50us=15096.30 p95us=26400.40 p99us=26400.40 +PLAYER PERF loopback-rpc/bytes=32768/sessions=256 n=128 ops=830.36 allocBytes=unsupported allocationSamples=10 allocEventsPerOp=112.60 p50us=1230.40 p95us=3500.80 p99us=3549.90 +PLAYER PERF loopback-broadcast/bytes=32768/sessions=256 n=20 ops=8.67 allocBytes=unsupported allocationSamples=10 allocEventsPerOp=9995.60 p50us=114364.50 p95us=124433.00 p99us=124433.00 +PLAYER Queue pending=32 bytes=32768 replaced=96 rejected=0 +PLAYER Context 10000 retirements ms=123.6386 +PLAYER EventBus 10000 posts allocationEvents=10000 ms=0.8038 +PLAYER EventBus isolated 10000 posts allocationEvents=0 +[ShrinkDataSaver] 槽位 0 已保存。(2 个模块) +PLAYER Save 102400 background=False captureMs=11.8073 encodeMs=7.7169 encryptMs=0 writeMs=17.803 bytes=103171 +[ShrinkDataSaver] 槽位 0 已保存。(2 个模块) +PLAYER Save 102400 background=True captureMs=0.1186 encodeMs=0.2584 encryptMs=0 writeMs=29.4953 bytes=103171 +[ShrinkDataSaver] 槽位 0 已保存。(2 个模块) +PLAYER Save 1048576 background=False captureMs=1.3102 encodeMs=7.8701 encryptMs=0 writeMs=13.9663 bytes=1049347 +[ShrinkDataSaver] 槽位 0 已保存。(2 个模块) +PLAYER Save 1048576 background=True captureMs=0.1341 encodeMs=7.7226 encryptMs=0 writeMs=23.1749 bytes=1049347 +[ShrinkDataSaver] 槽位 0 已保存。(2 个模块) +PLAYER Save 10485760 background=False captureMs=0.1424 encodeMs=28.173 encryptMs=0 writeMs=32.9821 bytes=10486531 +[ShrinkDataSaver] 槽位 0 已保存。(2 个模块) +PLAYER Save 10485760 background=True captureMs=0.162 encodeMs=25.6417 encryptMs=0 writeMs=24.231 bytes=10486531 +PLAYER Context 10000 retirements ms=123.6386 +PLAYER Context 10000 retirements ms=123.6386 +PLAYER EventBus 10000 posts allocationEvents=10000 ms=0.8038 +PLAYER EventBus isolated 10000 posts allocationEvents=0 +PLAYER EventBus 10000 posts allocationEvents=10000 ms=0.8038 +PLAYER EventBus isolated 10000 posts allocationEvents=0 +[ShrinkDataSaver] 槽位 0 已保存。(2 个模块) +PLAYER Save 102400 background=False captureMs=11.8073 encodeMs=7.7169 encryptMs=0 writeMs=17.803 bytes=103171 +[ShrinkDataSaver] 槽位 0 已保存。(2 个模块) +PLAYER Save 102400 background=True captureMs=0.1186 encodeMs=0.2584 encryptMs=0 writeMs=29.4953 bytes=103171 +[ShrinkDataSaver] 槽位 0 已保存。(2 个模块) +PLAYER Save 1048576 background=False captureMs=1.3102 encodeMs=7.8701 encryptMs=0 writeMs=13.9663 bytes=1049347 +[ShrinkDataSaver] 槽位 0 已保存。(2 个模块) +PLAYER Save 1048576 background=True captureMs=0.1341 encodeMs=7.7226 encryptMs=0 writeMs=23.1749 bytes=1049347 +[ShrinkDataSaver] 槽位 0 已保存。(2 个模块) +PLAYER Save 10485760 background=False captureMs=0.1424 encodeMs=28.173 encryptMs=0 writeMs=32.9821 bytes=10486531 +[ShrinkDataSaver] 槽位 0 已保存。(2 个模块) +PLAYER Save 10485760 background=True captureMs=0.162 encodeMs=25.6417 encryptMs=0 writeMs=24.231 bytes=10486531 +[ShrinkDataSaver] 槽位 0 已保存。(2 个模块) +PLAYER Save 102400 background=False captureMs=11.8073 encodeMs=7.7169 encryptMs=0 writeMs=17.803 bytes=103171 +[ShrinkDataSaver] 槽位 0 已保存。(2 个模块) +PLAYER Save 102400 background=True captureMs=0.1186 encodeMs=0.2584 encryptMs=0 writeMs=29.4953 bytes=103171 +[ShrinkDataSaver] 槽位 0 已保存。(2 个模块) +PLAYER Save 1048576 background=False captureMs=1.3102 encodeMs=7.8701 encryptMs=0 writeMs=13.9663 bytes=1049347 +[ShrinkDataSaver] 槽位 0 已保存。(2 个模块) +PLAYER Save 1048576 background=True captureMs=0.1341 encodeMs=7.7226 encryptMs=0 writeMs=23.1749 bytes=1049347 +[ShrinkDataSaver] 槽位 0 已保存。(2 个模块) +PLAYER Save 10485760 background=False captureMs=0.1424 encodeMs=28.173 encryptMs=0 writeMs=32.9821 bytes=10486531 +[ShrinkDataSaver] 槽位 0 已保存。(2 个模块) +PLAYER Save 10485760 background=True captureMs=0.162 encodeMs=25.6417 encryptMs=0 writeMs=24.231 bytes=10486531 +PLAYER allocation calibration managedCounterDelta=0 profilerEvents=10; use profiler events, not uncalibrated bytes. +PLAYER PERF context-diagnostic/components=10 n=100 ops=71994.24 allocBytes=unsupported allocationSamples=10 allocEventsPerOp=135.00 p50us=13.00 p95us=20.20 p99us=25.60 +PLAYER PERF context-diagnostic/components=100 n=100 ops=6276.68 allocBytes=unsupported allocationSamples=10 allocEventsPerOp=1224.00 p50us=118.80 p95us=190.80 p99us=1980.20 +PLAYER PERF context-diagnostic/components=1000 n=100 ops=391.93 allocBytes=unsupported allocationSamples=10 allocEventsPerOp=12033.00 p50us=1841.90 p95us=5669.60 p99us=7090.60 +PLAYER PERF event-async-completed n=10000 ops=1531534.29 allocBytes=unsupported allocationSamples=10 allocEventsPerOp=3.00 p50us=0.30 p95us=0.60 p99us=1.10 +PLAYER PERF event-bindings n=1000 ops=3377237.42 allocBytes=unsupported allocationSamples=10 allocEventsPerOp=2.00 p50us=0.20 p95us=0.60 p99us=1.00 +PLAYER PERF event-register-withdraw n=10000 ops=725100.06 allocBytes=unsupported allocationSamples=10 allocEventsPerOp=10.00 p50us=0.80 p95us=1.70 p99us=2.40 +PLAYER PERF loopback-rpc/bytes=64/sessions=1 n=128 ops=42139.92 allocBytes=unsupported allocationSamples=10 allocEventsPerOp=100.00 p50us=22.40 p95us=32.20 p99us=35.80 +PLAYER PERF loopback-broadcast/bytes=64/sessions=1 n=20 ops=135777.33 allocBytes=unsupported allocationSamples=10 allocEventsPerOp=37.00 p50us=7.20 p95us=9.20 p99us=9.20 +PLAYER PERF loopback-rpc/bytes=64/sessions=32 n=128 ops=22296.16 allocBytes=unsupported allocationSamples=10 allocEventsPerOp=100.00 p50us=24.60 p95us=46.10 p99us=96.20 +PLAYER PERF loopback-broadcast/bytes=64/sessions=32 n=20 ops=3283.21 allocBytes=unsupported allocationSamples=10 allocEventsPerOp=1060.00 p50us=215.80 p95us=1999.10 p99us=1999.10 +PLAYER PERF loopback-rpc/bytes=64/sessions=256 n=128 ops=16764.02 allocBytes=unsupported allocationSamples=10 allocEventsPerOp=100.00 p50us=33.90 p95us=42.50 p99us=115.00 +PLAYER PERF loopback-broadcast/bytes=64/sessions=256 n=20 ops=331.02 allocBytes=unsupported allocationSamples=10 allocEventsPerOp=8452.00 p50us=3052.70 p95us=5858.40 p99us=5858.40 +PLAYER PERF loopback-rpc/bytes=1024/sessions=1 n=128 ops=13102.67 allocBytes=unsupported allocationSamples=10 allocEventsPerOp=102.00 p50us=27.70 p95us=302.40 p99us=1201.80 +PLAYER PERF loopback-broadcast/bytes=1024/sessions=1 n=20 ops=117164.62 allocBytes=unsupported allocationSamples=10 allocEventsPerOp=38.00 p50us=8.40 p95us=10.40 p99us=10.40 +PLAYER PERF loopback-rpc/bytes=1024/sessions=32 n=128 ops=15497.49 allocBytes=unsupported allocationSamples=10 allocEventsPerOp=102.00 p50us=27.60 p95us=62.90 p99us=1200.70 +PLAYER PERF loopback-broadcast/bytes=1024/sessions=32 n=20 ops=1648.17 allocBytes=unsupported allocationSamples=10 allocEventsPerOp=1092.00 p50us=361.60 p95us=1577.50 p99us=1577.50 +PLAYER PERF loopback-rpc/bytes=1024/sessions=256 n=128 ops=12226.92 allocBytes=unsupported allocationSamples=10 allocEventsPerOp=102.00 p50us=29.00 p95us=305.10 p99us=1402.90 +PLAYER PERF loopback-broadcast/bytes=1024/sessions=256 n=20 ops=194.15 allocBytes=unsupported allocationSamples=10 allocEventsPerOp=8708.00 p50us=4612.30 p95us=10631.00 p99us=10631.00 +PLAYER PERF loopback-rpc/bytes=32768/sessions=1 n=128 ops=828.26 allocBytes=unsupported allocationSamples=10 allocEventsPerOp=112.60 p50us=1123.40 p95us=3500.10 p99us=3837.10 +PLAYER PERF loopback-broadcast/bytes=32768/sessions=1 n=20 ops=1815.71 allocBytes=unsupported allocationSamples=10 allocEventsPerOp=43.00 p50us=98.60 p95us=3193.40 p99us=3193.40 +PLAYER PERF loopback-rpc/bytes=32768/sessions=32 n=128 ops=842.40 allocBytes=unsupported allocationSamples=10 allocEventsPerOp=112.00 p50us=1163.90 p95us=3475.30 p99us=3500.30 +PLAYER PERF loopback-broadcast/bytes=32768/sessions=32 n=20 ops=65.08 allocBytes=unsupported allocationSamples=10 allocEventsPerOp=1252.90 p50us=15096.30 p95us=26400.40 p99us=26400.40 +PLAYER PERF loopback-rpc/bytes=32768/sessions=256 n=128 ops=830.36 allocBytes=unsupported allocationSamples=10 allocEventsPerOp=112.60 p50us=1230.40 p95us=3500.80 p99us=3549.90 +PLAYER PERF loopback-broadcast/bytes=32768/sessions=256 n=20 ops=8.67 allocBytes=unsupported allocationSamples=10 allocEventsPerOp=9995.60 p50us=114364.50 p95us=124433.00 p99us=124433.00 +PLAYER Queue pending=32 bytes=32768 replaced=96 rejected=0 +PLAYER allocation calibration managedCounterDelta=0 profilerEvents=10; use profiler events, not uncalibrated bytes. +PLAYER allocation calibration managedCounterDelta=0 profilerEvents=10; use profiler events, not uncalibrated bytes. +PLAYER PERF context-diagnostic/components=10 n=100 ops=71994.24 allocBytes=unsupported allocationSamples=10 allocEventsPerOp=135.00 p50us=13.00 p95us=20.20 p99us=25.60 +PLAYER PERF context-diagnostic/components=100 n=100 ops=6276.68 allocBytes=unsupported allocationSamples=10 allocEventsPerOp=1224.00 p50us=118.80 p95us=190.80 p99us=1980.20 +PLAYER PERF context-diagnostic/components=1000 n=100 ops=391.93 allocBytes=unsupported allocationSamples=10 allocEventsPerOp=12033.00 p50us=1841.90 p95us=5669.60 p99us=7090.60 +PLAYER PERF event-async-completed n=10000 ops=1531534.29 allocBytes=unsupported allocationSamples=10 allocEventsPerOp=3.00 p50us=0.30 p95us=0.60 p99us=1.10 +PLAYER PERF event-bindings n=1000 ops=3377237.42 allocBytes=unsupported allocationSamples=10 allocEventsPerOp=2.00 p50us=0.20 p95us=0.60 p99us=1.00 +PLAYER PERF event-register-withdraw n=10000 ops=725100.06 allocBytes=unsupported allocationSamples=10 allocEventsPerOp=10.00 p50us=0.80 p95us=1.70 p99us=2.40 +PLAYER PERF context-diagnostic/components=10 n=100 ops=71994.24 allocBytes=unsupported allocationSamples=10 allocEventsPerOp=135.00 p50us=13.00 p95us=20.20 p99us=25.60 +PLAYER PERF context-diagnostic/components=100 n=100 ops=6276.68 allocBytes=unsupported allocationSamples=10 allocEventsPerOp=1224.00 p50us=118.80 p95us=190.80 p99us=1980.20 +PLAYER PERF context-diagnostic/components=1000 n=100 ops=391.93 allocBytes=unsupported allocationSamples=10 allocEventsPerOp=12033.00 p50us=1841.90 p95us=5669.60 p99us=7090.60 +PLAYER PERF event-async-completed n=10000 ops=1531534.29 allocBytes=unsupported allocationSamples=10 allocEventsPerOp=3.00 p50us=0.30 p95us=0.60 p99us=1.10 +PLAYER PERF event-bindings n=1000 ops=3377237.42 allocBytes=unsupported allocationSamples=10 allocEventsPerOp=2.00 p50us=0.20 p95us=0.60 p99us=1.00 +PLAYER PERF event-register-withdraw n=10000 ops=725100.06 allocBytes=unsupported allocationSamples=10 allocEventsPerOp=10.00 p50us=0.80 p95us=1.70 p99us=2.40 +PLAYER PERF loopback-rpc/bytes=64/sessions=1 n=128 ops=42139.92 allocBytes=unsupported allocationSamples=10 allocEventsPerOp=100.00 p50us=22.40 p95us=32.20 p99us=35.80 +PLAYER PERF loopback-broadcast/bytes=64/sessions=1 n=20 ops=135777.33 allocBytes=unsupported allocationSamples=10 allocEventsPerOp=37.00 p50us=7.20 p95us=9.20 p99us=9.20 +PLAYER PERF loopback-rpc/bytes=64/sessions=32 n=128 ops=22296.16 allocBytes=unsupported allocationSamples=10 allocEventsPerOp=100.00 p50us=24.60 p95us=46.10 p99us=96.20 +PLAYER PERF loopback-broadcast/bytes=64/sessions=32 n=20 ops=3283.21 allocBytes=unsupported allocationSamples=10 allocEventsPerOp=1060.00 p50us=215.80 p95us=1999.10 p99us=1999.10 +PLAYER PERF loopback-rpc/bytes=64/sessions=256 n=128 ops=16764.02 allocBytes=unsupported allocationSamples=10 allocEventsPerOp=100.00 p50us=33.90 p95us=42.50 p99us=115.00 +PLAYER PERF loopback-broadcast/bytes=64/sessions=256 n=20 ops=331.02 allocBytes=unsupported allocationSamples=10 allocEventsPerOp=8452.00 p50us=3052.70 p95us=5858.40 p99us=5858.40 +PLAYER PERF loopback-rpc/bytes=1024/sessions=1 n=128 ops=13102.67 allocBytes=unsupported allocationSamples=10 allocEventsPerOp=102.00 p50us=27.70 p95us=302.40 p99us=1201.80 +PLAYER PERF loopback-broadcast/bytes=1024/sessions=1 n=20 ops=117164.62 allocBytes=unsupported allocationSamples=10 allocEventsPerOp=38.00 p50us=8.40 p95us=10.40 p99us=10.40 +PLAYER PERF loopback-rpc/bytes=1024/sessions=32 n=128 ops=15497.49 allocBytes=unsupported allocationSamples=10 allocEventsPerOp=102.00 p50us=27.60 p95us=62.90 p99us=1200.70 +PLAYER PERF loopback-broadcast/bytes=1024/sessions=32 n=20 ops=1648.17 allocBytes=unsupported allocationSamples=10 allocEventsPerOp=1092.00 p50us=361.60 p95us=1577.50 p99us=1577.50 +PLAYER PERF loopback-rpc/bytes=1024/sessions=256 n=128 ops=12226.92 allocBytes=unsupported allocationSamples=10 allocEventsPerOp=102.00 p50us=29.00 p95us=305.10 p99us=1402.90 +PLAYER PERF loopback-broadcast/bytes=1024/sessions=256 n=20 ops=194.15 allocBytes=unsupported allocationSamples=10 allocEventsPerOp=8708.00 p50us=4612.30 p95us=10631.00 p99us=10631.00 +PLAYER PERF loopback-rpc/bytes=32768/sessions=1 n=128 ops=828.26 allocBytes=unsupported allocationSamples=10 allocEventsPerOp=112.60 p50us=1123.40 p95us=3500.10 p99us=3837.10 +PLAYER PERF loopback-broadcast/bytes=32768/sessions=1 n=20 ops=1815.71 allocBytes=unsupported allocationSamples=10 allocEventsPerOp=43.00 p50us=98.60 p95us=3193.40 p99us=3193.40 +PLAYER PERF loopback-rpc/bytes=32768/sessions=32 n=128 ops=842.40 allocBytes=unsupported allocationSamples=10 allocEventsPerOp=112.00 p50us=1163.90 p95us=3475.30 p99us=3500.30 +PLAYER PERF loopback-broadcast/bytes=32768/sessions=32 n=20 ops=65.08 allocBytes=unsupported allocationSamples=10 allocEventsPerOp=1252.90 p50us=15096.30 p95us=26400.40 p99us=26400.40 +PLAYER PERF loopback-rpc/bytes=32768/sessions=256 n=128 ops=830.36 allocBytes=unsupported allocationSamples=10 allocEventsPerOp=112.60 p50us=1230.40 p95us=3500.80 p99us=3549.90 +PLAYER PERF loopback-broadcast/bytes=32768/sessions=256 n=20 ops=8.67 allocBytes=unsupported allocationSamples=10 allocEventsPerOp=9995.60 p50us=114364.50 p95us=124433.00 p99us=124433.00 +PLAYER PERF loopback-rpc/bytes=64/sessions=1 n=128 ops=42139.92 allocBytes=unsupported allocationSamples=10 allocEventsPerOp=100.00 p50us=22.40 p95us=32.20 p99us=35.80 +PLAYER PERF loopback-broadcast/bytes=64/sessions=1 n=20 ops=135777.33 allocBytes=unsupported allocationSamples=10 allocEventsPerOp=37.00 p50us=7.20 p95us=9.20 p99us=9.20 +PLAYER PERF loopback-rpc/bytes=64/sessions=32 n=128 ops=22296.16 allocBytes=unsupported allocationSamples=10 allocEventsPerOp=100.00 p50us=24.60 p95us=46.10 p99us=96.20 +PLAYER PERF loopback-broadcast/bytes=64/sessions=32 n=20 ops=3283.21 allocBytes=unsupported allocationSamples=10 allocEventsPerOp=1060.00 p50us=215.80 p95us=1999.10 p99us=1999.10 +PLAYER PERF loopback-rpc/bytes=64/sessions=256 n=128 ops=16764.02 allocBytes=unsupported allocationSamples=10 allocEventsPerOp=100.00 p50us=33.90 p95us=42.50 p99us=115.00 +PLAYER PERF loopback-broadcast/bytes=64/sessions=256 n=20 ops=331.02 allocBytes=unsupported allocationSamples=10 allocEventsPerOp=8452.00 p50us=3052.70 p95us=5858.40 p99us=5858.40 +PLAYER PERF loopback-rpc/bytes=1024/sessions=1 n=128 ops=13102.67 allocBytes=unsupported allocationSamples=10 allocEventsPerOp=102.00 p50us=27.70 p95us=302.40 p99us=1201.80 +PLAYER PERF loopback-broadcast/bytes=1024/sessions=1 n=20 ops=117164.62 allocBytes=unsupported allocationSamples=10 allocEventsPerOp=38.00 p50us=8.40 p95us=10.40 p99us=10.40 +PLAYER PERF loopback-rpc/bytes=1024/sessions=32 n=128 ops=15497.49 allocBytes=unsupported allocationSamples=10 allocEventsPerOp=102.00 p50us=27.60 p95us=62.90 p99us=1200.70 +PLAYER PERF loopback-broadcast/bytes=1024/sessions=32 n=20 ops=1648.17 allocBytes=unsupported allocationSamples=10 allocEventsPerOp=1092.00 p50us=361.60 p95us=1577.50 p99us=1577.50 +PLAYER PERF loopback-rpc/bytes=1024/sessions=256 n=128 ops=12226.92 allocBytes=unsupported allocationSamples=10 allocEventsPerOp=102.00 p50us=29.00 p95us=305.10 p99us=1402.90 +PLAYER PERF loopback-broadcast/bytes=1024/sessions=256 n=20 ops=194.15 allocBytes=unsupported allocationSamples=10 allocEventsPerOp=8708.00 p50us=4612.30 p95us=10631.00 p99us=10631.00 +PLAYER PERF loopback-rpc/bytes=32768/sessions=1 n=128 ops=828.26 allocBytes=unsupported allocationSamples=10 allocEventsPerOp=112.60 p50us=1123.40 p95us=3500.10 p99us=3837.10 +PLAYER PERF loopback-broadcast/bytes=32768/sessions=1 n=20 ops=1815.71 allocBytes=unsupported allocationSamples=10 allocEventsPerOp=43.00 p50us=98.60 p95us=3193.40 p99us=3193.40 +PLAYER PERF loopback-rpc/bytes=32768/sessions=32 n=128 ops=842.40 allocBytes=unsupported allocationSamples=10 allocEventsPerOp=112.00 p50us=1163.90 p95us=3475.30 p99us=3500.30 +PLAYER PERF loopback-broadcast/bytes=32768/sessions=32 n=20 ops=65.08 allocBytes=unsupported allocationSamples=10 allocEventsPerOp=1252.90 p50us=15096.30 p95us=26400.40 p99us=26400.40 +PLAYER PERF loopback-rpc/bytes=32768/sessions=256 n=128 ops=830.36 allocBytes=unsupported allocationSamples=10 allocEventsPerOp=112.60 p50us=1230.40 p95us=3500.80 p99us=3549.90 +PLAYER PERF loopback-broadcast/bytes=32768/sessions=256 n=20 ops=8.67 allocBytes=unsupported allocationSamples=10 allocEventsPerOp=9995.60 p50us=114364.50 p95us=124433.00 p99us=124433.00 +PLAYER Queue pending=32 bytes=32768 replaced=96 rejected=0 +PLAYER Queue pending=32 bytes=32768 replaced=96 rejected=0 + \ No newline at end of file diff --git a/Docs/Performance/Data/2026-09-28/waiting-query.json b/Docs/Performance/Data/2026-09-28/waiting-query.json new file mode 100644 index 0000000..738563d --- /dev/null +++ b/Docs/Performance/Data/2026-09-28/waiting-query.json @@ -0,0 +1,71 @@ +{ + "success": true, + "formatVersion": 1, + "toolVersion": "0.1.2.0", + "command": "flow", + "query": "key:acceptance.missing", + "buildId": "948c506123180e66b99b27e65945088a722988b7be5c5548faeaa46a58966104", + "offset": 0, + "limit": 20, + "nextOffset": null, + "coverage": [ + "Installed ShrinkSDK package sources are included; other external packages are reference-only.", + "Snapshot: Default Context and named EventBus instances only; host-owned instances require CaptureHostState registration." + ], + "coverageTruncated": false, + "semanticCoverage": { + "incompleteCount": 10, + "examples": [ + "Editor", + "Runtime", + "Core", + "Editor", + "Rules" + ] + }, + "validation": [], + "total": 2, + "truncated": false, + "items": [ + { + "From": "fiber:acceptance-waiting:1", + "To": "key:acceptance.missing", + "Kind": "requires@acceptance.missing [acceptance-waiting]", + "Evidence": "runtime-bound", + "Location": { + "File": "D:\\UnityBuilds\\ShrinkSDK\\Artifacts\\Inspection\\runtime.json", + "Line": 1 + } + }, + { + "Scope": "acceptance-waiting", + "Name": "Fibers", + "Value": { + "Uid": 1, + "Name": "acceptance-waiter", + "Status": 1, + "Retired": false, + "InTransition": false, + "Inject": [ + "acceptance.missing" + ], + "Provide": [], + "Dependencies": [ + { + "Key": "acceptance.missing", + "Realm": "acceptance.missing", + "CurrentProviderUid": null, + "CommittedProviderUid": null, + "PotentialProviderUids": [], + "IsSatisfied": false + } + ], + "ErrorType": null + }, + "Location": { + "File": "D:\\UnityBuilds\\ShrinkSDK\\Artifacts\\Inspection\\runtime.json", + "Line": 1 + } + } + ] +} diff --git a/Docs/ReuseFirstDesign.md b/Docs/ReuseFirstDesign.md new file mode 100644 index 0000000..7478c48 --- /dev/null +++ b/Docs/ReuseFirstDesign.md @@ -0,0 +1,29 @@ +# 新系统与玩法内容:先查可复用能力 + +仅在新增系统、玩法内容或扩展机制时使用本文。普通修复先定位受影响代码。 + +1. 用一句话明确目标行为和约束;不要增加未提出的玩法联动。 +2. 执行 `shrink inspect capabilities "关键词" --root <项目>`。先读最多三个候选的用途、限制、扩展点和证据,再查选中模块的 `symbol` / `flow`。中文使用项目业务词,例如 `存档`、`状态`、`属性`;不同叫法可另查别名。 +3. 按 **配置 → 组合 → 扩展已有机制 → 新模块** 判断。候选命中不是复用证明;对照真实接口、实例和行为测试。资料缺失不能解释成能力不存在。 +4. 确认状态归属、操作入口、结构化结果和退出时的撤销。只展开实际相关的持久化、联机、UI 或生命周期约束。 +5. 明确列出已存在、本次新增和待确认。不要为复用绕开权限、反向依赖模块、用无结果事件执行需要裁决的操作,或把大量胶水代码包装成零新增能力。 + +默认输出四项:复用方案及证据、必要改动、协作关系、影响与验证。只有会改变结果而无法从项目推导的选择才询问用户。 + +例如击杀怒气机制,需要分别核对击杀事实、计量、条件触发、属性修饰及效果撤回。存在 EventBus 不证明这些业务能力已经存在;存在存档功能也不意味着这次怒气必须持久化。 + +## 能力说明格式 + +在模块现有 README 中只保留一个区段。说明业务含义,不重复生成的 API 签名、依赖和版本。 + +```text + +Capability: 能实现的行为 +Aliases: 常见中文叫法 English aliases +Limits: 明确不支持的行为或成立条件 +Extension: 可配置或组合的位置;需要新增代码的边界 +Evidence: 相对本 README 的源码、配置类型和可运行示例路径 + +``` + +框架维护 SDK 能力,消费项目维护自己的业务能力。业务目录即使共享 asmdef 也可以声明能力。不得假定所有项目都有背包、战斗、任务或属性系统。 diff --git a/Godot/Packages/ShrinkSDK.Godot b/Godot/Packages/ShrinkSDK.Godot index 77adb61..e355bbf 160000 --- a/Godot/Packages/ShrinkSDK.Godot +++ b/Godot/Packages/ShrinkSDK.Godot @@ -1 +1 @@ -Subproject commit 77adb61b3b0f719a223a104b45ecf46c7a35eb2a +Subproject commit e355bbfd355048fc6bb9369f90ffaf2db662b18a diff --git a/Godot/Samples/ShrinkSDK.Godot.Sample/SampleRoot.cs b/Godot/Samples/ShrinkSDK.Godot.Sample/SampleRoot.cs index 56d33de..453f4de 100644 --- a/Godot/Samples/ShrinkSDK.Godot.Sample/SampleRoot.cs +++ b/Godot/Samples/ShrinkSDK.Godot.Sample/SampleRoot.cs @@ -25,6 +25,12 @@ public partial class SampleRoot : ShrinkGodotHost private async UniTaskVoid RunSmokeAsync() { var marker = RequireWoven(GetType().Assembly); + var goldenPath = ProjectSettings.GlobalizePath("res://../../../Assets/AgentSupportTests/Runtime/Resources/ShrinkV2Golden.bytes"); + var golden = System.IO.File.ReadAllBytes(goldenPath); + var codec = new ShrinkJsonNetworkSerializer(); + var decoded = (CrossHostMessage)codec.Deserialize(ShrinkPacketCodec.Decode(golden).Payload, typeof(CrossHostMessage)); + using (var encoded = ShrinkPacketCodec.Encode(new ShrinkNetworkPacket { Opcode = 42, Route = "跨宿主" }, codec, decoded)) + if (decoded.Text != "跨宿主-v2" || !encoded.WrittenMemory.ToArray().SequenceEqual(golden)) throw new InvalidOperationException("Cross-host v2 frame differs."); using var binding = EventBus.Attach(new GodotSubscriber()); EventBus.Post(new GodotSmokeEvent()); var assembly = GetType().Assembly; @@ -122,3 +128,5 @@ public sealed class GodotInstaller : IShrinkAppModuleInstaller return UniTask.CompletedTask; } } + +public sealed class CrossHostMessage : IShrinkNetworkMessage { public string Text { get; set; } = ""; } diff --git a/Godot/Tests/NuGetConsumer/NuGetConsumer.csproj b/Godot/Tests/NuGetConsumer/NuGetConsumer.csproj index b58691f..b3c734d 100644 --- a/Godot/Tests/NuGetConsumer/NuGetConsumer.csproj +++ b/Godot/Tests/NuGetConsumer/NuGetConsumer.csproj @@ -5,8 +5,8 @@ ShrinkSDK.NuGetConsumer - - - + + + diff --git a/README.md b/README.md index 9ff7e85..1ce20f4 100644 --- a/README.md +++ b/README.md @@ -4,6 +4,13 @@ ShrinkSDK 是一组可独立组合的 Unity / Godot C# 运行时模块。共享 Workspace 用于跨包集成、Unity 场景验证、Godot 消费者验证和 CodeGen 织入测试。每个 `Assets/Modules/` 是独立公开仓库的 Git submodule;根仓库保留相邻 `.meta`,不要删除或重新生成这些文件。 +## 本地导航与升级 + +- [shrink inspect 使用入口](Tools/AgentSupport/README.md):模块、能力、符号、调用关系、变更影响与确定性检查。 +- [新增系统/玩法的复用判断](Docs/ReuseFirstDesign.md):按需读取,不要求普通修复加载整套设计资料。 +- [可编译接入示例](Docs/IntegrationExamples.md)、[v2 协议与 API 迁移](Docs/NetworkV2Migration.md)。 +- [性能与跨宿主验证](Docs/Performance/2026-09-28.md)。本次版本为本地可分发包,尚未发布到远程 registry。 + ## 获取 Workspace ```text @@ -120,3 +127,5 @@ public partial class GameRoot : ShrinkGodotHost ## 第三方与合规 概念参考(NeoForge / Minecraft Forge、MessagePipe)与随仓库分发的第三方二进制(Kcp-CSharp.dll 等)的许可证遵从情况见 [THIRD-PARTY-NOTICES.md](THIRD-PARTY-NOTICES.md)。 + +本次本地交付的验收结果与覆盖限制见 [Agent 支持验收](Docs/AgentSupportAcceptance.md)。 diff --git a/Tools/AgentSupport/Acceptance.md b/Tools/AgentSupport/Acceptance.md new file mode 100644 index 0000000..1282978 --- /dev/null +++ b/Tools/AgentSupport/Acceptance.md @@ -0,0 +1,25 @@ +# Agent 验收任务 + +用冷上下文开始,每项只给需求与 CLI 使用入口。记录查询次数、无关文件读取、事实错误、无必要新增代码和最终验证点;不得把关键词命中算作完成。 + +| 导航任务 | 应找到的证据 | +|---|---| +| 启动应用 | Starter 组合与 LoaderHost | +| 事件未触发 | 发布、订阅以及是否实际绑定的区别 | +| 远程命令 | command/execute、Network handler、Command 执行 | +| provider 缺失 | Context key、候选 provider、运行态等待 | +| 存档入口 | ShrinkSave、模块采集、存储实现 | +| 模组退役 | Mod host、fiber、effect 撤回 | +| Godot 主线程派发 | ShrinkGodotHost 和主线程队列 | +| 修改网络服务的影响 | 直接调用、桥接和相关验证入口 | + +精确查询通常应在三次命令内到达关键链路;若索引覆盖不足,应明确说明并查看原始证据。 + +| 复用任务 | 正确改动层级 | +|---|---| +| 默认网络派发每帧数量调整 | 先查宿主已有配置,不新写调度器 | +| Context 卸载时取消事件订阅 | 组合已有 Context EventBus adapter,不新写全局订阅生命周期 | +| 为新存储介质保存既有槽位 | 实现 IStorageProvider,并确认宿主初始化注入入口;缺口需要窄扩展,沿用存档和迁移机制 | +| 没有任何业务模块的新怒气玩法 | 承认计量/触发等业务缺口;可复用基础设施但不假称有完整玩法 | + +真实消费项目的业务能力由该项目提供,不从上述 SDK 示例推断其存在。新 CLI 的静态回归测试不能替代冷上下文 agent 验收。 diff --git a/Tools/AgentSupport/Benchmarks/Benchmarks.csproj b/Tools/AgentSupport/Benchmarks/Benchmarks.csproj new file mode 100644 index 0000000..bdf86a1 --- /dev/null +++ b/Tools/AgentSupport/Benchmarks/Benchmarks.csproj @@ -0,0 +1,12 @@ + + + Exe + net8.0;net10.0 + false + + + + + + + diff --git a/Tools/AgentSupport/Benchmarks/Prepare-Baseline.ps1 b/Tools/AgentSupport/Benchmarks/Prepare-Baseline.ps1 new file mode 100644 index 0000000..b5e032e --- /dev/null +++ b/Tools/AgentSupport/Benchmarks/Prepare-Baseline.ps1 @@ -0,0 +1,14 @@ +$ErrorActionPreference = 'Stop' +$root = [IO.Path]::GetFullPath((Join-Path $PSScriptRoot '../../..')) +$destination = Join-Path $root 'Artifacts/AgentSupport/BaselineContext' +New-Item -ItemType Directory -Force -Path $destination | Out-Null +$module = Join-Path $root 'Assets/Modules/ShrinkContext.Core' +$files = git -C $module ls-tree -r --name-only HEAD Runtime +foreach ($file in $files) { + if (-not $file.EndsWith('.cs')) { continue } + $target = Join-Path $destination $file + New-Item -ItemType Directory -Force -Path ([IO.Path]::GetDirectoryName($target)) | Out-Null + $source = (git -C $module show "HEAD:$file") -join "`n" + [IO.File]::WriteAllText($target, $source.Replace('ShrinkContext', 'BaselineContext'), [Text.UTF8Encoding]::new($false)) +} +git -C $module rev-parse HEAD | Set-Content -Encoding utf8 (Join-Path $destination 'revision.txt') diff --git a/Tools/AgentSupport/Benchmarks/Program.cs b/Tools/AgentSupport/Benchmarks/Program.cs new file mode 100644 index 0000000..3f21952 --- /dev/null +++ b/Tools/AgentSupport/Benchmarks/Program.cs @@ -0,0 +1,173 @@ +using System.Diagnostics; +using System.IO.Pipelines; +using System.Text.Json; +using Cysharp.Threading.Tasks; +using ShrinkContext; +using ShrinkNetwork; +using ShrinkNetwork.MessagePack; +using ShrinkEventBus; +using ShrinkDataSaver; +using BC = BaselineContext; + +if (args.Length > 0 && args[0] == "--wire") +{ + using var frame = ShrinkPacketCodec.Encode(new ShrinkNetworkPacket { Opcode = 42, Route = "跨宿主" }, new ShrinkJsonNetworkSerializer(), new NetworkTransportTests.Request { Text = "跨宿主-v2" }); + var path = "Assets/AgentSupportTests/Runtime/Resources/ShrinkV2Golden.bytes"; + Directory.CreateDirectory(Path.GetDirectoryName(path)!); File.WriteAllBytes(path, frame.WrittenMemory.ToArray()); + var mp = new ShrinkMessagePackNetworkSerializer(TestMessageResolver.Instance); mp.Register(); + using var packed = ShrinkPacketCodec.Encode(new ShrinkNetworkPacket { Opcode = 43, Route = "messagepack" }, mp, new CodecMessage { Value = 42, Text = "跨宿主-v2" }); + File.WriteAllBytes("Artifacts/AgentSupport/messagepack-golden.bytes", packed.WrittenMemory.ToArray()); return; +} +if (args.Length > 0 && args[0] == "--tcp") { await TcpBenchmarks.Run(args.Length > 1 ? args[1] : "Artifacts/AgentSupport/tcp-benchmark.json"); return; } +var rows = new List(); +void Measure(string scenario, int iterations, Action action, object? parameters = null) +{ + for (var i = 0; i < Math.Min(iterations, 100); i++) action(); + GC.Collect(); GC.WaitForPendingFinalizers(); GC.Collect(); + var latencies = new double[iterations]; + var allocated = GC.GetAllocatedBytesForCurrentThread(); + var start = Stopwatch.GetTimestamp(); + for (var i = 0; i < iterations; i++) + { + var tick = Stopwatch.GetTimestamp(); action(); + latencies[i] = (Stopwatch.GetTimestamp() - tick) * 1e6 / Stopwatch.Frequency; + } + var elapsed = (Stopwatch.GetTimestamp() - start) / (double)Stopwatch.Frequency; + allocated = GC.GetAllocatedBytesForCurrentThread() - allocated; + Array.Sort(latencies); + rows.Add(new { scenario, parameters, iterations, throughputPerSecond = iterations / elapsed, + allocatedBytesPerOperation = allocated / (double)iterations, p50Microseconds = latencies[(int)(iterations * .50)], + p95Microseconds = latencies[(int)(iterations * .95)], p99Microseconds = latencies[(int)(iterations * .99)] }); +} + +var json = new ShrinkJsonNetworkSerializer(); +var messagePack = new ShrinkMessagePackNetworkSerializer(TestMessageResolver.Instance); messagePack.Register(); +foreach (var size in new[] { 64, 1024, 32768 }) +{ + var message = new CodecMessage { Text = new string('x', size) }; + var payload = json.Serialize(message); + Measure("network/legacy-json-envelope", 1000, () => json.Serialize(new LegacyPacket { Payload = payload }), new { payloadBytes = size }); + var packet = new ShrinkNetworkPacket { Payload = payload }; + Measure("network/v2-envelope", 1000, () => { using var encoded = ShrinkPacketCodec.Encode(packet); }, new { payloadBytes = size }); + Measure("network/v2-json-message", 1000, () => { using var encoded = ShrinkPacketCodec.Encode(new(), json, message); }, new { payloadBytes = size }); + Measure("network/v2-generated-messagepack", 1000, () => { using var encoded = ShrinkPacketCodec.Encode(new(), messagePack, message); }, new { payloadBytes = size }); + foreach (var connections in new[] { 1, 32, 256 }) + { + var client = new ShrinkNetworkService(); var server = new ShrinkNetworkService(); + var a = new ShrinkLoopbackTransport(); var b = new ShrinkLoopbackTransport(); a.LinkPeer(b); + client.RegisterMessage(1); server.RegisterMessage(1); + server.RegisterHandler((_, m) => UniTask.CompletedTask); + client.BindTransport(a); server.BindTransport(b); + for (var i = 0; i < connections; i++) a.OpenSession(i); + var session = 0; + Measure("network/loopback-session-send", 1000, () => client.SendAsync(client.Sessions[session++ % connections], message).GetAwaiter().GetResult(), new { payloadBytes = size, connections }); + Measure("network/loopback-broadcast-shared-payload", 25, () => { + foreach (var peer in client.Sessions.Values) client.SendSerializedAsync(peer, typeof(CodecMessage), payload).GetAwaiter().GetResult(); + }, new { payloadBytes = size, connections }); + a.Stop(); b.Stop(); + } +} + +foreach (var count in new[] { 10, 100, 1000 }) +{ + var current = new ShrinkContextRuntime(); var baseline = new BC.BaselineContextRuntime(); + for (var i = 0; i < count; i++) { current.Use(new Component("k" + i, i == 0 ? null : "k" + (i-1))); baseline.Use(new BaselineComponent("k" + i, i == 0 ? null : "k" + (i-1))); } + Measure("context/current-diagnostics", 100, () => current.CaptureDiagnostic(), new { components = count }); + Measure("context/baseline-diagnostics", 100, () => baseline.CaptureDiagnostic(), new { components = count }); + current.ShutdownAsync().GetAwaiter().GetResult(); + // Baseline recursively unloads deep chains; retire leaves first to obtain diagnostic measurements safely. + foreach (var fiber in baseline.Fibers.Reverse().ToArray()) baseline.RetireAsync(fiber).GetAwaiter().GetResult(); + baseline.ShutdownAsync().GetAwaiter().GetResult(); +} +var live = new ShrinkContextRuntime(); var old = new BC.BaselineContextRuntime(); +var memoryBefore = GC.GetTotalMemory(true); +for (var i = 0; i < 10000; i++) live.RetireAsync(live.Use(new Component("cycle"))).GetAwaiter().GetResult(); +var currentRetained = GC.GetTotalMemory(true) - memoryBefore; +memoryBefore = GC.GetTotalMemory(true); +for (var i = 0; i < 10000; i++) old.RetireAsync(old.Use(new BaselineComponent("cycle"))).GetAwaiter().GetResult(); +var baselineRetained = GC.GetTotalMemory(true) - memoryBefore; +rows.Add(new { scenario = "context/10000-retirements", currentRetainedBytes = currentRetained, baselineRetainedBytes = baselineRetained, + currentFibers = live.Fibers.Count, currentHistory = live.CaptureDiagnostic().Fibers.Count, baselineFibers = old.Fibers.Count }); +GC.KeepAlive(live); GC.KeepAlive(old); + +var bus = EventBus.Default; +using (var binding = ShrinkGeneratedBinding.Subscribe(ShrinkGeneratedBinding.RuntimeResolver, null, null, null, _ => { }, ShrinkEventPriority.Normal, 0, false)) +{ + Measure("event/sync", 100000, () => bus.Post(new Event())); + Measure("event/async-sync-subscriber", 10000, () => bus.PostAsync(new Event()).GetAwaiter().GetResult()); + Measure("event/binding-diagnostics", 1000, () => EventBus.CaptureBindings(bus)); +} +Measure("event/register-withdraw", 10000, () => ShrinkGeneratedBinding.Subscribe(ShrinkGeneratedBinding.RuntimeResolver, null, null, null, _ => { }, ShrinkEventPriority.Normal, 0, false).Dispose()); +using (var binding = ShrinkGeneratedBinding.SubscribeAsync(ShrinkGeneratedBinding.RuntimeResolver, null, null, null, (_, _) => UniTask.CompletedTask, ShrinkEventPriority.Normal, 0, false)) + Measure("event/async-subscriber", 10000, () => bus.PostAsync(new Event()).GetAwaiter().GetResult()); + +using (var queue = new ShrinkNetworkWorkQueue(1024, 1024 * 1024)) +{ + var completions = new List>(); + for (var i = 0; i < 10000; i++) completions.Add(queue.EnqueueAsync(i % 32, "state", 1024, () => UniTask.CompletedTask, "player")); + var before = queue.CaptureDiagnostics(); + await queue.PumpAsync(1024, 4096, TimeSpan.FromMilliseconds(2)); + rows.Add(new { scenario = "queue/burst-state", before, after = queue.CaptureDiagnostics() }); + queue.Dispose(); foreach (var completion in completions) await completion; +} + +var saveRoot = Path.GetFullPath(args.Length > 1 ? args[1] : "Artifacts/AgentSupport/BenchmarkSaves"); +ShrinkDataSaverRuntime.Initialize(new ShrinkDataSaverRuntimeConfig { RootPath = saveRoot }); +foreach (var size in new[] { 100 * 1024, 1024 * 1024, 10 * 1024 * 1024 }) +foreach (var background in new[] { false, true }) +{ + var state = new string('s', size); + ShrinkSave.RegisterModule("benchmark", () => new { state }, _ => { }); + foreach (var encrypted in new[] { false, true }) + { + var samples = new List(); + var allocated = GC.GetTotalAllocatedBytes(true); var elapsed = Stopwatch.StartNew(); + for (var i = 0; i < 10; i++) + { + var options = new SaveOptions { CollectTimings = true, EncodeInBackground = background, Encrypt = encrypted, EncryptionKey = "benchmark-only" }; + await ShrinkSave.SaveSlotAsync(0, options); samples.Add(options.LastTimings); + } + rows.Add(new { scenario = "save/phases", snapshotBytes = size, background, encrypted, elapsedMilliseconds = elapsed.Elapsed.TotalMilliseconds, + processAllocatedBytes = GC.GetTotalAllocatedBytes(true) - allocated, samples }); + } + ShrinkSave.UnregisterModule("benchmark"); +} + +// In-process backend comparison only: not a socket throughput claim and never a runtime dependency. +var pipe = new Pipe(new PipeOptions(pauseWriterThreshold: long.MaxValue, resumeWriterThreshold: long.MaxValue)); +var chunk = new byte[1024]; +Measure("experiment/pipelines-buffer-roundtrip", 10000, () => { + pipe.Writer.WriteAsync(chunk).GetAwaiter().GetResult(); + var result = pipe.Reader.ReadAsync().GetAwaiter().GetResult(); pipe.Reader.AdvanceTo(result.Buffer.End); +}); +await pipe.Writer.CompleteAsync(); await pipe.Reader.CompleteAsync(); +var output = Path.GetFullPath(args.Length > 0 ? args[0] : "Artifacts/AgentSupport/benchmark.json"); +Directory.CreateDirectory(Path.GetDirectoryName(output)!); +await File.WriteAllTextAsync(output, JsonSerializer.Serialize(new { runtime = System.Runtime.InteropServices.RuntimeInformation.FrameworkDescription, + host = "standalone .NET; Loopback sessions are not OS connections; diagnostic microbenchmarks are not Player data", atUtc = DateTime.UtcNow, rows }, new JsonSerializerOptions { WriteIndented = true })); +Console.WriteLine(output); + +public sealed class LegacyPacket +{ + public int ProtocolVersion = 1, SchemaVersion = 1, Opcode = 1; + public ShrinkRequestToken RequestToken; + public string SessionToken = "", Route = ""; + public long SessionTokenExpiresAtUnixTimeSeconds; + public ShrinkNetworkPacketKind Kind; + public byte[] Payload = []; +} +public readonly struct Event : IShrinkEvent { } +public sealed class Component(string key, string? dependency = null) : IShrinkComponent +{ + public string Name => key; + public IReadOnlyList Inject { get; } = dependency == null ? Array.Empty() : new[] { dependency }; + public IReadOnlyList Provide { get; } = new[] { key }; + public UniTask ApplyAsync(ShrinkCtx ctx, object? config) { ctx.Set(key, this); return UniTask.CompletedTask; } +} +public sealed class BaselineComponent(string key, string? dependency = null) : BC.IShrinkComponent +{ + public string Name => key; + public IReadOnlyList Inject { get; } = dependency == null ? Array.Empty() : new[] { dependency }; + public IReadOnlyList Provide { get; } = new[] { key }; + public UniTask ApplyAsync(BC.ShrinkCtx ctx, object? config) { ctx.Set(key, this); return UniTask.CompletedTask; } +} diff --git a/Tools/AgentSupport/Benchmarks/TcpBenchmarks.cs b/Tools/AgentSupport/Benchmarks/TcpBenchmarks.cs new file mode 100644 index 0000000..94c6fdf --- /dev/null +++ b/Tools/AgentSupport/Benchmarks/TcpBenchmarks.cs @@ -0,0 +1,74 @@ +using System.Diagnostics; +using System.Net; +using System.Net.Sockets; +using System.Text.Json; +using Cysharp.Threading.Tasks; +using ShrinkNetwork; + +public static class TcpBenchmarks +{ + public static async Task Run(string output) + { + var rows = new List(); + foreach (var connections in new[] { 1, 32, 256 }) + { + var probe = new TcpListener(IPAddress.Loopback, 0); probe.Start(); var port = ((IPEndPoint)probe.LocalEndpoint).Port; probe.Stop(); + var transport = new ShrinkTcpServerTransport(IPAddress.Loopback, port); + var server = Service(); + server.RegisterRequestHandler((_, request) => UniTask.FromResult(new NetworkTransportTests.Response { Text = request.Text })); + server.BindTransport(transport); + var clients = new List<(ShrinkNetworkService service, ShrinkTcpClientTransport transport, ShrinkNetworkSession session)>(); + long notifications = 0; + try + { + for (var i = 0; i < connections; i++) + { + var client = Service(); var link = new ShrinkTcpClientTransport("127.0.0.1", port); + var connected = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); + client.OnSessionConnected += s => connected.TrySetResult(s); + client.RegisterHandler((_, _) => { Interlocked.Increment(ref notifications); return UniTask.CompletedTask; }); + client.BindTransport(link); clients.Add((client, link, await connected.Task.WaitAsync(TimeSpan.FromSeconds(10)))); + } + foreach (var bytes in new[] { 64, 1024, 32768 }) + { + var samples = new List(); + var message = new NetworkTransportTests.Request { Text = new string('t', bytes) }; + var allocated = GC.GetTotalAllocatedBytes(true); var elapsed = Stopwatch.StartNew(); + for (var i = 0; i < 256; i++) + { + var client = clients[i % clients.Count]; var start = Stopwatch.GetTimestamp(); + var response = await client.service.CallAsync(client.session, message).AsTask().WaitAsync(TimeSpan.FromSeconds(10)); + if (response.Text.Length != bytes) throw new InvalidDataException("RPC content mismatch"); + samples.Add(Stopwatch.GetElapsedTime(start).TotalMicroseconds); + } + samples.Sort(); + rows.Add(new { scenario = "tcp/rpc", connections, bytes, operations = 256, throughputPerSecond = 256 / elapsed.Elapsed.TotalSeconds, + p50Microseconds = samples[128], p95Microseconds = samples[243], p99Microseconds = samples[253], processAllocatedBytes = GC.GetTotalAllocatedBytes(true) - allocated }); + var payload = new ShrinkJsonNetworkSerializer().Serialize(message); var before = Interlocked.Read(ref notifications); elapsed.Restart(); + foreach (var session in server.Sessions.Values) await server.SendSerializedAsync(session, typeof(NetworkTransportTests.Request), payload); + while (Interlocked.Read(ref notifications) - before < connections) + { if (elapsed.Elapsed > TimeSpan.FromSeconds(10)) throw new TimeoutException("Broadcast drain"); await Task.Delay(1); } + rows.Add(new { scenario = "tcp/broadcast", connections, bytes, elapsedMilliseconds = elapsed.Elapsed.TotalMilliseconds, delivered = Interlocked.Read(ref notifications) - before }); + } + using var queue = new ShrinkNetworkDispatchQueue(32, byteCapacity: 32768, perSessionCapacity: 2); + server.DispatchScheduler = queue; + var attempts = connections * 8; + for (var i = 0; i < attempts; i++) + { + var c = clients[i % clients.Count]; + if (c.service.Sessions.Count != 0) try { await c.service.SendAsync(c.session, new NetworkTransportTests.Request { Text = new string('s', 1024) }); } catch (IOException) { } + } + await Task.Delay(100); + var beforePump = queue.CaptureDiagnostics(); await queue.PumpAsync(32, 8192, TimeSpan.FromMilliseconds(2)); + rows.Add(new { scenario = "tcp/slow-consumer-burst", connections, attempts, beforePump, afterPump = queue.CaptureDiagnostics(), metrics = server.GetDiagnosticsSnapshot() }); + } + finally { foreach (var c in clients) c.transport.Stop(); transport.Stop(); } + Console.WriteLine("TCP completed: " + connections); + } + await File.WriteAllTextAsync(output, JsonSerializer.Serialize(new { runtime = System.Runtime.InteropServices.RuntimeInformation.FrameworkDescription, host = "Windows loopback TCP; sequential round-trip samples, multiple real connections", rows }, new JsonSerializerOptions { WriteIndented = true })); + } + private static ShrinkNetworkService Service() + { + var service = new ShrinkNetworkService(); service.RegisterMessage(1); service.RegisterMessage(2); return service; + } +} diff --git a/Tools/AgentSupport/Build-UnityAnalyzer.ps1 b/Tools/AgentSupport/Build-UnityAnalyzer.ps1 new file mode 100644 index 0000000..af57e38 --- /dev/null +++ b/Tools/AgentSupport/Build-UnityAnalyzer.ps1 @@ -0,0 +1,8 @@ +$ErrorActionPreference = 'Stop' +$root = [IO.Path]::GetFullPath((Join-Path $PSScriptRoot '../..')) +$project = Join-Path $root 'Assets/Modules/ShrinkShared.CodeGen/DotNet~/ShrinkSDK.CodeGen.Analyzers' +$output = Join-Path $root 'Artifacts/AgentSupport/unity-analyzer' +& dotnet build $project -c Release -p:RoslynVersion=3.8.0 "-p:OutputPath=$output/" --nologo /nodeReuse:false +if ($LASTEXITCODE -ne 0) { throw 'Unity analyzer build failed.' } +Copy-Item -LiteralPath (Join-Path $output 'ShrinkSDK.CodeGen.Analyzers.dll') -Destination (Join-Path $root 'Assets/Modules/ShrinkShared.CodeGen/Analyzers/ShrinkSDK.CodeGen.Analyzers.dll') +# Keep the Unity-generated .meta: RoslynAnalyzer label and disabled runtime plugin platforms. diff --git a/Tools/AgentSupport/Directory.Build.props b/Tools/AgentSupport/Directory.Build.props new file mode 100644 index 0000000..b6c82f2 --- /dev/null +++ b/Tools/AgentSupport/Directory.Build.props @@ -0,0 +1,7 @@ + + + latest + enable + enable + + diff --git a/Tools/AgentSupport/Install-Unity.py b/Tools/AgentSupport/Install-Unity.py new file mode 100644 index 0000000..c889533 --- /dev/null +++ b/Tools/AgentSupport/Install-Unity.py @@ -0,0 +1,43 @@ +"""Build portable local UPM packages and update only ShrinkSDK dependencies in a consumer.""" +import argparse, json, tarfile, shutil +from pathlib import Path +parser = argparse.ArgumentParser() +parser.add_argument("consumer", type=Path) +parser.add_argument("--messagepack", action="store_true", help="Requires MessagePack 3.x and its generator already installed in Unity") +args = parser.parse_args() +sdk = Path(__file__).resolve().parents[2] +consumer = args.consumer.resolve() +manifest_path = consumer / "Packages/manifest.json" +manifest = json.loads(manifest_path.read_text(encoding="utf-8-sig")) +catalog = {} +for path in list((sdk / "Assets/Modules").glob("*/package.json")) + [sdk / "Assets/ShrinkInspection/package.json", sdk / "Assets/Modules/ShrinkNetwork/Adapters~/MessagePack/package.json"]: + meta = json.loads(path.read_text(encoding="utf-8-sig")) + catalog[meta["name"]] = (path.parent, meta) +selected = set() +def include(name): + if name in selected: return + if name not in catalog: raise ValueError("Missing local SDK package: " + name) + selected.add(name) + for dep in catalog[name][1].get("dependencies", {}): + if dep.startswith("com.cneicy.shrink-"): include(dep) +for name in manifest["dependencies"]: + if name.startswith("com.cneicy.shrink-"): include(name) +include("com.cneicy.shrink-inspection") +if args.messagepack: include("com.cneicy.shrink-network-messagepack") +output = consumer / "Packages/ShrinkSDK" +output.mkdir(exist_ok=True) +backup = sdk / "Artifacts/AgentSupport" / (consumer.name + "-manifest-before.json") +backup.parent.mkdir(parents=True, exist_ok=True) +if not backup.exists(): shutil.copy2(manifest_path, backup) +excluded = {".git", ".vs", "bin", "obj", "node_modules", "Library", "Temp", "Development~"} +for name in sorted(selected): + root, meta = catalog[name] + filename = name + "-" + meta["version"] + ".tgz" + with tarfile.open(output / filename, "w:gz") as archive: + for path in sorted(root.rglob("*")): + relative = path.relative_to(root) + if path.is_file() and not any(part in excluded for part in relative.parts) and not path.is_symlink(): + archive.add(path, arcname="package/" + relative.as_posix(), recursive=False) + manifest["dependencies"][name] = "file:ShrinkSDK/" + filename + print(name, meta["version"]) +manifest_path.write_text(json.dumps(manifest, indent=2, ensure_ascii=False) + "\n", encoding="utf-8") diff --git a/Tools/AgentSupport/Pack-Local.ps1 b/Tools/AgentSupport/Pack-Local.ps1 new file mode 100644 index 0000000..52f7e74 --- /dev/null +++ b/Tools/AgentSupport/Pack-Local.ps1 @@ -0,0 +1,36 @@ +param([string]$Feed = 'Artifacts/AgentSupport/packages') +$ErrorActionPreference = 'Stop' +$root = [IO.Path]::GetFullPath((Join-Path $PSScriptRoot '../..')) +$feedPath = [IO.Path]::GetFullPath((Join-Path $root $Feed)) +New-Item -ItemType Directory -Force -Path $feedPath | Out-Null +$catalog = Get-Content (Join-Path $root 'Tools/Release/package-versions.json') -Raw | ConvertFrom-Json +$projects = @{} +foreach ($project in Get-ChildItem (Join-Path $root 'Assets/Modules') -Filter '*.csproj' -Recurse) { + if ($project.FullName -match '[\\/](obj|bin|Development~|Tools~|packages)[\\/]') { continue } + [xml]$xml = Get-Content $project.FullName -Raw + $id = @($xml.Project.PropertyGroup.PackageId | Where-Object { $_ })[0] + if ($id) { $projects[[string]$id] = $project.FullName } +} +foreach ($project in Get-ChildItem (Join-Path $root 'Godot/Packages') -Filter '*.csproj' -Recurse) { + [xml]$xml = Get-Content $project.FullName -Raw + $id = @($xml.Project.PropertyGroup.PackageId | Where-Object { $_ })[0] + if ($id) { $projects[[string]$id] = $project.FullName } +} +$projects['ShrinkSDK.Inspection'] = Join-Path $root 'Assets/ShrinkInspection/DotNet~/ShrinkSDK.Inspection.csproj' +$done = [Collections.Generic.HashSet[string]]::new() +$visiting = [Collections.Generic.HashSet[string]]::new() +function Build-Package([string]$id) { + if ($done.Contains($id)) { return } + if (-not $visiting.Add($id)) { throw "NuGet dependency cycle: $id" } + $project = $projects[$id] + [xml]$xml = Get-Content $project -Raw + foreach ($dependency in $xml.Project.ItemGroup.PackageReference) { + if ($dependency -and $projects.ContainsKey([string]$dependency.Include)) { Build-Package ([string]$dependency.Include) } + } + $log = Join-Path $feedPath "$id.pack.log" + & dotnet pack $project -c Release -o $feedPath "-p:RestoreSources=$feedPath" -p:NuGetAudit=false --nologo --verbosity quiet *> $log + if ($LASTEXITCODE -ne 0) { Get-Content $log -Tail 20; throw "Pack failed: $id" } + [void]$visiting.Remove($id); [void]$done.Add($id) + Write-Output "Packed $id" +} +foreach ($id in @($projects.Keys | Sort-Object)) { Build-Package $id } diff --git a/Tools/AgentSupport/README.md b/Tools/AgentSupport/README.md new file mode 100644 index 0000000..3d20db9 --- /dev/null +++ b/Tools/AgentSupport/README.md @@ -0,0 +1,37 @@ +# ShrinkSDK Inspect + +本地语义导航与能力查询工具。无需模型 API、向量数据库或常驻服务。 + +```powershell +dotnet pack Tools/AgentSupport/Shrink.Inspect -c Release -o Artifacts/Inspect +dotnet tool install ShrinkSDK.Inspect --tool-path Artifacts/Inspect/bin --add-source Artifacts/Inspect --version 0.1.2 +Artifacts/Inspect/bin/shrink inspect capabilities "存档" --root . +Artifacts/Inspect/bin/shrink inspect modules --root . +``` + +支持 `modules`、`capabilities`、`symbol`、`flow`、`impact`、`check`。能力默认 3 项;其他结果默认 20 项,链路深度 2。使用 `--module`、`--limit`、`--depth` 缩小或展开,`--json` 返回结构化数据。 + +Unity 使用菜单 **Tools/ShrinkSDK/Inspection/Export compilation inputs** 导出实际编译引用、源文件和宏,然后传入 `--compilation Artifacts/Inspection/compilation.json`。没有导出时 IDE 工程输入只作为可能过期的辅助;无工程输入时采用标记不完整的源码候选索引。 + +.NET/Godot 消费项目先正常 restore/build,再使用 `--project path/to/project.csproj`,通过 MSBuildWorkspace 获取语义。工具需要 .NET 8 或兼容运行时,以及可加载该项目的 SDK。 + +```text +shrink inspect flow "route:command/execute" --compilation Artifacts/Inspection/compilation.json +shrink inspect symbol ShrinkContextRuntime --compilation Artifacts/Inspection/compilation.json +shrink inspect impact ShrinkNetworkService.cs --compilation Artifacts/Inspection/compilation.json +shrink inspect check --compilation Artifacts/Inspection/compilation.json --json +``` + +`static` 是静态绑定,`candidate` 是动态候选/待注册关系,`runtime-bound` 来自相同构建的按需快照,`unresolved` 表示信息不足。静态订阅关系不等于活跃订阅;影响查询不证明没有其他间接影响。索引每次从当前输入构建,不保存不可见的跨任务记忆。 + +通过 `--snapshot` 加载宿主导出的快照;无可验证构建标识或标识不符时不合并。工具不自动连接生产宿主,不输出消息正文或令牌。 + +新增内容的工作方式见 [复用优先设计](../../Docs/ReuseFirstDesign.md)。源码、配置与行为验证优先于描述;无命中不意味着不存在能力。 + +退出码:0 查询完成,1 `check` 发现确定错误,2 参数/输入错误。覆盖不足以结构化 coverage 返回,不伪装为完整检查通过。 + +当前目录下的 Unity PackageCache 只纳入已安装的 `com.cneicy.shrink-*` 源码与能力说明,其他第三方包作为引用。消费项目用自己的 README 声明业务能力,工具不会从 SDK 基础设施推断玩法已经存在。 + +本机仅安装较新 .NET SDK 时,CLI 允许前滚到最新已安装主版本;共享内核仍为 netstandard2.1。能力查询无需 MSBuild,`--project` 需要可加载目标项目的 SDK。 + +本地发布与验证入口:`Pack-Local.ps1` 打包 NuGet,`Install-Unity.py <工程路径>` 生成可随工程保存的 UPM tarball;它只改消费工程的 ShrinkSDK 依赖并保留原 manifest 备份。新版安装和回退说明见 [迁移指南](../../Docs/NetworkV2Migration.md)。 diff --git a/Tools/AgentSupport/RuntimeTests/BridgeRetentionTests.cs b/Tools/AgentSupport/RuntimeTests/BridgeRetentionTests.cs new file mode 100644 index 0000000..e38c08f --- /dev/null +++ b/Tools/AgentSupport/RuntimeTests/BridgeRetentionTests.cs @@ -0,0 +1,22 @@ +using System.Runtime.CompilerServices; +using NUnit.Framework; +using ShrinkCommand; +using ShrinkCommand.Integration; +using ShrinkNetwork; + +public class BridgeRetentionTests +{ + [Test] + public void CommandBridgeDoesNotKeepRetiredNetworkServiceAlive() + { + var weak = Register(); GC.Collect(); GC.WaitForPendingFinalizers(); GC.Collect(); + Assert.That(weak.IsAlive, Is.False); + } + [MethodImpl(MethodImplOptions.NoInlining)] + private static WeakReference Register() + { + var service = new ShrinkNetworkService(); + ShrinkNetworkCommandBridge.RegisterService(service, new ShrinkCommandService()); + return new WeakReference(service); + } +} diff --git a/Tools/AgentSupport/RuntimeTests/NetworkCodecTests.cs b/Tools/AgentSupport/RuntimeTests/NetworkCodecTests.cs new file mode 100644 index 0000000..d4d5d6f --- /dev/null +++ b/Tools/AgentSupport/RuntimeTests/NetworkCodecTests.cs @@ -0,0 +1,132 @@ +using System.Buffers; +using System.Buffers.Binary; +using Cysharp.Threading.Tasks; +using MessagePack; +using NUnit.Framework; +using ShrinkNetwork; +using ShrinkNetwork.MessagePack; + +[MessagePackObject] +public partial class CodecMessage : IShrinkNetworkMessage +{ + [Key(0)] public int Value { get; set; } + [Key(1)] public string Text { get; set; } = ""; +} + +[GeneratedMessagePackResolver] +public partial class TestMessageResolver { } + +public class NetworkCodecTests +{ + [TestCase(false)] [TestCase(true)] + public void EnvelopeRoundTripsPayloadWithoutEncodingItAgain(bool messagePack) + { + IShrinkNetworkBufferSerializer serializer; + if (messagePack) { var mp = new ShrinkMessagePackNetworkSerializer(TestMessageResolver.Instance); mp.Register(); serializer = mp; } + else serializer = new ShrinkJsonNetworkSerializer(); + var counting = new CountingSerializer(serializer); + var message = new CodecMessage { Value = 42, Text = "跨宿主消息" }; + using var encoded = ShrinkPacketCodec.Encode(new ShrinkNetworkPacket { Opcode = 123, Route = "中文/route", SessionToken = "secret", RequestToken = new(9) }, counting, message); + var packet = ShrinkPacketCodec.Decode(encoded.WrittenMemory); + var decoded = (CodecMessage)serializer.Deserialize(packet.Payload, typeof(CodecMessage)); + Assert.That(counting.Count, Is.EqualTo(1)); + Assert.That(decoded.Text, Is.EqualTo(message.Text)); + Assert.That(decoded.Value, Is.EqualTo(42)); + Assert.That(packet.Route, Is.EqualTo("中文/route")); + Assert.That(packet.SessionToken, Is.EqualTo("secret")); + Assert.That(packet.RequestToken.Value, Is.EqualTo(9)); + Assert.That(encoded.WrittenCount, Is.EqualTo(ShrinkPacketCodec.HeaderSize + System.Text.Encoding.UTF8.GetByteCount(packet.Route!) + 6 + packet.Payload.Length)); + } + + [Test] + public void RejectsOldVersionMalformedLengthsAndTrailingData() + { + Assert.Throws(() => ShrinkPacketCodec.Decode(System.Text.Encoding.UTF8.GetBytes("{\"ProtocolVersion\":1}"))); + using var encoded = ShrinkPacketCodec.Encode(new ShrinkNetworkPacket { Payload = new byte[] { 1, 2, 3 } }); + var bytes = encoded.WrittenMemory.ToArray(); + BinaryPrimitives.WriteUInt16LittleEndian(bytes.AsSpan(4), 1); + Assert.Throws(() => ShrinkPacketCodec.Decode(bytes)); + BinaryPrimitives.WriteUInt16LittleEndian(bytes.AsSpan(4), 2); + BinaryPrimitives.WriteInt32LittleEndian(bytes.AsSpan(29), int.MaxValue); + Assert.Throws(() => ShrinkPacketCodec.Decode(bytes)); + Assert.Throws(() => ShrinkPacketCodec.Decode(encoded.WrittenMemory[..^1])); + Assert.Throws(() => ShrinkPacketCodec.Decode(encoded.WrittenMemory.ToArray().Concat(new byte[1]).ToArray())); + } + + [Test] + public void InvalidUtf8HeaderIsAProtocolRejection() + { + using var encoded = ShrinkPacketCodec.Encode(new ShrinkNetworkPacket { Route = "r", SessionToken = "t" }); + foreach (var offset in new[] { ShrinkPacketCodec.HeaderSize, ShrinkPacketCodec.HeaderSize + 1 }) + { + var bytes = encoded.WrittenMemory.ToArray(); bytes[offset] = 0xff; + Assert.Throws(() => ShrinkPacketCodec.Decode(bytes)); + } + } + + [Test] + public void BorrowedDecodeAndWriterDisposeHaveExplicitLifetimes() + { + var writer = ShrinkPacketCodec.Encode(new ShrinkNetworkPacket { Payload = new byte[] { 7 } }); + var packet = ShrinkPacketCodec.Decode(writer.WrittenMemory); + Assert.That(packet.Payload.Span[0], Is.EqualTo(7)); + writer.Dispose(); writer.Dispose(); + Assert.Throws(() => writer.GetMemory()); + } + + [TestCase(false)] [TestCase(true)] + public async Task ServiceKeepsBufferAliveUntilTransportCompletes(bool fail) + { + var transport = new DeferredTransport(); + var service = new ShrinkNetworkService(); + service.RegisterMessage(1); + service.BindTransport(transport); + transport.Connect(); + var sending = service.SendAsync(service.Sessions[1], new CodecMessage { Text = "alive" }).AsTask(); + Assert.That(sending.IsCompleted, Is.False); + Assert.That(((CodecMessage)new ShrinkJsonNetworkSerializer().Deserialize(ShrinkPacketCodec.Decode(transport.Memory).Payload, typeof(CodecMessage))).Text, Is.EqualTo("alive")); + if (fail) transport.Completion.TrySetException(new IOException("disconnect")); else transport.Completion.TrySetResult(); + if (fail) Assert.ThrowsAsync(async () => await sending); else await sending; + Assert.That(transport.Memory.Span.ToArray(), Is.All.EqualTo(0), "pooled buffer is cleared and released after completion/failure"); + } + + [Test] + public async Task QueuedLoopbackReceiverOwnsPacketAfterSenderReturns() + { + using var queue = new ShrinkNetworkDispatchQueue(4); + var client = new ShrinkNetworkService(); + var server = new ShrinkNetworkService { DispatchScheduler = queue }; + client.RegisterMessage(1); server.RegisterMessage(1); + var received = ""; + server.RegisterHandler((ctx, message) => { received = message.Text; return UniTask.CompletedTask; }); + var left = new ShrinkLoopbackTransport(); var right = new ShrinkLoopbackTransport(); left.LinkPeer(right); + client.BindTransport(left); server.BindTransport(right); left.OpenSession(1); + await client.SendAsync(client.Sessions[1], new CodecMessage { Text = "retained" }); + Assert.That(received, Is.Empty); + await queue.PumpAsync(4); + Assert.That(received, Is.EqualTo("retained")); + } + + private sealed class CountingSerializer(IShrinkNetworkBufferSerializer inner) : IShrinkNetworkBufferSerializer + { + public int Count; + public void Serialize(IBufferWriter writer, object value) { Count++; inner.Serialize(writer, value); } + public object Deserialize(ReadOnlyMemory payload, Type type) => inner.Deserialize(payload, type); + public byte[] Serialize(object value) => throw new AssertionException("Legacy allocation path used"); + public object Deserialize(byte[] payload, Type type) => inner.Deserialize(payload, type); + public T Deserialize(byte[] payload) => inner.Deserialize(payload); + } + private sealed class DeferredTransport : IShrinkNetworkMemoryTransport + { + public bool IsStarted { get; private set; } + public event Action? OnEvent; + public UniTaskCompletionSource Completion = new(); + public ReadOnlyMemory Memory; + public void Start() => IsStarted = true; + public void Stop() => IsStarted = false; + public void Connect() => OnEvent?.Invoke(ShrinkNetworkTransportEvent.Connected(1, "test")); + public void Send(long id, byte[] data) => throw new AssertionException("Legacy transport used"); + public UniTask SendAsync(long id, byte[] data) => throw new AssertionException("Legacy transport used"); + public UniTask SendAsync(long id, ReadOnlyMemory data, CancellationToken ct = default) { Memory = data; return Completion.Task; } + } +} diff --git a/Tools/AgentSupport/RuntimeTests/NetworkQueueTests.cs b/Tools/AgentSupport/RuntimeTests/NetworkQueueTests.cs new file mode 100644 index 0000000..ada47fb --- /dev/null +++ b/Tools/AgentSupport/RuntimeTests/NetworkQueueTests.cs @@ -0,0 +1,66 @@ +using Cysharp.Threading.Tasks; +using NUnit.Framework; +using ShrinkNetwork; + +public class NetworkQueueTests +{ + [Test] + public async Task StateReplacementIsScopedToSessionAndMovesAfterReliableWork() + { + using var queue = new ShrinkNetworkWorkQueue(10, 100); + var output = new List(); + UniTask Write(int i) { output.Add(i); return UniTask.CompletedTask; } + var old = queue.EnqueueAsync(1, "position", 10, () => Write(1), "player"); + var reliable = queue.EnqueueAsync(1, "position", 10, () => Write(2)); + var other = queue.EnqueueAsync(2, "position", 10, () => Write(3), "player"); + var latest = queue.EnqueueAsync(1, "position", 20, () => Write(4), "player"); + Assert.That(await old, Is.EqualTo(ShrinkNetworkQueueResult.Replaced)); + Assert.That(queue.CaptureDiagnostics().PendingBytes, Is.EqualTo(40)); + Assert.That(await queue.PumpAsync(10), Is.EqualTo(3)); + Assert.That(output, Is.EqualTo(new[] { 2, 3, 4 })); + foreach (var result in new[] { await reliable, await other, await latest }) Assert.That(result, Is.EqualTo(ShrinkNetworkQueueResult.Completed)); + } + + [Test] + public async Task CapacityRejectionDoesNotLoseExistingReliableWorkOrState() + { + using var queue = new ShrinkNetworkWorkQueue(2, 20, 1); + var a = queue.EnqueueAsync(1, "a", 10, () => UniTask.CompletedTask, "x"); + Assert.That(await queue.EnqueueAsync(1, "a", 1, () => UniTask.CompletedTask), Is.EqualTo(ShrinkNetworkQueueResult.Rejected)); + Assert.That(await queue.EnqueueAsync(1, "a", 21, () => UniTask.CompletedTask, "x"), Is.EqualTo(ShrinkNetworkQueueResult.Rejected)); + await queue.PumpAsync(1); + Assert.That(await a, Is.EqualTo(ShrinkNetworkQueueResult.Completed)); + Assert.That(queue.CaptureDiagnostics().PendingBytes, Is.Zero); + } + + [Test] + public async Task BudgetDoesNotPreemptHandlerAndConcurrentPumpsDoNotRunHandlersInParallel() + { + using var queue = new ShrinkNetworkWorkQueue(4, 100); + var completion = new UniTaskCompletionSource(); + var first = queue.EnqueueAsync(1, "a", 10, () => completion.Task); + var second = queue.EnqueueAsync(1, "a", 10, () => UniTask.CompletedTask); + var pump = queue.PumpAsync(4, 100, TimeSpan.FromMilliseconds(1)).AsTask(); + await Task.Delay(10); + Assert.That(pump.IsCompleted, Is.False); + Assert.That(await queue.PumpAsync(4), Is.Zero); + completion.TrySetResult(); + Assert.That(await pump, Is.EqualTo(1)); + Assert.That(await first, Is.EqualTo(ShrinkNetworkQueueResult.Completed)); + queue.Dispose(); + Assert.That(await second, Is.EqualTo(ShrinkNetworkQueueResult.Canceled)); + } + + [Test] + public async Task ByteBudgetMakesProgressForLargePacketsAndHandlerFailureDoesNotLeakQueue() + { + using var queue = new ShrinkNetworkWorkQueue(4, 100); + var failed = queue.EnqueueAsync(1, "a", 30, () => UniTask.FromException(new IOException("handler"))).AsTask(); + var next = queue.EnqueueAsync(1, "a", 10, () => UniTask.CompletedTask); + Assert.That(await queue.PumpAsync(4, 1), Is.EqualTo(1)); + Assert.ThrowsAsync(async () => await failed); + Assert.That(await queue.PumpAsync(4), Is.EqualTo(1)); + Assert.That(await next, Is.EqualTo(ShrinkNetworkQueueResult.Completed)); + Assert.That(queue.CaptureDiagnostics().PendingCount, Is.Zero); + } +} diff --git a/Tools/AgentSupport/RuntimeTests/NetworkTransportTests.cs b/Tools/AgentSupport/RuntimeTests/NetworkTransportTests.cs new file mode 100644 index 0000000..4d2e605 --- /dev/null +++ b/Tools/AgentSupport/RuntimeTests/NetworkTransportTests.cs @@ -0,0 +1,70 @@ +using System.Net; +using System.Net.Sockets; +using Cysharp.Threading.Tasks; +using NUnit.Framework; +using ShrinkNetwork; + +[NonParallelizable] +public class NetworkTransportTests +{ + [Test] + public async Task TcpRpcRoundTripAndCanceledSendPreserveNextFrame() + { + var reservation = new TcpListener(IPAddress.Loopback, 0); reservation.Start(); + var port = ((IPEndPoint)reservation.LocalEndpoint).Port; reservation.Stop(); + var serverTransport = new ShrinkTcpServerTransport(IPAddress.Loopback, port); + var clientTransport = new ShrinkTcpClientTransport("127.0.0.1", port); + var server = Service(); var client = Service(); + var connected = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); + client.OnSessionConnected += session => connected.TrySetResult(session); + server.RegisterRequestHandler((_, request) => UniTask.FromResult(new Response { Text = request.Text })); + try + { + server.BindTransport(serverTransport); client.BindTransport(clientTransport); + var session = await connected.Task.WaitAsync(TimeSpan.FromSeconds(5)); + using var cancellation = new CancellationTokenSource(); cancellation.Cancel(); + Assert.CatchAsync(async () => await clientTransport.SendAsync(1, new ReadOnlyMemory(new byte[100]), cancellation.Token)); + var reply = await client.CallAsync(session, new Request { Text = new string('t', 32768) }).AsTask().WaitAsync(TimeSpan.FromSeconds(5)); + Assert.That(reply.Text.Length, Is.EqualTo(32768)); + Assert.That(client.GetDiagnosticsSnapshot().RpcCompleted, Is.EqualTo(1)); + } + finally { clientTransport.Stop(); serverTransport.Stop(); } + } + + [Test] + public async Task NestedRpcResponseBypassesSerialHandlerQueue() + { + var a = new ShrinkLoopbackTransport(); var b = new ShrinkLoopbackTransport(); a.LinkPeer(b); + var client = Service(); var server = Service(); + client.RegisterRequestHandler((_, req) => UniTask.FromResult(new Response { Text = "nested:" + req.Text })); + server.RegisterRequestHandler(async (ctx, req) => await server.CallAsync(ctx.Session, req)); + client.BindTransport(a); server.BindTransport(b); a.OpenSession(1); + var response = await client.CallAsync(client.Sessions[1], new Request { Text = "ok" }).AsTask().WaitAsync(TimeSpan.FromSeconds(3)); + Assert.That(response.Text, Is.EqualTo("nested:ok")); + a.Stop(); b.Stop(); + } + + [Test] + public async Task CongestionDisconnectsReliablePeerAndDoesNotResurrectSession() + { + var a = new ShrinkLoopbackTransport(); var b = new ShrinkLoopbackTransport(); a.LinkPeer(b); + using var queue = new ShrinkNetworkDispatchQueue(1, byteCapacity: 1024); + var client = Service(); var server = Service(); server.DispatchScheduler = queue; + client.BindTransport(a); server.BindTransport(b); a.OpenSession(1); + var session = client.Sessions[1]; + var rejected = 0; server.OnDispatchRejected += _ => rejected++; + await client.SendAsync(session, new Request()); + await client.SendAsync(session, new Request()); + Assert.That(rejected, Is.EqualTo(1)); + Assert.That(server.Sessions, Is.Empty); + await queue.PumpAsync(1); + Assert.That(server.Sessions, Is.Empty); + a.Stop(); b.Stop(); + } + private static ShrinkNetworkService Service() + { + var service = new ShrinkNetworkService(); service.RegisterMessage(1); service.RegisterMessage(2); return service; + } + public sealed class Request : IShrinkNetworkRequest { public string Text { get; set; } = ""; } + public sealed class Response : ShrinkRpcResponseBase { public string Text { get; set; } = ""; } +} diff --git a/Tools/AgentSupport/RuntimeTests/RuntimeTests.csproj b/Tools/AgentSupport/RuntimeTests/RuntimeTests.csproj new file mode 100644 index 0000000..b20c9ed --- /dev/null +++ b/Tools/AgentSupport/RuntimeTests/RuntimeTests.csproj @@ -0,0 +1,28 @@ + + + net8.0 + false + true + true + + + + + + + + + + + + + + + + + + + + + + diff --git a/Tools/AgentSupport/RuntimeTests/SaveSnapshotTests.cs b/Tools/AgentSupport/RuntimeTests/SaveSnapshotTests.cs new file mode 100644 index 0000000..0bcaa65 --- /dev/null +++ b/Tools/AgentSupport/RuntimeTests/SaveSnapshotTests.cs @@ -0,0 +1,79 @@ +using NUnit.Framework; +using ShrinkDataSaver; +using ShrinkSDK.Inspection; +using ShrinkNetwork; +using Cysharp.Threading.Tasks; + +[NonParallelizable] +public class SaveSnapshotTests +{ + private string _directory = null!; + [SetUp] public void Setup() + { + _directory = Path.Combine(Path.GetTempPath(), "ShrinkSaveTests", Guid.NewGuid().ToString("N")); + ShrinkSave.Initialize(new LocalStorageProvider(_directory), "saves", ".json", 1); + } + [TearDown] public void Cleanup() + { + ShrinkSave.UnregisterModule("test-snapshot"); + if (Directory.Exists(_directory)) Directory.Delete(_directory, true); + } + + [TestCase(false)] [TestCase(true)] + public async Task CaptureRunsOnCallerAndWorkerEncodingPreservesFormat(bool encrypt) + { + var owner = Environment.CurrentManagedThreadId; + var captured = 0; + var value = new SaveDto { Value = "original" }; + ShrinkSave.RegisterModule("test-snapshot", () => { captured = Environment.CurrentManagedThreadId; return value; }, _ => { }); + var options = new SaveOptions { EncodeInBackground = true, CollectTimings = true, Encrypt = encrypt, EncryptionKey = "test-only" }; + var saving = ShrinkSave.SaveSlotAsync(0, options).AsTask(); + value.Value = "changed-after-capture"; + await saving; + var file = Directory.GetFiles(Path.Combine(_directory, "saves"), "*.json").Single(); + var packet = DataSerializer.Deserialize(await File.ReadAllBytesAsync(file)); + var text = encrypt ? System.Text.Encoding.UTF8.GetString(SaveEncryptor.Decrypt(Convert.FromBase64String(packet.EncryptedModules), "test-only")) : packet.Modules["test-snapshot"].ToString(); + Assert.That(captured, Is.EqualTo(owner)); + Assert.That(text, Does.Contain("original").And.Not.Contain("changed-after-capture")); + Assert.That(options.LastTimings.EncodedBytes, Is.GreaterThan(0)); + Assert.That(options.LastTimings.CaptureMilliseconds, Is.GreaterThanOrEqualTo(0)); + } + + [Test] + public async Task CancellationLeavesExistingFileAndBackupsRecoverCorruption() + { + var storage = new LocalStorageProvider(_directory); + await storage.WriteAsync("slot.json", new byte[] { 1 }); + await storage.WriteAsync("slot.json", new byte[] { 2 }); + using var cancellation = new CancellationTokenSource(); cancellation.Cancel(); + Assert.CatchAsync(async () => await storage.WriteAsync("slot.json", new byte[] { 3 }, cancellation.Token)); + Assert.That(await storage.ReadAsync("slot.json"), Is.EqualTo(new byte[] { 2 })); + Assert.That(await storage.ReadAsync("slot.json.bak1"), Is.EqualTo(new byte[] { 1 })); + Assert.That(File.Exists(Path.Combine(_directory, "slot.json.tmp")), Is.False); + } + + [Test] + public async Task CorruptPrimarySaveFallsBackToPreviousValidPacket() + { + await ShrinkSave.SaveSlotAsync(0, new SaveOptions { SlotName = "previous" }); + await ShrinkSave.SaveSlotAsync(0, new SaveOptions { SlotName = "latest" }); + var primary = Directory.GetFiles(Path.Combine(_directory, "saves"), "*.json").Single(); + await File.WriteAllTextAsync(primary, "corrupt"); + Assert.That((await ShrinkSave.GetMetaAsync(0)).SlotName, Is.EqualTo("previous")); + } + + [Test] + public void RuntimeSnapshotExportsBindingsWithoutSessionSecrets() + { + var service = new ShrinkNetworkService(); service.RegisterMessage(42, "test/message"); + service.RegisterHandler(Receive); + var snapshot = new ShrinkInspectionSnapshot("build-a"); + snapshot.AddNetwork("server", service); + var json = snapshot.ToJson(); + Assert.That(snapshot.Relations.Any(r => r.From == "opcode:42" && r.To == "type:CodecMessage")); + Assert.That(snapshot.Relations.Any(r => r.To == "M:SaveSnapshotTests.Receive(ShrinkNetwork.ShrinkNetworkContext,CodecMessage)")); + Assert.That(json, Does.Not.Contain("Payload").And.Not.Contain("SessionToken")); + } + private static UniTask Receive(ShrinkNetworkContext ctx, CodecMessage message) => UniTask.CompletedTask; + public sealed class SaveDto { public string Value { get; set; } = ""; } +} diff --git a/Tools/AgentSupport/Shrink.Inspect.Tests/AnalyzerTests.cs b/Tools/AgentSupport/Shrink.Inspect.Tests/AnalyzerTests.cs new file mode 100644 index 0000000..c87580f --- /dev/null +++ b/Tools/AgentSupport/Shrink.Inspect.Tests/AnalyzerTests.cs @@ -0,0 +1,37 @@ +using System.Collections.Immutable; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp; +using Microsoft.CodeAnalysis.Diagnostics; +using NUnit.Framework; +using ShrinkSDK.CodeGen.Analyzers; + +public class AnalyzerTests +{ + private static readonly MetadataReference[] SystemReferences = ((string)AppContext.GetData("TRUSTED_PLATFORM_ASSEMBLIES")!).Split(Path.PathSeparator).Select(p => MetadataReference.CreateFromFile(p)).ToArray(); + private static CSharpCompilation Compile(string name, string source, params MetadataReference[] references) => CSharpCompilation.Create(name, [CSharpSyntaxTree.ParseText(source)], SystemReferences.Concat(references), new CSharpCompilationOptions(OutputKind.DynamicallyLinkedLibrary)); + private static MetadataReference Emit(CSharpCompilation compilation) + { + using var stream = new MemoryStream(); var result = compilation.Emit(stream); + Assert.That(result.Success, Is.True, string.Join(";", result.Diagnostics)); + return MetadataReference.CreateFromImage(stream.ToArray()); + } + [Test] public async Task DuplicateContractsAcrossReferencedAssembliesAreErrors() + { + var contract = Emit(Compile("Contract", "namespace ShrinkNetwork { public class ShrinkNetworkMessageAttribute : System.Attribute { public ShrinkNetworkMessageAttribute(int opcode, string route) {} } }")); + var a = Emit(Compile("A", "[ShrinkNetwork.ShrinkNetworkMessage(1, \"one\")] public class A {}".Replace("\\", ""), contract)); + var b = Emit(Compile("B", "[ShrinkNetwork.ShrinkNetworkMessage(1, \"two\")] public class B {}".Replace("\\", ""), contract)); + var diagnostics = await Compile("Consumer", "class Consumer {}", contract, a, b).WithAnalyzers(ImmutableArray.Create(new ShrinkCodeGenAnalyzer())).GetAnalyzerDiagnosticsAsync(); + Assert.That(diagnostics.Count(d => d.Id == "SHRINK002"), Is.EqualTo(1)); + } + [Test] public async Task DefiniteKeyAndSubscriptionErrorsHaveStableCodes() + { + var compilation = Compile("Consumer", """ + namespace ShrinkContext { public class ShrinkKey { public ShrinkKey(string package, string name, int major) {} } } + namespace ShrinkEventBus { public interface IShrinkEvent {} public class ShrinkSubscribeAttribute : System.Attribute {} } + class Consumer { object key = new ShrinkContext.ShrinkKey("", "rage", 0); [ShrinkEventBus.ShrinkSubscribe] void On(int wrong) {} } + """); + var diagnostics = await compilation.WithAnalyzers(ImmutableArray.Create(new ShrinkCodeGenAnalyzer())).GetAnalyzerDiagnosticsAsync(); + Assert.That(diagnostics.Count(d => d.Id == "SHRINK004"), Is.EqualTo(2)); + Assert.That(diagnostics.Count(d => d.Id == "SHRINK001"), Is.EqualTo(1)); + } +} diff --git a/Tools/AgentSupport/Shrink.Inspect.Tests/IdentityTests.cs b/Tools/AgentSupport/Shrink.Inspect.Tests/IdentityTests.cs new file mode 100644 index 0000000..ae103e1 --- /dev/null +++ b/Tools/AgentSupport/Shrink.Inspect.Tests/IdentityTests.cs @@ -0,0 +1,54 @@ +using System.Text.Json; +using NUnit.Framework; +using ShrinkSDK.Inspect; +using Index = ShrinkSDK.Inspect.Index; + +public class IdentityTests +{ + [TestCase("a", true)] [TestCase("b", false)] [TestCase("", false)] + public void SnapshotNeedsMatchingVerifiedBuild(string build, bool accepted) + { + var path = Path.GetTempFileName(); + try + { + File.WriteAllText(path, "{\"BuildId\":\"a\",\"Relations\":[{\"From\":\"type:Request\",\"To\":\"M:Handler.Run\",\"Kind\":\"handler\",\"Scope\":\"server\"}]}"); + var index = new Index { BuildId = build }; + InspectCommand.AttachSnapshot(index, path); + Assert.That(index.Relations.Count, Is.EqualTo(accepted ? 1 : 0)); + if (accepted) Assert.That(index.Relations[0].Evidence, Is.EqualTo("runtime-bound")); + else Assert.That(index.Coverage, Is.Not.Empty); + } + finally { File.Delete(path); } + } + + [Test] + public async Task ChangedSourceInvalidatesRuntimeMerge() + { + var dir = Path.Combine(Path.GetTempPath(), "ShrinkInspectIdentity", Guid.NewGuid().ToString("N")); Directory.CreateDirectory(dir); + try + { + var source = Path.Combine(dir, "A.cs"); File.WriteAllText(source, "class A {}"); + var manifest = new CompilationManifest { Root = dir, BuildId = "a", Assemblies = [new() { Name = "A", Sources = [source] }], SourceTicks = new() { [source] = 0 } }; + var path = Path.Combine(dir, "compilation.json"); File.WriteAllText(path, JsonSerializer.Serialize(manifest)); + var index = new Index { Root = dir }; + await SemanticIndex.Build(index, path, null); + Assert.That(index.BuildId, Is.Empty); + Assert.That(index.Coverage.Any(c => c.Contains("Stale"))); + } + finally { Directory.Delete(dir, true); } + } + [Test] + public void MatchingSnapshotExposesBoundedStateItems() + { + var path = Path.GetTempFileName(); + try + { + File.WriteAllText(path, """{"BuildId":"a","State":{"app":{"Fibers":[{"Name":"wait","Status":"Waiting"},{"Name":"ready","Status":"Active"}]}}} """); + var index = new Index { BuildId = "a" }; InspectCommand.AttachSnapshot(index, path); + Assert.That(index.RuntimeStates.Count, Is.EqualTo(2)); + Assert.That(index.RuntimeStates[0].Value.GetProperty("Name").GetString(), Is.EqualTo("wait")); + } + finally { File.Delete(path); } + } + +} diff --git a/Tools/AgentSupport/Shrink.Inspect.Tests/NavigationTests.cs b/Tools/AgentSupport/Shrink.Inspect.Tests/NavigationTests.cs new file mode 100644 index 0000000..4bc9fd3 --- /dev/null +++ b/Tools/AgentSupport/Shrink.Inspect.Tests/NavigationTests.cs @@ -0,0 +1,84 @@ +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp; +using NUnit.Framework; +using ShrinkSDK.Inspect; +using Index = ShrinkSDK.Inspect.Index; + +public class NavigationTests +{ + private string _root = null!; + [SetUp] public void SetUp() { _root = Path.Combine(Path.GetTempPath(), "ShrinkInspectTests", Guid.NewGuid().ToString("N")); Directory.CreateDirectory(_root); } + [TearDown] public void TearDown() => Directory.Delete(_root, true); + + [Test] public void ExactOverloadsAndVirtualCandidatesStayDistinct() + { + var index = Analyze("public class A { public void Start() { Call(1); } public void Call(int x) {} public void Call(string x) {} public virtual void Tick() {} public void Run() { Tick(); } }"); + Assert.That(index.Relations.Any(r => r.From == "M:A.Start" && r.To == "M:A.Call(System.Int32)" && r.Evidence == "static")); + Assert.That(index.Relations.Any(r => r.To == "M:A.Call(System.String)"), Is.False); + Assert.That(index.Relations.Any(r => r.To == "M:A.Tick" && r.Evidence == "candidate")); + } + + [Test] public void UnresolvedCallIsNotInventedAsStatic() + { + var index = Analyze("class A { void Run() { Missing.Go(); } }"); + Assert.That(index.Relations.Any(r => r.To == "Missing.Go" && r.Evidence == "unresolved")); + Assert.That(index.Coverage, Is.Not.Empty); + } + + [Test] public void FrameworkEventEdgesBridgePublisherAndSubscriber() + { + var index = Analyze(""" + using System; + namespace ShrinkEventBus { public class ShrinkSubscribeAttribute : Attribute {} public static class EventBus { public static void Post(T value) {} } } + public struct Killed {} + public class Combat { public void Kill() { ShrinkEventBus.EventBus.Post(new Killed()); } } + public class Rage { [ShrinkEventBus.ShrinkSubscribe] public void Gain(Killed value) {} } + """); + var edges = InspectCommand.Walk(index, "M:Combat.Kill", 2, null).ToArray(); + Assert.That(edges.Any(e => e.Kind == "publishes" && e.To == "type:Killed")); + Assert.That(edges.Any(e => e.Kind == "subscribes" && e.To == "M:Rage.Gain(Killed)" && e.Evidence == "candidate")); + } + + [Test] public void OpcodeAndRouteUniquenessAreIndependent() + { + var index = Analyze(""" + using System; + namespace ShrinkNetwork { public class ShrinkNetworkMessageAttribute : Attribute { public ShrinkNetworkMessageAttribute(int id, string route) {} } } + [ShrinkNetwork.ShrinkNetworkMessage(1,"a")] class A {} + [ShrinkNetwork.ShrinkNetworkMessage(1,"b")] class B {} + [ShrinkNetwork.ShrinkNetworkMessage(2,"a")] class C {} + """); + SemanticIndex.CheckRelations(index); + Assert.That(index.Findings.Any(f => f.Message.Contains("opcode:1"))); + Assert.That(index.Findings.Any(f => f.Message.Contains("route:a"))); + } + + [Test] public void CapabilityMatchesDoNotImplyMissingAbilitiesAreAbsent() + { + Directory.CreateDirectory(Path.Combine(_root, "Inventory")); + File.WriteAllText(Path.Combine(_root, "Inventory", "inventory.asmdef"), "{\"name\":\"Inventory\"}"); + File.WriteAllText(Path.Combine(_root, "Inventory", "README.md"), """ + + Capability: 配置物品容量 + Aliases: 背包 inventory 容量 + Limits: 不负责装备属性 + Extension: SlotOptions + Evidence: Inventory.cs + + """); + Directory.CreateDirectory(Path.Combine(_root, "Combat")); + File.WriteAllText(Path.Combine(_root, "Combat", "combat.asmdef"), "{\"name\":\"Combat\"}"); + var index = new Index { Root = _root }; ModuleCatalog.Load(index); + Assert.That(ModuleCatalog.Search(index, "背包", null).Single().Limits, Is.EqualTo("不负责装备属性")); + Assert.That(index.Modules.Single(m => m.Name == "Combat").SemanticDescriptionComplete, Is.False); + Assert.That(ModuleCatalog.Search(index, "怒气", null), Is.Empty); + } + + private Index Analyze(string code) + { + var references = ((string)AppContext.GetData("TRUSTED_PLATFORM_ASSEMBLIES")!).Split(Path.PathSeparator).Select(p => MetadataReference.CreateFromFile(p)); + var compilation = CSharpCompilation.Create("Fixture", [CSharpSyntaxTree.ParseText(code, path: Path.Combine(_root, "Fixture.cs"))], references, + new CSharpCompilationOptions(OutputKind.DynamicallyLinkedLibrary)); + var index = new Index { Root = _root }; SemanticIndex.Analyze(index, compilation); return index; + } +} diff --git a/Tools/AgentSupport/Shrink.Inspect.Tests/Shrink.Inspect.Tests.csproj b/Tools/AgentSupport/Shrink.Inspect.Tests/Shrink.Inspect.Tests.csproj new file mode 100644 index 0000000..866b274 --- /dev/null +++ b/Tools/AgentSupport/Shrink.Inspect.Tests/Shrink.Inspect.Tests.csproj @@ -0,0 +1,9 @@ + + net8.0false + + + + + + + diff --git a/Tools/AgentSupport/Shrink.Inspect/Models.cs b/Tools/AgentSupport/Shrink.Inspect/Models.cs new file mode 100644 index 0000000..9c805ab --- /dev/null +++ b/Tools/AgentSupport/Shrink.Inspect/Models.cs @@ -0,0 +1,40 @@ +namespace ShrinkSDK.Inspect; + +public sealed record SourceLocation(string File, int Line); +public sealed record SymbolInfo(string Id, string Name, string Module, string Kind, SourceLocation Location); +public sealed record Relation(string From, string To, string Kind, string Evidence, SourceLocation Location); +public sealed record RuntimeState(string Scope, string Name, System.Text.Json.JsonElement Value, SourceLocation Location); +public sealed record Finding(string Code, string Severity, string Message, SourceLocation Location); +public sealed record Capability(string Module, string Summary, string Aliases, string Limits, string Extension, + string Evidence, SourceLocation Location, int Score = 0, string MatchReason = ""); +public sealed record ModuleInfo(string Name, string Root, string Version, string[] Dependencies, + string[] Assemblies, string[] Validation, bool SemanticDescriptionComplete); +public sealed class CompilationInput +{ + public string Name { get; set; } = ""; + public string[] Sources { get; set; } = []; + public string[] References { get; set; } = []; + public string[] Defines { get; set; } = []; + public string Output { get; set; } = ""; +} +public sealed class CompilationManifest +{ + public string Root { get; set; } = ""; + public string BuildId { get; set; } = ""; + public CompilationInput[] Assemblies { get; set; } = []; + public Dictionary SourceTicks { get; set; } = new(); +} +public sealed class Index +{ + public string Root { get; set; } = ""; + public string BuildId { get; set; } = ""; + public List Coverage { get; } = []; + public List Modules { get; } = []; + public List Capabilities { get; } = []; + public Dictionary Symbols { get; } = new(StringComparer.Ordinal); + public List Relations { get; } = []; + public List RuntimeStates { get; } = []; + public List Findings { get; } = []; + public string ModuleFor(string file) => Modules.Where(m => file.StartsWith(m.Root + Path.DirectorySeparatorChar, + StringComparison.OrdinalIgnoreCase)).OrderByDescending(m => m.Root.Length).FirstOrDefault()?.Name ?? "consumer"; +} diff --git a/Tools/AgentSupport/Shrink.Inspect/ModuleCatalog.cs b/Tools/AgentSupport/Shrink.Inspect/ModuleCatalog.cs new file mode 100644 index 0000000..152f8b7 --- /dev/null +++ b/Tools/AgentSupport/Shrink.Inspect/ModuleCatalog.cs @@ -0,0 +1,105 @@ +using System.Text.Json; +using System.Text.RegularExpressions; + +namespace ShrinkSDK.Inspect; + +public static class ModuleCatalog +{ + private static readonly HashSet Excluded = new(StringComparer.OrdinalIgnoreCase) + { ".git", "Library", "Temp", "obj", "bin", "Logs", "Builds", "Artifacts", "node_modules", "Development~", "Samples~" }; + + public static IEnumerable Files(string root, string pattern) + { + foreach (var file in Directory.EnumerateFiles(root, pattern)) yield return file; + foreach (var dir in Directory.EnumerateDirectories(root)) + { + if (Excluded.Contains(Path.GetFileName(dir)) || (File.GetAttributes(dir) & FileAttributes.ReparsePoint) != 0) continue; + foreach (var file in Files(dir, pattern)) yield return file; + } + } + + public static void Load(Index index) + { + var installed = Path.Combine(index.Root, "Library", "PackageCache"); + var scanRoots = new List { index.Root }; + if (Directory.Exists(installed)) scanRoots.AddRange(Directory.GetDirectories(installed, "com.cneicy.shrink-*@*")); + var packages = scanRoots.SelectMany(r => Files(r, "package.json")).Select(path => (path, doc: Read(path))) + .Where(x => x.doc.TryGetProperty("name", out _)).ToArray(); + var asmdefs = scanRoots.SelectMany(r => Files(r, "*.asmdef")).ToArray(); + var roots = packages.Select(p => Path.GetDirectoryName(p.path)!).ToHashSet(StringComparer.OrdinalIgnoreCase); + foreach (var path in asmdefs) + { + var dir = Path.GetDirectoryName(path)!; + if (!roots.Any(root => dir.Equals(root, StringComparison.OrdinalIgnoreCase) || dir.StartsWith(root + Path.DirectorySeparatorChar, StringComparison.OrdinalIgnoreCase))) roots.Add(dir); + } + // Business directories can share an asmdef while retaining separate ownership. + foreach (var readme in scanRoots.SelectMany(r => Files(r, "README.md"))) + if (File.ReadAllText(readme).Contains("", StringComparison.Ordinal)) roots.Add(Path.GetDirectoryName(readme)!); + + foreach (var root in roots.Order(StringComparer.Ordinal)) + { + var package = packages.FirstOrDefault(p => Path.GetDirectoryName(p.path) == root); + var name = package.doc.ValueKind == JsonValueKind.Object ? String(package.doc, "name") : Path.GetFileName(root); + var version = package.doc.ValueKind == JsonValueKind.Object ? String(package.doc, "version") : "unversioned"; + var dependencies = package.doc.ValueKind == JsonValueKind.Object && package.doc.TryGetProperty("dependencies", out var deps) + ? deps.EnumerateObject().Select(p => p.Name + "@" + p.Value.GetString()).ToArray() : []; + var assemblies = asmdefs.Where(p => p.StartsWith(root + Path.DirectorySeparatorChar, StringComparison.OrdinalIgnoreCase)) + .Select(p => String(Read(p), "name")).ToArray(); + var readme = Path.Combine(root, "README.md"); + var complete = File.Exists(readme) && ReadCapabilities(name, readme, index.Capabilities); + var validation = Files(root, "*.cs").Where(p => p.Contains("Tests", StringComparison.OrdinalIgnoreCase)) + .Select(p => Path.GetRelativePath(index.Root, p)).Take(8).ToArray(); + index.Modules.Add(new(name, root, version, dependencies, assemblies, validation, complete)); + } + foreach (var module in index.Modules) + foreach (var dep in module.Dependencies) + { + var split = dep.LastIndexOf('@'); + if (split <= 0) continue; + var target = index.Modules.FirstOrDefault(m => m.Name == dep[..split]); + if (target == null) continue; + if (target.Version != dep[(split + 1)..]) index.Findings.Add(new("SHRINK101", "error", $"{module.Name} requires {dep}, workspace has {target.Version}", new(Path.Combine(module.Root, "package.json"), 1))); + if (!module.Name.Contains("integration") && !module.Name.Contains("starter") && target.Name.Contains("-integration-")) + index.Findings.Add(new("SHRINK102", "error", $"Main package {module.Name} depends on integration {target.Name}.", new(Path.Combine(module.Root, "package.json"), 1))); + } + foreach (var module in index.Modules) + Visit(module, new HashSet(), new HashSet()); + void Visit(ModuleInfo current, HashSet path, HashSet seen) + { + if (!path.Add(current.Name)) { index.Findings.Add(new("SHRINK103", "error", "Package dependency cycle: " + string.Join(" -> ", path) + " -> " + current.Name, new(Path.Combine(current.Root, "package.json"), 1))); return; } + if (seen.Add(current.Name)) + foreach (var dep in current.Dependencies) + { + var target = index.Modules.FirstOrDefault(m => dep.StartsWith(m.Name + "@", StringComparison.Ordinal)); + if (target != null) Visit(target, path, seen); + } + path.Remove(current.Name); + } + } + + private static bool ReadCapabilities(string module, string path, List output) + { + var lines = File.ReadAllLines(path); + var start = Array.IndexOf(lines, ""); + var end = Array.IndexOf(lines, ""); + if (start < 0 || end <= start) return false; + string Field(string key) => lines.Skip(start + 1).Take(end - start - 1) + .FirstOrDefault(s => s.StartsWith(key + ":", StringComparison.Ordinal))?[(key.Length + 1)..].Trim() ?? ""; + output.Add(new(module, Field("Capability"), Field("Aliases"), Field("Limits"), Field("Extension"), Field("Evidence"), new(path, start + 1))); + return new[] { "Capability", "Limits", "Extension", "Evidence" }.All(key => Field(key).Length > 0); + } + + public static IEnumerable Search(Index index, string query, string? module) + { + var terms = Regex.Split(query.Trim(), @"[\s,,;;/]+").Where(t => t.Length > 0).ToArray(); + return index.Capabilities.Where(c => module == null || c.Module.Contains(module, StringComparison.OrdinalIgnoreCase)) + .Select(c => { + var text = c.Module + " " + c.Summary + " " + c.Aliases; + var matches = terms.Where(t => text.Contains(t, StringComparison.OrdinalIgnoreCase)).ToArray(); + return c with { Score = matches.Length, MatchReason = string.Join(", ", matches) }; + }).Where(c => terms.Length == 0 || c.Score > 0).OrderByDescending(c => c.Score).ThenBy(c => c.Module, StringComparer.Ordinal); + } + + private static JsonElement Read(string path) { using var doc = JsonDocument.Parse(File.ReadAllText(path)); return doc.RootElement.Clone(); } + private static string String(JsonElement e, string name) => e.TryGetProperty(name, out var p) ? p.GetString() ?? "" : ""; +} diff --git a/Tools/AgentSupport/Shrink.Inspect/Program.cs b/Tools/AgentSupport/Shrink.Inspect/Program.cs new file mode 100644 index 0000000..82bde5d --- /dev/null +++ b/Tools/AgentSupport/Shrink.Inspect/Program.cs @@ -0,0 +1,153 @@ +using System.Text.Json; +using ShrinkSDK.Inspect; +using Index = ShrinkSDK.Inspect.Index; + +return await InspectCommand.Run(args); + +public static class InspectCommand +{ + public static async Task Run(string[] args) + { + try + { + if (args.Length < 2 || args[0] != "inspect") + { + Console.WriteLine("shrink inspect modules|capabilities|symbol|flow|impact|check [query] [--root dir] [--module name] [--limit n] [--depth n] [--json] [--compilation file|--project csproj] [--snapshot file]"); + return args.Length == 0 ? 0 : 2; + } + var options = new Dictionary(StringComparer.Ordinal); + var words = new List(); + for (var i = 2; i < args.Length; i++) + { + if (args[i] == "--json") options["json"] = "true"; + else if (args[i].StartsWith("--", StringComparison.Ordinal)) + { + var key = args[i][2..]; + if (key is not ("root" or "module" or "limit" or "offset" or "depth" or "compilation" or "project" or "snapshot")) throw new ArgumentException("Unknown option --" + key); + if (++i >= args.Length) throw new ArgumentException("Missing value for --" + key); + options[key] = args[i]; + } + else words.Add(args[i]); + } + string? Option(string key) => options.GetValueOrDefault(key); + var command = args[1]; + if (command is not ("modules" or "capabilities" or "symbol" or "flow" or "impact" or "check")) throw new ArgumentException("Unknown query " + command); + var query = string.Join(" ", words); + if (command is "symbol" or "flow" or "impact" && query.Length == 0) throw new ArgumentException("An exact symbol, route, key or file query is required."); + var limit = int.Parse(Option("limit") ?? (command == "capabilities" ? "3" : "20")); + var offset = int.Parse(Option("offset") ?? "0"); + var depth = int.Parse(Option("depth") ?? "2"); + if (limit is < 1 or > 1000 || depth is < 0 or > 10 || offset < 0) throw new ArgumentException("limit must be 1..1000; depth must be 0..10; offset must be nonnegative."); + var index = new Index { Root = Path.GetFullPath(Option("root") ?? Environment.CurrentDirectory) }; + if (!Directory.Exists(index.Root)) throw new DirectoryNotFoundException(index.Root); + if (Option("project") != null && Option("compilation") != null) throw new ArgumentException("Choose --project or --compilation, not both."); + ModuleCatalog.Load(index); + if (command is not ("modules" or "capabilities")) + { + await SemanticIndex.Build(index, Option("compilation"), Option("project"), checkRules: command == "check"); + index.Relations.RemoveAll(r => string.IsNullOrEmpty(r.From) || string.IsNullOrEmpty(r.To)); + var unique = index.Relations.Distinct().ToArray(); + index.Relations.Clear(); index.Relations.AddRange(unique); + SemanticIndex.CheckRelations(index); + } + if (Option("snapshot") is { } snapshot) AttachSnapshot(index, snapshot); + var moduleFilter = Option("module"); + var stateQuery = query.StartsWith("key:", StringComparison.Ordinal) ? query[4..] : query; + bool MatchModule(string module) => moduleFilter == null || module.Contains(moduleFilter, StringComparison.OrdinalIgnoreCase); + object[] all = command switch + { + "modules" => index.Modules.Where(m => MatchModule(m.Name)).Cast().ToArray(), + "capabilities" => ModuleCatalog.Search(index, query, moduleFilter).Cast().ToArray(), + "symbol" => index.Symbols.Values.Where(s => MatchModule(s.Module) && (s.Id.Contains(query, StringComparison.OrdinalIgnoreCase) || s.Name.Contains(query, StringComparison.OrdinalIgnoreCase))).OrderBy(s => s.Id).Cast().Concat(Walk(index, query, 1, moduleFilter)).ToArray(), + "flow" or "impact" => Walk(index, query, depth, moduleFilter).Cast().Concat(index.RuntimeStates.Where(s => s.Scope.Contains(query, StringComparison.OrdinalIgnoreCase) || s.Value.ToString().Contains(stateQuery, StringComparison.OrdinalIgnoreCase))).ToArray(), + "check" => index.Findings.Distinct().Cast().ToArray(), + _ => [] + }; + var missingDescriptions = index.Modules.Where(m => MatchModule(m.Name) && !m.SemanticDescriptionComplete).Select(m => m.Name).ToArray(); + var result = new { + success = true, formatVersion = 1, toolVersion = typeof(InspectCommand).Assembly.GetName().Version?.ToString(), command, query, buildId = index.BuildId, + offset, limit, nextOffset = all.Length > offset + limit ? (int?)(offset + limit) : null, + coverage = index.Coverage.Distinct().Take(12), coverageTruncated = index.Coverage.Distinct().Count() > 12, + semanticCoverage = new { incompleteCount = missingDescriptions.Length, examples = missingDescriptions.Take(5) }, + validation = command == "impact" ? index.Modules.Where(m => all.OfType().Any(r => index.ModuleFor(r.Location.File) == m.Name)).SelectMany(m => m.Validation).Distinct().Take(8).ToArray() : Array.Empty(), + total = all.Length, truncated = all.Length > offset + limit, items = all.Skip(offset).Take(limit) + }; + if (options.ContainsKey("json")) Console.WriteLine(JsonSerializer.Serialize(result, SemanticIndex.Json)); + else + { + Console.WriteLine($"# {command}: {query}\n\nShowing {Math.Min(limit, Math.Max(0, all.Length - offset))}/{all.Length}." + (all.Length > limit ? " Truncated; narrow the query or continue with --offset." : "")); + if (missingDescriptions.Length > 0) Console.WriteLine($"\nSemantic descriptions incomplete: {missingDescriptions.Length} modules. No match is not proof of absence."); + foreach (var warning in index.Coverage.Distinct().Take(12)) Console.WriteLine("\n> " + warning); + foreach (var item in all.Skip(offset).Take(limit)) Render(item); + } + return command == "check" && index.Findings.Any(f => f.Severity == "error") ? 1 : 0; + } + catch (Exception e) when (e is ArgumentException or IOException or JsonException or InvalidOperationException or FormatException or OverflowException) + { + if (args.Contains("--json")) Console.WriteLine(JsonSerializer.Serialize(new { success = false, code = "SHRINK-INSPECT-INPUT", error = e.Message }, SemanticIndex.Json)); + else Console.Error.WriteLine("SHRINK-INSPECT-INPUT: " + e.Message); + return 2; + } + } + + public static IEnumerable Walk(Index index, string query, int depth, string? module) + { + var ids = index.Symbols.Values.Where(s => + (s.Id.Contains(query, StringComparison.OrdinalIgnoreCase) || s.Name.Contains(query, StringComparison.OrdinalIgnoreCase) || s.Location.File.Contains(query, StringComparison.OrdinalIgnoreCase))).Select(s => s.Id).ToHashSet(); + foreach (var edge in index.Relations) + foreach (var endpoint in new[] { edge.From, edge.To }) + if (endpoint.Contains(query, StringComparison.OrdinalIgnoreCase)) ids.Add(endpoint); + var seen = new HashSet(); + for (var step = 0; step < depth; step++) + { + var next = new HashSet(); + foreach (var edge in index.Relations.Where(r => ids.Contains(r.From) || ids.Contains(r.To))) + if (seen.Add(edge)) { // External utility methods do not connect every unrelated caller into one flow. + foreach (var endpoint in new[] { edge.From, edge.To }) + if (index.Symbols.ContainsKey(endpoint) || !endpoint.StartsWith("M:", StringComparison.Ordinal)) next.Add(endpoint); + if (module == null || index.ModuleFor(edge.Location.File).Contains(module, StringComparison.OrdinalIgnoreCase) || + new[] { edge.From, edge.To }.Any(id => index.Symbols.TryGetValue(id, out var symbol) && symbol.Module.Contains(module, StringComparison.OrdinalIgnoreCase))) yield return edge; } + ids.UnionWith(next); + } + } + + public static void AttachSnapshot(Index index, string path) + { + using var document = JsonDocument.Parse(File.ReadAllText(path)); + var root = document.RootElement; + var identity = root.TryGetProperty("BuildId", out var b) ? b.GetString() : null; + if (string.IsNullOrEmpty(index.BuildId) || identity != index.BuildId) + { index.Coverage.Add("Runtime snapshot build mismatch or unverified source identity. Runtime relations were NOT merged."); return; } + if (root.TryGetProperty("Coverage", out var coverage)) + foreach (var item in coverage.EnumerateArray()) index.Coverage.Add("Snapshot: " + item.GetString()); + if (root.TryGetProperty("State", out var state)) + foreach (var scope in state.EnumerateObject()) + foreach (var field in scope.Value.EnumerateObject()) + if (field.Value.ValueKind == JsonValueKind.Array) + foreach (var entry in field.Value.EnumerateArray()) index.RuntimeStates.Add(new(scope.Name, field.Name, entry.Clone(), new(Path.GetFullPath(path), 1))); + else index.RuntimeStates.Add(new(scope.Name, field.Name, field.Value.Clone(), new(Path.GetFullPath(path), 1))); + if (!root.TryGetProperty("Relations", out var relations)) return; + foreach (var relation in relations.EnumerateArray()) + { + var from = relation.GetProperty("From").GetString()!; + var to = relation.GetProperty("To").GetString()!; + var kind = relation.GetProperty("Kind").GetString()!; + var scope = relation.TryGetProperty("Scope", out var scopeElement) ? scopeElement.GetString() : ""; + index.Relations.Add(new(from, to, kind + " [" + scope + "]", "runtime-bound", new(Path.GetFullPath(path), 1))); + } + } + + private static void Render(object item) + { + string Link(SourceLocation loc) => $"[{Path.GetFileName(loc.File)}:{loc.Line}]({loc.File.Replace('\\', '/')}:{loc.Line})"; + Console.WriteLine(item switch { + Capability c => $"\n- **{c.Module}** — {c.Summary}\n Match: {c.MatchReason}\n Limits: {c.Limits}\n Extension: {c.Extension}\n Evidence: {c.Evidence}; {Link(c.Location)}", + ModuleInfo m => $"\n- **{m.Name} {m.Version}** — {m.Root}\n Dependencies: {string.Join(", ", m.Dependencies)}\n Validation: {string.Join(", ", m.Validation)}", + SymbolInfo s => $"\n- `{s.Id}` ({s.Module}) — {Link(s.Location)}", + Relation r => $"\n- `{r.From}` → `{r.To}` [{r.Kind}; {r.Evidence}] — {Link(r.Location)}", + RuntimeState s => $"\n- Runtime `{s.Scope}/{s.Name}`: {s.Value} — {Link(s.Location)}", + Finding f => $"\n- {f.Severity} **{f.Code}** {f.Message} — {Link(f.Location)}", + _ => "\n" + JsonSerializer.Serialize(item) + }); + } +} diff --git a/Tools/AgentSupport/Shrink.Inspect/SemanticIndex.cs b/Tools/AgentSupport/Shrink.Inspect/SemanticIndex.cs new file mode 100644 index 0000000..b7054fa --- /dev/null +++ b/Tools/AgentSupport/Shrink.Inspect/SemanticIndex.cs @@ -0,0 +1,202 @@ +using System.Text.Json; +using System.Xml.Linq; +using Microsoft.Build.Locator; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp; +using Microsoft.CodeAnalysis.CSharp.Syntax; +using Microsoft.CodeAnalysis.MSBuild; +using Microsoft.CodeAnalysis.Diagnostics; +using System.Collections.Immutable; +using ShrinkSDK.CodeGen.Analyzers; + +namespace ShrinkSDK.Inspect; + +public static class SemanticIndex +{ + public static readonly JsonSerializerOptions Json = new() { PropertyNameCaseInsensitive = true, WriteIndented = true, Encoder = System.Text.Encodings.Web.JavaScriptEncoder.UnsafeRelaxedJsonEscaping }; + public static async Task Build(Index index, string? manifestPath, string? projectPath, bool checkRules = false) + { + if (projectPath != null) + { + if (!MSBuildLocator.IsRegistered) MSBuildLocator.RegisterDefaults(); + await LoadProject(index, Path.GetFullPath(projectPath), checkRules); + return; + } + if (manifestPath != null) + { + var manifest = JsonSerializer.Deserialize(File.ReadAllText(manifestPath), Json) ?? throw new InvalidDataException("Invalid compilation manifest."); + index.BuildId = manifest.BuildId; + var stale = manifest.SourceTicks.Where(pair => !File.Exists(pair.Key) || File.GetLastWriteTimeUtc(pair.Key).Ticks != pair.Value).Take(5).Select(p => p.Key).ToArray(); + if (stale.Length > 0) { index.Coverage.Add("Stale compilation input: " + string.Join(", ", stale)); index.BuildId = ""; } + if (manifest.SourceTicks.Count == 0 || manifest.Assemblies.SelectMany(a => a.Sources).Any(s => !manifest.SourceTicks.ContainsKey(s))) + { index.Coverage.Add("Compilation manifest lacks complete source state; runtime identity cannot be verified."); index.BuildId = ""; } + foreach (var input in manifest.Assemblies) + { + var sources = input.Sources.Where(path => InScope(index.Root, path)).ToArray(); + if (sources.Length == 0) continue; + input.Sources = sources; + Analyze(index, Compile(input), checkRules: checkRules); + } + index.Coverage.Add("Installed ShrinkSDK package sources are included; other external packages are reference-only."); + return; + } + var projects = Directory.GetFiles(index.Root, "*.csproj"); + var found = false; + foreach (var path in projects) + { + var xml = XDocument.Load(path); + var sources = xml.Descendants().Where(e => e.Name.LocalName == "Compile").Select(e => e.Attribute("Include")?.Value) + .Where(s => s != null && !s.Contains('*')).Select(s => Path.GetFullPath(Path.Combine(index.Root, s!))).Where(File.Exists).ToArray(); + if (sources.Length == 0) continue; + var references = xml.Descendants().Where(e => e.Name.LocalName == "HintPath").Select(e => Path.GetFullPath(Path.Combine(index.Root, e.Value))).Where(File.Exists).ToArray(); + var projectReferences = xml.Descendants().Where(e => e.Name.LocalName == "ProjectReference").Select(e => e.Attribute("Include")?.Value) + .Where(p => p != null).Select(p => Path.Combine(index.Root, "Library", "ScriptAssemblies", Path.GetFileNameWithoutExtension(p!) + ".dll")).Where(File.Exists); + references = references.Concat(projectReferences).Distinct(StringComparer.OrdinalIgnoreCase).ToArray(); + var defines = xml.Descendants().FirstOrDefault(e => e.Name.LocalName == "DefineConstants")?.Value.Split(';') ?? []; + Analyze(index, Compile(new() { Name = Path.GetFileNameWithoutExtension(path), Sources = sources, References = references, Defines = defines }), checkRules: checkRules); + found = true; + } + if (found) { index.Coverage.Add("Unity IDE project inputs may be stale. Export compilation.json from the Editor for verified references/defines and runtime identity."); return; } + index.Coverage.Add("No compilation input: source-only candidate index; conditional compilation and references are incomplete. Use --compilation or --project."); + Analyze(index, Compile(new() { Name = "SourceOnly", Sources = ModuleCatalog.Files(index.Root, "*.cs").ToArray(), + References = ((string?)AppContext.GetData("TRUSTED_PLATFORM_ASSEMBLIES"))?.Split(Path.PathSeparator) ?? [] }), candidateOnly: true); + } + + private static async Task LoadProject(Index index, string path, bool checkRules) + { + using var workspace = MSBuildWorkspace.Create(); + workspace.WorkspaceFailed += (_, e) => index.Coverage.Add(e.Diagnostic.Message); + var project = await workspace.OpenProjectAsync(path); + foreach (var p in project.Solution.Projects) + { + var compilation = await p.GetCompilationAsync(); + if (compilation != null) Analyze(index, compilation, checkRules: checkRules); + } + index.Coverage.Add("Current source project analysis; no runtime build identity unless a matching exported manifest is supplied."); + } + + private static bool InScope(string root, string path) + { + var relative = Path.GetRelativePath(root, path); + var parts = relative.Split(Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar); + if (parts.Length > 2 && parts[0] == "Library" && parts[1] == "PackageCache" && parts[2].StartsWith("com.cneicy.shrink-", StringComparison.Ordinal)) return true; + return !relative.StartsWith("..") && !relative.Split(Path.DirectorySeparatorChar).Any(p => p is "Library" or "Temp" or "obj" or "bin" or "Artifacts"); + } + + private static Compilation Compile(CompilationInput input) + { + var options = CSharpParseOptions.Default.WithLanguageVersion(LanguageVersion.Latest).WithPreprocessorSymbols(input.Defines.Where(d => !string.IsNullOrWhiteSpace(d))); + var trees = input.Sources.Where(File.Exists).Distinct(StringComparer.OrdinalIgnoreCase) + .Select(path => CSharpSyntaxTree.ParseText(File.ReadAllText(path), options, path)); + var references = input.References.Where(File.Exists).Distinct(StringComparer.OrdinalIgnoreCase).Select(path => MetadataReference.CreateFromFile(path)); + return CSharpCompilation.Create(input.Name, trees, references, new CSharpCompilationOptions(OutputKind.DynamicallyLinkedLibrary, allowUnsafe: true)); + } + + public static string Id(ISymbol symbol) => symbol.OriginalDefinition.GetDocumentationCommentId() ?? symbol.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat); + private static SourceLocation At(SyntaxNode node) => new(node.SyntaxTree.FilePath, node.GetLocation().GetLineSpan().StartLinePosition.Line + 1); + private static string TypeId(ITypeSymbol type) => "type:" + type.ToDisplayString(); + + public static void Analyze(Index index, Compilation compilation, bool candidateOnly = false, bool checkRules = false) + { + var errors = compilation.GetDiagnostics().Where(d => d.Severity == DiagnosticSeverity.Error).Take(3).ToArray(); + if (errors.Length > 0) index.Coverage.Add($"{compilation.AssemblyName}: incomplete compilation: " + string.Join(" | ", errors.Select(e => e.Id + " " + e.GetMessage()))); + if (checkRules && !candidateOnly) + { + var diagnostics = compilation.WithAnalyzers(ImmutableArray.Create(new ShrinkCodeGenAnalyzer())) + .GetAnalyzerDiagnosticsAsync().GetAwaiter().GetResult(); + foreach (var diagnostic in diagnostics) + { + var line = diagnostic.Location.GetLineSpan(); + index.Findings.Add(new(diagnostic.Id, diagnostic.Severity.ToString().ToLowerInvariant(), diagnostic.GetMessage(), + new(string.IsNullOrEmpty(line.Path) ? index.Root : line.Path, line.StartLinePosition.Line + 1))); + } + } + foreach (var tree in compilation.SyntaxTrees) + { + var model = compilation.GetSemanticModel(tree); + foreach (var node in tree.GetRoot().DescendantNodes()) + { + if (node is BaseTypeDeclarationSyntax or BaseMethodDeclarationSyntax or PropertyDeclarationSyntax) + { + var symbol = model.GetDeclaredSymbol(node); + if (symbol == null) continue; + var id = Id(symbol); + index.Symbols[id] = new(id, symbol.ToDisplayString(), index.ModuleFor(tree.FilePath), symbol.Kind.ToString(), At(node)); + if (symbol is INamedTypeSymbol type) + { + Edge(id, TypeId(type), "declares", candidateOnly ? "candidate" : "static", node); + foreach (var attr in type.GetAttributes().Where(a => a.AttributeClass?.ToDisplayString() == "ShrinkNetwork.ShrinkNetworkMessageAttribute")) + { + if (attr.ConstructorArguments.Length == 0) continue; + Edge("opcode:" + attr.ConstructorArguments[0].Value, TypeId(type), "message", "static", node); + if (attr.ConstructorArguments.Length > 1 && attr.ConstructorArguments[1].Value is string route && !string.IsNullOrWhiteSpace(route)) Edge("route:" + route.Trim(), TypeId(type), "message", "static", node); + } + } + foreach (var attr in symbol.GetAttributes()) + { + var attrName = attr.AttributeClass?.ToDisplayString(); + if (symbol is IMethodSymbol method && attrName == "ShrinkEventBus.ShrinkSubscribeAttribute" && method.Parameters.Length > 0) + Edge(TypeId(method.Parameters[0].Type), id, "subscribes", "candidate", node); + if (attrName?.Contains("ShrinkCommand", StringComparison.Ordinal) == true && attr.ConstructorArguments.FirstOrDefault().Value is string command) + Edge("command:" + command, id, "command", "candidate", node); + } + } + if (node is not InvocationExpressionSyntax invocation) continue; + var info = model.GetSymbolInfo(invocation); + var target = info.Symbol as IMethodSymbol; + var enclosing = model.GetEnclosingSymbol(invocation.SpanStart); + if (enclosing == null) continue; + var from = Id(enclosing); + if (target == null) + { + foreach (var candidate in info.CandidateSymbols) Edge(from, Id(candidate), "calls", "candidate", node); + if (info.CandidateSymbols.Length == 0) Edge(from, invocation.Expression.ToString(), "calls", "unresolved", node); + continue; + } + var evidence = candidateOnly || target.IsVirtual || target.ContainingType.TypeKind == TypeKind.Interface || target.MethodKind == MethodKind.DelegateInvoke ? "candidate" : "static"; + Edge(from, Id(target), "calls", evidence, node); + var owner = target.ContainingNamespace.ToDisplayString(); + if (owner.StartsWith("ShrinkEventBus", StringComparison.Ordinal) && target.Name is "Post" or "PostAsync" && invocation.ArgumentList.Arguments.Count > 0) + { + var type = model.GetTypeInfo(invocation.ArgumentList.Arguments[0].Expression).Type; + if (type != null) Edge(from, TypeId(type), "publishes", evidence, node); + } + if (owner.StartsWith("ShrinkContext", StringComparison.Ordinal) && target.Name is "Get" or "Set" or "TryGet" && invocation.ArgumentList.Arguments.Count > 0) + { + var arg = invocation.ArgumentList.Arguments[0].Expression; + var key = model.GetConstantValue(arg); + var keyName = key.HasValue && key.Value is string value ? value : model.GetSymbolInfo(arg).Symbol?.ToDisplayString() ?? arg.ToString(); + var type = target.TypeArguments.FirstOrDefault()?.ToDisplayString() ?? "unknown"; + Edge(from, "key:" + keyName, target.Name == "Set" ? "provides:" + type : "consumes:" + type, evidence, node); + } + if (owner.StartsWith("ShrinkNetwork", StringComparison.Ordinal) && target.Name.StartsWith("Register", StringComparison.Ordinal) && target.TypeArguments.Length > 0) + { + Edge(TypeId(target.TypeArguments[0]), from, "registers", "candidate", node); + foreach (var arg in invocation.ArgumentList.Arguments) + { + var handler = model.GetSymbolInfo(arg.Expression).Symbol; + if (handler is IMethodSymbol) Edge(TypeId(target.TypeArguments[0]), Id(handler), "handler", "candidate", arg); + if (arg.Expression is LambdaExpressionSyntax lambda) + foreach (var call in lambda.DescendantNodes().OfType()) + if (model.GetSymbolInfo(call).Symbol is IMethodSymbol forwarded) + Edge(TypeId(target.TypeArguments[0]), Id(forwarded), "handler-forward", "candidate", call); + } + } + } + } + void Edge(string from, string to, string kind, string evidence, SyntaxNode node) => index.Relations.Add(new(from, to, kind, evidence, At(node))); + } + + public static void CheckRelations(Index index) + { + foreach (var group in index.Relations.Where(r => r.Kind == "message" && r.Evidence == "static").GroupBy(r => r.From)) + if (group.Select(r => r.To).Distinct().Count() > 1) + foreach (var edge in group) index.Findings.Add(new("SHRINK002", "error", $"Duplicate {group.Key}. Assign a unique opcode and nonempty route; see Network README.", edge.Location)); + foreach (var group in index.Relations.Where(r => r.Evidence == "static" && (r.Kind.StartsWith("provides:") || r.Kind.StartsWith("consumes:"))).GroupBy(r => r.To)) + { + var provided = group.Where(r => r.Kind.StartsWith("provides:")).Select(r => r.Kind[9..]).Distinct().ToArray(); + // Exact generic type mismatches may be legal covariance/interface use. Report for review, not as a proof of invalidity. + if (provided.Length > 1) index.Findings.Add(new("SHRINK104", "warning", $"{group.Key} is provided with different generic types: {string.Join(", ", provided)}. Check realm and assignability.", group.First().Location)); + } + } +} diff --git a/Tools/AgentSupport/Shrink.Inspect/Shrink.Inspect.csproj b/Tools/AgentSupport/Shrink.Inspect/Shrink.Inspect.csproj new file mode 100644 index 0000000..18b9b02 --- /dev/null +++ b/Tools/AgentSupport/Shrink.Inspect/Shrink.Inspect.csproj @@ -0,0 +1,18 @@ + + + Exe + net8.0 + LatestMajor + true + shrink + ShrinkSDK.Inspect + 0.1.2 + Bounded, evidence-based ShrinkSDK capability and code navigation. + + + + + + + + diff --git a/Tools/Release/package-versions.json b/Tools/Release/package-versions.json index 5db0f97..97e2c6f 100644 --- a/Tools/Release/package-versions.json +++ b/Tools/Release/package-versions.json @@ -4,12 +4,12 @@ { "directory": "ShrinkApp.Core", "name": "com.cneicy.shrink-app-core", - "version": "0.2.0", + "version": "0.2.3", "repository": "https://git.crash.work/ShrinkSDK/ShrinkApp.Core.git", "nugetPackages": [ { "id": "ShrinkSDK.App.Core", - "version": "0.2.0", + "version": "0.2.3", "project": "Assets/Modules/ShrinkApp.Core/DotNet~/ShrinkSDK.App.Core.csproj" } ] @@ -17,12 +17,12 @@ { "directory": "ShrinkApp.Starter.Basic", "name": "com.cneicy.shrink-app-starter-basic", - "version": "0.3.0", + "version": "0.3.7", "repository": "https://git.crash.work/ShrinkSDK/ShrinkApp.Starter.Basic.git", "nugetPackages": [ { "id": "ShrinkSDK.App.Starter.Basic", - "version": "0.3.0", + "version": "0.3.7", "project": "Assets/Modules/ShrinkApp.Starter.Basic/DotNet~/ShrinkSDK.App.Starter.Basic.csproj" } ] @@ -30,12 +30,12 @@ { "directory": "ShrinkCommand", "name": "com.cneicy.shrink-command", - "version": "0.3.0", + "version": "0.4.2", "repository": "https://git.crash.work/ShrinkSDK/ShrinkCommand.git", "nugetPackages": [ { "id": "ShrinkSDK.Command", - "version": "0.3.0", + "version": "0.4.2", "project": "Assets/Modules/ShrinkCommand/DotNet~/ShrinkSDK.Command.csproj" } ] @@ -43,12 +43,12 @@ { "directory": "ShrinkCommand.Integration.App", "name": "com.cneicy.shrink-command-integration-app", - "version": "0.2.0", + "version": "0.2.5", "repository": "https://git.crash.work/ShrinkSDK/ShrinkCommand.Integration.App.git", "nugetPackages": [ { "id": "ShrinkSDK.Command.Integration.App", - "version": "0.2.0", + "version": "0.2.5", "project": "Assets/Modules/ShrinkCommand.Integration.App/DotNet~/ShrinkSDK.Command.Integration.App.csproj" } ] @@ -56,12 +56,12 @@ { "directory": "ShrinkCommand.Integration.EventBus", "name": "com.cneicy.shrink-command-integration-eventbus", - "version": "0.2.0", + "version": "0.2.5", "repository": "https://git.crash.work/ShrinkSDK/ShrinkCommand.Integration.EventBus.git", "nugetPackages": [ { "id": "ShrinkSDK.Command.Integration.EventBus", - "version": "0.2.0", + "version": "0.2.5", "project": "Assets/Modules/ShrinkCommand.Integration.EventBus/DotNet~/ShrinkSDK.Command.Integration.EventBus.csproj" } ] @@ -69,12 +69,12 @@ { "directory": "ShrinkCommand.Integration.Network", "name": "com.cneicy.shrink-command-integration-network", - "version": "0.2.0", + "version": "0.2.5", "repository": "https://git.crash.work/ShrinkSDK/ShrinkCommand.Integration.Network.git", "nugetPackages": [ { "id": "ShrinkSDK.Command.Integration.Network", - "version": "0.2.0", + "version": "0.2.5", "project": "Assets/Modules/ShrinkCommand.Integration.Network/DotNet~/ShrinkSDK.Command.Integration.Network.csproj" } ] @@ -82,12 +82,12 @@ { "directory": "ShrinkContext.AppAdapter", "name": "com.cneicy.shrink-context-app-adapter", - "version": "0.2.0", + "version": "0.2.4", "repository": "https://git.crash.work/ShrinkSDK/ShrinkContext.AppAdapter.git", "nugetPackages": [ { "id": "ShrinkSDK.Context.AppAdapter", - "version": "0.2.0", + "version": "0.2.4", "project": "Assets/Modules/ShrinkContext.AppAdapter/DotNet~/ShrinkSDK.Context.AppAdapter.csproj" } ] @@ -95,12 +95,12 @@ { "directory": "ShrinkContext.Core", "name": "com.cneicy.shrink-context-core", - "version": "0.2.0", + "version": "0.3.0", "repository": "https://git.crash.work/ShrinkSDK/ShrinkContext.Core.git", "nugetPackages": [ { "id": "ShrinkSDK.Context.Core", - "version": "0.2.0", + "version": "0.3.0", "project": "Assets/Modules/ShrinkContext.Core/DotNet~/ShrinkSDK.Context.Core.csproj" } ] @@ -108,12 +108,12 @@ { "directory": "ShrinkContext.EventBusAdapter", "name": "com.cneicy.shrink-context-eventbus-adapter", - "version": "0.2.0", + "version": "0.2.4", "repository": "https://git.crash.work/ShrinkSDK/ShrinkContext.EventBusAdapter.git", "nugetPackages": [ { "id": "ShrinkSDK.Context.EventBusAdapter", - "version": "0.2.0", + "version": "0.2.4", "project": "Assets/Modules/ShrinkContext.EventBusAdapter/DotNet~/ShrinkSDK.Context.EventBusAdapter.csproj" } ] @@ -121,12 +121,12 @@ { "directory": "ShrinkDataSaver", "name": "com.cneicy.shrink-datasaver", - "version": "2.3.0", + "version": "2.4.0", "repository": "https://git.crash.work/ShrinkSDK/ShrinkDataSaver.git", "nugetPackages": [ { "id": "ShrinkSDK.DataSaver", - "version": "2.3.0", + "version": "2.4.0", "project": "Assets/Modules/ShrinkDataSaver/DotNet~/ShrinkSDK.DataSaver.csproj" } ] @@ -134,12 +134,12 @@ { "directory": "ShrinkDataSaver.Integration.App", "name": "com.cneicy.shrink-datasaver-integration-app", - "version": "0.2.0", + "version": "0.2.5", "repository": "https://git.crash.work/ShrinkSDK/ShrinkDataSaver.Integration.App.git", "nugetPackages": [ { "id": "ShrinkSDK.DataSaver.Integration.App", - "version": "0.2.0", + "version": "0.2.5", "project": "Assets/Modules/ShrinkDataSaver.Integration.App/DotNet~/ShrinkSDK.DataSaver.Integration.App.csproj" } ] @@ -147,12 +147,12 @@ { "directory": "ShrinkDataSaver.Integration.EventBus", "name": "com.cneicy.shrink-datasaver-integration-eventbus", - "version": "2.2.0", + "version": "2.2.5", "repository": "https://git.crash.work/ShrinkSDK/ShrinkDataSaver.Integration.EventBus.git", "nugetPackages": [ { "id": "ShrinkSDK.DataSaver.Integration.EventBus", - "version": "2.2.0", + "version": "2.2.5", "project": "Assets/Modules/ShrinkDataSaver.Integration.EventBus/DotNet~/ShrinkSDK.DataSaver.Integration.EventBus.csproj" } ] @@ -160,12 +160,12 @@ { "directory": "ShrinkEventBus", "name": "com.cneicy.shrink-eventbus", - "version": "2.1.0", + "version": "2.2.2", "repository": "https://git.crash.work/ShrinkSDK/ShrinkEventBus.git", "nugetPackages": [ { "id": "ShrinkSDK.EventBus", - "version": "2.1.0", + "version": "2.2.2", "project": "Assets/Modules/ShrinkEventBus/DotNet~/ShrinkSDK.EventBus.csproj" } ] @@ -173,13 +173,13 @@ { "directory": "ShrinkEventBus.Entities", "name": "com.cneicy.shrink-eventbus-entities", - "version": "0.1.2", + "version": "0.1.5", "repository": "https://git.crash.work/ShrinkSDK/ShrinkEventBus.Entities.git" }, { "directory": "ShrinkInstaller", "name": "com.cneicy.shrink-installer", - "version": "0.2.6", + "version": "0.2.13", "repository": "https://git.crash.work/ShrinkSDK/Installer.git", "nugetPackages": [ { @@ -192,17 +192,17 @@ { "directory": "ShrinkModFramework", "name": "com.cneicy.shrink-mod-framework", - "version": "0.3.0", + "version": "0.3.6", "repository": "https://git.crash.work/ShrinkSDK/ShrinkModFramework.git", "nugetPackages": [ { "id": "ShrinkSDK.ModFramework", - "version": "0.3.0", + "version": "0.3.6", "project": "Assets/Modules/ShrinkModFramework/DotNet~/ShrinkSDK.ModFramework.csproj" }, { "id": "ShrinkSDK.ModFramework.Godot", - "version": "0.1.0", + "version": "0.1.5", "project": "Assets/Modules/ShrinkModFramework/Godot~/ShrinkSDK.ModFramework.Godot.csproj" } ] @@ -210,12 +210,12 @@ { "directory": "ShrinkNetwork", "name": "com.cneicy.shrink-network", - "version": "0.3.0", + "version": "0.4.2", "repository": "https://git.crash.work/ShrinkSDK/ShrinkNetwork.git", "nugetPackages": [ { "id": "ShrinkSDK.Network", - "version": "0.3.0", + "version": "0.4.2", "project": "Assets/Modules/ShrinkNetwork/DotNet~/ShrinkSDK.Network.csproj" } ] @@ -223,12 +223,12 @@ { "directory": "ShrinkNetwork.Integration.App", "name": "com.cneicy.shrink-network-integration-app", - "version": "0.2.0", + "version": "0.2.5", "repository": "https://git.crash.work/ShrinkSDK/ShrinkNetwork.Integration.App.git", "nugetPackages": [ { "id": "ShrinkSDK.Network.Integration.App", - "version": "0.2.0", + "version": "0.2.5", "project": "Assets/Modules/ShrinkNetwork.Integration.App/DotNet~/ShrinkSDK.Network.Integration.App.csproj" } ] @@ -236,12 +236,12 @@ { "directory": "ShrinkNetwork.Integration.EventBus", "name": "com.cneicy.shrink-network-integration-eventbus", - "version": "0.2.0", + "version": "0.2.5", "repository": "https://git.crash.work/ShrinkSDK/ShrinkNetwork.Integration.EventBus.git", "nugetPackages": [ { "id": "ShrinkSDK.Network.Integration.EventBus", - "version": "0.2.0", + "version": "0.2.5", "project": "Assets/Modules/ShrinkNetwork.Integration.EventBus/DotNet~/ShrinkSDK.Network.Integration.EventBus.csproj" } ] @@ -262,12 +262,12 @@ { "directory": "ShrinkShared.CodeGen", "name": "com.cneicy.shrink-shared-codegen", - "version": "0.1.1", + "version": "0.2.1", "repository": "https://git.crash.work/ShrinkSDK/ShrinkShared.CodeGen.git", "nugetPackages": [ { "id": "ShrinkSDK.CodeGen", - "version": "0.1.0", + "version": "0.2.0", "project": "Assets/Modules/ShrinkShared.CodeGen/DotNet~/ShrinkSDK.CodeGen.Task/ShrinkSDK.CodeGen.Task.csproj" } ] @@ -294,7 +294,7 @@ "standaloneNugetPackages": [ { "id": "ShrinkSDK.Godot", - "version": "0.1.0", + "version": "0.1.5", "project": "Godot/Packages/ShrinkSDK.Godot/ShrinkSDK.Godot.csproj", "repository": "https://git.crash.work/ShrinkSDK/ShrinkGodot.git" }