diff --git a/Dashboard/Controls/ResourceMetricsContent.xaml b/Dashboard/Controls/ResourceMetricsContent.xaml
index 9ea43c3e..9694d495 100644
--- a/Dashboard/Controls/ResourceMetricsContent.xaml
+++ b/Dashboard/Controls/ResourceMetricsContent.xaml
@@ -142,7 +142,14 @@
-
+
+
+
+
+
+
+
+
diff --git a/Dashboard/Controls/ResourceMetricsContent.xaml.cs b/Dashboard/Controls/ResourceMetricsContent.xaml.cs
index 604aaff0..8d986440 100644
--- a/Dashboard/Controls/ResourceMetricsContent.xaml.cs
+++ b/Dashboard/Controls/ResourceMetricsContent.xaml.cs
@@ -1880,6 +1880,12 @@ private async void WaitType_CheckChanged(object sender, RoutedEventArgs e)
await UpdateWaitStatsDetailChartAsync();
}
+ private void WaitStatsMetric_SelectionChanged(object sender, SelectionChangedEventArgs e)
+ {
+ if (_allWaitStatsDetailData != null)
+ LoadWaitStatsDetailChart(_allWaitStatsDetailData, _waitStatsDetailHoursBack, _waitStatsDetailFromDate, _waitStatsDetailToDate);
+ }
+
private void RefreshWaitTypeListOrder()
{
if (_waitTypeItems == null) return;
@@ -2050,9 +2056,12 @@ private void LoadWaitStatsDetailChart(List? data, int hoursB
WaitStatsDetailChart.Plot.Axes.Remove(existingWaitStatsPanel);
_legendPanels[WaitStatsDetailChart] = null;
}
+ bool useAvgPerWait = WaitStatsMetricCombo?.SelectedIndex == 1;
+
WaitStatsDetailChart.Plot.Clear();
TabHelpers.ApplyDarkModeToChart(WaitStatsDetailChart);
_waitStatsHover?.Clear();
+ if (_waitStatsHover != null) _waitStatsHover.Unit = useAvgPerWait ? "ms/wait" : "ms/sec";
if (data == null || data.Count == 0 || _waitTypeItems == null)
{
@@ -2067,7 +2076,6 @@ private void LoadWaitStatsDetailChart(List? data, int hoursB
WaitStatsDetailChart.Refresh();
return;
}
-
var colors = TabHelpers.ChartColors;
// Get all time points across all wait types for gap filling
@@ -2080,7 +2088,8 @@ private void LoadWaitStatsDetailChart(List? data, int hoursB
.GroupBy(d => d.CollectionTime)
.Select(g => new {
CollectionTime = g.Key,
- WaitTimeMsPerSecond = g.Sum(x => x.WaitTimeMsPerSecond)
+ WaitTimeMsPerSecond = g.Sum(x => x.WaitTimeMsPerSecond),
+ AvgMsPerWait = g.Average(x => x.AvgMsPerWait)
})
.OrderBy(d => d.CollectionTime)
.ToList();
@@ -2088,7 +2097,9 @@ private void LoadWaitStatsDetailChart(List? data, int hoursB
if (waitTypeData.Count >= 1)
{
var timePoints = waitTypeData.Select(d => d.CollectionTime);
- var values = waitTypeData.Select(d => (double)d.WaitTimeMsPerSecond);
+ var values = useAvgPerWait
+ ? waitTypeData.Select(d => (double)d.AvgMsPerWait)
+ : waitTypeData.Select(d => (double)d.WaitTimeMsPerSecond);
var (xs, ys) = TabHelpers.FillTimeSeriesGaps(timePoints, values);
var scatter = WaitStatsDetailChart.Plot.Add.Scatter(xs, ys);
@@ -2124,7 +2135,7 @@ private void LoadWaitStatsDetailChart(List? data, int hoursB
WaitStatsDetailChart.Plot.Axes.DateTimeTicksBottom();
WaitStatsDetailChart.Plot.Axes.SetLimitsX(xMin, xMax);
TabHelpers.SetChartYLimitsWithLegendPadding(WaitStatsDetailChart);
- WaitStatsDetailChart.Plot.YLabel("Wait Time (ms/sec)");
+ WaitStatsDetailChart.Plot.YLabel(useAvgPerWait ? "Avg Wait Time (ms/wait)" : "Wait Time (ms/sec)");
WaitStatsDetailChart.Plot.HideGrid();
TabHelpers.LockChartVerticalAxis(WaitStatsDetailChart);
WaitStatsDetailChart.Refresh();
diff --git a/Dashboard/Helpers/ChartHoverHelper.cs b/Dashboard/Helpers/ChartHoverHelper.cs
index 747c5d55..048e0c37 100644
--- a/Dashboard/Helpers/ChartHoverHelper.cs
+++ b/Dashboard/Helpers/ChartHoverHelper.cs
@@ -18,7 +18,7 @@ internal sealed class ChartHoverHelper
private readonly List<(ScottPlot.Plottables.Scatter Scatter, string Label)> _scatters = new();
private readonly Popup _popup;
private readonly TextBlock _text;
- private readonly string _unit;
+ private string _unit;
private DateTime _lastUpdate;
public ChartHoverHelper(ScottPlot.WPF.WpfPlot chart, string unit)
@@ -53,6 +53,8 @@ public ChartHoverHelper(ScottPlot.WPF.WpfPlot chart, string unit)
chart.MouseLeave += OnMouseLeave;
}
+ public string Unit { get => _unit; set => _unit = value; }
+
public void Clear() => _scatters.Clear();
public void Add(ScottPlot.Plottables.Scatter scatter, string label) =>
diff --git a/Dashboard/Models/WaitStatsDataPoint.cs b/Dashboard/Models/WaitStatsDataPoint.cs
index 5471e88c..eba716b2 100644
--- a/Dashboard/Models/WaitStatsDataPoint.cs
+++ b/Dashboard/Models/WaitStatsDataPoint.cs
@@ -16,5 +16,6 @@ public class WaitStatsDataPoint
public string WaitType { get; set; } = string.Empty;
public decimal WaitTimeMsPerSecond { get; set; }
public decimal SignalWaitTimeMsPerSecond { get; set; }
+ public decimal AvgMsPerWait { get; set; }
}
}
diff --git a/Dashboard/Services/DatabaseService.ResourceMetrics.cs b/Dashboard/Services/DatabaseService.ResourceMetrics.cs
index 2d2fb6f8..5bead59f 100644
--- a/Dashboard/Services/DatabaseService.ResourceMetrics.cs
+++ b/Dashboard/Services/DatabaseService.ResourceMetrics.cs
@@ -1723,6 +1723,14 @@ PARTITION BY
ORDER BY
ws.collection_time
),
+ waiting_tasks_delta =
+ ws.waiting_tasks_count - LAG(ws.waiting_tasks_count, 1, ws.waiting_tasks_count) OVER
+ (
+ PARTITION BY
+ ws.wait_type
+ ORDER BY
+ ws.collection_time
+ ),
interval_seconds =
DATEDIFF
(
@@ -1754,6 +1762,12 @@ ELSE 0
WHEN wd.interval_seconds > 0
THEN CAST(wd.signal_wait_time_ms_delta AS decimal(19, 4)) / wd.interval_seconds
ELSE 0
+ END,
+ avg_ms_per_wait =
+ CASE
+ WHEN wd.waiting_tasks_delta > 0
+ THEN CAST(wd.wait_time_ms_delta AS decimal(19, 4)) / wd.waiting_tasks_delta
+ ELSE 0
END
FROM wait_deltas AS wd
WHERE wd.wait_time_ms_delta > 0
@@ -1788,6 +1802,14 @@ PARTITION BY
ORDER BY
ws.collection_time
),
+ waiting_tasks_delta =
+ ws.waiting_tasks_count - LAG(ws.waiting_tasks_count, 1, ws.waiting_tasks_count) OVER
+ (
+ PARTITION BY
+ ws.wait_type
+ ORDER BY
+ ws.collection_time
+ ),
interval_seconds =
DATEDIFF
(
@@ -1818,6 +1840,12 @@ ELSE 0
WHEN wd.interval_seconds > 0
THEN CAST(wd.signal_wait_time_ms_delta AS decimal(19, 4)) / wd.interval_seconds
ELSE 0
+ END,
+ avg_ms_per_wait =
+ CASE
+ WHEN wd.waiting_tasks_delta > 0
+ THEN CAST(wd.wait_time_ms_delta AS decimal(19, 4)) / wd.waiting_tasks_delta
+ ELSE 0
END
FROM wait_deltas AS wd
WHERE wd.wait_time_ms_delta > 0
@@ -1840,7 +1868,8 @@ ORDER BY
CollectionTime = reader.GetDateTime(0),
WaitType = reader.IsDBNull(1) ? string.Empty : reader.GetString(1),
WaitTimeMsPerSecond = reader.IsDBNull(2) ? 0m : Convert.ToDecimal(reader.GetValue(2), CultureInfo.InvariantCulture),
- SignalWaitTimeMsPerSecond = reader.IsDBNull(3) ? 0m : Convert.ToDecimal(reader.GetValue(3), CultureInfo.InvariantCulture)
+ SignalWaitTimeMsPerSecond = reader.IsDBNull(3) ? 0m : Convert.ToDecimal(reader.GetValue(3), CultureInfo.InvariantCulture),
+ AvgMsPerWait = reader.IsDBNull(4) ? 0m : Convert.ToDecimal(reader.GetValue(4), CultureInfo.InvariantCulture)
});
}
@@ -1890,6 +1919,12 @@ ORDER BY ws.collection_time
PARTITION BY ws.wait_type
ORDER BY ws.collection_time
),
+ waiting_tasks_delta =
+ ws.waiting_tasks_count - LAG(ws.waiting_tasks_count, 1, ws.waiting_tasks_count) OVER
+ (
+ PARTITION BY ws.wait_type
+ ORDER BY ws.collection_time
+ ),
interval_seconds =
DATEDIFF(SECOND, LAG(ws.collection_time, 1, ws.collection_time) OVER
(
@@ -1911,6 +1946,10 @@ THEN CAST(wd.wait_time_ms_delta AS decimal(19, 4)) / wd.interval_seconds
signal_wait_time_ms_per_second =
CASE WHEN wd.interval_seconds > 0
THEN CAST(wd.signal_wait_time_ms_delta AS decimal(19, 4)) / wd.interval_seconds
+ ELSE 0 END,
+ avg_ms_per_wait =
+ CASE WHEN wd.waiting_tasks_delta > 0
+ THEN CAST(wd.wait_time_ms_delta AS decimal(19, 4)) / wd.waiting_tasks_delta
ELSE 0 END
FROM wait_deltas AS wd
WHERE wd.wait_time_ms_delta > 0
@@ -1939,6 +1978,12 @@ ORDER BY ws.collection_time
PARTITION BY ws.wait_type
ORDER BY ws.collection_time
),
+ waiting_tasks_delta =
+ ws.waiting_tasks_count - LAG(ws.waiting_tasks_count, 1, ws.waiting_tasks_count) OVER
+ (
+ PARTITION BY ws.wait_type
+ ORDER BY ws.collection_time
+ ),
interval_seconds =
DATEDIFF(SECOND, LAG(ws.collection_time, 1, ws.collection_time) OVER
(
@@ -1959,6 +2004,10 @@ THEN CAST(wd.wait_time_ms_delta AS decimal(19, 4)) / wd.interval_seconds
signal_wait_time_ms_per_second =
CASE WHEN wd.interval_seconds > 0
THEN CAST(wd.signal_wait_time_ms_delta AS decimal(19, 4)) / wd.interval_seconds
+ ELSE 0 END,
+ avg_ms_per_wait =
+ CASE WHEN wd.waiting_tasks_delta > 0
+ THEN CAST(wd.wait_time_ms_delta AS decimal(19, 4)) / wd.waiting_tasks_delta
ELSE 0 END
FROM wait_deltas AS wd
WHERE wd.wait_time_ms_delta > 0
@@ -1982,7 +2031,8 @@ WHERE wd.wait_time_ms_delta > 0
CollectionTime = reader.GetDateTime(0),
WaitType = reader.IsDBNull(1) ? string.Empty : reader.GetString(1),
WaitTimeMsPerSecond = reader.IsDBNull(2) ? 0m : Convert.ToDecimal(reader.GetValue(2), CultureInfo.InvariantCulture),
- SignalWaitTimeMsPerSecond = reader.IsDBNull(3) ? 0m : Convert.ToDecimal(reader.GetValue(3), CultureInfo.InvariantCulture)
+ SignalWaitTimeMsPerSecond = reader.IsDBNull(3) ? 0m : Convert.ToDecimal(reader.GetValue(3), CultureInfo.InvariantCulture),
+ AvgMsPerWait = reader.IsDBNull(4) ? 0m : Convert.ToDecimal(reader.GetValue(4), CultureInfo.InvariantCulture)
});
}
diff --git a/Lite/Controls/ServerTab.xaml b/Lite/Controls/ServerTab.xaml
index 192b3a8a..c99a7afe 100644
--- a/Lite/Controls/ServerTab.xaml
+++ b/Lite/Controls/ServerTab.xaml
@@ -145,7 +145,20 @@
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/Lite/Controls/ServerTab.xaml.cs b/Lite/Controls/ServerTab.xaml.cs
index 4e046652..0d16810a 100644
--- a/Lite/Controls/ServerTab.xaml.cs
+++ b/Lite/Controls/ServerTab.xaml.cs
@@ -1139,6 +1139,11 @@ private void WaitTypeClearAll_Click(object sender, RoutedEventArgs e)
_ = UpdateWaitStatsChartFromPickerAsync();
}
+ private void WaitStatsMetric_SelectionChanged(object sender, SelectionChangedEventArgs e)
+ {
+ _ = UpdateWaitStatsChartFromPickerAsync();
+ }
+
private void WaitType_CheckChanged(object sender, RoutedEventArgs e)
{
if (_isUpdatingWaitTypeSelection) return;
@@ -1158,6 +1163,9 @@ private async System.Threading.Tasks.Task UpdateWaitStatsChartFromPickerAsync()
if (selected.Count == 0) { WaitStatsChart.Refresh(); return; }
+ bool useAvgPerWait = WaitStatsMetricCombo?.SelectedIndex == 1;
+ if (_waitStatsHover != null) _waitStatsHover.Unit = useAvgPerWait ? "ms/wait" : "ms/sec";
+
var hoursBack = GetHoursBack();
DateTime? fromDate = null;
DateTime? toDate = null;
@@ -1179,14 +1187,16 @@ private async System.Threading.Tasks.Task UpdateWaitStatsChartFromPickerAsync()
if (trend.Count == 0) continue;
var times = trend.Select(t => t.CollectionTime.AddMinutes(UtcOffsetMinutes).ToOADate()).ToArray();
- var waitTime = trend.Select(t => t.WaitTimeMsPerSecond).ToArray();
+ var values = useAvgPerWait
+ ? trend.Select(t => t.AvgMsPerWait).ToArray()
+ : trend.Select(t => t.WaitTimeMsPerSecond).ToArray();
- var plot = WaitStatsChart.Plot.Add.Scatter(times, waitTime);
+ var plot = WaitStatsChart.Plot.Add.Scatter(times, values);
plot.LegendText = selected[i].DisplayName;
plot.Color = ScottPlot.Color.FromHex(SeriesColors[i % SeriesColors.Length]);
_waitStatsHover?.Add(plot, selected[i].DisplayName);
- if (waitTime.Length > 0) globalMax = Math.Max(globalMax, waitTime.Max());
+ if (values.Length > 0) globalMax = Math.Max(globalMax, values.Max());
}
WaitStatsChart.Plot.Axes.DateTimeTicksBottom();
@@ -1203,7 +1213,7 @@ private async System.Threading.Tasks.Task UpdateWaitStatsChartFromPickerAsync()
}
WaitStatsChart.Plot.Axes.SetLimitsX(rangeStart.ToOADate(), rangeEnd.ToOADate());
ReapplyAxisColors(WaitStatsChart);
- WaitStatsChart.Plot.YLabel("Wait Time (ms/sec)");
+ WaitStatsChart.Plot.YLabel(useAvgPerWait ? "Avg Wait Time (ms/wait)" : "Wait Time (ms/sec)");
SetChartYLimitsWithLegendPadding(WaitStatsChart, 0, globalMax > 0 ? globalMax : 100);
ShowChartLegend(WaitStatsChart);
WaitStatsChart.Refresh();
diff --git a/Lite/Helpers/ChartHoverHelper.cs b/Lite/Helpers/ChartHoverHelper.cs
index 5c1e41a0..71a8fbb7 100644
--- a/Lite/Helpers/ChartHoverHelper.cs
+++ b/Lite/Helpers/ChartHoverHelper.cs
@@ -18,7 +18,7 @@ internal sealed class ChartHoverHelper
private readonly List<(ScottPlot.Plottables.Scatter Scatter, string Label)> _scatters = new();
private readonly Popup _popup;
private readonly TextBlock _text;
- private readonly string _unit;
+ private string _unit;
private DateTime _lastUpdate;
public ChartHoverHelper(ScottPlot.WPF.WpfPlot chart, string unit)
@@ -53,6 +53,8 @@ public ChartHoverHelper(ScottPlot.WPF.WpfPlot chart, string unit)
chart.MouseLeave += OnMouseLeave;
}
+ public string Unit { get => _unit; set => _unit = value; }
+
public void Clear() => _scatters.Clear();
public void Add(ScottPlot.Plottables.Scatter scatter, string label) =>
diff --git a/Lite/Services/LocalDataService.WaitStats.cs b/Lite/Services/LocalDataService.WaitStats.cs
index b5678d77..b4c73d06 100644
--- a/Lite/Services/LocalDataService.WaitStats.cs
+++ b/Lite/Services/LocalDataService.WaitStats.cs
@@ -112,6 +112,7 @@ WITH raw AS
collection_time,
delta_wait_time_ms,
delta_signal_wait_time_ms,
+ delta_waiting_tasks,
date_diff('second', LAG(collection_time) OVER (ORDER BY collection_time), collection_time) AS interval_seconds
FROM wait_stats
WHERE server_id = $1
@@ -122,7 +123,8 @@ FROM wait_stats
SELECT
collection_time,
CASE WHEN interval_seconds > 0 THEN CAST(delta_wait_time_ms AS DOUBLE) / interval_seconds ELSE 0 END AS wait_time_ms_per_second,
- CASE WHEN interval_seconds > 0 THEN CAST(delta_signal_wait_time_ms AS DOUBLE) / interval_seconds ELSE 0 END AS signal_wait_time_ms_per_second
+ CASE WHEN interval_seconds > 0 THEN CAST(delta_signal_wait_time_ms AS DOUBLE) / interval_seconds ELSE 0 END AS signal_wait_time_ms_per_second,
+ CASE WHEN delta_waiting_tasks > 0 THEN CAST(delta_wait_time_ms AS DOUBLE) / delta_waiting_tasks ELSE 0 END AS avg_ms_per_wait
FROM raw
ORDER BY collection_time";
@@ -139,7 +141,8 @@ FROM raw
{
CollectionTime = reader.GetDateTime(0),
WaitTimeMsPerSecond = reader.IsDBNull(1) ? 0 : reader.GetDouble(1),
- SignalWaitTimeMsPerSecond = reader.IsDBNull(2) ? 0 : reader.GetDouble(2)
+ SignalWaitTimeMsPerSecond = reader.IsDBNull(2) ? 0 : reader.GetDouble(2),
+ AvgMsPerWait = reader.IsDBNull(3) ? 0 : reader.GetDouble(3)
});
}
@@ -155,6 +158,8 @@ public class WaitStatsRow
public long TotalSignalWaitTimeMs { get; set; }
public long ResourceWaitTimeMs => TotalWaitTimeMs - TotalSignalWaitTimeMs;
public long SampleCount { get; set; }
+ public double AvgWaitMsPerTask => TotalWaitingTasks > 0 ? (double)TotalWaitTimeMs / TotalWaitingTasks : 0;
+ public string AvgWaitMsFormatted => AvgWaitMsPerTask < 0.1 ? "< 0.1 ms" : $"{AvgWaitMsPerTask:F1} ms";
public string TotalWaitTimeFormatted => FormatMs(TotalWaitTimeMs);
public string SignalWaitTimeFormatted => FormatMs(TotalSignalWaitTimeMs);
public string ResourceWaitTimeFormatted => FormatMs(ResourceWaitTimeMs);
@@ -181,4 +186,5 @@ public class WaitStatsTrendPoint
public DateTime CollectionTime { get; set; }
public double WaitTimeMsPerSecond { get; set; }
public double SignalWaitTimeMsPerSecond { get; set; }
+ public double AvgMsPerWait { get; set; }
}