diff --git a/CHANGELOG.md b/CHANGELOG.md index 0dbc6413..e9a156bc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,7 +5,50 @@ 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.8.0] - TBD +## [2.9.0] - TBD + +### Important + +- **Breaking change to `config.data_retention`** — the `@truncate_all` parameter has been removed. Pass `@retention_days = 0` for the same behavior. `@retention_days = NULL` (default) respects per-collector retention from `config.collection_schedule` with a 30-day fallback for unscheduled tables; `@retention_days = N > 0` overrides every table to N days. Any existing Agent jobs or scripts calling `data_retention @truncate_all = 1` need to be updated ([#900]) +- **New `config.collector_database_exclusions` table** for per-database collector exclusions. Eight per-database collectors filter against this table; system databases remain hard-skipped by the collectors themselves. Existing installs get the table on the next upgrade — `install/01_install_database.sql` and `config.ensure_config_tables` both create it under an `IF OBJECT_ID … IS NULL` guard ([#887]) + +### Added + +- **Per-database collector exclusions** — exclude noisy or unimportant databases from per-database collectors. Dashboard side adds `config.collector_database_exclusions` and filters 8 collectors (`query_stats`, `query_store`, `procedure_stats`, `file_io_stats`, `waiting_tasks`, `database_configuration`, `database_size_stats`, `server_properties`). Lite side adds an `ExcludedDatabases` list per server in `servers.json` and filters 9 collectors ([#887]) +- **`Off` collection preset** — `EXECUTE config.apply_collection_preset @preset_name = N'Off'` disables every collector in one call. Pair with a second Agent job that applies a non-`Off` preset at the start of your active window for overnight / quiet-hours scoping. Non-`Off` presets now also set `enabled = 1` across the board so the switch from `Off → Balanced` reliably resumes collection ([#888]) +- **Purge Now action** in Manage Servers — confirm dialog with a mode picker (Use configured / 1 / 3 / 7 / Custom / All) drives `config.data_retention`; right-click menu on the Manage Servers grid mirrors every per-row action (Edit, Toggle Favorite, Check Server Version, Purge Now, Remove) ([#900]) +- **Total non-idle CPU on Lite Overview** — headline value shows total CPU with the SQL-only value alongside (e.g. `64% (SQL 60%)`); new `CpuAlertMode` dropdown in Settings → Alerts (Total / SqlOnly) drives both the alert evaluator and headline color; tray notifications and email alerts label the value as "Total CPU" or "SQL CPU" ([#899]) +- **Resume gap detection** — `query_stats`, `procedure_stats`, and `query_store` collectors skip the historical sweep on first run after an Off preset, Agent stoppage, or server reboot. When the last successful run is older than 5× the configured `frequency_minutes` (floored at 30 minutes), the cutoff clamps to `SYSDATETIME()` so only forward-going data is collected on resume — preventing the tempdb blowout that hit the original reporter ([#892]) +- **Right-click View Plan** on Dashboard Blocked Process Reports (View Blocked Plan + View Blocking Plan), Dashboard Deadlocks, and Lite Deadlocks grids. Plan lookup hits `sys.dm_exec_query_stats` + `sys.dm_exec_text_query_plan` on the monitored server, falling back to `executionStack/frame` entries when the process-level `sql_handle` is empty or evicted ([#880]) +- **Open Log Folder** sidebar button in Lite — opens `%LocalAppData%\PerformanceMonitorLite\logs\` in Explorer for grabbing historical logs to attach to bug reports. Sits below View Log, which retains its existing behavior of opening today's log file ([#873]) +- **Installed Version column** in the Manage Servers grid for both Dashboard and Lite. Dashboard shows the PerformanceMonitor database version on each server (probed in parallel via `GetInstalledVersionAsync`, with `Not installed` / `Unavailable` fallbacks). Lite shows the running app's own version on every row, mirroring Full's column header for consistency. +- **Lite-style server card indicators in Full** — back-ported the Ellipse-with-DataTriggers status dot (Online/Offline/Warning/Unknown) and the right-aligned favorite star from Lite to the Full Dashboard's server list, matching Lite's visual treatment. +- **Architecture overview** at `docs/how-collection-works.md` covering the minute loop, dispatcher, collector shape, `config.collection_schedule`, retention, and the Dashboard read path + +### Changed + +- **PlanIconMapper synced** with PerformanceStudio v1.9.0 improvements — columnstore storage type on scan/delete/insert/update/merge operators routes to `columnstore_index_*` icons (covers CCI and NCCI); `Parallelism` operator subtypes (Repartition Streams, Distribute Streams, Gather Streams) get their own icons +- **`Microsoft.Data.SqlClient` 6.1.4 → 7.0.1** — major-version bump. Azure/Entra dependencies were split out of the core package in 7.0; `Microsoft.Data.SqlClient.Extensions.Azure 1.0.0` added to Dashboard, Lite, and Installer.Core for `ActiveDirectoryInteractive` connections +- **`ModelContextProtocol` 0.7.0-preview.1 → 1.2.0** — off the preview tag and onto stable 1.x in Dashboard and Lite +- **`DuckDB.NET` 1.5.0 → 1.5.2** in Lite — fixes unbounded row group growth on indexed tables under repeated load+insert cycles, memory leaks and race conditions in prepared statements, WAL checkpoint marking, and Windows UTF-8/UTF-16 handling +- **`Microsoft.Extensions.*` 10.0.5 → 10.0.7**, **`System.Text.Json` 10.0.5 → 10.0.7**, **`ScottPlot.WPF` 5.1.57 → 5.1.58** — patch-level bumps with no expected behavioral change +- **Theme polish** on grids and plan viewer in Dashboard and Lite — thanks [@ClaudioESSilva](https://github.com/ClaudioESSilva) ([#889]) + +### Fixed + +- **Install loop timeout** raised from 5 minutes to 1 hour. `install/98_validate_installation.sql` runs every enabled collector with `@debug = 1` in a single batch; on large databases (reporter had 7.2M rows in `collect.query_stats`, 4.4M in `collect.query_store_data`) this took ~9 minutes and was blowing the 5-minute timeout, failing the install or upgrade ([#884]) + +[#873]: https://github.com/erikdarlingdata/PerformanceMonitor/issues/873 +[#880]: https://github.com/erikdarlingdata/PerformanceMonitor/issues/880 +[#884]: https://github.com/erikdarlingdata/PerformanceMonitor/issues/884 +[#887]: https://github.com/erikdarlingdata/PerformanceMonitor/issues/887 +[#888]: https://github.com/erikdarlingdata/PerformanceMonitor/issues/888 +[#889]: https://github.com/erikdarlingdata/PerformanceMonitor/issues/889 +[#892]: https://github.com/erikdarlingdata/PerformanceMonitor/issues/892 +[#899]: https://github.com/erikdarlingdata/PerformanceMonitor/issues/899 +[#900]: https://github.com/erikdarlingdata/PerformanceMonitor/issues/900 + +## [2.8.0] - 2026-04-22 ### Important diff --git a/Dashboard/Controls/FinOpsContent.xaml b/Dashboard/Controls/FinOpsContent.xaml index 43573d47..0d8d0ba7 100644 --- a/Dashboard/Controls/FinOpsContent.xaml +++ b/Dashboard/Controls/FinOpsContent.xaml @@ -111,6 +111,9 @@ + + + @@ -416,6 +419,9 @@ + + + diff --git a/Dashboard/Controls/PlanViewerControl.xaml b/Dashboard/Controls/PlanViewerControl.xaml index d41669fc..2f720e50 100644 --- a/Dashboard/Controls/PlanViewerControl.xaml +++ b/Dashboard/Controls/PlanViewerControl.xaml @@ -77,15 +77,15 @@ + Background="{DynamicResource PlanMissingIndexBgBrush}" + BorderBrush="{DynamicResource PlanMissingIndexBorderBrush}" BorderThickness="0,0,1,0"> + Background="{DynamicResource PlanWaitStatsBgBrush}"> + (TryFindResource("PlanTooltipBgBrush") as SolidColorBrush) ?? new SolidColorBrush(Color.FromRgb(0x1A, 0x1D, 0x23)); + private SolidColorBrush TooltipBorderBrush => + (TryFindResource("PlanTooltipBorderBrush") as SolidColorBrush) ?? new SolidColorBrush(Color.FromRgb(0x3A, 0x3D, 0x45)); + private SolidColorBrush TooltipFgBrush => + (TryFindResource("PlanPanelTextBrush") as SolidColorBrush) ?? new SolidColorBrush(Color.FromRgb(0xE4, 0xE6, 0xEB)); + private SolidColorBrush MutedBrush => + (TryFindResource("PlanPanelMutedBrush") as SolidColorBrush) ?? new SolidColorBrush(Color.FromRgb(0xE4, 0xE6, 0xEB)); + private SolidColorBrush SectionHeaderBrush => + (TryFindResource("PlanSectionHeaderBrush") as SolidColorBrush) ?? new SolidColorBrush(Color.FromRgb(0x4F, 0xA3, 0xFF)); + private SolidColorBrush PropSeparatorBrush => + (TryFindResource("PlanPropSeparatorBrush") as SolidColorBrush) ?? new SolidColorBrush(Color.FromRgb(0x2A, 0x2D, 0x35)); + // Current property section for collapsible groups private StackPanel? _currentPropertySection; @@ -55,6 +64,28 @@ public partial class PlanViewerControl : UserControl public PlanViewerControl() { InitializeComponent(); + Helpers.ThemeManager.ThemeChanged += OnThemeChanged; + Unloaded += (_, _) => Helpers.ThemeManager.ThemeChanged -= OnThemeChanged; + } + + private void OnThemeChanged(string _) + { + if (_currentStatement == null) return; + + var nodeToRestore = _selectedNode; + RenderStatement(_currentStatement); + + if (nodeToRestore == null) return; + + // Find the re-created border for the previously selected node and reopen properties + foreach (var child in PlanCanvas.Children) + { + if (child is Border b && b.Tag == nodeToRestore) + { + SelectNode(b, nodeToRestore); + break; + } + } } public void LoadPlan(string planXml, string label, string? queryText = null) @@ -488,14 +519,14 @@ void AddRow(string label, string value) var lbl = new TextBlock { Text = label, - Foreground = new SolidColorBrush(Color.FromRgb(0xE0, 0xE0, 0xE0)), + Foreground = MutedBrush, FontSize = 12, Margin = new Thickness(0, 1, 12, 1) }; var val = new TextBlock { Text = value, - Foreground = new SolidColorBrush(Colors.White), + Foreground = TooltipFgBrush, FontSize = 12, FontWeight = FontWeights.SemiBold, HorizontalAlignment = HorizontalAlignment.Right, @@ -528,8 +559,8 @@ void AddRow(string label, string value) return new Border { - Background = new SolidColorBrush(Color.FromRgb(0x1E, 0x1E, 0x2E)), - BorderBrush = new SolidColorBrush(Color.FromRgb(0x3A, 0x3A, 0x5A)), + Background = TooltipBgBrush, + BorderBrush = TooltipBorderBrush, BorderThickness = new Thickness(1), Padding = new Thickness(10, 6, 10, 6), CornerRadius = new CornerRadius(4), @@ -571,6 +602,7 @@ private void SelectNode(Border border, PlanNode node) _selectedNodeOriginalBorder = border.BorderBrush; _selectedNodeOriginalThickness = border.BorderThickness; _selectedNodeBorder = border; + _selectedNode = node; border.BorderBrush = SelectionBrush; border.BorderThickness = new Thickness(2); @@ -1504,7 +1536,7 @@ private void AddPropertySection(string title) Margin = new Thickness(0, 2, 0, 0), Padding = new Thickness(0), Foreground = SectionHeaderBrush, - Background = new SolidColorBrush(Color.FromArgb(0x18, 0x4F, 0xA3, 0xFF)), + Background = (TryFindResource("BackgroundLighterBrush") as SolidColorBrush) ?? new SolidColorBrush(Color.FromArgb(0x18, 0x4F, 0xA3, 0xFF)), BorderBrush = PropSeparatorBrush, BorderThickness = new Thickness(0, 0, 0, 1) }; @@ -1568,6 +1600,7 @@ private void ClosePropertiesPanel() _selectedNodeBorder.BorderThickness = _selectedNodeOriginalThickness; _selectedNodeBorder = null; } + _selectedNode = null; } #endregion @@ -1780,7 +1813,7 @@ private ToolTip BuildNodeTooltip(PlanNode node, List? allWarnings = return tip; } - private static void AddTooltipSection(StackPanel parent, string title) + private void AddTooltipSection(StackPanel parent, string title) { parent.Children.Add(new TextBlock { @@ -1792,7 +1825,7 @@ private static void AddTooltipSection(StackPanel parent, string title) }); } - private static void AddTooltipRow(StackPanel parent, string label, string value, bool isCode = false) + private void AddTooltipRow(StackPanel parent, string label, string value, bool isCode = false) { var row = new StackPanel { Orientation = Orientation.Horizontal, Margin = new Thickness(0, 1, 0, 1) }; row.Children.Add(new TextBlock @@ -1835,19 +1868,19 @@ private void ShowMissingIndexes(List indexes) { Text = mi.Table, FontWeight = FontWeights.SemiBold, - Foreground = new SolidColorBrush((Color)ColorConverter.ConvertFromString("#E4E6EB")), + Foreground = TooltipFgBrush, FontSize = 12 }); headerRow.Children.Add(new TextBlock { Text = $" \u2014 Impact: ", - Foreground = new SolidColorBrush((Color)ColorConverter.ConvertFromString("#E4E6EB")), + Foreground = MutedBrush, FontSize = 12 }); headerRow.Children.Add(new TextBlock { Text = $"{mi.Impact:F1}%", - Foreground = new SolidColorBrush((Color)ColorConverter.ConvertFromString("#FFB347")), + Foreground = OrangeBrush, FontSize = 12 }); itemPanel.Children.Add(headerRow); @@ -1859,7 +1892,7 @@ private void ShowMissingIndexes(List indexes) Text = mi.CreateStatement, FontFamily = new FontFamily("Consolas"), FontSize = 11, - Foreground = new SolidColorBrush((Color)ColorConverter.ConvertFromString("#E4E6EB")), + Foreground = TooltipFgBrush, Background = Brushes.Transparent, BorderThickness = new Thickness(0), IsReadOnly = true, @@ -1932,7 +1965,7 @@ private void ShowWaitStats(List waits, bool isActualPlan) { Text = w.WaitType, FontSize = 12, - Foreground = new SolidColorBrush((Color)ColorConverter.ConvertFromString("#E4E6EB")), + Foreground = TooltipFgBrush, VerticalAlignment = VerticalAlignment.Center, Margin = new Thickness(0, 2, 10, 2) }; @@ -1958,7 +1991,7 @@ private void ShowWaitStats(List waits, bool isActualPlan) { Text = $"{w.WaitTimeMs:N0}ms ({w.WaitCount:N0} waits)", FontSize = 12, - Foreground = new SolidColorBrush((Color)ColorConverter.ConvertFromString("#E4E6EB")), + Foreground = TooltipFgBrush, VerticalAlignment = VerticalAlignment.Center, Margin = new Thickness(0, 2, 0, 2) }; @@ -2014,8 +2047,8 @@ private void ShowRuntimeSummary(PlanStatement statement) { RuntimeSummaryContent.Children.Clear(); - var labelColor = "#E4E6EB"; - var valueColor = "#E4E6EB"; + var labelBrush = MutedBrush; + var valueBrush = TooltipFgBrush; var grid = new Grid(); grid.ColumnDefinitions.Add(new ColumnDefinition { Width = GridLength.Auto }); @@ -2030,7 +2063,7 @@ void AddRow(string label, string value) { Text = label, FontSize = 11, - Foreground = new SolidColorBrush((Color)ColorConverter.ConvertFromString(labelColor)), + Foreground = labelBrush, HorizontalAlignment = HorizontalAlignment.Left, Margin = new Thickness(0, 1, 8, 1) }; @@ -2042,7 +2075,7 @@ void AddRow(string label, string value) { Text = value, FontSize = 11, - Foreground = new SolidColorBrush((Color)ColorConverter.ConvertFromString(valueColor)), + Foreground = valueBrush, Margin = new Thickness(0, 1, 0, 1) }; Grid.SetRow(valueText, rowIndex); diff --git a/Dashboard/Controls/QueryPerformanceContent.xaml b/Dashboard/Controls/QueryPerformanceContent.xaml index 235f8efd..aa033e13 100644 --- a/Dashboard/Controls/QueryPerformanceContent.xaml +++ b/Dashboard/Controls/QueryPerformanceContent.xaml @@ -879,6 +879,7 @@ + @@ -887,19 +888,19 @@ - + - + - + - + @@ -1194,6 +1195,7 @@ + @@ -1203,19 +1205,19 @@ - + - + - + - + @@ -1646,6 +1648,7 @@ + @@ -1654,19 +1657,19 @@ - + - + - + - + diff --git a/Dashboard/Dashboard.csproj b/Dashboard/Dashboard.csproj index aff84b73..1d01ed09 100644 --- a/Dashboard/Dashboard.csproj +++ b/Dashboard/Dashboard.csproj @@ -7,10 +7,10 @@ PerformanceMonitorDashboard.Program PerformanceMonitorDashboard SQL Server Performance Monitor Dashboard - 2.8.0 - 2.8.0.0 - 2.8.0.0 - 2.8.0 + 2.9.0 + 2.9.0.0 + 2.9.0.0 + 2.9.0 Darling Data, LLC Copyright © 2026 Darling Data, LLC EDD.ico @@ -35,13 +35,14 @@ - - - - - - - + + + + + + + + diff --git a/Dashboard/ExcludedDatabasesDialog.xaml b/Dashboard/ExcludedDatabasesDialog.xaml new file mode 100644 index 00000000..00789a61 --- /dev/null +++ b/Dashboard/ExcludedDatabasesDialog.xaml @@ -0,0 +1,49 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +