174 lines
11 KiB
C#
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; }
|
|
}
|