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/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/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/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; 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..98e54ebe --- /dev/null +++ b/src/Extensions/ACAT.Extensions.UI/Diagnostics/PerformanceDashboard.xaml @@ -0,0 +1,243 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +