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; } }