Files
Workspace/Tools/AgentSupport/Benchmarks/Program.cs
T
cneicy 08246ddb01
Validate ShrinkSDK Workspace / catalog (push) Successful in 5s
Validate ShrinkSDK Workspace / unity (push) Successful in 3m20s
feat: integrate performance upgrade and agent inspection tooling
2026-09-29 10:16:02 +08:00

174 lines
11 KiB
C#

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<CodecMessage>();
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<object>();
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<CodecMessage>();
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<CodecMessage>(1); server.RegisterMessage<CodecMessage>(1);
server.RegisterHandler<CodecMessage>((_, 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<Event>(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<Event>(ShrinkGeneratedBinding.RuntimeResolver, null, null, null, _ => { }, ShrinkEventPriority.Normal, 0, false).Dispose());
using (var binding = ShrinkGeneratedBinding.SubscribeAsync<Event>(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<UniTask<ShrinkNetworkQueueResult>>();
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<SavePhaseTimings>();
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<string> Inject { get; } = dependency == null ? Array.Empty<string>() : new[] { dependency };
public IReadOnlyList<string> 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<string> Inject { get; } = dependency == null ? Array.Empty<string>() : new[] { dependency };
public IReadOnlyList<string> Provide { get; } = new[] { key };
public UniTask ApplyAsync(BC.ShrinkCtx ctx, object? config) { ctx.Set(key, this); return UniTask.CompletedTask; }
}