Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
87 changes: 87 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,67 @@ 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.0.0] - 2026-02-25

### Important

- **Schema upgrade**: The `collect.memory_grant_stats` table gains new delta columns and drops unused warning columns. The `collect.session_wait_stats` table, its collector procedure, reporting view, and schedule entry are removed (zero UI coverage). Upgrade scripts run automatically via the CLI/GUI installer and use idempotent checks.

### Added

- **Graphical query plan viewer** — native ShowPlan rendering in both Dashboard and Lite with SSMS-parity operator icons, properties panel, tooltips, warning/parallelism badges, and tabbed plan display ([#220])
- **Actual execution plan support** — execute queries with SET STATISTICS XML ON to capture actual plans, with loading indicator and confirmation dialog ([#233])
- **PlanAnalyzer** — automated plan analysis with rules for missing indexes, eager spools, key lookups, implicit conversions, memory grants, and more
- **Current Active Queries live snapshot** — real-time view of running queries with estimated/live plan download ([#149])
- **Memory clerks tab** in Lite with picker-driven chart ([#145])
- **Current Waits charts** in Blocking tab for both Dashboard and Lite ([#280])
- **File I/O throughput charts** — read/write throughput trends, file-level latency breakdown, queued I/O overlay ([#281])
- **Memory grant stats charts** — standardized collection with delta framework integration and trend visualization ([#281])
- **CPU scheduler pressure status** — real-time scheduler, worker, runnable task counts with color-coded pressure level below CPU chart
- **Collection log drill-down** and daily summary in Lite ([#138])
- **Collector duration trends chart** in Dashboard Collection Health ([#138])
- **Themed perfmon counter packs** — 14 new counters with organized themed groups ([#255])
- **User-configurable connection timeout** setting ([#236])
- **Per-collector retention** — uses per-collector retention from `config.collection_schedule` in data retention ([#237])
- **Query identifiers** in drill-down windows — query hash, plan hash, SQL handle visible for identification ([#268])
- **Trace pattern drill-down** with missing columns and query text tooltips ([#273])
- **Query Store Regressions drill-down** with TVF rewrite for performance ([#274])
- **CLI `--help` flag** for installer ([#111])
- Sort arrows, right-aligned numerics, and initial sort indicators across all grids ([#110])
- Copyable plan viewer properties ([#269])
- Standardized chart save/export filenames between Dashboard and Lite ([#284])
- Full Dashboard column parity for query_stats, procedure_stats, and query_store_stats
- Min/max extremes surfaced in both apps — physical reads, rows, grant KB, spills, CLR time, log bytes ([#281])

### Changed

- Query Store detection uses `sys.database_query_store_options` instead of `sys.databases.is_query_store_on` for Azure SQL DB compatibility ([#287])
- Config tab consolidation, DB drop on server remove, DuckDB-first plan lookups, procedure stats parity
- Collector health status now detects consecutive recent failures — 5+ consecutive errors = FAILING, 3+ = WARNING
- Plan buttons now show a MessageBox when no plan is available instead of silently doing nothing
- CSV export uses locale-appropriate separators for non-US locales ([#240])
- Query Store Regressions and Query Trace Patterns migrated to popup grid filtering ([#260])
- NuGet packages updated; xUnit v3 migration

### Fixed

- **DuckDB file corruption** during maintenance — ReaderWriterLockSlim coordination, archive-all-and-reset at 512MB replaces compaction ([#218])
- Archive view column mismatch, wait_stats thread-safety, and percent_complete type cast ([#234])
- Collector health status bar text color ([#234])
- View Plan for Query Store and Query Store Regressions tabs ([#261])
- Query Store drill-down time filter alignment with main view ([#263])
- Execution count mismatches between main views and drill-downs
- Drill-down chart UX — sparse data markers, hover tooltips, window sizing ([#271])
- Truncated status text in Add Server dialog ([#257])
- Scrollbar visibility, self-filtering artifacts, missing columns, and context menus ([#245], [#246], [#247], [#248])
- query_stats and procedure_stats collectors ignoring recent queries
- Blank tooltips on warning and parallel badge icons
- Missing chart context menu on File I/O Throughput charts in Lite

### Removed

- `collect.session_wait_stats` table, `collect.session_wait_stats_collector` procedure, `report.session_wait_analysis` view, and schedule entry — zero UI coverage, never surfaced in Dashboard or Lite ([#281])

## [1.3.0] - 2026-02-20

### Important
Expand Down Expand Up @@ -119,6 +180,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Delta normalization for per-second rate calculations
- Dark theme UI

[2.0.0]: https://github.com/erikdarlingdata/PerformanceMonitor/compare/v1.3.0...v2.0.0
[1.3.0]: https://github.com/erikdarlingdata/PerformanceMonitor/compare/v1.2.0...v1.3.0
[1.2.0]: https://github.com/erikdarlingdata/PerformanceMonitor/compare/v1.1.0...v1.2.0
[1.1.0]: https://github.com/erikdarlingdata/PerformanceMonitor/compare/v1.0.0...v1.1.0
Expand Down Expand Up @@ -187,3 +249,28 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
[#206]: https://github.com/erikdarlingdata/PerformanceMonitor/issues/206
[#210]: https://github.com/erikdarlingdata/PerformanceMonitor/issues/210
[#214]: https://github.com/erikdarlingdata/PerformanceMonitor/issues/214
[#218]: https://github.com/erikdarlingdata/PerformanceMonitor/issues/218
[#220]: https://github.com/erikdarlingdata/PerformanceMonitor/issues/220
[#233]: https://github.com/erikdarlingdata/PerformanceMonitor/issues/233
[#234]: https://github.com/erikdarlingdata/PerformanceMonitor/issues/234
[#236]: https://github.com/erikdarlingdata/PerformanceMonitor/issues/236
[#237]: https://github.com/erikdarlingdata/PerformanceMonitor/issues/237
[#240]: https://github.com/erikdarlingdata/PerformanceMonitor/issues/240
[#245]: https://github.com/erikdarlingdata/PerformanceMonitor/issues/245
[#246]: https://github.com/erikdarlingdata/PerformanceMonitor/issues/246
[#247]: https://github.com/erikdarlingdata/PerformanceMonitor/issues/247
[#248]: https://github.com/erikdarlingdata/PerformanceMonitor/issues/248
[#255]: https://github.com/erikdarlingdata/PerformanceMonitor/issues/255
[#257]: https://github.com/erikdarlingdata/PerformanceMonitor/issues/257
[#260]: https://github.com/erikdarlingdata/PerformanceMonitor/issues/260
[#261]: https://github.com/erikdarlingdata/PerformanceMonitor/issues/261
[#263]: https://github.com/erikdarlingdata/PerformanceMonitor/issues/263
[#268]: https://github.com/erikdarlingdata/PerformanceMonitor/issues/268
[#269]: https://github.com/erikdarlingdata/PerformanceMonitor/issues/269
[#271]: https://github.com/erikdarlingdata/PerformanceMonitor/issues/271
[#273]: https://github.com/erikdarlingdata/PerformanceMonitor/issues/273
[#274]: https://github.com/erikdarlingdata/PerformanceMonitor/issues/274
[#280]: https://github.com/erikdarlingdata/PerformanceMonitor/issues/280
[#281]: https://github.com/erikdarlingdata/PerformanceMonitor/issues/281
[#284]: https://github.com/erikdarlingdata/PerformanceMonitor/issues/284
[#287]: https://github.com/erikdarlingdata/PerformanceMonitor/issues/287
81 changes: 53 additions & 28 deletions Dashboard/Controls/QueryPerformanceContent.xaml.cs
Original file line number Diff line number Diff line change
Expand Up @@ -699,45 +699,60 @@ private void ApplyCurrentActiveFilters()

private void DownloadCurrentActiveEstPlan_Click(object sender, RoutedEventArgs e)
{
if (sender is Button button && button.DataContext is LiveQueryItem item && !string.IsNullOrEmpty(item.QueryPlan))
if (sender is not Button button || button.DataContext is not LiveQueryItem item) return;

if (string.IsNullOrEmpty(item.QueryPlan))
{
var timestamp = DateTime.Now.ToString("yyyyMMdd_HHmmss", CultureInfo.InvariantCulture);
var defaultFileName = $"estimated_plan_{item.SessionId}_{timestamp}.sqlplan";
MessageBox.Show("No estimated plan is available for this query.", "No Plan Available",
MessageBoxButton.OK, MessageBoxImage.Information);
return;
}

var saveFileDialog = new SaveFileDialog
{
FileName = defaultFileName,
DefaultExt = ".sqlplan",
Filter = "SQL Plan (*.sqlplan)|*.sqlplan|XML Files (*.xml)|*.xml|All Files (*.*)|*.*",
Title = "Save Query Plan"
};
var timestamp = DateTime.Now.ToString("yyyyMMdd_HHmmss", CultureInfo.InvariantCulture);
var defaultFileName = $"estimated_plan_{item.SessionId}_{timestamp}.sqlplan";

if (saveFileDialog.ShowDialog() == true)
{
File.WriteAllText(saveFileDialog.FileName, item.QueryPlan);
}
var saveFileDialog = new SaveFileDialog
{
FileName = defaultFileName,
DefaultExt = ".sqlplan",
Filter = "SQL Plan (*.sqlplan)|*.sqlplan|XML Files (*.xml)|*.xml|All Files (*.*)|*.*",
Title = "Save Query Plan"
};

if (saveFileDialog.ShowDialog() == true)
{
File.WriteAllText(saveFileDialog.FileName, item.QueryPlan);
}
}

private void DownloadCurrentActiveLivePlan_Click(object sender, RoutedEventArgs e)
{
if (sender is Button button && button.DataContext is LiveQueryItem item && !string.IsNullOrEmpty(item.LiveQueryPlan))
if (sender is not Button button || button.DataContext is not LiveQueryItem item) return;

if (string.IsNullOrEmpty(item.LiveQueryPlan))
{
var timestamp = DateTime.Now.ToString("yyyyMMdd_HHmmss", CultureInfo.InvariantCulture);
var defaultFileName = $"live_plan_{item.SessionId}_{timestamp}.sqlplan";
MessageBox.Show(
"No live query plan is available for this session. The query may have completed before the plan could be captured.",
"No Plan Available",
MessageBoxButton.OK,
MessageBoxImage.Information);
return;
}

var saveFileDialog = new SaveFileDialog
{
FileName = defaultFileName,
DefaultExt = ".sqlplan",
Filter = "SQL Plan (*.sqlplan)|*.sqlplan|XML Files (*.xml)|*.xml|All Files (*.*)|*.*",
Title = "Save Live Query Plan"
};
var timestamp = DateTime.Now.ToString("yyyyMMdd_HHmmss", CultureInfo.InvariantCulture);
var defaultFileName = $"live_plan_{item.SessionId}_{timestamp}.sqlplan";

if (saveFileDialog.ShowDialog() == true)
{
File.WriteAllText(saveFileDialog.FileName, item.LiveQueryPlan);
}
var saveFileDialog = new SaveFileDialog
{
FileName = defaultFileName,
DefaultExt = ".sqlplan",
Filter = "SQL Plan (*.sqlplan)|*.sqlplan|XML Files (*.xml)|*.xml|All Files (*.*)|*.*",
Title = "Save Live Query Plan"
};

if (saveFileDialog.ShowDialog() == true)
{
File.WriteAllText(saveFileDialog.FileName, item.LiveQueryPlan);
}
}

Expand Down Expand Up @@ -798,7 +813,17 @@ private async void ViewEstimatedPlan_Click(object sender, RoutedEventArgs e)
}

if (planXml != null)
{
ViewPlanRequested?.Invoke(planXml, label, queryText);
}
else
{
MessageBox.Show(
"No query plan is available for this row. The plan may have been evicted from the plan cache since it was last collected.",
"No Plan Available",
MessageBoxButton.OK,
MessageBoxImage.Information);
}
}

private async void GetActualPlan_Click(object sender, RoutedEventArgs e)
Expand Down
5 changes: 5 additions & 0 deletions Dashboard/Controls/ResourceMetricsContent.xaml
Original file line number Diff line number Diff line change
Expand Up @@ -44,9 +44,14 @@
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition Height="*"/>
<RowDefinition Height="Auto"/>
</Grid.RowDefinitions>
<TextBlock Text="CPU Utilization (%)" FontWeight="Bold" FontSize="14" Margin="10,5" Foreground="{DynamicResource ForegroundBrush}"/>
<ScottPlot:WpfPlot Grid.Row="1" x:Name="ServerUtilTrendsCpuChart" Margin="5"/>
<TextBlock Grid.Row="2" x:Name="CpuSchedulerStatusText"
Margin="10,0,10,5" FontSize="11"
HorizontalAlignment="Center"
Foreground="{DynamicResource ForegroundBrush}"/>
</Grid>
</Border>

Expand Down
46 changes: 46 additions & 0 deletions Dashboard/Controls/ResourceMetricsContent.xaml.cs
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Documents;
using System.Windows.Media;
using Microsoft.Win32;
using PerformanceMonitorDashboard.Models;
Expand Down Expand Up @@ -982,13 +983,58 @@ private async Task RefreshServerTrendsAsync()
LoadServerTrendsTempdbChart(await tempdbTask, _serverTrendsHoursBack, _serverTrendsFromDate, _serverTrendsToDate);
LoadServerTrendsMemoryChart(await memoryTask, _serverTrendsHoursBack, _serverTrendsFromDate, _serverTrendsToDate);
LoadServerTrendsPerfmonChart(await perfmonTask, _serverTrendsHoursBack, _serverTrendsFromDate, _serverTrendsToDate);

try
{
var pressure = await _databaseService.GetCpuPressureAsync();
UpdateCpuSchedulerStatus(pressure);
}
catch (Exception pressureEx)
{
Logger.Error($"Error loading CPU scheduler pressure: {pressureEx.Message}", pressureEx);
}
}
catch (Exception ex)
{
Logger.Error($"Error loading server trends: {ex.Message}", ex);
}
}

private void UpdateCpuSchedulerStatus(CpuPressureItem? pressure)
{
if (pressure == null)
{
CpuSchedulerStatusText.Text = "";
return;
}

CpuSchedulerStatusText.Inlines.Clear();

var summary = $"Schedulers: {pressure.TotalSchedulers} | " +
$"Workers: {pressure.TotalWorkers:N0}/{pressure.MaxWorkers:N0} ({pressure.WorkerUtilizationPercent:F1}%) | " +
$"Runnable: {pressure.TotalRunnableTasks} ({pressure.AvgRunnableTasksPerScheduler:F2}/sched) | " +
$"Active: {pressure.TotalActiveRequests} | " +
$"Queued: {pressure.TotalQueuedRequests} | ";

CpuSchedulerStatusText.Inlines.Add(new Run(summary));

var levelText = pressure.PressureLevel;
var levelRun = new Run(levelText);

if (levelText.Contains("CRITICAL") || levelText.Contains("HIGH"))
{
levelRun.Foreground = new SolidColorBrush(Color.FromRgb(0xFF, 0x44, 0x44));
levelRun.FontWeight = FontWeights.Bold;
}
else if (levelText.Contains("MEDIUM"))
{
levelRun.Foreground = new SolidColorBrush(Color.FromRgb(0xFF, 0xA5, 0x00));
levelRun.FontWeight = FontWeights.Bold;
}

CpuSchedulerStatusText.Inlines.Add(levelRun);
}

private void LoadServerTrendsCpuChart(IEnumerable<CpuSpikeItem> cpuData, int hoursBack, DateTime? fromDate, DateTime? toDate)
{
DateTime rangeEnd = toDate ?? Helpers.ServerTimeHelper.ServerNow;
Expand Down
2 changes: 1 addition & 1 deletion Dashboard/Dashboard.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
<UseWPF>true</UseWPF>
<AssemblyName>PerformanceMonitorDashboard</AssemblyName>
<Product>SQL Server Performance Monitor Dashboard</Product>
<Version>1.3.0</Version>
<Version>2.0.0</Version>
<Company>Darling Data, LLC</Company>
<Copyright>Copyright © 2026 Darling Data, LLC</Copyright>
<ApplicationIcon>EDD.ico</ApplicationIcon>
Expand Down
11 changes: 11 additions & 0 deletions Dashboard/MainWindow.xaml.cs
Original file line number Diff line number Diff line change
Expand Up @@ -445,6 +445,17 @@ private void OpenServerTab(ServerConnection server)

/* Set server UTC offset for chart axis bounds */
var connStatus = _serverManager.GetConnectionStatus(server.Id);
if (!connStatus.UtcOffsetMinutes.HasValue)
{
/* Background check hasn't run yet — fetch offset synchronously so
the first tab open doesn't default to local timezone. */
try
{
_serverManager.CheckConnectionAsync(server.Id).GetAwaiter().GetResult();
connStatus = _serverManager.GetConnectionStatus(server.Id);
}
catch { /* Fall through to local offset default */ }
}
var utcOffset = connStatus.UtcOffsetMinutes ?? (int)TimeZoneInfo.Local.GetUtcOffset(DateTime.UtcNow).TotalMinutes;
Helpers.ServerTimeHelper.UtcOffsetMinutes = utcOffset;

Expand Down
8 changes: 4 additions & 4 deletions Installer/PerformanceMonitorInstaller.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -20,10 +20,10 @@
<!-- Application metadata -->
<AssemblyName>PerformanceMonitorInstaller</AssemblyName>
<Product>SQL Server Performance Monitor Installer</Product>
<Version>1.3.0</Version>
<AssemblyVersion>1.3.0.0</AssemblyVersion>
<FileVersion>1.3.0.0</FileVersion>
<InformationalVersion>1.3.0</InformationalVersion>
<Version>2.0.0</Version>
<AssemblyVersion>2.0.0.0</AssemblyVersion>
<FileVersion>2.0.0.0</FileVersion>
<InformationalVersion>2.0.0</InformationalVersion>
<Company>Darling Data, LLC</Company>
<Copyright>Copyright © 2026 Darling Data, LLC</Copyright>
<Description>Installation utility for SQL Server Performance Monitor - Supports SQL Server 2016-2025</Description>
Expand Down
2 changes: 1 addition & 1 deletion InstallerGui/InstallerGui.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
<AssemblyName>PerformanceMonitorInstallerGui</AssemblyName>
<RootNamespace>PerformanceMonitorInstallerGui</RootNamespace>
<Product>SQL Server Performance Monitor Installer</Product>
<Version>1.3.0</Version>
<Version>2.0.0</Version>
<Company>Darling Data, LLC</Company>
<Copyright>Copyright © 2026 Darling Data, LLC</Copyright>
<ApplicationIcon>EDD.ico</ApplicationIcon>
Expand Down
Loading
Loading