371 lines
16 KiB
C#
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);
|
|
}
|
|
}
|