#nullable enable using System; using System.Diagnostics; using System.Text; using System.Threading; using Cysharp.Threading.Tasks; using UnityEngine; namespace ShrinkEventBus.Benchmark { public static class ShrinkEventBusBenchmark { private readonly struct BenchmarkEvent : IShrinkEvent { public BenchmarkEvent(int value) => Value = value; public int Value { get; } } private sealed class BenchmarkClassEvent : IShrinkEvent { public int Value { get; set; } } private sealed class CanceledBenchmarkEvent : IShrinkCancelableEvent { public bool IsCanceled { get; private set; } = true; public void SetCanceled(bool value) => IsCanceled = value; } [ShrinkEventSubscriber] private sealed class OneHandler { [ShrinkSubscribe(Bus = "benchmark:one")] private void H01(BenchmarkEvent value) => Consume(value); } [ShrinkEventSubscriber] private sealed class EightHandlers { [ShrinkSubscribe(Bus = "benchmark:eight")] private void H01(BenchmarkEvent value) => Consume(value); [ShrinkSubscribe(Bus = "benchmark:eight")] private void H02(BenchmarkEvent value) => Consume(value); [ShrinkSubscribe(Bus = "benchmark:eight")] private void H03(BenchmarkEvent value) => Consume(value); [ShrinkSubscribe(Bus = "benchmark:eight")] private void H04(BenchmarkEvent value) => Consume(value); [ShrinkSubscribe(Bus = "benchmark:eight")] private void H05(BenchmarkEvent value) => Consume(value); [ShrinkSubscribe(Bus = "benchmark:eight")] private void H06(BenchmarkEvent value) => Consume(value); [ShrinkSubscribe(Bus = "benchmark:eight")] private void H07(BenchmarkEvent value) => Consume(value); [ShrinkSubscribe(Bus = "benchmark:eight")] private void H08(BenchmarkEvent value) => Consume(value); } [ShrinkEventSubscriber] private sealed class EightClassHandlers { [ShrinkSubscribe(Bus = "benchmark:eight-class")] private void H01(BenchmarkClassEvent value) => Consume(value); [ShrinkSubscribe(Bus = "benchmark:eight-class")] private void H02(BenchmarkClassEvent value) => Consume(value); [ShrinkSubscribe(Bus = "benchmark:eight-class")] private void H03(BenchmarkClassEvent value) => Consume(value); [ShrinkSubscribe(Bus = "benchmark:eight-class")] private void H04(BenchmarkClassEvent value) => Consume(value); [ShrinkSubscribe(Bus = "benchmark:eight-class")] private void H05(BenchmarkClassEvent value) => Consume(value); [ShrinkSubscribe(Bus = "benchmark:eight-class")] private void H06(BenchmarkClassEvent value) => Consume(value); [ShrinkSubscribe(Bus = "benchmark:eight-class")] private void H07(BenchmarkClassEvent value) => Consume(value); [ShrinkSubscribe(Bus = "benchmark:eight-class")] private void H08(BenchmarkClassEvent value) => Consume(value); } [ShrinkEventSubscriber] private sealed class ThirtyHandlers { [ShrinkSubscribe(Bus = "benchmark:thirty")] private void H01(BenchmarkEvent value) => Consume(value); [ShrinkSubscribe(Bus = "benchmark:thirty")] private void H02(BenchmarkEvent value) => Consume(value); [ShrinkSubscribe(Bus = "benchmark:thirty")] private void H03(BenchmarkEvent value) => Consume(value); [ShrinkSubscribe(Bus = "benchmark:thirty")] private void H04(BenchmarkEvent value) => Consume(value); [ShrinkSubscribe(Bus = "benchmark:thirty")] private void H05(BenchmarkEvent value) => Consume(value); [ShrinkSubscribe(Bus = "benchmark:thirty")] private void H06(BenchmarkEvent value) => Consume(value); [ShrinkSubscribe(Bus = "benchmark:thirty")] private void H07(BenchmarkEvent value) => Consume(value); [ShrinkSubscribe(Bus = "benchmark:thirty")] private void H08(BenchmarkEvent value) => Consume(value); [ShrinkSubscribe(Bus = "benchmark:thirty")] private void H09(BenchmarkEvent value) => Consume(value); [ShrinkSubscribe(Bus = "benchmark:thirty")] private void H10(BenchmarkEvent value) => Consume(value); [ShrinkSubscribe(Bus = "benchmark:thirty")] private void H11(BenchmarkEvent value) => Consume(value); [ShrinkSubscribe(Bus = "benchmark:thirty")] private void H12(BenchmarkEvent value) => Consume(value); [ShrinkSubscribe(Bus = "benchmark:thirty")] private void H13(BenchmarkEvent value) => Consume(value); [ShrinkSubscribe(Bus = "benchmark:thirty")] private void H14(BenchmarkEvent value) => Consume(value); [ShrinkSubscribe(Bus = "benchmark:thirty")] private void H15(BenchmarkEvent value) => Consume(value); [ShrinkSubscribe(Bus = "benchmark:thirty")] private void H16(BenchmarkEvent value) => Consume(value); [ShrinkSubscribe(Bus = "benchmark:thirty")] private void H17(BenchmarkEvent value) => Consume(value); [ShrinkSubscribe(Bus = "benchmark:thirty")] private void H18(BenchmarkEvent value) => Consume(value); [ShrinkSubscribe(Bus = "benchmark:thirty")] private void H19(BenchmarkEvent value) => Consume(value); [ShrinkSubscribe(Bus = "benchmark:thirty")] private void H20(BenchmarkEvent value) => Consume(value); [ShrinkSubscribe(Bus = "benchmark:thirty")] private void H21(BenchmarkEvent value) => Consume(value); [ShrinkSubscribe(Bus = "benchmark:thirty")] private void H22(BenchmarkEvent value) => Consume(value); [ShrinkSubscribe(Bus = "benchmark:thirty")] private void H23(BenchmarkEvent value) => Consume(value); [ShrinkSubscribe(Bus = "benchmark:thirty")] private void H24(BenchmarkEvent value) => Consume(value); [ShrinkSubscribe(Bus = "benchmark:thirty")] private void H25(BenchmarkEvent value) => Consume(value); [ShrinkSubscribe(Bus = "benchmark:thirty")] private void H26(BenchmarkEvent value) => Consume(value); [ShrinkSubscribe(Bus = "benchmark:thirty")] private void H27(BenchmarkEvent value) => Consume(value); [ShrinkSubscribe(Bus = "benchmark:thirty")] private void H28(BenchmarkEvent value) => Consume(value); [ShrinkSubscribe(Bus = "benchmark:thirty")] private void H29(BenchmarkEvent value) => Consume(value); [ShrinkSubscribe(Bus = "benchmark:thirty")] private void H30(BenchmarkEvent value) => Consume(value); } [ShrinkEventSubscriber] private sealed class CanceledHandlers { [ShrinkSubscribe(Bus = "benchmark:canceled")] private void H01(CanceledBenchmarkEvent value) => _sink++; [ShrinkSubscribe(Bus = "benchmark:canceled")] private void H02(CanceledBenchmarkEvent value) => _sink++; [ShrinkSubscribe(Bus = "benchmark:canceled")] private void H03(CanceledBenchmarkEvent value) => _sink++; [ShrinkSubscribe(Bus = "benchmark:canceled")] private void H04(CanceledBenchmarkEvent value) => _sink++; [ShrinkSubscribe(Bus = "benchmark:canceled")] private void H05(CanceledBenchmarkEvent value) => _sink++; [ShrinkSubscribe(Bus = "benchmark:canceled")] private void H06(CanceledBenchmarkEvent value) => _sink++; [ShrinkSubscribe(Bus = "benchmark:canceled")] private void H07(CanceledBenchmarkEvent value) => _sink++; [ShrinkSubscribe(Bus = "benchmark:canceled")] private void H08(CanceledBenchmarkEvent value) => _sink++; [ShrinkSubscribe(Bus = "benchmark:canceled")] private void H09(CanceledBenchmarkEvent value) => _sink++; [ShrinkSubscribe(Bus = "benchmark:canceled")] private void H10(CanceledBenchmarkEvent value) => _sink++; } [ShrinkEventSubscriber] private sealed class AsyncHandler { [ShrinkSubscribe(Bus = "benchmark:async")] private UniTask Handle(BenchmarkEvent value, CancellationToken cancellationToken) { Consume(value); return UniTask.CompletedTask; } } [ShrinkEventSubscriber] private sealed class RegistrationHandler { [ShrinkSubscribe(Bus = "benchmark:registration")] private void Handle(BenchmarkEvent value) => Consume(value); } private static int _sink; public static string Run(int iterations = 1_000_000, int massIterations = 10_000, int registrationIterations = 5_000) { var keys = new[] { new ShrinkBusKey("benchmark", "empty"), new ShrinkBusKey("benchmark", "one"), new ShrinkBusKey("benchmark", "eight"), new ShrinkBusKey("benchmark", "eight-class"), new ShrinkBusKey("benchmark", "thirty"), new ShrinkBusKey("benchmark", "canceled"), new ShrinkBusKey("benchmark", "async"), new ShrinkBusKey("benchmark", "registration") }; using var host = new ShrinkEventBusHost(); var empty = host.CreateBus(keys[0], ShrinkBusOptions.Inline()); var one = host.CreateBus(keys[1], ShrinkBusOptions.Inline()); var eight = host.CreateBus(keys[2], ShrinkBusOptions.Inline()); var eightClass = host.CreateBus(keys[3], ShrinkBusOptions.Inline()); var thirty = host.CreateBus(keys[4], ShrinkBusOptions.Inline()); var canceled = host.CreateBus(keys[5], ShrinkBusOptions.Inline()); var asyncBus = host.CreateBus(keys[6], ShrinkBusOptions.Inline()); host.CreateBus(keys[7], ShrinkBusOptions.Inline()); using var oneBinding = host.Attach(new OneHandler()); using var eightBinding = host.Attach(new EightHandlers()); using var eightClassBinding = host.Attach(new EightClassHandlers()); using var thirtyBinding = host.Attach(new ThirtyHandlers()); using var canceledBinding = host.Attach(new CanceledHandlers()); using var asyncBinding = host.Attach(new AsyncHandler()); var report = new StringBuilder(2048); report.AppendLine("ShrinkEventBus 2.0 Benchmark Report"); report.AppendLine($"iter={iterations} mass={massIterations} reg={registrationIterations}"); report.AppendLine($"Unity {Application.unityVersion}, {Application.platform}, PlayMode={Application.isPlaying}"); report.AppendLine(); var value = new BenchmarkEvent(1); var classValue = new BenchmarkClassEvent { Value = 1 }; var canceledValue = new CanceledBenchmarkEvent(); Warmup(empty, one, eight, eightClass, thirty, canceled, value, classValue, canceledValue); MeasurePost(report, "Post / 0 handlers", iterations, empty, in value); MeasurePost(report, "Post / 1 handler", iterations, one, in value); MeasurePost(report, "Post / 8 handlers", massIterations, eight, in value); MeasurePost(report, "Post / 8 handlers class", massIterations, eightClass, in classValue); MeasurePost(report, "Post / 30 handlers", massIterations, thirty, in value); MeasurePost(report, "Post / canceled skips 10", massIterations, canceled, in canceledValue); MeasureAsync(report, "PostAsync / 1 async handler", massIterations, () => asyncBus.PostAsync(value)); Measure(report, "Generated Attach + Dispose", registrationIterations, () => { host.Attach(new RegistrationHandler()).Dispose(); }); var before = GC.GetAllocatedBytesForCurrentThread(); for (var i = 0; i < iterations; i++) one.Post(in value); var allocated = GC.GetAllocatedBytesForCurrentThread() - before; report.AppendLine($"Post / struct hot path allocation : {allocated} B / {iterations} ops"); report.AppendLine($"sink={_sink}"); var result = report.ToString(); UnityEngine.Debug.Log(result); return result; } private static void Warmup(IShrinkEventBus empty, IShrinkEventBus one, IShrinkEventBus eight, IShrinkEventBus eightClass, IShrinkEventBus thirty, IShrinkEventBus canceled, BenchmarkEvent value, BenchmarkClassEvent classValue, CanceledBenchmarkEvent canceledValue) { for (var i = 0; i < 10_000; i++) { empty.Post(in value); one.Post(in value); eight.Post(in value); eightClass.Post(classValue); thirty.Post(in value); canceled.Post(canceledValue); } GC.Collect(); GC.WaitForPendingFinalizers(); GC.Collect(); } private static void Measure(StringBuilder report, string name, int iterations, Action action) { var gen0 = GC.CollectionCount(0); var beforeBytes = GC.GetAllocatedBytesForCurrentThread(); var stopwatch = Stopwatch.StartNew(); for (var i = 0; i < iterations; i++) action(); stopwatch.Stop(); AppendResult(report, name, iterations, stopwatch.Elapsed.TotalMilliseconds, GC.GetAllocatedBytesForCurrentThread() - beforeBytes, GC.CollectionCount(0) - gen0); } private static void MeasurePost(StringBuilder report, string name, int iterations, IShrinkEventBus bus, in TEvent eventData) where TEvent : IShrinkEvent { var gen0 = GC.CollectionCount(0); var beforeBytes = GC.GetAllocatedBytesForCurrentThread(); var stopwatch = Stopwatch.StartNew(); for (var i = 0; i < iterations; i++) bus.Post(in eventData); stopwatch.Stop(); AppendResult(report, name, iterations, stopwatch.Elapsed.TotalMilliseconds, GC.GetAllocatedBytesForCurrentThread() - beforeBytes, GC.CollectionCount(0) - gen0); } private static void MeasureAsync(StringBuilder report, string name, int iterations, Func> action) { var gen0 = GC.CollectionCount(0); var beforeBytes = GC.GetAllocatedBytesForCurrentThread(); var stopwatch = Stopwatch.StartNew(); for (var i = 0; i < iterations; i++) action().GetAwaiter().GetResult(); stopwatch.Stop(); AppendResult(report, name, iterations, stopwatch.Elapsed.TotalMilliseconds, GC.GetAllocatedBytesForCurrentThread() - beforeBytes, GC.CollectionCount(0) - gen0); } private static void AppendResult(StringBuilder report, string name, int iterations, double milliseconds, long allocatedBytes, int gen0Collections) { var microseconds = milliseconds * 1000d / iterations; var throughput = iterations / (milliseconds / 1000d); report.AppendLine($"{name,-34} {milliseconds,10:F3} ms {microseconds,9:F4} us/op " + $"{throughput,12:F0} ops/s alloc={allocatedBytes} B gen0={gen0Collections}"); } private static void Consume(BenchmarkEvent value) { _sink = unchecked(_sink + value.Value); } private static void Consume(BenchmarkClassEvent value) { _sink = unchecked(_sink + value.Value); } } }