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
+205
View File
@@ -0,0 +1,205 @@
#nullable enable
using System;
using System.Collections.Concurrent;
using System.Threading;
using System.Threading.Tasks;
namespace ShrinkEventBus
{
internal interface IDotNetScheduler : IDisposable
{
bool IsOnSchedulerThread { get; }
bool TryPost(Action action);
ValueTask PostAsync(Func<ValueTask> action, CancellationToken cancellationToken);
}
internal sealed class QueueFullException : InvalidOperationException { }
internal static class DotNetSchedulerFactory
{
public static IDotNetScheduler Create(string name, ShrinkBusOptions options)
{
return options.Scheduler switch
{
ShrinkBusSchedulerKind.MainThread => new SynchronizationContextScheduler(options),
ShrinkBusSchedulerKind.DedicatedThread => new DedicatedScheduler(name, options),
ShrinkBusSchedulerKind.TaskPool => new TaskPoolScheduler(options),
_ => new InlineScheduler()
};
}
}
internal sealed class InlineScheduler : IDotNetScheduler
{
public bool IsOnSchedulerThread => true;
public bool TryPost(Action action) { action(); return true; }
public ValueTask PostAsync(Func<ValueTask> action, CancellationToken cancellationToken)
{
cancellationToken.ThrowIfCancellationRequested();
return action();
}
public void Dispose() { }
}
internal sealed class SynchronizationContextScheduler : IDotNetScheduler
{
private readonly SynchronizationContext _context;
private readonly int _threadId;
private int _disposed;
public SynchronizationContextScheduler(ShrinkBusOptions options)
{
_context = SynchronizationContext.Current ?? throw new InvalidOperationException(
"MainThread scheduler requires a current SynchronizationContext in a pure .NET host.");
_threadId = Thread.CurrentThread.ManagedThreadId;
}
public bool IsOnSchedulerThread => Thread.CurrentThread.ManagedThreadId == _threadId;
public bool TryPost(Action action)
{
if (Volatile.Read(ref _disposed) != 0)
return false;
if (IsOnSchedulerThread)
action();
else
_context.Post(_ => action(), null);
return true;
}
public ValueTask PostAsync(Func<ValueTask> action, CancellationToken cancellationToken)
{
if (IsOnSchedulerThread)
return action();
return new ValueTask(PostCoreAsync(action, cancellationToken));
}
private Task PostCoreAsync(Func<ValueTask> action, CancellationToken cancellationToken)
{
var completion = new TaskCompletionSource<bool>(TaskCreationOptions.RunContinuationsAsynchronously);
_context.Post(async _ =>
{
try { cancellationToken.ThrowIfCancellationRequested(); await action(); completion.SetResult(true); }
catch (OperationCanceledException) { completion.SetCanceled(); }
catch (Exception ex) { completion.SetException(ex); }
}, null);
return completion.Task;
}
public void Dispose() => Interlocked.Exchange(ref _disposed, 1);
}
internal sealed class TaskPoolScheduler : IDotNetScheduler
{
private readonly SemaphoreSlim _concurrency;
private readonly int _capacity;
private int _pending;
private int _disposed;
public TaskPoolScheduler(ShrinkBusOptions options)
{
var concurrency = options.DispatchMode == ShrinkDispatchMode.Ordered ? 1 : options.MaxConcurrency;
_concurrency = new SemaphoreSlim(concurrency, concurrency);
_capacity = options.QueueCapacity;
}
public bool IsOnSchedulerThread => false;
public bool TryPost(Action action)
{
if (Volatile.Read(ref _disposed) != 0 || Interlocked.Increment(ref _pending) > _capacity)
{
Interlocked.Decrement(ref _pending);
return false;
}
_ = Task.Run(async () =>
{
await _concurrency.WaitAsync().ConfigureAwait(false);
try { action(); }
finally { _concurrency.Release(); Interlocked.Decrement(ref _pending); }
});
return true;
}
public async ValueTask PostAsync(Func<ValueTask> action, CancellationToken cancellationToken)
{
if (Volatile.Read(ref _disposed) != 0 || Interlocked.Increment(ref _pending) > _capacity)
{
Interlocked.Decrement(ref _pending);
throw new QueueFullException();
}
await _concurrency.WaitAsync(cancellationToken).ConfigureAwait(false);
try { await action().ConfigureAwait(false); }
finally { _concurrency.Release(); Interlocked.Decrement(ref _pending); }
}
public void Dispose()
{
Interlocked.Exchange(ref _disposed, 1);
_concurrency.Dispose();
}
}
internal sealed class DedicatedScheduler : IDotNetScheduler
{
private sealed class Work
{
public Func<ValueTask> Action = null!;
public TaskCompletionSource<bool>? Completion;
}
private readonly BlockingCollection<Work> _queue;
private readonly Thread _thread;
private int _threadId;
private int _disposed;
public DedicatedScheduler(string name, ShrinkBusOptions options)
{
_queue = new BlockingCollection<Work>(options.QueueCapacity);
_thread = new Thread(Run) { IsBackground = true, Name = $"ShrinkBus:{name}" };
_thread.Start();
}
public bool IsOnSchedulerThread => Thread.CurrentThread.ManagedThreadId == Volatile.Read(ref _threadId);
public bool TryPost(Action action)
{
if (IsOnSchedulerThread) { action(); return true; }
return Volatile.Read(ref _disposed) == 0 && _queue.TryAdd(new Work
{
Action = () => { action(); return default; }
});
}
public ValueTask PostAsync(Func<ValueTask> action, CancellationToken cancellationToken)
{
if (IsOnSchedulerThread)
return action();
var completion = new TaskCompletionSource<bool>(TaskCreationOptions.RunContinuationsAsynchronously);
if (Volatile.Read(ref _disposed) != 0 || !_queue.TryAdd(new Work
{ Action = action, Completion = completion }))
throw new QueueFullException();
return new ValueTask(completion.Task);
}
public void Dispose()
{
if (Interlocked.Exchange(ref _disposed, 1) != 0)
return;
_queue.CompleteAdding();
}
private void Run()
{
Volatile.Write(ref _threadId, Thread.CurrentThread.ManagedThreadId);
foreach (var work in _queue.GetConsumingEnumerable())
{
try { work.Action().AsTask().GetAwaiter().GetResult(); work.Completion?.SetResult(true); }
catch (OperationCanceledException) { work.Completion?.SetCanceled(); }
catch (Exception ex) { work.Completion?.SetException(ex); }
}
_queue.Dispose();
}
}
}