feat(cordis): 完成阶段5配置与调试工具

This commit is contained in:
2026-08-17 01:58:46 +08:00
parent 8738e633ee
commit 5765f411e9
29 changed files with 1322 additions and 18 deletions
@@ -0,0 +1,19 @@
{
"name": "ShrinkContext.AppAdapter.Editor",
"rootNamespace": "ShrinkContext.AppAdapter.Editor",
"references": [
"ShrinkContext.AppAdapter.Runtime",
"ShrinkContext.Core.Runtime",
"UniTask"
],
"includePlatforms": [
"Editor"
],
"excludePlatforms": [],
"allowUnsafeCode": false,
"overrideReferences": false,
"autoReferenced": true,
"defineConstraints": [],
"versionDefines": [],
"noEngineReferences": false
}
@@ -0,0 +1,7 @@
fileFormatVersion: 2
guid: 71b936124873ba345a9430e664e7cfcd
AssemblyDefinitionImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,189 @@
#nullable enable
using System;
using System.Collections.Generic;
using System.Diagnostics;
using Cysharp.Threading.Tasks;
namespace ShrinkContext.AppAdapter.Editor
{
public sealed class ShrinkContextBenchmarkReport
{
internal ShrinkContextBenchmarkReport(int unrelatedFiberCount, int reloadIterations,
double notifyMilliseconds, long notificationDispatchCount, long notificationCandidateVisits,
long naiveFiberVisits, int consumerApplyCount, int failureIterations,
int restoredFailureCount, double restoreMilliseconds, long managedMemoryDeltaBytes)
{
UnrelatedFiberCount = unrelatedFiberCount;
ReloadIterations = reloadIterations;
NotifyMilliseconds = notifyMilliseconds;
NotificationDispatchCount = notificationDispatchCount;
NotificationCandidateVisits = notificationCandidateVisits;
NaiveFiberVisits = naiveFiberVisits;
ConsumerApplyCount = consumerApplyCount;
FailureIterations = failureIterations;
RestoredFailureCount = restoredFailureCount;
RestoreMilliseconds = restoreMilliseconds;
ManagedMemoryDeltaBytes = managedMemoryDeltaBytes;
}
public int UnrelatedFiberCount { get; }
public int ReloadIterations { get; }
public double NotifyMilliseconds { get; }
public long NotificationDispatchCount { get; }
public long NotificationCandidateVisits { get; }
public long NaiveFiberVisits { get; }
public int ConsumerApplyCount { get; }
public int FailureIterations { get; }
public int RestoredFailureCount { get; }
public double RestoreMilliseconds { get; }
public long ManagedMemoryDeltaBytes { get; }
}
/// <summary>Editor 可重复容量基准;耗时仅报告,不作为跨机器通过阈值。</summary>
public static class ShrinkContextBenchmarkRunner
{
public static async UniTask<ShrinkContextBenchmarkReport> RunAsync(
int unrelatedFiberCount = 1000, int reloadIterations = 100, int failureIterations = 25)
{
if (unrelatedFiberCount < 0)
throw new ArgumentOutOfRangeException(nameof(unrelatedFiberCount));
if (reloadIterations <= 0)
throw new ArgumentOutOfRangeException(nameof(reloadIterations));
if (failureIterations <= 0)
throw new ArgumentOutOfRangeException(nameof(failureIterations));
var memoryBefore = GC.GetTotalMemory(false);
var runtime = new ShrinkContextRuntime();
var targetConsumer = new BenchmarkConsumer("benchmark.target");
runtime.Use(targetConsumer);
for (var i = 0; i < unrelatedFiberCount; i++)
runtime.Use(new BenchmarkConsumer("benchmark.unrelated." + i));
var notifyStopwatch = Stopwatch.StartNew();
for (var i = 0; i < reloadIterations; i++)
{
var provider = runtime.Use(new BenchmarkProvider("provider-" + i, "benchmark.target"));
if (provider.LastError != null)
throw new InvalidOperationException("Benchmark provider failed.", provider.LastError);
await runtime.RetireAsync(provider);
}
notifyStopwatch.Stop();
var notifySnapshot = runtime.CaptureDiagnostic();
var naiveFiberVisits = notifySnapshot.Notifications.DispatchCount * runtime.Fibers.Count;
var catalog = new ShrinkComponentCatalog();
catalog.Register("stable", () => new BenchmarkProvider("stable", "benchmark.restore"));
catalog.Register("failing", () => new BenchmarkFailingProvider("benchmark.restore"));
var restoreRuntime = new ShrinkContextRuntime();
var loader = new ShrinkContextLoader(restoreRuntime, catalog);
await loader.ApplyAsync(new[] { new ShrinkLoaderEntry("provider", "stable") });
var restoredFailures = 0;
var restoreStopwatch = Stopwatch.StartNew();
for (var i = 0; i < failureIterations; i++)
{
try
{
await loader.ApplyAsync(new[] { new ShrinkLoaderEntry("provider", "failing") });
}
catch (ShrinkLoaderException)
{
if (loader.LastTransaction?.PreviousCompositionRestored == true &&
loader.TryGetFiber("provider", out var restored) &&
restored.State == ShrinkFiberState.Active)
{
restoredFailures++;
}
}
}
restoreStopwatch.Stop();
await runtime.ShutdownAsync();
await restoreRuntime.ShutdownAsync();
var memoryAfter = GC.GetTotalMemory(false);
return new ShrinkContextBenchmarkReport(
unrelatedFiberCount,
reloadIterations,
notifyStopwatch.Elapsed.TotalMilliseconds,
notifySnapshot.Notifications.DispatchCount,
notifySnapshot.Notifications.CandidateVisitCount,
naiveFiberVisits,
targetConsumer.ApplyCount,
failureIterations,
restoredFailures,
restoreStopwatch.Elapsed.TotalMilliseconds,
memoryAfter - memoryBefore);
}
private sealed class BenchmarkProvider : IShrinkComponent
{
private readonly string[] _provide;
public BenchmarkProvider(string name, string key)
{
Name = name;
Key = key;
_provide = new[] { key };
}
public string Name { get; }
private string Key { get; }
public IReadOnlyList<string> Inject => Array.Empty<string>();
public IReadOnlyList<string> Provide => _provide;
public UniTask ApplyAsync(ShrinkCtx ctx, object? config)
{
ctx.Set(Key, Name);
return UniTask.CompletedTask;
}
}
private sealed class BenchmarkConsumer : IShrinkComponent
{
private readonly string[] _inject;
public BenchmarkConsumer(string key)
{
Key = key;
_inject = new[] { key };
}
private string Key { get; }
public string Name => "consumer:" + Key;
public int ApplyCount { get; private set; }
public IReadOnlyList<string> Inject => _inject;
public IReadOnlyList<string> Provide => Array.Empty<string>();
public UniTask ApplyAsync(ShrinkCtx ctx, object? config)
{
ctx.Get<string>(Key);
ApplyCount++;
return UniTask.CompletedTask;
}
}
private sealed class BenchmarkFailingProvider : IShrinkComponent
{
private readonly string[] _provide;
public BenchmarkFailingProvider(string key)
{
Key = key;
_provide = new[] { key };
}
private string Key { get; }
public string Name => "benchmark-failing";
public IReadOnlyList<string> Inject => Array.Empty<string>();
public IReadOnlyList<string> Provide => _provide;
public UniTask ApplyAsync(ShrinkCtx ctx, object? config)
{
ctx.Set(Key, Name);
throw new InvalidOperationException("Expected benchmark replacement failure.");
}
}
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: db4f7dd3ed1ecab4e8a04fd83ca0b0fb
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,370 @@
#nullable enable
using System;
using System.Collections;
using System.IO;
using System.Linq;
using System.Reflection;
using Cysharp.Threading.Tasks;
using UnityEditor;
using UnityEngine;
namespace ShrinkContext.AppAdapter.Editor
{
public sealed class ShrinkContextDiagnosticsWindow : EditorWindow
{
private readonly string[] _tabs = { "运行时", "组合配置", "容量基准" };
private int _selectedTab;
private Vector2 _runtimeScroll;
private Vector2 _jsonScroll;
private Vector2 _benchmarkScroll;
private bool _autoRefresh = true;
private ShrinkAppCompositionProfile? _profile;
private string _compositionJson = string.Empty;
private string? _compositionError;
private int _unrelatedFibers = 1000;
private int _reloadIterations = 100;
private int _failureIterations = 25;
private bool _benchmarkRunning;
private ShrinkContextBenchmarkReport? _benchmarkReport;
private string? _benchmarkError;
[MenuItem("ShrinkSDK/Cordis/诊断与组合")]
public static void ShowWindow()
{
var window = GetWindow<ShrinkContextDiagnosticsWindow>("Cordis Diagnostics");
window.minSize = new Vector2(720f, 520f);
}
private void OnEnable()
{
_profile = ShrinkAppCompositionProfile.LoadDefault();
RefreshCompositionJson();
}
private void OnInspectorUpdate()
{
if (_autoRefresh && _selectedTab == 0)
Repaint();
}
private void OnGUI()
{
DrawToolbar();
switch (_selectedTab)
{
case 0:
DrawRuntime();
break;
case 1:
DrawComposition();
break;
case 2:
DrawBenchmarks();
break;
}
}
private void DrawToolbar()
{
EditorGUILayout.BeginHorizontal(EditorStyles.toolbar);
_selectedTab = GUILayout.Toolbar(_selectedTab, _tabs, EditorStyles.toolbarButton);
GUILayout.FlexibleSpace();
if (_selectedTab == 0)
_autoRefresh = GUILayout.Toggle(_autoRefresh, "自动刷新", EditorStyles.toolbarButton);
EditorGUILayout.EndHorizontal();
}
private void DrawRuntime()
{
if (!Application.isPlaying)
{
EditorGUILayout.HelpBox("进入 Play Mode 后显示真实 ShrinkAppLoaderHost 状态。", MessageType.Info);
DrawModDiagnostics();
return;
}
var bootstrap = ShrinkAppLoaderBootstrapper.Instance;
if (bootstrap == null || bootstrap.Host == null)
{
EditorGUILayout.HelpBox("当前 Play Mode 没有 ShrinkAppLoaderBootstrapper。", MessageType.Warning);
DrawModDiagnostics();
return;
}
var host = bootstrap.Host;
var snapshot = host.Context.CaptureDiagnostic();
EditorGUILayout.LabelField("ContextLoader", EditorStyles.boldLabel);
EditorGUILayout.LabelField("状态", host.IsRunning ? "运行中" : "未启动");
EditorGUILayout.LabelField("模块", $"active={host.ActiveModuleIds.Count}, waiting={host.WaitingModuleIds.Count}");
EditorGUILayout.LabelField("余效应", $"bindings={snapshot.BindingCount}, indexedKeys={snapshot.Notifications.IndexedKeyCount}");
EditorGUILayout.LabelField("通知", $"dispatch={snapshot.Notifications.DispatchCount}, candidateVisits={snapshot.Notifications.CandidateVisitCount}");
DrawTransaction(host.Loader.LastTransaction);
EditorGUILayout.Space(6f);
EditorGUILayout.LabelField($"Fibers ({snapshot.Fibers.Count})", EditorStyles.boldLabel);
_runtimeScroll = EditorGUILayout.BeginScrollView(_runtimeScroll);
foreach (var fiber in snapshot.Fibers)
{
EditorGUILayout.BeginVertical(EditorStyles.helpBox);
EditorGUILayout.BeginHorizontal();
EditorGUILayout.LabelField($"#{fiber.Uid} {fiber.Name}", EditorStyles.boldLabel);
GUILayout.FlexibleSpace();
EditorGUILayout.LabelField(fiber.Status.ToString(), GUILayout.Width(90f));
EditorGUILayout.EndHorizontal();
EditorGUILayout.LabelField("target", fiber.TargetProviderUids.Count == 0
? "-"
: string.Join(", ", fiber.TargetProviderUids));
EditorGUILayout.LabelField("inject", fiber.Inject.Count == 0 ? "-" : string.Join(", ", fiber.Inject));
EditorGUILayout.LabelField("provide", fiber.Provide.Count == 0 ? "-" : string.Join(", ", fiber.Provide));
if (fiber.InterceptKeys.Count > 0)
EditorGUILayout.LabelField("intercept", string.Join(", ", fiber.InterceptKeys));
if (!string.IsNullOrWhiteSpace(fiber.ErrorMessage))
EditorGUILayout.HelpBox($"{fiber.ErrorType}: {fiber.ErrorMessage}", MessageType.Error);
foreach (var dependency in fiber.Dependencies.Where(item => !item.IsSatisfied))
{
EditorGUILayout.LabelField("waiting",
$"{dependency.Key} @ {dependency.Realm}; potential=[{string.Join(", ", dependency.PotentialProviderUids)}]");
}
EditorGUILayout.EndVertical();
}
EditorGUILayout.EndScrollView();
DrawModDiagnostics();
}
private static void DrawTransaction(ShrinkLoaderTransactionDiagnostic? transaction)
{
EditorGUILayout.Space(6f);
EditorGUILayout.LabelField("最近事务", EditorStyles.boldLabel);
if (transaction == null)
{
EditorGUILayout.LabelField("尚无事务。");
return;
}
EditorGUILayout.LabelField("阶段", $"#{transaction.Generation} {transaction.Phase}");
if (!string.IsNullOrWhiteSpace(transaction.CurrentEntryId))
EditorGUILayout.LabelField("条目", transaction.CurrentEntryId);
if (!string.IsNullOrWhiteSpace(transaction.ErrorMessage))
{
var restore = transaction.RestoreAttempted
? $"restore={transaction.PreviousCompositionRestored}"
: "restore=not-attempted";
EditorGUILayout.HelpBox($"{transaction.ErrorType}: {transaction.ErrorMessage}\n{restore}",
transaction.PreviousCompositionRestored ? MessageType.Warning : MessageType.Error);
}
}
private void DrawComposition()
{
var selected = (ShrinkAppCompositionProfile?)EditorGUILayout.ObjectField(
"Profile", _profile, typeof(ShrinkAppCompositionProfile), false);
if (selected != _profile)
{
_profile = selected;
RefreshCompositionJson();
}
if (_profile == null)
{
EditorGUILayout.HelpBox(
"未选择 Composition Profile。可通过 Assets/Create/ShrinkSDK/Cordis/Composition Profile 创建。",
MessageType.Info);
return;
}
EditorGUILayout.LabelField("条目", _profile.Entries.Count.ToString());
EditorGUILayout.LabelField("未列出条目", _profile.IncludeUnlistedEntries ? "保留" : "排除");
if (Application.isPlaying)
EditorGUILayout.HelpBox("修改资产只影响下一次宿主启动,不会重写当前运行中的组合。", MessageType.Info);
EditorGUILayout.BeginHorizontal();
if (GUILayout.Button("从资产刷新 JSON"))
RefreshCompositionJson();
if (GUILayout.Button("将 JSON 写回资产"))
ApplyCompositionJson();
if (GUILayout.Button("导入 JSON..."))
ImportCompositionJson();
if (GUILayout.Button("导出 JSON..."))
ExportCompositionJson();
EditorGUILayout.EndHorizontal();
if (!string.IsNullOrWhiteSpace(_compositionError))
EditorGUILayout.HelpBox(_compositionError, MessageType.Error);
_jsonScroll = EditorGUILayout.BeginScrollView(_jsonScroll);
_compositionJson = EditorGUILayout.TextArea(_compositionJson, GUILayout.ExpandHeight(true));
EditorGUILayout.EndScrollView();
}
private void RefreshCompositionJson()
{
try
{
_compositionJson = _profile != null ? _profile.ToJson() : string.Empty;
_compositionError = null;
}
catch (Exception ex)
{
_compositionError = ex.Message;
}
}
private void ApplyCompositionJson()
{
if (_profile == null)
return;
try
{
var document = ShrinkAppCompositionDocument.FromJson(_compositionJson);
Undo.RecordObject(_profile, "Apply Cordis composition JSON");
_profile.SetDocument(document);
EditorUtility.SetDirty(_profile);
AssetDatabase.SaveAssets();
_compositionError = null;
}
catch (Exception ex)
{
_compositionError = ex.Message;
}
}
private void ImportCompositionJson()
{
var path = EditorUtility.OpenFilePanel("导入 Cordis Composition", string.Empty, "json");
if (string.IsNullOrWhiteSpace(path))
return;
try
{
_compositionJson = File.ReadAllText(path);
ShrinkAppCompositionDocument.FromJson(_compositionJson);
_compositionError = null;
}
catch (Exception ex)
{
_compositionError = ex.Message;
}
}
private void ExportCompositionJson()
{
var path = EditorUtility.SaveFilePanel("导出 Cordis Composition", string.Empty,
"ShrinkAppComposition", "json");
if (string.IsNullOrWhiteSpace(path))
return;
try
{
ShrinkAppCompositionDocument.FromJson(_compositionJson);
File.WriteAllText(path, _compositionJson);
_compositionError = null;
}
catch (Exception ex)
{
_compositionError = ex.Message;
}
}
private void DrawBenchmarks()
{
EditorGUILayout.HelpBox(
"耗时只用于本机前后对比;自动测试验证倒排索引候选规模和失败恢复次数,不设置毫秒阈值。",
MessageType.Info);
_unrelatedFibers = Mathf.Max(0, EditorGUILayout.IntField("无关 fibers", _unrelatedFibers));
_reloadIterations = Mathf.Max(1, EditorGUILayout.IntField("provider 替换次数", _reloadIterations));
_failureIterations = Mathf.Max(1, EditorGUILayout.IntField("失败恢复次数", _failureIterations));
EditorGUI.BeginDisabledGroup(_benchmarkRunning || Application.isPlaying);
if (GUILayout.Button(_benchmarkRunning ? "运行中..." : "运行基准"))
RunBenchmarksAsync().Forget();
EditorGUI.EndDisabledGroup();
if (Application.isPlaying)
EditorGUILayout.HelpBox("容量基准仅在 Edit Mode 运行,避免污染当前游戏上下文。", MessageType.Warning);
if (!string.IsNullOrWhiteSpace(_benchmarkError))
EditorGUILayout.HelpBox(_benchmarkError, MessageType.Error);
_benchmarkScroll = EditorGUILayout.BeginScrollView(_benchmarkScroll);
if (_benchmarkReport != null)
{
EditorGUILayout.LabelField("Notify / Reload", EditorStyles.boldLabel);
EditorGUILayout.LabelField("耗时", $"{_benchmarkReport.NotifyMilliseconds:F3} ms");
EditorGUILayout.LabelField("dispatch", _benchmarkReport.NotificationDispatchCount.ToString());
EditorGUILayout.LabelField("indexed candidate visits", _benchmarkReport.NotificationCandidateVisits.ToString());
EditorGUILayout.LabelField("naive all-fiber visits", _benchmarkReport.NaiveFiberVisits.ToString());
EditorGUILayout.LabelField("consumer apply", $"{_benchmarkReport.ConsumerApplyCount}/{_benchmarkReport.ReloadIterations}");
EditorGUILayout.Space(6f);
EditorGUILayout.LabelField("失败恢复", EditorStyles.boldLabel);
EditorGUILayout.LabelField("耗时", $"{_benchmarkReport.RestoreMilliseconds:F3} ms");
EditorGUILayout.LabelField("恢复成功", $"{_benchmarkReport.RestoredFailureCount}/{_benchmarkReport.FailureIterations}");
EditorGUILayout.LabelField("GC 管理堆差值(噪声参考)", $"{_benchmarkReport.ManagedMemoryDeltaBytes} bytes");
}
EditorGUILayout.EndScrollView();
DrawModDiagnostics();
}
private async UniTaskVoid RunBenchmarksAsync()
{
_benchmarkRunning = true;
_benchmarkError = null;
try
{
_benchmarkReport = await ShrinkContextBenchmarkRunner.RunAsync(
_unrelatedFibers, _reloadIterations, _failureIterations);
}
catch (Exception ex)
{
_benchmarkError = ex.ToString();
}
finally
{
_benchmarkRunning = false;
Repaint();
}
}
private static void DrawModDiagnostics()
{
EditorGUILayout.Space(8f);
EditorGUILayout.LabelField("外部程序集常驻", EditorStyles.boldLabel);
var snapshot = CaptureOptionalModDiagnostic();
if (snapshot == null)
{
EditorGUILayout.LabelField("ShrinkModFramework 未载入或未提供诊断 API。");
return;
}
var current = ReadProperty(snapshot, "CurrentRevisionCount");
var resident = ReadProperty(snapshot, "ResidentRevisionCount");
var bytes = ReadProperty(snapshot, "EstimatedResidentBytes");
var limit = ReadProperty(snapshot, "SoftLimit");
var exceeded = ReadProperty(snapshot, "IsSoftLimitExceeded");
EditorGUILayout.LabelField("汇总",
$"current={current}, resident={resident}, bytes={bytes}, softLimit={limit}, exceeded={exceeded}");
if (ReadProperty(snapshot, "ResidentRevisions") is not IEnumerable revisions)
return;
foreach (var revision in revisions)
{
if (revision == null)
continue;
var marker = Equals(ReadProperty(revision, "IsCurrent"), true) ? "current" : "resident";
var name = ReadProperty(revision, "AssemblyName");
var hash = ReadProperty(revision, "Revision")?.ToString() ?? string.Empty;
if (hash.Length > 12)
hash = hash.Substring(0, 12);
EditorGUILayout.LabelField($"{marker}: {name}",
$"{hash}, {ReadProperty(revision, "LoadedBytes")} bytes");
}
}
private static object? CaptureOptionalModDiagnostic()
{
var type = AppDomain.CurrentDomain.GetAssemblies()
.Select(assembly => assembly.GetType("ShrinkModFramework.ShrinkModDiagnostics", false))
.FirstOrDefault(candidate => candidate != null);
var method = type?.GetMethod("CaptureExternalAssemblies", BindingFlags.Public | BindingFlags.Static);
return method?.Invoke(null, new object?[] { null });
}
private static object? ReadProperty(object instance, string name) =>
instance.GetType().GetProperty(name, BindingFlags.Public | BindingFlags.Instance)?.GetValue(instance);
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 4b6ea571e4b10e2488d1024c473528b1
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant: