chore: initialize standalone UPM package
Publish UPM package / publish (push) Failing after 1s

This commit is contained in:
2026-08-26 02:50:18 +08:00
commit 1c921f5aec
86 changed files with 6934 additions and 0 deletions
+49
View File
@@ -0,0 +1,49 @@
name: Publish UPM package
on:
push:
tags:
- 'v*'
workflow_dispatch:
jobs:
publish:
runs-on: ubuntu-latest
permissions:
contents: read
packages: write
env:
NODE_AUTH_TOKEN: ${{ secrets.SHRINKSDK_PACKAGE_TOKEN }}
steps:
- name: Fetch tagged revision
shell: bash
run: |
set -eu
ref="${{ gitea.sha }}"
test -n "$ref"
git init .
git remote add origin "https://git.crash.work/ShrinkSDK/ShrinkEventBus.git"
git fetch --depth=1 origin "$ref"
git checkout --detach FETCH_HEAD
- name: Validate immutable release version
shell: bash
run: |
set -eu
tag="$(git describe --exact-match --tags HEAD)"
version="$(node -p "require('./package.json').version")"
test "$tag" = "v$version"
npm pack --dry-run
- name: Publish to ShrinkSDK registry
shell: bash
run: |
set -eu
: "${NODE_AUTH_TOKEN:?SHRINKSDK_PACKAGE_TOKEN is required}"
npmrc="$HOME/.npmrc"
cleanup() { rm -f "$npmrc"; }
trap cleanup EXIT
printf '%s\n' \
'registry=https://git.crash.work/api/packages/ShrinkSDK/npm/' \
'//git.crash.work/api/packages/ShrinkSDK/npm/:_authToken=${NODE_AUTH_TOKEN}' > "$npmrc"
npm publish --registry=https://git.crash.work/api/packages/ShrinkSDK/npm/
+39
View File
@@ -0,0 +1,39 @@
name: Verify standalone Unity package
on:
workflow_dispatch:
jobs:
editmode:
runs-on: unity-2022.3.62f3
container:
image: docker.1panel.live/unityci/editor:ubuntu-2022.3.62f3-windows-mono-3
volumes:
- /www/dk_project/bt-recovery/gitea/runner/seedskey-unity-license:/root/.local/share/unity3d/Unity
- /www/dk_project/bt-recovery/gitea/runner/seedskey-unity-entitlements:/root/.config/unity3d/Unity/licenses
steps:
- name: Fetch selected revision
shell: bash
run: |
set -eu
ref="${{ gitea.sha }}"
git init .
git remote add origin "https://git.crash.work/ShrinkSDK/ShrinkEventBus.git"
git fetch --depth=1 origin "$ref"
git checkout --detach FETCH_HEAD
- name: Run package EditMode tests
shell: bash
run: |
set -eu
unity_bin="$(command -v unity-editor || command -v unity || command -v Unity || true)"
test -n "$unity_bin"
"$unity_bin" \
-batchmode \
-nographics \
-quit \
-projectPath "$PWD/Development~/UnityProject" \
-runTests \
-testPlatform EditMode \
-testResults "$PWD/TestResults/editmode.xml" \
-logFile "$PWD/TestResults/unity.log"
+109
View File
@@ -0,0 +1,109 @@
# This .gitignore file should be placed at the root of your Unity project directory
#
# Get latest from https://github.com/github/gitignore/blob/main/Unity.gitignore
#
.utmp/
/[Ll]ibrary/
/[Tt]emp/
/[Oo]bj/
/[Bb]uild/
/[Bb]uilds/
/[Ll]ogs/
/[Uu]ser[Ss]ettings/
*.log
# By default unity supports Blender asset imports, *.blend1 blender files do not need to be commited to version control.
*.blend1
*.blend1.meta
# MemoryCaptures can get excessive in size.
# They also could contain extremely sensitive data
/[Mm]emoryCaptures/
# Recordings can get excessive in size
/[Rr]ecordings/
# Uncomment this line if you wish to ignore the asset store tools plugin
# /[Aa]ssets/AssetStoreTools*
# Autogenerated Jetbrains Rider plugin
/[Aa]ssets/Plugins/Editor/JetBrains*
# Jetbrains Rider personal-layer settings
*.DotSettings.user
# Visual Studio cache directory
.vs/
# Gradle cache directory
.gradle/
# Autogenerated VS/MD/Consulo solution and project files
ExportedObj/
.consulo/
*.csproj
*.unityproj
*.sln
*.suo
*.tmp
*.user
*.userprefs
*.pidb
*.booproj
*.svd
*.pdb
*.mdb
*.opendb
*.VC.db
# Unity3D generated meta files
*.pidb.meta
*.pdb.meta
*.mdb.meta
# Unity3D generated file on crash reports
sysinfo.txt
# Mono auto generated files
mono_crash.*
# Builds
*.apk
*.aab
*.unitypackage
*.unitypackage.meta
*.app
# Crashlytics generated file
crashlytics-build.properties
# TestRunner generated files
InitTestScene*.unity*
# Addressables default ignores, before user customizations
/ServerData
/[Aa]ssets/StreamingAssets/aa*
/[Aa]ssets/AddressableAssetsData/link.xml*
/[Aa]ssets/Addressables_Temp*
# By default, Addressables content builds will generate addressables_content_state.bin
# files in platform-specific subfolders, for example:
# /Assets/AddressableAssetsData/OSX/addressables_content_state.bin
/[Aa]ssets/AddressableAssetsData/*/*.bin*
# Visual Scripting auto-generated files
/[Aa]ssets/Unity.VisualScripting.Generated/VisualScripting.Flow/UnitOptions.db
/[Aa]ssets/Unity.VisualScripting.Generated/VisualScripting.Flow/UnitOptions.db.meta
/[Aa]ssets/Unity.VisualScripting.Generated/VisualScripting.Core/Property Providers
/[Aa]ssets/Unity.VisualScripting.Generated/VisualScripting.Core/Property Providers.meta
# Auto-generated scenes by play mode tests
/[Aa]ssets/[Ii]nit[Tt]est[Ss]cene*.unity*
/Development~/UnityProject/[Ll]ibrary/
/Development~/UnityProject/[Tt]emp/
/Development~/UnityProject/[Oo]bj/
/Development~/UnityProject/[Ll]ogs/
/Development~/UnityProject/[Uu]ser[Ss]ettings/
/Development~/UnityProject/TestResults/
/Tools~/**/[Bb]in/
/Tools~/**/[Oo]bj/
*.user
*.DotSettings.user
+8
View File
@@ -0,0 +1,8 @@
.git/
.gitea/
Development~/
Tools~/
*.csproj
*.sln
*.user
*.DotSettings.user
+72
View File
@@ -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.
+7
View File
@@ -0,0 +1,7 @@
fileFormatVersion: 2
guid: 6e379caaacb945699a297286798bc4e2
TextScriptImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:
+8
View File
@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: 2cc73e57d6ec4457a425843f734b6a70
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:
+83
View File
@@ -0,0 +1,83 @@
╔══════════════════════════════════════════════════╗
║ ShrinkEventBus Benchmark Report ║
║ iter= 1000000 mass= 10000 reg= 5000 scan=1000 ║
╚══════════════════════════════════════════════════╝
基准对象:独立实例 busAllowPerPhaseDispatch 已开启)
采集于 1.3.0EventBase 瘦身后;Unity 2022.3.62f3, Editor Play Mode
▶ TriggerEvent × 无订阅者(基线)
总耗时 : 111.466 ms
单次 : 0.1115 μs/op
吞吐量 : 8971378 ops/s
▶ TriggerEvent × 单订阅者同步
总耗时 : 163.073 ms
单次 : 0.1631 μs/op
吞吐量 : 6132242 ops/s
▶ TriggerEvent × 父事件监听子事件
总耗时 : 171.424 ms
单次 : 0.1714 μs/op
吞吐量 : 5833479 ops/s
▶ TriggerEvent × 30 订阅者同步
总耗时 : 38.639 ms
单次 : 3.8639 μs/op
吞吐量 : 258805 ops/s
▶ TriggerEvent(Phase=HIGH) × 3 phase 监听
总耗时 : 164.197 ms
单次 : 0.1642 μs/op
吞吐量 : 6090238 ops/s
▶ TriggerEventAsync × 单订阅者异步
总耗时 : 0.856 ms
单次 : 0.8557 μs/op
吞吐量 : 1168634 ops/s
▶ RegisterEvent(delegate)
总耗时 : 14.418 ms
单次 : 2.8836 μs/op
吞吐量 : 346791 ops/s
▶ UnregisterEvent(delegate)
总耗时 : 3.427 ms
单次 : 0.6853 μs/op
吞吐量 : 1459130 ops/s
▶ Register/Unregister(instance scan)
总耗时 : 13.097 ms
单次 : 13.0971 μs/op
吞吐量 : 76353 ops/s
▶ Register/Unregister(static type scan)
总耗时 : 9.337 ms
单次 : 9.3374 μs/op
吞吐量 : 107096 ops/s
▶ Register/Unregister(MethodInfo)
总耗时 : 6.513 ms
单次 : 6.5134 μs/op
吞吐量 : 153530 ops/s
▶ TriggerEvent × new() [GC Gen0= 1]
总耗时 : 266.536 ms
单次 : 0.2665 μs/op
吞吐量 : 3751843 ops/s
▶ TriggerEvent × Pool.Get() [GC Gen0= 0]
总耗时 : 242.015 ms
单次 : 0.2420 μs/op
吞吐量 : 4131982 ops/s
▶ TriggerEvent × 已取消事件跳过(10 订阅者)
总耗时 : 52.777 ms
单次 : 0.5278 μs/op
吞吐量 : 1894776 ops/s
══════════════════════════════════════════════════
全部测试完成
对比 1.1.x 旧样本:new() 路径 7684ms / GC Gen0=6457 → 266ms / GC Gen0=1
(EventBase 不再每实例分配内置 ListenerListnew() 路径 GC 基本归零,
对象池从“高频必需”降级为可选优化。)
+7
View File
@@ -0,0 +1,7 @@
fileFormatVersion: 2
guid: 87ccd659dba774447b9d6941761b8300
TextScriptImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:
+13
View File
@@ -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:
+282
View File
@@ -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);
}
}
}
+11
View File
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 76b9239259544977bd27221d3607aed1
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
+78
View File
@@ -0,0 +1,78 @@
# Changelog
本文件记录 `ShrinkEventBus` 在当前工作区中的包内变更。
## [2.0.0] - 2026-08-23
### Added
- 新增统一 `IShrinkEvent` 契约、`ShrinkEventSubscriber` / `ShrinkSubscribe` 特性与 `Post` / `PostAsync` 入口。
- 新增命名多 BusGame、Scene、Mod、World、Server 及自定义 `ShrinkBusKey`
- Bus 创建时可选择 Inline、Unity MainThread、DedicatedThread 或 TaskPool 调度器,并支持有界队列、溢出策略、最大并发与关闭排空。
- Unity 中异步 handler 与调度优先使用 UniTask;新增 `ShrinkMonoEventScope` 管理普通 MonoBehaviour 的绑定生命周期。
- ILPostProcessor 为特性标记的普通 C# / Mono 类型生成 `IShrinkGeneratedSubscriber` 强类型绑定,发布热路径不使用反射调用。
- 新增可选 `com.cneicy.shrink-eventbus-entities` 包,Burst Job 通过 NativeQueue writer/playback 进入同一 Bus。
- 新增 `Tools~/DotNet/ShrinkEventBus.Core` ValueTask 运行时与 Roslyn incremental generator,纯 .NET 宿主无需 Unity 或 UniTask。
- EventBus 可视化调试器使用 UI Toolkit 重写,新增 Bus 导航、实时指标、分发耗时、线程/执行模式/结果详情、问题筛选、跟随和 CSV 导出;详细采样只在窗口主动采集时启用。
- 新增独立 Play Mode benchmark,覆盖 0/1/8/30 handler、取消、异步和生成绑定开销。
- Inline Bus 使用专用运行时实现,非取消全同步 channel 在订阅变化时生成有序 multicast dispatcher`ShrinkPostResult` 压缩为 32-bit 状态,保持 API 不变。
### Breaking
- 删除 `EventBase``EventBusSubscriber` / `EventSubscribe``RegisterEvent` / `SubscribeEvent` / `TriggerEvent`、手工 Delegate 注册和旧反射通道。
- 所有事件改为实现 `IShrinkEvent`;取消和结果分别实现 `IShrinkCancelableEvent``IShrinkResultEvent<TResult>`
## [1.3.0] - 2026-06-12
### Breaking
- 移除 `EventBase.GetListenerList()`。事件对象不再内置 `ListenerList`,改为持有派发时的快照数组;调试请使用 `GetSubscribers()`(现在返回拷贝)。
- 数字优先级 `0` 现在映射到 `NORMAL`(原为 `LOW`),与枚举重载的默认值保持一致。
- 所有 int 数字优先级重载(`RegisterEvent` / `SubscribeEvent`)不再提供默认值 `0`,必须显式传入数字——消除与枚举重载之间的重载二义性。不带优先级的调用现在唯一解析到枚举重载(`NORMAL`)。
### Fixed
- 织入前会检查类型自身及基类链上是否存在实例 `[EventSubscribe]` 方法,没有的类型不再织入,避免在 `Awake` 中抛出注册异常。
- `AutoRegister`(含 Mod 框架反射路径)遇到没有任何匹配 `[EventSubscribe]` 方法的类型时,改为输出警告并跳过,不再抛异常;显式 `Register()` 仍保持严格抛错。
- `ListenerList` 父子链脏标记传播与快照重建之间的竞态:同一总线内所有监听列表(含父子链)现共用一把锁。
- `RegisterCore` 的 check-then-act 竞态(并发下同一实例可能被注册两次)。
- `EventPool.Release` 的双重归还竞态;对象池增加容量上限(128),超出直接丢弃。
- 父子事件同 phase 内的 handler 顺序:现在按数字优先级跨父子稳定归并(平局时子类型 handler 在前),数字优先级不再只在单层内生效。
### Changed
- 派发热路径重构:事件对象的监听者快照从“逐个带锁二分插入内置 ListenerList”改为一次数组引用赋值;`EventId` 改为懒生成;事件元数据缓存改用 `ConcurrentDictionary`。每个事件实例的构造分配显著减少。
- 删除死代码 `EventCache<T>`(README 此前描述的“泛型静态缓存热路径”自实例化总线改造后已不存在)。
- 恢复模块内 `CodeGen/` 作为可选本地织入管线:命中 `ShrinkShared.CodeGen` 覆盖范围的程序集继续由共享管线优先处理,其余只引用 `ShrinkEventBus.Runtime` 的程序集改由本地 `EventBusILPostProcessor` 兜底,避免 `[EventBusSubscriber]` 自动注册失效。
- `GetEventSubscribers` / `GetAllSubscribersSnapshot` / `EventBase.GetSubscribers` 返回内部快照的拷贝,外部修改不再影响派发顺序。
- `SetPhase` / `IsCanceled` / `Result` 的校验异常现在携带具体错误消息。
- ILPP 的诊断列表与程序集解析器改为调用内局部状态,消除共享实例并发隐患。
## [1.2.0] - 2026-05-18
### Changed
- `MonoBehaviour` 自动注册/反注册的 IL 织入已并入共享 `ShrinkShared.CodeGen` 管线;当前工作区同时保留 `ShrinkEventBus` 本地 `CodeGen/` 作为可选兜底,以兼容纯 `ShrinkEventBus.Runtime` 引用程序集。
- 保留现有 `[EventBusSubscriber]` 用法与自动生命周期行为,不要求业务层改写订阅代码。
## [1.1.6] - 2026-05-04
### Changed
- 运行时总线升级为“默认静态门面 + 可实例化 `IShrinkEventBus`/Builder”双层结构,支持异常策略、事件类型校验、按 phase 分发。
- 手动注册能力从纯 delegate 扩展为 `object / Type / MethodInfo` 三类入口,并在注册阶段严格校验签名、静态/实例模式和事件参数类型。
- `ListenerList` 改为按 phase 分桶 + 快照缓存模型,并支持父事件监听子事件。
- 事件查看器不再反射私有字典,改走公开快照接口。
- `EventBusBenchmark` 扩展为覆盖实例 bus、扫描注册、继承监听、phase 分发、对象池与取消事件跳过等关键场景。
## [1.1.5] - 2026-04-07
### Changed
- 修正 `PrepareForDispatch()`,现在只重置派发期状态,不再误清业务载荷。
- 修正同步 `TriggerEvent(...)` 遇到异步 handler 时的行为,改为向异步处理器分发脱离原对象的事件快照,避免直接复用原事件对象。
- `OnEventTriggered` 与编辑器追踪钩子改为在派发完成后触发,同时覆盖无监听者事件。
- `ShrinkNetworkEventBusBridge` 的异步转发改为先克隆事件,降低桥接层状态污染风险。
- 事件查看器的实时日志改为快照模型,支持关键词过滤、按“有监听者 / 无监听者”筛选、同类事件折叠聚合显示。
- 编辑器菜单迁移到 `ShrinkSDK/事件总线/事件查看器`
- README 同步更新新的调试入口与同步/异步派发语义说明。
+7
View File
@@ -0,0 +1,7 @@
fileFormatVersion: 2
guid: 9b2928da4a2426743bd762f5ebb9c232
TextScriptImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:
+8
View File
@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: c2bfff1573c10da47ae73a47b8841279
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:
+8
View File
@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: c297548ebc9165f448111a35488d368e
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:
+529
View File
@@ -0,0 +1,529 @@
#nullable enable
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using Mono.Cecil;
using Mono.Cecil.Cil;
using Mono.Cecil.Rocks;
using Mono.Cecil.Pdb;
using Unity.CompilationPipeline.Common.Diagnostics;
using Unity.CompilationPipeline.Common.ILPostProcessing;
namespace ShrinkEventBus.CodeGen
{
public sealed class EventBusILPostProcessor : ILPostProcessor
{
private const string RuntimeAssemblyName = "ShrinkEventBus.Runtime";
public override ILPostProcessor GetInstance() => this;
public override bool WillProcess(ICompiledAssembly compiledAssembly)
{
if (!ReferencesAssembly(compiledAssembly, RuntimeAssemblyName))
return false;
return true;
}
public override ILPostProcessResult Process(ICompiledAssembly compiledAssembly)
{
var diagnostics = new List<DiagnosticMessage>();
if (!WillProcess(compiledAssembly))
return new ILPostProcessResult(compiledAssembly.InMemoryAssembly, diagnostics);
var assemblyDefinition = AssemblyDefinitionFor(compiledAssembly);
var module = assemblyDefinition.MainModule;
try
{
var generatedSubscriberType = FindType(module, "ShrinkEventBus.ShrinkEventSubscriberAttribute", RuntimeAssemblyName);
var generatedSubscribeType = FindType(module, "ShrinkEventBus.ShrinkSubscribeAttribute", RuntimeAssemblyName);
if (generatedSubscriberType == null || generatedSubscribeType == null)
return GetResult(assemblyDefinition, diagnostics);
foreach (var type in GetAllTypes(module.Types)
.Where(type => !type.IsInterface && !type.IsAbstract)
.Where(type => HasAttribute(type, generatedSubscriberType))
.Where(type => type.Methods.Any(method =>
!method.IsStatic && HasAttribute(method, generatedSubscribeType))))
{
InjectGeneratedBinding(type, module, generatedSubscribeType);
}
var staticSubscriberTypes = GetAllTypes(module.Types)
.Where(type => HasAttribute(type, generatedSubscriberType))
.Where(type => type.Methods.Any(method =>
method.IsStatic && HasAttribute(method, generatedSubscribeType)))
.ToArray();
if (staticSubscriberTypes.Length > 0)
InjectStaticBootstrap(module, staticSubscriberTypes, generatedSubscribeType);
}
catch (Exception ex)
{
diagnostics.Add(new DiagnosticMessage
{
DiagnosticType = DiagnosticType.Error,
MessageData = $"[ShrinkEventBus.CodeGen] {ex.Message}"
});
}
return GetResult(assemblyDefinition, diagnostics);
}
private static bool ReferencesAssembly(ICompiledAssembly compiledAssembly, string assemblyName)
{
return compiledAssembly.References.Any(reference =>
string.Equals(Path.GetFileNameWithoutExtension(reference), assemblyName, StringComparison.Ordinal));
}
private static bool HasAttribute(ICustomAttributeProvider provider, TypeReference expectedAttributeType)
{
return provider.CustomAttributes.Any(attribute => attribute.AttributeType.FullName == expectedAttributeType.FullName);
}
private static IEnumerable<TypeDefinition> GetAllTypes(IEnumerable<TypeDefinition> roots)
{
foreach (var type in roots)
{
yield return type;
foreach (var nested in GetAllTypes(type.NestedTypes))
yield return nested;
}
}
private static void InjectGeneratedBinding(TypeDefinition type, ModuleDefinition module,
TypeReference subscribeAttributeType)
{
var generatedInterface = FindType(module,
"ShrinkEventBus.IShrinkGeneratedSubscriber", RuntimeAssemblyName)
?? throw new InvalidOperationException("IShrinkGeneratedSubscriber was not found.");
if (type.Interfaces.Any(item => item.InterfaceType.FullName == generatedInterface.FullName))
return;
var resolverType = FindType(module, "ShrinkEventBus.IShrinkBusResolver", RuntimeAssemblyName)
?? throw new InvalidOperationException("IShrinkBusResolver was not found.");
var busKeyType = FindType(module, "ShrinkEventBus.ShrinkBusKey", RuntimeAssemblyName)
?? throw new InvalidOperationException("ShrinkBusKey was not found.");
var bindingType = FindType(module, "ShrinkEventBus.ShrinkEventBinding", RuntimeAssemblyName)
?? throw new InvalidOperationException("ShrinkEventBinding was not found.");
var bindingHelperType = FindType(module, "ShrinkEventBus.ShrinkGeneratedBinding", RuntimeAssemblyName)
?? throw new InvalidOperationException("ShrinkGeneratedBinding was not found.");
var priorityType = FindType(module, "ShrinkEventBus.ShrinkEventPriority", RuntimeAssemblyName)
?? throw new InvalidOperationException("ShrinkEventPriority was not found.");
var asyncHandlerType = FindType(module, "ShrinkEventBus.ShrinkAsyncEventHandler`1", RuntimeAssemblyName)
?? throw new InvalidOperationException("ShrinkAsyncEventHandler was not found.");
var nullableBusKey = new GenericInstanceType(module.ImportReference(typeof(Nullable<>)));
nullableBusKey.GenericArguments.Add(busKeyType);
var disposableType = module.ImportReference(typeof(IDisposable));
var bindingDefinition = bindingType.Resolve()
?? throw new InvalidOperationException("ShrinkEventBinding could not be resolved.");
var bindingCtor = module.ImportReference(bindingDefinition.Methods.Single(method =>
method.IsConstructor && method.Parameters.Count == 0));
var bindingAdd = module.ImportReference(bindingDefinition.Methods.Single(method =>
method.Name == "Add" && method.Parameters.Count == 1));
var helperDefinition = bindingHelperType.Resolve()
?? throw new InvalidOperationException("ShrinkGeneratedBinding could not be resolved.");
var syncSubscribe = module.ImportReference(helperDefinition.Methods.Single(method =>
method.Name == "Subscribe" && method.HasGenericParameters));
var asyncSubscribe = module.ImportReference(helperDefinition.Methods.Single(method =>
method.Name == "SubscribeAsync" && method.HasGenericParameters));
var legacyAsyncSubscribe = module.ImportReference(helperDefinition.Methods.Single(method =>
method.Name == "SubscribeAsyncLegacy" && method.HasGenericParameters));
var interfaceDefinition = generatedInterface.Resolve()
?? throw new InvalidOperationException("IShrinkGeneratedSubscriber could not be resolved.");
var interfaceMethod = module.ImportReference(interfaceDefinition.Methods.Single(method =>
method.Name == "AttachGenerated"));
var generatedMethod = new MethodDefinition(
"ShrinkEventBus.IShrinkGeneratedSubscriber.AttachGenerated",
MethodAttributes.Private | MethodAttributes.Final | MethodAttributes.HideBySig |
MethodAttributes.NewSlot | MethodAttributes.Virtual,
disposableType);
generatedMethod.Parameters.Add(new ParameterDefinition("resolver", ParameterAttributes.None, resolverType));
generatedMethod.Parameters.Add(new ParameterDefinition("defaultBus", ParameterAttributes.Optional,
nullableBusKey));
generatedMethod.Overrides.Add(interfaceMethod);
generatedMethod.Body.InitLocals = true;
var bindingLocal = new VariableDefinition(bindingType);
generatedMethod.Body.Variables.Add(bindingLocal);
var il = generatedMethod.Body.GetILProcessor();
il.Emit(OpCodes.Newobj, bindingCtor);
il.Emit(OpCodes.Stloc, bindingLocal);
var classDefaultBus = ReadStringProperty(
type.CustomAttributes.First(attribute =>
attribute.AttributeType.FullName == "ShrinkEventBus.ShrinkEventSubscriberAttribute"),
"DefaultBus");
foreach (var handler in type.Methods.Where(method =>
!method.IsStatic && HasAttribute(method, subscribeAttributeType)).ToArray())
{
EmitGeneratedSubscription(il, module, type, handler,
handler.CustomAttributes.First(attribute =>
attribute.AttributeType.FullName == subscribeAttributeType.FullName),
classDefaultBus, bindingLocal, bindingAdd, syncSubscribe, asyncSubscribe,
legacyAsyncSubscribe, asyncHandlerType);
}
il.Emit(OpCodes.Ldloc, bindingLocal);
il.Emit(OpCodes.Ret);
type.Interfaces.Add(new InterfaceImplementation(generatedInterface));
type.Methods.Add(generatedMethod);
}
private static void EmitGeneratedSubscription(ILProcessor il, ModuleDefinition module,
TypeDefinition ownerType, MethodDefinition handler, CustomAttribute attribute,
string classDefaultBus, VariableDefinition bindingLocal, MethodReference bindingAdd,
MethodReference syncSubscribe, MethodReference asyncSubscribe,
MethodReference legacyAsyncSubscribe, TypeReference asyncHandlerType)
{
if (handler.Parameters.Count is < 1 or > 2)
throw new InvalidOperationException(
$"[ShrinkSubscribe] method {handler.FullName} must have one event parameter and an optional CancellationToken.");
var eventType = module.ImportReference(handler.Parameters[0].ParameterType);
var eventInterface = FindType(module, "ShrinkEventBus.IShrinkEvent", RuntimeAssemblyName)
?? throw new InvalidOperationException("IShrinkEvent was not found.");
if (!ImplementsInterface(eventType, eventInterface.FullName))
throw new InvalidOperationException(
$"[ShrinkSubscribe] method {handler.FullName} event parameter must implement IShrinkEvent.");
var bus = ReadStringProperty(attribute, "Bus");
if (string.IsNullOrWhiteSpace(bus))
bus = classDefaultBus;
var priority = ReadIntProperty(attribute, "Priority", 2);
var numericPriority = ReadIntProperty(attribute, "NumericPriority", 0);
var receiveCanceled = ReadBoolProperty(attribute, "ReceiveCanceled", false);
MethodReference openSubscribe;
TypeReference delegateType;
if (handler.ReturnType.FullName == module.TypeSystem.Void.FullName && handler.Parameters.Count == 1)
{
openSubscribe = syncSubscribe;
delegateType = MakeGenericType(module, typeof(Action<>), eventType);
}
else if (handler.ReturnType.FullName == "Cysharp.Threading.Tasks.UniTask" &&
handler.Parameters.Count == 1)
{
openSubscribe = legacyAsyncSubscribe;
var uniTaskType = FindType(module, "Cysharp.Threading.Tasks.UniTask", "UniTask")
?? module.ImportReference(handler.ReturnType);
delegateType = MakeGenericType(module, typeof(Func<,>), eventType, uniTaskType);
}
else if (handler.ReturnType.FullName == "Cysharp.Threading.Tasks.UniTask" &&
handler.Parameters.Count == 2 &&
handler.Parameters[1].ParameterType.FullName == typeof(System.Threading.CancellationToken).FullName)
{
openSubscribe = asyncSubscribe;
var closedAsyncHandler = new GenericInstanceType(asyncHandlerType);
closedAsyncHandler.GenericArguments.Add(eventType);
delegateType = closedAsyncHandler;
}
else
{
throw new InvalidOperationException(
$"Unsupported [ShrinkSubscribe] signature: {handler.FullName}. Use void(T), UniTask(T), or UniTask(T, CancellationToken).");
}
var closedSubscribe = new GenericInstanceMethod(openSubscribe);
closedSubscribe.GenericArguments.Add(eventType);
var delegateCtor = MakeDelegateConstructor(module, delegateType);
il.Emit(OpCodes.Ldloc, bindingLocal);
il.Emit(OpCodes.Ldarg_1);
il.Emit(OpCodes.Ldarg_2);
il.Emit(OpCodes.Ldstr, bus ?? string.Empty);
il.Emit(OpCodes.Ldarg_0);
il.Emit(OpCodes.Ldarg_0);
il.Emit(OpCodes.Ldftn, module.ImportReference(handler));
il.Emit(OpCodes.Newobj, delegateCtor);
il.Emit(OpCodes.Ldc_I4, priority);
il.Emit(OpCodes.Ldc_I4, numericPriority);
il.Emit(receiveCanceled ? OpCodes.Ldc_I4_1 : OpCodes.Ldc_I4_0);
il.Emit(OpCodes.Call, closedSubscribe);
il.Emit(OpCodes.Callvirt, bindingAdd);
}
private static void InjectStaticBootstrap(ModuleDefinition module,
IReadOnlyList<TypeDefinition> subscriberTypes, TypeReference subscribeAttributeType)
{
var registryType = FindType(module, "ShrinkEventBus.ShrinkStaticBindingRegistry", RuntimeAssemblyName)
?? throw new InvalidOperationException("ShrinkStaticBindingRegistry was not found.");
var asyncHandlerType = FindType(module, "ShrinkEventBus.ShrinkAsyncEventHandler`1", RuntimeAssemblyName)
?? throw new InvalidOperationException("ShrinkAsyncEventHandler was not found.");
var registryDefinition = registryType.Resolve()
?? throw new InvalidOperationException("ShrinkStaticBindingRegistry could not be resolved.");
var syncRegister = module.ImportReference(registryDefinition.Methods.Single(method =>
method.Name == "Register" && method.HasGenericParameters));
var asyncRegister = module.ImportReference(registryDefinition.Methods.Single(method =>
method.Name == "RegisterAsync" && method.HasGenericParameters));
var legacyAsyncRegister = module.ImportReference(registryDefinition.Methods.Single(method =>
method.Name == "RegisterAsyncLegacy" && method.HasGenericParameters));
var bootstrap = new TypeDefinition("ShrinkEventBus.Generated",
"ShrinkGeneratedStaticBindings",
TypeAttributes.Abstract | TypeAttributes.Sealed | TypeAttributes.NotPublic,
module.TypeSystem.Object);
module.Types.Add(bootstrap);
var register = new MethodDefinition("Register",
MethodAttributes.Assembly | MethodAttributes.Static | MethodAttributes.HideBySig,
module.TypeSystem.Void);
bootstrap.Methods.Add(register);
var il = register.Body.GetILProcessor();
foreach (var type in subscriberTypes)
{
var classDefaultBus = ReadStringProperty(
type.CustomAttributes.First(attribute =>
attribute.AttributeType.FullName == "ShrinkEventBus.ShrinkEventSubscriberAttribute"),
"DefaultBus");
foreach (var handler in type.Methods.Where(method =>
method.IsStatic && HasAttribute(method, subscribeAttributeType)).ToArray())
{
EmitStaticGeneratedSubscription(il, module, handler,
handler.CustomAttributes.First(attribute =>
attribute.AttributeType.FullName == subscribeAttributeType.FullName),
classDefaultBus, syncRegister, asyncRegister,
legacyAsyncRegister, asyncHandlerType);
}
}
il.Emit(OpCodes.Ret);
InjectModuleInitializer(module, register);
}
private static void EmitStaticGeneratedSubscription(ILProcessor il, ModuleDefinition module,
MethodDefinition handler, CustomAttribute attribute, string classDefaultBus,
MethodReference syncRegister, MethodReference asyncRegister,
MethodReference legacyAsyncRegister, TypeReference asyncHandlerType)
{
if (handler.Parameters.Count is < 1 or > 2)
throw new InvalidOperationException(
$"[ShrinkSubscribe] method {handler.FullName} must have one event parameter and an optional CancellationToken.");
var eventType = module.ImportReference(handler.Parameters[0].ParameterType);
var eventInterface = FindType(module, "ShrinkEventBus.IShrinkEvent", RuntimeAssemblyName)
?? throw new InvalidOperationException("IShrinkEvent was not found.");
if (!ImplementsInterface(eventType, eventInterface.FullName))
throw new InvalidOperationException(
$"[ShrinkSubscribe] method {handler.FullName} event parameter must implement IShrinkEvent.");
var bus = ReadStringProperty(attribute, "Bus");
if (string.IsNullOrWhiteSpace(bus))
bus = classDefaultBus;
var priority = ReadIntProperty(attribute, "Priority", 2);
var numericPriority = ReadIntProperty(attribute, "NumericPriority", 0);
var receiveCanceled = ReadBoolProperty(attribute, "ReceiveCanceled", false);
MethodReference openRegister;
TypeReference delegateType;
if (handler.ReturnType.FullName == module.TypeSystem.Void.FullName && handler.Parameters.Count == 1)
{
openRegister = syncRegister;
delegateType = MakeGenericType(module, typeof(Action<>), eventType);
}
else if (handler.ReturnType.FullName == "Cysharp.Threading.Tasks.UniTask" &&
handler.Parameters.Count == 1)
{
openRegister = legacyAsyncRegister;
var uniTaskType = FindType(module, "Cysharp.Threading.Tasks.UniTask", "UniTask")
?? module.ImportReference(handler.ReturnType);
delegateType = MakeGenericType(module, typeof(Func<,>), eventType, uniTaskType);
}
else if (handler.ReturnType.FullName == "Cysharp.Threading.Tasks.UniTask" &&
handler.Parameters.Count == 2 &&
handler.Parameters[1].ParameterType.FullName == typeof(System.Threading.CancellationToken).FullName)
{
openRegister = asyncRegister;
var closedAsyncHandler = new GenericInstanceType(asyncHandlerType);
closedAsyncHandler.GenericArguments.Add(eventType);
delegateType = closedAsyncHandler;
}
else
{
throw new InvalidOperationException(
$"Unsupported static [ShrinkSubscribe] signature: {handler.FullName}.");
}
var closedRegister = new GenericInstanceMethod(openRegister);
closedRegister.GenericArguments.Add(eventType);
var delegateCtor = MakeDelegateConstructor(module, delegateType);
var handlerBridge = CreateStaticHandlerBridge(module, handler);
il.Emit(OpCodes.Ldstr, bus ?? string.Empty);
il.Emit(OpCodes.Ldnull);
il.Emit(OpCodes.Ldftn, module.ImportReference(handlerBridge));
il.Emit(OpCodes.Newobj, delegateCtor);
il.Emit(OpCodes.Ldc_I4, priority);
il.Emit(OpCodes.Ldc_I4, numericPriority);
il.Emit(receiveCanceled ? OpCodes.Ldc_I4_1 : OpCodes.Ldc_I4_0);
il.Emit(OpCodes.Call, closedRegister);
}
private static MethodDefinition CreateStaticHandlerBridge(ModuleDefinition module,
MethodDefinition handler)
{
var bridge = new MethodDefinition(
$"ShrinkEventBus.GeneratedStaticHandler_{handler.MetadataToken.ToInt32():X8}",
MethodAttributes.Assembly | MethodAttributes.Static | MethodAttributes.HideBySig,
module.ImportReference(handler.ReturnType));
for (var i = 0; i < handler.Parameters.Count; i++)
{
var parameter = handler.Parameters[i];
bridge.Parameters.Add(new ParameterDefinition(parameter.Name, parameter.Attributes,
module.ImportReference(parameter.ParameterType)));
}
var il = bridge.Body.GetILProcessor();
for (var i = 0; i < bridge.Parameters.Count; i++)
il.Emit(OpCodes.Ldarg, bridge.Parameters[i]);
il.Emit(OpCodes.Call, module.ImportReference(handler));
il.Emit(OpCodes.Ret);
handler.DeclaringType.Methods.Add(bridge);
return bridge;
}
private static void InjectModuleInitializer(ModuleDefinition module, MethodReference register)
{
var moduleType = module.Types.First(type => type.Name == "<Module>");
var initializer = moduleType.Methods.FirstOrDefault(method => method.Name == ".cctor");
if (initializer == null)
{
initializer = new MethodDefinition(".cctor",
MethodAttributes.Private | MethodAttributes.Static |
MethodAttributes.HideBySig | MethodAttributes.SpecialName |
MethodAttributes.RTSpecialName,
module.TypeSystem.Void);
initializer.Body.GetILProcessor().Emit(OpCodes.Ret);
moduleType.Methods.Add(initializer);
}
var processor = initializer.Body.GetILProcessor();
processor.InsertBefore(initializer.Body.Instructions[0],
processor.Create(OpCodes.Call, register));
}
private static GenericInstanceType MakeGenericType(ModuleDefinition module, Type openType,
params TypeReference[] arguments)
{
var result = new GenericInstanceType(module.ImportReference(openType));
foreach (var argument in arguments)
result.GenericArguments.Add(argument);
return result;
}
private static MethodReference MakeDelegateConstructor(ModuleDefinition module, TypeReference delegateType)
{
var ctor = new MethodReference(".ctor", module.TypeSystem.Void, delegateType)
{
HasThis = true,
CallingConvention = MethodCallingConvention.Default
};
ctor.Parameters.Add(new ParameterDefinition(module.TypeSystem.Object));
ctor.Parameters.Add(new ParameterDefinition(module.TypeSystem.IntPtr));
return ctor;
}
private static bool ImplementsInterface(TypeReference type, string interfaceFullName)
{
TypeDefinition? current;
try
{
current = type.Resolve();
}
catch
{
return false;
}
while (current != null)
{
if (current.Interfaces.Any(item => item.InterfaceType.FullName == interfaceFullName))
return true;
try
{
current = current.BaseType?.Resolve();
}
catch
{
return false;
}
}
return false;
}
private static string ReadStringProperty(CustomAttribute attribute, string name)
{
foreach (var property in attribute.Properties)
{
if (property.Name == name)
return property.Argument.Value as string ?? string.Empty;
}
return string.Empty;
}
private static int ReadIntProperty(CustomAttribute attribute, string name, int defaultValue)
{
foreach (var property in attribute.Properties)
{
if (property.Name == name)
return Convert.ToInt32(property.Argument.Value);
}
return defaultValue;
}
private static bool ReadBoolProperty(CustomAttribute attribute, string name, bool defaultValue)
{
foreach (var property in attribute.Properties)
{
if (property.Name == name)
return Convert.ToBoolean(property.Argument.Value);
}
return defaultValue;
}
private static TypeReference? FindType(ModuleDefinition module, string fullName, string assemblyName)
{
var resolved = Type.GetType($"{fullName}, {assemblyName}", false);
return resolved == null ? null : module.ImportReference(resolved);
}
private static AssemblyDefinition AssemblyDefinitionFor(ICompiledAssembly compiledAssembly)
{
var assemblyResolver = new PostProcessorAssemblyResolver(compiledAssembly);
var readerParameters = new ReaderParameters
{
SymbolStream = new MemoryStream(compiledAssembly.InMemoryAssembly.PdbData.ToArray()),
SymbolReaderProvider = new PdbReaderProvider(),
AssemblyResolver = assemblyResolver,
ReflectionImporterProvider = new PostProcessorReflectionImporterProvider(),
ReadingMode = ReadingMode.Immediate
};
var assemblyDefinition = AssemblyDefinition.ReadAssembly(
new MemoryStream(compiledAssembly.InMemoryAssembly.PeData.ToArray()),
readerParameters);
assemblyResolver.AddAssemblyDefinitionBeingOperatedOn(assemblyDefinition);
return assemblyDefinition;
}
private static ILPostProcessResult GetResult(AssemblyDefinition assemblyDefinition, List<DiagnosticMessage> diagnostics)
{
var pe = new MemoryStream();
var pdb = new MemoryStream();
assemblyDefinition.Write(pe, new WriterParameters
{
SymbolWriterProvider = new PdbWriterProvider(),
SymbolStream = pdb,
WriteSymbols = true
});
return new ILPostProcessResult(new InMemoryAssembly(pe.ToArray(), pdb.ToArray()), diagnostics);
}
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 5f611b284dae01a46bc990b87b8bca90
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,107 @@
#nullable enable
using System.Collections.Generic;
using System.IO;
using System.Threading;
using Mono.Cecil;
using Unity.CompilationPipeline.Common.ILPostProcessing;
namespace ShrinkEventBus.CodeGen
{
public sealed class PostProcessorAssemblyResolver : IAssemblyResolver
{
private readonly string[] _references;
private readonly Dictionary<string, AssemblyDefinition> _cache = new();
private AssemblyDefinition? _self;
public PostProcessorAssemblyResolver(ICompiledAssembly compiledAssembly)
{
_references = compiledAssembly.References;
}
public void AddAssemblyDefinitionBeingOperatedOn(AssemblyDefinition assemblyDefinition)
{
_self = assemblyDefinition;
}
public AssemblyDefinition? Resolve(AssemblyNameReference name)
{
return Resolve(name, new ReaderParameters(ReadingMode.Deferred));
}
public AssemblyDefinition? Resolve(AssemblyNameReference name, ReaderParameters parameters)
{
lock (_cache)
{
if (name.Name == _self?.Name.Name)
return _self;
var path = FindPath(name);
if (path == null)
return null;
var key = $"{path}{File.GetLastWriteTime(path)}";
if (_cache.TryGetValue(key, out var cached))
return cached;
parameters.AssemblyResolver = this;
var ms = ReadFileWithRetry(path);
var pdbPath = Path.ChangeExtension(path, ".pdb");
if (File.Exists(pdbPath))
parameters.SymbolStream = ReadFileWithRetry(pdbPath);
var assembly = AssemblyDefinition.ReadAssembly(ms, parameters);
_cache[key] = assembly;
return assembly;
}
}
private string? FindPath(AssemblyNameReference name)
{
foreach (var reference in _references)
{
if (Path.GetFileNameWithoutExtension(reference) == name.Name)
return reference;
}
var dirs = new HashSet<string>();
foreach (var reference in _references)
{
var dir = Path.GetDirectoryName(reference);
if (dir != null)
dirs.Add(dir);
}
foreach (var dir in dirs)
{
var candidate = Path.Combine(dir, $"{name.Name}.dll");
if (File.Exists(candidate))
return candidate;
}
return null;
}
private static MemoryStream ReadFileWithRetry(string path, int retries = 5)
{
for (var i = 0; i < retries; i++)
{
try
{
return new MemoryStream(File.ReadAllBytes(path));
}
catch (IOException) when (i < retries - 1)
{
Thread.Sleep(100);
}
}
throw new IOException($"[ShrinkEventBus.CodeGen] 无法读取文件: {path}");
}
public void Dispose()
{
}
}
}
@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: aeea50f82bab4ee2833c671d801c95e3
timeCreated: 1772908894
@@ -0,0 +1,37 @@
#nullable enable
using System.Linq;
using System.Reflection;
using Mono.Cecil;
namespace ShrinkEventBus.CodeGen
{
public sealed class PostProcessorReflectionImporter : DefaultReflectionImporter
{
private const string CoreLibName = "System.Private.CoreLib";
private readonly AssemblyNameReference? _corlib;
public PostProcessorReflectionImporter(ModuleDefinition module)
: base(module)
{
_corlib = module.AssemblyReferences.FirstOrDefault(
assembly => assembly.Name is "mscorlib" or "netstandard");
}
public override AssemblyNameReference ImportReference(AssemblyName reference)
{
if (_corlib != null && reference.Name == CoreLibName)
return _corlib;
return base.ImportReference(reference);
}
}
public sealed class PostProcessorReflectionImporterProvider : IReflectionImporterProvider
{
public IReflectionImporter GetReflectionImporter(ModuleDefinition module)
{
return new PostProcessorReflectionImporter(module);
}
}
}
@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 0a6c2401798b42f0bbcde72796d2fa9c
timeCreated: 1772908906
@@ -0,0 +1,23 @@
{
"name": "Unity.ShrinkEventBus.CodeGen",
"rootNamespace": "ShrinkEventBus.CodeGen",
"references": [
"ShrinkEventBus.Runtime"
],
"includePlatforms": [
"Editor"
],
"excludePlatforms": [],
"allowUnsafeCode": true,
"overrideReferences": true,
"precompiledReferences": [
"Mono.Cecil.dll",
"Mono.Cecil.Mdb.dll",
"Mono.Cecil.Pdb.dll",
"Mono.Cecil.Rocks.dll"
],
"autoReferenced": false,
"defineConstraints": [],
"versionDefines": [],
"noEngineReferences": false
}
@@ -0,0 +1,7 @@
fileFormatVersion: 2
guid: 91161d4c2ae3ae74c9184ba33d9aa52e
AssemblyDefinitionImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:
+6
View File
@@ -0,0 +1,6 @@
[Ll]ibrary/
[Tt]emp/
[Oo]bj/
[Ll]ogs/
[Uu]ser[Ss]ettings/
TestResults/
@@ -0,0 +1,15 @@
{
"scopedRegistries": [
{
"name": "ShrinkSDK",
"url": "https://git.crash.work/api/packages/ShrinkSDK/npm/",
"scopes": [
"com.cneicy"
]
}
],
"dependencies": {
"com.unity.test-framework": "1.1.33",
"com.cneicy.shrink-eventbus": "file:../../.."
}
}
@@ -0,0 +1,2 @@
m_EditorVersion: 2022.3.62f3
m_EditorVersionWithRevision: 2022.3.62f3 (96770f904ca7)
+3
View File
@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 8bf98940dc144bd2bffd55ce21295bbd
timeCreated: 1772878399
+906
View File
@@ -0,0 +1,906 @@
#nullable enable
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Globalization;
using System.IO;
using System.Linq;
using System.Text;
using System.Threading;
using UnityEditor;
using UnityEditor.UIElements;
using UnityEngine;
using UnityEngine.UIElements;
namespace ShrinkEventBus.Editor
{
public sealed class EventBusViewerWindow : EditorWindow
{
private sealed class BusRow
{
public string Key = string.Empty;
public string FilterKey = string.Empty;
public string Scheduler = string.Empty;
public string Mode = string.Empty;
public string Queue = string.Empty;
public string Overflow = string.Empty;
public int Subscribers;
}
private sealed class EventRow
{
public long Sequence;
public DateTime TimestampUtc;
public string Bus = string.Empty;
public string EventType = string.Empty;
public string ShortEventType = string.Empty;
public string Scheduler = string.Empty;
public string Mode = string.Empty;
public int ThreadId;
public double DurationMicroseconds;
public bool IsAsync;
public bool Accepted;
public bool Handled;
public bool Canceled;
public ShrinkPostFailure Failure;
public string Status => Failure != ShrinkPostFailure.None
? FormatFailure(Failure)
: Canceled
? "已取消"
: Handled
? "已处理"
: "无处理器";
}
private const int MaxEvents = 5000;
private const int MaxPendingEvents = 20000;
private static readonly Color SuccessColor = new(0.32f, 0.72f, 0.48f);
private static readonly Color WarningColor = new(0.94f, 0.68f, 0.24f);
private static readonly Color ErrorColor = new(0.95f, 0.36f, 0.34f);
private static readonly Color MutedColor = new(0.58f, 0.61f, 0.66f);
private readonly object _eventGate = new();
private readonly Queue<EventRow> _pendingEvents = new();
private readonly List<EventRow> _events = new();
private readonly List<EventRow> _filteredEvents = new();
private readonly List<BusRow> _buses = new();
private ListView? _busList;
private ListView? _eventList;
private Label? _eventEmptyState;
private ToolbarSearchField? _search;
private ToolbarToggle? _captureToggle;
private ToolbarToggle? _problemOnlyToggle;
private ToolbarToggle? _autoScrollToggle;
private Label? _captureState;
private Label? _status;
private Label? _busDescription;
private Label? _busCountMetric;
private Label? _handlerCountMetric;
private Label? _capturedMetric;
private Label? _rateMetric;
private Label? _latencyMetric;
private Label? _problemMetric;
private Label? _detailTitle;
private Label? _detailTime;
private Label? _detailBus;
private Label? _detailExecution;
private Label? _detailThread;
private Label? _detailDuration;
private Label? _detailResult;
private string _selectedBus = string.Empty;
private long _selectedEventSequence;
private long _nextSequence;
private long _droppedCaptureCount;
private double _nextRefresh;
private bool _captureRequested = true;
private bool _diagnosticsSubscribed;
[MenuItem("ShrinkSDK/事件总线/事件查看器")]
private static void Open()
{
var window = GetWindow<EventBusViewerWindow>();
window.titleContent = new GUIContent("事件总线 2.0");
window.minSize = new Vector2(980f, 560f);
window.Show();
}
private void OnEnable()
{
SetDiagnosticsSubscription(_captureRequested);
EditorApplication.update += Tick;
}
private void OnDisable()
{
SetDiagnosticsSubscription(false);
EditorApplication.update -= Tick;
}
public void CreateGUI()
{
rootVisualElement.Clear();
rootVisualElement.style.flexDirection = FlexDirection.Column;
rootVisualElement.style.backgroundColor = EditorGUIUtility.isProSkin
? new Color(0.105f, 0.112f, 0.125f)
: new Color(0.88f, 0.89f, 0.91f);
rootVisualElement.Add(BuildToolbar());
rootVisualElement.Add(BuildMetricsBand());
var mainSplit = new TwoPaneSplitView(0, 330f, TwoPaneSplitViewOrientation.Horizontal);
mainSplit.style.flexGrow = 1f;
mainSplit.Add(BuildBusPane());
mainSplit.Add(BuildEventWorkspace());
rootVisualElement.Add(mainSplit);
_status = new Label();
_status.style.height = 24f;
_status.style.paddingLeft = 10f;
_status.style.paddingTop = 4f;
_status.style.borderTopWidth = 1f;
_status.style.borderTopColor = BorderColor();
_status.style.color = MutedColor;
rootVisualElement.Add(_status);
RefreshAll();
}
private VisualElement BuildToolbar()
{
var toolbar = new Toolbar();
toolbar.style.height = 34f;
var title = new Label("ShrinkEventBus");
title.style.unityFontStyleAndWeight = FontStyle.Bold;
title.style.fontSize = 13f;
title.style.marginLeft = 4f;
title.style.marginRight = 8f;
toolbar.Add(title);
_captureState = new Label();
_captureState.style.minWidth = 48f;
_captureState.style.height = 18f;
_captureState.style.marginRight = 10f;
_captureState.style.paddingLeft = 6f;
_captureState.style.paddingRight = 6f;
_captureState.style.unityTextAlign = TextAnchor.MiddleCenter;
_captureState.style.unityFontStyleAndWeight = FontStyle.Bold;
_captureState.style.fontSize = 9f;
toolbar.Add(_captureState);
_search = new ToolbarSearchField { tooltip = "按总线标识或事件类型筛选" };
_search.style.width = 260f;
_search.RegisterValueChangedCallback(_ => ApplyFilter());
toolbar.Add(_search);
_problemOnlyToggle = new ToolbarToggle { text = "仅问题" };
_problemOnlyToggle.tooltip = "只显示已取消、已拒绝或失败的发布";
_problemOnlyToggle.RegisterValueChangedCallback(_ => ApplyFilter());
toolbar.Add(_problemOnlyToggle);
_autoScrollToggle = new ToolbarToggle { text = "跟随", value = true };
_autoScrollToggle.tooltip = "自动保持最新事件可见";
toolbar.Add(_autoScrollToggle);
_captureToggle = new ToolbarToggle { text = "捕获", value = _captureRequested };
_captureToggle.tooltip = "启用详细分发采样";
_captureToggle.RegisterValueChangedCallback(evt =>
{
_captureRequested = evt.newValue;
SetDiagnosticsSubscription(evt.newValue);
UpdateCaptureState();
});
toolbar.Add(_captureToggle);
var spacer = new VisualElement();
spacer.style.flexGrow = 1f;
toolbar.Add(spacer);
toolbar.Add(new ToolbarButton(RefreshAll) { text = "刷新", tooltip = "刷新总线状态" });
toolbar.Add(new ToolbarButton(ExportCsv) { text = "导出", tooltip = "将当前可见事件导出为 CSV" });
toolbar.Add(new ToolbarButton(ClearEvents) { text = "清空", tooltip = "清空已捕获事件" });
UpdateCaptureState();
return toolbar;
}
private VisualElement BuildMetricsBand()
{
var band = new VisualElement();
band.style.height = 58f;
band.style.flexDirection = FlexDirection.Row;
band.style.borderBottomWidth = 1f;
band.style.borderBottomColor = BorderColor();
band.style.backgroundColor = PanelColor();
_busCountMetric = AddMetric(band, "总线");
_handlerCountMetric = AddMetric(band, "处理器");
_capturedMetric = AddMetric(band, "已捕获");
_rateMetric = AddMetric(band, "事件 / 秒");
_latencyMetric = AddMetric(band, "平均分发耗时");
_problemMetric = AddMetric(band, "问题");
return band;
}
private static Label AddMetric(VisualElement parent, string caption)
{
var block = new VisualElement();
block.style.flexGrow = 1f;
block.style.flexBasis = 0f;
block.style.paddingLeft = 12f;
block.style.paddingTop = 6f;
block.style.borderRightWidth = 1f;
block.style.borderRightColor = BorderColor();
var captionLabel = new Label(caption);
captionLabel.style.fontSize = 9f;
captionLabel.style.color = MutedColor;
block.Add(captionLabel);
var value = new Label("0");
value.style.fontSize = 17f;
value.style.unityFontStyleAndWeight = FontStyle.Bold;
value.style.marginTop = 1f;
block.Add(value);
parent.Add(block);
return value;
}
private VisualElement BuildBusPane()
{
var pane = new VisualElement();
pane.style.flexGrow = 1f;
pane.style.backgroundColor = PanelColor();
pane.Add(BuildBusHeader());
_busList = new ListView(_buses, 30f, MakeBusRow, BindBusRow)
{
selectionType = SelectionType.Single,
showAlternatingRowBackgrounds = AlternatingRowBackground.ContentOnly,
virtualizationMethod = CollectionVirtualizationMethod.FixedHeight
};
_busList.style.flexGrow = 1f;
_busList.selectionChanged += selection =>
{
var selected = selection.OfType<BusRow>().FirstOrDefault();
if (selected == null)
return;
_selectedBus = selected.FilterKey;
UpdateBusDescription(selected);
ApplyFilter();
};
pane.Add(_busList);
_busDescription = new Label();
_busDescription.style.minHeight = 48f;
_busDescription.style.paddingLeft = 10f;
_busDescription.style.paddingRight = 8f;
_busDescription.style.paddingTop = 7f;
_busDescription.style.paddingBottom = 6f;
_busDescription.style.borderTopWidth = 1f;
_busDescription.style.borderTopColor = BorderColor();
_busDescription.style.color = MutedColor;
_busDescription.style.whiteSpace = WhiteSpace.Normal;
pane.Add(_busDescription);
return pane;
}
private VisualElement BuildEventWorkspace()
{
var split = new TwoPaneSplitView(1, 185f, TwoPaneSplitViewOrientation.Vertical);
split.style.flexGrow = 1f;
split.Add(BuildEventPane());
split.Add(BuildDetailPane());
return split;
}
private VisualElement BuildEventPane()
{
var pane = new VisualElement();
pane.style.flexGrow = 1f;
pane.Add(BuildEventHeader());
_eventList = new ListView(_filteredEvents, 28f, MakeEventRow, BindEventRow)
{
selectionType = SelectionType.Single,
showAlternatingRowBackgrounds = AlternatingRowBackground.ContentOnly,
virtualizationMethod = CollectionVirtualizationMethod.FixedHeight
};
_eventList.style.flexGrow = 1f;
_eventList.selectionChanged += selection =>
{
var selected = selection.OfType<EventRow>().FirstOrDefault();
if (selected == null)
return;
_selectedEventSequence = selected.Sequence;
ShowEventDetails(selected);
};
pane.Add(_eventList);
_eventEmptyState = new Label("暂无事件");
_eventEmptyState.style.flexGrow = 1f;
_eventEmptyState.style.unityTextAlign = TextAnchor.MiddleCenter;
_eventEmptyState.style.color = MutedColor;
pane.Add(_eventEmptyState);
return pane;
}
private VisualElement BuildDetailPane()
{
var pane = new VisualElement();
pane.style.flexGrow = 1f;
pane.style.backgroundColor = PanelColor();
pane.style.borderTopWidth = 1f;
pane.style.borderTopColor = BorderColor();
_detailTitle = new Label("未选择事件");
_detailTitle.style.fontSize = 13f;
_detailTitle.style.unityFontStyleAndWeight = FontStyle.Bold;
_detailTitle.style.marginLeft = 12f;
_detailTitle.style.marginTop = 9f;
_detailTitle.style.marginBottom = 7f;
pane.Add(_detailTitle);
var grid = new VisualElement();
grid.style.flexDirection = FlexDirection.Row;
grid.style.flexWrap = Wrap.Wrap;
grid.style.paddingLeft = 8f;
grid.style.paddingRight = 8f;
_detailTime = AddDetailField(grid, "时间");
_detailBus = AddDetailField(grid, "总线");
_detailExecution = AddDetailField(grid, "执行方式");
_detailThread = AddDetailField(grid, "线程");
_detailDuration = AddDetailField(grid, "分发耗时");
_detailResult = AddDetailField(grid, "结果");
pane.Add(grid);
return pane;
}
private static Label AddDetailField(VisualElement grid, string caption)
{
var field = new VisualElement();
field.style.width = Length.Percent(33.333f);
field.style.minWidth = 190f;
field.style.paddingLeft = 5f;
field.style.paddingRight = 5f;
field.style.paddingBottom = 8f;
var captionLabel = new Label(caption);
captionLabel.style.fontSize = 9f;
captionLabel.style.color = MutedColor;
field.Add(captionLabel);
var value = new Label("-");
value.style.marginTop = 2f;
value.style.overflow = Overflow.Hidden;
value.style.whiteSpace = WhiteSpace.Normal;
field.Add(value);
grid.Add(field);
return value;
}
private static VisualElement BuildBusHeader()
{
var header = BuildHeaderContainer("总线");
var columns = BuildColumnRow();
columns.Add(MakeHeaderCell("标识", 1f));
columns.Add(MakeHeaderCell("调度器", 0f, 92f));
columns.Add(MakeHeaderCell("处理器", 0f, 58f, TextAnchor.MiddleRight));
header.Add(columns);
return header;
}
private static VisualElement BuildEventHeader()
{
var header = BuildHeaderContainer("事件流");
var columns = BuildColumnRow();
columns.Add(MakeHeaderCell("时间", 0f, 92f));
columns.Add(MakeHeaderCell("总线", 0f, 132f));
columns.Add(MakeHeaderCell("事件", 1f));
columns.Add(MakeHeaderCell("方式", 0f, 54f));
columns.Add(MakeHeaderCell("线程", 0f, 56f, TextAnchor.MiddleRight));
columns.Add(MakeHeaderCell("耗时", 0f, 82f, TextAnchor.MiddleRight));
columns.Add(MakeHeaderCell("结果", 0f, 90f));
header.Add(columns);
return header;
}
private static VisualElement BuildHeaderContainer(string title)
{
var header = new VisualElement();
header.style.height = 52f;
header.style.borderBottomWidth = 1f;
header.style.borderBottomColor = BorderColor();
header.style.backgroundColor = HeaderColor();
var titleLabel = new Label(title);
titleLabel.style.height = 27f;
titleLabel.style.paddingLeft = 10f;
titleLabel.style.paddingTop = 7f;
titleLabel.style.unityFontStyleAndWeight = FontStyle.Bold;
titleLabel.style.fontSize = 10f;
header.Add(titleLabel);
return header;
}
private static VisualElement BuildColumnRow()
{
var row = new VisualElement();
row.style.height = 24f;
row.style.flexDirection = FlexDirection.Row;
row.style.alignItems = Align.Center;
return row;
}
private static Label MakeHeaderCell(string text, float grow, float width = 0f,
TextAnchor alignment = TextAnchor.MiddleLeft)
{
var label = MakeCell(string.Empty, grow, width, alignment);
label.text = text;
label.style.fontSize = 9f;
label.style.color = MutedColor;
return label;
}
private static VisualElement MakeBusRow()
{
var row = new VisualElement { name = "bus-row" };
row.style.flexDirection = FlexDirection.Row;
row.style.alignItems = Align.Center;
row.Add(MakeCell("key", 1f));
row.Add(MakeCell("scheduler", 0f, 92f));
row.Add(MakeCell("handlers", 0f, 58f, TextAnchor.MiddleRight));
return row;
}
private void BindBusRow(VisualElement element, int index)
{
if (index < 0 || index >= _buses.Count)
return;
var item = _buses[index];
element.Q<Label>("key").text = item.Key;
element.Q<Label>("scheduler").text = item.Scheduler;
element.Q<Label>("handlers").text = item.Subscribers.ToString(CultureInfo.InvariantCulture);
element.tooltip = item.FilterKey.Length == 0
? "汇总视图"
: $"{item.Mode} | 队列 {item.Queue} | 溢出策略 {item.Overflow}";
}
private static VisualElement MakeEventRow()
{
var row = new VisualElement { name = "event-row" };
row.style.flexDirection = FlexDirection.Row;
row.style.alignItems = Align.Center;
row.Add(MakeCell("time", 0f, 92f));
row.Add(MakeCell("bus", 0f, 132f));
row.Add(MakeCell("type", 1f));
row.Add(MakeCell("kind", 0f, 54f));
row.Add(MakeCell("thread", 0f, 56f, TextAnchor.MiddleRight));
row.Add(MakeCell("latency", 0f, 82f, TextAnchor.MiddleRight));
row.Add(MakeCell("status", 0f, 90f));
return row;
}
private void BindEventRow(VisualElement element, int index)
{
if (index < 0 || index >= _filteredEvents.Count)
return;
var item = _filteredEvents[index];
element.Q<Label>("time").text = item.TimestampUtc.ToLocalTime().ToString("HH:mm:ss.fff");
element.Q<Label>("bus").text = item.Bus;
element.Q<Label>("type").text = item.ShortEventType;
element.Q<Label>("kind").text = item.IsAsync ? "异步" : "同步";
element.Q<Label>("thread").text = "线程 " + item.ThreadId;
element.Q<Label>("latency").text = FormatDuration(item.DurationMicroseconds);
var status = element.Q<Label>("status");
status.text = item.Status;
status.style.color = item.Failure != ShrinkPostFailure.None
? ErrorColor
: item.Canceled
? WarningColor
: item.Handled
? SuccessColor
: MutedColor;
element.tooltip = $"{item.EventType}\n{item.Scheduler} / {item.Mode} / 线程 {item.ThreadId}";
}
private static Label MakeCell(string name, float grow, float width = 0f,
TextAnchor alignment = TextAnchor.MiddleLeft)
{
var label = new Label { name = name };
label.style.flexGrow = grow;
label.style.minWidth = 0f;
label.style.paddingLeft = 6f;
label.style.paddingRight = 4f;
label.style.unityTextAlign = alignment;
label.style.overflow = Overflow.Hidden;
label.style.whiteSpace = WhiteSpace.NoWrap;
if (width > 0f)
{
label.style.width = width;
label.style.flexShrink = 0f;
}
else
{
label.style.flexShrink = 1f;
}
return label;
}
private void RecordEvent(ShrinkEventTrace trace)
{
var row = new EventRow
{
Sequence = Interlocked.Increment(ref _nextSequence),
TimestampUtc = trace.TimestampUtc,
Bus = trace.BusKey.Id,
EventType = trace.EventType.FullName ?? trace.EventType.Name,
ShortEventType = trace.EventType.Name,
Scheduler = FormatScheduler(trace.Scheduler),
Mode = FormatDispatchMode(trace.DispatchMode),
ThreadId = trace.ThreadId,
DurationMicroseconds = trace.ElapsedTimestampTicks * (1_000_000d / Stopwatch.Frequency),
IsAsync = trace.IsAsync,
Accepted = trace.Result.Accepted,
Handled = trace.Result.Handled,
Canceled = trace.Result.Canceled,
Failure = trace.Result.Failure
};
lock (_eventGate)
{
if (_pendingEvents.Count >= MaxPendingEvents)
{
Interlocked.Increment(ref _droppedCaptureCount);
return;
}
_pendingEvents.Enqueue(row);
}
}
private void Tick()
{
if (EditorApplication.timeSinceStartup < _nextRefresh)
return;
_nextRefresh = EditorApplication.timeSinceStartup + 0.15;
DrainEvents();
RefreshBuses();
ApplyFilter();
}
private void RefreshAll()
{
DrainEvents();
RefreshBuses();
ApplyFilter();
}
private void RefreshBuses()
{
var snapshot = EventBus.Snapshot()
.OrderBy(item => item.Key.Id, StringComparer.Ordinal)
.ToArray();
var totalSubscribers = snapshot.Sum(item => item.Subscribers);
_buses.Clear();
_buses.Add(new BusRow
{
Key = "全部总线",
FilterKey = string.Empty,
Scheduler = "混合",
Mode = "混合",
Queue = "-",
Overflow = "-",
Subscribers = totalSubscribers
});
foreach (var bus in snapshot)
{
_buses.Add(new BusRow
{
Key = bus.Key.Id,
FilterKey = bus.Key.Id,
Scheduler = FormatScheduler(bus.Options.Scheduler),
Mode = FormatDispatchMode(bus.Options.DispatchMode),
Queue = bus.Options.QueueCapacity.ToString(CultureInfo.InvariantCulture),
Overflow = FormatOverflowPolicy(bus.Options.OverflowPolicy),
Subscribers = bus.Subscribers
});
}
_busList?.RefreshItems();
var selectedIndex = Math.Max(0, _buses.FindIndex(item => item.FilterKey == _selectedBus));
if (_busList != null && _busList.selectedIndex != selectedIndex)
_busList.selectedIndex = selectedIndex;
if (selectedIndex < _buses.Count)
UpdateBusDescription(_buses[selectedIndex]);
if (_busCountMetric != null)
_busCountMetric.text = snapshot.Length.ToString(CultureInfo.InvariantCulture);
if (_handlerCountMetric != null)
_handlerCountMetric.text = totalSubscribers.ToString("N0", CultureInfo.InvariantCulture);
}
private void DrainEvents()
{
lock (_eventGate)
{
while (_pendingEvents.Count > 0)
_events.Add(_pendingEvents.Dequeue());
}
if (_events.Count > MaxEvents)
_events.RemoveRange(0, _events.Count - MaxEvents);
}
private void ApplyFilter()
{
var query = _search?.value?.Trim() ?? string.Empty;
var problemOnly = _problemOnlyToggle?.value == true;
_filteredEvents.Clear();
for (var i = _events.Count - 1; i >= 0; i--)
{
var item = _events[i];
if (_selectedBus.Length > 0 && !string.Equals(item.Bus, _selectedBus, StringComparison.Ordinal))
continue;
if (problemOnly && item.Failure == ShrinkPostFailure.None && !item.Canceled && item.Accepted)
continue;
if (query.Length > 0 &&
item.Bus.IndexOf(query, StringComparison.OrdinalIgnoreCase) < 0 &&
item.EventType.IndexOf(query, StringComparison.OrdinalIgnoreCase) < 0)
continue;
_filteredEvents.Add(item);
}
if (_eventList != null)
{
var hasVisibleEvents = _filteredEvents.Count > 0;
_eventList.style.display = hasVisibleEvents ? DisplayStyle.Flex : DisplayStyle.None;
if (hasVisibleEvents)
_eventList.RefreshItems();
}
if (_eventEmptyState != null)
_eventEmptyState.style.display = _filteredEvents.Count == 0
? DisplayStyle.Flex
: DisplayStyle.None;
RestoreEventSelection();
UpdateMetrics();
UpdateStatus();
if (_autoScrollToggle?.value == true && _filteredEvents.Count > 0 && _selectedEventSequence == 0)
_eventList?.ScrollToItem(0);
}
private void RestoreEventSelection()
{
if (_eventList == null || _selectedEventSequence == 0)
return;
var index = _filteredEvents.FindIndex(item => item.Sequence == _selectedEventSequence);
if (index < 0)
{
_selectedEventSequence = 0;
_eventList.ClearSelection();
ClearEventDetails();
return;
}
if (_eventList.selectedIndex != index)
_eventList.selectedIndex = index;
}
private void UpdateMetrics()
{
var now = DateTime.UtcNow;
var recent = 0;
var problems = 0;
var duration = 0d;
for (var i = 0; i < _filteredEvents.Count; i++)
{
var item = _filteredEvents[i];
if ((now - item.TimestampUtc).TotalSeconds <= 1d)
recent++;
if (item.Failure != ShrinkPostFailure.None || item.Canceled || !item.Accepted)
problems++;
duration += item.DurationMicroseconds;
}
if (_capturedMetric != null)
_capturedMetric.text = _events.Count.ToString("N0", CultureInfo.InvariantCulture);
if (_rateMetric != null)
_rateMetric.text = recent.ToString("N0", CultureInfo.InvariantCulture);
if (_latencyMetric != null)
_latencyMetric.text = _filteredEvents.Count == 0
? "0 微秒"
: FormatDuration(duration / _filteredEvents.Count);
if (_problemMetric != null)
{
_problemMetric.text = problems.ToString("N0", CultureInfo.InvariantCulture);
_problemMetric.style.color = problems > 0 ? ErrorColor : SuccessColor;
}
}
private void UpdateStatus()
{
if (_status == null)
return;
var dropped = Interlocked.Read(ref _droppedCaptureCount);
var scope = _selectedBus.Length == 0 ? "全部总线" : _selectedBus;
_status.text = $"可见 {_filteredEvents.Count:N0} 条 / 保留 {_events.Count:N0} 条 | {scope} | 上限 {MaxEvents:N0} 条" +
(dropped > 0 ? $" | 捕获溢出 {dropped:N0} 条" : string.Empty);
}
private void UpdateBusDescription(BusRow row)
{
if (_busDescription == null)
return;
_busDescription.text = row.FilterKey.Length == 0
? $"汇总 | {row.Subscribers:N0} 个处理器"
: $"{row.Scheduler} / {row.Mode}\n队列 {row.Queue} | 溢出策略 {row.Overflow} | {row.Subscribers:N0} 个处理器";
}
private void ShowEventDetails(EventRow item)
{
if (_detailTitle == null)
return;
_detailTitle.text = item.EventType;
_detailTitle.tooltip = item.EventType;
if (_detailTime != null)
_detailTime.text = item.TimestampUtc.ToLocalTime().ToString("yyyy-MM-dd HH:mm:ss.fff");
if (_detailBus != null)
_detailBus.text = item.Bus;
if (_detailExecution != null)
_detailExecution.text = $"{(item.IsAsync ? "" : "")} | {item.Scheduler} / {item.Mode}";
if (_detailThread != null)
_detailThread.text = "托管线程 " + item.ThreadId;
if (_detailDuration != null)
_detailDuration.text = item.DurationMicroseconds.ToString("N3", CultureInfo.InvariantCulture) + " 微秒";
if (_detailResult != null)
{
_detailResult.text = $"{item.Status} | 已接收={FormatBoolean(item.Accepted)} 已处理={FormatBoolean(item.Handled)} 已取消={FormatBoolean(item.Canceled)}";
_detailResult.style.color = item.Failure != ShrinkPostFailure.None
? ErrorColor
: item.Canceled
? WarningColor
: SuccessColor;
}
}
private void ClearEventDetails()
{
if (_detailTitle != null)
_detailTitle.text = "未选择事件";
foreach (var label in new[]
{
_detailTime, _detailBus, _detailExecution, _detailThread, _detailDuration, _detailResult
})
{
if (label != null)
{
label.text = "-";
label.style.color = StyleKeyword.Null;
}
}
}
private void SetDiagnosticsSubscription(bool enabled)
{
if (enabled == _diagnosticsSubscribed)
return;
if (enabled)
EventBus.DetailedPosted += RecordEvent;
else
EventBus.DetailedPosted -= RecordEvent;
_diagnosticsSubscribed = enabled;
}
private void UpdateCaptureState()
{
if (_captureState == null)
return;
_captureState.text = _diagnosticsSubscribed ? "实时" : "已暂停";
_captureState.style.color = _diagnosticsSubscribed ? SuccessColor : WarningColor;
_captureState.style.backgroundColor = _diagnosticsSubscribed
? new Color(0.12f, 0.28f, 0.19f)
: new Color(0.32f, 0.25f, 0.11f);
}
private void ClearEvents()
{
lock (_eventGate)
_pendingEvents.Clear();
_events.Clear();
_filteredEvents.Clear();
_selectedEventSequence = 0;
Interlocked.Exchange(ref _droppedCaptureCount, 0);
_eventList?.ClearSelection();
ClearEventDetails();
ApplyFilter();
}
private void ExportCsv()
{
var path = EditorUtility.SaveFilePanel("导出 EventBus 跟踪记录", string.Empty,
$"ShrinkEventBus-{DateTime.Now:yyyyMMdd-HHmmss}.csv", "csv");
if (string.IsNullOrWhiteSpace(path))
return;
var builder = new StringBuilder(Math.Max(256, _filteredEvents.Count * 128));
builder.AppendLine("序号,UTC时间,总线,事件类型,调度器,分发模式,执行方式,线程ID,耗时微秒,已接收,已处理,已取消,失败原因");
for (var i = _filteredEvents.Count - 1; i >= 0; i--)
{
var item = _filteredEvents[i];
builder.Append(item.Sequence).Append(',')
.Append(item.TimestampUtc.ToString("O")).Append(',')
.Append(Csv(item.Bus)).Append(',')
.Append(Csv(item.EventType)).Append(',')
.Append(item.Scheduler).Append(',')
.Append(item.Mode).Append(',')
.Append(item.IsAsync ? "异步" : "同步").Append(',')
.Append(item.ThreadId).Append(',')
.Append(item.DurationMicroseconds.ToString("F3", CultureInfo.InvariantCulture)).Append(',')
.Append(FormatBoolean(item.Accepted)).Append(',')
.Append(FormatBoolean(item.Handled)).Append(',')
.Append(FormatBoolean(item.Canceled)).Append(',')
.Append(FormatFailure(item.Failure)).AppendLine();
}
File.WriteAllText(path, builder.ToString(), new UTF8Encoding(false));
EditorUtility.RevealInFinder(path);
}
private static string Csv(string value) =>
'"' + value.Replace("\"", "\"\"") + '"';
private static string FormatDuration(double microseconds) => microseconds < 1000d
? microseconds.ToString("N2", CultureInfo.InvariantCulture) + " 微秒"
: (microseconds / 1000d).ToString("N2", CultureInfo.InvariantCulture) + " 毫秒";
private static string FormatBoolean(bool value) => value ? "是" : "否";
private static string FormatScheduler(ShrinkBusSchedulerKind scheduler) => scheduler switch
{
ShrinkBusSchedulerKind.Inline => "当前线程",
ShrinkBusSchedulerKind.MainThread => "主线程",
ShrinkBusSchedulerKind.DedicatedThread => "专属线程",
ShrinkBusSchedulerKind.TaskPool => "任务池",
_ => scheduler.ToString()
};
private static string FormatDispatchMode(ShrinkDispatchMode mode) => mode switch
{
ShrinkDispatchMode.Ordered => "有序",
ShrinkDispatchMode.Parallel => "并行",
_ => mode.ToString()
};
private static string FormatOverflowPolicy(ShrinkQueueOverflowPolicy policy) => policy switch
{
ShrinkQueueOverflowPolicy.Reject => "拒绝",
ShrinkQueueOverflowPolicy.DropNewest => "丢弃最新",
ShrinkQueueOverflowPolicy.DropOldest => "丢弃最旧",
ShrinkQueueOverflowPolicy.Wait => "等待",
_ => policy.ToString()
};
private static string FormatFailure(ShrinkPostFailure failure) => failure switch
{
ShrinkPostFailure.None => "无",
ShrinkPostFailure.BusStopped => "总线已停止",
ShrinkPostFailure.QueueFull => "队列已满",
ShrinkPostFailure.Canceled => "已取消",
ShrinkPostFailure.HandlerException => "处理器异常",
ShrinkPostFailure.InvalidEvent => "无效事件",
_ => failure.ToString()
};
private static Color PanelColor() => EditorGUIUtility.isProSkin
? new Color(0.13f, 0.137f, 0.15f)
: new Color(0.94f, 0.95f, 0.96f);
private static Color HeaderColor() => EditorGUIUtility.isProSkin
? new Color(0.16f, 0.168f, 0.183f)
: new Color(0.82f, 0.84f, 0.87f);
private static Color BorderColor() => EditorGUIUtility.isProSkin
? new Color(0.24f, 0.25f, 0.28f)
: new Color(0.66f, 0.68f, 0.72f);
}
}
+3
View File
@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 16e1b18e2a2748e494f2ba648b4697a8
timeCreated: 1772878417
+13
View File
@@ -0,0 +1,13 @@
{
"name": "ShrinkEventBus.Editor",
"rootNamespace": "ShrinkEventBus.Editor",
"references": [
"ShrinkEventBus.Runtime",
"UniTask"
],
"includePlatforms": ["Editor"],
"excludePlatforms": [],
"allowUnsafeCode": false,
"overrideReferences": false,
"autoReferenced": false
}
+3
View File
@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: f0a89bdb25764f1f8a8e4dcc915abe39
timeCreated: 1772900825
+21
View File
@@ -0,0 +1,21 @@
MIT License
Copyright (c) 2026 Eicy
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
+7
View File
@@ -0,0 +1,7 @@
fileFormatVersion: 2
guid: a726eae075081a24785e6f3f4a4efe6d
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:
+171
View File
@@ -0,0 +1,171 @@
# ShrinkEventBus 2.0
ShrinkEventBus 2.0 是面向 Unity、普通 C# 宿主和 Entities/Burst 生产端的统一事件总线。运行时只有一种事件模型、一个订阅入口和一组发布 API;不同 Bus 只表达生命周期、所有权与调度策略。
## 核心契约
```csharp
public interface IShrinkEvent { }
public interface IShrinkCancelableEvent : IShrinkEvent
{
bool IsCanceled { get; }
void SetCanceled(bool value);
}
public interface IShrinkResultEvent<TResult> : IShrinkEvent
{
TResult Result { get; }
void SetResult(TResult result);
}
```
事件可以是 class、struct 或 unmanaged struct,不需要继承 SDK 基类。
## 多 Bus
```csharp
var gameBus = EventBus.GetOrCreateBus(
ShrinkBusKey.Game,
ShrinkBusOptions.MainThread());
var modBus = EventBus.CreateBus(
ShrinkBusKey.Mod("com.example.mod"),
ShrinkBusOptions.DedicatedThread());
var workerBus = EventBus.CreateBus(
new ShrinkBusKey("worker", "pathfinding"),
ShrinkBusOptions.TaskPool(maxConcurrency: 4));
```
标准 Key 包括 `Game``Server``Scene(id)``Mod(id)``World(id)`。调度器在 Bus 创建时确定,之后不可修改:
| Scheduler | 语义 |
|---|---|
| `Inline` | 在发布线程立即执行 |
| `MainThread` | Unity PlayerLoop 主线程;已在主线程时直调 |
| `DedicatedThread` | 有界队列、专属线程、Ordered 串行 |
| `TaskPool` | 有界队列、显式最大并发;Parallel 忽略优先级顺序 |
`Post` 表示投递:同线程同步完成,跨线程入队后返回。`PostAsync` 等待全部 handler 完成,并传播取消与异常。需要结果或最终取消状态时使用 `PostAsync`
## 唯一订阅入口
```csharp
[ShrinkEventSubscriber(
OwnerId = "com.example.mod",
DefaultBus = "mod:com.example.mod")]
public sealed class PlayerHandlers
{
[ShrinkSubscribe(Priority = ShrinkEventPriority.High)]
private void OnJoined(PlayerJoinedEvent value)
{
}
[ShrinkSubscribe]
private async UniTask OnLoaded(
PlayerLoadedEvent value,
CancellationToken cancellationToken)
{
await UniTask.Yield(cancellationToken);
}
}
```
Bus 解析顺序:方法 `Bus`、类型 `DefaultBus``Attach` 传入默认 Bus、否则 `game`
普通对象由宿主管理生命周期:
```csharp
using var binding = EventBus.Attach(new PlayerHandlers());
```
MonoBehaviour 不需要继承 SDK 基类。可自行在 `OnEnable/OnDisable` 中 Attach/Dispose,也可以在 GameObject 上添加 `ShrinkMonoEventScope`,由它统一绑定同对象或子层级中的生成 subscriber。
静态类型同样只使用特性:
```csharp
[ShrinkEventSubscriber(DefaultBus = "game")]
public static class GlobalHandlers
{
[ShrinkSubscribe]
private static void OnStartup(GameStartupEvent value) { }
}
```
Unity ILPostProcessor 会生成直接调用桥和静态模块初始化注册;纯 .NET incremental generator 要求 subscriber 是 `partial`,并生成 ValueTask handler 绑定。生产路径不枚举程序集和方法,不使用 `MethodInfo.Invoke``DynamicInvoke``object[]` 参数。
## Unity 与纯 .NET
Unity 包依赖 UniTask
```csharp
UniTask<ShrinkPostResult> PostAsync<TEvent>(...)
```
纯 .NET 实现在 `Tools~/DotNet/ShrinkEventBus.Core`
```csharp
ValueTask<ShrinkPostResult> PostAsync<TEvent>(...)
```
两边共享事件与特性契约,awaitable 和调度实现按宿主选择,不把 UniTask/ValueTask 写进事件数据。
## ECS/Burst
`com.cneicy.shrink-eventbus-entities` 提供:
```csharp
ShrinkEcsEventWriter<TEvent>
ShrinkEcsEventQueue<TEvent>.Playback(IShrinkEventBus bus)
```
Burst Job 只写 unmanaged 事实事件到 NativeQueuePlayback 在托管/ECS 系统阶段进入相同 Bus。取消、结果或等待异步完成的事件不应从 Burst Job 直接发布。
## 队列与关闭
`ShrinkBusOptions` 配置队列容量、溢出策略、最大并发、关闭排空和超时。关键业务默认使用 `Reject``DropNewest/DropOldest` 只适用于明确允许丢失的数据;`Wait` 只由 `PostAsync` 使用。
## UI Toolkit 调试器
菜单:`ShrinkSDK/事件总线/事件查看器`
窗口使用密集的 Bus 导航、事件流和事件详情三段布局,提供 Bus/Scheduler/DispatchMode/队列/溢出策略/handler 数量、实时速率、平均分发耗时、同步或异步执行方式、实际线程、`ShrinkPostResult`、问题筛选、跟随、搜索、CSV 导出与最多 5000 条环形保留。详细采样只在窗口开启且 `Capture` 为启用状态时生效,暂停或关闭窗口后不计时、不保留事件对象。
它只观察静态 `EventBus` 宿主管理的 Bus,不捕获独立 `ShrinkEventBusHost`;性能基准仍使用独立 Host,避免 UI 采样进入被测热路径。
## Benchmark
实现位于 `Benchmark/ShrinkEventBusBenchmark.cs`,入口:
```csharp
ShrinkEventBusBenchmark.Run(
iterations: 1_000_000,
massIterations: 10_000,
registrationIterations: 5_000);
```
2026-08-24 在 Unity `2022.3.62f3`、Windows Editor Play Mode、独立 Inline Host 下三轮中位数(全局调试采样不观察该 Host):
| 场景 | 2.0 ops/s | 1.3.0 基线 |
|---|---:|---:|
| 0 handler | 129,282,649 | 8,971,378 |
| 1 handler | 67,249,586 | 6,132,242 |
| 8 handlers, struct | 35,024,798 | 无同口径数据 |
| 8 handlers, class | 25,321,199 | MessagePipe 图示 25,639,260 |
| 30 handlers | 10,823,612 | 258,805 |
| canceled skips 10 | 9,029,517 | 1,894,776 |
| 1 async handler | 5,909,446 | 1,168,634 |
| generated Attach + Dispose | 674,291 | instance scan 76,353 |
同步 struct `Post` 热路径实测为 `0 B / 10,000,000 ops`;全局门面在没有 Network bridge 对象观察者时也不会为 struct 事件装箱。MessagePipe 图来自不同机器和 .NET benchmark,class 结果只能说明已达到同一吞吐量级,不能作为跨设备胜负结论。当前 Windows x64 Development IL2CPP Player 已构建成功;仍应在目标平台 Player/设备上复测吞吐。
## 设计边界
- handler 不单独选择线程;需要不同线程亲和性时发布到另一个 Bus。
- Parallel 分发不承诺优先级顺序。
- 同步 `Post` 遇到异步 handler 时是 fire-and-forget;需要等待使用 `PostAsync`
- 外部 Mod DLL 必须携带生成合同;没有合同的生产 DLL 不自动反射注册。
- Editor 诊断是可选观察层,不参与正式无诊断热路径。
架构审计与来源说明见 `Docs/JustAnyProjectArchitectureAudit.md` 和根目录 `DESIGN.md`
+3
View File
@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: e08e301c95a34847a1071cd96bba1eeb
timeCreated: 1772900157
+3
View File
@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: c410edd1d6b644b498d39ff5e1fbc98b
timeCreated: 1772900792
+5
View File
@@ -0,0 +1,5 @@
using System.Runtime.CompilerServices;
[assembly: InternalsVisibleTo("ShrinkNetwork.Integration.EventBus")]
[assembly: InternalsVisibleTo("ShrinkEventBus.Editor")]
[assembly: InternalsVisibleTo("ShrinkEventBus.Tests")]
+11
View File
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 43e85f78c24f1d94ba7d06a4fa37e937
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
+196
View File
@@ -0,0 +1,196 @@
#nullable enable
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Runtime.CompilerServices;
using System.Threading;
using Cysharp.Threading.Tasks;
namespace ShrinkEventBus
{
internal readonly struct ShrinkEventTrace
{
public ShrinkEventTrace(DateTime timestampUtc, Type eventType, ShrinkBusKey busKey,
ShrinkBusOptions options, int threadId, long elapsedTimestampTicks,
ShrinkPostResult result, bool isAsync)
{
TimestampUtc = timestampUtc;
EventType = eventType;
BusKey = busKey;
Scheduler = options.Scheduler;
DispatchMode = options.DispatchMode;
ThreadId = threadId;
ElapsedTimestampTicks = elapsedTimestampTicks;
Result = result;
IsAsync = isAsync;
}
public DateTime TimestampUtc { get; }
public Type EventType { get; }
public ShrinkBusKey BusKey { get; }
public ShrinkBusSchedulerKind Scheduler { get; }
public ShrinkDispatchMode DispatchMode { get; }
public int ThreadId { get; }
public long ElapsedTimestampTicks { get; }
public ShrinkPostResult Result { get; }
public bool IsAsync { get; }
}
public static class EventBus
{
private sealed class GlobalBusResolver : IShrinkBusResolver
{
public IShrinkEventBus GetBus(ShrinkBusKey key)
{
if (TryGetBus(key, out var bus))
return bus;
throw new KeyNotFoundException($"Bus '{key}' is not registered.");
}
public bool TryGetBus(ShrinkBusKey key, out IShrinkEventBus bus) =>
EventBus.TryGetBus(key, out bus);
}
private static readonly ConcurrentDictionary<ShrinkBusKey, IShrinkEventBus> Buses = new();
internal static readonly IShrinkBusResolver Resolver = new GlobalBusResolver();
private static readonly IShrinkEventBus DefaultBus = CreateDefaultBus();
private static Action<IShrinkEvent, Type, ShrinkBusKey>? _posted;
private static Action<ShrinkEventTrace>? _detailedPosted;
internal static event Action<IShrinkEvent, Type, ShrinkBusKey> Posted
{
add
{
_posted += value;
}
remove
{
_posted -= value;
}
}
internal static bool HasPostedObservers
{
[MethodImpl(MethodImplOptions.AggressiveInlining)]
get => Volatile.Read(ref _posted) != null;
}
internal static event Action<ShrinkEventTrace> DetailedPosted
{
add => _detailedPosted += value;
remove => _detailedPosted -= value;
}
internal static bool HasDetailedPostedObservers
{
[MethodImpl(MethodImplOptions.AggressiveInlining)]
get => Volatile.Read(ref _detailedPosted) != null;
}
public static IShrinkEventBus Default => DefaultBus;
public static IShrinkEventBus CreateBus(ShrinkBusKey key, ShrinkBusOptions options)
{
if (options == null)
throw new ArgumentNullException(nameof(options));
var bus = BuildBus(key, options);
if (!Buses.TryAdd(key, bus))
{
bus.Dispose();
throw new InvalidOperationException($"Bus '{key}' is already registered.");
}
ShrinkStaticBindingRegistry.AttachForBus(key, Resolver);
return bus;
}
public static IShrinkEventBus GetOrCreateBus(ShrinkBusKey key, ShrinkBusOptions options)
{
if (options == null)
throw new ArgumentNullException(nameof(options));
var bus = Buses.GetOrAdd(key, busKey => BuildBus(busKey, options));
ShrinkStaticBindingRegistry.AttachForBus(key, Resolver);
return bus;
}
public static bool TryGetBus(ShrinkBusKey key, out IShrinkEventBus bus) =>
Buses.TryGetValue(key, out bus!);
public static bool RemoveBus(ShrinkBusKey key)
{
if (key == ShrinkBusKey.Game || !Buses.TryRemove(key, out var bus))
return false;
ShrinkStaticBindingRegistry.DetachBus(key);
bus.Dispose();
return true;
}
public static ShrinkPostResult Post<TEvent>(in TEvent eventData)
where TEvent : IShrinkEvent => DefaultBus.Post(in eventData);
public static UniTask<ShrinkPostResult> PostAsync<TEvent>(TEvent eventData,
CancellationToken cancellationToken = default)
where TEvent : IShrinkEvent => DefaultBus.PostAsync(eventData, cancellationToken);
public static IDisposable Attach(object target, ShrinkBusKey? defaultBus = null)
{
if (target == null)
throw new ArgumentNullException(nameof(target));
if (target is not IShrinkGeneratedSubscriber generated)
throw new InvalidOperationException(
$"Type {target.GetType().FullName} has no generated [ShrinkSubscribe] binding.");
return generated.AttachGenerated(Resolver, defaultBus ?? ShrinkBusKey.Game);
}
internal static IReadOnlyList<(ShrinkBusKey Key, ShrinkBusOptions Options, int Subscribers)> Snapshot()
{
var snapshot = new List<(ShrinkBusKey, ShrinkBusOptions, int)>(Buses.Count);
foreach (var pair in Buses)
{
var subscribers = pair.Value is ShrinkEventBusInstance instance
? instance.SubscriberCount
: 0;
snapshot.Add((pair.Key, pair.Value.Options, subscribers));
}
return snapshot;
}
private static IShrinkEventBus CreateDefaultBus()
{
var bus = BuildBus(ShrinkBusKey.Game, ShrinkBusOptions.MainThread());
Buses[ShrinkBusKey.Game] = bus;
ShrinkStaticBindingRegistry.AttachForBus(ShrinkBusKey.Game, Resolver);
return bus;
}
private static IShrinkEventBus BuildBus(ShrinkBusKey key, ShrinkBusOptions options) =>
new ShrinkEventBusBuilder()
.WithKey(key)
.WithOptions(options)
.WithPostObserver(NotifyPostedObject)
.Build();
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static void NotifyPostedObject(IShrinkEvent eventData, Type eventType, ShrinkBusKey key)
{
_posted?.Invoke(eventData, eventType, key);
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
internal static void NotifyDetailedPosted(ShrinkEventTrace trace)
{
var observers = Volatile.Read(ref _detailedPosted);
if (observers == null)
return;
try
{
observers(trace);
}
catch (Exception exception)
{
UnityEngine.Debug.LogException(exception);
}
}
}
}
+3
View File
@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: b995a65fd1324255a5ade1ae1f0adf32
timeCreated: 1760099076
+9
View File
@@ -0,0 +1,9 @@
namespace ShrinkEventBus
{
public enum EventResult
{
DEFAULT,
ALLOW,
DENY
}
}
+3
View File
@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: d5213a68ce8f433a98f272904a5d7bd2
timeCreated: 1760098791
+23
View File
@@ -0,0 +1,23 @@
#nullable enable
using System;
using System.Threading;
using Cysharp.Threading.Tasks;
namespace ShrinkEventBus
{
public interface IShrinkEventBus : IDisposable
{
ShrinkBusKey Key { get; }
ShrinkBusOptions Options { get; }
ShrinkPostResult Post<TEvent>(in TEvent eventData)
where TEvent : IShrinkEvent;
UniTask<ShrinkPostResult> PostAsync<TEvent>(TEvent eventData,
CancellationToken cancellationToken = default)
where TEvent : IShrinkEvent;
IDisposable Attach(object target);
}
}
+11
View File
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: cb69f308f5c727542ac602709b02c628
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
+546
View File
@@ -0,0 +1,546 @@
#nullable enable
using System;
using System.Collections.Concurrent;
using System.Diagnostics;
using System.Threading;
using Cysharp.Threading.Tasks;
namespace ShrinkEventBus
{
internal interface IShrinkBusScheduler : IDisposable
{
bool IsOnSchedulerThread { get; }
bool TryPost(Action action);
UniTask PostAsync(Func<UniTask> action, CancellationToken cancellationToken);
UniTask ShutdownAsync(bool drain, TimeSpan timeout);
}
internal sealed class ShrinkQueueFullException : InvalidOperationException
{
public ShrinkQueueFullException(string schedulerName)
: base($"Scheduler queue '{schedulerName}' is full.")
{
}
}
internal static class ShrinkBusSchedulerFactory
{
public static IShrinkBusScheduler Create(ShrinkBusOptions options, string name)
{
return options.Scheduler switch
{
ShrinkBusSchedulerKind.MainThread => new ShrinkMainThreadScheduler(name, options),
ShrinkBusSchedulerKind.DedicatedThread => new ShrinkDedicatedThreadScheduler(name, options),
ShrinkBusSchedulerKind.TaskPool => new ShrinkTaskPoolScheduler(name, options),
_ => new ShrinkInlineScheduler()
};
}
}
internal sealed class ShrinkSchedulerWorkItem
{
private readonly UniTaskCompletionSource? _completion;
public ShrinkSchedulerWorkItem(Func<UniTask> action, bool awaitable)
{
Action = action ?? throw new ArgumentNullException(nameof(action));
if (awaitable)
_completion = new UniTaskCompletionSource();
}
public Func<UniTask> Action { get; }
public UniTask Completion => _completion?.Task ?? UniTask.CompletedTask;
public void Complete() => _completion?.TrySetResult();
public void Fail(Exception exception) => _completion?.TrySetException(exception);
public void Cancel(CancellationToken cancellationToken) => _completion?.TrySetCanceled(cancellationToken);
public void Drop(string schedulerName) =>
_completion?.TrySetException(new ShrinkQueueFullException(schedulerName));
}
internal sealed class ShrinkSchedulerQueue : IDisposable
{
private readonly ConcurrentQueue<ShrinkSchedulerWorkItem> _queue = new();
private readonly SemaphoreSlim _slots;
private readonly ShrinkQueueOverflowPolicy _overflowPolicy;
private readonly string _name;
private int _disposed;
public ShrinkSchedulerQueue(string name, ShrinkBusOptions options)
{
_name = name;
_overflowPolicy = options.OverflowPolicy;
_slots = new SemaphoreSlim(options.QueueCapacity, options.QueueCapacity);
}
public bool IsEmpty => _queue.IsEmpty;
public bool TryEnqueue(ShrinkSchedulerWorkItem item)
{
if (Volatile.Read(ref _disposed) != 0)
return false;
if (!_slots.Wait(0))
{
if (_overflowPolicy != ShrinkQueueOverflowPolicy.DropOldest ||
!_queue.TryDequeue(out var dropped))
return false;
dropped.Drop(_name);
_slots.Release();
if (!_slots.Wait(0))
return false;
}
_queue.Enqueue(item);
return true;
}
public async UniTask EnqueueAsync(ShrinkSchedulerWorkItem item, CancellationToken cancellationToken)
{
if (Volatile.Read(ref _disposed) != 0)
throw new ObjectDisposedException(_name);
if (_overflowPolicy == ShrinkQueueOverflowPolicy.Wait)
{
await _slots.WaitAsync(cancellationToken).AsUniTask(useCurrentSynchronizationContext: false);
if (Volatile.Read(ref _disposed) != 0)
{
_slots.Release();
throw new ObjectDisposedException(_name);
}
_queue.Enqueue(item);
return;
}
if (!TryEnqueue(item))
throw new ShrinkQueueFullException(_name);
}
public bool TryDequeue(out ShrinkSchedulerWorkItem item)
{
if (!_queue.TryDequeue(out item!))
return false;
_slots.Release();
return true;
}
public void DropPending()
{
while (_queue.TryDequeue(out var item))
{
_slots.Release();
item.Drop(_name);
}
}
public void Dispose()
{
if (Interlocked.Exchange(ref _disposed, 1) != 0)
return;
DropPending();
_slots.Dispose();
}
}
internal static class ShrinkSchedulerWorkItemRunner
{
public static async UniTask RunAsync(ShrinkSchedulerWorkItem item)
{
try
{
await item.Action();
item.Complete();
}
catch (OperationCanceledException ex)
{
item.Cancel(ex.CancellationToken);
}
catch (Exception ex)
{
item.Fail(ex);
ShrinkEventDiagnostics.LogException(ex);
}
}
public static void RunBlocking(ShrinkSchedulerWorkItem item)
{
try
{
item.Action().GetAwaiter().GetResult();
item.Complete();
}
catch (OperationCanceledException ex)
{
item.Cancel(ex.CancellationToken);
}
catch (Exception ex)
{
item.Fail(ex);
ShrinkEventDiagnostics.LogException(ex);
}
}
}
internal sealed class ShrinkInlineScheduler : IShrinkBusScheduler
{
public bool IsOnSchedulerThread => true;
public bool TryPost(Action action)
{
action();
return true;
}
public UniTask PostAsync(Func<UniTask> action, CancellationToken cancellationToken)
{
cancellationToken.ThrowIfCancellationRequested();
return action();
}
public UniTask ShutdownAsync(bool drain, TimeSpan timeout) => UniTask.CompletedTask;
public void Dispose() { }
}
internal sealed class ShrinkMainThreadScheduler : IShrinkBusScheduler
{
private readonly ShrinkSchedulerQueue _queue;
private int _pumpScheduled;
private int _disposed;
public ShrinkMainThreadScheduler(string name, ShrinkBusOptions options)
{
_queue = new ShrinkSchedulerQueue(name, options);
}
public bool IsOnSchedulerThread => PlayerLoopHelper.IsMainThread;
public bool TryPost(Action action)
{
if (Volatile.Read(ref _disposed) != 0)
return false;
if (IsOnSchedulerThread)
{
action();
return true;
}
var item = new ShrinkSchedulerWorkItem(() =>
{
action();
return UniTask.CompletedTask;
}, awaitable: false);
if (!_queue.TryEnqueue(item))
return false;
SchedulePump();
return true;
}
public async UniTask PostAsync(Func<UniTask> action, CancellationToken cancellationToken)
{
if (Volatile.Read(ref _disposed) != 0)
throw new ObjectDisposedException(nameof(ShrinkMainThreadScheduler));
cancellationToken.ThrowIfCancellationRequested();
if (IsOnSchedulerThread)
{
await action();
return;
}
var item = new ShrinkSchedulerWorkItem(action, awaitable: true);
await _queue.EnqueueAsync(item, cancellationToken);
SchedulePump();
await item.Completion.AttachExternalCancellation(cancellationToken);
}
public async UniTask ShutdownAsync(bool drain, TimeSpan timeout)
{
Interlocked.Exchange(ref _disposed, 1);
if (!drain)
_queue.DropPending();
else
await ShrinkSchedulerShutdown.WaitUntilDrainedAsync(
_queue, () => Volatile.Read(ref _pumpScheduled) != 0, timeout);
_queue.Dispose();
}
public void Dispose()
{
if (Interlocked.Exchange(ref _disposed, 1) != 0)
return;
_queue.Dispose();
}
private void SchedulePump()
{
if (Interlocked.Exchange(ref _pumpScheduled, 1) != 0)
return;
UniTask.Void(PumpAsync);
}
private async UniTaskVoid PumpAsync()
{
await UniTask.SwitchToMainThread();
try
{
while (_queue.TryDequeue(out var item))
await ShrinkSchedulerWorkItemRunner.RunAsync(item);
}
finally
{
Interlocked.Exchange(ref _pumpScheduled, 0);
if (!_queue.IsEmpty && Volatile.Read(ref _disposed) == 0)
SchedulePump();
}
}
}
internal sealed class ShrinkTaskPoolScheduler : IShrinkBusScheduler
{
private readonly ShrinkSchedulerQueue _queue;
private readonly int _maxConcurrency;
private int _workers;
private int _disposed;
public ShrinkTaskPoolScheduler(string name, ShrinkBusOptions options)
{
_queue = new ShrinkSchedulerQueue(name, options);
_maxConcurrency = options.DispatchMode == ShrinkDispatchMode.Ordered ? 1 : options.MaxConcurrency;
}
public bool IsOnSchedulerThread => false;
public bool TryPost(Action action)
{
if (Volatile.Read(ref _disposed) != 0)
return false;
var item = new ShrinkSchedulerWorkItem(() =>
{
action();
return UniTask.CompletedTask;
}, awaitable: false);
if (!_queue.TryEnqueue(item))
return false;
ScheduleWorkers();
return true;
}
public async UniTask PostAsync(Func<UniTask> action, CancellationToken cancellationToken)
{
if (Volatile.Read(ref _disposed) != 0)
throw new ObjectDisposedException(nameof(ShrinkTaskPoolScheduler));
var item = new ShrinkSchedulerWorkItem(action, awaitable: true);
await _queue.EnqueueAsync(item, cancellationToken);
ScheduleWorkers();
await item.Completion.AttachExternalCancellation(cancellationToken);
}
public async UniTask ShutdownAsync(bool drain, TimeSpan timeout)
{
Interlocked.Exchange(ref _disposed, 1);
if (!drain)
_queue.DropPending();
else
await ShrinkSchedulerShutdown.WaitUntilDrainedAsync(
_queue, () => Volatile.Read(ref _workers) != 0, timeout);
_queue.Dispose();
}
public void Dispose()
{
if (Interlocked.Exchange(ref _disposed, 1) != 0)
return;
_queue.Dispose();
}
private void ScheduleWorkers()
{
while (!_queue.IsEmpty)
{
var current = Volatile.Read(ref _workers);
if (current >= _maxConcurrency ||
Interlocked.CompareExchange(ref _workers, current + 1, current) != current)
return;
UniTask.Void(WorkerAsync);
}
}
private async UniTaskVoid WorkerAsync()
{
await UniTask.SwitchToThreadPool();
try
{
while (_queue.TryDequeue(out var item))
await ShrinkSchedulerWorkItemRunner.RunAsync(item);
}
finally
{
Interlocked.Decrement(ref _workers);
if (!_queue.IsEmpty && Volatile.Read(ref _disposed) == 0)
ScheduleWorkers();
}
}
}
internal sealed class ShrinkDedicatedThreadScheduler : IShrinkBusScheduler
{
private readonly ShrinkSchedulerQueue _queue;
private readonly AutoResetEvent _signal = new(false);
private readonly ManualResetEventSlim _stopped = new(false);
private readonly Thread _thread;
private int _disposed;
private int _threadId;
private int _shutdownTimedOut;
private int _waitHandlesDisposed;
public ShrinkDedicatedThreadScheduler(string name, ShrinkBusOptions options)
{
_queue = new ShrinkSchedulerQueue(name, options);
_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 (Volatile.Read(ref _disposed) != 0)
return false;
if (IsOnSchedulerThread)
{
action();
return true;
}
var item = new ShrinkSchedulerWorkItem(() =>
{
action();
return UniTask.CompletedTask;
}, awaitable: false);
if (!_queue.TryEnqueue(item))
return false;
_signal.Set();
return true;
}
public async UniTask PostAsync(Func<UniTask> action, CancellationToken cancellationToken)
{
if (Volatile.Read(ref _disposed) != 0)
throw new ObjectDisposedException(nameof(ShrinkDedicatedThreadScheduler));
if (IsOnSchedulerThread)
{
await action();
return;
}
var item = new ShrinkSchedulerWorkItem(action, awaitable: true);
await _queue.EnqueueAsync(item, cancellationToken);
_signal.Set();
await item.Completion.AttachExternalCancellation(cancellationToken);
}
public async UniTask ShutdownAsync(bool drain, TimeSpan timeout)
{
Interlocked.Exchange(ref _disposed, 1);
if (!drain)
_queue.DropPending();
_signal.Set();
var waitMs = timeout == Timeout.InfiniteTimeSpan
? Timeout.Infinite
: Math.Max(0, (int)Math.Min(int.MaxValue, timeout.TotalMilliseconds));
var stopped = await UniTask.RunOnThreadPool(() => _stopped.Wait(waitMs));
if (!stopped)
{
_queue.DropPending();
Volatile.Write(ref _shutdownTimedOut, 1);
try
{
if (_stopped.IsSet)
DisposeWaitHandles();
}
catch (ObjectDisposedException)
{
}
return;
}
DisposeWaitHandles();
}
public void Dispose()
{
if (Interlocked.Exchange(ref _disposed, 1) != 0)
return;
_queue.DropPending();
Volatile.Write(ref _shutdownTimedOut, 1);
_signal.Set();
}
private void Run()
{
Volatile.Write(ref _threadId, Thread.CurrentThread.ManagedThreadId);
try
{
while (Volatile.Read(ref _disposed) == 0 || !_queue.IsEmpty)
{
if (!_queue.TryDequeue(out var item))
{
_signal.WaitOne(50);
continue;
}
ShrinkSchedulerWorkItemRunner.RunBlocking(item);
}
}
finally
{
_stopped.Set();
if (Volatile.Read(ref _shutdownTimedOut) != 0)
DisposeWaitHandles();
}
}
private void DisposeWaitHandles()
{
if (Interlocked.Exchange(ref _waitHandlesDisposed, 1) != 0)
return;
_queue.Dispose();
_signal.Dispose();
_stopped.Dispose();
}
}
internal static class ShrinkEventDiagnostics
{
public static void LogException(Exception exception)
{
#if UNITY_5_3_OR_NEWER
UnityEngine.Debug.LogException(exception);
#else
Trace.TraceError(exception.ToString());
#endif
}
}
internal static class ShrinkSchedulerShutdown
{
public static async UniTask WaitUntilDrainedAsync(ShrinkSchedulerQueue queue,
Func<bool> hasActiveWork, TimeSpan timeout)
{
var started = Stopwatch.StartNew();
while (!queue.IsEmpty || hasActiveWork())
{
if (timeout != Timeout.InfiniteTimeSpan && started.Elapsed >= timeout)
{
queue.DropPending();
return;
}
await UniTask.Delay(1, ignoreTimeScale: true);
}
}
}
}
+11
View File
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 8d5885f09c9a4de89cd95de44433bb17
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
+47
View File
@@ -0,0 +1,47 @@
using System;
namespace ShrinkEventBus
{
[AttributeUsage(AttributeTargets.Class, Inherited = false, AllowMultiple = false)]
public sealed class ShrinkEventSubscriberAttribute : Attribute
{
public string OwnerId { get; set; } = string.Empty;
public string DefaultBus { get; set; } = string.Empty;
}
[AttributeUsage(AttributeTargets.Method, Inherited = false, AllowMultiple = false)]
public sealed class ShrinkSubscribeAttribute : Attribute
{
public string Bus { get; set; } = string.Empty;
public ShrinkEventPriority Priority { get; set; } = ShrinkEventPriority.Normal;
public int NumericPriority { get; set; }
public bool ReceiveCanceled { get; set; }
public ShrinkSubscribeAttribute() { }
public ShrinkSubscribeAttribute(ShrinkEventPriority priority, bool receiveCanceled = false)
{
Priority = priority;
ReceiveCanceled = receiveCanceled;
}
public ShrinkSubscribeAttribute(int priority)
{
NumericPriority = priority;
Priority = priority switch
{
>= 100 => ShrinkEventPriority.Highest,
>= 50 => ShrinkEventPriority.High,
>= 0 => ShrinkEventPriority.Normal,
>= -50 => ShrinkEventPriority.Low,
_ => ShrinkEventPriority.Lowest
};
}
}
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Struct, Inherited = false)]
public sealed class ShrinkEventAttribute : Attribute
{
public ShrinkDispatchMode Dispatch { get; set; } = ShrinkDispatchMode.Ordered;
}
}
+11
View File
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 32e8ae06929942288c57cd8c89b3d3a8
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
+293
View File
@@ -0,0 +1,293 @@
#nullable enable
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Threading;
using Cysharp.Threading.Tasks;
namespace ShrinkEventBus
{
public interface IShrinkBusResolver
{
IShrinkEventBus GetBus(ShrinkBusKey key);
bool TryGetBus(ShrinkBusKey key, out IShrinkEventBus bus);
}
public interface IShrinkGeneratedSubscriber
{
IDisposable AttachGenerated(IShrinkBusResolver resolver, ShrinkBusKey? defaultBus = null);
}
public sealed class ShrinkEventBinding : IDisposable
{
private readonly List<IDisposable> _items = new();
private bool _disposed;
public void Add(IDisposable subscription)
{
if (subscription == null)
throw new ArgumentNullException(nameof(subscription));
if (_disposed)
{
subscription.Dispose();
throw new ObjectDisposedException(nameof(ShrinkEventBinding));
}
_items.Add(subscription);
}
public void Dispose()
{
if (_disposed)
return;
_disposed = true;
for (var i = _items.Count - 1; i >= 0; i--)
_items[i].Dispose();
_items.Clear();
}
}
public static class ShrinkStaticBindingRegistry
{
private sealed class Entry
{
public long Id;
public ShrinkBusKey Key;
public Func<IShrinkBusResolver, IDisposable> Factory = null!;
}
private static readonly object Gate = new();
private static readonly List<Entry> Entries = new();
private static readonly Dictionary<long, IDisposable> Bindings = new();
private static long _nextId;
public static void Register(ShrinkBusKey key,
Func<IShrinkBusResolver, IDisposable> factory)
{
if (factory == null)
throw new ArgumentNullException(nameof(factory));
lock (Gate)
{
var entry = new Entry
{
Id = Interlocked.Increment(ref _nextId),
Key = key,
Factory = factory
};
Entries.Add(entry);
TryAttachLocked(entry, EventBus.Resolver);
}
}
public static void Register<TEvent>(string bus, Action<TEvent> handler,
ShrinkEventPriority priority, int numericPriority, bool receiveCanceled)
where TEvent : IShrinkEvent
{
var key = ShrinkBusKey.Parse(bus);
Register(key, resolver => ShrinkGeneratedBinding.Subscribe(
resolver, null, bus, null, handler, priority, numericPriority, receiveCanceled));
}
public static void RegisterAsync<TEvent>(string bus, ShrinkAsyncEventHandler<TEvent> handler,
ShrinkEventPriority priority, int numericPriority, bool receiveCanceled)
where TEvent : IShrinkEvent
{
var key = ShrinkBusKey.Parse(bus);
Register(key, resolver => ShrinkGeneratedBinding.SubscribeAsync(
resolver, null, bus, null, handler, priority, numericPriority, receiveCanceled));
}
public static void RegisterAsyncLegacy<TEvent>(string bus, Func<TEvent, UniTask> handler,
ShrinkEventPriority priority, int numericPriority, bool receiveCanceled)
where TEvent : IShrinkEvent
{
var key = ShrinkBusKey.Parse(bus);
Register(key, resolver => ShrinkGeneratedBinding.SubscribeAsyncLegacy(
resolver, null, bus, null, handler, priority, numericPriority, receiveCanceled));
}
internal static void AttachForBus(ShrinkBusKey key, IShrinkBusResolver resolver)
{
lock (Gate)
{
for (var i = 0; i < Entries.Count; i++)
{
if (Entries[i].Key == key)
TryAttachLocked(Entries[i], resolver);
}
}
}
internal static void DetachBus(ShrinkBusKey key)
{
lock (Gate)
{
for (var i = 0; i < Entries.Count; i++)
{
var entry = Entries[i];
if (entry.Key != key || !Bindings.TryGetValue(entry.Id, out var binding))
continue;
Bindings.Remove(entry.Id);
binding.Dispose();
}
}
}
private static void TryAttachLocked(Entry entry, IShrinkBusResolver resolver)
{
if (Bindings.ContainsKey(entry.Id) || !resolver.TryGetBus(entry.Key, out _))
return;
Bindings.Add(entry.Id, entry.Factory(resolver));
}
}
public sealed class ShrinkEventBusHost : IShrinkBusResolver, IDisposable
{
private readonly ConcurrentDictionary<ShrinkBusKey, IShrinkEventBus> _buses = new();
private bool _disposed;
public IEnumerable<IShrinkEventBus> Buses => _buses.Values;
public IShrinkEventBus CreateBus(ShrinkBusKey key, ShrinkBusOptions options)
{
if (_disposed)
throw new ObjectDisposedException(nameof(ShrinkEventBusHost));
if (options == null)
throw new ArgumentNullException(nameof(options));
var bus = new ShrinkEventBusBuilder()
.WithKey(key)
.WithOptions(options)
.Build();
if (!_buses.TryAdd(key, bus))
{
if (bus is IDisposable disposable)
disposable.Dispose();
throw new InvalidOperationException($"Bus '{key}' is already registered.");
}
return bus;
}
public IShrinkEventBus GetOrCreateBus(ShrinkBusKey key, Func<ShrinkBusOptions> optionsFactory)
{
if (_disposed)
throw new ObjectDisposedException(nameof(ShrinkEventBusHost));
if (optionsFactory == null)
throw new ArgumentNullException(nameof(optionsFactory));
return _buses.GetOrAdd(key, busKey =>
new ShrinkEventBusBuilder()
.WithKey(busKey)
.WithOptions(optionsFactory())
.Build());
}
public IShrinkEventBus GetBus(ShrinkBusKey key)
{
if (_buses.TryGetValue(key, out var bus))
return bus;
throw new KeyNotFoundException($"Bus '{key}' is not registered.");
}
public bool TryGetBus(ShrinkBusKey key, out IShrinkEventBus bus) => _buses.TryGetValue(key, out bus!);
public IDisposable Attach(object target, ShrinkBusKey? defaultBus = null)
{
if (target == null)
throw new ArgumentNullException(nameof(target));
if (target is IShrinkGeneratedSubscriber generated)
return generated.AttachGenerated(this, defaultBus);
var bus = GetBus(defaultBus ?? ShrinkBusKey.Game);
return bus.Attach(target);
}
public bool RemoveBus(ShrinkBusKey key)
{
if (!_buses.TryRemove(key, out var bus))
return false;
if (bus is IDisposable disposable)
disposable.Dispose();
return true;
}
public void Dispose()
{
if (_disposed)
return;
_disposed = true;
foreach (var bus in _buses.Values)
{
if (bus is IDisposable disposable)
disposable.Dispose();
}
_buses.Clear();
}
public async UniTask ShutdownAsync()
{
if (_disposed)
return;
_disposed = true;
foreach (var bus in _buses.Values)
{
if (bus is ShrinkEventBusInstance instance)
await instance.ShutdownAsync();
else
bus.Dispose();
}
_buses.Clear();
}
}
public static class ShrinkGeneratedBinding
{
public static IShrinkBusResolver RuntimeResolver => EventBus.Resolver;
public static IDisposable Subscribe<TEvent>(IShrinkBusResolver resolver, ShrinkBusKey? defaultBus,
string? configuredBus, object? owner, Action<TEvent> handler, ShrinkEventPriority priority,
int numericPriority, bool receiveCanceled) where TEvent : IShrinkEvent
{
var bus = ResolveBus(resolver, defaultBus, configuredBus);
if (bus is not ShrinkEventBusInstance instance)
throw new InvalidOperationException("Generated bindings require the built-in ShrinkEventBus implementation.");
return instance.SubscribeGenerated(handler,
new ShrinkSubscribeDescriptor(configuredBus, priority, numericPriority, receiveCanceled));
}
public static IDisposable SubscribeAsync<TEvent>(IShrinkBusResolver resolver, ShrinkBusKey? defaultBus,
string? configuredBus, object? owner, ShrinkAsyncEventHandler<TEvent> handler,
ShrinkEventPriority priority, int numericPriority, bool receiveCanceled) where TEvent : IShrinkEvent
{
var bus = ResolveBus(resolver, defaultBus, configuredBus);
if (bus is not ShrinkEventBusInstance instance)
throw new InvalidOperationException("Generated bindings require the built-in ShrinkEventBus implementation.");
return instance.SubscribeGenerated(handler,
new ShrinkSubscribeDescriptor(configuredBus, priority, numericPriority, receiveCanceled));
}
public static IDisposable SubscribeAsyncLegacy<TEvent>(IShrinkBusResolver resolver,
ShrinkBusKey? defaultBus, string? configuredBus, object? owner,
Func<TEvent, UniTask> handler, ShrinkEventPriority priority,
int numericPriority, bool receiveCanceled) where TEvent : IShrinkEvent
{
if (handler == null)
throw new ArgumentNullException(nameof(handler));
return SubscribeAsync<TEvent>(resolver, defaultBus, configuredBus, owner,
(eventData, _) => handler(eventData), priority, numericPriority, receiveCanceled);
}
private static IShrinkEventBus ResolveBus(IShrinkBusResolver resolver, ShrinkBusKey? defaultBus,
string? configuredBus)
{
if (resolver == null)
throw new ArgumentNullException(nameof(resolver));
var key = !string.IsNullOrWhiteSpace(configuredBus)
? ShrinkBusKey.Parse(configuredBus)
: defaultBus ?? ShrinkBusKey.Game;
return resolver.GetBus(key);
}
}
}
+11
View File
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: f19286886fb64b3ba7bfbd010c540913
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
+12
View File
@@ -0,0 +1,12 @@
{
"name": "ShrinkEventBus.Runtime",
"rootNamespace": "ShrinkEventBus",
"references": [
"UniTask"
],
"includePlatforms": [],
"excludePlatforms": [],
"allowUnsafeCode": false,
"overrideReferences": false,
"autoReferenced": true
}
@@ -0,0 +1,7 @@
fileFormatVersion: 2
guid: f9a7e109b2ecd4946ba9202d2288af15
AssemblyDefinitionImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:
+36
View File
@@ -0,0 +1,36 @@
#nullable enable
using System;
namespace ShrinkEventBus
{
public sealed class ShrinkEventBusBuilder
{
internal ShrinkBusKey Key { get; private set; } = ShrinkBusKey.Game;
internal ShrinkBusOptions Options { get; private set; } = ShrinkBusOptions.Inline();
internal Action<IShrinkEvent, Type, ShrinkBusKey>? PostObserver { get; private set; }
public ShrinkEventBusBuilder WithKey(ShrinkBusKey key)
{
Key = key;
return this;
}
public ShrinkEventBusBuilder WithOptions(ShrinkBusOptions options)
{
Options = (options ?? throw new ArgumentNullException(nameof(options))).CloneValidated();
return this;
}
public IShrinkEventBus Build() => Options.Scheduler == ShrinkBusSchedulerKind.Inline
? new ShrinkInlineEventBusInstance(this)
: new ShrinkEventBusInstance(this);
internal ShrinkEventBusBuilder WithPostObserver(
Action<IShrinkEvent, Type, ShrinkBusKey> observer)
{
PostObserver = observer ?? throw new ArgumentNullException(nameof(observer));
return this;
}
}
}
+11
View File
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: aad8b06ecb53b8a4998fd9944673e537
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
+272
View File
@@ -0,0 +1,272 @@
#nullable enable
using System;
using System.Collections.Generic;
using System.Runtime.CompilerServices;
using System.Threading;
using Cysharp.Threading.Tasks;
namespace ShrinkEventBus
{
public delegate UniTask ShrinkAsyncEventHandler<in TEvent>(TEvent eventData,
CancellationToken cancellationToken) where TEvent : IShrinkEvent;
internal interface IShrinkEventChannel
{
int Count { get; }
bool RemoveSubscription(long subscriptionId);
void Clear();
void DetachSlot(int slot);
}
internal static class ShrinkEventChannelSlots<TEvent> where TEvent : IShrinkEvent
{
private static readonly object Gate = new();
private static ShrinkEventChannel<TEvent>?[] _slots =
Array.Empty<ShrinkEventChannel<TEvent>?>();
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static ShrinkEventChannel<TEvent>? Get(int slot)
{
var snapshot = _slots;
return (uint)slot < (uint)snapshot.Length ? snapshot[slot] : null;
}
public static void Set(int slot, ShrinkEventChannel<TEvent> channel)
{
lock (Gate)
{
var current = _slots;
var length = current.Length;
if (length <= slot)
{
length = Math.Max(4, length);
while (length <= slot)
length *= 2;
}
var next = new ShrinkEventChannel<TEvent>?[length];
Array.Copy(current, next, current.Length);
next[slot] = channel;
Volatile.Write(ref _slots, next);
}
}
public static void Clear(int slot, ShrinkEventChannel<TEvent> channel)
{
lock (Gate)
{
var current = _slots;
if ((uint)slot >= (uint)current.Length || !ReferenceEquals(current[slot], channel))
return;
var next = (ShrinkEventChannel<TEvent>?[])current.Clone();
next[slot] = null;
Volatile.Write(ref _slots, next);
}
}
}
internal sealed class ShrinkEventChannel<TEvent> : IShrinkEventChannel
where TEvent : IShrinkEvent
{
private static readonly bool SupportsCancellation =
typeof(IShrinkCancelableEvent).IsAssignableFrom(typeof(TEvent));
private sealed class HandlerEntry
{
public long SubscriptionId;
public long RegistrationOrder;
public Action<TEvent>? SyncHandler;
public ShrinkAsyncEventHandler<TEvent>? AsyncHandler;
public ShrinkSubscribeDescriptor Descriptor;
}
private readonly object _gate = new();
private readonly List<HandlerEntry> _entries = new();
private HandlerEntry[] _snapshot = Array.Empty<HandlerEntry>();
private Action<TEvent>? _syncDispatcher;
public int Count => _snapshot.Length;
public void Add(long subscriptionId, long registrationOrder, Action<TEvent> handler,
ShrinkSubscribeDescriptor descriptor)
{
if (handler == null)
throw new ArgumentNullException(nameof(handler));
lock (_gate)
{
_entries.Add(new HandlerEntry
{
SubscriptionId = subscriptionId,
RegistrationOrder = registrationOrder,
SyncHandler = handler,
Descriptor = descriptor
});
RebuildSnapshot();
}
}
public void Add(long subscriptionId, long registrationOrder,
ShrinkAsyncEventHandler<TEvent> handler, ShrinkSubscribeDescriptor descriptor)
{
if (handler == null)
throw new ArgumentNullException(nameof(handler));
lock (_gate)
{
_entries.Add(new HandlerEntry
{
SubscriptionId = subscriptionId,
RegistrationOrder = registrationOrder,
AsyncHandler = handler,
Descriptor = descriptor
});
RebuildSnapshot();
}
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public ShrinkPostResult Post(in TEvent eventData)
{
var syncDispatcher = _syncDispatcher;
if (syncDispatcher != null)
{
syncDispatcher(eventData);
return new ShrinkPostResult(true, true, false);
}
var snapshot = _snapshot;
var handled = false;
for (var i = 0; i < snapshot.Length; i++)
{
var entry = snapshot[i];
if (IsCanceled(eventData) && !entry.Descriptor.ReceiveCanceled)
continue;
if (entry.SyncHandler != null)
entry.SyncHandler(eventData);
else if (entry.AsyncHandler != null)
entry.AsyncHandler(eventData, CancellationToken.None).Forget(ShrinkEventDiagnostics.LogException);
else
continue;
handled = true;
}
return ShrinkPostResult.Completed(handled, IsCanceled(eventData));
}
public async UniTask<ShrinkPostResult> PostAsync(TEvent eventData, ShrinkDispatchMode dispatchMode,
CancellationToken cancellationToken)
{
var snapshot = _snapshot;
var handled = false;
if (dispatchMode == ShrinkDispatchMode.Parallel)
{
var tasks = new List<UniTask>(snapshot.Length);
for (var i = 0; i < snapshot.Length; i++)
{
cancellationToken.ThrowIfCancellationRequested();
var entry = snapshot[i];
if (IsCanceled(eventData) && !entry.Descriptor.ReceiveCanceled)
continue;
if (entry.SyncHandler != null)
entry.SyncHandler(eventData);
else if (entry.AsyncHandler != null)
tasks.Add(entry.AsyncHandler(eventData, cancellationToken));
else
continue;
handled = true;
}
if (tasks.Count > 0)
await UniTask.WhenAll(tasks);
}
else
{
for (var i = 0; i < snapshot.Length; i++)
{
cancellationToken.ThrowIfCancellationRequested();
var entry = snapshot[i];
if (IsCanceled(eventData) && !entry.Descriptor.ReceiveCanceled)
continue;
if (entry.SyncHandler != null)
entry.SyncHandler(eventData);
else if (entry.AsyncHandler != null)
await entry.AsyncHandler(eventData, cancellationToken);
else
continue;
handled = true;
}
}
return ShrinkPostResult.Completed(handled, IsCanceled(eventData));
}
public bool RemoveSubscription(long subscriptionId)
{
lock (_gate)
{
var removed = _entries.RemoveAll(entry => entry.SubscriptionId == subscriptionId) > 0;
RebuildSnapshot();
return removed;
}
}
public void Clear()
{
lock (_gate)
{
_entries.Clear();
_snapshot = Array.Empty<HandlerEntry>();
_syncDispatcher = null;
}
}
public void DetachSlot(int slot) => ShrinkEventChannelSlots<TEvent>.Clear(slot, this);
private void RebuildSnapshot()
{
_entries.Sort(static (left, right) =>
{
var leftOrder = left.Descriptor.NumericPriority == 0
? (int)left.Descriptor.Priority * 1000
: -left.Descriptor.NumericPriority;
var rightOrder = right.Descriptor.NumericPriority == 0
? (int)right.Descriptor.Priority * 1000
: -right.Descriptor.NumericPriority;
var priority = leftOrder.CompareTo(rightOrder);
return priority != 0
? priority
: left.RegistrationOrder.CompareTo(right.RegistrationOrder);
});
_snapshot = _entries.ToArray();
Action<TEvent>? dispatcher = null;
if (!SupportsCancellation)
{
for (var i = 0; i < _snapshot.Length; i++)
{
var handler = _snapshot[i].SyncHandler;
if (handler == null)
{
dispatcher = null;
break;
}
dispatcher += handler;
}
}
Volatile.Write(ref _syncDispatcher, dispatcher);
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static bool IsCanceled(TEvent eventData)
{
if (!SupportsCancellation)
return false;
return ((IShrinkCancelableEvent)(object)eventData!).IsCanceled;
}
}
}
+11
View File
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 5ffae3efb7664efcb82f2bbefbdd5a4e
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
+392
View File
@@ -0,0 +1,392 @@
#nullable enable
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Runtime.CompilerServices;
using System.Threading;
using Cysharp.Threading.Tasks;
namespace ShrinkEventBus
{
internal class ShrinkEventBusInstance : IShrinkEventBus
{
private sealed class SingleBusResolver : IShrinkBusResolver
{
private readonly IShrinkEventBus _bus;
public SingleBusResolver(IShrinkEventBus bus) => _bus = bus;
public IShrinkEventBus GetBus(ShrinkBusKey key) => key == _bus.Key
? _bus
: throw new KeyNotFoundException($"Bus '{key}' is not available from '{_bus.Key}'.");
public bool TryGetBus(ShrinkBusKey key, out IShrinkEventBus bus)
{
bus = _bus;
return key == _bus.Key;
}
}
private sealed class EventSubscription : IDisposable
{
private ShrinkEventBusInstance? _owner;
private readonly long _subscriptionId;
public EventSubscription(ShrinkEventBusInstance owner, long subscriptionId)
{
_owner = owner;
_subscriptionId = subscriptionId;
}
public void Dispose()
{
Interlocked.Exchange(ref _owner, null)?.UnregisterSubscription(_subscriptionId);
}
}
private static int _nextBusSlot;
private readonly Dictionary<Type, IShrinkEventChannel> _channels = new();
private readonly object _channelGate = new();
private readonly IShrinkBusScheduler _scheduler;
private readonly Action<IShrinkEvent, Type, ShrinkBusKey>? _postObserver;
private readonly int _busSlot;
private readonly bool _isInline;
private long _nextSubscriptionId;
private int _subscriberCount;
private int _disposed;
public ShrinkEventBusInstance(ShrinkEventBusBuilder builder)
{
if (builder == null)
throw new ArgumentNullException(nameof(builder));
Key = builder.Key;
Options = builder.Options.CloneValidated();
_busSlot = Interlocked.Increment(ref _nextBusSlot);
_scheduler = ShrinkBusSchedulerFactory.Create(Options, Key.Id);
_postObserver = builder.PostObserver;
_isInline = Options.Scheduler == ShrinkBusSchedulerKind.Inline;
}
public ShrinkBusKey Key { get; }
public ShrinkBusOptions Options { get; }
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public virtual ShrinkPostResult Post<TEvent>(in TEvent eventData) where TEvent : IShrinkEvent
{
if (eventData is null)
throw new ArgumentNullException(nameof(eventData));
if (Volatile.Read(ref _disposed) != 0)
return TraceWithoutDispatch(in eventData,
ShrinkPostResult.Rejected(ShrinkPostFailure.BusStopped), false);
if (_isInline || _scheduler.IsOnSchedulerThread)
return Dispatch(in eventData);
var queuedEvent = eventData;
if (_scheduler.TryPost(() => Dispatch(in queuedEvent)))
return new ShrinkPostResult(true, false, false);
return TraceWithoutDispatch(in eventData,
ShrinkPostResult.Rejected(ShrinkPostFailure.QueueFull), false);
}
public virtual UniTask<ShrinkPostResult> PostAsync<TEvent>(TEvent eventData,
CancellationToken cancellationToken = default) where TEvent : IShrinkEvent
{
if (eventData is null)
throw new ArgumentNullException(nameof(eventData));
if (Volatile.Read(ref _disposed) != 0)
return UniTask.FromResult(TraceWithoutDispatch(in eventData,
ShrinkPostResult.Rejected(ShrinkPostFailure.BusStopped), true));
if (cancellationToken.IsCancellationRequested)
return UniTask.FromResult(TraceWithoutDispatch(in eventData,
new ShrinkPostResult(true, false, false, ShrinkPostFailure.Canceled), true));
if (_isInline || _scheduler.IsOnSchedulerThread)
return DispatchAsync(eventData, cancellationToken);
return PostScheduledAsync(eventData, cancellationToken);
}
private async UniTask<ShrinkPostResult> PostScheduledAsync<TEvent>(TEvent eventData,
CancellationToken cancellationToken) where TEvent : IShrinkEvent
{
var result = default(ShrinkPostResult);
var dispatchStarted = false;
try
{
await _scheduler.PostAsync(async () =>
{
dispatchStarted = true;
result = await DispatchAsync(eventData, cancellationToken);
}, cancellationToken);
return result;
}
catch (ShrinkQueueFullException)
{
return TraceWithoutDispatch(in eventData,
ShrinkPostResult.Rejected(ShrinkPostFailure.QueueFull), true);
}
catch (ObjectDisposedException)
{
return TraceWithoutDispatch(in eventData,
ShrinkPostResult.Rejected(ShrinkPostFailure.BusStopped), true);
}
catch (OperationCanceledException) when (cancellationToken.IsCancellationRequested)
{
var canceled = new ShrinkPostResult(true, false, false, ShrinkPostFailure.Canceled);
return dispatchStarted ? canceled : TraceWithoutDispatch(in eventData, canceled, true);
}
}
public IDisposable Attach(object target)
{
if (target == null)
throw new ArgumentNullException(nameof(target));
if (target is not IShrinkGeneratedSubscriber generated)
throw new InvalidOperationException(
$"Type {target.GetType().FullName} has no generated [ShrinkSubscribe] binding.");
return generated.AttachGenerated(new SingleBusResolver(this), Key);
}
internal IDisposable SubscribeGenerated<TEvent>(Action<TEvent> handler,
ShrinkSubscribeDescriptor descriptor) where TEvent : IShrinkEvent
{
var subscriptionId = Interlocked.Increment(ref _nextSubscriptionId);
GetOrCreateChannel<TEvent>().Add(subscriptionId, subscriptionId, handler, descriptor);
Interlocked.Increment(ref _subscriberCount);
return new EventSubscription(this, subscriptionId);
}
internal IDisposable SubscribeGenerated<TEvent>(ShrinkAsyncEventHandler<TEvent> handler,
ShrinkSubscribeDescriptor descriptor) where TEvent : IShrinkEvent
{
var subscriptionId = Interlocked.Increment(ref _nextSubscriptionId);
GetOrCreateChannel<TEvent>().Add(subscriptionId, subscriptionId, handler, descriptor);
Interlocked.Increment(ref _subscriberCount);
return new EventSubscription(this, subscriptionId);
}
internal int SubscriberCount
{
get
{
return Math.Max(0, Volatile.Read(ref _subscriberCount));
}
}
internal async UniTask ShutdownAsync()
{
if (Interlocked.Exchange(ref _disposed, 1) != 0)
return;
await _scheduler.ShutdownAsync(Options.DrainOnShutdown, Options.ShutdownTimeout);
ClearChannels();
}
public void Dispose()
{
if (Interlocked.Exchange(ref _disposed, 1) != 0)
return;
_scheduler.Dispose();
ClearChannels();
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
protected ShrinkPostResult Dispatch<TEvent>(in TEvent eventData) where TEvent : IShrinkEvent
{
if (_postObserver != null && EventBus.HasDetailedPostedObservers)
return DispatchObserved(in eventData);
var channel = ShrinkEventChannelSlots<TEvent>.Get(_busSlot);
var result = channel == null
? new ShrinkPostResult(true, false, false)
: channel.Post(in eventData);
if (_postObserver != null && EventBus.HasPostedObservers)
_postObserver(eventData, typeof(TEvent), Key);
return result;
}
protected async UniTask<ShrinkPostResult> DispatchAsync<TEvent>(TEvent eventData,
CancellationToken cancellationToken) where TEvent : IShrinkEvent
{
if (_postObserver != null && EventBus.HasDetailedPostedObservers)
return await DispatchAsyncObserved(eventData, cancellationToken);
var channel = ShrinkEventChannelSlots<TEvent>.Get(_busSlot);
var result = channel == null
? ShrinkPostResult.Completed(false)
: await channel.PostAsync(eventData, Options.DispatchMode, cancellationToken);
if (_postObserver != null && EventBus.HasPostedObservers)
_postObserver(eventData, typeof(TEvent), Key);
return result;
}
private ShrinkPostResult DispatchObserved<TEvent>(in TEvent eventData) where TEvent : IShrinkEvent
{
var startedAt = Stopwatch.GetTimestamp();
var threadId = Thread.CurrentThread.ManagedThreadId;
var channel = ShrinkEventChannelSlots<TEvent>.Get(_busSlot);
ShrinkPostResult result;
try
{
result = channel == null
? new ShrinkPostResult(true, false, false)
: channel.Post(in eventData);
}
catch
{
var failedResult = new ShrinkPostResult(true, channel != null,
eventData is IShrinkCancelableEvent cancelable && cancelable.IsCanceled,
ShrinkPostFailure.HandlerException);
EventBus.NotifyDetailedPosted(new ShrinkEventTrace(DateTime.UtcNow, typeof(TEvent), Key,
Options, threadId, Stopwatch.GetTimestamp() - startedAt, failedResult, false));
throw;
}
var elapsedTicks = Stopwatch.GetTimestamp() - startedAt;
if (EventBus.HasPostedObservers)
_postObserver!(eventData, typeof(TEvent), Key);
EventBus.NotifyDetailedPosted(new ShrinkEventTrace(DateTime.UtcNow, typeof(TEvent), Key,
Options, threadId, elapsedTicks, result, false));
return result;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
protected ShrinkPostResult TraceWithoutDispatch<TEvent>(in TEvent eventData,
ShrinkPostResult result, bool isAsync) where TEvent : IShrinkEvent
{
if (_postObserver != null && EventBus.HasDetailedPostedObservers)
{
EventBus.NotifyDetailedPosted(new ShrinkEventTrace(DateTime.UtcNow, typeof(TEvent), Key,
Options, Thread.CurrentThread.ManagedThreadId, 0L, result, isAsync));
}
return result;
}
private async UniTask<ShrinkPostResult> DispatchAsyncObserved<TEvent>(TEvent eventData,
CancellationToken cancellationToken) where TEvent : IShrinkEvent
{
var startedAt = Stopwatch.GetTimestamp();
var threadId = Thread.CurrentThread.ManagedThreadId;
var channel = ShrinkEventChannelSlots<TEvent>.Get(_busSlot);
ShrinkPostResult result;
try
{
result = channel == null
? ShrinkPostResult.Completed(false)
: await channel.PostAsync(eventData, Options.DispatchMode, cancellationToken);
}
catch (OperationCanceledException) when (cancellationToken.IsCancellationRequested)
{
var canceledResult = new ShrinkPostResult(true, channel != null, false,
ShrinkPostFailure.Canceled);
EventBus.NotifyDetailedPosted(new ShrinkEventTrace(DateTime.UtcNow, typeof(TEvent), Key,
Options, threadId, Stopwatch.GetTimestamp() - startedAt, canceledResult, true));
throw;
}
catch
{
var failedResult = new ShrinkPostResult(true, channel != null,
eventData is IShrinkCancelableEvent cancelable && cancelable.IsCanceled,
ShrinkPostFailure.HandlerException);
EventBus.NotifyDetailedPosted(new ShrinkEventTrace(DateTime.UtcNow, typeof(TEvent), Key,
Options, threadId, Stopwatch.GetTimestamp() - startedAt, failedResult, true));
throw;
}
var elapsedTicks = Stopwatch.GetTimestamp() - startedAt;
if (EventBus.HasPostedObservers)
_postObserver!(eventData, typeof(TEvent), Key);
EventBus.NotifyDetailedPosted(new ShrinkEventTrace(DateTime.UtcNow, typeof(TEvent), Key,
Options, threadId, elapsedTicks, result, true));
return result;
}
private ShrinkEventChannel<TEvent> GetOrCreateChannel<TEvent>() where TEvent : IShrinkEvent
{
var existing = ShrinkEventChannelSlots<TEvent>.Get(_busSlot);
if (existing != null)
return existing;
lock (_channelGate)
{
existing = ShrinkEventChannelSlots<TEvent>.Get(_busSlot);
if (existing != null)
return existing;
var channel = new ShrinkEventChannel<TEvent>();
_channels.Add(typeof(TEvent), channel);
ShrinkEventChannelSlots<TEvent>.Set(_busSlot, channel);
return channel;
}
}
private void UnregisterSubscription(long subscriptionId)
{
lock (_channelGate)
{
foreach (var channel in _channels.Values)
{
if (channel.RemoveSubscription(subscriptionId))
{
Interlocked.Decrement(ref _subscriberCount);
break;
}
}
}
}
private void ClearChannels()
{
lock (_channelGate)
{
foreach (var channel in _channels.Values)
{
channel.DetachSlot(_busSlot);
channel.Clear();
}
_channels.Clear();
Volatile.Write(ref _subscriberCount, 0);
}
}
protected bool IsDisposed
{
[MethodImpl(MethodImplOptions.AggressiveInlining)]
get => Volatile.Read(ref _disposed) != 0;
}
}
internal sealed class ShrinkInlineEventBusInstance : ShrinkEventBusInstance
{
public ShrinkInlineEventBusInstance(ShrinkEventBusBuilder builder) : base(builder)
{
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public override ShrinkPostResult Post<TEvent>(in TEvent eventData)
{
if (eventData is null)
throw new ArgumentNullException(nameof(eventData));
if (IsDisposed)
return TraceWithoutDispatch(in eventData,
ShrinkPostResult.Rejected(ShrinkPostFailure.BusStopped), false);
return Dispatch(in eventData);
}
public override UniTask<ShrinkPostResult> PostAsync<TEvent>(TEvent eventData,
CancellationToken cancellationToken = default)
{
if (eventData is null)
throw new ArgumentNullException(nameof(eventData));
if (IsDisposed)
return UniTask.FromResult(TraceWithoutDispatch(in eventData,
ShrinkPostResult.Rejected(ShrinkPostFailure.BusStopped), true));
if (cancellationToken.IsCancellationRequested)
return UniTask.FromResult(TraceWithoutDispatch(in eventData,
new ShrinkPostResult(true, false, false, ShrinkPostFailure.Canceled), true));
return DispatchAsync(eventData, cancellationToken);
}
}
}
+11
View File
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 92c1c7cd96e37c8458600728c7c0c57f
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
+213
View File
@@ -0,0 +1,213 @@
#nullable enable
using System;
using System.Runtime.CompilerServices;
namespace ShrinkEventBus
{
/// <summary>Marker shared by managed, Unity and ECS event payloads.</summary>
public interface IShrinkEvent
{
}
public interface IShrinkCancelableEvent : IShrinkEvent
{
bool IsCanceled { get; }
void SetCanceled(bool value);
}
public interface IShrinkResultEvent<TResult> : IShrinkEvent
{
TResult Result { get; }
void SetResult(TResult result);
}
public enum ShrinkEventPriority
{
Highest = 0,
High = 1,
Normal = 2,
Low = 3,
Lowest = 4,
Monitor = 5
}
public enum ShrinkDispatchMode
{
Ordered = 0,
Parallel = 1
}
public enum ShrinkBusSchedulerKind
{
Inline = 0,
MainThread = 1,
DedicatedThread = 2,
TaskPool = 3
}
public enum ShrinkQueueOverflowPolicy
{
Reject = 0,
DropNewest = 1,
DropOldest = 2,
Wait = 3
}
public enum ShrinkPostFailure
{
None = 0,
BusStopped = 1,
QueueFull = 2,
Canceled = 3,
HandlerException = 4,
InvalidEvent = 5
}
public readonly struct ShrinkPostResult
{
private const int AcceptedMask = 1 << 0;
private const int HandledMask = 1 << 1;
private const int CanceledMask = 1 << 2;
private const int FailureShift = 8;
private readonly int _value;
public ShrinkPostResult(bool accepted, bool handled, bool canceled,
ShrinkPostFailure failure = ShrinkPostFailure.None)
{
_value = (accepted ? AcceptedMask : 0) |
(handled ? HandledMask : 0) |
(canceled ? CanceledMask : 0) |
((int)failure << FailureShift);
}
public bool Accepted => (_value & AcceptedMask) != 0;
public bool Handled => (_value & HandledMask) != 0;
public bool Canceled => (_value & CanceledMask) != 0;
public ShrinkPostFailure Failure => (ShrinkPostFailure)((uint)_value >> FailureShift);
public bool Succeeded => Accepted && Failure == ShrinkPostFailure.None;
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static ShrinkPostResult Rejected(ShrinkPostFailure failure) =>
new(false, false, false, failure);
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static ShrinkPostResult Completed(bool handled, bool canceled = false) =>
new(true, handled, canceled);
}
public readonly struct ShrinkBusKey : IEquatable<ShrinkBusKey>
{
public ShrinkBusKey(string scope, string name)
{
if (string.IsNullOrWhiteSpace(scope))
throw new ArgumentException("Bus scope must not be empty.", nameof(scope));
if (string.IsNullOrWhiteSpace(name))
throw new ArgumentException("Bus name must not be empty.", nameof(name));
Scope = scope.Trim().ToLowerInvariant();
Name = name.Trim();
}
public string Scope { get; }
public string Name { get; }
public string Id => Scope == "game" && Name == "default" ? "game" : $"{Scope}:{Name}";
public static ShrinkBusKey Game => new("game", "default");
public static ShrinkBusKey Server => new("server", "default");
public static ShrinkBusKey Scene(string sceneId) => new("scene", sceneId);
public static ShrinkBusKey Mod(string modId) => new("mod", modId);
public static ShrinkBusKey World(string worldId) => new("world", worldId);
public static ShrinkBusKey Parse(string? value)
{
if (string.IsNullOrWhiteSpace(value) || string.Equals(value.Trim(), "game", StringComparison.OrdinalIgnoreCase))
return Game;
var normalized = value.Trim();
var separator = normalized.IndexOf(':');
return separator <= 0 || separator == normalized.Length - 1
? new ShrinkBusKey("custom", normalized)
: new ShrinkBusKey(normalized.Substring(0, separator), normalized.Substring(separator + 1));
}
public bool Equals(ShrinkBusKey other) =>
string.Equals(Scope, other.Scope, StringComparison.OrdinalIgnoreCase) &&
string.Equals(Name, other.Name, StringComparison.Ordinal);
public override bool Equals(object? obj) => obj is ShrinkBusKey other && Equals(other);
public override int GetHashCode() => HashCode.Combine(Scope.ToLowerInvariant(), Name);
public override string ToString() => Id;
public static bool operator ==(ShrinkBusKey left, ShrinkBusKey right) => left.Equals(right);
public static bool operator !=(ShrinkBusKey left, ShrinkBusKey right) => !left.Equals(right);
}
public sealed class ShrinkBusOptions
{
public ShrinkBusSchedulerKind Scheduler { get; set; } = ShrinkBusSchedulerKind.Inline;
public ShrinkDispatchMode DispatchMode { get; set; } = ShrinkDispatchMode.Ordered;
public int QueueCapacity { get; set; } = 4096;
public int MaxConcurrency { get; set; } = Math.Max(1, Environment.ProcessorCount);
public ShrinkQueueOverflowPolicy OverflowPolicy { get; set; } = ShrinkQueueOverflowPolicy.Reject;
public bool DrainOnShutdown { get; set; } = true;
public TimeSpan ShutdownTimeout { get; set; } = TimeSpan.FromSeconds(5);
public static ShrinkBusOptions Inline(ShrinkDispatchMode mode = ShrinkDispatchMode.Ordered) =>
new() { Scheduler = ShrinkBusSchedulerKind.Inline, DispatchMode = mode };
public static ShrinkBusOptions MainThread(int queueCapacity = 4096) =>
new() { Scheduler = ShrinkBusSchedulerKind.MainThread, QueueCapacity = queueCapacity };
public static ShrinkBusOptions DedicatedThread(int queueCapacity = 4096) =>
new() { Scheduler = ShrinkBusSchedulerKind.DedicatedThread, QueueCapacity = queueCapacity };
public static ShrinkBusOptions TaskPool(int maxConcurrency = 0, int queueCapacity = 4096) =>
new()
{
Scheduler = ShrinkBusSchedulerKind.TaskPool,
DispatchMode = ShrinkDispatchMode.Parallel,
QueueCapacity = queueCapacity,
MaxConcurrency = maxConcurrency > 0 ? maxConcurrency : Math.Max(1, Environment.ProcessorCount)
};
internal ShrinkBusOptions CloneValidated()
{
if (QueueCapacity <= 0)
throw new ArgumentOutOfRangeException(nameof(QueueCapacity));
if (MaxConcurrency <= 0)
throw new ArgumentOutOfRangeException(nameof(MaxConcurrency));
if (ShutdownTimeout < TimeSpan.Zero)
throw new ArgumentOutOfRangeException(nameof(ShutdownTimeout));
if (OverflowPolicy == ShrinkQueueOverflowPolicy.Wait && Scheduler == ShrinkBusSchedulerKind.Inline)
throw new InvalidOperationException("Inline buses do not have a queue and cannot use Wait overflow policy.");
return new ShrinkBusOptions
{
Scheduler = Scheduler,
DispatchMode = DispatchMode,
QueueCapacity = QueueCapacity,
MaxConcurrency = MaxConcurrency,
OverflowPolicy = OverflowPolicy,
DrainOnShutdown = DrainOnShutdown,
ShutdownTimeout = ShutdownTimeout
};
}
}
public readonly struct ShrinkSubscribeDescriptor
{
public ShrinkSubscribeDescriptor(string? bus, ShrinkEventPriority priority,
int numericPriority, bool receiveCanceled)
{
Bus = bus ?? string.Empty;
Priority = priority;
NumericPriority = numericPriority;
ReceiveCanceled = receiveCanceled;
}
public string Bus { get; }
public ShrinkEventPriority Priority { get; }
public int NumericPriority { get; }
public bool ReceiveCanceled { get; }
}
}
+11
View File
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 04b409c00e3144fd8d9f3efb7c2901b7
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
+71
View File
@@ -0,0 +1,71 @@
#nullable enable
using System;
using UnityEngine;
namespace ShrinkEventBus
{
/// <summary>
/// Optional Unity lifecycle host for generated event bindings. It does not require
/// the target component to inherit from an SDK base class.
/// </summary>
[DisallowMultipleComponent]
public sealed class ShrinkMonoEventScope : MonoBehaviour
{
[SerializeField] private string bus = "game";
[SerializeField] private bool includeChildren;
private IDisposable? _binding;
private ShrinkEventBinding? _bindingGroup;
public string BusId
{
get => bus;
set => bus = value ?? string.Empty;
}
private void OnEnable()
{
RefreshBindings();
}
private void OnDisable()
{
ReleaseBindings();
}
public void RefreshBindings()
{
ReleaseBindings();
var key = ShrinkBusKey.Parse(bus);
if (!EventBus.TryGetBus(key, out var targetBus))
targetBus = EventBus.GetOrCreateBus(key, ShrinkBusOptions.MainThread());
var components = includeChildren
? GetComponentsInChildren<MonoBehaviour>(true)
: GetComponents<MonoBehaviour>();
for (var i = 0; i < components.Length; i++)
TryAttach(components[i], targetBus);
}
public void ReleaseBindings()
{
_binding?.Dispose();
_binding = null;
_bindingGroup = null;
}
private void TryAttach(MonoBehaviour target, IShrinkEventBus targetBus)
{
if (target == null || ReferenceEquals(target, this))
return;
if (target is not IShrinkGeneratedSubscriber)
return;
var binding = targetBus.Attach(target);
if (_bindingGroup == null)
_bindingGroup = new ShrinkEventBinding();
_bindingGroup.Add(binding);
_binding = _bindingGroup;
}
}
}
+11
View File
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 5fe48da79b2f4a10a338e497f4ce5ae1
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
+8
View File
@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: a41c52d2d40c42479d1f1dfd2d35ae3c
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,15 @@
{
"name": "ShrinkEventBus.PlayMode.Tests",
"rootNamespace": "ShrinkEventBus.PlayMode.Tests",
"references": [
"ShrinkEventBus.Runtime",
"UniTask"
],
"optionalUnityReferences": ["TestAssemblies"],
"includePlatforms": [],
"excludePlatforms": [],
"allowUnsafeCode": false,
"overrideReferences": false,
"autoReferenced": false,
"defineConstraints": ["UNITY_INCLUDE_TESTS"]
}
@@ -0,0 +1,7 @@
fileFormatVersion: 2
guid: b03fa714413c4af5ad4140e7a409ef2f
AssemblyDefinitionImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,48 @@
#nullable enable
using System.Collections;
using Cysharp.Threading.Tasks;
using NUnit.Framework;
using UnityEngine;
using UnityEngine.TestTools;
namespace ShrinkEventBus.PlayMode.Tests
{
internal readonly struct MonoLifecycleEvent : IShrinkEvent
{
public MonoLifecycleEvent(int value) => Value = value;
public int Value { get; }
}
[ShrinkEventSubscriber(DefaultBus = "game")]
internal sealed class MonoLifecycleTarget : MonoBehaviour
{
public int Sum { get; private set; }
[ShrinkSubscribe]
private void OnEvent(MonoLifecycleEvent value) => Sum += value.Value;
}
public sealed class ShrinkMonoEventScopePlayModeTests
{
[UnityTest]
public IEnumerator OnEnableAttachesAndOnDisableReleases()
{
var gameObject = new GameObject("ShrinkMonoEventScope-PlayMode");
gameObject.SetActive(false);
var target = gameObject.AddComponent<MonoLifecycleTarget>();
gameObject.AddComponent<ShrinkMonoEventScope>();
gameObject.SetActive(true);
yield return null;
yield return EventBus.PostAsync(new MonoLifecycleEvent(3)).ToCoroutine();
Assert.AreEqual(3, target.Sum);
gameObject.SetActive(false);
yield return null;
yield return EventBus.PostAsync(new MonoLifecycleEvent(5)).ToCoroutine();
Assert.AreEqual(3, target.Sum);
Object.Destroy(gameObject);
}
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: f42b5f6f2249483b9978f0fa729d479b
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
+8
View File
@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: 6f28409a6ba08444cbdbfc5eb37b1809
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:
+23
View File
@@ -0,0 +1,23 @@
{
"name": "ShrinkEventBus.Tests",
"rootNamespace": "ShrinkEventBus.Tests",
"references": [
"ShrinkEventBus.Runtime",
"UniTask",
"UnityEngine.TestRunner",
"UnityEditor.TestRunner"
],
"includePlatforms": [
"Editor"
],
"excludePlatforms": [],
"allowUnsafeCode": false,
"overrideReferences": false,
"precompiledReferences": [],
"autoReferenced": false,
"defineConstraints": [
"UNITY_INCLUDE_TESTS"
],
"versionDefines": [],
"noEngineReferences": false
}
+7
View File
@@ -0,0 +1,7 @@
fileFormatVersion: 2
guid: 8c47d36b8919bbf4d9c39817eaf577a9
AssemblyDefinitionImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:
+440
View File
@@ -0,0 +1,440 @@
#nullable enable
using System;
using System.Collections;
using System.Collections.Generic;
using System.Threading;
using Cysharp.Threading.Tasks;
using NUnit.Framework;
using UnityEngine;
using UnityEngine.TestTools;
namespace ShrinkEventBus.Tests
{
internal readonly struct GeneratedPingEvent : IShrinkEvent
{
public GeneratedPingEvent(int value) => Value = value;
public int Value { get; }
}
[ShrinkEventSubscriber(DefaultBus = "game")]
internal sealed class CodeGeneratedTarget
{
public int Sum { get; private set; }
[ShrinkSubscribe]
private void OnPing(GeneratedPingEvent evt) => Sum += evt.Value;
}
[ShrinkEventSubscriber(DefaultBus = "game")]
internal static class StaticCodeGeneratedTarget
{
public static int Sum { get; set; }
[ShrinkSubscribe]
private static void OnPing(GeneratedPingEvent evt) => Sum += evt.Value;
}
[ShrinkEventSubscriber(DefaultBus = "mod:late-static")]
internal static class DelayedStaticCodeGeneratedTarget
{
public static int Sum { get; set; }
[ShrinkSubscribe]
private static void OnPing(GeneratedPingEvent evt) => Sum += evt.Value;
}
[ShrinkEventSubscriber(DefaultBus = "game")]
internal sealed class MonoCodeGeneratedTarget : MonoBehaviour
{
public int Sum { get; private set; }
[ShrinkSubscribe]
private void OnPing(GeneratedPingEvent evt) => Sum += evt.Value;
}
public sealed class ShrinkEventBusV2Tests
{
private readonly struct PingEvent : IShrinkEvent
{
public PingEvent(int value) => Value = value;
public int Value { get; }
}
private readonly struct DiagnosticEvent : IShrinkEvent
{
}
private sealed class CancelEvent : IShrinkCancelableEvent
{
public bool IsCanceled { get; private set; }
public void SetCanceled(bool value) => IsCanceled = value;
}
private sealed class ResultEvent : IShrinkResultEvent<int>
{
public int Result { get; private set; }
public void SetResult(int result) => Result = result;
}
private sealed class GeneratedTarget : IShrinkGeneratedSubscriber
{
public int Sum;
public IDisposable AttachGenerated(IShrinkBusResolver resolver, ShrinkBusKey? defaultBus = null)
{
var binding = new ShrinkEventBinding();
binding.Add(ShrinkGeneratedBinding.Subscribe<PingEvent>(resolver, defaultBus, string.Empty,
this, OnPing, ShrinkEventPriority.Normal, 0, false));
return binding;
}
private void OnPing(PingEvent evt) => Sum += evt.Value;
}
[Test]
public void MultipleBusesAreIsolated()
{
using var host = new ShrinkEventBusHost();
var game = host.CreateBus(ShrinkBusKey.Game, ShrinkBusOptions.Inline());
var mod = host.CreateBus(ShrinkBusKey.Mod("sample"), ShrinkBusOptions.Inline());
var gameHits = 0;
var modHits = 0;
using var gameBinding = ShrinkGeneratedBinding.Subscribe<PingEvent>(host, ShrinkBusKey.Game,
string.Empty, this, evt => gameHits += evt.Value, ShrinkEventPriority.Normal, 0, false);
using var modBinding = ShrinkGeneratedBinding.Subscribe<PingEvent>(host, ShrinkBusKey.Mod("sample"),
string.Empty, this, evt => modHits += evt.Value, ShrinkEventPriority.Normal, 0, false);
game.Post(new PingEvent(2));
mod.Post(new PingEvent(3));
Assert.AreEqual(2, gameHits);
Assert.AreEqual(3, modHits);
}
[Test]
public void GeneratedTargetAttachDisposesAllHandlers()
{
using var host = new ShrinkEventBusHost();
var game = host.CreateBus(ShrinkBusKey.Game, ShrinkBusOptions.Inline());
var target = new GeneratedTarget();
var binding = host.Attach(target, ShrinkBusKey.Game);
game.Post(new PingEvent(4));
binding.Dispose();
game.Post(new PingEvent(7));
Assert.AreEqual(4, target.Sum);
}
[Test]
public void AttributeOnlyTargetUsesIlGeneratedBinding()
{
using var host = new ShrinkEventBusHost();
var game = host.CreateBus(ShrinkBusKey.Game, ShrinkBusOptions.Inline());
var target = new CodeGeneratedTarget();
using (host.Attach(target, ShrinkBusKey.Game))
game.Post(new GeneratedPingEvent(9));
game.Post(new GeneratedPingEvent(4));
Assert.AreEqual(9, target.Sum);
}
[Test]
public void StaticAttributeSubscriberUsesModuleInitializerBinding()
{
StaticCodeGeneratedTarget.Sum = 0;
EventBus.Post(new GeneratedPingEvent(11));
Assert.AreEqual(11, StaticCodeGeneratedTarget.Sum);
}
[Test]
public void StaticSubscriberAttachesWhenNamedBusIsCreatedLater()
{
var key = ShrinkBusKey.Mod("late-static");
EventBus.RemoveBus(key);
DelayedStaticCodeGeneratedTarget.Sum = 0;
var bus = EventBus.CreateBus(key, ShrinkBusOptions.Inline());
bus.Post(new GeneratedPingEvent(13));
Assert.AreEqual(13, DelayedStaticCodeGeneratedTarget.Sum);
Assert.IsTrue(EventBus.RemoveBus(key));
}
[UnityTest]
public IEnumerator MonoScopeRefreshAndReleaseControlGeneratedBindings()
{
var gameObject = new GameObject("ShrinkEventBusV2-MonoScope");
gameObject.SetActive(false);
var target = gameObject.AddComponent<MonoCodeGeneratedTarget>();
var scope = gameObject.AddComponent<ShrinkMonoEventScope>();
Assert.IsInstanceOf<IShrinkGeneratedSubscriber>(target);
scope.RefreshBindings();
yield return null;
yield return EventBus.PostAsync(new GeneratedPingEvent(5)).ToCoroutine();
Assert.AreEqual(5, target.Sum);
scope.ReleaseBindings();
yield return EventBus.PostAsync(new GeneratedPingEvent(7)).ToCoroutine();
Assert.AreEqual(5, target.Sum);
UnityEngine.Object.DestroyImmediate(gameObject);
}
[Test]
public void CanceledEventsSkipNormalHandlersButReachMonitors()
{
using var host = new ShrinkEventBusHost();
var game = host.CreateBus(ShrinkBusKey.Game, ShrinkBusOptions.Inline());
var normal = 0;
var monitor = 0;
using var first = ShrinkGeneratedBinding.Subscribe<CancelEvent>(host, ShrinkBusKey.Game,
string.Empty, this, evt => evt.SetCanceled(true), ShrinkEventPriority.Highest, 0, false);
using var second = ShrinkGeneratedBinding.Subscribe<CancelEvent>(host, ShrinkBusKey.Game,
string.Empty, this, _ => normal++, ShrinkEventPriority.Normal, 0, false);
using var third = ShrinkGeneratedBinding.Subscribe<CancelEvent>(host, ShrinkBusKey.Game,
string.Empty, this, _ => monitor++, ShrinkEventPriority.Monitor, 0, true);
var result = game.Post(new CancelEvent());
Assert.IsTrue(result.Canceled);
Assert.AreEqual(0, normal);
Assert.AreEqual(1, monitor);
}
[Test]
public void PriorityAndNumericPriorityAreStable()
{
using var host = new ShrinkEventBusHost();
var game = host.CreateBus(ShrinkBusKey.Game, ShrinkBusOptions.Inline());
var order = new List<string>();
using var normal = ShrinkGeneratedBinding.Subscribe<PingEvent>(host, ShrinkBusKey.Game,
string.Empty, this, _ => order.Add("normal"), ShrinkEventPriority.Normal, 0, false);
using var high = ShrinkGeneratedBinding.Subscribe<PingEvent>(host, ShrinkBusKey.Game,
string.Empty, this, _ => order.Add("high"), ShrinkEventPriority.High, 0, false);
using var numeric = ShrinkGeneratedBinding.Subscribe<PingEvent>(host, ShrinkBusKey.Game,
string.Empty, this, _ => order.Add("numeric"), ShrinkEventPriority.Normal, 75, false);
game.Post(new PingEvent(1));
CollectionAssert.AreEqual(new[] { "numeric", "high", "normal" }, order);
}
[Test]
public void ResultEventUsesTypedResultContract()
{
using var host = new ShrinkEventBusHost();
var game = host.CreateBus(ShrinkBusKey.Game, ShrinkBusOptions.Inline());
using var binding = ShrinkGeneratedBinding.Subscribe<ResultEvent>(host, ShrinkBusKey.Game,
string.Empty, this, value => value.SetResult(42),
ShrinkEventPriority.Normal, 0, false);
var value = new ResultEvent();
var result = game.Post(value);
Assert.IsTrue(result.Handled);
Assert.AreEqual(42, value.Result);
}
[Test]
public void HandlerExceptionPropagatesFromPost()
{
using var host = new ShrinkEventBusHost();
var game = host.CreateBus(ShrinkBusKey.Game, ShrinkBusOptions.Inline());
using var binding = ShrinkGeneratedBinding.Subscribe<PingEvent>(host, ShrinkBusKey.Game,
string.Empty, this, _ => throw new InvalidOperationException("expected"),
ShrinkEventPriority.Normal, 0, false);
var exception = Assert.Throws<InvalidOperationException>(() => game.Post(new PingEvent(1)));
Assert.AreEqual("expected", exception!.Message);
}
[UnityTest]
public IEnumerator PostAsyncCancellationReturnsCanceledFailure()
{
using var host = new ShrinkEventBusHost();
var bus = host.CreateBus(new ShrinkBusKey("worker", "cancellation"),
ShrinkBusOptions.DedicatedThread());
using var source = new CancellationTokenSource();
source.Cancel();
ShrinkPostResult result = default;
yield return bus.PostAsync(new PingEvent(1), source.Token)
.ContinueWith(value => result = value)
.ToCoroutine();
Assert.AreEqual(ShrinkPostFailure.Canceled, result.Failure);
}
[UnityTest]
public IEnumerator DedicatedThreadShutdownDrainsAcceptedWork()
{
var host = new ShrinkEventBusHost();
var key = new ShrinkBusKey("worker", "drain");
var bus = host.CreateBus(key, ShrinkBusOptions.DedicatedThread(queueCapacity: 4));
var hits = 0;
using var binding = ShrinkGeneratedBinding.Subscribe<PingEvent>(host, key,
string.Empty, this, _ => Interlocked.Increment(ref hits),
ShrinkEventPriority.Normal, 0, false);
Assert.IsTrue(bus.Post(new PingEvent(1)).Accepted);
Assert.IsTrue(bus.Post(new PingEvent(2)).Accepted);
yield return host.ShutdownAsync().ToCoroutine();
Assert.AreEqual(2, Volatile.Read(ref hits));
}
[Test]
public void DedicatedThreadRejectsWhenBoundedQueueIsFull()
{
using var host = new ShrinkEventBusHost();
var key = new ShrinkBusKey("worker", "bounded");
var options = ShrinkBusOptions.DedicatedThread(queueCapacity: 1);
options.OverflowPolicy = ShrinkQueueOverflowPolicy.Reject;
var bus = host.CreateBus(key, options);
using var entered = new ManualResetEventSlim(false);
using var release = new ManualResetEventSlim(false);
var hits = 0;
using var binding = ShrinkGeneratedBinding.Subscribe<PingEvent>(host, key, string.Empty,
this, _ =>
{
Interlocked.Increment(ref hits);
entered.Set();
release.Wait(TimeSpan.FromSeconds(2));
}, ShrinkEventPriority.Normal, 0, false);
Assert.IsTrue(bus.Post(new PingEvent(1)).Accepted);
Assert.IsTrue(entered.Wait(TimeSpan.FromSeconds(2)));
Assert.IsTrue(bus.Post(new PingEvent(2)).Accepted);
var rejected = bus.Post(new PingEvent(3));
release.Set();
Assert.IsFalse(rejected.Accepted);
Assert.AreEqual(ShrinkPostFailure.QueueFull, rejected.Failure);
Assert.IsTrue(SpinWait.SpinUntil(() => Volatile.Read(ref hits) == 2, TimeSpan.FromSeconds(2)));
}
[UnityTest]
public IEnumerator TaskPoolHonorsMaximumConcurrency()
{
using var host = new ShrinkEventBusHost();
var key = new ShrinkBusKey("worker", "parallel");
var bus = host.CreateBus(key, ShrinkBusOptions.TaskPool(maxConcurrency: 2, queueCapacity: 8));
var active = 0;
var maxActive = 0;
using var binding = ShrinkGeneratedBinding.SubscribeAsync<PingEvent>(host, key, string.Empty,
this, async (_, _) =>
{
var current = Interlocked.Increment(ref active);
UpdateMaximum(ref maxActive, current);
Thread.Sleep(30);
Interlocked.Decrement(ref active);
await UniTask.CompletedTask;
}, ShrinkEventPriority.Normal, 0, false);
var posts = new List<UniTask<ShrinkPostResult>>();
for (var i = 0; i < 4; i++)
posts.Add(bus.PostAsync(new PingEvent(i)));
yield return UniTask.WhenAll(posts).ToCoroutine();
Assert.AreEqual(2, maxActive);
}
[Test]
public void WarmStructPostDoesNotAllocate()
{
using var host = new ShrinkEventBusHost();
var game = host.CreateBus(ShrinkBusKey.Game, ShrinkBusOptions.Inline());
var sum = 0;
using var binding = ShrinkGeneratedBinding.Subscribe<PingEvent>(host, ShrinkBusKey.Game,
string.Empty, this, evt => sum += evt.Value, ShrinkEventPriority.Normal, 0, false);
for (var i = 0; i < 100; i++)
game.Post(new PingEvent(i));
var before = GC.GetAllocatedBytesForCurrentThread();
for (var i = 0; i < 10_000; i++)
game.Post(new PingEvent(i));
var allocated = GC.GetAllocatedBytesForCurrentThread() - before;
Assert.AreEqual(0, allocated);
Assert.Greater(sum, 0);
}
[Test]
public void GlobalDetailedObserverReportsDispatchMetadata()
{
var captured = false;
var trace = default(ShrinkEventTrace);
void Capture(ShrinkEventTrace value)
{
trace = value;
captured = true;
}
EventBus.DetailedPosted += Capture;
try
{
EventBus.Post(new DiagnosticEvent());
}
finally
{
EventBus.DetailedPosted -= Capture;
}
Assert.IsTrue(captured);
Assert.AreEqual(typeof(DiagnosticEvent), trace.EventType);
Assert.AreEqual(ShrinkBusKey.Game, trace.BusKey);
Assert.AreEqual(ShrinkBusSchedulerKind.MainThread, trace.Scheduler);
Assert.IsTrue(trace.Result.Accepted);
Assert.GreaterOrEqual(trace.ElapsedTimestampTicks, 0L);
}
[Test]
public void GlobalStructPostDoesNotAllocateWithoutObservers()
{
var value = new DiagnosticEvent();
for (var i = 0; i < 100; i++)
EventBus.Post(in value);
var before = GC.GetAllocatedBytesForCurrentThread();
for (var i = 0; i < 10_000; i++)
EventBus.Post(in value);
var allocated = GC.GetAllocatedBytesForCurrentThread() - before;
Assert.AreEqual(0, allocated);
}
[UnityTest]
public IEnumerator DedicatedThreadPostAsyncRunsOnDedicatedThread()
{
using var host = new ShrinkEventBusHost();
var bus = host.CreateBus(new ShrinkBusKey("worker", "tests"),
ShrinkBusOptions.DedicatedThread());
var callerThread = Thread.CurrentThread.ManagedThreadId;
var handlerThread = callerThread;
using var binding = ShrinkGeneratedBinding.Subscribe<PingEvent>(host,
new ShrinkBusKey("worker", "tests"), string.Empty, this,
_ => handlerThread = Thread.CurrentThread.ManagedThreadId,
ShrinkEventPriority.Normal, 0, false);
yield return bus.PostAsync(new PingEvent(1)).ToCoroutine();
Assert.AreNotEqual(callerThread, handlerThread);
}
private static void UpdateMaximum(ref int location, int candidate)
{
while (true)
{
var current = Volatile.Read(ref location);
if (candidate <= current || Interlocked.CompareExchange(ref location, candidate, current) == current)
return;
}
}
}
}
+11
View File
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: f11f5ea66801460ca4ec08d35763d627
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
+11
View File
@@ -0,0 +1,11 @@
# ShrinkEventBus for .NET
- `ShrinkEventBus.Core``netstandard2.1` ValueTask 运行时,不引用 UnityEngine 或 UniTask。
- `ShrinkEventBus.Generator`Roslyn incremental generator,为 `partial` 普通 C# subscriber 生成强类型绑定。
- `ShrinkEventBus.Smoke``net8.0` 烟测,覆盖多 Bus 隔离、Inline 和 DedicatedThread。
```bash
dotnet build Tools~/DotNet/ShrinkEventBus.Core/ShrinkEventBus.Core.csproj -c Release
dotnet build Tools~/DotNet/ShrinkEventBus.Generator/ShrinkEventBus.Generator.csproj -c Release
dotnet run --project Tools~/DotNet/ShrinkEventBus.Smoke/ShrinkEventBus.Smoke.csproj -c Release
```
@@ -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();
}
}
}
@@ -0,0 +1,494 @@
#nullable enable
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;
namespace ShrinkEventBus
{
public delegate ValueTask ShrinkAsyncEventHandler<in TEvent>(TEvent eventData,
CancellationToken cancellationToken) where TEvent : IShrinkEvent;
public interface IShrinkEventBus : IDisposable
{
ShrinkBusKey Key { get; }
ShrinkBusOptions Options { get; }
ShrinkPostResult Post<TEvent>(in TEvent eventData) where TEvent : IShrinkEvent;
ValueTask<ShrinkPostResult> PostAsync<TEvent>(TEvent eventData,
CancellationToken cancellationToken = default) where TEvent : IShrinkEvent;
IDisposable Attach(object target);
}
public interface IShrinkBusResolver
{
IShrinkEventBus GetBus(ShrinkBusKey key);
bool TryGetBus(ShrinkBusKey key, out IShrinkEventBus bus);
}
public interface IShrinkGeneratedSubscriber
{
IDisposable AttachGenerated(IShrinkBusResolver resolver, ShrinkBusKey? defaultBus = null);
}
public sealed class ShrinkEventBinding : IDisposable
{
private readonly List<IDisposable> _items = new List<IDisposable>();
private bool _disposed;
public void Add(IDisposable item)
{
if (item == null)
throw new ArgumentNullException(nameof(item));
if (_disposed)
{
item.Dispose();
throw new ObjectDisposedException(nameof(ShrinkEventBinding));
}
_items.Add(item);
}
public void Dispose()
{
if (_disposed)
return;
_disposed = true;
for (var i = _items.Count - 1; i >= 0; i--)
_items[i].Dispose();
_items.Clear();
}
}
public static class ShrinkStaticBindingRegistry
{
internal sealed class Entry
{
public long Id;
public ShrinkBusKey Key;
public Func<IShrinkBusResolver, IDisposable> Factory = null!;
}
private static readonly object Gate = new object();
private static readonly List<Entry> Entries = new List<Entry>();
private static readonly List<WeakReference<ShrinkEventBusHost>> Hosts =
new List<WeakReference<ShrinkEventBusHost>>();
private static long _nextId;
public static void Register(ShrinkBusKey key,
Func<IShrinkBusResolver, IDisposable> factory)
{
if (factory == null)
throw new ArgumentNullException(nameof(factory));
ShrinkEventBusHost[] hosts;
Entry entry;
lock (Gate)
{
entry = new Entry
{
Id = Interlocked.Increment(ref _nextId),
Key = key,
Factory = factory
};
Entries.Add(entry);
hosts = LiveHostsLocked();
}
for (var i = 0; i < hosts.Length; i++)
hosts[i].TryAttachStatic(entry);
}
internal static void TrackHost(ShrinkEventBusHost host)
{
lock (Gate)
{
LiveHostsLocked();
Hosts.Add(new WeakReference<ShrinkEventBusHost>(host));
}
}
internal static Entry[] GetEntries(ShrinkBusKey key)
{
lock (Gate)
return Entries.FindAll(item => item.Key == key).ToArray();
}
private static ShrinkEventBusHost[] LiveHostsLocked()
{
var result = new List<ShrinkEventBusHost>(Hosts.Count);
for (var i = Hosts.Count - 1; i >= 0; i--)
{
if (Hosts[i].TryGetTarget(out var host))
result.Add(host);
else
Hosts.RemoveAt(i);
}
return result.ToArray();
}
}
public sealed class ShrinkEventBusHost : IShrinkBusResolver, IDisposable
{
private readonly ConcurrentDictionary<ShrinkBusKey, IShrinkEventBus> _buses =
new ConcurrentDictionary<ShrinkBusKey, IShrinkEventBus>();
private readonly object _staticGate = new object();
private readonly Dictionary<long, IDisposable> _staticBindings =
new Dictionary<long, IDisposable>();
private bool _disposed;
public ShrinkEventBusHost()
{
ShrinkStaticBindingRegistry.TrackHost(this);
}
public IShrinkEventBus CreateBus(ShrinkBusKey key, ShrinkBusOptions options)
{
if (_disposed)
throw new ObjectDisposedException(nameof(ShrinkEventBusHost));
var bus = new DotNetShrinkEventBus(key, options);
if (!_buses.TryAdd(key, bus))
{
bus.Dispose();
throw new InvalidOperationException($"Bus '{key}' is already registered.");
}
var entries = ShrinkStaticBindingRegistry.GetEntries(key);
for (var i = 0; i < entries.Length; i++)
TryAttachStatic(entries[i]);
return bus;
}
public IShrinkEventBus GetBus(ShrinkBusKey key)
{
if (_buses.TryGetValue(key, out var bus))
return bus;
throw new KeyNotFoundException($"Bus '{key}' is not registered.");
}
public bool TryGetBus(ShrinkBusKey key, out IShrinkEventBus bus) =>
_buses.TryGetValue(key, out bus!);
public IDisposable Attach(object target, ShrinkBusKey? defaultBus = null)
{
if (target is not IShrinkGeneratedSubscriber generated)
throw new InvalidOperationException(
$"Type {target?.GetType().FullName ?? "<null>"} has no generated event binding.");
return generated.AttachGenerated(this, defaultBus);
}
public void Dispose()
{
if (_disposed)
return;
_disposed = true;
lock (_staticGate)
{
foreach (var binding in _staticBindings.Values)
binding.Dispose();
_staticBindings.Clear();
}
foreach (var bus in _buses.Values)
bus.Dispose();
_buses.Clear();
}
internal void TryAttachStatic(ShrinkStaticBindingRegistry.Entry entry)
{
lock (_staticGate)
{
if (_disposed || _staticBindings.ContainsKey(entry.Id) || !_buses.ContainsKey(entry.Key))
return;
_staticBindings.Add(entry.Id, entry.Factory(this));
}
}
}
public static class ShrinkGeneratedBinding
{
public static IDisposable Subscribe<TEvent>(IShrinkBusResolver resolver, ShrinkBusKey? defaultBus,
string? configuredBus, object? owner, Action<TEvent> handler, ShrinkEventPriority priority,
int numericPriority, bool receiveCanceled) where TEvent : IShrinkEvent
{
return Resolve(resolver, defaultBus, configuredBus).Subscribe(
owner, handler, priority, numericPriority, receiveCanceled);
}
public static IDisposable SubscribeAsync<TEvent>(IShrinkBusResolver resolver, ShrinkBusKey? defaultBus,
string? configuredBus, object? owner, ShrinkAsyncEventHandler<TEvent> handler,
ShrinkEventPriority priority, int numericPriority, bool receiveCanceled) where TEvent : IShrinkEvent
{
return Resolve(resolver, defaultBus, configuredBus).SubscribeAsync(
owner, handler, priority, numericPriority, receiveCanceled);
}
private static DotNetShrinkEventBus Resolve(IShrinkBusResolver resolver, ShrinkBusKey? defaultBus,
string? configuredBus)
{
var key = string.IsNullOrWhiteSpace(configuredBus)
? defaultBus ?? ShrinkBusKey.Game
: ShrinkBusKey.Parse(configuredBus);
return resolver.GetBus(key) as DotNetShrinkEventBus
?? throw new InvalidOperationException("Generated bindings require the built-in bus implementation.");
}
}
internal sealed class DotNetShrinkEventBus : IShrinkEventBus
{
private readonly ConcurrentDictionary<Type, IEventChannel> _channels =
new ConcurrentDictionary<Type, IEventChannel>();
private readonly IDotNetScheduler _scheduler;
private long _nextSubscriptionId;
private bool _disposed;
public DotNetShrinkEventBus(ShrinkBusKey key, ShrinkBusOptions options)
{
Key = key;
Options = (options ?? throw new ArgumentNullException(nameof(options))).CloneValidated();
_scheduler = DotNetSchedulerFactory.Create(key.Id, Options);
}
public ShrinkBusKey Key { get; }
public ShrinkBusOptions Options { get; }
public ShrinkPostResult Post<TEvent>(in TEvent eventData) where TEvent : IShrinkEvent
{
if (_disposed)
return ShrinkPostResult.Rejected(ShrinkPostFailure.BusStopped);
var copied = eventData;
if (_scheduler.IsOnSchedulerThread)
return Channel<TEvent>().Post(copied);
return _scheduler.TryPost(() => Channel<TEvent>().Post(copied))
? new ShrinkPostResult(true, false, false)
: ShrinkPostResult.Rejected(ShrinkPostFailure.QueueFull);
}
public async ValueTask<ShrinkPostResult> PostAsync<TEvent>(TEvent eventData,
CancellationToken cancellationToken = default) where TEvent : IShrinkEvent
{
if (_disposed)
return ShrinkPostResult.Rejected(ShrinkPostFailure.BusStopped);
ShrinkPostResult result = default;
try
{
await _scheduler.PostAsync(async () =>
{
result = await Channel<TEvent>().PostAsync(eventData, Options.DispatchMode, cancellationToken)
.ConfigureAwait(false);
}, cancellationToken).ConfigureAwait(false);
return result;
}
catch (QueueFullException)
{
return ShrinkPostResult.Rejected(ShrinkPostFailure.QueueFull);
}
catch (OperationCanceledException) when (cancellationToken.IsCancellationRequested)
{
return new ShrinkPostResult(true, false, false, ShrinkPostFailure.Canceled);
}
}
public IDisposable Attach(object target)
{
if (target is not IShrinkGeneratedSubscriber generated)
throw new InvalidOperationException(
$"Type {target?.GetType().FullName ?? "<null>"} has no generated event binding.");
return generated.AttachGenerated(new SingleBusResolver(this), Key);
}
public IDisposable Subscribe<TEvent>(object? owner, Action<TEvent> handler,
ShrinkEventPriority priority, int numericPriority, bool receiveCanceled) where TEvent : IShrinkEvent
{
return Channel<TEvent>().Add(Interlocked.Increment(ref _nextSubscriptionId), owner,
handler, null, priority, numericPriority, receiveCanceled);
}
public IDisposable SubscribeAsync<TEvent>(object? owner, ShrinkAsyncEventHandler<TEvent> handler,
ShrinkEventPriority priority, int numericPriority, bool receiveCanceled) where TEvent : IShrinkEvent
{
return Channel<TEvent>().Add(Interlocked.Increment(ref _nextSubscriptionId), owner,
null, handler, priority, numericPriority, receiveCanceled);
}
public void Dispose()
{
if (_disposed)
return;
_disposed = true;
_scheduler.Dispose();
foreach (var channel in _channels.Values)
channel.Clear();
_channels.Clear();
}
private EventChannel<TEvent> Channel<TEvent>() where TEvent : IShrinkEvent =>
(EventChannel<TEvent>)_channels.GetOrAdd(typeof(TEvent), _ => new EventChannel<TEvent>());
private sealed class SingleBusResolver : IShrinkBusResolver
{
private readonly IShrinkEventBus _bus;
public SingleBusResolver(IShrinkEventBus bus) => _bus = bus;
public IShrinkEventBus GetBus(ShrinkBusKey key) => key == _bus.Key
? _bus
: throw new KeyNotFoundException($"Bus '{key}' is not available.");
public bool TryGetBus(ShrinkBusKey key, out IShrinkEventBus bus)
{
bus = _bus;
return key == _bus.Key;
}
}
}
internal interface IEventChannel
{
void Clear();
}
internal sealed class EventChannel<TEvent> : IEventChannel where TEvent : IShrinkEvent
{
private sealed class Entry
{
public long Id = 0;
public object? Owner;
public Action<TEvent>? Sync;
public ShrinkAsyncEventHandler<TEvent>? Async;
public ShrinkEventPriority Priority;
public int NumericPriority;
public bool ReceiveCanceled;
}
private readonly object _gate = new object();
private readonly List<Entry> _entries = new List<Entry>();
private Entry[] _snapshot = Array.Empty<Entry>();
public IDisposable Add(long id, object? owner, Action<TEvent>? sync,
ShrinkAsyncEventHandler<TEvent>? asyncHandler, ShrinkEventPriority priority,
int numericPriority, bool receiveCanceled)
{
lock (_gate)
{
_entries.Add(new Entry
{
Id = id,
Owner = owner,
Sync = sync,
Async = asyncHandler,
Priority = priority,
NumericPriority = numericPriority,
ReceiveCanceled = receiveCanceled
});
Rebuild();
}
return new Subscription(this, id);
}
public ShrinkPostResult Post(TEvent eventData)
{
var handled = false;
var snapshot = _snapshot;
for (var i = 0; i < snapshot.Length; i++)
{
var entry = snapshot[i];
if (IsCanceled(eventData) && !entry.ReceiveCanceled)
continue;
if (entry.Sync != null)
entry.Sync(eventData);
else if (entry.Async != null)
_ = entry.Async(eventData, CancellationToken.None).AsTask();
else
continue;
handled = true;
}
return ShrinkPostResult.Completed(handled, IsCanceled(eventData));
}
public async ValueTask<ShrinkPostResult> PostAsync(TEvent eventData, ShrinkDispatchMode mode,
CancellationToken cancellationToken)
{
var handled = false;
var snapshot = _snapshot;
if (mode == ShrinkDispatchMode.Parallel)
{
var tasks = new List<Task>();
for (var i = 0; i < snapshot.Length; i++)
{
var entry = snapshot[i];
if (IsCanceled(eventData) && !entry.ReceiveCanceled)
continue;
if (entry.Sync != null)
entry.Sync(eventData);
else if (entry.Async != null)
tasks.Add(entry.Async(eventData, cancellationToken).AsTask());
else
continue;
handled = true;
}
if (tasks.Count > 0)
await Task.WhenAll(tasks).ConfigureAwait(false);
}
else
{
for (var i = 0; i < snapshot.Length; i++)
{
cancellationToken.ThrowIfCancellationRequested();
var entry = snapshot[i];
if (IsCanceled(eventData) && !entry.ReceiveCanceled)
continue;
if (entry.Sync != null)
entry.Sync(eventData);
else if (entry.Async != null)
await entry.Async(eventData, cancellationToken).ConfigureAwait(false);
else
continue;
handled = true;
}
}
return ShrinkPostResult.Completed(handled, IsCanceled(eventData));
}
public void Clear()
{
lock (_gate)
{
_entries.Clear();
_snapshot = Array.Empty<Entry>();
}
}
private void Remove(long id)
{
lock (_gate)
{
_entries.RemoveAll(item => item.Id == id);
Rebuild();
}
}
private void Rebuild()
{
_entries.Sort((left, right) =>
{
var leftValue = left.NumericPriority == 0 ? (int)left.Priority * 1000 : -left.NumericPriority;
var rightValue = right.NumericPriority == 0 ? (int)right.Priority * 1000 : -right.NumericPriority;
var result = leftValue.CompareTo(rightValue);
return result != 0 ? result : left.Id.CompareTo(right.Id);
});
_snapshot = _entries.ToArray();
}
private static bool IsCanceled(TEvent value) =>
value is IShrinkCancelableEvent cancelable && cancelable.IsCanceled;
private sealed class Subscription : IDisposable
{
private EventChannel<TEvent>? _owner;
private readonly long _id;
public Subscription(EventChannel<TEvent> owner, long id)
{
_owner = owner;
_id = id;
}
public void Dispose() => Interlocked.Exchange(ref _owner, null)?.Remove(_id);
}
}
}
@@ -0,0 +1,309 @@
#nullable enable
using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Linq;
using System.Text;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp;
using Microsoft.CodeAnalysis.CSharp.Syntax;
using Microsoft.CodeAnalysis.Text;
namespace ShrinkEventBus.Generator
{
[Generator]
public sealed class ShrinkEventBusGenerator : IIncrementalGenerator
{
private const string SubscriberAttribute = "ShrinkEventBus.ShrinkEventSubscriberAttribute";
private const string SubscribeAttribute = "ShrinkEventBus.ShrinkSubscribeAttribute";
private static readonly DiagnosticDescriptor PartialRequired = new DiagnosticDescriptor(
"SHRINKEVENT001", "Subscriber must be partial",
"Subscriber type '{0}' must be partial so ShrinkEventBus can generate a reflection-free binding",
"ShrinkEventBus", DiagnosticSeverity.Error, true);
private static readonly DiagnosticDescriptor UnsupportedHandler = new DiagnosticDescriptor(
"SHRINKEVENT002", "Unsupported handler signature",
"Handler '{0}' must be void(TEvent), UniTask(TEvent), UniTask(TEvent, CancellationToken), or ValueTask(TEvent, CancellationToken)",
"ShrinkEventBus", DiagnosticSeverity.Error, true);
private static readonly DiagnosticDescriptor TopLevelRequired = new DiagnosticDescriptor(
"SHRINKEVENT003", "Top-level subscriber required",
"Subscriber type '{0}' must be top-level in the current generator version",
"ShrinkEventBus", DiagnosticSeverity.Error, true);
public void Initialize(IncrementalGeneratorInitializationContext context)
{
context.RegisterSourceOutput(context.CompilationProvider,
static (sourceContext, compilation) =>
{
if (compilation.GetTypeByMetadataName(
"System.Runtime.CompilerServices.ModuleInitializerAttribute") == null)
{
sourceContext.AddSource("ShrinkEventBus.ModuleInitializerAttribute.g.cs",
SourceText.From(
"namespace System.Runtime.CompilerServices { [global::System.AttributeUsage(global::System.AttributeTargets.Method, Inherited = false)] internal sealed class ModuleInitializerAttribute : global::System.Attribute { } }",
Encoding.UTF8));
}
});
var candidates = context.SyntaxProvider.ForAttributeWithMetadataName(
SubscriberAttribute,
static (node, _) => node is ClassDeclarationSyntax,
static (syntaxContext, _) => (INamedTypeSymbol)syntaxContext.TargetSymbol);
context.RegisterSourceOutput(candidates.Collect(), Generate);
}
private static void Generate(SourceProductionContext context,
ImmutableArray<INamedTypeSymbol> candidates)
{
foreach (var type in candidates)
GenerateType(context, type);
}
private static void GenerateType(SourceProductionContext context, INamedTypeSymbol type)
{
if (type.ContainingType != null)
{
context.ReportDiagnostic(Diagnostic.Create(TopLevelRequired,
type.Locations.FirstOrDefault(), type.ToDisplayString()));
return;
}
var isPartial = type.DeclaringSyntaxReferences
.Select(reference => reference.GetSyntax())
.OfType<ClassDeclarationSyntax>()
.Any(declaration => declaration.Modifiers.Any(SyntaxKind.PartialKeyword));
if (!isPartial)
{
context.ReportDiagnostic(Diagnostic.Create(PartialRequired,
type.Locations.FirstOrDefault(), type.ToDisplayString()));
return;
}
var handlers = new List<HandlerModel>();
foreach (var method in type.GetMembers().OfType<IMethodSymbol>())
{
var attribute = method.GetAttributes().FirstOrDefault(item =>
item.AttributeClass?.ToDisplayString() == SubscribeAttribute);
if (attribute == null)
continue;
if (!TryCreateHandler(method, attribute, type.IsStatic, out var handler))
{
context.ReportDiagnostic(Diagnostic.Create(UnsupportedHandler,
method.Locations.FirstOrDefault(), method.ToDisplayString()));
continue;
}
handlers.Add(handler);
}
var subscriber = type.GetAttributes().First(item =>
item.AttributeClass?.ToDisplayString() == SubscriberAttribute);
var defaultBus = ReadString(subscriber, "DefaultBus");
var source = type.IsStatic
? BuildStaticSource(type, handlers, defaultBus)
: BuildSource(type, handlers, defaultBus);
var hint = type.ToDisplayString().Replace('.', '_').Replace('+', '_') + ".ShrinkEvents.g.cs";
context.AddSource(hint, SourceText.From(source, Encoding.UTF8));
}
private static bool TryCreateHandler(IMethodSymbol method, AttributeData attribute,
bool staticSubscriber,
out HandlerModel model)
{
model = default;
if (method.IsStatic != staticSubscriber || method.Parameters.Length == 0 || method.Parameters.Length > 2)
return false;
var eventType = method.Parameters[0].Type.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat);
var returnType = method.ReturnType.ToDisplayString();
var isSync = method.ReturnsVoid && method.Parameters.Length == 1;
var isValueTask = returnType == "System.Threading.Tasks.ValueTask" &&
method.Parameters.Length == 2 &&
method.Parameters[1].Type.ToDisplayString() == "System.Threading.CancellationToken";
var isUniTask = returnType == "Cysharp.Threading.Tasks.UniTask";
var isUniTaskLegacy = isUniTask && method.Parameters.Length == 1;
var isUniTaskCancelable = isUniTask && method.Parameters.Length == 2 &&
method.Parameters[1].Type.ToDisplayString() == "System.Threading.CancellationToken";
if (!isSync && !isValueTask && !isUniTaskLegacy && !isUniTaskCancelable)
return false;
var bindingMethod = isSync
? "Subscribe"
: isUniTaskLegacy ? "SubscribeAsyncLegacy" : "SubscribeAsync";
model = new HandlerModel(
method.Name,
eventType,
bindingMethod,
ReadString(attribute, "Bus"),
ReadInt(attribute, "Priority", 2),
ReadInt(attribute, "NumericPriority", 0),
ReadBool(attribute, "ReceiveCanceled", false));
return true;
}
private static string BuildSource(INamedTypeSymbol type, IReadOnlyList<HandlerModel> handlers,
string defaultBus)
{
var builder = new StringBuilder();
builder.AppendLine("// <auto-generated />");
builder.AppendLine("#nullable enable");
if (!type.ContainingNamespace.IsGlobalNamespace)
{
builder.Append("namespace ").Append(type.ContainingNamespace.ToDisplayString()).AppendLine();
builder.AppendLine("{");
}
var accessibility = type.DeclaredAccessibility switch
{
Accessibility.Public => "public ",
Accessibility.Internal => "internal ",
_ => string.Empty
};
builder.Append(" ").Append(accessibility).Append("partial class ")
.Append(type.Name).AppendLine(" : global::ShrinkEventBus.IShrinkGeneratedSubscriber");
builder.AppendLine(" {");
builder.AppendLine(" global::System.IDisposable global::ShrinkEventBus.IShrinkGeneratedSubscriber.AttachGenerated(");
builder.AppendLine(" global::ShrinkEventBus.IShrinkBusResolver resolver,");
builder.AppendLine(" global::ShrinkEventBus.ShrinkBusKey? defaultBus)");
builder.AppendLine(" {");
builder.AppendLine(" var binding = new global::ShrinkEventBus.ShrinkEventBinding();");
foreach (var handler in handlers)
{
var bus = string.IsNullOrWhiteSpace(handler.Bus) ? defaultBus : handler.Bus;
builder.Append(" binding.Add(global::ShrinkEventBus.ShrinkGeneratedBinding.")
.Append(handler.BindingMethod).Append('<').Append(handler.EventType).AppendLine(">(");
builder.Append(" resolver, defaultBus, ")
.Append(ToLiteral(bus)).AppendLine(", this,");
builder.Append(" this.").Append(handler.MethodName)
.Append(", (global::ShrinkEventBus.ShrinkEventPriority)")
.Append(handler.Priority).Append(", ").Append(handler.NumericPriority).Append(", ")
.Append(handler.ReceiveCanceled ? "true" : "false").AppendLine("));");
}
builder.AppendLine(" return binding;");
builder.AppendLine(" }");
builder.AppendLine(" }");
if (!type.ContainingNamespace.IsGlobalNamespace)
builder.AppendLine("}");
return builder.ToString();
}
private static string BuildStaticSource(INamedTypeSymbol type,
IReadOnlyList<HandlerModel> handlers, string defaultBus)
{
var builder = new StringBuilder();
builder.AppendLine("// <auto-generated />");
builder.AppendLine("#nullable enable");
if (!type.ContainingNamespace.IsGlobalNamespace)
{
builder.Append("namespace ").Append(type.ContainingNamespace.ToDisplayString()).AppendLine();
builder.AppendLine("{");
}
var accessibility = type.DeclaredAccessibility switch
{
Accessibility.Public => "public ",
Accessibility.Internal => "internal ",
_ => string.Empty
};
builder.Append(" ").Append(accessibility).Append("static partial class ")
.Append(type.Name).AppendLine();
builder.AppendLine(" {");
builder.AppendLine(" [global::System.Runtime.CompilerServices.ModuleInitializer]");
builder.AppendLine(" internal static void ShrinkEventBus_RegisterStaticBindings()");
builder.AppendLine(" {");
for (var i = 0; i < handlers.Count; i++)
{
var bus = string.IsNullOrWhiteSpace(handlers[i].Bus) ? defaultBus : handlers[i].Bus;
builder.Append(" global::ShrinkEventBus.ShrinkStaticBindingRegistry.Register(")
.Append("global::ShrinkEventBus.ShrinkBusKey.Parse(")
.Append(ToLiteral(bus)).Append("), ShrinkEventBus_Bind_").Append(i).AppendLine(");");
}
builder.AppendLine(" }");
for (var i = 0; i < handlers.Count; i++)
{
var handler = handlers[i];
var bus = string.IsNullOrWhiteSpace(handler.Bus) ? defaultBus : handler.Bus;
builder.Append(" private static global::System.IDisposable ShrinkEventBus_Bind_")
.Append(i).AppendLine("(global::ShrinkEventBus.IShrinkBusResolver resolver)");
builder.AppendLine(" {");
builder.Append(" return global::ShrinkEventBus.ShrinkGeneratedBinding.")
.Append(handler.BindingMethod).Append('<').Append(handler.EventType).AppendLine(">(");
builder.Append(" resolver, null, ").Append(ToLiteral(bus))
.AppendLine(", null,");
builder.Append(" ").Append(handler.MethodName)
.Append(", (global::ShrinkEventBus.ShrinkEventPriority)")
.Append(handler.Priority).Append(", ").Append(handler.NumericPriority).Append(", ")
.Append(handler.ReceiveCanceled ? "true" : "false").AppendLine(");");
builder.AppendLine(" }");
}
builder.AppendLine(" }");
if (!type.ContainingNamespace.IsGlobalNamespace)
builder.AppendLine("}");
return builder.ToString();
}
private static string ReadString(AttributeData attribute, string name)
{
foreach (var pair in attribute.NamedArguments)
{
if (pair.Key == name)
return pair.Value.Value as string ?? string.Empty;
}
return string.Empty;
}
private static int ReadInt(AttributeData attribute, string name, int defaultValue)
{
foreach (var pair in attribute.NamedArguments)
{
if (pair.Key == name && pair.Value.Value != null)
return Convert.ToInt32(pair.Value.Value);
}
return defaultValue;
}
private static bool ReadBool(AttributeData attribute, string name, bool defaultValue)
{
foreach (var pair in attribute.NamedArguments)
{
if (pair.Key == name && pair.Value.Value is bool value)
return value;
}
return defaultValue;
}
private static string ToLiteral(string value) => SymbolDisplay.FormatLiteral(value ?? string.Empty, true);
private readonly struct HandlerModel
{
public HandlerModel(string methodName, string eventType, string bindingMethod, string bus,
int priority, int numericPriority, bool receiveCanceled)
{
MethodName = methodName;
EventType = eventType;
BindingMethod = bindingMethod;
Bus = bus;
Priority = priority;
NumericPriority = numericPriority;
ReceiveCanceled = receiveCanceled;
}
public string MethodName { get; }
public string EventType { get; }
public string BindingMethod { get; }
public string Bus { get; }
public int Priority { get; }
public int NumericPriority { get; }
public bool ReceiveCanceled { get; }
}
}
}
@@ -0,0 +1,62 @@
using System;
using System.Threading;
using System.Threading.Tasks;
using ShrinkEventBus;
internal readonly struct PingEvent : IShrinkEvent
{
public PingEvent(int value) => Value = value;
public int Value { get; }
}
[ShrinkEventSubscriber]
internal sealed partial class SmokeSubscriber
{
public int Sum { get; private set; }
public int ThreadId { get; private set; }
[ShrinkSubscribe]
private void OnPing(PingEvent evt)
{
Sum += evt.Value;
ThreadId = Thread.CurrentThread.ManagedThreadId;
}
}
[ShrinkEventSubscriber(DefaultBus = "game")]
internal static partial class StaticSmokeSubscriber
{
public static int Sum { get; set; }
[ShrinkSubscribe]
private static void OnPing(PingEvent evt) => Sum += evt.Value;
}
internal static class Program
{
private static async Task<int> Main()
{
using var host = new ShrinkEventBusHost();
var game = host.CreateBus(ShrinkBusKey.Game, ShrinkBusOptions.Inline());
var workerKey = new ShrinkBusKey("worker", "smoke");
var worker = host.CreateBus(workerKey, ShrinkBusOptions.DedicatedThread());
var gameSubscriber = new SmokeSubscriber();
var workerSubscriber = new SmokeSubscriber();
var callerThread = Thread.CurrentThread.ManagedThreadId;
using var gameBinding = host.Attach(gameSubscriber, ShrinkBusKey.Game);
using var workerBinding = host.Attach(workerSubscriber, workerKey);
game.Post(new PingEvent(2));
await worker.PostAsync(new PingEvent(3));
if (gameSubscriber.Sum != 2 || workerSubscriber.Sum != 3 || StaticSmokeSubscriber.Sum != 2 ||
workerSubscriber.ThreadId == callerThread)
{
Console.Error.WriteLine("FAIL");
return 1;
}
Console.WriteLine("PASS");
return 0;
}
}
BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 91 KiB

+169
View File
@@ -0,0 +1,169 @@
fileFormatVersion: 2
guid: c0d34cdfe6432fc45b8e3aa75065b201
TextureImporter:
internalIDToNameTable:
- first:
213: -6576374159750988736
second: img1_0
externalObjects: {}
serializedVersion: 13
mipmaps:
mipMapMode: 0
enableMipMap: 0
sRGBTexture: 1
linearTexture: 0
fadeOut: 0
borderMipMap: 0
mipMapsPreserveCoverage: 0
alphaTestReferenceValue: 0.5
mipMapFadeDistanceStart: 1
mipMapFadeDistanceEnd: 3
bumpmap:
convertToNormalMap: 0
externalNormalMap: 0
heightScale: 0.25
normalMapFilter: 0
flipGreenChannel: 0
isReadable: 0
streamingMipmaps: 0
streamingMipmapsPriority: 0
vTOnly: 0
ignoreMipmapLimit: 0
grayScaleToAlpha: 0
generateCubemap: 6
cubemapConvolution: 0
seamlessCubemap: 0
textureFormat: 1
maxTextureSize: 2048
textureSettings:
serializedVersion: 2
filterMode: 1
aniso: 1
mipBias: 0
wrapU: 1
wrapV: 1
wrapW: 1
nPOTScale: 0
lightmap: 0
compressionQuality: 50
spriteMode: 2
spriteExtrude: 1
spriteMeshType: 1
alignment: 0
spritePivot: {x: 0.5, y: 0.5}
spritePixelsToUnits: 100
spriteBorder: {x: 0, y: 0, z: 0, w: 0}
spriteGenerateFallbackPhysicsShape: 1
alphaUsage: 1
alphaIsTransparency: 1
spriteTessellationDetail: -1
textureType: 8
textureShape: 1
singleChannelComponent: 0
flipbookRows: 1
flipbookColumns: 1
maxTextureSizeSet: 0
compressionQualitySet: 0
textureFormatSet: 0
ignorePngGamma: 0
applyGammaDecoding: 0
swizzle: 50462976
cookieLightType: 0
platformSettings:
- serializedVersion: 4
buildTarget: DefaultTexturePlatform
maxTextureSize: 2048
resizeAlgorithm: 0
textureFormat: -1
textureCompression: 1
compressionQuality: 50
crunchedCompression: 0
allowsAlphaSplitting: 0
overridden: 0
ignorePlatformSupport: 0
androidETC2FallbackOverride: 0
forceMaximumCompressionQuality_BC6H_BC7: 0
- serializedVersion: 4
buildTarget: Standalone
maxTextureSize: 2048
resizeAlgorithm: 0
textureFormat: -1
textureCompression: 1
compressionQuality: 50
crunchedCompression: 0
allowsAlphaSplitting: 0
overridden: 0
ignorePlatformSupport: 0
androidETC2FallbackOverride: 0
forceMaximumCompressionQuality_BC6H_BC7: 0
- serializedVersion: 4
buildTarget: Android
maxTextureSize: 2048
resizeAlgorithm: 0
textureFormat: -1
textureCompression: 1
compressionQuality: 50
crunchedCompression: 0
allowsAlphaSplitting: 0
overridden: 0
ignorePlatformSupport: 0
androidETC2FallbackOverride: 0
forceMaximumCompressionQuality_BC6H_BC7: 0
- serializedVersion: 4
buildTarget: WebGL
maxTextureSize: 2048
resizeAlgorithm: 0
textureFormat: -1
textureCompression: 1
compressionQuality: 50
crunchedCompression: 0
allowsAlphaSplitting: 0
overridden: 0
ignorePlatformSupport: 0
androidETC2FallbackOverride: 0
forceMaximumCompressionQuality_BC6H_BC7: 0
spriteSheet:
serializedVersion: 2
sprites:
- serializedVersion: 2
name: img1_0
rect:
serializedVersion: 2
x: 0
y: 0
width: 659
height: 658
alignment: 0
pivot: {x: 0, y: 0}
border: {x: 0, y: 0, z: 0, w: 0}
customData:
outline: []
physicsShape: []
tessellationDetail: -1
bones: []
spriteID: 04cfbf97b860cb4a0800000000000000
internalID: -6576374159750988736
vertices: []
indices:
edges: []
weights: []
outline: []
customData:
physicsShape: []
bones: []
spriteID:
internalID: 0
vertices: []
indices:
edges: []
weights: []
secondaryTextures: []
spriteCustomMetadata:
entries: []
nameFileIdTable:
img1_0: -6576374159750988736
mipmapLimitGroupName:
pSDRemoveMatte: 0
userData:
assetBundleName:
assetBundleVariant:
BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 73 KiB

+169
View File
@@ -0,0 +1,169 @@
fileFormatVersion: 2
guid: 265b481fbf28c6949a2e93bcfcdbe946
TextureImporter:
internalIDToNameTable:
- first:
213: -1076633883802439962
second: img2_0
externalObjects: {}
serializedVersion: 13
mipmaps:
mipMapMode: 0
enableMipMap: 0
sRGBTexture: 1
linearTexture: 0
fadeOut: 0
borderMipMap: 0
mipMapsPreserveCoverage: 0
alphaTestReferenceValue: 0.5
mipMapFadeDistanceStart: 1
mipMapFadeDistanceEnd: 3
bumpmap:
convertToNormalMap: 0
externalNormalMap: 0
heightScale: 0.25
normalMapFilter: 0
flipGreenChannel: 0
isReadable: 0
streamingMipmaps: 0
streamingMipmapsPriority: 0
vTOnly: 0
ignoreMipmapLimit: 0
grayScaleToAlpha: 0
generateCubemap: 6
cubemapConvolution: 0
seamlessCubemap: 0
textureFormat: 1
maxTextureSize: 2048
textureSettings:
serializedVersion: 2
filterMode: 1
aniso: 1
mipBias: 0
wrapU: 1
wrapV: 1
wrapW: 1
nPOTScale: 0
lightmap: 0
compressionQuality: 50
spriteMode: 2
spriteExtrude: 1
spriteMeshType: 1
alignment: 0
spritePivot: {x: 0.5, y: 0.5}
spritePixelsToUnits: 100
spriteBorder: {x: 0, y: 0, z: 0, w: 0}
spriteGenerateFallbackPhysicsShape: 1
alphaUsage: 1
alphaIsTransparency: 1
spriteTessellationDetail: -1
textureType: 8
textureShape: 1
singleChannelComponent: 0
flipbookRows: 1
flipbookColumns: 1
maxTextureSizeSet: 0
compressionQualitySet: 0
textureFormatSet: 0
ignorePngGamma: 0
applyGammaDecoding: 0
swizzle: 50462976
cookieLightType: 0
platformSettings:
- serializedVersion: 4
buildTarget: DefaultTexturePlatform
maxTextureSize: 2048
resizeAlgorithm: 0
textureFormat: -1
textureCompression: 1
compressionQuality: 50
crunchedCompression: 0
allowsAlphaSplitting: 0
overridden: 0
ignorePlatformSupport: 0
androidETC2FallbackOverride: 0
forceMaximumCompressionQuality_BC6H_BC7: 0
- serializedVersion: 4
buildTarget: Standalone
maxTextureSize: 2048
resizeAlgorithm: 0
textureFormat: -1
textureCompression: 1
compressionQuality: 50
crunchedCompression: 0
allowsAlphaSplitting: 0
overridden: 0
ignorePlatformSupport: 0
androidETC2FallbackOverride: 0
forceMaximumCompressionQuality_BC6H_BC7: 0
- serializedVersion: 4
buildTarget: Android
maxTextureSize: 2048
resizeAlgorithm: 0
textureFormat: -1
textureCompression: 1
compressionQuality: 50
crunchedCompression: 0
allowsAlphaSplitting: 0
overridden: 0
ignorePlatformSupport: 0
androidETC2FallbackOverride: 0
forceMaximumCompressionQuality_BC6H_BC7: 0
- serializedVersion: 4
buildTarget: WebGL
maxTextureSize: 2048
resizeAlgorithm: 0
textureFormat: -1
textureCompression: 1
compressionQuality: 50
crunchedCompression: 0
allowsAlphaSplitting: 0
overridden: 0
ignorePlatformSupport: 0
androidETC2FallbackOverride: 0
forceMaximumCompressionQuality_BC6H_BC7: 0
spriteSheet:
serializedVersion: 2
sprites:
- serializedVersion: 2
name: img2_0
rect:
serializedVersion: 2
x: 0
y: 0
width: 659
height: 658
alignment: 0
pivot: {x: 0, y: 0}
border: {x: 0, y: 0, z: 0, w: 0}
customData:
outline: []
physicsShape: []
tessellationDetail: -1
bones: []
spriteID: 6ee6ecadf270f01f0800000000000000
internalID: -1076633883802439962
vertices: []
indices:
edges: []
weights: []
outline: []
customData:
physicsShape: []
bones: []
spriteID:
internalID: 0
vertices: []
indices:
edges: []
weights: []
secondaryTextures: []
spriteCustomMetadata:
entries: []
nameFileIdTable:
img2_0: -1076633883802439962
mipmapLimitGroupName:
pSDRemoveMatte: 0
userData:
assetBundleName:
assetBundleVariant:
+24
View File
@@ -0,0 +1,24 @@
{
"name": "com.cneicy.shrink-eventbus",
"version": "2.0.0",
"displayName": "ShrinkEventBus",
"description": "多 Bus、特性强类型注册、UniTask 调度与低分配发布的 Unity 事件总线。",
"unity": "2022.3",
"documentationUrl": "https://git.crash.work/ShrinkSDK/ShrinkEventBus",
"changelogUrl": "https://git.crash.work/ShrinkSDK/ShrinkEventBus/src/branch/main/CHANGELOG.md",
"licensesUrl": "https://git.crash.work/ShrinkSDK/ShrinkEventBus/src/branch/main/LICENSE",
"dependencies": {
"com.cysharp.unitask": "2.5.10",
"com.unity.nuget.mono-cecil": "1.11.4"
},
"keywords": [
"eventbus",
"event",
"messaging",
"unitask"
],
"author": {
"name": "cneicy",
"url": "https://git.crash.work/ShrinkSDK"
}
}
+3
View File
@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 09a3fe335f19475db0839afd05b0d8cb
timeCreated: 1772900717