perf(context)!: release retired fibers and bound diagnostic history

This commit is contained in:
2026-09-29 10:14:41 +08:00
parent b93f3e6122
commit 637bed8081
8 changed files with 220 additions and 19 deletions
+1 -1
View File
@@ -5,7 +5,7 @@
<AssemblyName>ShrinkContext.Core.Runtime</AssemblyName> <AssemblyName>ShrinkContext.Core.Runtime</AssemblyName>
<RootNamespace>ShrinkContext</RootNamespace> <RootNamespace>ShrinkContext</RootNamespace>
<PackageId>ShrinkSDK.Context.Core</PackageId> <PackageId>ShrinkSDK.Context.Core</PackageId>
<Version>0.2.0</Version> <Version>0.3.0</Version>
<Description>ShrinkSDK reversible context and fiber runtime.</Description> <Description>ShrinkSDK reversible context and fiber runtime.</Description>
<ShrinkCodeGenEnabled>false</ShrinkCodeGenEnabled> <ShrinkCodeGenEnabled>false</ShrinkCodeGenEnabled>
</PropertyGroup> </PropertyGroup>
+10
View File
@@ -69,3 +69,13 @@
- 仅主线程使用;异步转换在同步可完成路径上内联跑完(测试无需 PlayerLoop 泵即可确定性断言)。 - 仅主线程使用;异步转换在同步可完成路径上内联跑完(测试无需 PlayerLoop 泵即可确定性断言)。
- 测试中的 `TestAwait.Run` 同步阻塞等待,仅用于保证延续同步完成的测试代码。 - 测试中的 `TestAwait.Run` 同步阻塞等待,仅用于保证延续同步完成的测试代码。
## 可复用能力
<!-- shrink:capabilities -->
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)
<!-- /shrink:capabilities -->
+13
View File
@@ -14,6 +14,7 @@ namespace ShrinkContext
public sealed class ShrinkCtx public sealed class ShrinkCtx
{ {
private readonly List<ShrinkEffectHandle> _effects = new(); private readonly List<ShrinkEffectHandle> _effects = new();
private bool _disposing;
private readonly Dictionary<string, string>? _isolateOverlay; private readonly Dictionary<string, string>? _isolateOverlay;
private Dictionary<string, IReadOnlyDictionary<string, object?>>? _interceptOverlay; private Dictionary<string, IReadOnlyDictionary<string, object?>>? _interceptOverlay;
@@ -81,6 +82,13 @@ namespace ShrinkContext
internal void Attach(ShrinkEffectHandle handle) => _effects.Add(handle); 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);
}
/// <summary> /// <summary>
/// 前向已在外部执行完毕的效应:仅登记逆操作(供桥接适配器把既有 API 的"注册/注销"对包装为可逆效应)。 /// 前向已在外部执行完毕的效应:仅登记逆操作(供桥接适配器把既有 API 的"注册/注销"对包装为可逆效应)。
/// </summary> /// </summary>
@@ -97,11 +105,16 @@ namespace ShrinkContext
/// <summary>按 LIFO 顺序执行本上下文注册的全部逆操作。</summary> /// <summary>按 LIFO 顺序执行本上下文注册的全部逆操作。</summary>
public async UniTask DisposeAsync() public async UniTask DisposeAsync()
{
_disposing = true;
try
{ {
for (var i = _effects.Count - 1; i >= 0; i--) for (var i = _effects.Count - 1; i >= 0; i--)
await _effects[i].DisposeAsync(); await _effects[i].DisposeAsync();
_effects.Clear(); _effects.Clear();
} }
finally { _disposing = false; }
}
// ---------------- 余效应(论文 3.2 / 算法 2) ---------------- // ---------------- 余效应(论文 3.2 / 算法 2) ----------------
+2
View File
@@ -62,6 +62,8 @@ namespace ShrinkContext
internal long[]? Target; internal long[]? Target;
internal bool InTransition; internal bool InTransition;
internal bool Retired; internal bool Retired;
internal ShrinkCtx? EffectOwner;
internal ShrinkEffectHandle? Instantiation;
internal HashSet<string> InjectSet { get; } internal HashSet<string> InjectSet { get; }
internal HashSet<string> ProvideSet { get; } internal HashSet<string> ProvideSet { get; }
} }
+91 -14
View File
@@ -20,16 +20,22 @@ namespace ShrinkContext
{ {
private readonly ShrinkCtx _rootCtx; private readonly ShrinkCtx _rootCtx;
private readonly List<ShrinkFiber> _fibers = new(); private readonly List<ShrinkFiber> _fibers = new();
private readonly Queue<ShrinkFiberDiagnostic> _retiredHistory = new();
private readonly int _retiredHistoryCapacity;
private readonly Dictionary<string, ShrinkBinding> _store = new(); private readonly Dictionary<string, ShrinkBinding> _store = new();
private readonly Dictionary<string, HashSet<ShrinkFiber>> _injectIndex = private readonly Dictionary<string, HashSet<ShrinkFiber>> _injectIndex =
new(StringComparer.Ordinal); new(StringComparer.Ordinal);
private readonly Queue<Action> _transitionWork = new();
private bool _drainingTransitionWork;
private long _uidCounter; private long _uidCounter;
private long _notificationDispatchCount; private long _notificationDispatchCount;
private long _notificationCandidateVisitCount; private long _notificationCandidateVisitCount;
private int _lastNotificationCandidateCount; 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); _rootCtx = new ShrinkCtx(this, null, null, null);
} }
@@ -65,6 +71,8 @@ namespace ShrinkContext
var instantiationHandle = new ShrinkEffectHandle(); var instantiationHandle = new ShrinkEffectHandle();
instantiationHandle.AttachInverse(() => RetireCoreAsync(fiber)); instantiationHandle.AttachInverse(() => RetireCoreAsync(fiber));
parent.Attach(instantiationHandle); parent.Attach(instantiationHandle);
fiber.EffectOwner = parent;
fiber.Instantiation = instantiationHandle;
Refresh(fiber); Refresh(fiber);
return fiber; return fiber;
@@ -77,11 +85,16 @@ namespace ShrinkContext
public async UniTask RetireAsync(ShrinkFiber fiber) public async UniTask RetireAsync(ShrinkFiber fiber)
{ {
if (fiber.Retired) if (fiber.Retired)
{
if (fiber.InTransition) await fiber.Inertia;
await ReleaseRetiredAsync(fiber);
return; return;
}
UnindexFiber(fiber); UnindexFiber(fiber);
if (fiber.State == ShrinkFiberState.Inactive && !fiber.InTransition) if (fiber.State == ShrinkFiberState.Inactive && !fiber.InTransition)
{ {
fiber.Retired = true; fiber.Retired = true;
await ReleaseRetiredAsync(fiber);
return; return;
} }
@@ -89,12 +102,33 @@ namespace ShrinkContext
fiber.Target = null; fiber.Target = null;
if (!fiber.InTransition) if (!fiber.InTransition)
{ {
fiber.InTransition = true; ScheduleTransition(fiber);
fiber.Inertia = RunTransitionAsync(fiber);
} }
if (fiber.InTransition) if (fiber.InTransition)
await fiber.Inertia; 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<long>(),
Array.Empty<ShrinkDependencyDiagnostic>(), Array.Empty<string>(),
fiber.LastError?.GetType().FullName, fiber.LastError?.Message));
} }
/// <summary>卸载全部纤程并回滚根上下文效应。</summary> /// <summary>卸载全部纤程并回滚根上下文效应。</summary>
@@ -123,13 +157,47 @@ namespace ShrinkContext
fiber.Target = target; fiber.Target = target;
if (!fiber.InTransition) if (!fiber.InTransition)
{ {
fiber.InTransition = true; ScheduleTransition(fiber);
fiber.Inertia = RunTransitionAsync(fiber);
} }
return true; 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) private async UniTask RunTransitionAsync(ShrinkFiber fiber)
{ {
try try
@@ -457,8 +525,21 @@ namespace ShrinkContext
/// <summary>捕获不含可变运行时引用的诊断快照。</summary> /// <summary>捕获不含可变运行时引用的诊断快照。</summary>
public ShrinkContextRuntimeDiagnostic CaptureDiagnostic() public ShrinkContextRuntimeDiagnostic CaptureDiagnostic()
{ {
var diagnostics = new List<ShrinkFiberDiagnostic>(_fibers.Count); var providers = new Dictionary<(string key, string realm), List<long>>();
foreach (var fiber in _fibers.OrderBy(item => item.Uid)) 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<long>());
entries.Add(candidate.Uid);
}
}
var diagnostics = new List<ShrinkFiberDiagnostic>(_fibers.Count + _retiredHistory.Count);
diagnostics.AddRange(_retiredHistory);
foreach (var fiber in _fibers)
{ {
var dependencies = new List<ShrinkDependencyDiagnostic>(); var dependencies = new List<ShrinkDependencyDiagnostic>();
foreach (var key in fiber.Component.Inject ?? Array.Empty<string>()) foreach (var key in fiber.Component.Inject ?? Array.Empty<string>())
@@ -467,12 +548,8 @@ namespace ShrinkContext
var current = ResolveBinding(fiber.Ctx, key); var current = ResolveBinding(fiber.Ctx, key);
ShrinkBinding? committed = null; ShrinkBinding? committed = null;
fiber.Committed?.TryGetValue(key, out committed); fiber.Committed?.TryGetValue(key, out committed);
var potential = _fibers var potential = providers.TryGetValue((key, realm), out var candidates)
.Where(candidate => !candidate.Retired && candidate.ProvideSet.Contains(key) && ? candidates.ToArray() : Array.Empty<long>();
candidate.Ctx.ResolveRealm(key) == realm)
.Select(candidate => candidate.Uid)
.OrderBy(uid => uid)
.ToArray();
dependencies.Add(new ShrinkDependencyDiagnostic( dependencies.Add(new ShrinkDependencyDiagnostic(
key, key,
@@ -499,7 +576,7 @@ namespace ShrinkContext
} }
return new ShrinkContextRuntimeDiagnostic( return new ShrinkContextRuntimeDiagnostic(
diagnostics, diagnostics.OrderBy(item => item.Uid).ToArray(),
_store.Count, _store.Count,
new ShrinkNotificationDiagnostic( new ShrinkNotificationDiagnostic(
_notificationDispatchCount, _notificationDispatchCount,
+88
View File
@@ -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<string>() : new[] { "chain-" + (index - 1) };
Provide = new[] { Name };
}
public string Name { get; }
public System.Collections.Generic.IReadOnlyList<string> Inject { get; }
public System.Collections.Generic.IReadOnlyList<string> 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());
}
}
}
+11
View File
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 3593eb7178803dd40b5e070709a5fe8b
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "com.cneicy.shrink-context-core", "name": "com.cneicy.shrink-context-core",
"version": "0.2.0", "version": "0.3.0",
"displayName": "ShrinkContext Core", "displayName": "ShrinkContext Core",
"description": "Cordis 时空可组合性范式的 Unity/C# 原型核心:可逆效应追踪、响应式余效应与组件纤程生命周期。", "description": "Cordis 时空可组合性范式的 Unity/C# 原型核心:可逆效应追踪、响应式余效应与组件纤程生命周期。",
"unity": "2022.3", "unity": "2022.3",