diff --git a/Directory.Packages.props b/Directory.Packages.props
index 7a89abfc..9053ee56 100644
--- a/Directory.Packages.props
+++ b/Directory.Packages.props
@@ -6,6 +6,7 @@
+
@@ -14,6 +15,7 @@
+
@@ -23,9 +25,10 @@
-
+
+
-
\ No newline at end of file
+
diff --git a/src/AutoOS.App/Assets/Fluent/Benchmarks.png b/src/AutoOS.App/Assets/Fluent/Benchmarks.png
new file mode 100644
index 00000000..48807bc4
Binary files /dev/null and b/src/AutoOS.App/Assets/Fluent/Benchmarks.png differ
diff --git a/src/AutoOS.App/Assets/NavViewMenu/BreadcrumbPageMappingsSettings.cs b/src/AutoOS.App/Assets/NavViewMenu/BreadcrumbPageMappingsSettings.cs
index 8e563cde..7fcd9585 100644
--- a/src/AutoOS.App/Assets/NavViewMenu/BreadcrumbPageMappingsSettings.cs
+++ b/src/AutoOS.App/Assets/NavViewMenu/BreadcrumbPageMappingsSettings.cs
@@ -21,6 +21,7 @@ public partial class BreadcrumbPageMappingsSettings
{ typeof(Views.Settings.UpdatePage), new BreadcrumbPageConfig { PageTitle = null, IsHeaderVisible = false, ClearNavigation = false } },
{ typeof(Views.Settings.BrowsersPage), new BreadcrumbPageConfig { PageTitle = null, IsHeaderVisible = false, ClearNavigation = false } },
{ typeof(Views.Settings.ApplicationsPage), new BreadcrumbPageConfig { PageTitle = null, IsHeaderVisible = false, ClearNavigation = false } },
+ { typeof(Views.Settings.BenchmarksPage), new BreadcrumbPageConfig { PageTitle = null, IsHeaderVisible = false, ClearNavigation = false } },
{ typeof(Views.Settings.GamesPage), new BreadcrumbPageConfig { PageTitle = null, IsHeaderVisible = false, ClearNavigation = false } }
};
}
diff --git a/src/AutoOS.App/Assets/NavViewMenu/NavigationPageMappingsSettings.cs b/src/AutoOS.App/Assets/NavViewMenu/NavigationPageMappingsSettings.cs
index 8359d3da..1df660d2 100644
--- a/src/AutoOS.App/Assets/NavViewMenu/NavigationPageMappingsSettings.cs
+++ b/src/AutoOS.App/Assets/NavViewMenu/NavigationPageMappingsSettings.cs
@@ -19,6 +19,7 @@ public partial class NavigationPageMappingsSettings
{"AutoOS.Views.Settings.UpdatePage", typeof(AutoOS.Views.Settings.UpdatePage)},
{"AutoOS.Views.Settings.BrowsersPage", typeof(AutoOS.Views.Settings.BrowsersPage)},
{"AutoOS.Views.Settings.ApplicationsPage", typeof(AutoOS.Views.Settings.ApplicationsPage)},
+ {"AutoOS.Views.Settings.BenchmarksPage", typeof(AutoOS.Views.Settings.BenchmarksPage)},
{"AutoOS.Views.Settings.GamesPage", typeof(AutoOS.Views.Settings.GamesPage)}
};
}
diff --git a/src/AutoOS.App/Assets/NavViewMenu/Settings.json b/src/AutoOS.App/Assets/NavViewMenu/Settings.json
index d8a78125..ae6b5214 100644
--- a/src/AutoOS.App/Assets/NavViewMenu/Settings.json
+++ b/src/AutoOS.App/Assets/NavViewMenu/Settings.json
@@ -114,6 +114,13 @@
"ImagePath": "ms-appx:///Assets/Fluent/Applications.png",
"HideItem": true
},
+ {
+ "UniqueId": "AutoOS.Views.Settings.BenchmarksPage",
+ "Title": "Benchmarks",
+ "Subtitle": "Benchmarks",
+ "ImagePath": "ms-appx:///Assets/Fluent/Benchmarks.png",
+ "HideItem": true
+ },
{
"UniqueId": "AutoOS.Views.Settings.GamesPage",
"Title": "Games",
diff --git a/src/AutoOS.App/AutoOS.App.csproj b/src/AutoOS.App/AutoOS.App.csproj
index e56ad51a..3f29e8eb 100644
--- a/src/AutoOS.App/AutoOS.App.csproj
+++ b/src/AutoOS.App/AutoOS.App.csproj
@@ -1,68 +1,69 @@
-
- WinExe
- AutoOS
- AutoOS
- app.manifest
- win-$(Platform).pubxml
- true
- false
- true
- partial
- true
- true
- 2
- MSIX
- Assets\AppIcon.ico
- 1.4.6.1
-
- false
-
+
+ WinExe
+ AutoOS
+ AutoOS
+ app.manifest
+ win-$(Platform).pubxml
+ true
+ false
+ true
+ partial
+ true
+ true
+ 2
+ MSIX
+ Assets\AppIcon.ico
+ 1.4.6.1
+
+ false
+
-
-
- False
- False
- True
- False
- True
- True
- $(DefaultItemExcludes);AppPackages\**
- False
- True
- C:\Users\AutoOS\Desktop\AutoOS\AutoOS_TemporaryKey.pfx
- SHA256
- False
- False
- False
- Never
- 0
-
+
+
+ False
+ False
+ True
+ False
+ True
+ True
+ $(DefaultItemExcludes);AppPackages\**
+ False
+ True
+ C:\Users\AutoOS\Desktop\AutoOS\AutoOS_TemporaryKey.pfx
+ SHA256
+ False
+ False
+ False
+ Never
+ 0
+
-
-
- PreserveNewest
-
-
- PreserveNewest
-
-
+
+
+ PreserveNewest
+
+
+ PreserveNewest
+
+
-
-
- PreserveNewest
-
-
+
+
+ PreserveNewest
+
+
-
-
-
-
-
+
+
+
+
+
+
@@ -75,69 +76,72 @@
Tools extension to be activated for this project even if the Windows App SDK Nuget
package has not yet been restored.
-->
-
-
-
+
+
+
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
-
-
-
-
-
-
+
+
+
+
+
+
-
-
- PreserveNewest
-
-
+
+
+ PreserveNewest
+
+
-
-
- runtimes\win-x64\native\leveldb.dll
- PreserveNewest
-
-
- runtimes\win-arm64\native\leveldb.dll
- PreserveNewest
-
-
- runtimes\win-x86\native\leveldb.dll
- PreserveNewest
-
-
+
+
+ runtimes\win-x64\native\leveldb.dll
+ PreserveNewest
+
+
+ runtimes\win-arm64\native\leveldb.dll
+ PreserveNewest
+
+
+ runtimes\win-x86\native\leveldb.dll
+ PreserveNewest
+
+
-
-
- true
-
+
+ true
+
-
-
-
+
+
+
diff --git a/src/AutoOS.App/Common/CommandBarExtensions.cs b/src/AutoOS.App/Common/CommandBarExtensions.cs
index cdb84ab3..e7cb38db 100644
--- a/src/AutoOS.App/Common/CommandBarExtensions.cs
+++ b/src/AutoOS.App/Common/CommandBarExtensions.cs
@@ -1,4 +1,5 @@
using System.Runtime.CompilerServices;
+using CommunityToolkit.WinUI.Controls;
namespace AutoOS.Common;
@@ -57,88 +58,111 @@ public static void SetOverflowButtonAlignment(DependencyObject obj, HorizontalAl
obj.SetValue(OverflowButtonAlignmentProperty, value);
}
- private static void OnApplyOverflowIndentChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
+ private static void ForEachCommandBar(DependencyObject dependencyObject, Action action)
{
- if (d is not CommandBar commandBar)
- return;
-
- bool newValue = (bool)e.NewValue;
-
- if (newValue)
+ if (dependencyObject is CommandBar commandBar)
{
- commandBar.Opening += CommandBar_Opening;
- commandBar.Closed += CommandBar_Closed;
-
- if (commandBar.IsOpen)
- {
- ApplyOverflowContainerMargins(commandBar);
- }
-
- if (commandBar.IsLoaded)
+ action(commandBar);
+ }
+ else if (dependencyObject is TabbedCommandBar tabbedCommandBar)
+ {
+ if (tabbedCommandBar.IsLoaded)
{
- UpdateCommandAlignment(commandBar, GetCommandAlignment(commandBar));
- UpdateOverflowButtonAlignment(commandBar, GetOverflowButtonAlignment(commandBar));
+ foreach (var item in tabbedCommandBar.MenuItems.OfType())
+ action(item);
}
else
{
RoutedEventHandler loadedHandler = null;
loadedHandler = (sender, args) =>
{
- commandBar.Loaded -= loadedHandler;
- UpdateCommandAlignment(commandBar, GetCommandAlignment(commandBar));
- UpdateOverflowButtonAlignment(commandBar, GetOverflowButtonAlignment(commandBar));
+ tabbedCommandBar.Loaded -= loadedHandler;
+ foreach (var item in tabbedCommandBar.MenuItems.OfType())
+ action(item);
};
- commandBar.Loaded += loadedHandler;
+ tabbedCommandBar.Loaded += loadedHandler;
}
}
- else
- {
- commandBar.Opening -= CommandBar_Opening;
- commandBar.Closed -= CommandBar_Closed;
- RestoreOverflowContainerMargins(commandBar);
- }
}
- private static void OnCommandAlignmentChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
+ private static void OnApplyOverflowIndentChanged(DependencyObject dependencyObject, DependencyPropertyChangedEventArgs args)
{
- if (d is not CommandBar commandBar)
- return;
+ bool newValue = (bool)args.NewValue;
- if (commandBar.IsLoaded)
- {
- UpdateCommandAlignment(commandBar, (HorizontalAlignment)e.NewValue);
- }
- else
+ ForEachCommandBar(dependencyObject, commandBar =>
{
- RoutedEventHandler loadedHandler = null;
- loadedHandler = (sender, args) =>
+ if (newValue)
{
- commandBar.Loaded -= loadedHandler;
- UpdateCommandAlignment(commandBar, GetCommandAlignment(commandBar));
- };
- commandBar.Loaded += loadedHandler;
- }
+ commandBar.Opening += CommandBar_Opening;
+ commandBar.Closed += CommandBar_Closed;
+
+ if (commandBar.IsOpen)
+ ApplyOverflowContainerMargins(commandBar);
+
+ if (commandBar.IsLoaded)
+ {
+ UpdateCommandAlignment(commandBar, GetCommandAlignment(dependencyObject));
+ UpdateOverflowButtonAlignment(commandBar, GetOverflowButtonAlignment(dependencyObject));
+ }
+ else
+ {
+ RoutedEventHandler loadedHandler = null;
+ loadedHandler = (sender, eventArgs) =>
+ {
+ commandBar.Loaded -= loadedHandler;
+ UpdateCommandAlignment(commandBar, GetCommandAlignment(dependencyObject));
+ UpdateOverflowButtonAlignment(commandBar, GetOverflowButtonAlignment(dependencyObject));
+ };
+ commandBar.Loaded += loadedHandler;
+ }
+ }
+ else
+ {
+ commandBar.Opening -= CommandBar_Opening;
+ commandBar.Closed -= CommandBar_Closed;
+ RestoreOverflowContainerMargins(commandBar);
+ }
+ });
}
- private static void OnOverflowButtonAlignmentChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
+ private static void OnCommandAlignmentChanged(DependencyObject dependencyObject, DependencyPropertyChangedEventArgs args)
{
- if (d is not CommandBar commandBar)
- return;
-
- if (commandBar.IsLoaded)
+ var alignment = (HorizontalAlignment)args.NewValue;
+ ForEachCommandBar(dependencyObject, commandBar =>
{
- UpdateOverflowButtonAlignment(commandBar, (HorizontalAlignment)e.NewValue);
- }
- else
+ if (commandBar.IsLoaded)
+ UpdateCommandAlignment(commandBar, alignment);
+ else
+ {
+ RoutedEventHandler loadedHandler = null;
+ loadedHandler = (sender, eventArgs) =>
+ {
+ commandBar.Loaded -= loadedHandler;
+ UpdateCommandAlignment(commandBar, alignment);
+ };
+ commandBar.Loaded += loadedHandler;
+ }
+ });
+ }
+
+ private static void OnOverflowButtonAlignmentChanged(DependencyObject dependencyObject, DependencyPropertyChangedEventArgs args)
+ {
+ var alignment = (HorizontalAlignment)args.NewValue;
+ ForEachCommandBar(dependencyObject, commandBar =>
{
- RoutedEventHandler loadedHandler = null;
- loadedHandler = (sender, args) =>
+ if (commandBar.IsLoaded)
+ UpdateOverflowButtonAlignment(commandBar, alignment);
+ else
{
- commandBar.Loaded -= loadedHandler;
- UpdateOverflowButtonAlignment(commandBar, GetOverflowButtonAlignment(commandBar));
- };
- commandBar.Loaded += loadedHandler;
- }
+ RoutedEventHandler loadedHandler = null;
+ loadedHandler = (sender, eventArgs) =>
+ {
+ commandBar.Loaded -= loadedHandler;
+ UpdateOverflowButtonAlignment(commandBar, alignment);
+ };
+ commandBar.Loaded += loadedHandler;
+ }
+ });
}
private static void UpdateCommandAlignment(CommandBar commandBar, HorizontalAlignment alignment)
@@ -163,16 +187,17 @@ private static void UpdateOverflowButtonAlignment(CommandBar commandBar, Horizon
catch { }
}
- private static T FindVisualChild(DependencyObject obj, string name) where T : DependencyObject
+ private static T FindVisualChild(DependencyObject parent, string childName) where T : DependencyObject
{
- for (int i = 0; i < Microsoft.UI.Xaml.Media.VisualTreeHelper.GetChildrenCount(obj); i++)
+ int count = Microsoft.UI.Xaml.Media.VisualTreeHelper.GetChildrenCount(parent);
+ for (int index = 0; index < count; index++)
{
- var child = Microsoft.UI.Xaml.Media.VisualTreeHelper.GetChild(obj, i);
- if (child is T t && (child as FrameworkElement)?.Name == name)
+ var child = Microsoft.UI.Xaml.Media.VisualTreeHelper.GetChild(parent, index);
+ if (child is T matched && (child as FrameworkElement)?.Name == childName)
{
- return t;
+ return matched;
}
- var childOfChild = FindVisualChild(child, name);
+ var childOfChild = FindVisualChild(child, childName);
if (childOfChild != null)
{
return childOfChild;
@@ -181,20 +206,16 @@ private static T FindVisualChild(DependencyObject obj, string name) where T :
return null;
}
- private static void CommandBar_Opening(object sender, object e)
+ private static void CommandBar_Opening(object sender, object eventArgs)
{
if (sender is CommandBar commandBar)
- {
ApplyOverflowContainerMargins(commandBar);
- }
}
- private static void CommandBar_Closed(object sender, object e)
+ private static void CommandBar_Closed(object sender, object eventArgs)
{
if (sender is CommandBar commandBar)
- {
RestoreOverflowContainerMargins(commandBar);
- }
}
private static void ApplyOverflowContainerMargins(CommandBar commandBar)
@@ -203,17 +224,14 @@ private static void ApplyOverflowContainerMargins(CommandBar commandBar)
foreach (var container in commandBar.PrimaryCommands.OfType())
{
- if (container.IsInOverflow)
- {
- margins.TryAdd(container, container.Margin);
+ if (container.IsInOverflow && margins.TryAdd(container, container.Margin))
container.Margin = new Thickness(32, 0, 0, 0);
- }
}
foreach (var container in commandBar.SecondaryCommands.OfType())
{
- margins.TryAdd(container, container.Margin);
- container.Margin = new Thickness(32, 0, 0, 0);
+ if (margins.TryAdd(container, container.Margin))
+ container.Margin = new Thickness(32, 0, 0, 0);
}
}
@@ -222,9 +240,7 @@ private static void RestoreOverflowContainerMargins(CommandBar commandBar)
if (_marginsMap.TryGetValue(commandBar, out var margins))
{
foreach (var (container, margin) in margins)
- {
container.Margin = margin;
- }
margins.Clear();
}
}
diff --git a/src/AutoOS.App/Themes/Styles.xaml b/src/AutoOS.App/Themes/Styles.xaml
index 6979f113..027685e7 100644
--- a/src/AutoOS.App/Themes/Styles.xaml
+++ b/src/AutoOS.App/Themes/Styles.xaml
@@ -881,4 +881,68 @@
+
diff --git a/src/AutoOS.App/Views/Settings/Benchmarks/BenchmarkDataHelpers.cs b/src/AutoOS.App/Views/Settings/Benchmarks/BenchmarkDataHelpers.cs
new file mode 100644
index 00000000..dd206291
--- /dev/null
+++ b/src/AutoOS.App/Views/Settings/Benchmarks/BenchmarkDataHelpers.cs
@@ -0,0 +1,136 @@
+using System.Globalization;
+using CsvHelper;
+using CsvHelper.Configuration;
+
+namespace AutoOS.Views.Settings.Benchmarks;
+
+internal static class BenchmarkCsv
+{
+ public static List ParseCsvLine(string line)
+ {
+ if (string.IsNullOrEmpty(line))
+ return [];
+ using var reader = new StringReader(line);
+ using var parser = new CsvParser(reader, new CsvConfiguration(CultureInfo.InvariantCulture)
+ {
+ HasHeaderRecord = false,
+ BadDataFound = null
+ });
+ if (!parser.Read())
+ return [];
+ return [.. parser.Record];
+ }
+}
+
+internal static class BenchmarkStatistics
+{
+ public static Metrics CalculateMetrics(double[] values, bool isFpsMetric)
+ {
+ var result = new Metrics();
+ if (values.Length == 0)
+ return result;
+
+ var sorted = values.OrderBy(v => v).ToArray();
+ int n = values.Length;
+ double sum = values.Sum();
+ double arithmeticMean = sum / n;
+
+ int c1 = Math.Max(1, (int)Math.Ceiling(n * 0.01));
+ int c01 = Math.Max(1, (int)Math.Ceiling(n * 0.001));
+
+ if (isFpsMetric)
+ {
+ result.Low01 = sorted.Take(c01).Average();
+ result.Low1 = sorted.Take(c1).Average();
+ }
+ else
+ {
+ var desc = sorted.Reverse().ToArray();
+ result.Low01 = desc.Take(c01).Average();
+ result.Low1 = desc.Take(c1).Average();
+ }
+
+ result.AvgArithmetic = arithmeticMean;
+ result.AvgHarmonic = HarmonicMean(values);
+ result.Min = sorted[0];
+ result.Max = sorted[n - 1];
+
+ result.P01 = Percentile(sorted, isFpsMetric ? 0.1 : 99.9);
+ result.P1 = Percentile(sorted, isFpsMetric ? 1 : 99);
+ result.P5 = Percentile(sorted, isFpsMetric ? 5 : 95);
+ result.P50Median = Percentile(sorted, 50);
+ result.P95 = Percentile(sorted, isFpsMetric ? 95 : 5);
+ result.P99 = Percentile(sorted, isFpsMetric ? 99 : 1);
+
+ double variance = values.Sum(v => (v - arithmeticMean) * (v - arithmeticMean)) / (n - 1);
+ result.StdDev = Math.Sqrt(variance);
+ result.Cv = arithmeticMean != 0 ? result.StdDev / arithmeticMean : 0;
+
+ double sumSqDiff = 0;
+ for (int i = 1; i < n; i++)
+ {
+ double diff = values[i] - values[i - 1];
+ sumSqDiff += diff * diff;
+ }
+ result.Rmssd = Math.Sqrt(sumSqDiff / (n - 1));
+
+ double sumRelSq = 0;
+ int validPairs = 0;
+ for (int i = 1; i < n; i++)
+ {
+ if (values[i - 1] == 0) continue;
+ double rel = (values[i] - values[i - 1]) / values[i - 1];
+ sumRelSq += rel * rel;
+ validPairs++;
+ }
+ result.StepwiseRelSD = validPairs > 0 ? Math.Sqrt(sumRelSq / validPairs) : 0;
+
+ return result;
+ }
+
+ private static double Percentile(double[] sorted, double percentile)
+ {
+ double position = percentile / 100.0 * (sorted.Length - 1);
+ int lower = (int)Math.Floor(position);
+ int upper = (int)Math.Ceiling(position);
+ if (lower == upper)
+ return sorted[lower];
+ double fraction = position - lower;
+ return sorted[lower] * (1 - fraction) + sorted[upper] * fraction;
+ }
+
+ private static double HarmonicMean(double[] values)
+ {
+ double reciprocalSum = 0;
+ int count = 0;
+ foreach (var v in values)
+ {
+ if (v > 0)
+ {
+ reciprocalSum += 1.0 / v;
+ count++;
+ }
+ }
+ return count > 0 ? count / reciprocalSum : 0;
+ }
+}
+
+public class Metrics
+{
+ public double Low01 { get; set; }
+ public double Low1 { get; set; }
+ public double AvgArithmetic { get; set; }
+ public double AvgHarmonic { get; set; }
+ public double Min { get; set; }
+ public double Max { get; set; }
+ public double P01 { get; set; }
+ public double P1 { get; set; }
+ public double P5 { get; set; }
+ public double P50Median { get; set; }
+ public double P95 { get; set; }
+ public double P99 { get; set; }
+ public double StdDev { get; set; }
+ public double Cv { get; set; }
+ public double Rmssd { get; set; }
+ public double StepwiseRelSD { get; set; }
+}
diff --git a/src/AutoOS.App/Views/Settings/Benchmarks/BenchmarksViewModel.cs b/src/AutoOS.App/Views/Settings/Benchmarks/BenchmarksViewModel.cs
new file mode 100644
index 00000000..43b6cffc
--- /dev/null
+++ b/src/AutoOS.App/Views/Settings/Benchmarks/BenchmarksViewModel.cs
@@ -0,0 +1,786 @@
+using System.Collections.ObjectModel;
+using CommunityToolkit.Mvvm.ComponentModel;
+using Microsoft.UI.Xaml.Media;
+
+namespace AutoOS.Views.Settings.Benchmarks;
+
+public sealed partial class BenchmarksViewModel : ObservableObject
+{
+ private string _activeTab = "Recordings";
+ private string _recordingState = "Empty";
+ private string _analysisState = "Empty";
+ private string _statisticsState = "Empty";
+ private int _selectedRecordingCount;
+ private bool _selectedRecordingsHaveSameProcess;
+ private string _processName = string.Empty;
+ private double _recordingDuration = 30;
+ private double _recordingDelay = 5;
+ private bool _isRecording;
+ private string _analysisChartType = "Bar";
+ private readonly HashSet _recordableProcesses = new(StringComparer.OrdinalIgnoreCase);
+ private ObservableCollection _processSuggestions = [];
+ private ObservableCollection _recordings = [];
+ private ObservableCollection _statisticsRows = [];
+ private ObservableCollection _fpsBarSeries = [];
+ private ObservableCollection _fpsRenderedBarSeries = [];
+ private ObservableCollection _fpsBarSeries2 = [];
+ private ObservableCollection _fpsRenderedBarSeries2 = [];
+ private ObservableCollection _metricSeries = [];
+ private ObservableCollection _metricSeries2 = [];
+ private Windows.UI.Color _fpsColor;
+ private Windows.UI.Color _fpsColor2;
+ private Windows.UI.Color _fpsRenderedColor;
+ private Windows.UI.Color _fpsRenderedColor2;
+ private SolidColorBrush _fpsChartColor;
+ private SolidColorBrush _fpsChartColor2;
+ private string _fpsChartYAxisLabel = "FPS";
+ private string _metricChartYAxisLabel = "Milliseconds (ms)";
+ private string _fpsChartLabelFormat = "0.#";
+ private bool _showFpsChart2;
+ private bool _showRenderedFps;
+ private bool _showRenderedFpsChart2;
+ private bool _showMetricChart2;
+ private string _fpsChartLabel = string.Empty;
+ private string _fpsRenderedChartLabel = string.Empty;
+ private string _fpsChartLabel2 = string.Empty;
+ private string _fpsRenderedChartLabel2 = string.Empty;
+ private string _metricChartLabel = string.Empty;
+ private string _metricChartLabel2 = string.Empty;
+ private string _recordingAHeader = "Recording A";
+ private string _recordingBHeader = "Recording B";
+
+ public string ActiveTab
+ {
+ get => _activeTab;
+ set => SetProperty(ref _activeTab, value);
+ }
+
+ public string RecordingState
+ {
+ get => _recordingState;
+ private set => SetProperty(ref _recordingState, value);
+ }
+
+ public string AnalysisState
+ {
+ get => _analysisState;
+ private set => SetProperty(ref _analysisState, value);
+ }
+
+ public string StatisticsState
+ {
+ get => _statisticsState;
+ private set => SetProperty(ref _statisticsState, value);
+ }
+
+ public ObservableCollection Recordings
+ {
+ get => _recordings;
+ private set => SetProperty(ref _recordings, value);
+ }
+
+ public ObservableCollection StatisticsRows
+ {
+ get => _statisticsRows;
+ set => SetProperty(ref _statisticsRows, value);
+ }
+
+ public ObservableCollection ProcessSuggestions
+ {
+ get => _processSuggestions;
+ private set => SetProperty(ref _processSuggestions, value);
+ }
+
+ public string ProcessName
+ {
+ get => _processName;
+ set
+ {
+ if (SetProperty(ref _processName, value))
+ {
+ FilterProcessSuggestions(value);
+ OnPropertyChanged(nameof(CanRecord));
+ }
+ }
+ }
+
+ public double RecordingDuration
+ {
+ get => _recordingDuration;
+ set
+ {
+ if (SetProperty(ref _recordingDuration, value))
+ OnPropertyChanged(nameof(CanRecord));
+ }
+ }
+
+ public double RecordingDelay
+ {
+ get => _recordingDelay;
+ set
+ {
+ if (SetProperty(ref _recordingDelay, value))
+ OnPropertyChanged(nameof(CanRecord));
+ }
+ }
+
+ public bool IsRecording
+ {
+ get => _isRecording;
+ set
+ {
+ if (SetProperty(ref _isRecording, value))
+ {
+ OnPropertyChanged(nameof(CanRecord));
+ OnPropertyChanged(nameof(RecordLabel));
+ }
+ }
+ }
+
+ public event Action MetricToggled;
+
+ private bool _showLow01 = true;
+ private bool _showLow1 = true;
+ private bool _showAvgArithmetic = true;
+ private bool _showAvgHarmonic = true;
+ private bool _showMin;
+ private bool _showMax;
+ private bool _showP01;
+ private bool _showP1;
+ private bool _showP5;
+ private bool _showP50Median = true;
+ private bool _showP95;
+ private bool _showP99;
+
+ public bool ShowLow01
+ {
+ get => _showLow01;
+ set
+ {
+ if (SetProperty(ref _showLow01, value))
+ MetricToggled?.Invoke();
+ }
+ }
+
+ public bool ShowLow1
+ {
+ get => _showLow1;
+ set
+ {
+ if (SetProperty(ref _showLow1, value))
+ MetricToggled?.Invoke();
+ }
+ }
+
+ public bool ShowAvgArithmetic
+ {
+ get => _showAvgArithmetic;
+ set
+ {
+ if (SetProperty(ref _showAvgArithmetic, value))
+ MetricToggled?.Invoke();
+ }
+ }
+
+ public bool ShowAvgHarmonic
+ {
+ get => _showAvgHarmonic;
+ set
+ {
+ if (SetProperty(ref _showAvgHarmonic, value))
+ MetricToggled?.Invoke();
+ }
+ }
+
+ public bool ShowMin
+ {
+ get => _showMin;
+ set
+ {
+ if (SetProperty(ref _showMin, value))
+ MetricToggled?.Invoke();
+ }
+ }
+
+ public bool ShowMax
+ {
+ get => _showMax;
+ set
+ {
+ if (SetProperty(ref _showMax, value))
+ MetricToggled?.Invoke();
+ }
+ }
+
+ public bool ShowP01
+ {
+ get => _showP01;
+ set
+ {
+ if (SetProperty(ref _showP01, value))
+ MetricToggled?.Invoke();
+ }
+ }
+
+ public bool ShowP1
+ {
+ get => _showP1;
+ set
+ {
+ if (SetProperty(ref _showP1, value))
+ MetricToggled?.Invoke();
+ }
+ }
+
+ public bool ShowP5
+ {
+ get => _showP5;
+ set
+ {
+ if (SetProperty(ref _showP5, value))
+ MetricToggled?.Invoke();
+ }
+ }
+
+ public bool ShowP50Median
+ {
+ get => _showP50Median;
+ set
+ {
+ if (SetProperty(ref _showP50Median, value))
+ MetricToggled?.Invoke();
+ }
+ }
+
+ public bool ShowP95
+ {
+ get => _showP95;
+ set
+ {
+ if (SetProperty(ref _showP95, value))
+ MetricToggled?.Invoke();
+ }
+ }
+
+ public bool ShowP99
+ {
+ get => _showP99;
+ set
+ {
+ if (SetProperty(ref _showP99, value))
+ MetricToggled?.Invoke();
+ }
+ }
+
+ public bool IsMetricEnabled(string metric) => metric switch
+ {
+ "0.1% Low" => ShowLow01,
+ "1% Low" => ShowLow1,
+ "Avg (Arithmetic)" => ShowAvgArithmetic,
+ "Avg (Harmonic)" => ShowAvgHarmonic,
+ "Min" => ShowMin,
+ "Max" => ShowMax,
+ "P0.1" => ShowP01,
+ "P1" => ShowP1,
+ "P5" => ShowP5,
+ "P50 (Median)" => ShowP50Median,
+ "P95" => ShowP95,
+ "P99" => ShowP99,
+ _ => true
+ };
+
+ public string AnalysisChartType
+ {
+ get => _analysisChartType;
+ set
+ {
+ if (SetProperty(ref _analysisChartType, value))
+ {
+ OnPropertyChanged(nameof(MetricVisibility));
+ OnPropertyChanged(nameof(BarMetricsVisibility));
+ }
+ }
+ }
+
+ public Visibility MetricVisibility =>
+ (AnalysisChartType is "Bar" or "Column") ? Visibility.Collapsed : Visibility.Visible;
+
+ public Visibility BarMetricsVisibility =>
+ (AnalysisChartType is "Bar" or "Column") ? Visibility.Visible : Visibility.Collapsed;
+
+ public bool CanRecord =>
+ IsRecording ||
+ (!string.IsNullOrWhiteSpace(ProcessName) &&
+ _recordableProcesses.Contains(ProcessName.Trim()) &&
+ RecordingDuration >= 3 &&
+ RecordingDelay >= 0);
+
+ public string RecordLabel => IsRecording ? "Recording..." : "Record";
+ public List