From d3eca1f155bece2f4e1de2184579e655cf7b828e Mon Sep 17 00:00:00 2001 From: cneicy Date: Tue, 29 Sep 2026 10:14:41 +0800 Subject: [PATCH] perf(context)!: release retired fibers and bound diagnostic history --- DotNet~/ShrinkSDK.Context.Core.csproj | 2 +- README.md | 10 +++ Runtime/Context/ShrinkCtx.cs | 19 ++++- Runtime/Fibers/ShrinkFiber.cs | 2 + Runtime/ShrinkContextRuntime.cs | 105 +++++++++++++++++++++---- Tests/RetirementRetentionTests.cs | 88 +++++++++++++++++++++ Tests/RetirementRetentionTests.cs.meta | 11 +++ package.json | 2 +- 8 files changed, 220 insertions(+), 19 deletions(-) create mode 100644 Tests/RetirementRetentionTests.cs create mode 100644 Tests/RetirementRetentionTests.cs.meta diff --git a/DotNet~/ShrinkSDK.Context.Core.csproj b/DotNet~/ShrinkSDK.Context.Core.csproj index b7d54aa..be806b1 100644 --- a/DotNet~/ShrinkSDK.Context.Core.csproj +++ b/DotNet~/ShrinkSDK.Context.Core.csproj @@ -5,7 +5,7 @@ ShrinkContext.Core.Runtime ShrinkContext ShrinkSDK.Context.Core - 0.2.0 + 0.3.0 ShrinkSDK reversible context and fiber runtime. false diff --git a/README.md b/README.md index 4372301..173b86d 100644 --- a/README.md +++ b/README.md @@ -69,3 +69,13 @@ - 仅主线程使用;异步转换在同步可完成路径上内联跑完(测试无需 PlayerLoop 泵即可确定性断言)。 - 测试中的 `TestAwait.Run` 同步阻塞等待,仅用于保证延续同步完成的测试代码。 + +## 可复用能力 + + +Capability: 动态模块依赖、provider 替换和可逆生命周期;声明式组合失败恢复 +Aliases: 上下文 组件 加载 卸载 装卸 依赖 热替换 isolate intercept Cordis +Limits: 不提供具体战斗、资源、背包机制;同一 runtime 的组合操作须在所属线程串行协调 +Extension: IShrinkComponent.Inject/Provide/Apply;ShrinkLoaderEntry 配置;ctx.Effect/Set/Get +Evidence: [ShrinkContextRuntime](Runtime/ShrinkContextRuntime.cs); [ShrinkContextLoader](Runtime/ShrinkContextDefaults.cs); [ShrinkCtx](Runtime/Context/ShrinkCtx.cs) + diff --git a/Runtime/Context/ShrinkCtx.cs b/Runtime/Context/ShrinkCtx.cs index 6d83f2c..80055e5 100644 --- a/Runtime/Context/ShrinkCtx.cs +++ b/Runtime/Context/ShrinkCtx.cs @@ -14,6 +14,7 @@ namespace ShrinkContext public sealed class ShrinkCtx { private readonly List _effects = new(); + private bool _disposing; private readonly Dictionary? _isolateOverlay; private Dictionary>? _interceptOverlay; @@ -81,6 +82,13 @@ namespace ShrinkContext internal void Attach(ShrinkEffectHandle handle) => _effects.Add(handle); + internal void Detach(ShrinkEffectHandle handle) + { + // During parent teardown the reverse traversal owns removal. + if (!_disposing) + _effects.Remove(handle); + } + /// /// 前向已在外部执行完毕的效应:仅登记逆操作(供桥接适配器把既有 API 的"注册/注销"对包装为可逆效应)。 /// @@ -98,9 +106,14 @@ namespace ShrinkContext /// 按 LIFO 顺序执行本上下文注册的全部逆操作。 public async UniTask DisposeAsync() { - for (var i = _effects.Count - 1; i >= 0; i--) - await _effects[i].DisposeAsync(); - _effects.Clear(); + _disposing = true; + try + { + for (var i = _effects.Count - 1; i >= 0; i--) + await _effects[i].DisposeAsync(); + _effects.Clear(); + } + finally { _disposing = false; } } // ---------------- 余效应(论文 3.2 / 算法 2) ---------------- diff --git a/Runtime/Fibers/ShrinkFiber.cs b/Runtime/Fibers/ShrinkFiber.cs index fbf9f66..c4e5c33 100644 --- a/Runtime/Fibers/ShrinkFiber.cs +++ b/Runtime/Fibers/ShrinkFiber.cs @@ -62,6 +62,8 @@ namespace ShrinkContext internal long[]? Target; internal bool InTransition; internal bool Retired; + internal ShrinkCtx? EffectOwner; + internal ShrinkEffectHandle? Instantiation; internal HashSet InjectSet { get; } internal HashSet ProvideSet { get; } } diff --git a/Runtime/ShrinkContextRuntime.cs b/Runtime/ShrinkContextRuntime.cs index da8ad68..d6958ec 100644 --- a/Runtime/ShrinkContextRuntime.cs +++ b/Runtime/ShrinkContextRuntime.cs @@ -20,16 +20,22 @@ namespace ShrinkContext { private readonly ShrinkCtx _rootCtx; private readonly List _fibers = new(); + private readonly Queue _retiredHistory = new(); + private readonly int _retiredHistoryCapacity; private readonly Dictionary _store = new(); private readonly Dictionary> _injectIndex = new(StringComparer.Ordinal); + private readonly Queue _transitionWork = new(); + private bool _drainingTransitionWork; private long _uidCounter; private long _notificationDispatchCount; private long _notificationCandidateVisitCount; private int _lastNotificationCandidateCount; - public ShrinkContextRuntime() + public ShrinkContextRuntime(int retiredHistoryCapacity = 64) { + if (retiredHistoryCapacity < 0) throw new ArgumentOutOfRangeException(nameof(retiredHistoryCapacity)); + _retiredHistoryCapacity = retiredHistoryCapacity; _rootCtx = new ShrinkCtx(this, null, null, null); } @@ -65,6 +71,8 @@ namespace ShrinkContext var instantiationHandle = new ShrinkEffectHandle(); instantiationHandle.AttachInverse(() => RetireCoreAsync(fiber)); parent.Attach(instantiationHandle); + fiber.EffectOwner = parent; + fiber.Instantiation = instantiationHandle; Refresh(fiber); return fiber; @@ -77,11 +85,16 @@ namespace ShrinkContext public async UniTask RetireAsync(ShrinkFiber fiber) { if (fiber.Retired) + { + if (fiber.InTransition) await fiber.Inertia; + await ReleaseRetiredAsync(fiber); return; + } UnindexFiber(fiber); if (fiber.State == ShrinkFiberState.Inactive && !fiber.InTransition) { fiber.Retired = true; + await ReleaseRetiredAsync(fiber); return; } @@ -89,12 +102,33 @@ namespace ShrinkContext fiber.Target = null; if (!fiber.InTransition) { - fiber.InTransition = true; - fiber.Inertia = RunTransitionAsync(fiber); + ScheduleTransition(fiber); } if (fiber.InTransition) await fiber.Inertia; + await ReleaseRetiredAsync(fiber); + } + + private async UniTask ReleaseRetiredAsync(ShrinkFiber fiber) + { + if (fiber.InTransition || fiber.State != ShrinkFiberState.Inactive || !_fibers.Remove(fiber)) return; + var owner = fiber.EffectOwner; + var handle = fiber.Instantiation; + fiber.EffectOwner = null; + fiber.Instantiation = null; + if (handle != null) + { + owner?.Detach(handle); + await handle.DisposeAsync(); + } + if (_retiredHistoryCapacity == 0) return; + while (_retiredHistory.Count >= _retiredHistoryCapacity) _retiredHistory.Dequeue(); + _retiredHistory.Enqueue(new ShrinkFiberDiagnostic(fiber.Uid, fiber.Name, + ShrinkFiberDiagnosticStatus.Retired, fiber.State, true, false, + fiber.InjectSet.ToArray(), fiber.ProvideSet.ToArray(), Array.Empty(), + Array.Empty(), Array.Empty(), + fiber.LastError?.GetType().FullName, fiber.LastError?.Message)); } /// 卸载全部纤程并回滚根上下文效应。 @@ -123,13 +157,47 @@ namespace ShrinkContext fiber.Target = target; if (!fiber.InTransition) { - fiber.InTransition = true; - fiber.Inertia = RunTransitionAsync(fiber); + ScheduleTransition(fiber); } return true; } + private void ScheduleTransition(ShrinkFiber fiber) + { + var completion = new UniTaskCompletionSource(); + fiber.InTransition = true; + fiber.Inertia = completion.Task.Preserve(); + EnqueueTransitionWork(() => RunScheduledTransitionAsync(fiber, completion).Forget()); + } + + private async UniTask RunScheduledTransitionAsync(ShrinkFiber fiber, UniTaskCompletionSource completion) + { + try + { + await RunTransitionAsync(fiber); + EnqueueTransitionWork(() => completion.TrySetResult()); + } + catch (Exception error) + { + EnqueueTransitionWork(() => completion.TrySetException(error)); + } + } + + private void EnqueueTransitionWork(Action work) + { + _transitionWork.Enqueue(work); + if (_drainingTransitionWork) return; + _drainingTransitionWork = true; + try + { + // Starting transitions and completing their awaiters both use this trampoline. + // Deep synchronous dependency chains therefore keep a bounded CLR call stack. + while (_transitionWork.Count > 0) _transitionWork.Dequeue()(); + } + finally { _drainingTransitionWork = false; } + } + private async UniTask RunTransitionAsync(ShrinkFiber fiber) { try @@ -457,8 +525,21 @@ namespace ShrinkContext /// 捕获不含可变运行时引用的诊断快照。 public ShrinkContextRuntimeDiagnostic CaptureDiagnostic() { - var diagnostics = new List(_fibers.Count); - foreach (var fiber in _fibers.OrderBy(item => item.Uid)) + var providers = new Dictionary<(string key, string realm), List>(); + foreach (var candidate in _fibers) + { + if (candidate.Retired) continue; + foreach (var key in candidate.ProvideSet) + { + if (!_injectIndex.ContainsKey(key)) continue; + var id = (key, candidate.Ctx.ResolveRealm(key)); + if (!providers.TryGetValue(id, out var entries)) providers.Add(id, entries = new List()); + entries.Add(candidate.Uid); + } + } + var diagnostics = new List(_fibers.Count + _retiredHistory.Count); + diagnostics.AddRange(_retiredHistory); + foreach (var fiber in _fibers) { var dependencies = new List(); foreach (var key in fiber.Component.Inject ?? Array.Empty()) @@ -467,12 +548,8 @@ namespace ShrinkContext var current = ResolveBinding(fiber.Ctx, key); ShrinkBinding? committed = null; fiber.Committed?.TryGetValue(key, out committed); - var potential = _fibers - .Where(candidate => !candidate.Retired && candidate.ProvideSet.Contains(key) && - candidate.Ctx.ResolveRealm(key) == realm) - .Select(candidate => candidate.Uid) - .OrderBy(uid => uid) - .ToArray(); + var potential = providers.TryGetValue((key, realm), out var candidates) + ? candidates.ToArray() : Array.Empty(); dependencies.Add(new ShrinkDependencyDiagnostic( key, @@ -499,7 +576,7 @@ namespace ShrinkContext } return new ShrinkContextRuntimeDiagnostic( - diagnostics, + diagnostics.OrderBy(item => item.Uid).ToArray(), _store.Count, new ShrinkNotificationDiagnostic( _notificationDispatchCount, diff --git a/Tests/RetirementRetentionTests.cs b/Tests/RetirementRetentionTests.cs new file mode 100644 index 0000000..11b0a54 --- /dev/null +++ b/Tests/RetirementRetentionTests.cs @@ -0,0 +1,88 @@ +#nullable enable +using System; +using System.Linq; +using System.Runtime.CompilerServices; +using Cysharp.Threading.Tasks; +using NUnit.Framework; + +namespace ShrinkContext.Tests +{ + public class RetirementRetentionTests + { + [Test] + public void DeepDependencyChainRetiresWithoutGrowingTheCallStack() + { + var runtime = new ShrinkContextRuntime(); + ShrinkFiber first = null; + for (var i = 0; i < 1000; i++) + { + var fiber = runtime.Use(new ChainComponent(i)); + if (i == 0) first = fiber; + } + TestAwait.Run(runtime.RetireAsync(first)); + Assert.That(runtime.Fibers.All(f => f.State == ShrinkFiberState.Inactive)); + Assert.That(runtime.CaptureDiagnostic().BindingCount, Is.Zero); + TestAwait.Run(runtime.ShutdownAsync()); + Assert.That(runtime.Fibers, Is.Empty); + } + + private sealed class ChainComponent : IShrinkComponent + { + public ChainComponent(int index) + { + Name = "chain-" + index; + Inject = index == 0 ? Array.Empty() : new[] { "chain-" + (index - 1) }; + Provide = new[] { Name }; + } + public string Name { get; } + public System.Collections.Generic.IReadOnlyList Inject { get; } + public System.Collections.Generic.IReadOnlyList Provide { get; } + public UniTask ApplyAsync(ShrinkCtx ctx, object config) { ctx.Set(Name, this); return UniTask.CompletedTask; } + } + + [Test] + public void RepeatedRetirementKeepsOnlyBoundedValueHistory() + { + var runtime = new ShrinkContextRuntime(8); + for (var i = 0; i < 10000; i++) + TestAwait.Run(runtime.RetireAsync(runtime.Use(new ProviderComponent("provider", "value", i)))); + Assert.That(runtime.Fibers, Is.Empty); + var diagnostic = runtime.CaptureDiagnostic(); + Assert.That(diagnostic.Fibers.Count, Is.EqualTo(8)); + Assert.That(diagnostic.Fibers.All(f => f.Retired)); + Assert.That(diagnostic.BindingCount, Is.Zero); + TestAwait.Run(runtime.ShutdownAsync()); + } + + [Test] + public void RuntimeAndParentEffectsDoNotRetainRetiredComponent() + { + var runtime = new ShrinkContextRuntime(); + var weak = CreateAndRetire(runtime); + GC.Collect(); GC.WaitForPendingFinalizers(); GC.Collect(); + Assert.That(weak.IsAlive, Is.False); + GC.KeepAlive(runtime); + } + + [MethodImpl(MethodImplOptions.NoInlining)] + private static WeakReference CreateAndRetire(ShrinkContextRuntime runtime) + { + var component = new ProviderComponent("collectible", "key", new byte[1024]); + var weak = new WeakReference(component); + TestAwait.Run(runtime.RetireAsync(runtime.Use(component))); + return weak; + } + + [Test] + public void DiagnosticProvidersRespectRealmsAndExcludeRetired() + { + var runtime = new ShrinkContextRuntime(); + var p = runtime.Use(new ProviderComponent("p", "key", "value")); + var c = runtime.Use(new ConsumerComponent("c", "key")); + Assert.That(runtime.CaptureDiagnostic().Fibers.Single(f => f.Uid == c.Uid).Dependencies.Single().PotentialProviderUids, Is.EqualTo(new[] { p.Uid })); + TestAwait.Run(runtime.RetireAsync(p)); + Assert.That(runtime.CaptureDiagnostic().Fibers.Single(f => f.Uid == c.Uid).Dependencies.Single().PotentialProviderUids, Is.Empty); + TestAwait.Run(runtime.ShutdownAsync()); + } + } +} diff --git a/Tests/RetirementRetentionTests.cs.meta b/Tests/RetirementRetentionTests.cs.meta new file mode 100644 index 0000000..fd8bc6e --- /dev/null +++ b/Tests/RetirementRetentionTests.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 3593eb7178803dd40b5e070709a5fe8b +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/package.json b/package.json index d5174cd..1080b1b 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "com.cneicy.shrink-context-core", - "version": "0.2.0", + "version": "0.3.0", "displayName": "ShrinkContext Core", "description": "Cordis 时空可组合性范式的 Unity/C# 原型核心:可逆效应追踪、响应式余效应与组件纤程生命周期。", "unity": "2022.3",