mirror of
https://github.com/SourMesen/Mesen2.git
synced 2025-04-02 10:21:44 -04:00
318 lines
10 KiB
C#
318 lines
10 KiB
C#
using Avalonia.Collections;
|
|
using Avalonia.Controls;
|
|
using Avalonia.Controls.Selection;
|
|
using Avalonia.VisualTree;
|
|
using Avalonia.Threading;
|
|
using DataBoxControl;
|
|
using Mesen.Config;
|
|
using Mesen.Debugger.Labels;
|
|
using Mesen.Debugger.Utilities;
|
|
using Mesen.Interop;
|
|
using Mesen.Localization;
|
|
using Mesen.Utilities;
|
|
using Mesen.ViewModels;
|
|
using ReactiveUI;
|
|
using ReactiveUI.Fody.Helpers;
|
|
using System;
|
|
using System.Collections.Generic;
|
|
using System.Collections.ObjectModel;
|
|
using System.ComponentModel;
|
|
using System.Linq;
|
|
|
|
namespace Mesen.Debugger.ViewModels
|
|
{
|
|
public class ProfilerWindowViewModel : DisposableViewModel
|
|
{
|
|
[Reactive] public List<ProfilerTab> ProfilerTabs { get; set; } = new List<ProfilerTab>();
|
|
[Reactive] public ProfilerTab? SelectedTab { get; set; } = null;
|
|
|
|
public List<object> FileMenuActions { get; } = new();
|
|
public List<object> ViewMenuActions { get; } = new();
|
|
|
|
public ProfilerConfig Config { get; }
|
|
|
|
public ProfilerWindowViewModel(Window? wnd)
|
|
{
|
|
Config = ConfigManager.Config.Debug.Profiler;
|
|
|
|
if(Design.IsDesignMode) {
|
|
return;
|
|
}
|
|
|
|
UpdateAvailableTabs();
|
|
|
|
AddDisposable(this.WhenAnyValue(x => x.SelectedTab).Subscribe(x => {
|
|
if(SelectedTab != null && EmuApi.IsPaused()) {
|
|
RefreshData();
|
|
}
|
|
}));
|
|
|
|
FileMenuActions = AddDisposables(new List<object>() {
|
|
new ContextMenuAction() {
|
|
ActionType = ActionType.ResetProfilerData,
|
|
OnClick = () => SelectedTab?.ResetData()
|
|
},
|
|
new ContextMenuAction() {
|
|
ActionType = ActionType.CopyToClipboard,
|
|
Shortcut = () => ConfigManager.Config.Debug.Shortcuts.Get(DebuggerShortcut.Copy),
|
|
OnClick = () => wnd?.GetVisualDescendants().Where(a => a is DataBox).Cast<DataBox>().FirstOrDefault()?.CopyToClipboard()
|
|
},
|
|
new ContextMenuSeparator(),
|
|
new ContextMenuAction() {
|
|
ActionType = ActionType.Exit,
|
|
OnClick = () => wnd?.Close()
|
|
}
|
|
});
|
|
|
|
ViewMenuActions = AddDisposables(new List<object>() {
|
|
new ContextMenuAction() {
|
|
ActionType = ActionType.Refresh,
|
|
Shortcut = () => ConfigManager.Config.Debug.Shortcuts.Get(DebuggerShortcut.Refresh),
|
|
OnClick = () => RefreshData()
|
|
},
|
|
new ContextMenuSeparator(),
|
|
new ContextMenuAction() {
|
|
ActionType = ActionType.EnableAutoRefresh,
|
|
IsSelected = () => Config.AutoRefresh,
|
|
OnClick = () => Config.AutoRefresh = !Config.AutoRefresh
|
|
},
|
|
new ContextMenuAction() {
|
|
ActionType = ActionType.RefreshOnBreakPause,
|
|
IsSelected = () => Config.RefreshOnBreakPause,
|
|
OnClick = () => Config.RefreshOnBreakPause = !Config.RefreshOnBreakPause
|
|
}
|
|
});
|
|
|
|
if(Design.IsDesignMode || wnd == null) {
|
|
return;
|
|
}
|
|
|
|
DebugShortcutManager.RegisterActions(wnd, FileMenuActions);
|
|
DebugShortcutManager.RegisterActions(wnd, ViewMenuActions);
|
|
|
|
LabelManager.OnLabelUpdated += LabelManager_OnLabelUpdated;
|
|
}
|
|
|
|
protected override void DisposeView()
|
|
{
|
|
LabelManager.OnLabelUpdated -= LabelManager_OnLabelUpdated;
|
|
}
|
|
|
|
private void LabelManager_OnLabelUpdated(object? sender, EventArgs e)
|
|
{
|
|
ProfilerTab tab = (SelectedTab ?? ProfilerTabs[0]);
|
|
Dispatcher.UIThread.Post(() => {
|
|
tab?.RefreshGrid();
|
|
});
|
|
}
|
|
|
|
public void UpdateAvailableTabs()
|
|
{
|
|
List<ProfilerTab> tabs = new();
|
|
foreach(CpuType type in EmuApi.GetRomInfo().CpuTypes) {
|
|
if(type.SupportsCallStack()) {
|
|
tabs.Add(new ProfilerTab() {
|
|
TabName = ResourceHelper.GetEnumText(type),
|
|
CpuType = type
|
|
});
|
|
}
|
|
}
|
|
|
|
ProfilerTabs = tabs;
|
|
SelectedTab = tabs[0];
|
|
}
|
|
|
|
public void RefreshData()
|
|
{
|
|
ProfilerTab tab = (SelectedTab ?? ProfilerTabs[0]);
|
|
tab.RefreshData();
|
|
Dispatcher.UIThread.Post(() => {
|
|
tab.RefreshGrid();
|
|
});
|
|
}
|
|
}
|
|
|
|
public class ProfilerTab : ReactiveObject
|
|
{
|
|
[Reactive] public string TabName { get; set; } = "";
|
|
[Reactive] public CpuType CpuType { get; set; } = CpuType.Snes;
|
|
[Reactive] public MesenList<ProfiledFunctionViewModel> GridData { get; private set; } = new();
|
|
[Reactive] public SelectionModel<ProfiledFunctionViewModel> Selection { get; set; } = new();
|
|
[Reactive] public SortState SortState { get; set; } = new();
|
|
public ProfilerConfig Config => ConfigManager.Config.Debug.Profiler;
|
|
public List<int> ColumnWidths { get; } = ConfigManager.Config.Debug.Profiler.ColumnWidths;
|
|
|
|
private object _updateLock = new();
|
|
private int _dataSize = 0;
|
|
private ProfiledFunction[] _coreProfilerData = new ProfiledFunction[100000];
|
|
private ProfiledFunction[] _profilerData = Array.Empty<ProfiledFunction>();
|
|
|
|
private UInt64 _totalCycles;
|
|
|
|
public ProfilerTab()
|
|
{
|
|
SortState.SetColumnSort("InclusiveTime", ListSortDirection.Descending, false);
|
|
}
|
|
|
|
public ProfiledFunction? GetRawData(int index)
|
|
{
|
|
ProfiledFunction[] data = _profilerData;
|
|
if(index < data.Length) {
|
|
return data[index];
|
|
}
|
|
return null;
|
|
}
|
|
|
|
public void ResetData()
|
|
{
|
|
DebugApi.ResetProfiler(CpuType);
|
|
GridData.Clear();
|
|
RefreshData();
|
|
RefreshGrid();
|
|
}
|
|
|
|
public void RefreshData()
|
|
{
|
|
lock(_updateLock) {
|
|
_dataSize = DebugApi.GetProfilerData(CpuType, ref _coreProfilerData);
|
|
}
|
|
}
|
|
|
|
public void RefreshGrid()
|
|
{
|
|
lock(_updateLock) {
|
|
Array.Resize(ref _profilerData, _dataSize);
|
|
Array.Copy(_coreProfilerData, _profilerData, _dataSize);
|
|
}
|
|
|
|
Sort();
|
|
|
|
UInt64 totalCycles = 0;
|
|
ProfiledFunction[] profilerData = _profilerData;
|
|
foreach(ProfiledFunction f in profilerData) {
|
|
totalCycles += f.ExclusiveCycles;
|
|
}
|
|
_totalCycles = totalCycles;
|
|
|
|
while(GridData.Count < profilerData.Length) {
|
|
GridData.Add(new ProfiledFunctionViewModel());
|
|
}
|
|
|
|
for(int i = 0; i < profilerData.Length; i++) {
|
|
GridData[i].Update(profilerData[i], CpuType, _totalCycles);
|
|
}
|
|
}
|
|
|
|
public void SortCommand(object? param)
|
|
{
|
|
RefreshGrid();
|
|
}
|
|
|
|
public void Sort()
|
|
{
|
|
CpuType cpuType = CpuType;
|
|
|
|
Dictionary<string, Func<ProfiledFunction, ProfiledFunction, int>> comparers = new() {
|
|
{ "FunctionName", (a, b) => string.Compare(a.GetFunctionName(cpuType), b.GetFunctionName(cpuType), StringComparison.OrdinalIgnoreCase) },
|
|
{ "CallCount", (a, b) => a.CallCount.CompareTo(b.CallCount) },
|
|
{ "InclusiveTime", (a, b) => a.InclusiveCycles.CompareTo(b.InclusiveCycles) },
|
|
{ "InclusiveTimePercent", (a, b) => a.InclusiveCycles.CompareTo(b.InclusiveCycles) },
|
|
{ "ExclusiveTime", (a, b) => a.ExclusiveCycles.CompareTo(b.ExclusiveCycles) },
|
|
{ "ExclusiveTimePercent", (a, b) => a.ExclusiveCycles.CompareTo(b.ExclusiveCycles) },
|
|
{ "AvgCycles", (a, b) => a.GetAvgCycles().CompareTo(b.GetAvgCycles()) },
|
|
{ "MinCycles", (a, b) => a.MinCycles.CompareTo(b.MinCycles) },
|
|
{ "MaxCycles", (a, b) => a.MaxCycles.CompareTo(b.MaxCycles) },
|
|
};
|
|
|
|
SortHelper.SortArray(_profilerData, SortState.SortOrder, comparers, "InclusiveTime");
|
|
}
|
|
}
|
|
|
|
public static class ProfiledFunctionExtensions
|
|
{
|
|
public static string GetFunctionName(this ProfiledFunction func, CpuType cpuType)
|
|
{
|
|
string functionName;
|
|
|
|
if(func.Address.Address == -1) {
|
|
functionName = "[Reset]";
|
|
} else {
|
|
CodeLabel? label = LabelManager.GetLabel((UInt32)func.Address.Address, func.Address.Type);
|
|
|
|
int hexCount = cpuType.GetAddressSize();
|
|
functionName = func.Address.Type.GetShortName() + ": $" + func.Address.Address.ToString("X" + hexCount.ToString());
|
|
if(label != null) {
|
|
functionName = label.Label + " (" + functionName + ")";
|
|
}
|
|
}
|
|
|
|
if(func.Flags.HasFlag(StackFrameFlags.Irq)) {
|
|
functionName = "[irq] " + functionName;
|
|
} else if(func.Flags.HasFlag(StackFrameFlags.Nmi)) {
|
|
functionName = "[nmi] " + functionName;
|
|
}
|
|
|
|
return functionName;
|
|
}
|
|
}
|
|
|
|
public class ProfiledFunctionViewModel : INotifyPropertyChanged
|
|
{
|
|
public event PropertyChangedEventHandler? PropertyChanged;
|
|
|
|
private string _functionName = "";
|
|
public string FunctionName
|
|
{
|
|
get
|
|
{
|
|
UpdateFields();
|
|
return _functionName;
|
|
}
|
|
}
|
|
|
|
public string ExclusiveCycles { get; set; } = "";
|
|
public string InclusiveCycles { get; set; } = "";
|
|
public string CallCount { get; set; } = "";
|
|
public string MinCycles { get; set; } = "";
|
|
public string MaxCycles { get; set; } = "";
|
|
|
|
public string ExclusivePercent { get; set; } = "";
|
|
public string InclusivePercent { get; set; } = "";
|
|
public string AvgCycles { get; set; } = "";
|
|
|
|
private ProfiledFunction _funcData;
|
|
private CpuType _cpuType;
|
|
private UInt64 _totalCycles;
|
|
|
|
public void Update(ProfiledFunction func, CpuType cpuType, UInt64 totalCycles)
|
|
{
|
|
_funcData = func;
|
|
_cpuType = cpuType;
|
|
_totalCycles = totalCycles;
|
|
|
|
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(ProfiledFunctionViewModel.FunctionName)));
|
|
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(ProfiledFunctionViewModel.ExclusiveCycles)));
|
|
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(ProfiledFunctionViewModel.InclusiveCycles)));
|
|
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(ProfiledFunctionViewModel.CallCount)));
|
|
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(ProfiledFunctionViewModel.MinCycles)));
|
|
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(ProfiledFunctionViewModel.MaxCycles)));
|
|
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(ProfiledFunctionViewModel.ExclusivePercent)));
|
|
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(ProfiledFunctionViewModel.InclusivePercent)));
|
|
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(ProfiledFunctionViewModel.AvgCycles)));
|
|
}
|
|
|
|
private void UpdateFields()
|
|
{
|
|
_functionName = _funcData.GetFunctionName(_cpuType);
|
|
ExclusiveCycles = _funcData.ExclusiveCycles.ToString();
|
|
InclusiveCycles = _funcData.InclusiveCycles.ToString();
|
|
CallCount = _funcData.CallCount.ToString();
|
|
MinCycles = _funcData.MinCycles == UInt64.MaxValue ? "n/a" : _funcData.MinCycles.ToString();
|
|
MaxCycles = _funcData.MaxCycles == 0 ? "n/a" : _funcData.MaxCycles.ToString();
|
|
|
|
AvgCycles = (_funcData.CallCount == 0 ? 0 : (_funcData.InclusiveCycles / _funcData.CallCount)).ToString();
|
|
ExclusivePercent = ((double)_funcData.ExclusiveCycles / _totalCycles * 100).ToString("0.00");
|
|
InclusivePercent = ((double)_funcData.InclusiveCycles / _totalCycles * 100).ToString("0.00");
|
|
}
|
|
}
|
|
}
|