#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("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); } }