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:
2026-08-26 01:17:39 +08:00
parent ad5a7b68a3
commit 724e0bc8d8
32 changed files with 1813 additions and 0 deletions
@@ -0,0 +1,72 @@
ShrinkEventBus 2.0 Benchmark Report
===================================
Date: 2026-08-24
Unity: 2022.3.62f3
Platform: WindowsEditor, Play Mode
Host: independent ShrinkEventBusHost, Inline scheduler
Diagnostics: independent Host; detailed global capture does not observe it
Final run: iter=10,000,000 mass=1,000,000 reg=5,000, three rounds
Median results
--------------
Post / 0 handlers
129,282,649 ops/s
Post / 1 handler
67,249,586 ops/s
Post / 8 handlers, struct payload
35,024,798 ops/s
Post / 8 handlers, class payload
25,321,199 ops/s
Post / 30 handlers
10,823,612 ops/s
Post / canceled skips 10
9,029,517 ops/s
PostAsync / 1 async handler
5,909,446 ops/s
Generated Attach + Dispose
674,291 ops/s
Struct hot path allocation
0 B / 10,000,000 Post operations
Comparison
----------
Supplied MessagePipe chart, class payload, 8 handlers:
25,639,260 publish/s
ShrinkEventBus 2.0, class payload, 8 handlers:
25,321,199 publish/s median
ShrinkEventBus 2.0, struct payload, 8 handlers:
35,024,798 publish/s median
Supplied ShrinkEventBus 1.3.0 sample:
0 handlers: 8,971,378 -> 129,282,649 ops/s
1 handler: 6,132,242 -> 67,249,586 ops/s
30 handlers: 258,805 -> 10,823,612 ops/s
canceled skips 10: 1,894,776 -> 9,029,517 ops/s
1 async handler: 1,168,634 -> 5,909,446 ops/s
instance scan/register: 76,353 -> 674,291 generated Attach/s
Implementation notes
--------------------
- Inline buses use a dedicated implementation so the JIT can inline the channel dispatch path.
- Non-cancelable, all-sync channels build a priority-ordered multicast dispatcher when subscriptions change.
- ShrinkPostResult stores Accepted/Handled/Canceled/Failure in one 32-bit value.
- Generic bus slots and handler snapshots are published with release semantics and read without a publish lock.
- Mixed sync/async channels, cancelable events, queued schedulers and PostAsync retain the full semantic path.
Results are local editor measurements, not cross-device guarantees. The supplied MessagePipe chart was
produced by a different runtime and machine; the class-payload comparison only establishes the same
order of throughput. Recheck on the target Player and hardware before release.
@@ -0,0 +1,7 @@
fileFormatVersion: 2
guid: 6e379caaacb945699a297286798bc4e2
TextScriptImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: 2cc73e57d6ec4457a425843f734b6a70
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,13 @@
{
"name": "ShrinkEventBus.Benchmark",
"rootNamespace": "ShrinkEventBus.Benchmark",
"references": [
"ShrinkEventBus.Runtime",
"UniTask"
],
"includePlatforms": [],
"excludePlatforms": [],
"allowUnsafeCode": false,
"overrideReferences": false,
"autoReferenced": true
}
@@ -0,0 +1,7 @@
fileFormatVersion: 2
guid: 4488cfe64b374f30b8081ce2f10b1ed5
AssemblyDefinitionImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:
@@ -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);
}
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 76b9239259544977bd27221d3607aed1
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: a41c52d2d40c42479d1f1dfd2d35ae3c
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant: