diff --git a/Editor/Event.meta b/Editor/Event.meta new file mode 100644 index 0000000..76ca8b1 --- /dev/null +++ b/Editor/Event.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: c2a7d2e65d45494cbf9c71c37614d89f +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Editor/Event/EventMonitorWindow.cs b/Editor/Event/EventMonitorWindow.cs new file mode 100644 index 0000000..0fa4a7f --- /dev/null +++ b/Editor/Event/EventMonitorWindow.cs @@ -0,0 +1,789 @@ +using System; +using System.Collections.Generic; +using System.Reflection; +using System.Text; +using UnityEditor; +using UnityEngine; + +namespace AlicizaX.Editor +{ + internal sealed class EventMonitorWindow : EditorWindow + { + private const string MenuPath = "Tools/AlicizaX/事件监视器"; + private const double RepaintIntervalSeconds = 0.25d; + private const float DefaultLeftPanelWidth = 360f; + private const float MinLeftPanelWidth = 260f; + private const float MinRightPanelWidth = 360f; + private const float SplitterWidth = 5f; + + private static readonly List s_KnownEventTypes = new(); + private static readonly Dictionary s_InitialCapacityCache = new(); + + private Vector2 _eventListScroll; + private Vector2 _subscriberScroll; + private Vector2 _historyScroll; + private string _searchText = string.Empty; + private bool _onlyInitialized; + private bool _autoRefresh = true; + private Type _selectedEventType; + private double _lastRepaintTime; + private float _leftPanelWidth = DefaultLeftPanelWidth; + private bool _isDraggingSplitter; + private DateTime _snapshotTimeUtc; + private readonly Dictionary _snapshotEntries = new(); + + private readonly struct EventRow + { + internal readonly Type EventType; + internal readonly bool Initialized; + internal readonly EventDebugSummary Summary; + internal readonly int InitialCapacity; + + internal EventRow(Type eventType, bool initialized, EventDebugSummary summary, int initialCapacity) + { + EventType = eventType; + Initialized = initialized; + Summary = summary; + InitialCapacity = initialCapacity; + } + } + + private readonly struct EventSnapshotEntry + { + internal readonly EventDebugSummary Summary; + internal readonly EventDebugSubscriberInfo[] Subscribers; + + internal EventSnapshotEntry(EventDebugSummary summary, EventDebugSubscriberInfo[] subscribers) + { + Summary = summary; + Subscribers = subscribers; + } + } + + private readonly struct EventAlert + { + internal readonly MessageType Type; + internal readonly string Message; + + internal EventAlert(MessageType type, string message) + { + Type = type; + Message = message; + } + } + + [MenuItem(MenuPath, priority = 310)] + private static void Open() + { + EventMonitorWindow window = GetWindow(); + window.titleContent = new GUIContent("事件监视器"); + window.minSize = new Vector2(1080f, 640f); + window.Show(); + } + + private void OnEnable() + { + RefreshKnownEventTypes(); + EditorApplication.update += HandleEditorUpdate; + } + + private void OnDisable() + { + EditorApplication.update -= HandleEditorUpdate; + } + + private void OnFocus() + { + RefreshKnownEventTypes(); + Repaint(); + } + + private void HandleEditorUpdate() + { + if (!_autoRefresh) + { + return; + } + + double time = EditorApplication.timeSinceStartup; + if (time - _lastRepaintTime < RepaintIntervalSeconds) + { + return; + } + + _lastRepaintTime = time; + Repaint(); + } + + private void OnGUI() + { + DrawToolbar(); + + Dictionary summaries = BuildSummaryMap(); + List rows = BuildRows(summaries); + EnsureSelection(rows); + + DrawSplitLayout(rows, summaries); + } + + private void DrawToolbar() + { + EditorGUILayout.BeginHorizontal(EditorStyles.toolbar); + + GUILayout.Label("搜索", GUILayout.Width(44f)); + _searchText = GUILayout.TextField( + _searchText, + GUI.skin.FindStyle("ToolbarSearchTextField") ?? GUI.skin.FindStyle("ToolbarSeachTextField") ?? EditorStyles.toolbarTextField, + GUILayout.MinWidth(220f), + GUILayout.ExpandWidth(true)); + _onlyInitialized = GUILayout.Toggle(_onlyInitialized, "仅已初始化", EditorStyles.toolbarButton, GUILayout.Width(110f)); + _autoRefresh = GUILayout.Toggle(_autoRefresh, "自动刷新", EditorStyles.toolbarButton, GUILayout.Width(92f)); + + if (GUILayout.Button("刷新", EditorStyles.toolbarButton, GUILayout.Width(70f))) + { + RefreshKnownEventTypes(); + Repaint(); + } + + if (GUILayout.Button("重置统计", EditorStyles.toolbarButton, GUILayout.Width(84f))) + { + EventDebugRegistry.ResetStats(); + Repaint(); + } + + if (GUILayout.Button("拍摄快照", EditorStyles.toolbarButton, GUILayout.Width(108f))) + { + CaptureSnapshot(); + Repaint(); + } + + using (new EditorGUI.DisabledScope(_snapshotEntries.Count == 0)) + { + if (GUILayout.Button("清空快照", EditorStyles.toolbarButton, GUILayout.Width(96f))) + { + _snapshotEntries.Clear(); + _snapshotTimeUtc = default; + Repaint(); + } + } + + EditorGUILayout.EndHorizontal(); + + EditorGUILayout.HelpBox( + "这是仅编辑器可用的事件监视器。事件派发期间如果发生订阅、取消订阅、清空或扩容操作,会在编辑器中立即抛出异常。", + MessageType.Info); + + if (_snapshotEntries.Count > 0) + { + EditorGUILayout.HelpBox( + $"快照拍摄时间:{_snapshotTimeUtc.ToLocalTime():HH:mm:ss},发生变化的事件数量:{CountChangedEvents()}。", + MessageType.None); + } + } + + private void DrawSplitLayout(List rows, Dictionary summaries) + { + float viewWidth = EditorGUIUtility.currentViewWidth; + float maxLeftWidth = Mathf.Max(MinLeftPanelWidth, viewWidth - MinRightPanelWidth - SplitterWidth - 24f); + _leftPanelWidth = Mathf.Clamp(_leftPanelWidth, MinLeftPanelWidth, maxLeftWidth); + + EditorGUILayout.BeginHorizontal(GUILayout.ExpandWidth(true), GUILayout.ExpandHeight(true)); + + EditorGUILayout.BeginVertical(GUILayout.Width(_leftPanelWidth), GUILayout.ExpandHeight(true)); + DrawEventList(rows); + EditorGUILayout.EndVertical(); + + Rect splitterRect = GUILayoutUtility.GetRect( + SplitterWidth, + SplitterWidth, + GUILayout.Width(SplitterWidth), + GUILayout.ExpandHeight(true)); + GUI.Box(splitterRect, GUIContent.none, EditorStyles.helpBox); + EditorGUIUtility.AddCursorRect(splitterRect, MouseCursor.ResizeHorizontal); + UpdateSplitter(splitterRect, maxLeftWidth); + + EditorGUILayout.BeginVertical(GUILayout.ExpandWidth(true), GUILayout.ExpandHeight(true)); + DrawDetailPanel(summaries); + EditorGUILayout.EndVertical(); + + EditorGUILayout.EndHorizontal(); + } + + private void UpdateSplitter(Rect splitterRect, float maxLeftWidth) + { + Event currentEvent = Event.current; + + switch (currentEvent.type) + { + case EventType.MouseDown: + if (currentEvent.button == 0 && splitterRect.Contains(currentEvent.mousePosition)) + { + _isDraggingSplitter = true; + currentEvent.Use(); + } + break; + case EventType.MouseDrag: + if (_isDraggingSplitter) + { + _leftPanelWidth = Mathf.Clamp(currentEvent.mousePosition.x, MinLeftPanelWidth, maxLeftWidth); + Repaint(); + currentEvent.Use(); + } + break; + case EventType.MouseUp: + if (_isDraggingSplitter) + { + _isDraggingSplitter = false; + currentEvent.Use(); + } + break; + } + } + + private void DrawEventList(List rows) + { + EditorGUILayout.BeginVertical(); + GUILayout.Label($"事件列表({rows.Count})", EditorStyles.boldLabel); + + _eventListScroll = EditorGUILayout.BeginScrollView(_eventListScroll); + for (int i = 0; i < rows.Count; i++) + { + DrawEventRow(rows[i]); + } + EditorGUILayout.EndScrollView(); + EditorGUILayout.EndVertical(); + } + + private void DrawEventRow(EventRow row) + { + bool selected = _selectedEventType == row.EventType; + + EditorGUILayout.BeginVertical(EditorStyles.helpBox); + + GUIStyle buttonStyle = selected ? EditorStyles.miniButtonMid : EditorStyles.miniButton; + if (GUILayout.Button(row.EventType.FullName ?? row.EventType.Name, buttonStyle)) + { + _selectedEventType = row.EventType; + } + + string capacityText = row.Initialized ? row.Summary.Capacity.ToString() : row.InitialCapacity.ToString(); + string status = row.Initialized ? "已初始化" : "未初始化"; + EditorGUILayout.LabelField( + $"订阅 {row.Summary.SubscriberCount} | 峰值 {row.Summary.PeakSubscriberCount} | 容量 {capacityText} | 发布 {row.Summary.PublishCount}", + EditorStyles.miniLabel); + EditorGUILayout.LabelField(status, EditorStyles.miniLabel); + + EditorGUILayout.EndVertical(); + } + + private void DrawDetailPanel(Dictionary summaries) + { + EditorGUILayout.BeginVertical(); + + if (_selectedEventType == null) + { + EditorGUILayout.HelpBox("请先从左侧选择一个事件类型。", MessageType.Info); + EditorGUILayout.EndVertical(); + return; + } + + GUILayout.Label(_selectedEventType.FullName ?? _selectedEventType.Name, EditorStyles.boldLabel); + + if (!summaries.TryGetValue(_selectedEventType, out EventDebugSummary summary)) + { + int initialCapacity = GetInitialCapacity(_selectedEventType); + EditorGUILayout.HelpBox("这个事件类型在当前域中还没有被初始化。", MessageType.Info); + EditorGUILayout.LabelField("初始容量", initialCapacity.ToString()); + EditorGUILayout.LabelField("当前订阅数", "0"); + EditorGUILayout.LabelField("发布次数", "0"); + EditorGUILayout.EndVertical(); + return; + } + + EventDebugRegistry.TryGetDetails(_selectedEventType, out _, out EventDebugSubscriberInfo[] subscribers); + DrawSummary(summary); + DrawAlerts(summary, subscribers); + DrawSnapshotDiff(_selectedEventType, summary, subscribers); + DrawSubscribers(subscribers); + DrawHistory(); + + EditorGUILayout.EndVertical(); + } + + private static void DrawSummary(EventDebugSummary summary) + { + EditorGUILayout.BeginVertical(EditorStyles.helpBox); + GUILayout.Label("摘要", EditorStyles.boldLabel); + EditorGUILayout.LabelField("当前订阅数", summary.SubscriberCount.ToString()); + EditorGUILayout.LabelField("峰值订阅数", summary.PeakSubscriberCount.ToString()); + EditorGUILayout.LabelField("当前容量", summary.Capacity.ToString()); + EditorGUILayout.LabelField("发布次数", summary.PublishCount.ToString()); + EditorGUILayout.LabelField("订阅次数", summary.SubscribeCount.ToString()); + EditorGUILayout.LabelField("取消订阅次数", summary.UnsubscribeCount.ToString()); + EditorGUILayout.LabelField("扩容次数", summary.ResizeCount.ToString()); + EditorGUILayout.LabelField("清空次数", summary.ClearCount.ToString()); + EditorGUILayout.LabelField("最后操作帧", summary.LastOperationFrame.ToString()); + EditorGUILayout.LabelField("最后操作时间", FormatTicks(summary.LastOperationTicksUtc)); + EditorGUILayout.EndVertical(); + } + + private static void DrawAlerts(EventDebugSummary summary, EventDebugSubscriberInfo[] subscribers) + { + List alerts = BuildAlerts(summary, subscribers); + if (alerts.Count == 0) + { + return; + } + + EditorGUILayout.Space(4f); + GUILayout.Label("告警", EditorStyles.boldLabel); + for (int i = 0; i < alerts.Count; i++) + { + EditorGUILayout.HelpBox(alerts[i].Message, alerts[i].Type); + } + } + + private void DrawSnapshotDiff(Type eventType, EventDebugSummary currentSummary, EventDebugSubscriberInfo[] currentSubscribers) + { + if (_snapshotEntries.Count == 0) + { + return; + } + + EditorGUILayout.Space(4f); + GUILayout.Label("快照对比", EditorStyles.boldLabel); + + if (!_snapshotEntries.TryGetValue(eventType, out EventSnapshotEntry snapshot)) + { + EditorGUILayout.HelpBox("这个事件在拍摄快照时还不存在。", MessageType.Info); + return; + } + + int subscriberDelta = currentSummary.SubscriberCount - snapshot.Summary.SubscriberCount; + long publishDelta = currentSummary.PublishCount - snapshot.Summary.PublishCount; + long subscribeDelta = currentSummary.SubscribeCount - snapshot.Summary.SubscribeCount; + long unsubscribeDelta = currentSummary.UnsubscribeCount - snapshot.Summary.UnsubscribeCount; + int resizeDelta = currentSummary.ResizeCount - snapshot.Summary.ResizeCount; + int capacityDelta = currentSummary.Capacity - snapshot.Summary.Capacity; + + EditorGUILayout.BeginVertical(EditorStyles.helpBox); + EditorGUILayout.LabelField("订阅数变化", FormatSigned(subscriberDelta)); + EditorGUILayout.LabelField("发布次数变化", FormatSigned(publishDelta)); + EditorGUILayout.LabelField("订阅次数变化", FormatSigned(subscribeDelta)); + EditorGUILayout.LabelField("取消订阅次数变化", FormatSigned(unsubscribeDelta)); + EditorGUILayout.LabelField("扩容次数变化", FormatSigned(resizeDelta)); + EditorGUILayout.LabelField("容量变化", FormatSigned(capacityDelta)); + + List addedSubscribers = GetSubscriberDiff(currentSubscribers, snapshot.Subscribers); + List removedSubscribers = GetSubscriberDiff(snapshot.Subscribers, currentSubscribers); + + if (addedSubscribers.Count == 0 && removedSubscribers.Count == 0) + { + EditorGUILayout.LabelField("订阅者集合", "无变化"); + } + else + { + if (addedSubscribers.Count > 0) + { + EditorGUILayout.LabelField("新增", string.Join(" | ", addedSubscribers)); + } + + if (removedSubscribers.Count > 0) + { + EditorGUILayout.LabelField("移除", string.Join(" | ", removedSubscribers)); + } + } + + EditorGUILayout.EndVertical(); + } + + private void DrawSubscribers(EventDebugSubscriberInfo[] subscribers) + { + EditorGUILayout.Space(4f); + GUILayout.Label("订阅者", EditorStyles.boldLabel); + + if (subscribers.Length == 0) + { + EditorGUILayout.HelpBox("当前没有活跃订阅者。", MessageType.None); + return; + } + + _subscriberScroll = EditorGUILayout.BeginScrollView(_subscriberScroll, GUILayout.Height(position.height * 0.45f)); + for (int i = 0; i < subscribers.Length; i++) + { + DrawSubscriberRow(subscribers[i]); + } + EditorGUILayout.EndScrollView(); + } + + private void CaptureSnapshot() + { + _snapshotEntries.Clear(); + _snapshotTimeUtc = DateTime.UtcNow; + + EventDebugSummary[] summaries = EventDebugRegistry.GetSummaries(); + for (int i = 0; i < summaries.Length; i++) + { + EventDebugSummary summary = summaries[i]; + EventDebugRegistry.TryGetDetails(summary.EventType, out _, out EventDebugSubscriberInfo[] subscribers); + _snapshotEntries[summary.EventType] = new EventSnapshotEntry(summary, subscribers); + } + } + + private int CountChangedEvents() + { + int changedCount = 0; + EventDebugSummary[] summaries = EventDebugRegistry.GetSummaries(); + for (int i = 0; i < summaries.Length; i++) + { + EventDebugSummary summary = summaries[i]; + if (!_snapshotEntries.TryGetValue(summary.EventType, out EventSnapshotEntry snapshot)) + { + changedCount++; + continue; + } + + if (summary.SubscriberCount != snapshot.Summary.SubscriberCount || + summary.PublishCount != snapshot.Summary.PublishCount || + summary.SubscribeCount != snapshot.Summary.SubscribeCount || + summary.UnsubscribeCount != snapshot.Summary.UnsubscribeCount || + summary.ResizeCount != snapshot.Summary.ResizeCount || + summary.Capacity != snapshot.Summary.Capacity) + { + changedCount++; + } + } + + return changedCount; + } + + private static void DrawSubscriberRow(EventDebugSubscriberInfo subscriber) + { + EditorGUILayout.BeginVertical(EditorStyles.helpBox); + EditorGUILayout.LabelField($"{subscriber.DeclaringTypeName}.{subscriber.MethodName}", EditorStyles.boldLabel); + EditorGUILayout.LabelField("槽位", subscriber.HandlerIndex.ToString()); + EditorGUILayout.LabelField("版本", subscriber.Version.ToString()); + EditorGUILayout.LabelField("目标", subscriber.TargetTypeName); + EditorGUILayout.LabelField("类型", subscriber.IsStatic ? "静态方法" : "实例方法"); + + if (subscriber.IsUnityObjectDestroyed) + { + EditorGUILayout.HelpBox("Unity 目标对象已经被销毁,但委托仍然存在。", MessageType.Warning); + } + + if (subscriber.UnityTarget != null && !subscriber.IsUnityObjectDestroyed) + { + if (GUILayout.Button("定位目标", GUILayout.Width(90f))) + { + EditorGUIUtility.PingObject(subscriber.UnityTarget); + } + } + + EditorGUILayout.EndVertical(); + } + + private void DrawHistory() + { + EditorGUILayout.Space(4f); + GUILayout.Label("最近操作", EditorStyles.boldLabel); + + EventDebugOperationRecord[] history = EventDebugRegistry.GetRecentOperations(); + if (history.Length == 0) + { + EditorGUILayout.HelpBox("当前域里还没有记录到任何操作。", MessageType.None); + return; + } + + _historyScroll = EditorGUILayout.BeginScrollView(_historyScroll); + for (int i = 0; i < history.Length; i++) + { + EventDebugOperationRecord record = history[i]; + EditorGUILayout.BeginHorizontal(EditorStyles.helpBox); + GUILayout.Label(GetOperationKindText(record.OperationKind), GUILayout.Width(84f)); + GUILayout.Label(record.EventType.FullName ?? record.EventType.Name); + GUILayout.FlexibleSpace(); + GUILayout.Label($"订阅 {record.SubscriberCount}", GUILayout.Width(72f)); + GUILayout.Label($"容量 {record.Capacity}", GUILayout.Width(68f)); + GUILayout.Label(FormatTicks(record.TicksUtc), GUILayout.Width(92f)); + EditorGUILayout.EndHorizontal(); + } + EditorGUILayout.EndScrollView(); + } + + private static Dictionary BuildSummaryMap() + { + EventDebugSummary[] summaries = EventDebugRegistry.GetSummaries(); + Dictionary map = new(summaries.Length); + for (int i = 0; i < summaries.Length; i++) + { + map[summaries[i].EventType] = summaries[i]; + } + + return map; + } + + private List BuildRows(Dictionary summaries) + { + List rows = new(s_KnownEventTypes.Count); + for (int i = 0; i < s_KnownEventTypes.Count; i++) + { + Type eventType = s_KnownEventTypes[i]; + bool initialized = summaries.TryGetValue(eventType, out EventDebugSummary summary); + EventDebugSummary rowSummary = initialized + ? summary + : new EventDebugSummary(eventType, false, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0); + + EventRow row = new EventRow(eventType, initialized, rowSummary, GetInitialCapacity(eventType)); + if (MatchesFilter(row)) + { + rows.Add(row); + } + } + + rows.Sort(CompareRows); + return rows; + } + + private bool MatchesFilter(EventRow row) + { + if (_onlyInitialized && !row.Initialized) + { + return false; + } + + if (string.IsNullOrEmpty(_searchText)) + { + return true; + } + + string fullName = row.EventType.FullName ?? row.EventType.Name; + return fullName.IndexOf(_searchText, StringComparison.OrdinalIgnoreCase) >= 0; + } + + private void EnsureSelection(List rows) + { + if (_selectedEventType != null) + { + for (int i = 0; i < rows.Count; i++) + { + if (rows[i].EventType == _selectedEventType) + { + return; + } + } + } + + if (rows.Count > 0) + { + _selectedEventType = rows[0].EventType; + } + else + { + _selectedEventType = null; + } + } + + private static int CompareRows(EventRow x, EventRow y) + { + int initializedCompare = y.Initialized.CompareTo(x.Initialized); + if (initializedCompare != 0) + { + return initializedCompare; + } + + int activeCompare = y.Summary.SubscriberCount.CompareTo(x.Summary.SubscriberCount); + if (activeCompare != 0) + { + return activeCompare; + } + + int publishCompare = y.Summary.PublishCount.CompareTo(x.Summary.PublishCount); + if (publishCompare != 0) + { + return publishCompare; + } + + return string.CompareOrdinal(x.EventType.FullName, y.EventType.FullName); + } + + private static void RefreshKnownEventTypes() + { + s_KnownEventTypes.Clear(); + s_InitialCapacityCache.Clear(); + foreach (Type eventType in TypeCache.GetTypesDerivedFrom()) + { + if (!eventType.IsValueType || eventType.IsAbstract) + { + continue; + } + + s_KnownEventTypes.Add(eventType); + } + + s_KnownEventTypes.Sort((x, y) => string.CompareOrdinal(x.FullName, y.FullName)); + } + + private static int GetInitialCapacity(Type eventType) + { + if (s_InitialCapacityCache.TryGetValue(eventType, out int cachedCapacity)) + { + return cachedCapacity; + } + + try + { + Type initialSizeType = typeof(EventInitialSize<>).MakeGenericType(eventType); + FieldInfo sizeField = initialSizeType.GetField("Size", BindingFlags.Public | BindingFlags.Static); + if (sizeField != null && sizeField.GetValue(null) is int size) + { + s_InitialCapacityCache[eventType] = size; + return size; + } + } + catch + { + } + + s_InitialCapacityCache[eventType] = 0; + return 0; + } + + private static string FormatTicks(long ticksUtc) + { + if (ticksUtc <= 0) + { + return "-"; + } + + return new DateTime(ticksUtc, DateTimeKind.Utc).ToLocalTime().ToString("HH:mm:ss"); + } + + private static string FormatSigned(int value) + { + if (value > 0) + { + return $"+{value}"; + } + + return value.ToString(); + } + + private static string FormatSigned(long value) + { + if (value > 0) + { + return $"+{value}"; + } + + return value.ToString(); + } + + private static List BuildAlerts(EventDebugSummary summary, EventDebugSubscriberInfo[] subscribers) + { + List alerts = new(); + int destroyedTargetCount = 0; + for (int i = 0; i < subscribers.Length; i++) + { + if (subscribers[i].IsUnityObjectDestroyed) + { + destroyedTargetCount++; + } + } + + if (destroyedTargetCount > 0) + { + alerts.Add(new EventAlert(MessageType.Warning, $"发现 {destroyedTargetCount} 个订阅者的 Unity 目标对象已经被销毁。")); + } + + if (summary.ResizeCount > 0) + { + alerts.Add(new EventAlert(MessageType.Warning, $"容器已经扩容 {summary.ResizeCount} 次,当前事件的 Prewarm 可能偏小。")); + } + + if (summary.PeakSubscriberCount > 0 && summary.Capacity >= summary.PeakSubscriberCount * 4) + { + alerts.Add(new EventAlert(MessageType.Info, $"当前容量 {summary.Capacity} 远大于峰值订阅数 {summary.PeakSubscriberCount},Prewarm 可能偏大。")); + } + + long churn = summary.SubscribeCount + summary.UnsubscribeCount; + if (churn > 0 && summary.PublishCount == 0) + { + alerts.Add(new EventAlert(MessageType.Info, $"这个事件在当前域中发生了 {churn} 次订阅/取消订阅操作,但从未被发布。")); + } + else if (summary.PublishCount > 0 && churn > summary.PublishCount * 4) + { + alerts.Add(new EventAlert(MessageType.Info, $"检测到较高的订阅抖动:订阅变更 {churn} 次,发布次数 {summary.PublishCount} 次。")); + } + + if (summary.SubscriberCount > 0 && summary.PublishCount == 0 && summary.LastOperationFrame > 0) + { + alerts.Add(new EventAlert(MessageType.Info, "这个事件当前已有订阅者,但在当前域中还没有被发布过。")); + } + + return alerts; + } + + private static List GetSubscriberDiff(EventDebugSubscriberInfo[] source, EventDebugSubscriberInfo[] baseline) + { + Dictionary counts = BuildSubscriberCounts(source); + Dictionary baselineCounts = BuildSubscriberCounts(baseline); + List result = new(); + + foreach (KeyValuePair pair in counts) + { + baselineCounts.TryGetValue(pair.Key, out int baselineCount); + int delta = pair.Value - baselineCount; + if (delta <= 0) + { + continue; + } + + result.Add(delta == 1 ? pair.Key : $"{pair.Key} x{delta}"); + } + + return result; + } + + private static Dictionary BuildSubscriberCounts(EventDebugSubscriberInfo[] subscribers) + { + Dictionary counts = new(); + for (int i = 0; i < subscribers.Length; i++) + { + string key = BuildSubscriberKey(subscribers[i]); + counts.TryGetValue(key, out int count); + counts[key] = count + 1; + } + + return counts; + } + + private static string BuildSubscriberKey(EventDebugSubscriberInfo subscriber) + { + StringBuilder builder = new(); + builder.Append(subscriber.DeclaringTypeName); + builder.Append('.'); + builder.Append(subscriber.MethodName); + builder.Append(" -> "); + builder.Append(subscriber.TargetTypeName); + if (subscriber.IsStatic) + { + builder.Append(" [static]"); + } + + return builder.ToString(); + } + + private static string GetOperationKindText(EventDebugOperationKind kind) + { + return kind switch + { + EventDebugOperationKind.Subscribe => "订阅", + EventDebugOperationKind.Unsubscribe => "取消订阅", + EventDebugOperationKind.Publish => "发布", + EventDebugOperationKind.Resize => "扩容", + EventDebugOperationKind.Clear => "清空", + _ => kind.ToString() + }; + } + } +} diff --git a/Editor/Event/EventMonitorWindow.cs.meta b/Editor/Event/EventMonitorWindow.cs.meta new file mode 100644 index 0000000..81be625 --- /dev/null +++ b/Editor/Event/EventMonitorWindow.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: 9f62c982b4564f0b89a347090eec406c +timeCreated: 1774080001 diff --git a/Runtime/ABase/Event/EventContainer.cs b/Runtime/ABase/Event/EventContainer.cs index 1a810e8..d8dc94f 100644 --- a/Runtime/ABase/Event/EventContainer.cs +++ b/Runtime/ABase/Event/EventContainer.cs @@ -24,6 +24,10 @@ namespace AlicizaX private static int _activeCount; private static int _version; +#if UNITY_EDITOR + private static int _publishDepth; +#endif + #if Event_StrictCheck private static System.Collections.Generic.HashSet> _activeHandlers; #endif @@ -47,11 +51,19 @@ namespace AlicizaX #if Event_StrictCheck _activeHandlers = new System.Collections.Generic.HashSet>(); #endif + +#if UNITY_EDITOR + EventDebugRegistry.RegisterContainer(GetDebugSubscriberCount, GetDebugCapacity, GetDebugSubscribers); +#endif } [MethodImpl(MethodImplOptions.AggressiveInlining)] public static EventRuntimeHandle Subscribe(Action callback) { +#if UNITY_EDITOR + ThrowIfMutatingDuringPublish("subscribe"); +#endif + #if Event_StrictCheck if (_activeHandlers.Contains(callback)) { @@ -77,6 +89,10 @@ namespace AlicizaX _versions[handlerIndex] = version; _activeSlots[handlerIndex] = activeIndex; +#if UNITY_EDITOR + EventDebugRegistry.RecordSubscribe(_activeCount, _callbacks.Length); +#endif + return new EventRuntimeHandle(TypeId, handlerIndex, version); } @@ -101,12 +117,19 @@ namespace AlicizaX _freeSlots[_freeCount++] = i; } +#if UNITY_EDITOR + EventDebugRegistry.RecordResize(_activeCount, _callbacks.Length); +#endif + return _freeSlots[--_freeCount]; } [MethodImpl(MethodImplOptions.AggressiveInlining)] private static void Unsubscribe(int handlerIndex, int version) { +#if UNITY_EDITOR + ThrowIfMutatingDuringPublish("unsubscribe"); +#endif if (_versions[handlerIndex] != version) return; @@ -131,13 +154,25 @@ namespace AlicizaX } _freeSlots[_freeCount++] = handlerIndex; + +#if UNITY_EDITOR + EventDebugRegistry.RecordUnsubscribe(_activeCount, _callbacks.Length); +#endif } [MethodImpl(MethodImplOptions.AggressiveInlining)] public static void Publish(in TPayload payload) { +#if UNITY_EDITOR + _publishDepth++; + try + { +#endif int count = _activeCount; +#if UNITY_EDITOR + EventDebugRegistry.RecordPublish(count, _callbacks.Length); +#endif if (count == 0) return; int[] indices = _activeIndices; @@ -162,13 +197,25 @@ namespace AlicizaX switch (count - i) { case 3: - callbacks[indices[i + 2]](payload); - goto case 2; - case 2: + callbacks[indices[i]](payload); callbacks[indices[i + 1]](payload); - goto case 1; - case 1: callbacks[indices[i]](payload); break; + callbacks[indices[i + 2]](payload); + break; + case 2: + callbacks[indices[i]](payload); + callbacks[indices[i + 1]](payload); + break; + case 1: + callbacks[indices[i]](payload); + break; } +#if UNITY_EDITOR + } + finally + { + _publishDepth--; + } +#endif } public static int SubscriberCount @@ -179,6 +226,10 @@ namespace AlicizaX public static void EnsureCapacity(int capacity) { +#if UNITY_EDITOR + ThrowIfMutatingDuringPublish("ensure capacity"); +#endif + if (_callbacks.Length >= capacity) return; int oldLen = _callbacks.Length; @@ -192,10 +243,18 @@ namespace AlicizaX { _freeSlots[_freeCount++] = i; } + +#if UNITY_EDITOR + EventDebugRegistry.RecordResize(_activeCount, _callbacks.Length); +#endif } public static void Clear() { +#if UNITY_EDITOR + ThrowIfMutatingDuringPublish("clear"); +#endif + for (int i = 0; i < _activeCount; i++) { int idx = _activeIndices[i]; @@ -206,9 +265,69 @@ namespace AlicizaX _activeCount = 0; +#if UNITY_EDITOR + EventDebugRegistry.RecordClear(_activeCount, _callbacks.Length); +#endif + #if Event_StrictCheck _activeHandlers.Clear(); #endif } + +#if UNITY_EDITOR + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static void ThrowIfMutatingDuringPublish(string operation) + { + if (_publishDepth <= 0) return; + + throw new InvalidOperationException( + $"EventContainer<{typeof(TPayload).Name}> cannot {operation} while publishing. " + + "Supporting dispatch-time mutations would require slower publish semantics."); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static int GetDebugSubscriberCount() => _activeCount; + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static int GetDebugCapacity() => _callbacks.Length; + + private static EventDebugSubscriberInfo[] GetDebugSubscribers() + { + int count = _activeCount; + if (count == 0) + { + return Array.Empty(); + } + + EventDebugSubscriberInfo[] subscribers = new EventDebugSubscriberInfo[count]; + for (int i = 0; i < count; i++) + { + int handlerIndex = _activeIndices[i]; + Action callback = _callbacks[handlerIndex]; + object target = callback.Target; + bool isStatic = target == null; + bool isUnityObjectDestroyed = false; + UnityEngine.Object unityTarget = null; + + if (!isStatic && target is UnityEngine.Object engineObject) + { + unityTarget = engineObject; + isUnityObjectDestroyed = engineObject == null; + } + + subscribers[i] = new EventDebugSubscriberInfo( + handlerIndex, + _versions[handlerIndex], + callback.Method.DeclaringType?.FullName ?? "", + callback.Method.Name, + target?.GetType().FullName ?? "", + unityTarget, + isStatic, + isUnityObjectDestroyed); + } + + return subscribers; + } +#endif } } diff --git a/Runtime/ABase/Event/EventDebugRegistry.cs b/Runtime/ABase/Event/EventDebugRegistry.cs new file mode 100644 index 0000000..a32ba82 --- /dev/null +++ b/Runtime/ABase/Event/EventDebugRegistry.cs @@ -0,0 +1,319 @@ +#if UNITY_EDITOR +using System; +using System.Collections.Generic; +using UnityEngine; + +namespace AlicizaX +{ + internal enum EventDebugOperationKind : byte + { + Subscribe, + Unsubscribe, + Publish, + Resize, + Clear + } + + internal readonly struct EventDebugSummary + { + internal readonly Type EventType; + internal readonly bool Initialized; + internal readonly int SubscriberCount; + internal readonly int PeakSubscriberCount; + internal readonly int Capacity; + internal readonly long PublishCount; + internal readonly long SubscribeCount; + internal readonly long UnsubscribeCount; + internal readonly int ResizeCount; + internal readonly int ClearCount; + internal readonly int LastOperationFrame; + internal readonly long LastOperationTicksUtc; + + internal EventDebugSummary( + Type eventType, + bool initialized, + int subscriberCount, + int peakSubscriberCount, + int capacity, + long publishCount, + long subscribeCount, + long unsubscribeCount, + int resizeCount, + int clearCount, + int lastOperationFrame, + long lastOperationTicksUtc) + { + EventType = eventType; + Initialized = initialized; + SubscriberCount = subscriberCount; + PeakSubscriberCount = peakSubscriberCount; + Capacity = capacity; + PublishCount = publishCount; + SubscribeCount = subscribeCount; + UnsubscribeCount = unsubscribeCount; + ResizeCount = resizeCount; + ClearCount = clearCount; + LastOperationFrame = lastOperationFrame; + LastOperationTicksUtc = lastOperationTicksUtc; + } + } + + internal readonly struct EventDebugSubscriberInfo + { + internal readonly int HandlerIndex; + internal readonly int Version; + internal readonly string DeclaringTypeName; + internal readonly string MethodName; + internal readonly string TargetTypeName; + internal readonly UnityEngine.Object UnityTarget; + internal readonly bool IsStatic; + internal readonly bool IsUnityObjectDestroyed; + + internal EventDebugSubscriberInfo( + int handlerIndex, + int version, + string declaringTypeName, + string methodName, + string targetTypeName, + UnityEngine.Object unityTarget, + bool isStatic, + bool isUnityObjectDestroyed) + { + HandlerIndex = handlerIndex; + Version = version; + DeclaringTypeName = declaringTypeName; + MethodName = methodName; + TargetTypeName = targetTypeName; + UnityTarget = unityTarget; + IsStatic = isStatic; + IsUnityObjectDestroyed = isUnityObjectDestroyed; + } + } + + internal readonly struct EventDebugOperationRecord + { + internal readonly Type EventType; + internal readonly EventDebugOperationKind OperationKind; + internal readonly int Frame; + internal readonly long TicksUtc; + internal readonly int SubscriberCount; + internal readonly int Capacity; + + internal EventDebugOperationRecord( + Type eventType, + EventDebugOperationKind operationKind, + int frame, + long ticksUtc, + int subscriberCount, + int capacity) + { + EventType = eventType; + OperationKind = operationKind; + Frame = frame; + TicksUtc = ticksUtc; + SubscriberCount = subscriberCount; + Capacity = capacity; + } + } + + internal static class EventDebugRegistry + { + private const int HistoryCapacity = 128; + + private sealed class State + { + internal readonly Type EventType; + internal Func SubscriberCountProvider; + internal Func CapacityProvider; + internal Func SubscribersProvider; + + internal int PeakSubscriberCount; + internal long PublishCount; + internal long SubscribeCount; + internal long UnsubscribeCount; + internal int ResizeCount; + internal int ClearCount; + internal int LastOperationFrame; + internal long LastOperationTicksUtc; + + internal State(Type eventType) + { + EventType = eventType; + } + } + + private static readonly Dictionary _states = new(); + private static readonly List _registrationOrder = new(); + private static EventDebugOperationRecord[] _history = new EventDebugOperationRecord[HistoryCapacity]; + private static int _historyWriteIndex; + private static int _historyCount; + + internal static void RegisterContainer( + Func subscriberCountProvider, + Func capacityProvider, + Func subscribersProvider) + where T : struct, IEventArgs + { + Type eventType = typeof(T); + if (!_states.TryGetValue(eventType, out State state)) + { + state = new State(eventType); + _states.Add(eventType, state); + _registrationOrder.Add(eventType); + } + + state.SubscriberCountProvider = subscriberCountProvider; + state.CapacityProvider = capacityProvider; + state.SubscribersProvider = subscribersProvider; + state.PeakSubscriberCount = Math.Max(state.PeakSubscriberCount, subscriberCountProvider()); + } + + internal static void RecordSubscribe(int subscriberCount, int capacity) where T : struct, IEventArgs + { + State state = GetState(); + state.SubscribeCount++; + state.PeakSubscriberCount = Math.Max(state.PeakSubscriberCount, subscriberCount); + MarkOperation(state, EventDebugOperationKind.Subscribe, subscriberCount, capacity); + } + + internal static void RecordUnsubscribe(int subscriberCount, int capacity) where T : struct, IEventArgs + { + State state = GetState(); + state.UnsubscribeCount++; + MarkOperation(state, EventDebugOperationKind.Unsubscribe, subscriberCount, capacity); + } + + internal static void RecordPublish(int subscriberCount, int capacity) where T : struct, IEventArgs + { + State state = GetState(); + state.PublishCount++; + MarkOperation(state, EventDebugOperationKind.Publish, subscriberCount, capacity); + } + + internal static void RecordResize(int subscriberCount, int capacity) where T : struct, IEventArgs + { + State state = GetState(); + state.ResizeCount++; + MarkOperation(state, EventDebugOperationKind.Resize, subscriberCount, capacity); + } + + internal static void RecordClear(int subscriberCount, int capacity) where T : struct, IEventArgs + { + State state = GetState(); + state.ClearCount++; + MarkOperation(state, EventDebugOperationKind.Clear, subscriberCount, capacity); + } + + internal static EventDebugSummary[] GetSummaries() + { + int count = _registrationOrder.Count; + EventDebugSummary[] summaries = new EventDebugSummary[count]; + for (int i = 0; i < count; i++) + { + summaries[i] = BuildSummary(_states[_registrationOrder[i]]); + } + + return summaries; + } + + internal static bool TryGetDetails(Type eventType, out EventDebugSummary summary, out EventDebugSubscriberInfo[] subscribers) + { + if (_states.TryGetValue(eventType, out State state)) + { + summary = BuildSummary(state); + subscribers = state.SubscribersProvider?.Invoke() ?? Array.Empty(); + return true; + } + + summary = default; + subscribers = Array.Empty(); + return false; + } + + internal static EventDebugOperationRecord[] GetRecentOperations() + { + EventDebugOperationRecord[] result = new EventDebugOperationRecord[_historyCount]; + for (int i = 0; i < _historyCount; i++) + { + int index = (_historyWriteIndex - 1 - i + _history.Length) % _history.Length; + result[i] = _history[index]; + } + + return result; + } + + internal static void ResetStats() + { + foreach (Type eventType in _registrationOrder) + { + State state = _states[eventType]; + state.PeakSubscriberCount = state.SubscriberCountProvider?.Invoke() ?? 0; + state.PublishCount = 0; + state.SubscribeCount = 0; + state.UnsubscribeCount = 0; + state.ResizeCount = 0; + state.ClearCount = 0; + state.LastOperationFrame = 0; + state.LastOperationTicksUtc = 0; + } + + _historyWriteIndex = 0; + _historyCount = 0; + } + + private static State GetState() where T : struct, IEventArgs + { + Type eventType = typeof(T); + if (_states.TryGetValue(eventType, out State state)) + { + return state; + } + + throw new InvalidOperationException($"Event debug state is not registered for {eventType.FullName}."); + } + + private static EventDebugSummary BuildSummary(State state) + { + int subscriberCount = state.SubscriberCountProvider?.Invoke() ?? 0; + int capacity = state.CapacityProvider?.Invoke() ?? 0; + + return new EventDebugSummary( + state.EventType, + state.SubscriberCountProvider != null, + subscriberCount, + state.PeakSubscriberCount, + capacity, + state.PublishCount, + state.SubscribeCount, + state.UnsubscribeCount, + state.ResizeCount, + state.ClearCount, + state.LastOperationFrame, + state.LastOperationTicksUtc); + } + + private static void MarkOperation(State state, EventDebugOperationKind kind, int subscriberCount, int capacity) + { + int frame = Time.frameCount; + long ticksUtc = DateTime.UtcNow.Ticks; + + state.LastOperationFrame = frame; + state.LastOperationTicksUtc = ticksUtc; + + _history[_historyWriteIndex] = new EventDebugOperationRecord( + state.EventType, + kind, + frame, + ticksUtc, + subscriberCount, + capacity); + + _historyWriteIndex = (_historyWriteIndex + 1) % _history.Length; + if (_historyCount < _history.Length) + { + _historyCount++; + } + } + } +} +#endif diff --git a/Runtime/ABase/Event/EventDebugRegistry.cs.meta b/Runtime/ABase/Event/EventDebugRegistry.cs.meta new file mode 100644 index 0000000..2568b45 --- /dev/null +++ b/Runtime/ABase/Event/EventDebugRegistry.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: e6f1e983f1df4d6cb307f1c0ab0af6b3 +timeCreated: 1774080000 diff --git a/Runtime/ABase/Event/EventPublisher.cs b/Runtime/ABase/Event/EventPublisher.cs index 31a5f59..028dbab 100644 --- a/Runtime/ABase/Event/EventPublisher.cs +++ b/Runtime/ABase/Event/EventPublisher.cs @@ -1,4 +1,4 @@ -using System; + using System; using System.Runtime.CompilerServices; using Unity.IL2CPP.CompilerServices; @@ -24,17 +24,6 @@ namespace AlicizaX EventContainer.Publish(in evt); } - [Il2CppSetOption(Option.NullChecks, false)] - [Il2CppSetOption(Option.DivideByZeroChecks, false)] - [Il2CppSetOption(Option.ArrayBoundsChecks, false)] - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static void Publish(Action init) where T : struct, IEventArgs - { - var evt = default(T); - init(evt); - Publish(in evt); - } - [MethodImpl(MethodImplOptions.AggressiveInlining)] public static int GetSubscriberCount() where T : struct, IEventArgs {