feat(eventbus): add platform runtimes and benchmarks
Add the Entities NativeQueue adapter, standalone .NET runtime and source generator, reproducible smoke coverage, and Unity benchmark assets for EventBus 2.0.
This commit is contained in:
@@ -0,0 +1,282 @@
|
||||
#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<TEvent>(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<UniTask<ShrinkPostResult>> 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user