From 0449e82c0ca76bb7d3b296d8ba1c1ae0040425b5 Mon Sep 17 00:00:00 2001 From: cneicy Date: Fri, 6 Nov 2026 20:52:09 +0800 Subject: [PATCH] perf: reduce diagnostics and queued dispatch allocations in 2.3.2 --- CHANGELOG.md | 7 +++ DotNet~/ShrinkSDK.EventBus.csproj | 2 +- README.md | 20 +++++--- Runtime/EventBus.cs | 42 ++++++++++++++--- Runtime/ShrinkBusSchedulers.cs | 46 ++++++++++--------- Samples~/DotNetConsole/Program.cs | 40 ++++++++++++++++ .../ShrinkEventBusDeferredDiagnosticsTests.cs | 29 ++++++++++++ package.json | 2 +- 8 files changed, 152 insertions(+), 36 deletions(-) create mode 100644 Samples~/DotNetConsole/Program.cs diff --git a/CHANGELOG.md b/CHANGELOG.md index 9cc4b71..6f363cb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,13 @@ 本文件记录 `ShrinkEventBus` 在当前工作区中的包内变更。 +## [2.3.2] - 2026-11-06 + +### Performance + +- 诊断观察者列表在订阅或退订时生成不可变快照,消除每次诊断派发的列表分配,保留逐个观察者的异常隔离。 +- 同步排队直接保存回调,移除额外闭包;主线程调度器复用泵送回调,保持延后事件的批次边界。 + ## [2.3.0] - 2026-11-06 ### Added diff --git a/DotNet~/ShrinkSDK.EventBus.csproj b/DotNet~/ShrinkSDK.EventBus.csproj index d54d74a..f5df8a1 100644 --- a/DotNet~/ShrinkSDK.EventBus.csproj +++ b/DotNet~/ShrinkSDK.EventBus.csproj @@ -5,7 +5,7 @@ ShrinkEventBus.Runtime ShrinkEventBus ShrinkSDK.EventBus - 2.3.1 + 2.3.2 ShrinkSDK typed event bus runtime. false diff --git a/README.md b/README.md index d5af685..d7e3084 100644 --- a/README.md +++ b/README.md @@ -166,23 +166,31 @@ public static class GlobalHandlers } ``` -Unity ILPostProcessor 会生成直接调用桥和静态模块初始化注册;纯 .NET incremental generator 要求 subscriber 是 `partial`,并生成 ValueTask handler 绑定。生产路径不枚举程序集和方法,不使用 `MethodInfo.Invoke`、`DynamicInvoke` 或 `object[]` 参数。 +Unity ILPostProcessor 会生成直接调用桥和静态模块初始化注册;NuGet 包通过 `ShrinkSDK.CodeGen` 生成绑定。生产路径不枚举程序集和方法,不使用 `MethodInfo.Invoke`、`DynamicInvoke` 或 `object[]` 参数。 ## Unity 与纯 .NET -Unity 包依赖 UniTask: +Unity 和正式 NuGet 包 `ShrinkSDK.EventBus` 均使用 UniTask,可直接 `await`: ```csharp UniTask PostAsync(...) ``` -纯 .NET 实现在 `Tools~/DotNet/ShrinkEventBus.Core`: +普通 C# 项目无需安装 Unity 或 Godot。使用 .NET 8 或更新 SDK 新建控制台项目后配置包源并安装: -```csharp -ValueTask PostAsync(...) +```shell +dotnet new console -n EventBusDemo +cd EventBusDemo +dotnet new nugetconfig +dotnet nuget add source https://git.crash.work/api/packages/ShrinkSDK/nuget/index.json --name ShrinkSDK --configfile nuget.config +dotnet add package ShrinkSDK.EventBus --version 2.3.2 ``` -两边共享事件与特性契约,awaitable 和调度实现按宿主选择,不把 UniTask/ValueTask 写进事件数据。 +UniTask、Runtime.Abstractions 和构建时 CodeGen 会随包自动还原,不需要手动添加分析器或项目引用。完整可运行示例见 [Samples~/DotNetConsole](Samples~/DotNetConsole/Program.cs),可用它替换新项目的 `Program.cs` 后执行 `dotnet run -c Release`。 + +控制台和后台服务建议显式创建 `Inline`、`DedicatedThread` 或 `TaskPool` Bus。`MainThread` 跨线程派发及 `PostDeferred` 需要宿主提供可用的主线程调度器;普通控制台没有 Unity/Godot 的帧循环。 + +`Tools~/DotNet/ShrinkEventBus.Core` 是独立的 ValueTask 工具实现,不是上述正式 NuGet 包的运行时。事件数据不应依赖 UniTask/ValueTask。 ## ECS/Burst diff --git a/Runtime/EventBus.cs b/Runtime/EventBus.cs index 0b3f4b3..cd3fbfd 100644 --- a/Runtime/EventBus.cs +++ b/Runtime/EventBus.cs @@ -57,6 +57,8 @@ namespace ShrinkEventBus private static readonly IShrinkEventBus DefaultBus = CreateDefaultBus(); private static Action? _posted; private static Action? _detailedPosted; + private static readonly object DetailedObserverGate = new(); + private static Action[]? _detailedObserverSnapshot; internal static event Action Posted { @@ -78,14 +80,41 @@ namespace ShrinkEventBus internal static event Action DetailedPosted { - add => _detailedPosted += value; - remove => _detailedPosted -= value; + add + { + lock (DetailedObserverGate) + { + _detailedPosted += value; + PublishDetailedObserverSnapshot(); + } + } + remove + { + lock (DetailedObserverGate) + { + _detailedPosted -= value; + PublishDetailedObserverSnapshot(); + } + } + } + + private static void PublishDetailedObserverSnapshot() + { + var invocationList = _detailedPosted?.GetInvocationList(); + Action[]? snapshot = null; + if (invocationList != null) + { + snapshot = new Action[invocationList.Length]; + for (var i = 0; i < snapshot.Length; i++) + snapshot[i] = (Action)invocationList[i]; + } + Volatile.Write(ref _detailedObserverSnapshot, snapshot); } internal static bool HasDetailedPostedObservers { [MethodImpl(MethodImplOptions.AggressiveInlining)] - get => Volatile.Read(ref _detailedPosted) != null; + get => Volatile.Read(ref _detailedObserverSnapshot) != null; } public static IShrinkEventBus Default => DefaultBus; @@ -212,15 +241,14 @@ namespace ShrinkEventBus [MethodImpl(MethodImplOptions.AggressiveInlining)] internal static void NotifyDetailedPosted(ShrinkEventTrace trace) { - var observers = Volatile.Read(ref _detailedPosted); + var observers = Volatile.Read(ref _detailedObserverSnapshot); if (observers == null) return; - var invocationList = observers.GetInvocationList(); - for (var i = 0; i < invocationList.Length; i++) + for (var i = 0; i < observers.Length; i++) { try { - ((Action)invocationList[i])(trace); + observers[i](trace); } catch (Exception exception) { diff --git a/Runtime/ShrinkBusSchedulers.cs b/Runtime/ShrinkBusSchedulers.cs index 98dcb15..6c27050 100644 --- a/Runtime/ShrinkBusSchedulers.cs +++ b/Runtime/ShrinkBusSchedulers.cs @@ -59,15 +59,29 @@ namespace ShrinkEventBus internal sealed class ShrinkSchedulerWorkItem { private readonly UniTaskCompletionSource? _completion; + private readonly Delegate _action; + + public ShrinkSchedulerWorkItem(Action action) + { + _action = action ?? throw new ArgumentNullException(nameof(action)); + } public ShrinkSchedulerWorkItem(Func action, bool awaitable) { - Action = action ?? throw new ArgumentNullException(nameof(action)); + _action = action ?? throw new ArgumentNullException(nameof(action)); if (awaitable) _completion = new UniTaskCompletionSource(); } - public Func Action { get; } + public UniTask Invoke() + { + if (_action is Action synchronousAction) + { + synchronousAction(); + return UniTask.CompletedTask; + } + return ((Func)_action)(); + } public UniTask Completion => _completion?.Task ?? UniTask.CompletedTask; public void Complete() => _completion?.TrySetResult(); @@ -169,7 +183,7 @@ namespace ShrinkEventBus { try { - await item.Action(); + await item.Invoke(); item.Complete(); } catch (OperationCanceledException ex) @@ -187,7 +201,7 @@ namespace ShrinkEventBus { try { - item.Action().GetAwaiter().GetResult(); + item.Invoke().GetAwaiter().GetResult(); item.Complete(); } catch (OperationCanceledException ex) @@ -232,6 +246,7 @@ namespace ShrinkEventBus private readonly MainThreadDispatcherContract _dispatcher; private readonly ShrinkSDK.Runtime.IShrinkDeferredMainThreadDispatcher? _deferredDispatcher; private readonly object _gate = new(); + private readonly Action _pump; private int _pumpScheduled; private int _disposed; private bool _discardPending; @@ -241,6 +256,7 @@ namespace ShrinkEventBus _queue = new ShrinkSchedulerQueue(name, options); _dispatcher = ShrinkEventBusRuntime.MainThreadDispatcher; _deferredDispatcher = _dispatcher as ShrinkSDK.Runtime.IShrinkDeferredMainThreadDispatcher; + _pump = () => PumpAsync().Forget(); } public bool IsOnSchedulerThread => _dispatcher.IsMainThread; @@ -269,11 +285,7 @@ namespace ShrinkEventBus lock (_gate) { if (_disposed != 0) return ShrinkDeferredPostStatus.Stopped; - var item = new ShrinkSchedulerWorkItem(() => - { - action(); - return UniTask.CompletedTask; - }, awaitable: false); + var item = new ShrinkSchedulerWorkItem(action); if (!_queue.TryEnqueue(item)) return ShrinkDeferredPostStatus.QueueFull; return SchedulePump() ? ShrinkDeferredPostStatus.Accepted : ShrinkDeferredPostStatus.SchedulerRejected; } @@ -333,8 +345,8 @@ namespace ShrinkEventBus try { accepted = _deferredDispatcher != null - ? _deferredDispatcher.TryPostDeferred(() => PumpAsync().Forget()) - : _dispatcher.TryPost(() => PumpAsync().Forget()); + ? _deferredDispatcher.TryPostDeferred(_pump) + : _dispatcher.TryPost(_pump); } catch { @@ -398,11 +410,7 @@ namespace ShrinkEventBus { if (Volatile.Read(ref _disposed) != 0) return false; - var item = new ShrinkSchedulerWorkItem(() => - { - action(); - return UniTask.CompletedTask; - }, awaitable: false); + var item = new ShrinkSchedulerWorkItem(action); if (!_queue.TryEnqueue(item)) return false; ScheduleWorkers(); @@ -503,11 +511,7 @@ namespace ShrinkEventBus return true; } - var item = new ShrinkSchedulerWorkItem(() => - { - action(); - return UniTask.CompletedTask; - }, awaitable: false); + var item = new ShrinkSchedulerWorkItem(action); if (!_queue.TryEnqueue(item)) return false; _signal.Set(); diff --git a/Samples~/DotNetConsole/Program.cs b/Samples~/DotNetConsole/Program.cs new file mode 100644 index 0000000..c749768 --- /dev/null +++ b/Samples~/DotNetConsole/Program.cs @@ -0,0 +1,40 @@ +using ShrinkEventBus; + +using var host = new ShrinkEventBusHost(); +var bus = host.CreateBus(ShrinkBusKey.Game, ShrinkBusOptions.Inline()); +var subscriber = new OrderSubscriber(); +using var binding = host.Attach(subscriber); +var diagnostics = 0; +using var observation = EventBus.ObserveDiagnostics(_ => diagnostics++); + +bus.Post(new OrderPlaced(2)); +await bus.PostAsync(new OrderPlaced(3)); +if (subscriber.Total != 5 || diagnostics != 2) + throw new InvalidOperationException("Generated subscription or diagnostics failed."); + +var workerKey = new ShrinkBusKey("worker", "orders"); +var worker = host.CreateBus(workerKey, ShrinkBusOptions.DedicatedThread()); +var workerSubscriber = new OrderSubscriber(); +using var workerBinding = host.Attach(workerSubscriber, workerKey); +await worker.PostAsync(new OrderPlaced(7)); +if (workerSubscriber.Total != 7) + throw new InvalidOperationException("Dedicated-thread delivery failed."); + +binding.Dispose(); +bus.Post(new OrderPlaced(10)); +if (subscriber.Total != 5) + throw new InvalidOperationException("Unsubscribe failed."); + +await host.ShutdownAsync(); +Console.WriteLine("EVENTBUS_CONSOLE_PASS: sync, async, diagnostics, worker, unsubscribe"); + +public readonly record struct OrderPlaced(int Amount) : IShrinkEvent; + +[ShrinkEventSubscriber] +public sealed partial class OrderSubscriber +{ + public int Total { get; private set; } + + [ShrinkSubscribe] + private void OnOrder(OrderPlaced order) => Total += order.Amount; +} diff --git a/Tests/ShrinkEventBusDeferredDiagnosticsTests.cs b/Tests/ShrinkEventBusDeferredDiagnosticsTests.cs index bac92a3..bd5a0ee 100644 --- a/Tests/ShrinkEventBusDeferredDiagnosticsTests.cs +++ b/Tests/ShrinkEventBusDeferredDiagnosticsTests.cs @@ -73,6 +73,35 @@ namespace ShrinkEventBus.Tests Assert.AreEqual(1, secondObserverCalls); } + [Test] + public void ObserveDiagnostics_MutationsApplyToNextDispatch() + { + var calls = new List(); + IDisposable second = null; + IDisposable added = null; + using var first = EventBus.ObserveDiagnostics(_ => + { + calls.Add("first"); + second.Dispose(); + if (added == null) + added = EventBus.ObserveDiagnostics(__ => calls.Add("added")); + }); + second = EventBus.ObserveDiagnostics(_ => calls.Add("second")); + try + { + EventBus.NotifyDetailedPosted(default); + CollectionAssert.AreEqual(new[] { "first", "second" }, calls); + calls.Clear(); + EventBus.NotifyDetailedPosted(default); + CollectionAssert.AreEqual(new[] { "first", "added" }, calls); + } + finally + { + second.Dispose(); + added?.Dispose(); + } + } + [Test] public void PostDeferred_DoesNotDispatchBeforePump() { diff --git a/package.json b/package.json index 28fc84c..3bfe68d 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "com.cneicy.shrink-eventbus", - "version": "2.3.1", + "version": "2.3.2", "displayName": "ShrinkEventBus", "description": "多 Bus、特性强类型注册、UniTask 调度与低分配发布的 Unity 事件总线。", "unity": "2022.3",