From 87cdbafa9439b60ed92a18b6469daec84f83d5be Mon Sep 17 00:00:00 2001 From: Erik Darling <2136037+erikdarlingdata@users.noreply.github.com> Date: Tue, 14 Apr 2026 13:22:13 -0400 Subject: [PATCH] Release v2.7.0: version bumps, changelog, auto-refresh fix - Bump version to 2.7.0 in all 4 csproj files - Add CHANGELOG entry for v2.7.0 (features, changes, fixes) - Update README: MultiSubnetFailover AG note, air-gapped community scripts - Replace DispatcherTimer auto-refresh with async Task.Delay loop to prevent priority starvation under heavy UI load (chart rendering) Co-Authored-By: Claude Opus 4.6 (1M context) --- CHANGELOG.md | 42 +++++++ Dashboard/Dashboard.csproj | 8 +- Dashboard/ServerTab.xaml.cs | 117 ++++++++----------- Installer.Core/Installer.Core.csproj | 8 +- Installer/PerformanceMonitorInstaller.csproj | 8 +- Lite/PerformanceMonitorLite.csproj | 8 +- README.md | 4 +- 7 files changed, 108 insertions(+), 87 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index e0ebd159..eee63d09 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,48 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [2.7.0] - 2026-04-13 + +### Added + +- **Host OS column** in Server Inventory for both Dashboard and Lite ([#748], [#823]) +- **Offline community script support** via `community/` directory for user-contributed scripts ([#814], [#822]) +- **MultiSubnetFailover connection option** in Dashboard and Lite for Always On availability groups ([#813], [#821]) + +### Changed + +- **PlanAnalyzer and ShowPlanParser** synced from PerformanceStudio with latest improvements ([#816]) +- **MCP query tools** optimized for large databases ([#826]) +- **Add Server dialog UX** improved with inline connection status and full-height window +- **"CPUs" renamed to "Logical CPUs"** for clarity in Lite ([#825]) + +### Fixed + +- **Dashboard auto-refresh stalling under load** — replaced DispatcherTimer with async Task.Delay loop to prevent priority starvation during heavy chart rendering ([#833], [#834]) +- **Lite auto-refresh silently skipping** every tick ([#824]) +- **Deadlock count not resetting** between collections ([#803], [#820]) +- **Upgrade filter skipping patch versions** during version comparison ([#817], [#819]) +- **Upgrade script executing against master** instead of PerformanceMonitor database ([#828]) +- **Duplicate release builds** triggering on both created and published events + +[#748]: https://github.com/erikdarlingdata/PerformanceMonitor/issues/748 +[#803]: https://github.com/erikdarlingdata/PerformanceMonitor/issues/803 +[#813]: https://github.com/erikdarlingdata/PerformanceMonitor/issues/813 +[#814]: https://github.com/erikdarlingdata/PerformanceMonitor/issues/814 +[#816]: https://github.com/erikdarlingdata/PerformanceMonitor/issues/816 +[#817]: https://github.com/erikdarlingdata/PerformanceMonitor/issues/817 +[#819]: https://github.com/erikdarlingdata/PerformanceMonitor/issues/819 +[#820]: https://github.com/erikdarlingdata/PerformanceMonitor/issues/820 +[#821]: https://github.com/erikdarlingdata/PerformanceMonitor/issues/821 +[#822]: https://github.com/erikdarlingdata/PerformanceMonitor/issues/822 +[#823]: https://github.com/erikdarlingdata/PerformanceMonitor/issues/823 +[#824]: https://github.com/erikdarlingdata/PerformanceMonitor/issues/824 +[#825]: https://github.com/erikdarlingdata/PerformanceMonitor/issues/825 +[#826]: https://github.com/erikdarlingdata/PerformanceMonitor/issues/826 +[#828]: https://github.com/erikdarlingdata/PerformanceMonitor/issues/828 +[#833]: https://github.com/erikdarlingdata/PerformanceMonitor/issues/833 +[#834]: https://github.com/erikdarlingdata/PerformanceMonitor/issues/834 + ## [2.6.0] - 2026-04-08 ### Added diff --git a/Dashboard/Dashboard.csproj b/Dashboard/Dashboard.csproj index 8fc70396..b7a0103f 100644 --- a/Dashboard/Dashboard.csproj +++ b/Dashboard/Dashboard.csproj @@ -7,10 +7,10 @@ PerformanceMonitorDashboard.Program PerformanceMonitorDashboard SQL Server Performance Monitor Dashboard - 2.6.0 - 2.6.0.0 - 2.6.0.0 - 2.6.0 + 2.7.0 + 2.7.0.0 + 2.7.0.0 + 2.7.0 Darling Data, LLC Copyright © 2026 Darling Data, LLC EDD.ico diff --git a/Dashboard/ServerTab.xaml.cs b/Dashboard/ServerTab.xaml.cs index 42ab6a1d..a67daf89 100644 --- a/Dashboard/ServerTab.xaml.cs +++ b/Dashboard/ServerTab.xaml.cs @@ -5,6 +5,7 @@ using System.Windows.Data; using System.Text; using System.Collections.Generic; +using System.Threading; using System.Threading.Tasks; using System.Windows; using System.Windows.Controls; @@ -46,6 +47,7 @@ public partial class ServerTab : UserControl private readonly UserPreferencesService _preferencesService; private DispatcherTimer? _autoRefreshTimer; + private CancellationTokenSource? _autoRefreshCts; private bool _isRefreshing; private DateTime _refreshStartedUtc; private bool _suppressPickerUpdates; @@ -127,7 +129,6 @@ public ServerTab(ServerConnection serverConnection, int utcOffsetMinutes = 0) InitializeDefaultTimeRanges(); SetupChartContextMenus(); - SetupAutoRefresh(); SetupSubTabContextMenus(); BlockingSlicer.RangeChanged += OnBlockingSlicerChanged; @@ -343,42 +344,62 @@ private void SetupAutoRefresh() if (prefs.AutoRefreshEnabled) { - _autoRefreshTimer = new DispatcherTimer - { - Interval = TimeSpan.FromSeconds(prefs.AutoRefreshIntervalSeconds) - }; - _autoRefreshTimer.Tick += async (s, e) => + StartAutoRefreshLoop(prefs.AutoRefreshIntervalSeconds); + AutoRefreshToggle.IsChecked = true; + AutoRefreshToggle.Content = $"Auto-Refresh: {prefs.AutoRefreshIntervalSeconds}s"; + } + else + { + AutoRefreshToggle.IsChecked = false; + AutoRefreshToggle.Content = "Auto-Refresh: Off"; + } + } + + /// + /// Async loop that replaces DispatcherTimer for auto-refresh. Task.Delay is not + /// subject to Dispatcher priority starvation under heavy UI load (chart rendering, + /// data binding) that can indefinitely defer Background-priority DispatcherTimer ticks. + /// + private async void StartAutoRefreshLoop(int intervalSeconds) + { + if (_autoRefreshCts != null && !_autoRefreshCts.IsCancellationRequested) + return; + + _autoRefreshCts?.Cancel(); + var cts = new CancellationTokenSource(); + _autoRefreshCts = cts; + + try + { + while (!cts.Token.IsCancellationRequested) { - _autoRefreshTimer?.Stop(); + await Task.Delay(TimeSpan.FromSeconds(intervalSeconds), cts.Token); + if (cts.Token.IsCancellationRequested) break; + try { + var sw = System.Diagnostics.Stopwatch.StartNew(); await RefreshVisibleTabAsync(); StatusText.Text = "Ready"; FooterText.Text = $"Last refresh: {DateTime.Now:yyyy-MM-dd HH:mm:ss} | Server: {_serverConnection.DisplayName}"; + Logger.Info($"Auto-refresh completed in {sw.ElapsedMilliseconds}ms for {_serverConnection.DisplayName}"); } - catch (Exception ex) + catch (Exception ex) when (ex is not OperationCanceledException) { Logger.Error($"Auto-refresh error: {ex.Message}", ex); StatusText.Text = "Auto-refresh error"; } - finally - { - _autoRefreshTimer?.Start(); - } - }; - _autoRefreshTimer.Start(); - AutoRefreshToggle.IsChecked = true; - AutoRefreshToggle.Content = $"Auto-Refresh: {prefs.AutoRefreshIntervalSeconds}s"; + } } - else + catch (OperationCanceledException) { - AutoRefreshToggle.IsChecked = false; - AutoRefreshToggle.Content = "Auto-Refresh: Off"; + // Normal shutdown } } private void ServerTab_Unloaded(object sender, RoutedEventArgs e) { + _autoRefreshCts?.Cancel(); _autoRefreshTimer?.Stop(); _autoRefreshTimer = null; @@ -448,7 +469,9 @@ private void OnThemeChanged(string _) public void RefreshAutoRefreshSettings() { - // Stop existing timer + // Stop existing loop and timer + _autoRefreshCts?.Cancel(); + _autoRefreshCts = null; _autoRefreshTimer?.Stop(); _autoRefreshTimer = null; @@ -457,30 +480,7 @@ public void RefreshAutoRefreshSettings() if (prefs.AutoRefreshEnabled) { - _autoRefreshTimer = new DispatcherTimer - { - Interval = TimeSpan.FromSeconds(prefs.AutoRefreshIntervalSeconds) - }; - _autoRefreshTimer.Tick += async (s, e) => - { - _autoRefreshTimer?.Stop(); - try - { - await RefreshVisibleTabAsync(); - StatusText.Text = "Ready"; - FooterText.Text = $"Last refresh: {DateTime.Now:yyyy-MM-dd HH:mm:ss} | Server: {_serverConnection.DisplayName}"; - } - catch (Exception ex) - { - Logger.Error($"Auto-refresh error: {ex.Message}", ex); - StatusText.Text = "Auto-refresh error"; - } - finally - { - _autoRefreshTimer?.Start(); - } - }; - _autoRefreshTimer.Start(); + StartAutoRefreshLoop(prefs.AutoRefreshIntervalSeconds); AutoRefreshToggle.IsChecked = true; AutoRefreshToggle.Content = $"Auto-Refresh: {prefs.AutoRefreshIntervalSeconds}s"; } @@ -506,30 +506,7 @@ private void AutoRefreshToggle_Click(object sender, RoutedEventArgs e) prefs.AutoRefreshEnabled = true; _preferencesService.SavePreferences(prefs); - _autoRefreshTimer = new DispatcherTimer - { - Interval = TimeSpan.FromSeconds(prefs.AutoRefreshIntervalSeconds) - }; - _autoRefreshTimer.Tick += async (s, args) => - { - _autoRefreshTimer?.Stop(); - try - { - await RefreshVisibleTabAsync(); - StatusText.Text = "Ready"; - FooterText.Text = $"Last refresh: {DateTime.Now:yyyy-MM-dd HH:mm:ss} | Server: {_serverConnection.DisplayName}"; - } - catch (Exception ex) - { - Logger.Error($"Auto-refresh error: {ex.Message}", ex); - StatusText.Text = "Auto-refresh error"; - } - finally - { - _autoRefreshTimer?.Start(); - } - }; - _autoRefreshTimer.Start(); + StartAutoRefreshLoop(prefs.AutoRefreshIntervalSeconds); AutoRefreshToggle.Content = $"Auto-Refresh: {prefs.AutoRefreshIntervalSeconds}s"; } else @@ -538,8 +515,7 @@ private void AutoRefreshToggle_Click(object sender, RoutedEventArgs e) prefs.AutoRefreshEnabled = false; _preferencesService.SavePreferences(prefs); - _autoRefreshTimer?.Stop(); - _autoRefreshTimer = null; + _autoRefreshCts?.Cancel(); AutoRefreshToggle.Content = "Auto-Refresh: Off"; } } @@ -643,6 +619,7 @@ private async void ServerTab_Loaded(object sender, RoutedEventArgs e) DefaultTraceTab.SetTimeRange(_globalHoursBack, _globalFromDate, _globalToDate); await LoadDataAsync(); + SetupAutoRefresh(); } catch (Exception ex) { diff --git a/Installer.Core/Installer.Core.csproj b/Installer.Core/Installer.Core.csproj index fbf0387b..bb5f1f32 100644 --- a/Installer.Core/Installer.Core.csproj +++ b/Installer.Core/Installer.Core.csproj @@ -7,10 +7,10 @@ Installer.Core Installer.Core SQL Server Performance Monitor Installer Core - 2.6.0 - 2.6.0.0 - 2.6.0.0 - 2.6.0 + 2.7.0 + 2.7.0.0 + 2.7.0.0 + 2.7.0 Darling Data, LLC Copyright (c) 2026 Darling Data, LLC true diff --git a/Installer/PerformanceMonitorInstaller.csproj b/Installer/PerformanceMonitorInstaller.csproj index 7c3fa1a9..a8293d15 100644 --- a/Installer/PerformanceMonitorInstaller.csproj +++ b/Installer/PerformanceMonitorInstaller.csproj @@ -20,10 +20,10 @@ PerformanceMonitorInstaller SQL Server Performance Monitor Installer - 2.6.0 - 2.6.0.0 - 2.6.0.0 - 2.6.0 + 2.7.0 + 2.7.0.0 + 2.7.0.0 + 2.7.0 Darling Data, LLC Copyright © 2026 Darling Data, LLC Installation utility for SQL Server Performance Monitor - Supports SQL Server 2016-2025 diff --git a/Lite/PerformanceMonitorLite.csproj b/Lite/PerformanceMonitorLite.csproj index d87dde0a..784cd81b 100644 --- a/Lite/PerformanceMonitorLite.csproj +++ b/Lite/PerformanceMonitorLite.csproj @@ -8,10 +8,10 @@ PerformanceMonitorLite PerformanceMonitorLite SQL Server Performance Monitor Lite - 2.6.0 - 2.6.0.0 - 2.6.0.0 - 2.6.0 + 2.7.0 + 2.7.0.0 + 2.7.0.0 + 2.7.0 Darling Data, LLC Copyright © 2026 Darling Data, LLC Lightweight SQL Server performance monitoring - no installation required on target servers diff --git a/README.md b/README.md index 9390c014..19402312 100644 --- a/README.md +++ b/README.md @@ -99,7 +99,7 @@ Data starts flowing within 1–5 minutes. That's it. No installation on your ser **Upgrading from zip?** Click **Import Settings** then **Import Data** in the sidebar and point both at your old Lite folder. Settings imports server connections, alert thresholds, SMTP config, and schedules. Data imports historical DuckDB + Parquet archives. **Auto-update users** (installed via Setup.exe) get updates automatically — no manual import needed. -**Always On AG?** Enable **ReadOnlyIntent** in the connection settings to route Lite's monitoring queries to a readable secondary, keeping the primary clear. +**Always On AG?** Enable **ReadOnlyIntent** in the connection settings to route Lite's monitoring queries to a readable secondary, keeping the primary clear. Enable **MultiSubnetFailover** for multi-subnet failover scenarios. ### Lite Collectors @@ -191,6 +191,8 @@ PerformanceMonitorInstaller.exe YourServerName sa YourPassword --uninstall The installer automatically tests the connection, checks the SQL Server version (2016+ required), executes SQL scripts, downloads community dependencies, creates SQL Agent jobs, and runs initial data collection. You can also install directly from the Dashboard's Add Server dialog. +**Air-gapped environments?** Place pre-downloaded community scripts (`sp_WhoIsActive.sql`, `DarlingData.sql`, `Install-All-Scripts.sql`) in a `community/` directory next to the installer. The installer uses local files when present and falls back to GitHub downloads otherwise. + ### CLI Installer Options | Option | Description |