diff --git a/Dashboard/Models/BlockedSessionTrendItem.cs b/Dashboard/Models/BlockedSessionTrendItem.cs
new file mode 100644
index 00000000..3ef85634
--- /dev/null
+++ b/Dashboard/Models/BlockedSessionTrendItem.cs
@@ -0,0 +1,19 @@
+/*
+ * Copyright (c) 2026 Erik Darling, Darling Data LLC
+ *
+ * This file is part of the SQL Server Performance Monitor.
+ *
+ * Licensed under the MIT License. See LICENSE file in the project root for full license information.
+ */
+
+using System;
+
+namespace PerformanceMonitorDashboard.Models
+{
+ public class BlockedSessionTrendItem
+ {
+ public DateTime CollectionTime { get; set; }
+ public string DatabaseName { get; set; } = string.Empty;
+ public int BlockedCount { get; set; }
+ }
+}
diff --git a/Dashboard/Models/WaitingTaskTrendItem.cs b/Dashboard/Models/WaitingTaskTrendItem.cs
new file mode 100644
index 00000000..5869e4d2
--- /dev/null
+++ b/Dashboard/Models/WaitingTaskTrendItem.cs
@@ -0,0 +1,19 @@
+/*
+ * Copyright (c) 2026 Erik Darling, Darling Data LLC
+ *
+ * This file is part of the SQL Server Performance Monitor.
+ *
+ * Licensed under the MIT License. See LICENSE file in the project root for full license information.
+ */
+
+using System;
+
+namespace PerformanceMonitorDashboard.Models
+{
+ public class WaitingTaskTrendItem
+ {
+ public DateTime CollectionTime { get; set; }
+ public string WaitType { get; set; } = string.Empty;
+ public long TotalWaitMs { get; set; }
+ }
+}
diff --git a/Dashboard/ServerTab.xaml b/Dashboard/ServerTab.xaml
index cb6d5f09..c9bffc01 100644
--- a/Dashboard/ServerTab.xaml
+++ b/Dashboard/ServerTab.xaml
@@ -550,6 +550,38 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/Dashboard/ServerTab.xaml.cs b/Dashboard/ServerTab.xaml.cs
index 47410495..29fe02b0 100644
--- a/Dashboard/ServerTab.xaml.cs
+++ b/Dashboard/ServerTab.xaml.cs
@@ -79,6 +79,8 @@ public partial class ServerTab : UserControl
private Helpers.ChartHoverHelper? _deadlocksHover;
private Helpers.ChartHoverHelper? _deadlockWaitTimeHover;
private Helpers.ChartHoverHelper? _collectorDurationHover;
+ private Helpers.ChartHoverHelper? _currentWaitsDurationHover;
+ private Helpers.ChartHoverHelper? _currentWaitsBlockedHover;
public ServerTab(ServerConnection serverConnection, int utcOffsetMinutes = 0)
{
@@ -94,6 +96,8 @@ public ServerTab(ServerConnection serverConnection, int utcOffsetMinutes = 0)
_deadlocksHover = new Helpers.ChartHoverHelper(BlockingStatsDeadlocksChart, "events");
_deadlockWaitTimeHover = new Helpers.ChartHoverHelper(BlockingStatsDeadlockWaitTimeChart, "ms");
_collectorDurationHover = new Helpers.ChartHoverHelper(CollectorDurationChart, "ms");
+ _currentWaitsDurationHover = new Helpers.ChartHoverHelper(CurrentWaitsDurationChart, "ms");
+ _currentWaitsBlockedHover = new Helpers.ChartHoverHelper(CurrentWaitsBlockedChart, "sessions");
_serverConnection = serverConnection;
UtcOffsetMinutes = utcOffsetMinutes;
@@ -1332,6 +1336,8 @@ private async Task LoadDataAsync()
var deadlocksTask = _databaseService.GetDeadlocksAsync();
var blockingStatsTask = _databaseService.GetBlockingDeadlockStatsAsync(_blockingStatsHoursBack, _blockingStatsFromDate, _blockingStatsToDate);
var lockWaitStatsTask = _databaseService.GetLockWaitStatsAsync(_blockingStatsHoursBack, _blockingStatsFromDate, _blockingStatsToDate);
+ var currentWaitsDurationTask = _databaseService.GetWaitingTaskTrendAsync(_blockingStatsHoursBack, _blockingStatsFromDate, _blockingStatsToDate);
+ var currentWaitsBlockedTask = _databaseService.GetBlockedSessionTrendAsync(_blockingStatsHoursBack, _blockingStatsFromDate, _blockingStatsToDate);
var performanceTask = PerformanceTab.RefreshAllDataAsync();
var memoryTask = MemoryTab.RefreshAllDataAsync();
@@ -1347,7 +1353,7 @@ private async Task LoadDataAsync()
// Wait for everything to complete before _isRefreshing resets
await Task.WhenAll(
- healthTask, durationLogsTask, blockingEventsTask, deadlocksTask, blockingStatsTask, lockWaitStatsTask,
+ healthTask, durationLogsTask, blockingEventsTask, deadlocksTask, blockingStatsTask, lockWaitStatsTask, currentWaitsDurationTask, currentWaitsBlockedTask,
performanceTask, memoryTask, resourceOverviewTask, runningJobsTask,
resourceMetricsTask, dailySummaryTask, criticalIssuesTask, defaultTraceTask, currentConfigTask, configChangesTask, systemEventsTask);
@@ -1390,6 +1396,10 @@ await Task.WhenAll(
var lockWaitStats = await lockWaitStatsTask;
LoadBlockingStatsCharts(blockingStats, _blockingStatsHoursBack, _blockingStatsFromDate, _blockingStatsToDate);
LoadLockWaitStatsChart(lockWaitStats, _blockingStatsHoursBack, _blockingStatsFromDate, _blockingStatsToDate);
+ var currentWaitsDuration = await currentWaitsDurationTask;
+ var currentWaitsBlocked = await currentWaitsBlockedTask;
+ LoadCurrentWaitsDurationChart(currentWaitsDuration, _blockingStatsHoursBack, _blockingStatsFromDate, _blockingStatsToDate);
+ LoadCurrentWaitsBlockedChart(currentWaitsBlocked, _blockingStatsHoursBack, _blockingStatsFromDate, _blockingStatsToDate);
}
catch (Exception blockingStatsEx)
{
@@ -1671,7 +1681,9 @@ private async void BlockingStats_Refresh_Click(object? sender, RoutedEventArgs e
var blockingStatsTask = _databaseService.GetBlockingDeadlockStatsAsync(_blockingStatsHoursBack, _blockingStatsFromDate, _blockingStatsToDate);
var lockWaitStatsTask = _databaseService.GetLockWaitStatsAsync(_blockingStatsHoursBack, _blockingStatsFromDate, _blockingStatsToDate);
- await Task.WhenAll(blockingStatsTask, lockWaitStatsTask);
+ var currentWaitsDurationTask = _databaseService.GetWaitingTaskTrendAsync(_blockingStatsHoursBack, _blockingStatsFromDate, _blockingStatsToDate);
+ var currentWaitsBlockedTask = _databaseService.GetBlockedSessionTrendAsync(_blockingStatsHoursBack, _blockingStatsFromDate, _blockingStatsToDate);
+ await Task.WhenAll(blockingStatsTask, lockWaitStatsTask, currentWaitsDurationTask, currentWaitsBlockedTask);
var data = await blockingStatsTask;
var lockWaitStats = await lockWaitStatsTask;
@@ -1679,6 +1691,8 @@ private async void BlockingStats_Refresh_Click(object? sender, RoutedEventArgs e
// Load charts with explicit time range for proper axis scaling
LoadBlockingStatsCharts(data, _blockingStatsHoursBack, _blockingStatsFromDate, _blockingStatsToDate);
LoadLockWaitStatsChart(lockWaitStats, _blockingStatsHoursBack, _blockingStatsFromDate, _blockingStatsToDate);
+ LoadCurrentWaitsDurationChart(await currentWaitsDurationTask, _blockingStatsHoursBack, _blockingStatsFromDate, _blockingStatsToDate);
+ LoadCurrentWaitsBlockedChart(await currentWaitsBlockedTask, _blockingStatsHoursBack, _blockingStatsFromDate, _blockingStatsToDate);
StatusText.Text = $"Loaded {data.Count} blocking/deadlock stats records";
}
catch (Exception ex)
@@ -1935,6 +1949,120 @@ private void LoadLockWaitStatsChart(List data, int hoursBack,
LockWaitStatsChart.Refresh();
}
+ private void LoadCurrentWaitsDurationChart(List data, int hoursBack, DateTime? fromDate, DateTime? toDate)
+ {
+ DateTime rangeEnd = toDate ?? Helpers.ServerTimeHelper.ServerNow;
+ DateTime rangeStart = fromDate ?? rangeEnd.AddHours(-hoursBack);
+ double xMin = rangeStart.ToOADate();
+ double xMax = rangeEnd.ToOADate();
+
+ if (_legendPanels.TryGetValue(CurrentWaitsDurationChart, out var existingPanel) && existingPanel != null)
+ {
+ CurrentWaitsDurationChart.Plot.Axes.Remove(existingPanel);
+ _legendPanels[CurrentWaitsDurationChart] = null;
+ }
+ CurrentWaitsDurationChart.Plot.Clear();
+ _currentWaitsDurationHover?.Clear();
+ ApplyDarkModeToChart(CurrentWaitsDurationChart);
+
+ var waitTypes = data.Select(d => d.WaitType).Distinct().OrderBy(w => w).ToList();
+ var colors = TabHelpers.ChartColors;
+
+ int colorIndex = 0;
+ foreach (var waitType in waitTypes)
+ {
+ var waitTypeData = data.Where(d => d.WaitType == waitType).OrderBy(d => d.CollectionTime).ToList();
+ if (waitTypeData.Count > 0)
+ {
+ var (xs, ys) = TabHelpers.FillTimeSeriesGaps(
+ waitTypeData.Select(d => d.CollectionTime),
+ waitTypeData.Select(d => (double)d.TotalWaitMs));
+
+ var scatter = CurrentWaitsDurationChart.Plot.Add.Scatter(xs, ys);
+ scatter.LineWidth = 2;
+ scatter.MarkerSize = 5;
+ scatter.Color = colors[colorIndex % colors.Length];
+ scatter.LegendText = waitType;
+ _currentWaitsDurationHover?.Add(scatter, waitType);
+ colorIndex++;
+ }
+ }
+
+ if (data.Count == 0)
+ {
+ double xCenter = xMin + (xMax - xMin) / 2;
+ var noDataText = CurrentWaitsDurationChart.Plot.Add.Text("No data for selected time range", xCenter, 0.5);
+ noDataText.LabelFontSize = 14;
+ noDataText.LabelFontColor = ScottPlot.Colors.Gray;
+ noDataText.LabelAlignment = ScottPlot.Alignment.MiddleCenter;
+ }
+
+ CurrentWaitsDurationChart.Plot.Axes.DateTimeTicksBottom();
+ CurrentWaitsDurationChart.Plot.Axes.SetLimitsX(xMin, xMax);
+ CurrentWaitsDurationChart.Plot.YLabel("Total Wait Duration (ms)");
+ _legendPanels[CurrentWaitsDurationChart] = CurrentWaitsDurationChart.Plot.ShowLegend(ScottPlot.Edge.Bottom);
+ CurrentWaitsDurationChart.Plot.Legend.FontSize = 12;
+ LockChartVerticalAxis(CurrentWaitsDurationChart);
+ CurrentWaitsDurationChart.Refresh();
+ }
+
+ private void LoadCurrentWaitsBlockedChart(List data, int hoursBack, DateTime? fromDate, DateTime? toDate)
+ {
+ DateTime rangeEnd = toDate ?? Helpers.ServerTimeHelper.ServerNow;
+ DateTime rangeStart = fromDate ?? rangeEnd.AddHours(-hoursBack);
+ double xMin = rangeStart.ToOADate();
+ double xMax = rangeEnd.ToOADate();
+
+ if (_legendPanels.TryGetValue(CurrentWaitsBlockedChart, out var existingPanel) && existingPanel != null)
+ {
+ CurrentWaitsBlockedChart.Plot.Axes.Remove(existingPanel);
+ _legendPanels[CurrentWaitsBlockedChart] = null;
+ }
+ CurrentWaitsBlockedChart.Plot.Clear();
+ _currentWaitsBlockedHover?.Clear();
+ ApplyDarkModeToChart(CurrentWaitsBlockedChart);
+
+ var databases = data.Select(d => d.DatabaseName).Distinct().OrderBy(d => d).ToList();
+ var colors = TabHelpers.ChartColors;
+
+ int colorIndex = 0;
+ foreach (var db in databases)
+ {
+ var dbData = data.Where(d => d.DatabaseName == db).OrderBy(d => d.CollectionTime).ToList();
+ if (dbData.Count > 0)
+ {
+ var (xs, ys) = TabHelpers.FillTimeSeriesGaps(
+ dbData.Select(d => d.CollectionTime),
+ dbData.Select(d => (double)d.BlockedCount));
+
+ var scatter = CurrentWaitsBlockedChart.Plot.Add.Scatter(xs, ys);
+ scatter.LineWidth = 2;
+ scatter.MarkerSize = 5;
+ scatter.Color = colors[colorIndex % colors.Length];
+ scatter.LegendText = db;
+ _currentWaitsBlockedHover?.Add(scatter, db);
+ colorIndex++;
+ }
+ }
+
+ if (data.Count == 0)
+ {
+ double xCenter = xMin + (xMax - xMin) / 2;
+ var noDataText = CurrentWaitsBlockedChart.Plot.Add.Text("No data for selected time range", xCenter, 0.5);
+ noDataText.LabelFontSize = 14;
+ noDataText.LabelFontColor = ScottPlot.Colors.Gray;
+ noDataText.LabelAlignment = ScottPlot.Alignment.MiddleCenter;
+ }
+
+ CurrentWaitsBlockedChart.Plot.Axes.DateTimeTicksBottom();
+ CurrentWaitsBlockedChart.Plot.Axes.SetLimitsX(xMin, xMax);
+ CurrentWaitsBlockedChart.Plot.YLabel("Blocked Sessions");
+ _legendPanels[CurrentWaitsBlockedChart] = CurrentWaitsBlockedChart.Plot.ShowLegend(ScottPlot.Edge.Bottom);
+ CurrentWaitsBlockedChart.Plot.Legend.FontSize = 12;
+ LockChartVerticalAxis(CurrentWaitsBlockedChart);
+ CurrentWaitsBlockedChart.Refresh();
+ }
+
// ====================================================================
// Context Menu Event Handlers
// ====================================================================
diff --git a/Dashboard/Services/DatabaseService.QueryPerformance.cs b/Dashboard/Services/DatabaseService.QueryPerformance.cs
index fd80e20e..e61e43da 100644
--- a/Dashboard/Services/DatabaseService.QueryPerformance.cs
+++ b/Dashboard/Services/DatabaseService.QueryPerformance.cs
@@ -2911,5 +2911,143 @@ AND dest.text IS NOT NULL
return items;
}
+
+ public async Task> GetWaitingTaskTrendAsync(int hoursBack = 24, DateTime? fromDate = null, DateTime? toDate = null)
+ {
+ var items = new List();
+
+ await using var tc = await OpenThrottledConnectionAsync();
+ var connection = tc.Connection;
+
+ string query;
+ if (fromDate.HasValue && toDate.HasValue)
+ {
+ query = @"
+ SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED;
+
+ SELECT
+ wt.collection_time,
+ wt.wait_type,
+ total_wait_ms = SUM(wt.wait_duration_ms)
+ FROM collect.waiting_tasks AS wt
+ WHERE wt.collection_time >= @from_date
+ AND wt.collection_time <= @to_date
+ AND wt.wait_type IS NOT NULL
+ GROUP BY
+ wt.collection_time,
+ wt.wait_type
+ ORDER BY
+ wt.collection_time,
+ wt.wait_type;";
+ }
+ else
+ {
+ query = @"
+ SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED;
+
+ SELECT
+ wt.collection_time,
+ wt.wait_type,
+ total_wait_ms = SUM(wt.wait_duration_ms)
+ FROM collect.waiting_tasks AS wt
+ WHERE wt.collection_time >= DATEADD(HOUR, @hours_back, SYSDATETIME())
+ AND wt.wait_type IS NOT NULL
+ GROUP BY
+ wt.collection_time,
+ wt.wait_type
+ ORDER BY
+ wt.collection_time,
+ wt.wait_type;";
+ }
+
+ using var command = new SqlCommand(query, connection);
+ command.CommandTimeout = 120;
+ command.Parameters.Add(new SqlParameter("@hours_back", SqlDbType.Int) { Value = -hoursBack });
+ if (fromDate.HasValue) command.Parameters.Add(new SqlParameter("@from_date", SqlDbType.DateTime2) { Value = fromDate.Value });
+ if (toDate.HasValue) command.Parameters.Add(new SqlParameter("@to_date", SqlDbType.DateTime2) { Value = toDate.Value });
+
+ using var reader = await command.ExecuteReaderAsync();
+ while (await reader.ReadAsync())
+ {
+ items.Add(new WaitingTaskTrendItem
+ {
+ CollectionTime = reader.GetDateTime(0),
+ WaitType = reader.IsDBNull(1) ? string.Empty : reader.GetString(1),
+ TotalWaitMs = reader.IsDBNull(2) ? 0L : Convert.ToInt64(reader.GetValue(2), CultureInfo.InvariantCulture)
+ });
+ }
+
+ return items;
+ }
+
+ public async Task> GetBlockedSessionTrendAsync(int hoursBack = 24, DateTime? fromDate = null, DateTime? toDate = null)
+ {
+ var items = new List();
+
+ await using var tc = await OpenThrottledConnectionAsync();
+ var connection = tc.Connection;
+
+ string query;
+ if (fromDate.HasValue && toDate.HasValue)
+ {
+ query = @"
+ SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED;
+
+ SELECT
+ wt.collection_time,
+ wt.database_name,
+ blocked_count = COUNT(*)
+ FROM collect.waiting_tasks AS wt
+ WHERE wt.blocking_session_id > 0
+ AND wt.collection_time >= @from_date
+ AND wt.collection_time <= @to_date
+ AND wt.database_name IS NOT NULL
+ GROUP BY
+ wt.collection_time,
+ wt.database_name
+ ORDER BY
+ wt.collection_time,
+ wt.database_name;";
+ }
+ else
+ {
+ query = @"
+ SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED;
+
+ SELECT
+ wt.collection_time,
+ wt.database_name,
+ blocked_count = COUNT(*)
+ FROM collect.waiting_tasks AS wt
+ WHERE wt.blocking_session_id > 0
+ AND wt.collection_time >= DATEADD(HOUR, @hours_back, SYSDATETIME())
+ AND wt.database_name IS NOT NULL
+ GROUP BY
+ wt.collection_time,
+ wt.database_name
+ ORDER BY
+ wt.collection_time,
+ wt.database_name;";
+ }
+
+ using var command = new SqlCommand(query, connection);
+ command.CommandTimeout = 120;
+ command.Parameters.Add(new SqlParameter("@hours_back", SqlDbType.Int) { Value = -hoursBack });
+ if (fromDate.HasValue) command.Parameters.Add(new SqlParameter("@from_date", SqlDbType.DateTime2) { Value = fromDate.Value });
+ if (toDate.HasValue) command.Parameters.Add(new SqlParameter("@to_date", SqlDbType.DateTime2) { Value = toDate.Value });
+
+ using var reader = await command.ExecuteReaderAsync();
+ while (await reader.ReadAsync())
+ {
+ items.Add(new BlockedSessionTrendItem
+ {
+ CollectionTime = reader.GetDateTime(0),
+ DatabaseName = reader.IsDBNull(1) ? string.Empty : reader.GetString(1),
+ BlockedCount = reader.IsDBNull(2) ? 0 : Convert.ToInt32(reader.GetValue(2), CultureInfo.InvariantCulture)
+ });
+ }
+
+ return items;
+ }
}
}
diff --git a/Lite/Controls/ServerTab.xaml b/Lite/Controls/ServerTab.xaml
index d77dbb59..fb0bc2d5 100644
--- a/Lite/Controls/ServerTab.xaml
+++ b/Lite/Controls/ServerTab.xaml
@@ -771,6 +771,20 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
_memoryClerkItems = new();
@@ -174,6 +176,8 @@ public ServerTab(ServerConnection server, DuckDbInitializer duckDb, CredentialSe
_memoryClerksHover = new Helpers.ChartHoverHelper(MemoryClerksChart, "MB");
_memoryGrantSizingHover = new Helpers.ChartHoverHelper(MemoryGrantSizingChart, "MB");
_memoryGrantActivityHover = new Helpers.ChartHoverHelper(MemoryGrantActivityChart, "");
+ _currentWaitsDurationHover = new Helpers.ChartHoverHelper(CurrentWaitsDurationChart, "ms");
+ _currentWaitsBlockedHover = new Helpers.ChartHoverHelper(CurrentWaitsBlockedChart, "sessions");
/* Initial load is triggered by MainWindow.ConnectToServer calling RefreshData()
after collectors finish - no Loaded handler needed */
@@ -510,10 +514,13 @@ await System.Threading.Tasks.Task.WhenAll(
var procDurationTrendTask = SafeQueryAsync(() => _dataService.GetProcedureDurationTrendAsync(_serverId, hoursBack, fromDate, toDate));
var queryStoreDurationTrendTask = SafeQueryAsync(() => _dataService.GetQueryStoreDurationTrendAsync(_serverId, hoursBack, fromDate, toDate));
var executionCountTrendTask = SafeQueryAsync(() => _dataService.GetExecutionCountTrendAsync(_serverId, hoursBack, fromDate, toDate));
+ var currentWaitsDurationTask = SafeQueryAsync(() => _dataService.GetWaitingTaskTrendAsync(_serverId, hoursBack, fromDate, toDate));
+ var currentWaitsBlockedTask = SafeQueryAsync(() => _dataService.GetBlockedSessionTrendAsync(_serverId, hoursBack, fromDate, toDate));
await System.Threading.Tasks.Task.WhenAll(
lockWaitTrendTask, blockingTrendTask, deadlockTrendTask,
- queryDurationTrendTask, procDurationTrendTask, queryStoreDurationTrendTask, executionCountTrendTask);
+ queryDurationTrendTask, procDurationTrendTask, queryStoreDurationTrendTask, executionCountTrendTask,
+ currentWaitsDurationTask, currentWaitsBlockedTask);
loadSw.Stop();
@@ -563,6 +570,8 @@ await System.Threading.Tasks.Task.WhenAll(
UpdateLockWaitTrendChart(lockWaitTrendTask.Result, hoursBack, fromDate, toDate);
UpdateBlockingTrendChart(blockingTrendTask.Result, hoursBack, fromDate, toDate);
UpdateDeadlockTrendChart(deadlockTrendTask.Result, hoursBack, fromDate, toDate);
+ UpdateCurrentWaitsDurationChart(currentWaitsDurationTask.Result, hoursBack, fromDate, toDate);
+ UpdateCurrentWaitsBlockedChart(currentWaitsBlockedTask.Result, hoursBack, fromDate, toDate);
UpdateQueryDurationTrendChart(queryDurationTrendTask.Result);
UpdateProcDurationTrendChart(procDurationTrendTask.Result);
UpdateQueryStoreDurationTrendChart(queryStoreDurationTrendTask.Result);
@@ -1192,6 +1201,138 @@ private void UpdateDeadlockTrendChart(List data, int hoursBack, Date
DeadlockTrendChart.Refresh();
}
+ /* ========== Current Waits Charts ========== */
+
+ private void UpdateCurrentWaitsDurationChart(List data, int hoursBack, DateTime? fromDate, DateTime? toDate)
+ {
+ ClearChart(CurrentWaitsDurationChart);
+ ApplyDarkTheme(CurrentWaitsDurationChart);
+
+ DateTime rangeStart, rangeEnd;
+ if (fromDate.HasValue && toDate.HasValue)
+ {
+ rangeStart = fromDate.Value;
+ rangeEnd = toDate.Value;
+ }
+ else
+ {
+ rangeEnd = DateTime.UtcNow.AddMinutes(UtcOffsetMinutes);
+ rangeStart = rangeEnd.AddHours(-hoursBack);
+ }
+
+ _currentWaitsDurationHover?.Clear();
+ if (data.Count == 0)
+ {
+ var zeroLine = CurrentWaitsDurationChart.Plot.Add.Scatter(
+ new[] { rangeStart.ToOADate(), rangeEnd.ToOADate() },
+ new[] { 0.0, 0.0 });
+ zeroLine.LegendText = "Current Waits";
+ zeroLine.Color = ScottPlot.Color.FromHex("#4FC3F7");
+ zeroLine.MarkerSize = 0;
+ CurrentWaitsDurationChart.Plot.Axes.DateTimeTicksBottom();
+ CurrentWaitsDurationChart.Plot.Axes.SetLimitsX(rangeStart.ToOADate(), rangeEnd.ToOADate());
+ ReapplyAxisColors(CurrentWaitsDurationChart);
+ CurrentWaitsDurationChart.Plot.YLabel("Total Wait Duration (ms)");
+ SetChartYLimitsWithLegendPadding(CurrentWaitsDurationChart, 0, 1);
+ ShowChartLegend(CurrentWaitsDurationChart);
+ CurrentWaitsDurationChart.Refresh();
+ return;
+ }
+
+ var grouped = data.GroupBy(d => d.WaitType).OrderBy(g => g.Key).ToList();
+ double globalMax = 0;
+
+ for (int i = 0; i < grouped.Count; i++)
+ {
+ var group = grouped[i];
+ var ordered = group.OrderBy(t => t.CollectionTime).ToList();
+ var times = ordered.Select(t => t.CollectionTime.AddMinutes(UtcOffsetMinutes).ToOADate()).ToArray();
+ var values = ordered.Select(t => (double)t.TotalWaitMs).ToArray();
+
+ var plot = CurrentWaitsDurationChart.Plot.Add.Scatter(times, values);
+ plot.LegendText = group.Key;
+ plot.LineWidth = 2;
+ plot.MarkerSize = 5;
+ plot.Color = ScottPlot.Color.FromHex(SeriesColors[i % SeriesColors.Length]);
+ _currentWaitsDurationHover?.Add(plot, group.Key);
+
+ if (values.Length > 0) globalMax = Math.Max(globalMax, values.Max());
+ }
+
+ CurrentWaitsDurationChart.Plot.Axes.DateTimeTicksBottom();
+ CurrentWaitsDurationChart.Plot.Axes.SetLimitsX(rangeStart.ToOADate(), rangeEnd.ToOADate());
+ ReapplyAxisColors(CurrentWaitsDurationChart);
+ CurrentWaitsDurationChart.Plot.YLabel("Total Wait Duration (ms)");
+ SetChartYLimitsWithLegendPadding(CurrentWaitsDurationChart, 0, globalMax > 0 ? globalMax : 1);
+ ShowChartLegend(CurrentWaitsDurationChart);
+ CurrentWaitsDurationChart.Refresh();
+ }
+
+ private void UpdateCurrentWaitsBlockedChart(List data, int hoursBack, DateTime? fromDate, DateTime? toDate)
+ {
+ ClearChart(CurrentWaitsBlockedChart);
+ ApplyDarkTheme(CurrentWaitsBlockedChart);
+
+ DateTime rangeStart, rangeEnd;
+ if (fromDate.HasValue && toDate.HasValue)
+ {
+ rangeStart = fromDate.Value;
+ rangeEnd = toDate.Value;
+ }
+ else
+ {
+ rangeEnd = DateTime.UtcNow.AddMinutes(UtcOffsetMinutes);
+ rangeStart = rangeEnd.AddHours(-hoursBack);
+ }
+
+ _currentWaitsBlockedHover?.Clear();
+ if (data.Count == 0)
+ {
+ var zeroLine = CurrentWaitsBlockedChart.Plot.Add.Scatter(
+ new[] { rangeStart.ToOADate(), rangeEnd.ToOADate() },
+ new[] { 0.0, 0.0 });
+ zeroLine.LegendText = "Blocked Sessions";
+ zeroLine.Color = ScottPlot.Color.FromHex("#E57373");
+ zeroLine.MarkerSize = 0;
+ CurrentWaitsBlockedChart.Plot.Axes.DateTimeTicksBottom();
+ CurrentWaitsBlockedChart.Plot.Axes.SetLimitsX(rangeStart.ToOADate(), rangeEnd.ToOADate());
+ ReapplyAxisColors(CurrentWaitsBlockedChart);
+ CurrentWaitsBlockedChart.Plot.YLabel("Blocked Sessions");
+ SetChartYLimitsWithLegendPadding(CurrentWaitsBlockedChart, 0, 1);
+ ShowChartLegend(CurrentWaitsBlockedChart);
+ CurrentWaitsBlockedChart.Refresh();
+ return;
+ }
+
+ var grouped = data.GroupBy(d => d.DatabaseName).OrderBy(g => g.Key).ToList();
+ double globalMax = 0;
+
+ for (int i = 0; i < grouped.Count; i++)
+ {
+ var group = grouped[i];
+ var ordered = group.OrderBy(t => t.CollectionTime).ToList();
+ var times = ordered.Select(t => t.CollectionTime.AddMinutes(UtcOffsetMinutes).ToOADate()).ToArray();
+ var values = ordered.Select(t => (double)t.BlockedCount).ToArray();
+
+ var plot = CurrentWaitsBlockedChart.Plot.Add.Scatter(times, values);
+ plot.LegendText = group.Key;
+ plot.LineWidth = 2;
+ plot.MarkerSize = 5;
+ plot.Color = ScottPlot.Color.FromHex(SeriesColors[i % SeriesColors.Length]);
+ _currentWaitsBlockedHover?.Add(plot, group.Key);
+
+ if (values.Length > 0) globalMax = Math.Max(globalMax, values.Max());
+ }
+
+ CurrentWaitsBlockedChart.Plot.Axes.DateTimeTicksBottom();
+ CurrentWaitsBlockedChart.Plot.Axes.SetLimitsX(rangeStart.ToOADate(), rangeEnd.ToOADate());
+ ReapplyAxisColors(CurrentWaitsBlockedChart);
+ CurrentWaitsBlockedChart.Plot.YLabel("Blocked Sessions");
+ SetChartYLimitsWithLegendPadding(CurrentWaitsBlockedChart, 0, globalMax > 0 ? globalMax : 1);
+ ShowChartLegend(CurrentWaitsBlockedChart);
+ CurrentWaitsBlockedChart.Refresh();
+ }
+
/* ========== Performance Trend Charts ========== */
private void UpdateQueryDurationTrendChart(List data)
diff --git a/Lite/Services/LocalDataService.WaitingTasks.cs b/Lite/Services/LocalDataService.WaitingTasks.cs
index e9ca02ad..0f0a3202 100644
--- a/Lite/Services/LocalDataService.WaitingTasks.cs
+++ b/Lite/Services/LocalDataService.WaitingTasks.cs
@@ -57,6 +57,97 @@ FROM waiting_tasks
return items;
}
+
+ ///
+ /// Gets waiting task duration trend grouped by wait type for charting.
+ ///
+ public async Task> GetWaitingTaskTrendAsync(int serverId, int hoursBack = 24, DateTime? fromDate = null, DateTime? toDate = null)
+ {
+ using var connection = await OpenConnectionAsync();
+ using var command = connection.CreateCommand();
+
+ var (startTime, endTime) = GetTimeRange(hoursBack, fromDate, toDate);
+
+ command.CommandText = @"
+SELECT
+ collection_time,
+ wait_type,
+ SUM(wait_duration_ms) AS total_wait_ms
+FROM waiting_tasks
+WHERE server_id = $1
+AND collection_time >= $2
+AND collection_time <= $3
+AND wait_type IS NOT NULL
+GROUP BY
+ collection_time,
+ wait_type
+ORDER BY
+ collection_time,
+ wait_type";
+
+ command.Parameters.Add(new DuckDBParameter { Value = serverId });
+ command.Parameters.Add(new DuckDBParameter { Value = startTime });
+ command.Parameters.Add(new DuckDBParameter { Value = endTime });
+
+ var items = new List();
+ using var reader = await command.ExecuteReaderAsync();
+ while (await reader.ReadAsync())
+ {
+ items.Add(new WaitingTaskTrendPoint
+ {
+ CollectionTime = reader.GetDateTime(0),
+ WaitType = reader.IsDBNull(1) ? "" : reader.GetString(1),
+ TotalWaitMs = reader.IsDBNull(2) ? 0 : ToInt64(reader.GetValue(2))
+ });
+ }
+ return items;
+ }
+
+ ///
+ /// Gets blocked session count trend grouped by database for charting.
+ ///
+ public async Task> GetBlockedSessionTrendAsync(int serverId, int hoursBack = 24, DateTime? fromDate = null, DateTime? toDate = null)
+ {
+ using var connection = await OpenConnectionAsync();
+ using var command = connection.CreateCommand();
+
+ var (startTime, endTime) = GetTimeRange(hoursBack, fromDate, toDate);
+
+ command.CommandText = @"
+SELECT
+ collection_time,
+ database_name,
+ COUNT(*) AS blocked_count
+FROM waiting_tasks
+WHERE server_id = $1
+AND blocking_session_id > 0
+AND collection_time >= $2
+AND collection_time <= $3
+AND database_name IS NOT NULL
+GROUP BY
+ collection_time,
+ database_name
+ORDER BY
+ collection_time,
+ database_name";
+
+ command.Parameters.Add(new DuckDBParameter { Value = serverId });
+ command.Parameters.Add(new DuckDBParameter { Value = startTime });
+ command.Parameters.Add(new DuckDBParameter { Value = endTime });
+
+ var items = new List();
+ using var reader = await command.ExecuteReaderAsync();
+ while (await reader.ReadAsync())
+ {
+ items.Add(new BlockedSessionTrendPoint
+ {
+ CollectionTime = reader.GetDateTime(0),
+ DatabaseName = reader.IsDBNull(1) ? "" : reader.GetString(1),
+ BlockedCount = reader.IsDBNull(2) ? 0 : Convert.ToInt32(reader.GetValue(2))
+ });
+ }
+ return items;
+ }
}
public class WaitingTaskRow
@@ -75,3 +166,17 @@ public class WaitingTaskRow
? $"{WaitDurationMs / 1000.0:F1} s"
: $"{WaitDurationMs / 60000.0:F1} min";
}
+
+public class WaitingTaskTrendPoint
+{
+ public DateTime CollectionTime { get; set; }
+ public string WaitType { get; set; } = "";
+ public long TotalWaitMs { get; set; }
+}
+
+public class BlockedSessionTrendPoint
+{
+ public DateTime CollectionTime { get; set; }
+ public string DatabaseName { get; set; } = "";
+ public int BlockedCount { get; set; }
+}
diff --git a/Lite/Services/RemoteCollectorService.WaitingTasks.cs b/Lite/Services/RemoteCollectorService.WaitingTasks.cs
index 168bef69..e3b31f70 100644
--- a/Lite/Services/RemoteCollectorService.WaitingTasks.cs
+++ b/Lite/Services/RemoteCollectorService.WaitingTasks.cs
@@ -32,7 +32,6 @@ private async Task CollectWaitingTasksAsync(ServerConnection server, Cancel
wait_type = wt.wait_type,
wait_duration_ms = wt.wait_duration_ms,
blocking_session_id = wt.blocking_session_id,
- resource_description = wt.resource_description,
database_name = DB_NAME(er.database_id)
FROM sys.dm_os_waiting_tasks AS wt
LEFT JOIN sys.dm_exec_requests AS er
@@ -73,8 +72,7 @@ AND wt.wait_type IS NOT NULL
var waitType = reader.IsDBNull(1) ? null : reader.GetString(1);
var waitDurationMs = reader.IsDBNull(2) ? 0L : reader.GetInt64(2);
var blockingSessionId = reader.IsDBNull(3) ? (short?)null : reader.GetInt16(3);
- var resourceDescription = reader.IsDBNull(4) ? null : reader.GetString(4);
- var databaseName = reader.IsDBNull(5) ? null : reader.GetString(5);
+ var databaseName = reader.IsDBNull(4) ? null : reader.GetString(4);
var row = appender.CreateRow();
row.AppendValue(GenerateCollectionId())
@@ -85,7 +83,7 @@ AND wt.wait_type IS NOT NULL
.AppendValue(waitType)
.AppendValue(waitDurationMs)
.AppendValue(blockingSessionId.HasValue ? (int?)blockingSessionId.Value : null)
- .AppendValue(resourceDescription)
+ .AppendValue((string?)null) /* resource_description — no longer collected */
.AppendValue(databaseName)
.EndRow();