Files
ShrinkContext.AppAdapter/Editor/ShrinkContextDiagnosticsWindow.cs
cneicy 52bda43e3c
Publish UPM package / publish (push) Successful in 2s
chore(editor): localize context entry points
2026-08-26 12:48:31 +08:00

371 lines
16 KiB
C#

#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/上下文/诊断与组合")]
public static void ShowWindow()
{
var window = GetWindow<ShrinkContextDiagnosticsWindow>("Context 诊断与组合");
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(
"未选择组合配置。可通过 Assets/Create/ShrinkSDK/上下文/组合配置创建。",
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);
}
}