907 lines
36 KiB
C#
907 lines
36 KiB
C#
#nullable enable
|
|
|
|
using System;
|
|
using System.Collections.Generic;
|
|
using System.Diagnostics;
|
|
using System.Globalization;
|
|
using System.IO;
|
|
using System.Linq;
|
|
using System.Text;
|
|
using System.Threading;
|
|
using UnityEditor;
|
|
using UnityEditor.UIElements;
|
|
using UnityEngine;
|
|
using UnityEngine.UIElements;
|
|
|
|
namespace ShrinkEventBus.Editor
|
|
{
|
|
public sealed class EventBusViewerWindow : EditorWindow
|
|
{
|
|
private sealed class BusRow
|
|
{
|
|
public string Key = string.Empty;
|
|
public string FilterKey = string.Empty;
|
|
public string Scheduler = string.Empty;
|
|
public string Mode = string.Empty;
|
|
public string Queue = string.Empty;
|
|
public string Overflow = string.Empty;
|
|
public int Subscribers;
|
|
}
|
|
|
|
private sealed class EventRow
|
|
{
|
|
public long Sequence;
|
|
public DateTime TimestampUtc;
|
|
public string Bus = string.Empty;
|
|
public string EventType = string.Empty;
|
|
public string ShortEventType = string.Empty;
|
|
public string Scheduler = string.Empty;
|
|
public string Mode = string.Empty;
|
|
public int ThreadId;
|
|
public double DurationMicroseconds;
|
|
public bool IsAsync;
|
|
public bool Accepted;
|
|
public bool Handled;
|
|
public bool Canceled;
|
|
public ShrinkPostFailure Failure;
|
|
|
|
public string Status => Failure != ShrinkPostFailure.None
|
|
? FormatFailure(Failure)
|
|
: Canceled
|
|
? "已取消"
|
|
: Handled
|
|
? "已处理"
|
|
: "无处理器";
|
|
}
|
|
|
|
private const int MaxEvents = 5000;
|
|
private const int MaxPendingEvents = 20000;
|
|
private static readonly Color SuccessColor = new(0.32f, 0.72f, 0.48f);
|
|
private static readonly Color WarningColor = new(0.94f, 0.68f, 0.24f);
|
|
private static readonly Color ErrorColor = new(0.95f, 0.36f, 0.34f);
|
|
private static readonly Color MutedColor = new(0.58f, 0.61f, 0.66f);
|
|
|
|
private readonly object _eventGate = new();
|
|
private readonly Queue<EventRow> _pendingEvents = new();
|
|
private readonly List<EventRow> _events = new();
|
|
private readonly List<EventRow> _filteredEvents = new();
|
|
private readonly List<BusRow> _buses = new();
|
|
|
|
private ListView? _busList;
|
|
private ListView? _eventList;
|
|
private Label? _eventEmptyState;
|
|
private ToolbarSearchField? _search;
|
|
private ToolbarToggle? _captureToggle;
|
|
private ToolbarToggle? _problemOnlyToggle;
|
|
private ToolbarToggle? _autoScrollToggle;
|
|
private Label? _captureState;
|
|
private Label? _status;
|
|
private Label? _busDescription;
|
|
private Label? _busCountMetric;
|
|
private Label? _handlerCountMetric;
|
|
private Label? _capturedMetric;
|
|
private Label? _rateMetric;
|
|
private Label? _latencyMetric;
|
|
private Label? _problemMetric;
|
|
private Label? _detailTitle;
|
|
private Label? _detailTime;
|
|
private Label? _detailBus;
|
|
private Label? _detailExecution;
|
|
private Label? _detailThread;
|
|
private Label? _detailDuration;
|
|
private Label? _detailResult;
|
|
|
|
private string _selectedBus = string.Empty;
|
|
private long _selectedEventSequence;
|
|
private long _nextSequence;
|
|
private long _droppedCaptureCount;
|
|
private double _nextRefresh;
|
|
private bool _captureRequested = true;
|
|
private bool _diagnosticsSubscribed;
|
|
|
|
[MenuItem("ShrinkSDK/事件总线/事件查看器")]
|
|
private static void Open()
|
|
{
|
|
var window = GetWindow<EventBusViewerWindow>();
|
|
window.titleContent = new GUIContent("事件总线 2.0");
|
|
window.minSize = new Vector2(980f, 560f);
|
|
window.Show();
|
|
}
|
|
|
|
private void OnEnable()
|
|
{
|
|
SetDiagnosticsSubscription(_captureRequested);
|
|
EditorApplication.update += Tick;
|
|
}
|
|
|
|
private void OnDisable()
|
|
{
|
|
SetDiagnosticsSubscription(false);
|
|
EditorApplication.update -= Tick;
|
|
}
|
|
|
|
public void CreateGUI()
|
|
{
|
|
rootVisualElement.Clear();
|
|
rootVisualElement.style.flexDirection = FlexDirection.Column;
|
|
rootVisualElement.style.backgroundColor = EditorGUIUtility.isProSkin
|
|
? new Color(0.105f, 0.112f, 0.125f)
|
|
: new Color(0.88f, 0.89f, 0.91f);
|
|
|
|
rootVisualElement.Add(BuildToolbar());
|
|
rootVisualElement.Add(BuildMetricsBand());
|
|
|
|
var mainSplit = new TwoPaneSplitView(0, 330f, TwoPaneSplitViewOrientation.Horizontal);
|
|
mainSplit.style.flexGrow = 1f;
|
|
mainSplit.Add(BuildBusPane());
|
|
mainSplit.Add(BuildEventWorkspace());
|
|
rootVisualElement.Add(mainSplit);
|
|
|
|
_status = new Label();
|
|
_status.style.height = 24f;
|
|
_status.style.paddingLeft = 10f;
|
|
_status.style.paddingTop = 4f;
|
|
_status.style.borderTopWidth = 1f;
|
|
_status.style.borderTopColor = BorderColor();
|
|
_status.style.color = MutedColor;
|
|
rootVisualElement.Add(_status);
|
|
|
|
RefreshAll();
|
|
}
|
|
|
|
private VisualElement BuildToolbar()
|
|
{
|
|
var toolbar = new Toolbar();
|
|
toolbar.style.height = 34f;
|
|
|
|
var title = new Label("ShrinkEventBus");
|
|
title.style.unityFontStyleAndWeight = FontStyle.Bold;
|
|
title.style.fontSize = 13f;
|
|
title.style.marginLeft = 4f;
|
|
title.style.marginRight = 8f;
|
|
toolbar.Add(title);
|
|
|
|
_captureState = new Label();
|
|
_captureState.style.minWidth = 48f;
|
|
_captureState.style.height = 18f;
|
|
_captureState.style.marginRight = 10f;
|
|
_captureState.style.paddingLeft = 6f;
|
|
_captureState.style.paddingRight = 6f;
|
|
_captureState.style.unityTextAlign = TextAnchor.MiddleCenter;
|
|
_captureState.style.unityFontStyleAndWeight = FontStyle.Bold;
|
|
_captureState.style.fontSize = 9f;
|
|
toolbar.Add(_captureState);
|
|
|
|
_search = new ToolbarSearchField { tooltip = "按总线标识或事件类型筛选" };
|
|
_search.style.width = 260f;
|
|
_search.RegisterValueChangedCallback(_ => ApplyFilter());
|
|
toolbar.Add(_search);
|
|
|
|
_problemOnlyToggle = new ToolbarToggle { text = "仅问题" };
|
|
_problemOnlyToggle.tooltip = "只显示已取消、已拒绝或失败的发布";
|
|
_problemOnlyToggle.RegisterValueChangedCallback(_ => ApplyFilter());
|
|
toolbar.Add(_problemOnlyToggle);
|
|
|
|
_autoScrollToggle = new ToolbarToggle { text = "跟随", value = true };
|
|
_autoScrollToggle.tooltip = "自动保持最新事件可见";
|
|
toolbar.Add(_autoScrollToggle);
|
|
|
|
_captureToggle = new ToolbarToggle { text = "捕获", value = _captureRequested };
|
|
_captureToggle.tooltip = "启用详细分发采样";
|
|
_captureToggle.RegisterValueChangedCallback(evt =>
|
|
{
|
|
_captureRequested = evt.newValue;
|
|
SetDiagnosticsSubscription(evt.newValue);
|
|
UpdateCaptureState();
|
|
});
|
|
toolbar.Add(_captureToggle);
|
|
|
|
var spacer = new VisualElement();
|
|
spacer.style.flexGrow = 1f;
|
|
toolbar.Add(spacer);
|
|
|
|
toolbar.Add(new ToolbarButton(RefreshAll) { text = "刷新", tooltip = "刷新总线状态" });
|
|
toolbar.Add(new ToolbarButton(ExportCsv) { text = "导出", tooltip = "将当前可见事件导出为 CSV" });
|
|
toolbar.Add(new ToolbarButton(ClearEvents) { text = "清空", tooltip = "清空已捕获事件" });
|
|
|
|
UpdateCaptureState();
|
|
return toolbar;
|
|
}
|
|
|
|
private VisualElement BuildMetricsBand()
|
|
{
|
|
var band = new VisualElement();
|
|
band.style.height = 58f;
|
|
band.style.flexDirection = FlexDirection.Row;
|
|
band.style.borderBottomWidth = 1f;
|
|
band.style.borderBottomColor = BorderColor();
|
|
band.style.backgroundColor = PanelColor();
|
|
|
|
_busCountMetric = AddMetric(band, "总线");
|
|
_handlerCountMetric = AddMetric(band, "处理器");
|
|
_capturedMetric = AddMetric(band, "已捕获");
|
|
_rateMetric = AddMetric(band, "事件 / 秒");
|
|
_latencyMetric = AddMetric(band, "平均分发耗时");
|
|
_problemMetric = AddMetric(band, "问题");
|
|
return band;
|
|
}
|
|
|
|
private static Label AddMetric(VisualElement parent, string caption)
|
|
{
|
|
var block = new VisualElement();
|
|
block.style.flexGrow = 1f;
|
|
block.style.flexBasis = 0f;
|
|
block.style.paddingLeft = 12f;
|
|
block.style.paddingTop = 6f;
|
|
block.style.borderRightWidth = 1f;
|
|
block.style.borderRightColor = BorderColor();
|
|
|
|
var captionLabel = new Label(caption);
|
|
captionLabel.style.fontSize = 9f;
|
|
captionLabel.style.color = MutedColor;
|
|
block.Add(captionLabel);
|
|
|
|
var value = new Label("0");
|
|
value.style.fontSize = 17f;
|
|
value.style.unityFontStyleAndWeight = FontStyle.Bold;
|
|
value.style.marginTop = 1f;
|
|
block.Add(value);
|
|
parent.Add(block);
|
|
return value;
|
|
}
|
|
|
|
private VisualElement BuildBusPane()
|
|
{
|
|
var pane = new VisualElement();
|
|
pane.style.flexGrow = 1f;
|
|
pane.style.backgroundColor = PanelColor();
|
|
pane.Add(BuildBusHeader());
|
|
|
|
_busList = new ListView(_buses, 30f, MakeBusRow, BindBusRow)
|
|
{
|
|
selectionType = SelectionType.Single,
|
|
showAlternatingRowBackgrounds = AlternatingRowBackground.ContentOnly,
|
|
virtualizationMethod = CollectionVirtualizationMethod.FixedHeight
|
|
};
|
|
_busList.style.flexGrow = 1f;
|
|
_busList.selectionChanged += selection =>
|
|
{
|
|
var selected = selection.OfType<BusRow>().FirstOrDefault();
|
|
if (selected == null)
|
|
return;
|
|
_selectedBus = selected.FilterKey;
|
|
UpdateBusDescription(selected);
|
|
ApplyFilter();
|
|
};
|
|
pane.Add(_busList);
|
|
|
|
_busDescription = new Label();
|
|
_busDescription.style.minHeight = 48f;
|
|
_busDescription.style.paddingLeft = 10f;
|
|
_busDescription.style.paddingRight = 8f;
|
|
_busDescription.style.paddingTop = 7f;
|
|
_busDescription.style.paddingBottom = 6f;
|
|
_busDescription.style.borderTopWidth = 1f;
|
|
_busDescription.style.borderTopColor = BorderColor();
|
|
_busDescription.style.color = MutedColor;
|
|
_busDescription.style.whiteSpace = WhiteSpace.Normal;
|
|
pane.Add(_busDescription);
|
|
return pane;
|
|
}
|
|
|
|
private VisualElement BuildEventWorkspace()
|
|
{
|
|
var split = new TwoPaneSplitView(1, 185f, TwoPaneSplitViewOrientation.Vertical);
|
|
split.style.flexGrow = 1f;
|
|
split.Add(BuildEventPane());
|
|
split.Add(BuildDetailPane());
|
|
return split;
|
|
}
|
|
|
|
private VisualElement BuildEventPane()
|
|
{
|
|
var pane = new VisualElement();
|
|
pane.style.flexGrow = 1f;
|
|
pane.Add(BuildEventHeader());
|
|
|
|
_eventList = new ListView(_filteredEvents, 28f, MakeEventRow, BindEventRow)
|
|
{
|
|
selectionType = SelectionType.Single,
|
|
showAlternatingRowBackgrounds = AlternatingRowBackground.ContentOnly,
|
|
virtualizationMethod = CollectionVirtualizationMethod.FixedHeight
|
|
};
|
|
_eventList.style.flexGrow = 1f;
|
|
_eventList.selectionChanged += selection =>
|
|
{
|
|
var selected = selection.OfType<EventRow>().FirstOrDefault();
|
|
if (selected == null)
|
|
return;
|
|
_selectedEventSequence = selected.Sequence;
|
|
ShowEventDetails(selected);
|
|
};
|
|
pane.Add(_eventList);
|
|
|
|
_eventEmptyState = new Label("暂无事件");
|
|
_eventEmptyState.style.flexGrow = 1f;
|
|
_eventEmptyState.style.unityTextAlign = TextAnchor.MiddleCenter;
|
|
_eventEmptyState.style.color = MutedColor;
|
|
pane.Add(_eventEmptyState);
|
|
return pane;
|
|
}
|
|
|
|
private VisualElement BuildDetailPane()
|
|
{
|
|
var pane = new VisualElement();
|
|
pane.style.flexGrow = 1f;
|
|
pane.style.backgroundColor = PanelColor();
|
|
pane.style.borderTopWidth = 1f;
|
|
pane.style.borderTopColor = BorderColor();
|
|
|
|
_detailTitle = new Label("未选择事件");
|
|
_detailTitle.style.fontSize = 13f;
|
|
_detailTitle.style.unityFontStyleAndWeight = FontStyle.Bold;
|
|
_detailTitle.style.marginLeft = 12f;
|
|
_detailTitle.style.marginTop = 9f;
|
|
_detailTitle.style.marginBottom = 7f;
|
|
pane.Add(_detailTitle);
|
|
|
|
var grid = new VisualElement();
|
|
grid.style.flexDirection = FlexDirection.Row;
|
|
grid.style.flexWrap = Wrap.Wrap;
|
|
grid.style.paddingLeft = 8f;
|
|
grid.style.paddingRight = 8f;
|
|
_detailTime = AddDetailField(grid, "时间");
|
|
_detailBus = AddDetailField(grid, "总线");
|
|
_detailExecution = AddDetailField(grid, "执行方式");
|
|
_detailThread = AddDetailField(grid, "线程");
|
|
_detailDuration = AddDetailField(grid, "分发耗时");
|
|
_detailResult = AddDetailField(grid, "结果");
|
|
pane.Add(grid);
|
|
return pane;
|
|
}
|
|
|
|
private static Label AddDetailField(VisualElement grid, string caption)
|
|
{
|
|
var field = new VisualElement();
|
|
field.style.width = Length.Percent(33.333f);
|
|
field.style.minWidth = 190f;
|
|
field.style.paddingLeft = 5f;
|
|
field.style.paddingRight = 5f;
|
|
field.style.paddingBottom = 8f;
|
|
|
|
var captionLabel = new Label(caption);
|
|
captionLabel.style.fontSize = 9f;
|
|
captionLabel.style.color = MutedColor;
|
|
field.Add(captionLabel);
|
|
|
|
var value = new Label("-");
|
|
value.style.marginTop = 2f;
|
|
value.style.overflow = Overflow.Hidden;
|
|
value.style.whiteSpace = WhiteSpace.Normal;
|
|
field.Add(value);
|
|
grid.Add(field);
|
|
return value;
|
|
}
|
|
|
|
private static VisualElement BuildBusHeader()
|
|
{
|
|
var header = BuildHeaderContainer("总线");
|
|
var columns = BuildColumnRow();
|
|
columns.Add(MakeHeaderCell("标识", 1f));
|
|
columns.Add(MakeHeaderCell("调度器", 0f, 92f));
|
|
columns.Add(MakeHeaderCell("处理器", 0f, 58f, TextAnchor.MiddleRight));
|
|
header.Add(columns);
|
|
return header;
|
|
}
|
|
|
|
private static VisualElement BuildEventHeader()
|
|
{
|
|
var header = BuildHeaderContainer("事件流");
|
|
var columns = BuildColumnRow();
|
|
columns.Add(MakeHeaderCell("时间", 0f, 92f));
|
|
columns.Add(MakeHeaderCell("总线", 0f, 132f));
|
|
columns.Add(MakeHeaderCell("事件", 1f));
|
|
columns.Add(MakeHeaderCell("方式", 0f, 54f));
|
|
columns.Add(MakeHeaderCell("线程", 0f, 56f, TextAnchor.MiddleRight));
|
|
columns.Add(MakeHeaderCell("耗时", 0f, 82f, TextAnchor.MiddleRight));
|
|
columns.Add(MakeHeaderCell("结果", 0f, 90f));
|
|
header.Add(columns);
|
|
return header;
|
|
}
|
|
|
|
private static VisualElement BuildHeaderContainer(string title)
|
|
{
|
|
var header = new VisualElement();
|
|
header.style.height = 52f;
|
|
header.style.borderBottomWidth = 1f;
|
|
header.style.borderBottomColor = BorderColor();
|
|
header.style.backgroundColor = HeaderColor();
|
|
var titleLabel = new Label(title);
|
|
titleLabel.style.height = 27f;
|
|
titleLabel.style.paddingLeft = 10f;
|
|
titleLabel.style.paddingTop = 7f;
|
|
titleLabel.style.unityFontStyleAndWeight = FontStyle.Bold;
|
|
titleLabel.style.fontSize = 10f;
|
|
header.Add(titleLabel);
|
|
return header;
|
|
}
|
|
|
|
private static VisualElement BuildColumnRow()
|
|
{
|
|
var row = new VisualElement();
|
|
row.style.height = 24f;
|
|
row.style.flexDirection = FlexDirection.Row;
|
|
row.style.alignItems = Align.Center;
|
|
return row;
|
|
}
|
|
|
|
private static Label MakeHeaderCell(string text, float grow, float width = 0f,
|
|
TextAnchor alignment = TextAnchor.MiddleLeft)
|
|
{
|
|
var label = MakeCell(string.Empty, grow, width, alignment);
|
|
label.text = text;
|
|
label.style.fontSize = 9f;
|
|
label.style.color = MutedColor;
|
|
return label;
|
|
}
|
|
|
|
private static VisualElement MakeBusRow()
|
|
{
|
|
var row = new VisualElement { name = "bus-row" };
|
|
row.style.flexDirection = FlexDirection.Row;
|
|
row.style.alignItems = Align.Center;
|
|
row.Add(MakeCell("key", 1f));
|
|
row.Add(MakeCell("scheduler", 0f, 92f));
|
|
row.Add(MakeCell("handlers", 0f, 58f, TextAnchor.MiddleRight));
|
|
return row;
|
|
}
|
|
|
|
private void BindBusRow(VisualElement element, int index)
|
|
{
|
|
if (index < 0 || index >= _buses.Count)
|
|
return;
|
|
var item = _buses[index];
|
|
element.Q<Label>("key").text = item.Key;
|
|
element.Q<Label>("scheduler").text = item.Scheduler;
|
|
element.Q<Label>("handlers").text = item.Subscribers.ToString(CultureInfo.InvariantCulture);
|
|
element.tooltip = item.FilterKey.Length == 0
|
|
? "汇总视图"
|
|
: $"{item.Mode} | 队列 {item.Queue} | 溢出策略 {item.Overflow}";
|
|
}
|
|
|
|
private static VisualElement MakeEventRow()
|
|
{
|
|
var row = new VisualElement { name = "event-row" };
|
|
row.style.flexDirection = FlexDirection.Row;
|
|
row.style.alignItems = Align.Center;
|
|
row.Add(MakeCell("time", 0f, 92f));
|
|
row.Add(MakeCell("bus", 0f, 132f));
|
|
row.Add(MakeCell("type", 1f));
|
|
row.Add(MakeCell("kind", 0f, 54f));
|
|
row.Add(MakeCell("thread", 0f, 56f, TextAnchor.MiddleRight));
|
|
row.Add(MakeCell("latency", 0f, 82f, TextAnchor.MiddleRight));
|
|
row.Add(MakeCell("status", 0f, 90f));
|
|
return row;
|
|
}
|
|
|
|
private void BindEventRow(VisualElement element, int index)
|
|
{
|
|
if (index < 0 || index >= _filteredEvents.Count)
|
|
return;
|
|
var item = _filteredEvents[index];
|
|
element.Q<Label>("time").text = item.TimestampUtc.ToLocalTime().ToString("HH:mm:ss.fff");
|
|
element.Q<Label>("bus").text = item.Bus;
|
|
element.Q<Label>("type").text = item.ShortEventType;
|
|
element.Q<Label>("kind").text = item.IsAsync ? "异步" : "同步";
|
|
element.Q<Label>("thread").text = "线程 " + item.ThreadId;
|
|
element.Q<Label>("latency").text = FormatDuration(item.DurationMicroseconds);
|
|
var status = element.Q<Label>("status");
|
|
status.text = item.Status;
|
|
status.style.color = item.Failure != ShrinkPostFailure.None
|
|
? ErrorColor
|
|
: item.Canceled
|
|
? WarningColor
|
|
: item.Handled
|
|
? SuccessColor
|
|
: MutedColor;
|
|
element.tooltip = $"{item.EventType}\n{item.Scheduler} / {item.Mode} / 线程 {item.ThreadId}";
|
|
}
|
|
|
|
private static Label MakeCell(string name, float grow, float width = 0f,
|
|
TextAnchor alignment = TextAnchor.MiddleLeft)
|
|
{
|
|
var label = new Label { name = name };
|
|
label.style.flexGrow = grow;
|
|
label.style.minWidth = 0f;
|
|
label.style.paddingLeft = 6f;
|
|
label.style.paddingRight = 4f;
|
|
label.style.unityTextAlign = alignment;
|
|
label.style.overflow = Overflow.Hidden;
|
|
label.style.whiteSpace = WhiteSpace.NoWrap;
|
|
if (width > 0f)
|
|
{
|
|
label.style.width = width;
|
|
label.style.flexShrink = 0f;
|
|
}
|
|
else
|
|
{
|
|
label.style.flexShrink = 1f;
|
|
}
|
|
return label;
|
|
}
|
|
|
|
private void RecordEvent(ShrinkEventTrace trace)
|
|
{
|
|
var row = new EventRow
|
|
{
|
|
Sequence = Interlocked.Increment(ref _nextSequence),
|
|
TimestampUtc = trace.TimestampUtc,
|
|
Bus = trace.BusKey.Id,
|
|
EventType = trace.EventType.FullName ?? trace.EventType.Name,
|
|
ShortEventType = trace.EventType.Name,
|
|
Scheduler = FormatScheduler(trace.Scheduler),
|
|
Mode = FormatDispatchMode(trace.DispatchMode),
|
|
ThreadId = trace.ThreadId,
|
|
DurationMicroseconds = trace.ElapsedTimestampTicks * (1_000_000d / Stopwatch.Frequency),
|
|
IsAsync = trace.IsAsync,
|
|
Accepted = trace.Result.Accepted,
|
|
Handled = trace.Result.Handled,
|
|
Canceled = trace.Result.Canceled,
|
|
Failure = trace.Result.Failure
|
|
};
|
|
|
|
lock (_eventGate)
|
|
{
|
|
if (_pendingEvents.Count >= MaxPendingEvents)
|
|
{
|
|
Interlocked.Increment(ref _droppedCaptureCount);
|
|
return;
|
|
}
|
|
_pendingEvents.Enqueue(row);
|
|
}
|
|
}
|
|
|
|
private void Tick()
|
|
{
|
|
if (EditorApplication.timeSinceStartup < _nextRefresh)
|
|
return;
|
|
_nextRefresh = EditorApplication.timeSinceStartup + 0.15;
|
|
DrainEvents();
|
|
RefreshBuses();
|
|
ApplyFilter();
|
|
}
|
|
|
|
private void RefreshAll()
|
|
{
|
|
DrainEvents();
|
|
RefreshBuses();
|
|
ApplyFilter();
|
|
}
|
|
|
|
private void RefreshBuses()
|
|
{
|
|
var snapshot = EventBus.Snapshot()
|
|
.OrderBy(item => item.Key.Id, StringComparer.Ordinal)
|
|
.ToArray();
|
|
var totalSubscribers = snapshot.Sum(item => item.Subscribers);
|
|
|
|
_buses.Clear();
|
|
_buses.Add(new BusRow
|
|
{
|
|
Key = "全部总线",
|
|
FilterKey = string.Empty,
|
|
Scheduler = "混合",
|
|
Mode = "混合",
|
|
Queue = "-",
|
|
Overflow = "-",
|
|
Subscribers = totalSubscribers
|
|
});
|
|
foreach (var bus in snapshot)
|
|
{
|
|
_buses.Add(new BusRow
|
|
{
|
|
Key = bus.Key.Id,
|
|
FilterKey = bus.Key.Id,
|
|
Scheduler = FormatScheduler(bus.Options.Scheduler),
|
|
Mode = FormatDispatchMode(bus.Options.DispatchMode),
|
|
Queue = bus.Options.QueueCapacity.ToString(CultureInfo.InvariantCulture),
|
|
Overflow = FormatOverflowPolicy(bus.Options.OverflowPolicy),
|
|
Subscribers = bus.Subscribers
|
|
});
|
|
}
|
|
|
|
_busList?.RefreshItems();
|
|
var selectedIndex = Math.Max(0, _buses.FindIndex(item => item.FilterKey == _selectedBus));
|
|
if (_busList != null && _busList.selectedIndex != selectedIndex)
|
|
_busList.selectedIndex = selectedIndex;
|
|
if (selectedIndex < _buses.Count)
|
|
UpdateBusDescription(_buses[selectedIndex]);
|
|
|
|
if (_busCountMetric != null)
|
|
_busCountMetric.text = snapshot.Length.ToString(CultureInfo.InvariantCulture);
|
|
if (_handlerCountMetric != null)
|
|
_handlerCountMetric.text = totalSubscribers.ToString("N0", CultureInfo.InvariantCulture);
|
|
}
|
|
|
|
private void DrainEvents()
|
|
{
|
|
lock (_eventGate)
|
|
{
|
|
while (_pendingEvents.Count > 0)
|
|
_events.Add(_pendingEvents.Dequeue());
|
|
}
|
|
if (_events.Count > MaxEvents)
|
|
_events.RemoveRange(0, _events.Count - MaxEvents);
|
|
}
|
|
|
|
private void ApplyFilter()
|
|
{
|
|
var query = _search?.value?.Trim() ?? string.Empty;
|
|
var problemOnly = _problemOnlyToggle?.value == true;
|
|
_filteredEvents.Clear();
|
|
for (var i = _events.Count - 1; i >= 0; i--)
|
|
{
|
|
var item = _events[i];
|
|
if (_selectedBus.Length > 0 && !string.Equals(item.Bus, _selectedBus, StringComparison.Ordinal))
|
|
continue;
|
|
if (problemOnly && item.Failure == ShrinkPostFailure.None && !item.Canceled && item.Accepted)
|
|
continue;
|
|
if (query.Length > 0 &&
|
|
item.Bus.IndexOf(query, StringComparison.OrdinalIgnoreCase) < 0 &&
|
|
item.EventType.IndexOf(query, StringComparison.OrdinalIgnoreCase) < 0)
|
|
continue;
|
|
_filteredEvents.Add(item);
|
|
}
|
|
|
|
if (_eventList != null)
|
|
{
|
|
var hasVisibleEvents = _filteredEvents.Count > 0;
|
|
_eventList.style.display = hasVisibleEvents ? DisplayStyle.Flex : DisplayStyle.None;
|
|
if (hasVisibleEvents)
|
|
_eventList.RefreshItems();
|
|
}
|
|
if (_eventEmptyState != null)
|
|
_eventEmptyState.style.display = _filteredEvents.Count == 0
|
|
? DisplayStyle.Flex
|
|
: DisplayStyle.None;
|
|
RestoreEventSelection();
|
|
UpdateMetrics();
|
|
UpdateStatus();
|
|
if (_autoScrollToggle?.value == true && _filteredEvents.Count > 0 && _selectedEventSequence == 0)
|
|
_eventList?.ScrollToItem(0);
|
|
}
|
|
|
|
private void RestoreEventSelection()
|
|
{
|
|
if (_eventList == null || _selectedEventSequence == 0)
|
|
return;
|
|
var index = _filteredEvents.FindIndex(item => item.Sequence == _selectedEventSequence);
|
|
if (index < 0)
|
|
{
|
|
_selectedEventSequence = 0;
|
|
_eventList.ClearSelection();
|
|
ClearEventDetails();
|
|
return;
|
|
}
|
|
if (_eventList.selectedIndex != index)
|
|
_eventList.selectedIndex = index;
|
|
}
|
|
|
|
private void UpdateMetrics()
|
|
{
|
|
var now = DateTime.UtcNow;
|
|
var recent = 0;
|
|
var problems = 0;
|
|
var duration = 0d;
|
|
for (var i = 0; i < _filteredEvents.Count; i++)
|
|
{
|
|
var item = _filteredEvents[i];
|
|
if ((now - item.TimestampUtc).TotalSeconds <= 1d)
|
|
recent++;
|
|
if (item.Failure != ShrinkPostFailure.None || item.Canceled || !item.Accepted)
|
|
problems++;
|
|
duration += item.DurationMicroseconds;
|
|
}
|
|
|
|
if (_capturedMetric != null)
|
|
_capturedMetric.text = _events.Count.ToString("N0", CultureInfo.InvariantCulture);
|
|
if (_rateMetric != null)
|
|
_rateMetric.text = recent.ToString("N0", CultureInfo.InvariantCulture);
|
|
if (_latencyMetric != null)
|
|
_latencyMetric.text = _filteredEvents.Count == 0
|
|
? "0 微秒"
|
|
: FormatDuration(duration / _filteredEvents.Count);
|
|
if (_problemMetric != null)
|
|
{
|
|
_problemMetric.text = problems.ToString("N0", CultureInfo.InvariantCulture);
|
|
_problemMetric.style.color = problems > 0 ? ErrorColor : SuccessColor;
|
|
}
|
|
}
|
|
|
|
private void UpdateStatus()
|
|
{
|
|
if (_status == null)
|
|
return;
|
|
var dropped = Interlocked.Read(ref _droppedCaptureCount);
|
|
var scope = _selectedBus.Length == 0 ? "全部总线" : _selectedBus;
|
|
_status.text = $"可见 {_filteredEvents.Count:N0} 条 / 保留 {_events.Count:N0} 条 | {scope} | 上限 {MaxEvents:N0} 条" +
|
|
(dropped > 0 ? $" | 捕获溢出 {dropped:N0} 条" : string.Empty);
|
|
}
|
|
|
|
private void UpdateBusDescription(BusRow row)
|
|
{
|
|
if (_busDescription == null)
|
|
return;
|
|
_busDescription.text = row.FilterKey.Length == 0
|
|
? $"汇总 | {row.Subscribers:N0} 个处理器"
|
|
: $"{row.Scheduler} / {row.Mode}\n队列 {row.Queue} | 溢出策略 {row.Overflow} | {row.Subscribers:N0} 个处理器";
|
|
}
|
|
|
|
private void ShowEventDetails(EventRow item)
|
|
{
|
|
if (_detailTitle == null)
|
|
return;
|
|
_detailTitle.text = item.EventType;
|
|
_detailTitle.tooltip = item.EventType;
|
|
if (_detailTime != null)
|
|
_detailTime.text = item.TimestampUtc.ToLocalTime().ToString("yyyy-MM-dd HH:mm:ss.fff");
|
|
if (_detailBus != null)
|
|
_detailBus.text = item.Bus;
|
|
if (_detailExecution != null)
|
|
_detailExecution.text = $"{(item.IsAsync ? "异步" : "同步")} | {item.Scheduler} / {item.Mode}";
|
|
if (_detailThread != null)
|
|
_detailThread.text = "托管线程 " + item.ThreadId;
|
|
if (_detailDuration != null)
|
|
_detailDuration.text = item.DurationMicroseconds.ToString("N3", CultureInfo.InvariantCulture) + " 微秒";
|
|
if (_detailResult != null)
|
|
{
|
|
_detailResult.text = $"{item.Status} | 已接收={FormatBoolean(item.Accepted)} 已处理={FormatBoolean(item.Handled)} 已取消={FormatBoolean(item.Canceled)}";
|
|
_detailResult.style.color = item.Failure != ShrinkPostFailure.None
|
|
? ErrorColor
|
|
: item.Canceled
|
|
? WarningColor
|
|
: SuccessColor;
|
|
}
|
|
}
|
|
|
|
private void ClearEventDetails()
|
|
{
|
|
if (_detailTitle != null)
|
|
_detailTitle.text = "未选择事件";
|
|
foreach (var label in new[]
|
|
{
|
|
_detailTime, _detailBus, _detailExecution, _detailThread, _detailDuration, _detailResult
|
|
})
|
|
{
|
|
if (label != null)
|
|
{
|
|
label.text = "-";
|
|
label.style.color = StyleKeyword.Null;
|
|
}
|
|
}
|
|
}
|
|
|
|
private void SetDiagnosticsSubscription(bool enabled)
|
|
{
|
|
if (enabled == _diagnosticsSubscribed)
|
|
return;
|
|
if (enabled)
|
|
EventBus.DetailedPosted += RecordEvent;
|
|
else
|
|
EventBus.DetailedPosted -= RecordEvent;
|
|
_diagnosticsSubscribed = enabled;
|
|
}
|
|
|
|
private void UpdateCaptureState()
|
|
{
|
|
if (_captureState == null)
|
|
return;
|
|
_captureState.text = _diagnosticsSubscribed ? "实时" : "已暂停";
|
|
_captureState.style.color = _diagnosticsSubscribed ? SuccessColor : WarningColor;
|
|
_captureState.style.backgroundColor = _diagnosticsSubscribed
|
|
? new Color(0.12f, 0.28f, 0.19f)
|
|
: new Color(0.32f, 0.25f, 0.11f);
|
|
}
|
|
|
|
private void ClearEvents()
|
|
{
|
|
lock (_eventGate)
|
|
_pendingEvents.Clear();
|
|
_events.Clear();
|
|
_filteredEvents.Clear();
|
|
_selectedEventSequence = 0;
|
|
Interlocked.Exchange(ref _droppedCaptureCount, 0);
|
|
_eventList?.ClearSelection();
|
|
ClearEventDetails();
|
|
ApplyFilter();
|
|
}
|
|
|
|
private void ExportCsv()
|
|
{
|
|
var path = EditorUtility.SaveFilePanel("导出 EventBus 跟踪记录", string.Empty,
|
|
$"ShrinkEventBus-{DateTime.Now:yyyyMMdd-HHmmss}.csv", "csv");
|
|
if (string.IsNullOrWhiteSpace(path))
|
|
return;
|
|
|
|
var builder = new StringBuilder(Math.Max(256, _filteredEvents.Count * 128));
|
|
builder.AppendLine("序号,UTC时间,总线,事件类型,调度器,分发模式,执行方式,线程ID,耗时微秒,已接收,已处理,已取消,失败原因");
|
|
for (var i = _filteredEvents.Count - 1; i >= 0; i--)
|
|
{
|
|
var item = _filteredEvents[i];
|
|
builder.Append(item.Sequence).Append(',')
|
|
.Append(item.TimestampUtc.ToString("O")).Append(',')
|
|
.Append(Csv(item.Bus)).Append(',')
|
|
.Append(Csv(item.EventType)).Append(',')
|
|
.Append(item.Scheduler).Append(',')
|
|
.Append(item.Mode).Append(',')
|
|
.Append(item.IsAsync ? "异步" : "同步").Append(',')
|
|
.Append(item.ThreadId).Append(',')
|
|
.Append(item.DurationMicroseconds.ToString("F3", CultureInfo.InvariantCulture)).Append(',')
|
|
.Append(FormatBoolean(item.Accepted)).Append(',')
|
|
.Append(FormatBoolean(item.Handled)).Append(',')
|
|
.Append(FormatBoolean(item.Canceled)).Append(',')
|
|
.Append(FormatFailure(item.Failure)).AppendLine();
|
|
}
|
|
File.WriteAllText(path, builder.ToString(), new UTF8Encoding(false));
|
|
EditorUtility.RevealInFinder(path);
|
|
}
|
|
|
|
private static string Csv(string value) =>
|
|
'"' + value.Replace("\"", "\"\"") + '"';
|
|
|
|
private static string FormatDuration(double microseconds) => microseconds < 1000d
|
|
? microseconds.ToString("N2", CultureInfo.InvariantCulture) + " 微秒"
|
|
: (microseconds / 1000d).ToString("N2", CultureInfo.InvariantCulture) + " 毫秒";
|
|
|
|
private static string FormatBoolean(bool value) => value ? "是" : "否";
|
|
|
|
private static string FormatScheduler(ShrinkBusSchedulerKind scheduler) => scheduler switch
|
|
{
|
|
ShrinkBusSchedulerKind.Inline => "当前线程",
|
|
ShrinkBusSchedulerKind.MainThread => "主线程",
|
|
ShrinkBusSchedulerKind.DedicatedThread => "专属线程",
|
|
ShrinkBusSchedulerKind.TaskPool => "任务池",
|
|
_ => scheduler.ToString()
|
|
};
|
|
|
|
private static string FormatDispatchMode(ShrinkDispatchMode mode) => mode switch
|
|
{
|
|
ShrinkDispatchMode.Ordered => "有序",
|
|
ShrinkDispatchMode.Parallel => "并行",
|
|
_ => mode.ToString()
|
|
};
|
|
|
|
private static string FormatOverflowPolicy(ShrinkQueueOverflowPolicy policy) => policy switch
|
|
{
|
|
ShrinkQueueOverflowPolicy.Reject => "拒绝",
|
|
ShrinkQueueOverflowPolicy.DropNewest => "丢弃最新",
|
|
ShrinkQueueOverflowPolicy.DropOldest => "丢弃最旧",
|
|
ShrinkQueueOverflowPolicy.Wait => "等待",
|
|
_ => policy.ToString()
|
|
};
|
|
|
|
private static string FormatFailure(ShrinkPostFailure failure) => failure switch
|
|
{
|
|
ShrinkPostFailure.None => "无",
|
|
ShrinkPostFailure.BusStopped => "总线已停止",
|
|
ShrinkPostFailure.QueueFull => "队列已满",
|
|
ShrinkPostFailure.Canceled => "已取消",
|
|
ShrinkPostFailure.HandlerException => "处理器异常",
|
|
ShrinkPostFailure.InvalidEvent => "无效事件",
|
|
_ => failure.ToString()
|
|
};
|
|
|
|
private static Color PanelColor() => EditorGUIUtility.isProSkin
|
|
? new Color(0.13f, 0.137f, 0.15f)
|
|
: new Color(0.94f, 0.95f, 0.96f);
|
|
|
|
private static Color HeaderColor() => EditorGUIUtility.isProSkin
|
|
? new Color(0.16f, 0.168f, 0.183f)
|
|
: new Color(0.82f, 0.84f, 0.87f);
|
|
|
|
private static Color BorderColor() => EditorGUIUtility.isProSkin
|
|
? new Color(0.24f, 0.25f, 0.28f)
|
|
: new Color(0.66f, 0.68f, 0.72f);
|
|
}
|
|
}
|