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
19 changes: 19 additions & 0 deletions Dashboard/Models/BlockedSessionTrendItem.cs
Original file line number Diff line number Diff line change
@@ -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; }
}
}
19 changes: 19 additions & 0 deletions Dashboard/Models/WaitingTaskTrendItem.cs
Original file line number Diff line number Diff line change
@@ -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; }
}
}
32 changes: 32 additions & 0 deletions Dashboard/ServerTab.xaml
Original file line number Diff line number Diff line change
Expand Up @@ -550,6 +550,38 @@
</Grid>
</TabItem>

<!-- Current Waits Sub-Tab -->
<TabItem Header="Current Waits">
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="*"/>
<RowDefinition Height="*"/>
</Grid.RowDefinitions>

<Border Grid.Row="0" BorderBrush="LightGray" BorderThickness="1" Margin="5,5,5,2">
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition Height="*"/>
</Grid.RowDefinitions>
<TextBlock Grid.Row="0" Text="Wait Duration by Wait Type" FontWeight="Bold" HorizontalAlignment="Center" Margin="0,5"/>
<ScottPlot:WpfPlot Grid.Row="1" x:Name="CurrentWaitsDurationChart"/>
</Grid>
</Border>

<Border Grid.Row="1" BorderBrush="LightGray" BorderThickness="1" Margin="5,2,5,5">
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition Height="*"/>
</Grid.RowDefinitions>
<TextBlock Grid.Row="0" Text="Blocked Sessions by Database" FontWeight="Bold" HorizontalAlignment="Center" Margin="0,5"/>
<ScottPlot:WpfPlot Grid.Row="1" x:Name="CurrentWaitsBlockedChart"/>
</Grid>
</Border>
</Grid>
</TabItem>

<!-- Blocking Sub-Tab -->
<TabItem Header="Blocking">
<Grid>
Expand Down
132 changes: 130 additions & 2 deletions Dashboard/ServerTab.xaml.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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)
{
Expand All @@ -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;
Expand Down Expand Up @@ -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();
Expand All @@ -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);

Expand Down Expand Up @@ -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)
{
Expand Down Expand Up @@ -1671,14 +1681,18 @@ 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;

// 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)
Expand Down Expand Up @@ -1935,6 +1949,120 @@ private void LoadLockWaitStatsChart(List<LockWaitStatsItem> data, int hoursBack,
LockWaitStatsChart.Refresh();
}

private void LoadCurrentWaitsDurationChart(List<WaitingTaskTrendItem> 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<BlockedSessionTrendItem> 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
// ====================================================================
Expand Down
Loading
Loading