From 1c921f5aec261ac22d462657f8146a5a5582c70a Mon Sep 17 00:00:00 2001 From: cneicy Date: Wed, 26 Aug 2026 02:50:18 +0800 Subject: [PATCH] chore: initialize standalone UPM package --- .gitea/workflows/publish.yml | 49 + .gitea/workflows/unity-verify.yml | 39 + .gitignore | 109 +++ .npmignore | 8 + Benchmark-2.0.txt | 72 ++ Benchmark-2.0.txt.meta | 7 + Benchmark.meta | 8 + Benchmark.txt | 83 ++ Benchmark.txt.meta | 7 + Benchmark/ShrinkEventBus.Benchmark.asmdef | 13 + .../ShrinkEventBus.Benchmark.asmdef.meta | 7 + Benchmark/ShrinkEventBusBenchmark.cs | 282 ++++++ Benchmark/ShrinkEventBusBenchmark.cs.meta | 11 + CHANGELOG.md | 78 ++ CHANGELOG.md.meta | 7 + CodeGen.meta | 8 + CodeGen/Editor.meta | 8 + CodeGen/Editor/EventBusILPostProcessor.cs | 529 ++++++++++ .../Editor/EventBusILPostProcessor.cs.meta | 11 + .../Editor/PostProcessorAssemblyResolver.cs | 107 +++ .../PostProcessorAssemblyResolver.cs.meta | 3 + .../Editor/PostProcessorReflectionImporter.cs | 37 + .../PostProcessorReflectionImporter.cs.meta | 3 + .../Unity.ShrinkEventBus.CodeGen.asmdef | 23 + .../Unity.ShrinkEventBus.CodeGen.asmdef.meta | 7 + Development~/UnityProject/.gitignore | 6 + Development~/UnityProject/Assets/.gitkeep | 0 .../UnityProject/Packages/manifest.json | 15 + .../ProjectSettings/ProjectVersion.txt | 2 + Editor.meta | 3 + Editor/EventBusViewerWindow.cs | 906 ++++++++++++++++++ Editor/EventBusViewerWindow.cs.meta | 3 + Editor/ShrinkEventBus.Editor.asmdef | 13 + Editor/ShrinkEventBus.Editor.asmdef.meta | 3 + LICENSE | 21 + LICENSE.meta | 7 + README.md | 171 ++++ README.md.meta | 3 + Runtime.meta | 3 + Runtime/AssemblyInfo.cs | 5 + Runtime/AssemblyInfo.cs.meta | 11 + Runtime/EventBus.cs | 196 ++++ Runtime/EventBus.cs.meta | 3 + Runtime/EventResult.cs | 9 + Runtime/EventResult.cs.meta | 3 + Runtime/IShrinkEventBus.cs | 23 + Runtime/IShrinkEventBus.cs.meta | 11 + Runtime/ShrinkBusSchedulers.cs | 546 +++++++++++ Runtime/ShrinkBusSchedulers.cs.meta | 11 + Runtime/ShrinkEventAttributes.cs | 47 + Runtime/ShrinkEventAttributes.cs.meta | 11 + Runtime/ShrinkEventBindings.cs | 293 ++++++ Runtime/ShrinkEventBindings.cs.meta | 11 + Runtime/ShrinkEventBus.Runtime.asmdef | 12 + Runtime/ShrinkEventBus.Runtime.asmdef.meta | 7 + Runtime/ShrinkEventBusBuilder.cs | 36 + Runtime/ShrinkEventBusBuilder.cs.meta | 11 + Runtime/ShrinkEventBusCore.cs | 272 ++++++ Runtime/ShrinkEventBusCore.cs.meta | 11 + Runtime/ShrinkEventBusInstance.cs | 392 ++++++++ Runtime/ShrinkEventBusInstance.cs.meta | 11 + Runtime/ShrinkEventContracts.cs | 213 ++++ Runtime/ShrinkEventContracts.cs.meta | 11 + Runtime/ShrinkMonoEventScope.cs | 71 ++ Runtime/ShrinkMonoEventScope.cs.meta | 11 + Tests.PlayMode.meta | 8 + .../ShrinkEventBus.PlayMode.Tests.asmdef | 15 + .../ShrinkEventBus.PlayMode.Tests.asmdef.meta | 7 + .../ShrinkMonoEventScopePlayModeTests.cs | 48 + .../ShrinkMonoEventScopePlayModeTests.cs.meta | 11 + Tests.meta | 8 + Tests/ShrinkEventBus.Tests.asmdef | 23 + Tests/ShrinkEventBus.Tests.asmdef.meta | 7 + Tests/ShrinkEventBusV2Tests.cs | 440 +++++++++ Tests/ShrinkEventBusV2Tests.cs.meta | 11 + Tools~/DotNet/README.md | 11 + .../DotNet/ShrinkEventBus.Core/Schedulers.cs | 205 ++++ .../ShrinkEventBus.Core/ShrinkEventBus.cs | 494 ++++++++++ .../ShrinkEventBusGenerator.cs | 309 ++++++ Tools~/DotNet/ShrinkEventBus.Smoke/Program.cs | 62 ++ img1.png | Bin 0 -> 92955 bytes img1.png.meta | 169 ++++ img2.png | Bin 0 -> 75197 bytes img2.png.meta | 169 ++++ package.json | 24 + package.json.meta | 3 + 86 files changed, 6934 insertions(+) create mode 100644 .gitea/workflows/publish.yml create mode 100644 .gitea/workflows/unity-verify.yml create mode 100644 .gitignore create mode 100644 .npmignore create mode 100644 Benchmark-2.0.txt create mode 100644 Benchmark-2.0.txt.meta create mode 100644 Benchmark.meta create mode 100644 Benchmark.txt create mode 100644 Benchmark.txt.meta create mode 100644 Benchmark/ShrinkEventBus.Benchmark.asmdef create mode 100644 Benchmark/ShrinkEventBus.Benchmark.asmdef.meta create mode 100644 Benchmark/ShrinkEventBusBenchmark.cs create mode 100644 Benchmark/ShrinkEventBusBenchmark.cs.meta create mode 100644 CHANGELOG.md create mode 100644 CHANGELOG.md.meta create mode 100644 CodeGen.meta create mode 100644 CodeGen/Editor.meta create mode 100644 CodeGen/Editor/EventBusILPostProcessor.cs create mode 100644 CodeGen/Editor/EventBusILPostProcessor.cs.meta create mode 100644 CodeGen/Editor/PostProcessorAssemblyResolver.cs create mode 100644 CodeGen/Editor/PostProcessorAssemblyResolver.cs.meta create mode 100644 CodeGen/Editor/PostProcessorReflectionImporter.cs create mode 100644 CodeGen/Editor/PostProcessorReflectionImporter.cs.meta create mode 100644 CodeGen/Editor/Unity.ShrinkEventBus.CodeGen.asmdef create mode 100644 CodeGen/Editor/Unity.ShrinkEventBus.CodeGen.asmdef.meta create mode 100644 Development~/UnityProject/.gitignore create mode 100644 Development~/UnityProject/Assets/.gitkeep create mode 100644 Development~/UnityProject/Packages/manifest.json create mode 100644 Development~/UnityProject/ProjectSettings/ProjectVersion.txt create mode 100644 Editor.meta create mode 100644 Editor/EventBusViewerWindow.cs create mode 100644 Editor/EventBusViewerWindow.cs.meta create mode 100644 Editor/ShrinkEventBus.Editor.asmdef create mode 100644 Editor/ShrinkEventBus.Editor.asmdef.meta create mode 100644 LICENSE create mode 100644 LICENSE.meta create mode 100644 README.md create mode 100644 README.md.meta create mode 100644 Runtime.meta create mode 100644 Runtime/AssemblyInfo.cs create mode 100644 Runtime/AssemblyInfo.cs.meta create mode 100644 Runtime/EventBus.cs create mode 100644 Runtime/EventBus.cs.meta create mode 100644 Runtime/EventResult.cs create mode 100644 Runtime/EventResult.cs.meta create mode 100644 Runtime/IShrinkEventBus.cs create mode 100644 Runtime/IShrinkEventBus.cs.meta create mode 100644 Runtime/ShrinkBusSchedulers.cs create mode 100644 Runtime/ShrinkBusSchedulers.cs.meta create mode 100644 Runtime/ShrinkEventAttributes.cs create mode 100644 Runtime/ShrinkEventAttributes.cs.meta create mode 100644 Runtime/ShrinkEventBindings.cs create mode 100644 Runtime/ShrinkEventBindings.cs.meta create mode 100644 Runtime/ShrinkEventBus.Runtime.asmdef create mode 100644 Runtime/ShrinkEventBus.Runtime.asmdef.meta create mode 100644 Runtime/ShrinkEventBusBuilder.cs create mode 100644 Runtime/ShrinkEventBusBuilder.cs.meta create mode 100644 Runtime/ShrinkEventBusCore.cs create mode 100644 Runtime/ShrinkEventBusCore.cs.meta create mode 100644 Runtime/ShrinkEventBusInstance.cs create mode 100644 Runtime/ShrinkEventBusInstance.cs.meta create mode 100644 Runtime/ShrinkEventContracts.cs create mode 100644 Runtime/ShrinkEventContracts.cs.meta create mode 100644 Runtime/ShrinkMonoEventScope.cs create mode 100644 Runtime/ShrinkMonoEventScope.cs.meta create mode 100644 Tests.PlayMode.meta create mode 100644 Tests.PlayMode/ShrinkEventBus.PlayMode.Tests.asmdef create mode 100644 Tests.PlayMode/ShrinkEventBus.PlayMode.Tests.asmdef.meta create mode 100644 Tests.PlayMode/ShrinkMonoEventScopePlayModeTests.cs create mode 100644 Tests.PlayMode/ShrinkMonoEventScopePlayModeTests.cs.meta create mode 100644 Tests.meta create mode 100644 Tests/ShrinkEventBus.Tests.asmdef create mode 100644 Tests/ShrinkEventBus.Tests.asmdef.meta create mode 100644 Tests/ShrinkEventBusV2Tests.cs create mode 100644 Tests/ShrinkEventBusV2Tests.cs.meta create mode 100644 Tools~/DotNet/README.md create mode 100644 Tools~/DotNet/ShrinkEventBus.Core/Schedulers.cs create mode 100644 Tools~/DotNet/ShrinkEventBus.Core/ShrinkEventBus.cs create mode 100644 Tools~/DotNet/ShrinkEventBus.Generator/ShrinkEventBusGenerator.cs create mode 100644 Tools~/DotNet/ShrinkEventBus.Smoke/Program.cs create mode 100644 img1.png create mode 100644 img1.png.meta create mode 100644 img2.png create mode 100644 img2.png.meta create mode 100644 package.json create mode 100644 package.json.meta diff --git a/.gitea/workflows/publish.yml b/.gitea/workflows/publish.yml new file mode 100644 index 0000000..6c70bcf --- /dev/null +++ b/.gitea/workflows/publish.yml @@ -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/ \ No newline at end of file diff --git a/.gitea/workflows/unity-verify.yml b/.gitea/workflows/unity-verify.yml new file mode 100644 index 0000000..cac655a --- /dev/null +++ b/.gitea/workflows/unity-verify.yml @@ -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" \ No newline at end of file diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..e907c27 --- /dev/null +++ b/.gitignore @@ -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 diff --git a/.npmignore b/.npmignore new file mode 100644 index 0000000..700f03d --- /dev/null +++ b/.npmignore @@ -0,0 +1,8 @@ +.git/ +.gitea/ +Development~/ +Tools~/ +*.csproj +*.sln +*.user +*.DotSettings.user diff --git a/Benchmark-2.0.txt b/Benchmark-2.0.txt new file mode 100644 index 0000000..78cc2c7 --- /dev/null +++ b/Benchmark-2.0.txt @@ -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. diff --git a/Benchmark-2.0.txt.meta b/Benchmark-2.0.txt.meta new file mode 100644 index 0000000..7a514e0 --- /dev/null +++ b/Benchmark-2.0.txt.meta @@ -0,0 +1,7 @@ +fileFormatVersion: 2 +guid: 6e379caaacb945699a297286798bc4e2 +TextScriptImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Benchmark.meta b/Benchmark.meta new file mode 100644 index 0000000..631f9ea --- /dev/null +++ b/Benchmark.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 2cc73e57d6ec4457a425843f734b6a70 +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Benchmark.txt b/Benchmark.txt new file mode 100644 index 0000000..fb2a369 --- /dev/null +++ b/Benchmark.txt @@ -0,0 +1,83 @@ +╔══════════════════════════════════════════════════╗ +║ ShrinkEventBus Benchmark Report ║ +║ iter= 1000000 mass= 10000 reg= 5000 scan=1000 ║ +╚══════════════════════════════════════════════════╝ + 基准对象:独立实例 bus(AllowPerPhaseDispatch 已开启) + 采集于 1.3.0(EventBase 瘦身后;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 不再每实例分配内置 ListenerList,new() 路径 GC 基本归零, + 对象池从“高频必需”降级为可选优化。) diff --git a/Benchmark.txt.meta b/Benchmark.txt.meta new file mode 100644 index 0000000..ee8b618 --- /dev/null +++ b/Benchmark.txt.meta @@ -0,0 +1,7 @@ +fileFormatVersion: 2 +guid: 87ccd659dba774447b9d6941761b8300 +TextScriptImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Benchmark/ShrinkEventBus.Benchmark.asmdef b/Benchmark/ShrinkEventBus.Benchmark.asmdef new file mode 100644 index 0000000..aacb28f --- /dev/null +++ b/Benchmark/ShrinkEventBus.Benchmark.asmdef @@ -0,0 +1,13 @@ +{ + "name": "ShrinkEventBus.Benchmark", + "rootNamespace": "ShrinkEventBus.Benchmark", + "references": [ + "ShrinkEventBus.Runtime", + "UniTask" + ], + "includePlatforms": [], + "excludePlatforms": [], + "allowUnsafeCode": false, + "overrideReferences": false, + "autoReferenced": true +} diff --git a/Benchmark/ShrinkEventBus.Benchmark.asmdef.meta b/Benchmark/ShrinkEventBus.Benchmark.asmdef.meta new file mode 100644 index 0000000..2afe56e --- /dev/null +++ b/Benchmark/ShrinkEventBus.Benchmark.asmdef.meta @@ -0,0 +1,7 @@ +fileFormatVersion: 2 +guid: 4488cfe64b374f30b8081ce2f10b1ed5 +AssemblyDefinitionImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Benchmark/ShrinkEventBusBenchmark.cs b/Benchmark/ShrinkEventBusBenchmark.cs new file mode 100644 index 0000000..8bf96e0 --- /dev/null +++ b/Benchmark/ShrinkEventBusBenchmark.cs @@ -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(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> 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); + } + } +} diff --git a/Benchmark/ShrinkEventBusBenchmark.cs.meta b/Benchmark/ShrinkEventBusBenchmark.cs.meta new file mode 100644 index 0000000..3cdcbae --- /dev/null +++ b/Benchmark/ShrinkEventBusBenchmark.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 76b9239259544977bd27221d3607aed1 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..0259ef2 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,78 @@ +# Changelog + +本文件记录 `ShrinkEventBus` 在当前工作区中的包内变更。 + +## [2.0.0] - 2026-08-23 + +### Added + +- 新增统一 `IShrinkEvent` 契约、`ShrinkEventSubscriber` / `ShrinkSubscribe` 特性与 `Post` / `PostAsync` 入口。 +- 新增命名多 Bus:Game、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`。 + +## [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`(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 同步更新新的调试入口与同步/异步派发语义说明。 diff --git a/CHANGELOG.md.meta b/CHANGELOG.md.meta new file mode 100644 index 0000000..1467115 --- /dev/null +++ b/CHANGELOG.md.meta @@ -0,0 +1,7 @@ +fileFormatVersion: 2 +guid: 9b2928da4a2426743bd762f5ebb9c232 +TextScriptImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/CodeGen.meta b/CodeGen.meta new file mode 100644 index 0000000..f041337 --- /dev/null +++ b/CodeGen.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: c2bfff1573c10da47ae73a47b8841279 +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/CodeGen/Editor.meta b/CodeGen/Editor.meta new file mode 100644 index 0000000..cada1a3 --- /dev/null +++ b/CodeGen/Editor.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: c297548ebc9165f448111a35488d368e +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/CodeGen/Editor/EventBusILPostProcessor.cs b/CodeGen/Editor/EventBusILPostProcessor.cs new file mode 100644 index 0000000..f139d7d --- /dev/null +++ b/CodeGen/Editor/EventBusILPostProcessor.cs @@ -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(); + 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 GetAllTypes(IEnumerable 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 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 == ""); + 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 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); + } + } +} diff --git a/CodeGen/Editor/EventBusILPostProcessor.cs.meta b/CodeGen/Editor/EventBusILPostProcessor.cs.meta new file mode 100644 index 0000000..6f56fd9 --- /dev/null +++ b/CodeGen/Editor/EventBusILPostProcessor.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 5f611b284dae01a46bc990b87b8bca90 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/CodeGen/Editor/PostProcessorAssemblyResolver.cs b/CodeGen/Editor/PostProcessorAssemblyResolver.cs new file mode 100644 index 0000000..1ef8de1 --- /dev/null +++ b/CodeGen/Editor/PostProcessorAssemblyResolver.cs @@ -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 _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(); + 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() + { + } + } +} diff --git a/CodeGen/Editor/PostProcessorAssemblyResolver.cs.meta b/CodeGen/Editor/PostProcessorAssemblyResolver.cs.meta new file mode 100644 index 0000000..5aedb8f --- /dev/null +++ b/CodeGen/Editor/PostProcessorAssemblyResolver.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: aeea50f82bab4ee2833c671d801c95e3 +timeCreated: 1772908894 diff --git a/CodeGen/Editor/PostProcessorReflectionImporter.cs b/CodeGen/Editor/PostProcessorReflectionImporter.cs new file mode 100644 index 0000000..839c71f --- /dev/null +++ b/CodeGen/Editor/PostProcessorReflectionImporter.cs @@ -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); + } + } +} diff --git a/CodeGen/Editor/PostProcessorReflectionImporter.cs.meta b/CodeGen/Editor/PostProcessorReflectionImporter.cs.meta new file mode 100644 index 0000000..70b8deb --- /dev/null +++ b/CodeGen/Editor/PostProcessorReflectionImporter.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: 0a6c2401798b42f0bbcde72796d2fa9c +timeCreated: 1772908906 diff --git a/CodeGen/Editor/Unity.ShrinkEventBus.CodeGen.asmdef b/CodeGen/Editor/Unity.ShrinkEventBus.CodeGen.asmdef new file mode 100644 index 0000000..ffe61e9 --- /dev/null +++ b/CodeGen/Editor/Unity.ShrinkEventBus.CodeGen.asmdef @@ -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 +} diff --git a/CodeGen/Editor/Unity.ShrinkEventBus.CodeGen.asmdef.meta b/CodeGen/Editor/Unity.ShrinkEventBus.CodeGen.asmdef.meta new file mode 100644 index 0000000..7fd1430 --- /dev/null +++ b/CodeGen/Editor/Unity.ShrinkEventBus.CodeGen.asmdef.meta @@ -0,0 +1,7 @@ +fileFormatVersion: 2 +guid: 91161d4c2ae3ae74c9184ba33d9aa52e +AssemblyDefinitionImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Development~/UnityProject/.gitignore b/Development~/UnityProject/.gitignore new file mode 100644 index 0000000..aa2eb00 --- /dev/null +++ b/Development~/UnityProject/.gitignore @@ -0,0 +1,6 @@ +[Ll]ibrary/ +[Tt]emp/ +[Oo]bj/ +[Ll]ogs/ +[Uu]ser[Ss]ettings/ +TestResults/ \ No newline at end of file diff --git a/Development~/UnityProject/Assets/.gitkeep b/Development~/UnityProject/Assets/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/Development~/UnityProject/Packages/manifest.json b/Development~/UnityProject/Packages/manifest.json new file mode 100644 index 0000000..355680f --- /dev/null +++ b/Development~/UnityProject/Packages/manifest.json @@ -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:../../.." + } +} diff --git a/Development~/UnityProject/ProjectSettings/ProjectVersion.txt b/Development~/UnityProject/ProjectSettings/ProjectVersion.txt new file mode 100644 index 0000000..587f809 --- /dev/null +++ b/Development~/UnityProject/ProjectSettings/ProjectVersion.txt @@ -0,0 +1,2 @@ +m_EditorVersion: 2022.3.62f3 +m_EditorVersionWithRevision: 2022.3.62f3 (96770f904ca7) diff --git a/Editor.meta b/Editor.meta new file mode 100644 index 0000000..454aa37 --- /dev/null +++ b/Editor.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: 8bf98940dc144bd2bffd55ce21295bbd +timeCreated: 1772878399 \ No newline at end of file diff --git a/Editor/EventBusViewerWindow.cs b/Editor/EventBusViewerWindow.cs new file mode 100644 index 0000000..ff8b265 --- /dev/null +++ b/Editor/EventBusViewerWindow.cs @@ -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 _pendingEvents = new(); + private readonly List _events = new(); + private readonly List _filteredEvents = new(); + private readonly List _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(); + 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().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().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