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; } }