From ad9dcda69043050286ed4b3da6be48f5eccedfb1 Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Thu, 19 Feb 2026 23:56:23 +0000
Subject: [PATCH 1/6] Initial plan
From 100b0a311c843dd93d9d02801b5323f370c84094 Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Fri, 20 Feb 2026 00:07:04 +0000
Subject: [PATCH 2/6] Add performance monitoring enhancement: runtime metrics,
memory profiling, regression detection, and dashboard
Co-authored-by: michaelbeale-IL <63321611+michaelbeale-IL@users.noreply.github.com>
---
.github/workflows/build.yml | 6 +
.github/workflows/test.yml | 8 +
src/ACAT.sln | 19 ++
.../ACATTalk/PerformanceMonitor.cs | 41 +++
.../ACAT.Extensions.UI.csproj | 10 +
.../Diagnostics/PerformanceDashboard.xaml | 169 ++++++++++++
.../Diagnostics/PerformanceDashboard.xaml.cs | 252 +++++++++++++++++
.../ACATCore.Tests.Performance.csproj | 28 ++
.../MemoryProfilerTests.cs | 108 ++++++++
.../PerformanceRegressionDetectorTests.cs | 219 +++++++++++++++
.../RuntimeMetricsCollectorTests.cs | 129 +++++++++
src/Libraries/ACATCore/ACAT.Core.csproj | 4 +
.../Utility/Diagnostics/MemoryProfiler.cs | 181 +++++++++++++
.../Diagnostics/PerformanceBaseline.cs | 146 ++++++++++
.../PerformanceRegressionDetector.cs | 168 ++++++++++++
.../Metrics/RuntimeMetricsCollector.cs | 254 ++++++++++++++++++
16 files changed, 1742 insertions(+)
create mode 100644 src/Extensions/ACAT.Extensions.UI/Diagnostics/PerformanceDashboard.xaml
create mode 100644 src/Extensions/ACAT.Extensions.UI/Diagnostics/PerformanceDashboard.xaml.cs
create mode 100644 src/Libraries/ACATCore.Tests.Performance/ACATCore.Tests.Performance.csproj
create mode 100644 src/Libraries/ACATCore.Tests.Performance/MemoryProfilerTests.cs
create mode 100644 src/Libraries/ACATCore.Tests.Performance/PerformanceRegressionDetectorTests.cs
create mode 100644 src/Libraries/ACATCore.Tests.Performance/RuntimeMetricsCollectorTests.cs
create mode 100644 src/Libraries/ACATCore/Utility/Diagnostics/MemoryProfiler.cs
create mode 100644 src/Libraries/ACATCore/Utility/Diagnostics/PerformanceBaseline.cs
create mode 100644 src/Libraries/ACATCore/Utility/Diagnostics/PerformanceRegressionDetector.cs
create mode 100644 src/Libraries/ACATCore/Utility/Metrics/RuntimeMetricsCollector.cs
diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml
index 9ff8b0cc..0f87ad0a 100644
--- a/.github/workflows/build.yml
+++ b/.github/workflows/build.yml
@@ -67,6 +67,12 @@ jobs:
working-directory: src/
continue-on-error: false
+ - name: Run Performance Tests
+ run: |
+ dotnet test Libraries/ACATCore.Tests.Performance/ACATCore.Tests.Performance.csproj --configuration ${{ matrix.configuration }} --logger "trx;LogFileName=performance-tests.trx" --logger "console;verbosity=normal" --results-directory TestResults
+ working-directory: src/
+ continue-on-error: false
+
# Publish test results
- name: Publish Test Results
uses: dorny/test-reporter@v1
diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml
index 7f6a59ff..408e802b 100644
--- a/.github/workflows/test.yml
+++ b/.github/workflows/test.yml
@@ -61,6 +61,14 @@ jobs:
--results-directory TestResults/
working-directory: src/
+ - name: Run ACATCore.Tests.Performance
+ run: >
+ dotnet test Libraries/ACATCore.Tests.Performance/ACATCore.Tests.Performance.csproj
+ --configuration Debug
+ --logger "trx;LogFileName=performance-results.trx"
+ --results-directory TestResults/
+ working-directory: src/
+
- name: Upload test results
uses: actions/upload-artifact@v4
if: always()
diff --git a/src/ACAT.sln b/src/ACAT.sln
index badb22ed..a9694137 100644
--- a/src/ACAT.sln
+++ b/src/ACAT.sln
@@ -149,6 +149,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ACATCore.Tests.Logging", "L
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ACATCore.Tests.Integration", "Libraries\ACATCore.Tests.Integration\ACATCore.Tests.Integration.csproj", "{55D58F6D-68E0-52D6-5909-E5772FA29551}"
EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ACATCore.Tests.Performance", "Libraries\ACATCore.Tests.Performance\ACATCore.Tests.Performance.csproj", "{8B3C1A2D-4E5F-6789-ABCD-EF0123456789}"
+EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug_signed|Any CPU = Debug_signed|Any CPU
@@ -1218,6 +1220,23 @@ Global
{55D58F6D-68E0-52D6-5909-E5772FA29551}.Release|x64.Build.0 = Release|x64
{55D58F6D-68E0-52D6-5909-E5772FA29551}.Release|x86.ActiveCfg = Release|x64
{55D58F6D-68E0-52D6-5909-E5772FA29551}.Release|x86.Build.0 = Release|x64
+ {8B3C1A2D-4E5F-6789-ABCD-EF0123456789}.Debug_signed|Any CPU.ActiveCfg = Debug|Any CPU
+ {8B3C1A2D-4E5F-6789-ABCD-EF0123456789}.Debug_signed|x64.ActiveCfg = Debug|Any CPU
+ {8B3C1A2D-4E5F-6789-ABCD-EF0123456789}.Debug_signed|x86.ActiveCfg = Debug|Any CPU
+ {8B3C1A2D-4E5F-6789-ABCD-EF0123456789}.Debug_TestGTEC|Any CPU.ActiveCfg = Debug|Any CPU
+ {8B3C1A2D-4E5F-6789-ABCD-EF0123456789}.Debug_TestGTEC|x64.ActiveCfg = Debug|Any CPU
+ {8B3C1A2D-4E5F-6789-ABCD-EF0123456789}.Debug_TestGTEC|x86.ActiveCfg = Debug|Any CPU
+ {8B3C1A2D-4E5F-6789-ABCD-EF0123456789}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {8B3C1A2D-4E5F-6789-ABCD-EF0123456789}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {8B3C1A2D-4E5F-6789-ABCD-EF0123456789}.Debug|x64.ActiveCfg = Debug|Any CPU
+ {8B3C1A2D-4E5F-6789-ABCD-EF0123456789}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {8B3C1A2D-4E5F-6789-ABCD-EF0123456789}.Release_signed|Any CPU.ActiveCfg = Release|Any CPU
+ {8B3C1A2D-4E5F-6789-ABCD-EF0123456789}.Release_signed|x64.ActiveCfg = Release|Any CPU
+ {8B3C1A2D-4E5F-6789-ABCD-EF0123456789}.Release_signed|x86.ActiveCfg = Release|Any CPU
+ {8B3C1A2D-4E5F-6789-ABCD-EF0123456789}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {8B3C1A2D-4E5F-6789-ABCD-EF0123456789}.Release|Any CPU.Build.0 = Release|Any CPU
+ {8B3C1A2D-4E5F-6789-ABCD-EF0123456789}.Release|x64.ActiveCfg = Release|Any CPU
+ {8B3C1A2D-4E5F-6789-ABCD-EF0123456789}.Release|x86.ActiveCfg = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
diff --git a/src/Applications/ACATTalk/PerformanceMonitor.cs b/src/Applications/ACATTalk/PerformanceMonitor.cs
index a88896ee..c0511159 100644
--- a/src/Applications/ACATTalk/PerformanceMonitor.cs
+++ b/src/Applications/ACATTalk/PerformanceMonitor.cs
@@ -13,6 +13,8 @@
#if PERFORMANCE
+using ACAT.Core.Utility.Diagnostics;
+using ACAT.Core.Utility.Metrics;
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
@@ -27,6 +29,8 @@ namespace ACATTalk
///
/// Provides performance monitoring and baseline metrics collection
/// for ACATTalk application. Only compiled when PERFORMANCE symbol is defined.
+ /// Integrates , ,
+ /// and from the ACATCore library.
///
public static class PerformanceMonitor
{
@@ -38,6 +42,11 @@ public static class PerformanceMonitor
private static Timer _memoryMonitor;
private static readonly object _reportLock = new object();
+ // ---- ACATCore performance infrastructure ----
+ private static readonly RuntimeMetricsCollector _runtimeCollector = new RuntimeMetricsCollector();
+ private static readonly MemoryProfiler _memoryProfiler = new MemoryProfiler();
+ private static PerformanceRegressionDetector _regressionDetector;
+
///
/// Metric categories for organizing performance data
///
@@ -65,6 +74,19 @@ public static void Initialize()
// Monitor memory every 5 seconds
_memoryMonitor = new Timer(MonitorMemory, null, TimeSpan.FromSeconds(5), TimeSpan.FromSeconds(5));
+ // Start the ACATCore runtime metrics collector (5-second interval)
+ _runtimeCollector.Start(5000);
+
+ // Capture startup memory snapshot
+ _memoryProfiler.CaptureSnapshot("Startup");
+
+ // Load baseline (if present) or use defaults
+ string baselinePath = Path.Combine(
+ Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData),
+ "ACAT", "performance_baseline.json");
+ PerformanceBaselineData baseline = PerformanceBaseline.Load(baselinePath);
+ _regressionDetector = new PerformanceRegressionDetector(baseline);
+
LogEvent("PerformanceMonitor", "Performance monitoring initialized");
}
@@ -167,6 +189,7 @@ public static void Shutdown()
{
_applicationLifetime.Stop();
_memoryMonitor?.Dispose();
+ _runtimeCollector.Stop();
var process = Process.GetCurrentProcess();
long endWorkingSet = process.WorkingSet64;
@@ -177,6 +200,24 @@ public static void Shutdown()
RecordMetric("PeakMemoryUsage", _peakWorkingSet / (1024.0 * 1024.0), "MB", MetricCategory.Memory);
RecordMetric("MemoryGrowth", (endWorkingSet - _startWorkingSet) / (1024.0 * 1024.0), "MB", MetricCategory.Memory);
+ // Capture shutdown memory snapshot and check for regressions
+ MemorySnapshot shutdownSnap = _memoryProfiler.CaptureSnapshot("Shutdown");
+ if (_regressionDetector != null)
+ {
+ var observations = new Dictionary(StringComparer.OrdinalIgnoreCase)
+ {
+ ["TotalApplicationLifetime"] = _applicationLifetime.Elapsed.TotalSeconds * 1000,
+ ["PeakWorkingSetMB"] = _peakWorkingSet / (1024.0 * 1024.0),
+ ["ManagedHeapMB"] = shutdownSnap.ManagedHeapMB
+ };
+
+ IReadOnlyList regressions = _regressionDetector.DetectRegressions(observations);
+ foreach (RegressionResult r in regressions)
+ {
+ Debug.WriteLine($"[PerformanceMonitor] {r}");
+ }
+ }
+
GenerateReport();
}
diff --git a/src/Extensions/ACAT.Extensions.UI/ACAT.Extensions.UI.csproj b/src/Extensions/ACAT.Extensions.UI/ACAT.Extensions.UI.csproj
index ac67dd59..1789e2eb 100644
--- a/src/Extensions/ACAT.Extensions.UI/ACAT.Extensions.UI.csproj
+++ b/src/Extensions/ACAT.Extensions.UI/ACAT.Extensions.UI.csproj
@@ -26,6 +26,9 @@
+
+ PerformanceDashboard.xaml
+
@@ -141,6 +144,13 @@
+
+
+
+
+ MSBuild:Compile
+ Designer
+
diff --git a/src/Extensions/ACAT.Extensions.UI/Diagnostics/PerformanceDashboard.xaml b/src/Extensions/ACAT.Extensions.UI/Diagnostics/PerformanceDashboard.xaml
new file mode 100644
index 00000000..72cf90f7
--- /dev/null
+++ b/src/Extensions/ACAT.Extensions.UI/Diagnostics/PerformanceDashboard.xaml
@@ -0,0 +1,169 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/Extensions/ACAT.Extensions.UI/Diagnostics/PerformanceDashboard.xaml.cs b/src/Extensions/ACAT.Extensions.UI/Diagnostics/PerformanceDashboard.xaml.cs
new file mode 100644
index 00000000..e24a67dd
--- /dev/null
+++ b/src/Extensions/ACAT.Extensions.UI/Diagnostics/PerformanceDashboard.xaml.cs
@@ -0,0 +1,252 @@
+////////////////////////////////////////////////////////////////////////////
+//
+// Copyright 2013-2019; 2023 Intel Corporation
+// SPDX-License-Identifier: Apache-2.0
+//
+//
+// PerformanceDashboard.xaml.cs
+//
+// Code-behind for the WPF performance monitoring dashboard.
+// Refreshes live metrics every 2 seconds and supports CSV/JSON export.
+//
+////////////////////////////////////////////////////////////////////////////
+
+using ACAT.Core.Utility.Diagnostics;
+using ACAT.Core.Utility.Metrics;
+using Microsoft.Win32;
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Linq;
+using System.Text;
+using System.Text.Json;
+using System.Threading;
+using System.Windows;
+
+namespace ACAT.Extensions.UI.Diagnostics
+{
+ ///
+ /// Interaction logic for PerformanceDashboard.xaml.
+ /// Displays live runtime metrics, memory snapshots, and baseline
+ /// regression status in an accessible WPF window.
+ ///
+ public partial class PerformanceDashboard : Window
+ {
+ private readonly RuntimeMetricsCollector _collector;
+ private readonly MemoryProfiler _profiler;
+ private readonly PerformanceRegressionDetector _detector;
+ private Timer _refreshTimer;
+
+ ///
+ /// Initialises the dashboard with optional pre-existing components.
+ /// When parameters are omitted, new instances are created.
+ ///
+ /// Shared runtime-metrics collector (optional).
+ /// Shared memory profiler (optional).
+ /// Baseline thresholds for regression detection (optional).
+ public PerformanceDashboard(
+ RuntimeMetricsCollector collector = null,
+ MemoryProfiler profiler = null,
+ PerformanceBaselineData baseline = null)
+ {
+ InitializeComponent();
+
+ _collector = collector ?? new RuntimeMetricsCollector();
+ _profiler = profiler ?? new MemoryProfiler();
+ _detector = new PerformanceRegressionDetector(baseline);
+ }
+
+ // ----------------------------------------------------------------
+ // Window events
+ // ----------------------------------------------------------------
+
+ private void Window_Loaded(object sender, RoutedEventArgs e)
+ {
+ _refreshTimer = new Timer(OnRefreshTimer, null,
+ TimeSpan.Zero, TimeSpan.FromSeconds(2));
+ }
+
+ private void Window_Closed(object sender, EventArgs e)
+ {
+ _refreshTimer?.Dispose();
+ }
+
+ // ----------------------------------------------------------------
+ // Button handlers
+ // ----------------------------------------------------------------
+
+ private void OnRefreshClick(object sender, RoutedEventArgs e)
+ {
+ RefreshMetrics();
+ }
+
+ private void OnClearHistoryClick(object sender, RoutedEventArgs e)
+ {
+ _profiler.ClearSnapshots();
+ StatusBar.Text = "Sample history cleared.";
+ }
+
+ private void OnExportCsvClick(object sender, RoutedEventArgs e)
+ {
+ var dlg = new SaveFileDialog
+ {
+ Title = "Export Performance Data",
+ Filter = "CSV files (*.csv)|*.csv",
+ FileName = $"ACAT_Performance_{DateTime.Now:yyyyMMdd_HHmmss}.csv"
+ };
+
+ if (dlg.ShowDialog() == true)
+ {
+ ExportCsv(dlg.FileName);
+ }
+ }
+
+ private void OnExportJsonClick(object sender, RoutedEventArgs e)
+ {
+ var dlg = new SaveFileDialog
+ {
+ Title = "Export Performance Data",
+ Filter = "JSON files (*.json)|*.json",
+ FileName = $"ACAT_Performance_{DateTime.Now:yyyyMMdd_HHmmss}.json"
+ };
+
+ if (dlg.ShowDialog() == true)
+ {
+ ExportJson(dlg.FileName);
+ }
+ }
+
+ // ----------------------------------------------------------------
+ // Refresh logic
+ // ----------------------------------------------------------------
+
+ private void OnRefreshTimer(object state)
+ {
+ Dispatcher.InvokeAsync(RefreshMetrics);
+ }
+
+ private void RefreshMetrics()
+ {
+ try
+ {
+ MemorySnapshot snap = _profiler.CaptureSnapshot("Dashboard");
+ IReadOnlyList samples = _collector.GetSamples();
+
+ // Memory section
+ WorkingSetValue.Text = $"{snap.WorkingSetMB:F1} MB";
+ ManagedHeapValue.Text = $"{snap.ManagedHeapMB:F1} MB";
+ int totalGc = snap.GcCollections?.Sum() ?? 0;
+ GcCollectionsValue.Text = totalGc.ToString();
+
+ // Runtime section
+ UptimeValue.Text = FormatUptime(snap.Timestamp);
+ ThreadCountValue.Text = snap.ThreadCount.ToString();
+ HandleCountValue.Text = snap.HandleCount.ToString();
+
+ // Sample history
+ IReadOnlyList allSnaps = _profiler.GetSnapshots();
+ SampleCount.Text = $"{allSnaps.Count} sample(s)";
+ double peak = allSnaps.Count > 0 ? allSnaps.Max(s => s.WorkingSetMB) : 0;
+ PeakWorkingSet.Text = $"Peak WS: {peak:F1} MB";
+ LastSampleTime.Text = $"Last: {snap.Timestamp.ToLocalTime():HH:mm:ss}";
+
+ // Regression check
+ var observations = new Dictionary(StringComparer.OrdinalIgnoreCase)
+ {
+ ["PeakWorkingSetMB"] = peak,
+ ["ManagedHeapMB"] = snap.ManagedHeapMB
+ };
+
+ IReadOnlyList regressions = _detector.DetectRegressions(observations);
+ if (regressions.Count == 0)
+ {
+ RegressionStatus.Text = "✓ All metrics within baseline";
+ RegressionStatus.Foreground = System.Windows.Media.Brushes.LightGreen;
+ }
+ else
+ {
+ RegressionStatus.Text = string.Join(Environment.NewLine,
+ regressions.Select(r => $"⚠ {r.MetricName}: {r.ObservedValue:F1} > {r.ThresholdValue:F1} {r.Unit}"));
+ RegressionStatus.Foreground = System.Windows.Media.Brushes.OrangeRed;
+ }
+
+ StatusBar.Text = $"Updated {DateTime.Now:HH:mm:ss}";
+ }
+ catch (Exception ex)
+ {
+ StatusBar.Text = $"Refresh error: {ex.Message}";
+ }
+ }
+
+ // ----------------------------------------------------------------
+ // Export helpers
+ // ----------------------------------------------------------------
+
+ private void ExportCsv(string filePath)
+ {
+ try
+ {
+ IReadOnlyList snapshots = _profiler.GetSnapshots();
+ var sb = new StringBuilder();
+ sb.AppendLine("Timestamp,Label,WorkingSetMB,PrivateMemoryMB,ManagedHeapMB,ThreadCount,HandleCount");
+
+ foreach (MemorySnapshot s in snapshots)
+ {
+ sb.AppendLine(string.Format("{0:o},{1},{2:F2},{3:F2},{4:F2},{5},{6}",
+ s.Timestamp, s.Label,
+ s.WorkingSetMB, s.PrivateMemoryMB, s.ManagedHeapMB,
+ s.ThreadCount, s.HandleCount));
+ }
+
+ File.WriteAllText(filePath, sb.ToString());
+ StatusBar.Text = $"Exported to {filePath}";
+ }
+ catch (Exception ex)
+ {
+ StatusBar.Text = $"Export error: {ex.Message}";
+ }
+ }
+
+ private void ExportJson(string filePath)
+ {
+ try
+ {
+ IReadOnlyList snapshots = _profiler.GetSnapshots();
+ IReadOnlyDictionary entries = _collector.GetEntries();
+
+ var export = new
+ {
+ ExportedAt = DateTime.UtcNow,
+ MemorySnapshots = snapshots,
+ RuntimeMetrics = entries
+ };
+
+ var options = new JsonSerializerOptions { WriteIndented = true };
+ string json = JsonSerializer.Serialize(export, options);
+ File.WriteAllText(filePath, json);
+ StatusBar.Text = $"Exported to {filePath}";
+ }
+ catch (Exception ex)
+ {
+ StatusBar.Text = $"Export error: {ex.Message}";
+ }
+ }
+
+ // ----------------------------------------------------------------
+ // Private helpers
+ // ----------------------------------------------------------------
+
+ private static string FormatUptime(DateTime snapshotTime)
+ {
+ TimeSpan uptime = snapshotTime - System.Diagnostics.Process.GetCurrentProcess().StartTime.ToUniversalTime();
+ if (uptime.TotalSeconds < 0)
+ {
+ return "0 s";
+ }
+
+ return uptime.TotalHours >= 1
+ ? $"{uptime.Hours}h {uptime.Minutes}m"
+ : $"{(int)uptime.TotalSeconds} s";
+ }
+ }
+}
diff --git a/src/Libraries/ACATCore.Tests.Performance/ACATCore.Tests.Performance.csproj b/src/Libraries/ACATCore.Tests.Performance/ACATCore.Tests.Performance.csproj
new file mode 100644
index 00000000..80659c25
--- /dev/null
+++ b/src/Libraries/ACATCore.Tests.Performance/ACATCore.Tests.Performance.csproj
@@ -0,0 +1,28 @@
+
+
+ net481
+ 9.0
+ false
+ true
+ false
+
+
+ bin\$(Configuration)\
+ obj\
+ obj\$(Configuration)\
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/Libraries/ACATCore.Tests.Performance/MemoryProfilerTests.cs b/src/Libraries/ACATCore.Tests.Performance/MemoryProfilerTests.cs
new file mode 100644
index 00000000..d6ee1651
--- /dev/null
+++ b/src/Libraries/ACATCore.Tests.Performance/MemoryProfilerTests.cs
@@ -0,0 +1,108 @@
+////////////////////////////////////////////////////////////////////////////
+//
+// Copyright 2013-2019; 2023 Intel Corporation
+// SPDX-License-Identifier: Apache-2.0
+//
+//
+// MemoryProfilerTests.cs
+//
+// Unit tests for MemoryProfiler.
+//
+////////////////////////////////////////////////////////////////////////////
+
+using ACAT.Core.Utility.Diagnostics;
+using Microsoft.VisualStudio.TestTools.UnitTesting;
+using System.Collections.Generic;
+
+namespace ACATCore.Tests.Performance
+{
+ [TestClass]
+ public class MemoryProfilerTests
+ {
+ [TestMethod]
+ public void CaptureSnapshot_ReturnsValidSnapshot()
+ {
+ var profiler = new MemoryProfiler();
+
+ MemorySnapshot snap = profiler.CaptureSnapshot("Test");
+
+ Assert.IsNotNull(snap);
+ Assert.AreEqual("Test", snap.Label);
+ Assert.IsTrue(snap.WorkingSetMB > 0, "Working set must be positive");
+ Assert.IsTrue(snap.ManagedHeapMB >= 0, "Managed heap must be non-negative");
+ Assert.IsTrue(snap.ThreadCount > 0, "Thread count must be positive");
+ }
+
+ [TestMethod]
+ public void CaptureSnapshot_StoredInList()
+ {
+ var profiler = new MemoryProfiler();
+
+ profiler.CaptureSnapshot("First");
+ profiler.CaptureSnapshot("Second");
+
+ IReadOnlyList snapshots = profiler.GetSnapshots();
+ Assert.AreEqual(2, snapshots.Count);
+ Assert.AreEqual("First", snapshots[0].Label);
+ Assert.AreEqual("Second", snapshots[1].Label);
+ }
+
+ [TestMethod]
+ public void ClearSnapshots_RemovesAll()
+ {
+ var profiler = new MemoryProfiler();
+ profiler.CaptureSnapshot("A");
+ profiler.CaptureSnapshot("B");
+
+ profiler.ClearSnapshots();
+
+ Assert.AreEqual(0, profiler.GetSnapshots().Count);
+ }
+
+ [TestMethod]
+ public void CompareSnapshots_ReturnsReport()
+ {
+ var profiler = new MemoryProfiler();
+
+ MemorySnapshot before = profiler.CaptureSnapshot("Before");
+ // Allocate a small amount so the snapshot differs
+ byte[] dummy = new byte[1024];
+ MemorySnapshot after = profiler.CaptureSnapshot("After");
+
+ string report = MemoryProfiler.CompareSnapshots(before, after);
+
+ Assert.IsFalse(string.IsNullOrEmpty(report));
+ Assert.IsTrue(report.Contains("Memory Delta Report"));
+ Assert.IsTrue(report.Contains("Working Set"));
+ }
+
+ [TestMethod]
+ public void IsPotentialLeak_BelowThreshold_ReturnsFalse()
+ {
+ var before = new MemorySnapshot { WorkingSetMB = 100.0 };
+ var after = new MemorySnapshot { WorkingSetMB = 140.0 };
+
+ bool result = MemoryProfiler.IsPotentialLeak(before, after, thresholdMB: 50.0);
+
+ Assert.IsFalse(result);
+ }
+
+ [TestMethod]
+ public void IsPotentialLeak_AboveThreshold_ReturnsTrue()
+ {
+ var before = new MemorySnapshot { WorkingSetMB = 100.0 };
+ var after = new MemorySnapshot { WorkingSetMB = 200.0 };
+
+ bool result = MemoryProfiler.IsPotentialLeak(before, after, thresholdMB: 50.0);
+
+ Assert.IsTrue(result);
+ }
+
+ [TestMethod]
+ public void IsPotentialLeak_NullArguments_ReturnsFalse()
+ {
+ Assert.IsFalse(MemoryProfiler.IsPotentialLeak(null, new MemorySnapshot()));
+ Assert.IsFalse(MemoryProfiler.IsPotentialLeak(new MemorySnapshot(), null));
+ }
+ }
+}
diff --git a/src/Libraries/ACATCore.Tests.Performance/PerformanceRegressionDetectorTests.cs b/src/Libraries/ACATCore.Tests.Performance/PerformanceRegressionDetectorTests.cs
new file mode 100644
index 00000000..25b41fe7
--- /dev/null
+++ b/src/Libraries/ACATCore.Tests.Performance/PerformanceRegressionDetectorTests.cs
@@ -0,0 +1,219 @@
+////////////////////////////////////////////////////////////////////////////
+//
+// Copyright 2013-2019; 2023 Intel Corporation
+// SPDX-License-Identifier: Apache-2.0
+//
+//
+// PerformanceRegressionDetectorTests.cs
+//
+// Unit tests for PerformanceRegressionDetector and PerformanceBaseline.
+//
+////////////////////////////////////////////////////////////////////////////
+
+using ACAT.Core.Utility.Diagnostics;
+using Microsoft.VisualStudio.TestTools.UnitTesting;
+using System;
+using System.Collections.Generic;
+using System.IO;
+
+namespace ACATCore.Tests.Performance
+{
+ [TestClass]
+ public class PerformanceRegressionDetectorTests
+ {
+ // ----------------------------------------------------------------
+ // PerformanceBaseline tests
+ // ----------------------------------------------------------------
+
+ [TestMethod]
+ public void CreateDefault_ContainsExpectedThresholds()
+ {
+ PerformanceBaselineData data = PerformanceBaseline.CreateDefault();
+
+ Assert.IsNotNull(data);
+ Assert.IsTrue(data.Thresholds.ContainsKey("StartupTime"));
+ Assert.IsTrue(data.Thresholds.ContainsKey("UiInputLag"));
+ Assert.IsTrue(data.Thresholds.ContainsKey("PredictionLatency"));
+ Assert.IsTrue(data.Thresholds.ContainsKey("PeakWorkingSetMB"));
+ Assert.IsTrue(data.Thresholds.ContainsKey("ManagedHeapMB"));
+ }
+
+ [TestMethod]
+ public void SaveAndLoad_RoundTrip_PreservesThresholds()
+ {
+ string tmpFile = Path.Combine(Path.GetTempPath(), $"perf_baseline_{Guid.NewGuid()}.json");
+ try
+ {
+ PerformanceBaselineData original = PerformanceBaseline.CreateDefault();
+ PerformanceBaseline.Save(original, tmpFile);
+
+ PerformanceBaselineData loaded = PerformanceBaseline.Load(tmpFile);
+
+ Assert.IsNotNull(loaded);
+ foreach (string key in original.Thresholds.Keys)
+ {
+ Assert.IsTrue(loaded.Thresholds.ContainsKey(key),
+ $"Expected threshold '{key}' to be present after round-trip");
+ Assert.AreEqual(
+ original.Thresholds[key].MaxAcceptableValue,
+ loaded.Thresholds[key].MaxAcceptableValue,
+ 0.001,
+ $"Threshold value mismatch for '{key}'");
+ }
+ }
+ finally
+ {
+ File.Delete(tmpFile);
+ }
+ }
+
+ [TestMethod]
+ public void Load_MissingFile_ReturnsDefault()
+ {
+ string missingPath = Path.Combine(Path.GetTempPath(), "does_not_exist_abc123.json");
+
+ PerformanceBaselineData result = PerformanceBaseline.Load(missingPath);
+
+ Assert.IsNotNull(result);
+ Assert.IsTrue(result.Thresholds.Count > 0);
+ }
+
+ // ----------------------------------------------------------------
+ // PerformanceRegressionDetector tests
+ // ----------------------------------------------------------------
+
+ [TestMethod]
+ public void IsRegression_BelowThreshold_ReturnsFalse()
+ {
+ var detector = new PerformanceRegressionDetector();
+
+ bool regression = detector.IsRegression("StartupTime", 1500.0, out RegressionResult result);
+
+ Assert.IsFalse(regression);
+ Assert.IsNull(result);
+ }
+
+ [TestMethod]
+ public void IsRegression_AboveThreshold_ReturnsTrue()
+ {
+ var detector = new PerformanceRegressionDetector();
+
+ bool regression = detector.IsRegression("StartupTime", 5000.0, out RegressionResult result);
+
+ Assert.IsTrue(regression);
+ Assert.IsNotNull(result);
+ Assert.AreEqual("StartupTime", result.MetricName);
+ Assert.AreEqual(5000.0, result.ObservedValue, 0.001);
+ Assert.IsTrue(result.ExceedancePercent > 0);
+ }
+
+ [TestMethod]
+ public void IsRegression_UnknownMetric_ReturnsFalse()
+ {
+ var detector = new PerformanceRegressionDetector();
+
+ bool regression = detector.IsRegression("UnknownMetric", 99999.0, out RegressionResult result);
+
+ Assert.IsFalse(regression);
+ Assert.IsNull(result);
+ }
+
+ [TestMethod]
+ public void DetectRegressions_MultipleMetrics_FindsOnlyExceeded()
+ {
+ var detector = new PerformanceRegressionDetector();
+ var observations = new Dictionary(StringComparer.OrdinalIgnoreCase)
+ {
+ ["StartupTime"] = 1000.0, // under 3000ms threshold – OK
+ ["UiInputLag"] = 200.0, // over 100ms threshold – regression
+ ["PeakWorkingSetMB"] = 600.0 // over 500MB threshold – regression
+ };
+
+ IReadOnlyList regressions = detector.DetectRegressions(observations);
+
+ Assert.AreEqual(2, regressions.Count);
+ }
+
+ [TestMethod]
+ public void DetectRegressions_NullInput_ReturnsEmptyList()
+ {
+ var detector = new PerformanceRegressionDetector();
+
+ IReadOnlyList regressions = detector.DetectRegressions(null);
+
+ Assert.IsNotNull(regressions);
+ Assert.AreEqual(0, regressions.Count);
+ }
+
+ [TestMethod]
+ public void GenerateReport_NoRegressions_IndicatesPass()
+ {
+ var detector = new PerformanceRegressionDetector();
+ var observations = new Dictionary(StringComparer.OrdinalIgnoreCase)
+ {
+ ["StartupTime"] = 1000.0,
+ ["UiInputLag"] = 50.0
+ };
+
+ string report = detector.GenerateReport(observations);
+
+ Assert.IsTrue(report.Contains("No regressions detected"),
+ "Report should indicate no regressions when all metrics pass");
+ }
+
+ [TestMethod]
+ public void GenerateReport_WithRegressions_IncludesMetricName()
+ {
+ var detector = new PerformanceRegressionDetector();
+ var observations = new Dictionary(StringComparer.OrdinalIgnoreCase)
+ {
+ ["StartupTime"] = 9999.0 // well above threshold
+ };
+
+ string report = detector.GenerateReport(observations);
+
+ Assert.IsTrue(report.Contains("StartupTime"),
+ "Report should include regressing metric name");
+ }
+
+ [TestMethod]
+ public void RegressionResult_ToString_ContainsKeyInfo()
+ {
+ var result = new RegressionResult
+ {
+ MetricName = "UiInputLag",
+ ObservedValue = 250.0,
+ ThresholdValue = 100.0,
+ Unit = "ms"
+ };
+
+ string text = result.ToString();
+
+ Assert.IsTrue(text.Contains("UiInputLag"));
+ Assert.IsTrue(text.Contains("250"));
+ Assert.IsTrue(text.Contains("100"));
+ }
+
+ [TestMethod]
+ public void PerformanceBaseline_Save_CreatesDirectory()
+ {
+ string tmpDir = Path.Combine(Path.GetTempPath(), $"acat_test_{Guid.NewGuid()}");
+ string filePath = Path.Combine(tmpDir, "baseline.json");
+
+ try
+ {
+ PerformanceBaselineData data = PerformanceBaseline.CreateDefault();
+ PerformanceBaseline.Save(data, filePath);
+
+ Assert.IsTrue(File.Exists(filePath), "Baseline file should have been created");
+ }
+ finally
+ {
+ if (Directory.Exists(tmpDir))
+ {
+ Directory.Delete(tmpDir, recursive: true);
+ }
+ }
+ }
+ }
+}
diff --git a/src/Libraries/ACATCore.Tests.Performance/RuntimeMetricsCollectorTests.cs b/src/Libraries/ACATCore.Tests.Performance/RuntimeMetricsCollectorTests.cs
new file mode 100644
index 00000000..d3c48f89
--- /dev/null
+++ b/src/Libraries/ACATCore.Tests.Performance/RuntimeMetricsCollectorTests.cs
@@ -0,0 +1,129 @@
+////////////////////////////////////////////////////////////////////////////
+//
+// Copyright 2013-2019; 2023 Intel Corporation
+// SPDX-License-Identifier: Apache-2.0
+//
+//
+// RuntimeMetricsCollectorTests.cs
+//
+// Unit tests for RuntimeMetricsCollector.
+//
+////////////////////////////////////////////////////////////////////////////
+
+using ACAT.Core.Utility.Metrics;
+using Microsoft.VisualStudio.TestTools.UnitTesting;
+using System;
+using System.Collections.Generic;
+using System.Threading;
+
+namespace ACATCore.Tests.Performance
+{
+ [TestClass]
+ public class RuntimeMetricsCollectorTests
+ {
+ [TestMethod]
+ public void Record_SingleValue_StoresEntry()
+ {
+ var collector = new RuntimeMetricsCollector();
+
+ collector.Record("TestMetric", 42.0, RuntimeMetricCategory.General, "ms");
+
+ IReadOnlyDictionary entries = collector.GetEntries();
+ Assert.IsTrue(entries.ContainsKey("TestMetric"));
+ Assert.AreEqual(42.0, entries["TestMetric"].LastValue, 0.001);
+ Assert.AreEqual(1, entries["TestMetric"].Count);
+ }
+
+ [TestMethod]
+ public void Record_MultipleValues_AggregatesCorrectly()
+ {
+ var collector = new RuntimeMetricsCollector();
+
+ collector.Record("Latency", 10.0, RuntimeMetricCategory.Ui, "ms");
+ collector.Record("Latency", 20.0, RuntimeMetricCategory.Ui, "ms");
+ collector.Record("Latency", 30.0, RuntimeMetricCategory.Ui, "ms");
+
+ IReadOnlyDictionary entries = collector.GetEntries();
+ RuntimeMetricEntry entry = entries["Latency"];
+
+ Assert.AreEqual(3, entry.Count);
+ Assert.AreEqual(10.0, entry.Min, 0.001);
+ Assert.AreEqual(30.0, entry.Max, 0.001);
+ Assert.AreEqual(20.0, entry.Average, 0.001);
+ }
+
+ [TestMethod]
+ public void Record_NullOrEmptyName_DoesNotThrow()
+ {
+ var collector = new RuntimeMetricsCollector();
+
+ collector.Record(null, 1.0);
+ collector.Record(string.Empty, 1.0);
+
+ Assert.AreEqual(0, collector.GetEntries().Count);
+ }
+
+ [TestMethod]
+ public void Start_CollectsSamplesOverTime()
+ {
+ var collector = new RuntimeMetricsCollector();
+ var samplesReceived = new List();
+ collector.SampleCaptured += (s, e) => samplesReceived.Add(e);
+
+ collector.Start(intervalMs: 200);
+ Thread.Sleep(700);
+ collector.Stop();
+
+ // Expect at least 2 samples in ~700 ms with a 200 ms interval
+ Assert.IsTrue(samplesReceived.Count >= 2,
+ $"Expected >= 2 samples, got {samplesReceived.Count}");
+ }
+
+ [TestMethod]
+ public void Start_SamplesHavePositiveWorkingSet()
+ {
+ var collector = new RuntimeMetricsCollector();
+ collector.Start(intervalMs: 100);
+ Thread.Sleep(250);
+ collector.Stop();
+
+ IReadOnlyList samples = collector.GetSamples();
+ Assert.IsTrue(samples.Count > 0, "Should have captured at least one sample");
+
+ foreach (RuntimeMetricSample sample in samples)
+ {
+ Assert.IsTrue(sample.WorkingSetMB > 0, "Working set must be positive");
+ Assert.IsTrue(sample.ManagedHeapMB >= 0, "Managed heap must be non-negative");
+ Assert.IsTrue(sample.ThreadCount > 0, "Thread count must be positive");
+ }
+ }
+
+ [TestMethod]
+ public void Dispose_StopsSampling()
+ {
+ var collector = new RuntimeMetricsCollector();
+ collector.Start(intervalMs: 100);
+ Thread.Sleep(150);
+ collector.Dispose();
+
+ int countAfterDispose = collector.GetSamples().Count;
+ Thread.Sleep(300);
+ int countAfterWait = collector.GetSamples().Count;
+
+ Assert.AreEqual(countAfterDispose, countAfterWait,
+ "No new samples should be added after Dispose");
+ }
+
+ [TestMethod]
+ public void Record_CategoryAndUnitPreserved()
+ {
+ var collector = new RuntimeMetricsCollector();
+
+ collector.Record("PredLatency", 123.4, RuntimeMetricCategory.Prediction, "ms");
+
+ RuntimeMetricEntry entry = collector.GetEntries()["PredLatency"];
+ Assert.AreEqual(RuntimeMetricCategory.Prediction, entry.Category);
+ Assert.AreEqual("ms", entry.Unit);
+ }
+ }
+}
diff --git a/src/Libraries/ACATCore/ACAT.Core.csproj b/src/Libraries/ACATCore/ACAT.Core.csproj
index ce67ba75..d78a998b 100644
--- a/src/Libraries/ACATCore/ACAT.Core.csproj
+++ b/src/Libraries/ACATCore/ACAT.Core.csproj
@@ -420,6 +420,10 @@
+
+
+
+
diff --git a/src/Libraries/ACATCore/Utility/Diagnostics/MemoryProfiler.cs b/src/Libraries/ACATCore/Utility/Diagnostics/MemoryProfiler.cs
new file mode 100644
index 00000000..0d71c47b
--- /dev/null
+++ b/src/Libraries/ACATCore/Utility/Diagnostics/MemoryProfiler.cs
@@ -0,0 +1,181 @@
+////////////////////////////////////////////////////////////////////////////
+//
+// Copyright 2013-2019; 2023 Intel Corporation
+// SPDX-License-Identifier: Apache-2.0
+//
+//
+// MemoryProfiler.cs
+//
+// Captures memory snapshots and detects potential memory leaks by comparing
+// snapshots across key application lifecycle points.
+//
+////////////////////////////////////////////////////////////////////////////
+
+using System;
+using System.Collections.Generic;
+using System.Diagnostics;
+using System.Text;
+
+namespace ACAT.Core.Utility.Diagnostics
+{
+ ///
+ /// A point-in-time snapshot of the process memory state.
+ ///
+ public class MemorySnapshot
+ {
+ /// UTC timestamp of the snapshot.
+ public DateTime Timestamp { get; set; }
+
+ /// Human-readable label for this snapshot (e.g. "AfterStartup").
+ public string Label { get; set; }
+
+ /// Process working-set size in megabytes.
+ public double WorkingSetMB { get; set; }
+
+ /// Private (committed) memory in megabytes.
+ public double PrivateMemoryMB { get; set; }
+
+ /// Total managed (GC) heap size in megabytes.
+ public double ManagedHeapMB { get; set; }
+
+ /// GC collection counts per generation (index 0 = Gen0).
+ public int[] GcCollections { get; set; }
+
+ /// Number of OS threads owned by the process.
+ public int ThreadCount { get; set; }
+
+ /// Number of open file handles.
+ public int HandleCount { get; set; }
+ }
+
+ ///
+ /// Captures memory snapshots at named points in the application lifecycle
+ /// and provides basic leak-detection by comparing two snapshots.
+ ///
+ public class MemoryProfiler
+ {
+ private readonly List _snapshots = new List();
+ private readonly object _lock = new object();
+
+ ///
+ /// Capture a snapshot of the current process memory state.
+ ///
+ /// Optional descriptive label for this snapshot.
+ /// The captured snapshot.
+ public MemorySnapshot CaptureSnapshot(string label = "")
+ {
+ var process = Process.GetCurrentProcess();
+ process.Refresh();
+
+ int maxGen = GC.MaxGeneration;
+ var gcCounts = new int[maxGen + 1];
+ for (int gen = 0; gen <= maxGen; gen++)
+ {
+ gcCounts[gen] = GC.CollectionCount(gen);
+ }
+
+ var snapshot = new MemorySnapshot
+ {
+ Timestamp = DateTime.UtcNow,
+ Label = label ?? string.Empty,
+ WorkingSetMB = process.WorkingSet64 / (1024.0 * 1024.0),
+ PrivateMemoryMB = process.PrivateMemorySize64 / (1024.0 * 1024.0),
+ ManagedHeapMB = GC.GetTotalMemory(false) / (1024.0 * 1024.0),
+ GcCollections = gcCounts,
+ ThreadCount = process.Threads.Count,
+ HandleCount = process.HandleCount
+ };
+
+ lock (_lock)
+ {
+ _snapshots.Add(snapshot);
+ }
+
+ return snapshot;
+ }
+
+ ///
+ /// Returns all captured snapshots in capture order.
+ ///
+ public IReadOnlyList GetSnapshots()
+ {
+ lock (_lock)
+ {
+ return new List(_snapshots);
+ }
+ }
+
+ ///
+ /// Clears all stored snapshots.
+ ///
+ public void ClearSnapshots()
+ {
+ lock (_lock)
+ {
+ _snapshots.Clear();
+ }
+ }
+
+ ///
+ /// Compares two snapshots and returns a human-readable delta report.
+ ///
+ /// Earlier snapshot.
+ /// Later snapshot.
+ /// Report describing the memory delta between the two snapshots.
+ public static string CompareSnapshots(MemorySnapshot before, MemorySnapshot after)
+ {
+ if (before == null) throw new ArgumentNullException("before");
+ if (after == null) throw new ArgumentNullException("after");
+
+ var sb = new StringBuilder();
+ sb.AppendLine("Memory Delta Report");
+ sb.AppendLine(new string('-', 50));
+ sb.AppendFormat(" From : {0} ({1}){2}", before.Timestamp.ToLocalTime(), before.Label, Environment.NewLine);
+ sb.AppendFormat(" To : {0} ({1}){2}", after.Timestamp.ToLocalTime(), after.Label, Environment.NewLine);
+ sb.AppendLine();
+
+ double wsDelta = after.WorkingSetMB - before.WorkingSetMB;
+ double pmDelta = after.PrivateMemoryMB - before.PrivateMemoryMB;
+ double mhDelta = after.ManagedHeapMB - before.ManagedHeapMB;
+
+ sb.AppendFormat(" Working Set : {0:F1} MB → {1:F1} MB (Δ {2:+0.0;-0.0} MB){3}",
+ before.WorkingSetMB, after.WorkingSetMB, wsDelta, Environment.NewLine);
+ sb.AppendFormat(" Private Memory : {0:F1} MB → {1:F1} MB (Δ {2:+0.0;-0.0} MB){3}",
+ before.PrivateMemoryMB, after.PrivateMemoryMB, pmDelta, Environment.NewLine);
+ sb.AppendFormat(" Managed Heap : {0:F1} MB → {1:F1} MB (Δ {2:+0.0;-0.0} MB){3}",
+ before.ManagedHeapMB, after.ManagedHeapMB, mhDelta, Environment.NewLine);
+ sb.AppendFormat(" Threads : {0} → {1}{2}",
+ before.ThreadCount, after.ThreadCount, Environment.NewLine);
+ sb.AppendFormat(" Handles : {0} → {1}{2}",
+ before.HandleCount, after.HandleCount, Environment.NewLine);
+
+ if (before.GcCollections != null && after.GcCollections != null)
+ {
+ sb.AppendLine();
+ sb.AppendLine(" GC Collections:");
+ int gens = Math.Min(before.GcCollections.Length, after.GcCollections.Length);
+ for (int gen = 0; gen < gens; gen++)
+ {
+ int delta = after.GcCollections[gen] - before.GcCollections[gen];
+ sb.AppendFormat(" Gen{0}: {1:+0;-0;0} collection(s){2}", gen, delta, Environment.NewLine);
+ }
+ }
+
+ return sb.ToString();
+ }
+
+ ///
+ /// Returns true if the working-set growth between two snapshots
+ /// exceeds megabytes, indicating a potential leak.
+ ///
+ public static bool IsPotentialLeak(MemorySnapshot before, MemorySnapshot after, double thresholdMB = 50.0)
+ {
+ if (before == null || after == null)
+ {
+ return false;
+ }
+
+ return (after.WorkingSetMB - before.WorkingSetMB) > thresholdMB;
+ }
+ }
+}
diff --git a/src/Libraries/ACATCore/Utility/Diagnostics/PerformanceBaseline.cs b/src/Libraries/ACATCore/Utility/Diagnostics/PerformanceBaseline.cs
new file mode 100644
index 00000000..90716e37
--- /dev/null
+++ b/src/Libraries/ACATCore/Utility/Diagnostics/PerformanceBaseline.cs
@@ -0,0 +1,146 @@
+////////////////////////////////////////////////////////////////////////////
+//
+// Copyright 2013-2019; 2023 Intel Corporation
+// SPDX-License-Identifier: Apache-2.0
+//
+//
+// PerformanceBaseline.cs
+//
+// Stores and persists named performance baseline thresholds for regression
+// detection. Baselines are serialised as JSON so they can be committed to
+// source control or updated by CI pipelines.
+//
+////////////////////////////////////////////////////////////////////////////
+
+using System;
+using System.Collections.Generic;
+using System.Diagnostics;
+using System.IO;
+using System.Text.Json;
+
+namespace ACAT.Core.Utility.Diagnostics
+{
+ ///
+ /// A single named performance threshold used for regression detection.
+ ///
+ public class PerformanceThreshold
+ {
+ /// Human-readable name of the metric (e.g. "StartupTime").
+ public string Name { get; set; }
+
+ /// Maximum acceptable value. Values above this are regressions.
+ public double MaxAcceptableValue { get; set; }
+
+ /// Unit of the metric value (e.g. "ms", "MB").
+ public string Unit { get; set; }
+
+ /// Optional description of why this threshold was chosen.
+ public string Description { get; set; }
+ }
+
+ ///
+ /// Container for a complete set of baseline thresholds.
+ ///
+ public class PerformanceBaselineData
+ {
+ /// Version label for this baseline snapshot.
+ public string Version { get; set; } = "1.0";
+
+ /// UTC date/time when this baseline was recorded.
+ public DateTime CreatedAt { get; set; } = DateTime.UtcNow;
+
+ /// All defined thresholds, keyed by metric name.
+ public Dictionary Thresholds { get; set; }
+ = new Dictionary(StringComparer.OrdinalIgnoreCase);
+ }
+
+ ///
+ /// Loads and saves performance baselines from/to a JSON file.
+ ///
+ public static class PerformanceBaseline
+ {
+ private static readonly JsonSerializerOptions _jsonOptions = new JsonSerializerOptions
+ {
+ WriteIndented = true,
+ PropertyNamingPolicy = JsonNamingPolicy.CamelCase
+ };
+
+ ///
+ /// Returns a default baseline with well-known ACAT performance targets.
+ ///
+ public static PerformanceBaselineData CreateDefault()
+ {
+ var data = new PerformanceBaselineData();
+
+ Add(data, "StartupTime", 3000, "ms", "Application must start within 3 seconds");
+ Add(data, "UiInputLag", 100, "ms", "UI input response must be < 100 ms");
+ Add(data, "PredictionLatency", 500, "ms", "Word-prediction latency must be < 500 ms");
+ Add(data, "PeakWorkingSetMB", 500, "MB", "Peak working set must be < 500 MB");
+ Add(data, "ManagedHeapMB", 200, "MB", "Managed heap must be < 200 MB");
+
+ return data;
+ }
+
+ ///
+ /// Saves a baseline to a JSON file.
+ ///
+ /// The baseline data to save.
+ /// Destination file path.
+ public static void Save(PerformanceBaselineData data, string filePath)
+ {
+ if (data == null) throw new ArgumentNullException("data");
+ if (string.IsNullOrEmpty(filePath)) throw new ArgumentNullException("filePath");
+
+ string directory = Path.GetDirectoryName(filePath);
+ if (!string.IsNullOrEmpty(directory) && !Directory.Exists(directory))
+ {
+ Directory.CreateDirectory(directory);
+ }
+
+ string json = JsonSerializer.Serialize(data, _jsonOptions);
+ File.WriteAllText(filePath, json);
+ }
+
+ ///
+ /// Loads a baseline from a JSON file.
+ /// Returns when the file does not exist.
+ ///
+ /// Source file path.
+ /// Loaded or default baseline data.
+ public static PerformanceBaselineData Load(string filePath)
+ {
+ if (!File.Exists(filePath))
+ {
+ return CreateDefault();
+ }
+
+ try
+ {
+ string json = File.ReadAllText(filePath);
+ return JsonSerializer.Deserialize(json, _jsonOptions)
+ ?? CreateDefault();
+ }
+ catch (Exception ex)
+ {
+ Debug.WriteLine($"[PerformanceBaseline] Failed to load '{filePath}': {ex.Message}");
+ return CreateDefault();
+ }
+ }
+
+ // ----------------------------------------------------------------
+ // Private helpers
+ // ----------------------------------------------------------------
+
+ private static void Add(PerformanceBaselineData data, string name,
+ double max, string unit, string description)
+ {
+ data.Thresholds[name] = new PerformanceThreshold
+ {
+ Name = name,
+ MaxAcceptableValue = max,
+ Unit = unit,
+ Description = description
+ };
+ }
+ }
+}
diff --git a/src/Libraries/ACATCore/Utility/Diagnostics/PerformanceRegressionDetector.cs b/src/Libraries/ACATCore/Utility/Diagnostics/PerformanceRegressionDetector.cs
new file mode 100644
index 00000000..4fb1e16a
--- /dev/null
+++ b/src/Libraries/ACATCore/Utility/Diagnostics/PerformanceRegressionDetector.cs
@@ -0,0 +1,168 @@
+////////////////////////////////////////////////////////////////////////////
+//
+// Copyright 2013-2019; 2023 Intel Corporation
+// SPDX-License-Identifier: Apache-2.0
+//
+//
+// PerformanceRegressionDetector.cs
+//
+// Compares observed metric values against a persisted baseline and reports
+// any values that exceed their defined thresholds.
+//
+////////////////////////////////////////////////////////////////////////////
+
+using System;
+using System.Collections.Generic;
+using System.Text;
+
+namespace ACAT.Core.Utility.Diagnostics
+{
+ ///
+ /// Describes a single detected performance regression.
+ ///
+ public class RegressionResult
+ {
+ /// Metric name that regressed.
+ public string MetricName { get; set; }
+
+ /// Observed value of the metric.
+ public double ObservedValue { get; set; }
+
+ /// Baseline threshold that was exceeded.
+ public double ThresholdValue { get; set; }
+
+ /// Percentage by which the threshold was exceeded.
+ public double ExceedancePercent =>
+ ThresholdValue > 0 ? ((ObservedValue - ThresholdValue) / ThresholdValue) * 100.0 : 0.0;
+
+ /// Unit of the metric value.
+ public string Unit { get; set; }
+
+ /// Human-readable description of the regression.
+ public string Description { get; set; }
+
+ ///
+ public override string ToString()
+ {
+ return string.Format(
+ "REGRESSION [{0}]: observed {1:F1} {2} exceeds threshold {3:F1} {2} (+{4:F1}%)",
+ MetricName, ObservedValue, Unit, ThresholdValue, ExceedancePercent);
+ }
+ }
+
+ ///
+ /// Compares observed metric values against a
+ /// and identifies regressions.
+ ///
+ public class PerformanceRegressionDetector
+ {
+ private readonly PerformanceBaselineData _baseline;
+
+ ///
+ /// Initialises the detector with the supplied baseline.
+ ///
+ /// Baseline thresholds to compare against.
+ /// If null, is used.
+ public PerformanceRegressionDetector(PerformanceBaselineData baseline = null)
+ {
+ _baseline = baseline ?? PerformanceBaseline.CreateDefault();
+ }
+
+ ///
+ /// Checks a single observed value against its baseline threshold.
+ ///
+ /// Name of the metric (must match a threshold in the baseline).
+ /// Measured value.
+ /// Populated when the method returns true.
+ /// true if the observed value exceeds the threshold.
+ public bool IsRegression(string metricName, double observedValue,
+ out RegressionResult result)
+ {
+ result = null;
+
+ if (string.IsNullOrEmpty(metricName))
+ {
+ return false;
+ }
+
+ if (!_baseline.Thresholds.TryGetValue(metricName, out PerformanceThreshold threshold))
+ {
+ return false;
+ }
+
+ if (observedValue <= threshold.MaxAcceptableValue)
+ {
+ return false;
+ }
+
+ result = new RegressionResult
+ {
+ MetricName = metricName,
+ ObservedValue = observedValue,
+ ThresholdValue = threshold.MaxAcceptableValue,
+ Unit = threshold.Unit ?? string.Empty,
+ Description = threshold.Description ?? string.Empty
+ };
+
+ return true;
+ }
+
+ ///
+ /// Checks a dictionary of observed values against all matching thresholds.
+ ///
+ /// Map of metric name → observed value.
+ /// All detected regressions (empty list when none).
+ public IReadOnlyList DetectRegressions(
+ IDictionary observations)
+ {
+ var regressions = new List();
+
+ if (observations == null)
+ {
+ return regressions;
+ }
+
+ foreach (KeyValuePair kv in observations)
+ {
+ if (IsRegression(kv.Key, kv.Value, out RegressionResult r))
+ {
+ regressions.Add(r);
+ }
+ }
+
+ return regressions;
+ }
+
+ ///
+ /// Produces a human-readable summary report of all regressions found in
+ /// .
+ ///
+ /// Map of metric name → observed value.
+ /// Report string; indicates "no regressions" when the list is empty.
+ public string GenerateReport(IDictionary observations)
+ {
+ IReadOnlyList regressions = DetectRegressions(observations);
+
+ var sb = new StringBuilder();
+ sb.AppendLine("Performance Regression Report");
+ sb.AppendLine(new string('=', 60));
+
+ if (regressions.Count == 0)
+ {
+ sb.AppendLine(" No regressions detected. All metrics within baseline.");
+ }
+ else
+ {
+ sb.AppendFormat(" {0} regression(s) detected:{1}", regressions.Count, Environment.NewLine);
+ sb.AppendLine();
+ foreach (RegressionResult r in regressions)
+ {
+ sb.AppendLine(" " + r);
+ }
+ }
+
+ sb.AppendLine(new string('=', 60));
+ return sb.ToString();
+ }
+ }
+}
diff --git a/src/Libraries/ACATCore/Utility/Metrics/RuntimeMetricsCollector.cs b/src/Libraries/ACATCore/Utility/Metrics/RuntimeMetricsCollector.cs
new file mode 100644
index 00000000..9821441f
--- /dev/null
+++ b/src/Libraries/ACATCore/Utility/Metrics/RuntimeMetricsCollector.cs
@@ -0,0 +1,254 @@
+////////////////////////////////////////////////////////////////////////////
+//
+// Copyright 2013-2019; 2023 Intel Corporation
+// SPDX-License-Identifier: Apache-2.0
+//
+//
+// RuntimeMetricsCollector.cs
+//
+// Collects runtime performance metrics at configurable intervals.
+// Tracks CPU, memory, GC, and I/O counters without blocking the UI thread.
+//
+////////////////////////////////////////////////////////////////////////////
+
+using System;
+using System.Collections.Concurrent;
+using System.Collections.Generic;
+using System.Diagnostics;
+using System.Threading;
+
+namespace ACAT.Core.Utility.Metrics
+{
+ ///
+ /// Snapshot of runtime performance counters captured at a single point in time.
+ ///
+ public class RuntimeMetricSample
+ {
+ /// UTC timestamp of the sample.
+ public DateTime Timestamp { get; set; }
+
+ /// Process working-set size in megabytes.
+ public double WorkingSetMB { get; set; }
+
+ /// Managed (GC) heap size in megabytes.
+ public double ManagedHeapMB { get; set; }
+
+ /// Number of GC collections since process start (all generations).
+ public int GcCollectionCount { get; set; }
+
+ /// Number of active OS thread-pool threads.
+ public int ThreadCount { get; set; }
+
+ /// Elapsed application uptime in seconds.
+ public double UptimeSeconds { get; set; }
+ }
+
+ ///
+ /// Defines metric categories for runtime performance data.
+ ///
+ public enum RuntimeMetricCategory
+ {
+ Memory,
+ Cpu,
+ Io,
+ Prediction,
+ Ui,
+ General
+ }
+
+ ///
+ /// Collects and stores named runtime performance metrics.
+ /// All public members are thread-safe.
+ ///
+ public class RuntimeMetricsCollector : IDisposable
+ {
+ private readonly ConcurrentDictionary _entries =
+ new ConcurrentDictionary(StringComparer.Ordinal);
+
+ private readonly List _samples = new List();
+ private readonly object _samplesLock = new object();
+ private readonly Stopwatch _uptime = new Stopwatch();
+ private Timer _sampleTimer;
+ private volatile bool _disposed;
+
+ ///
+ /// Raised when a periodic sample is captured.
+ ///
+ public event EventHandler SampleCaptured;
+
+ ///
+ /// Starts periodic sampling at the specified interval.
+ ///
+ /// Sampling interval in milliseconds (minimum 100 ms).
+ public void Start(int intervalMs = 5000)
+ {
+ if (intervalMs < 100)
+ {
+ intervalMs = 100;
+ }
+
+ _uptime.Restart();
+ _sampleTimer = new Timer(OnSampleTimer, null, intervalMs, intervalMs);
+ }
+
+ ///
+ /// Stops periodic sampling.
+ ///
+ public void Stop()
+ {
+ _sampleTimer?.Dispose();
+ _sampleTimer = null;
+ _uptime.Stop();
+ }
+
+ ///
+ /// Records a named metric value with an optional category.
+ /// Values for the same name are aggregated (count, min, max, average).
+ ///
+ public void Record(string name, double value,
+ RuntimeMetricCategory category = RuntimeMetricCategory.General,
+ string unit = "")
+ {
+ if (string.IsNullOrEmpty(name))
+ {
+ return;
+ }
+
+ _entries.AddOrUpdate(
+ name,
+ _ => new RuntimeMetricEntry
+ {
+ Name = name,
+ Category = category,
+ Unit = unit,
+ Count = 1,
+ Sum = value,
+ Min = value,
+ Max = value,
+ LastValue = value,
+ LastUpdated = DateTime.UtcNow
+ },
+ (_, existing) =>
+ {
+ existing.Count++;
+ existing.Sum += value;
+ existing.Min = Math.Min(existing.Min, value);
+ existing.Max = Math.Max(existing.Max, value);
+ existing.LastValue = value;
+ existing.LastUpdated = DateTime.UtcNow;
+ return existing;
+ });
+ }
+
+ ///
+ /// Returns a read-only snapshot of all recorded metric entries.
+ ///
+ public IReadOnlyDictionary GetEntries()
+ {
+ return new Dictionary(_entries);
+ }
+
+ ///
+ /// Returns the list of periodic samples captured since was called.
+ ///
+ public IReadOnlyList GetSamples()
+ {
+ lock (_samplesLock)
+ {
+ return new List(_samples);
+ }
+ }
+
+ ///
+ public void Dispose()
+ {
+ if (_disposed)
+ {
+ return;
+ }
+
+ _disposed = true;
+ Stop();
+ }
+
+ // ----------------------------------------------------------------
+ // Private helpers
+ // ----------------------------------------------------------------
+
+ private void OnSampleTimer(object state)
+ {
+ try
+ {
+ var sample = CaptureSample();
+ lock (_samplesLock)
+ {
+ _samples.Add(sample);
+ }
+
+ SampleCaptured?.Invoke(this, sample);
+ }
+ catch (Exception ex)
+ {
+ Debug.WriteLine($"[RuntimeMetricsCollector] Sample error: {ex.Message}");
+ }
+ }
+
+ private RuntimeMetricSample CaptureSample()
+ {
+ var process = Process.GetCurrentProcess();
+ process.Refresh();
+
+ int gcCount = 0;
+ for (int gen = 0; gen <= GC.MaxGeneration; gen++)
+ {
+ gcCount += GC.CollectionCount(gen);
+ }
+
+ return new RuntimeMetricSample
+ {
+ Timestamp = DateTime.UtcNow,
+ WorkingSetMB = process.WorkingSet64 / (1024.0 * 1024.0),
+ ManagedHeapMB = GC.GetTotalMemory(false) / (1024.0 * 1024.0),
+ GcCollectionCount = gcCount,
+ ThreadCount = process.Threads.Count,
+ UptimeSeconds = _uptime.Elapsed.TotalSeconds
+ };
+ }
+ }
+
+ ///
+ /// Aggregated entry for a named runtime metric.
+ ///
+ public class RuntimeMetricEntry
+ {
+ /// Metric name.
+ public string Name { get; set; }
+
+ /// Metric category.
+ public RuntimeMetricCategory Category { get; set; }
+
+ /// Unit of the metric value (e.g. "ms", "MB").
+ public string Unit { get; set; }
+
+ /// Number of recorded samples.
+ public int Count { get; set; }
+
+ /// Sum of all recorded values (used to compute average).
+ public double Sum { get; set; }
+
+ /// Minimum recorded value.
+ public double Min { get; set; }
+
+ /// Maximum recorded value.
+ public double Max { get; set; }
+
+ /// Most recently recorded value.
+ public double LastValue { get; set; }
+
+ /// Average of all recorded values.
+ public double Average => Count > 0 ? Sum / Count : 0.0;
+
+ /// UTC timestamp of the last update.
+ public DateTime LastUpdated { get; set; }
+ }
+}
From b296d72babbbff2b5b2bfceba4e014821de95558 Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Fri, 20 Feb 2026 00:10:54 +0000
Subject: [PATCH 3/6] Fix spelling: Initialises -> Initializes in doc comments
Co-authored-by: michaelbeale-IL <63321611+michaelbeale-IL@users.noreply.github.com>
---
.../ACAT.Extensions.UI/Diagnostics/PerformanceDashboard.xaml.cs | 2 +-
.../Utility/Diagnostics/PerformanceRegressionDetector.cs | 2 +-
2 files changed, 2 insertions(+), 2 deletions(-)
diff --git a/src/Extensions/ACAT.Extensions.UI/Diagnostics/PerformanceDashboard.xaml.cs b/src/Extensions/ACAT.Extensions.UI/Diagnostics/PerformanceDashboard.xaml.cs
index e24a67dd..f7e30357 100644
--- a/src/Extensions/ACAT.Extensions.UI/Diagnostics/PerformanceDashboard.xaml.cs
+++ b/src/Extensions/ACAT.Extensions.UI/Diagnostics/PerformanceDashboard.xaml.cs
@@ -38,7 +38,7 @@ public partial class PerformanceDashboard : Window
private Timer _refreshTimer;
///
- /// Initialises the dashboard with optional pre-existing components.
+ /// Initializes the dashboard with optional pre-existing components.
/// When parameters are omitted, new instances are created.
///
/// Shared runtime-metrics collector (optional).
diff --git a/src/Libraries/ACATCore/Utility/Diagnostics/PerformanceRegressionDetector.cs b/src/Libraries/ACATCore/Utility/Diagnostics/PerformanceRegressionDetector.cs
index 4fb1e16a..6b1fbe9a 100644
--- a/src/Libraries/ACATCore/Utility/Diagnostics/PerformanceRegressionDetector.cs
+++ b/src/Libraries/ACATCore/Utility/Diagnostics/PerformanceRegressionDetector.cs
@@ -59,7 +59,7 @@ public class PerformanceRegressionDetector
private readonly PerformanceBaselineData _baseline;
///
- /// Initialises the detector with the supplied baseline.
+ /// Initializes the detector with the supplied baseline.
///
/// Baseline thresholds to compare against.
/// If null, is used.
From 4bb4b61a8650beb66b9dcd57638dcb666e056e8a Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Fri, 20 Feb 2026 02:15:55 +0000
Subject: [PATCH 4/6] Fix build error: use fully qualified
System.Text.Json.JsonSerializer to avoid shadowing by
ACAT.Core.Utility.JsonSerializer
Co-authored-by: michaelbeale-IL <63321611+michaelbeale-IL@users.noreply.github.com>
---
.../ACATCore/Utility/Diagnostics/PerformanceBaseline.cs | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/src/Libraries/ACATCore/Utility/Diagnostics/PerformanceBaseline.cs b/src/Libraries/ACATCore/Utility/Diagnostics/PerformanceBaseline.cs
index 90716e37..896d2a39 100644
--- a/src/Libraries/ACATCore/Utility/Diagnostics/PerformanceBaseline.cs
+++ b/src/Libraries/ACATCore/Utility/Diagnostics/PerformanceBaseline.cs
@@ -97,7 +97,7 @@ public static void Save(PerformanceBaselineData data, string filePath)
Directory.CreateDirectory(directory);
}
- string json = JsonSerializer.Serialize(data, _jsonOptions);
+ string json = System.Text.Json.JsonSerializer.Serialize(data, _jsonOptions);
File.WriteAllText(filePath, json);
}
@@ -117,7 +117,7 @@ public static PerformanceBaselineData Load(string filePath)
try
{
string json = File.ReadAllText(filePath);
- return JsonSerializer.Deserialize(json, _jsonOptions)
+ return System.Text.Json.JsonSerializer.Deserialize(json, _jsonOptions)
?? CreateDefault();
}
catch (Exception ex)
From 49c954f9d9de25f4361f9182673973ba1a298860 Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Fri, 20 Feb 2026 02:28:48 +0000
Subject: [PATCH 5/6] Add usage documentation to PerformanceDashboard XAML and
code-behind
Co-authored-by: michaelbeale-IL <63321611+michaelbeale-IL@users.noreply.github.com>
---
.../Diagnostics/PerformanceDashboard.xaml | 74 +++++++++++++++++++
.../Diagnostics/PerformanceDashboard.xaml.cs | 39 +++++++++-
2 files changed, 110 insertions(+), 3 deletions(-)
diff --git a/src/Extensions/ACAT.Extensions.UI/Diagnostics/PerformanceDashboard.xaml b/src/Extensions/ACAT.Extensions.UI/Diagnostics/PerformanceDashboard.xaml
index 72cf90f7..98e54ebe 100644
--- a/src/Extensions/ACAT.Extensions.UI/Diagnostics/PerformanceDashboard.xaml
+++ b/src/Extensions/ACAT.Extensions.UI/Diagnostics/PerformanceDashboard.xaml
@@ -1,3 +1,77 @@
+
- /// Interaction logic for PerformanceDashboard.xaml.
- /// Displays live runtime metrics, memory snapshots, and baseline
- /// regression status in an accessible WPF window.
+ /// Interaction logic for PerformanceDashboard.xaml — live performance monitoring window.
///
+ ///
+ ///
+ /// The dashboard auto-refreshes every 2 seconds and displays four panels:
+ ///
+ /// - Memory – working set, managed heap, GC collection count.
+ /// - Runtime – process uptime, thread count, OS handle count.
+ /// - Baseline Status – regression check against thresholds.
+ /// - Sample History – peak working-set and timestamp of the last refresh.
+ ///
+ ///
+ /// Minimal usage (self-contained, creates its own collectors):
+ ///
+ /// var dashboard = new PerformanceDashboard();
+ /// dashboard.Show();
+ ///
+ /// Shared collectors (display data already gathered by the application):
+ ///
+ /// var collector = new RuntimeMetricsCollector();
+ /// var profiler = new MemoryProfiler();
+ /// collector.Start(intervalMs: 5000);
+ ///
+ /// var dashboard = new PerformanceDashboard(collector, profiler);
+ /// dashboard.Show();
+ ///
+ /// Custom baseline (change regression thresholds):
+ ///
+ /// PerformanceBaselineData baseline = PerformanceBaseline.Load(baselinePath);
+ /// var dashboard = new PerformanceDashboard(collector, profiler, baseline);
+ /// dashboard.Show();
+ ///
+ ///
+ /// Toolbar buttons let the user export all captured data to CSV or JSON
+ /// via a standard SaveFileDialog, or clear the accumulated snapshot history.
+ ///
+ ///
public partial class PerformanceDashboard : Window
{
private readonly RuntimeMetricsCollector _collector;
From 95fb080fdca84f185b827ba8f939e712a71179c5 Mon Sep 17 00:00:00 2001
From: "Beale, Michael"
Date: Fri, 20 Feb 2026 06:15:33 -0800
Subject: [PATCH 6/6] added performance manager dashboard to acatapp and
acattalk for debugging
---
src/Applications/ACATApp/Program.cs | 12 ++++++++++++
src/Applications/ACATTalk/Program.cs | 12 ++++++++++++
2 files changed, 24 insertions(+)
diff --git a/src/Applications/ACATApp/Program.cs b/src/Applications/ACATApp/Program.cs
index 3bf11c29..3efa4f67 100644
--- a/src/Applications/ACATApp/Program.cs
+++ b/src/Applications/ACATApp/Program.cs
@@ -19,8 +19,11 @@
using ACAT.Core.PanelManagement.Interfaces;
using ACAT.Core.UserManagement;
using ACAT.Core.Utility;
+using ACAT.Core.Utility.Diagnostics;
+using ACAT.Core.Utility.Metrics;
using ACAT.Extension;
using ACAT.Extension.CommandHandlers;
+using ACAT.Extensions.UI.Diagnostics;
using ACATResources;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
@@ -65,6 +68,15 @@ public static void Main(string[] args)
return;
}
+#if DEBUG
+ var collector = new RuntimeMetricsCollector();
+ var profiler = new MemoryProfiler();
+ collector.Start(intervalMs: 5000);
+
+ var dashboard = new PerformanceDashboard(collector, profiler);
+ dashboard.Show();
+#endif
+
ShowSplashScreen("Starting ACAT");
var initialized = InitializeApplication();
diff --git a/src/Applications/ACATTalk/Program.cs b/src/Applications/ACATTalk/Program.cs
index c7a7a6b5..b0171e65 100644
--- a/src/Applications/ACATTalk/Program.cs
+++ b/src/Applications/ACATTalk/Program.cs
@@ -21,8 +21,11 @@
using ACAT.Core.PanelManagement.Interfaces;
using ACAT.Core.UserManagement;
using ACAT.Core.Utility;
+using ACAT.Core.Utility.Diagnostics;
+using ACAT.Core.Utility.Metrics;
using ACAT.Extension;
using ACAT.Extension.CommandHandlers;
+using ACAT.Extensions.UI.Diagnostics;
using ACATResources;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
@@ -53,6 +56,15 @@ public static void Main(string[] args)
PerformanceMonitor.LogEvent("Application", "Main entry point");
#endif
+#if DEBUG
+ var collector = new RuntimeMetricsCollector();
+ var profiler = new MemoryProfiler();
+ collector.Start(intervalMs: 5000);
+
+ var dashboard = new PerformanceDashboard(collector, profiler);
+ dashboard.Show();
+#endif
+
if (AppCommon.OtherInstancesRunning())
{
return;