diff --git a/Dashboard/Analysis/SqlServerBaselineProvider.cs b/Dashboard/Analysis/SqlServerBaselineProvider.cs index 1746028c..3d65ee20 100644 --- a/Dashboard/Analysis/SqlServerBaselineProvider.cs +++ b/Dashboard/Analysis/SqlServerBaselineProvider.cs @@ -463,7 +463,7 @@ private static double PoolVariance(List buckets, double grandMea return totalSumSq / (totalSamples - 1); } - private class CachedBaseline + private sealed class CachedBaseline { public DateTime ComputedAt { get; init; } public DateTime RealTime { get; init; } diff --git a/Dashboard/Controls/ConfigChangesContent.xaml b/Dashboard/Controls/ConfigChangesContent.xaml index a8e1f2d4..4f5d5e7d 100644 --- a/Dashboard/Controls/ConfigChangesContent.xaml +++ b/Dashboard/Controls/ConfigChangesContent.xaml @@ -15,7 +15,7 @@ - + diff --git a/Dashboard/Controls/CorrelatedTimelineLanesControl.xaml.cs b/Dashboard/Controls/CorrelatedTimelineLanesControl.xaml.cs index 8fe1e734..7d8f878f 100644 --- a/Dashboard/Controls/CorrelatedTimelineLanesControl.xaml.cs +++ b/Dashboard/Controls/CorrelatedTimelineLanesControl.xaml.cs @@ -320,7 +320,7 @@ private void UpdateBlockingLane(List<(double Time, double Value)> blockingData, } } - BlockingChart.Plot.Axes.DateTimeTicksBottom(); + BlockingChart.Plot.Axes.DateTimeTicksBottomDateChange(); BlockingChart.Plot.Axes.Bottom.TickLabelStyle.IsVisible = false; TabHelpers.ReapplyAxisColors(BlockingChart); @@ -394,7 +394,7 @@ private void UpdateLane(ScottPlot.WPF.WpfPlot chart, string title, _crosshairManager?.SetLaneData(chart, times, values); - chart.Plot.Axes.DateTimeTicksBottom(); + chart.Plot.Axes.DateTimeTicksBottomDateChange(); if (chart != FileIoChart) chart.Plot.Axes.Bottom.TickLabelStyle.IsVisible = false; diff --git a/Dashboard/Controls/CurrentConfigContent.xaml b/Dashboard/Controls/CurrentConfigContent.xaml index ab36dbdf..2cb4fa54 100644 --- a/Dashboard/Controls/CurrentConfigContent.xaml +++ b/Dashboard/Controls/CurrentConfigContent.xaml @@ -15,7 +15,7 @@ - + diff --git a/Dashboard/Controls/FinOpsContent.xaml b/Dashboard/Controls/FinOpsContent.xaml index bfea5d36..e635803b 100644 --- a/Dashboard/Controls/FinOpsContent.xaml +++ b/Dashboard/Controls/FinOpsContent.xaml @@ -44,7 +44,7 @@ SelectionChanged="ServerSelector_SelectionChanged"/> - + diff --git a/Dashboard/Controls/MemoryContent.xaml b/Dashboard/Controls/MemoryContent.xaml index 36db005e..45a0140e 100644 --- a/Dashboard/Controls/MemoryContent.xaml +++ b/Dashboard/Controls/MemoryContent.xaml @@ -32,7 +32,7 @@ - + diff --git a/Dashboard/Controls/MemoryContent.xaml.cs b/Dashboard/Controls/MemoryContent.xaml.cs index 522a6005..bc07e18d 100644 --- a/Dashboard/Controls/MemoryContent.xaml.cs +++ b/Dashboard/Controls/MemoryContent.xaml.cs @@ -353,7 +353,7 @@ private void LoadMemoryStatsOverviewChart(List memoryData, int noDataText.LabelAlignment = ScottPlot.Alignment.MiddleCenter; } - MemoryStatsOverviewChart.Plot.Axes.DateTimeTicksBottom(); + MemoryStatsOverviewChart.Plot.Axes.DateTimeTicksBottomDateChange(); MemoryStatsOverviewChart.Plot.Axes.SetLimitsX(xMin, xMax); MemoryStatsOverviewChart.Plot.YLabel("MB"); // Fixed negative space for legend @@ -605,7 +605,7 @@ private void LoadMemoryGrantSizingChart(List aggregated, int hou MemoryGrantSizingChart.Plot.Legend.FontSize = 12; } - MemoryGrantSizingChart.Plot.Axes.DateTimeTicksBottom(); + MemoryGrantSizingChart.Plot.Axes.DateTimeTicksBottomDateChange(); MemoryGrantSizingChart.Plot.Axes.SetLimitsX(xMin, xMax); MemoryGrantSizingChart.Plot.YLabel("MB"); MemoryGrantSizingChart.Plot.Axes.AutoScaleY(); @@ -675,7 +675,7 @@ private void LoadMemoryGrantActivityChart(List aggregated, int h MemoryGrantActivityChart.Plot.Legend.FontSize = 12; } - MemoryGrantActivityChart.Plot.Axes.DateTimeTicksBottom(); + MemoryGrantActivityChart.Plot.Axes.DateTimeTicksBottomDateChange(); MemoryGrantActivityChart.Plot.Axes.SetLimitsX(xMin, xMax); MemoryGrantActivityChart.Plot.YLabel("Count"); MemoryGrantActivityChart.Plot.Axes.AutoScaleY(); @@ -856,7 +856,7 @@ private async System.Threading.Tasks.Task UpdateMemoryClerksChartFromPickerAsync MemoryClerksTopText.Text = "N/A"; } - MemoryClerksChart.Plot.Axes.DateTimeTicksBottom(); + MemoryClerksChart.Plot.Axes.DateTimeTicksBottomDateChange(); MemoryClerksChart.Plot.Axes.SetLimitsX(xMin, xMax); MemoryClerksChart.Plot.YLabel("MB"); MemoryClerksChart.Plot.Axes.AutoScaleY(); @@ -1001,7 +1001,7 @@ private void LoadPlanCacheChart(IEnumerable data, int hoursB noDataText.LabelAlignment = ScottPlot.Alignment.MiddleCenter; } - PlanCacheChart.Plot.Axes.DateTimeTicksBottom(); + PlanCacheChart.Plot.Axes.DateTimeTicksBottomDateChange(); PlanCacheChart.Plot.Axes.SetLimitsX(xMin, xMax); PlanCacheChart.Plot.YLabel("MB"); // Fixed negative space for legend @@ -1120,7 +1120,7 @@ private void LoadMemoryPressureEventsChart(IEnumerable noDataText.LabelAlignment = ScottPlot.Alignment.MiddleCenter; } - MemoryPressureEventsChart.Plot.Axes.DateTimeTicksBottom(); + MemoryPressureEventsChart.Plot.Axes.DateTimeTicksBottomDateChange(); MemoryPressureEventsChart.Plot.Axes.SetLimitsX(xMin, xMax); MemoryPressureEventsChart.Plot.YLabel("Event Count"); // Fixed negative space for legend diff --git a/Dashboard/Controls/QueryPerformanceContent.xaml b/Dashboard/Controls/QueryPerformanceContent.xaml index 0c1694ec..235f8efd 100644 --- a/Dashboard/Controls/QueryPerformanceContent.xaml +++ b/Dashboard/Controls/QueryPerformanceContent.xaml @@ -46,7 +46,7 @@ - + diff --git a/Dashboard/Controls/QueryPerformanceContent.xaml.cs b/Dashboard/Controls/QueryPerformanceContent.xaml.cs index 8417afa8..0099aba5 100644 --- a/Dashboard/Controls/QueryPerformanceContent.xaml.cs +++ b/Dashboard/Controls/QueryPerformanceContent.xaml.cs @@ -2437,7 +2437,7 @@ private void LoadDurationChart(WpfPlot chart, IEnumerable tre _legendPanels[chart] = chart.Plot.ShowLegend(ScottPlot.Edge.Bottom); chart.Plot.Legend.FontSize = 12; - chart.Plot.Axes.DateTimeTicksBottom(); + chart.Plot.Axes.DateTimeTicksBottomDateChange(); chart.Plot.Axes.SetLimitsX(xMin, xMax); chart.Plot.YLabel("Duration (ms/sec)"); TabHelpers.LockChartVerticalAxis(chart); @@ -2492,7 +2492,7 @@ private void LoadExecChart(IEnumerable execTrends, int hours _legendPanels[QueryPerfTrendsExecChart] = QueryPerfTrendsExecChart.Plot.ShowLegend(ScottPlot.Edge.Bottom); QueryPerfTrendsExecChart.Plot.Legend.FontSize = 12; - QueryPerfTrendsExecChart.Plot.Axes.DateTimeTicksBottom(); + QueryPerfTrendsExecChart.Plot.Axes.DateTimeTicksBottomDateChange(); QueryPerfTrendsExecChart.Plot.Axes.SetLimitsX(xMin, xMax); QueryPerfTrendsExecChart.Plot.YLabel("Executions/sec"); TabHelpers.LockChartVerticalAxis(QueryPerfTrendsExecChart); diff --git a/Dashboard/Controls/ResourceMetricsContent.xaml b/Dashboard/Controls/ResourceMetricsContent.xaml index 9bcdac6e..471c8b3e 100644 --- a/Dashboard/Controls/ResourceMetricsContent.xaml +++ b/Dashboard/Controls/ResourceMetricsContent.xaml @@ -25,7 +25,7 @@ - + @@ -148,7 +148,7 @@ - + diff --git a/Dashboard/Controls/ResourceMetricsContent.xaml.cs b/Dashboard/Controls/ResourceMetricsContent.xaml.cs index 74329671..bcc2d7c3 100644 --- a/Dashboard/Controls/ResourceMetricsContent.xaml.cs +++ b/Dashboard/Controls/ResourceMetricsContent.xaml.cs @@ -404,7 +404,7 @@ private void LoadLatchStatsChart(IEnumerable data, int hoursBack noDataText.LabelAlignment = ScottPlot.Alignment.MiddleCenter; } - LatchStatsChart.Plot.Axes.DateTimeTicksBottom(); + LatchStatsChart.Plot.Axes.DateTimeTicksBottomDateChange(); LatchStatsChart.Plot.Axes.SetLimitsX(xMin, xMax); TabHelpers.SetChartYLimitsWithLegendPadding(LatchStatsChart); LatchStatsChart.Plot.YLabel("Wait Time (ms/sec)"); @@ -495,7 +495,7 @@ private void LoadSpinlockStatsChart(IEnumerable data, int hou noDataText.LabelAlignment = ScottPlot.Alignment.MiddleCenter; } - SpinlockStatsChart.Plot.Axes.DateTimeTicksBottom(); + SpinlockStatsChart.Plot.Axes.DateTimeTicksBottomDateChange(); SpinlockStatsChart.Plot.Axes.SetLimitsX(xMin, xMax); TabHelpers.SetChartYLimitsWithLegendPadding(SpinlockStatsChart); SpinlockStatsChart.Plot.YLabel("Collisions/sec"); @@ -603,7 +603,7 @@ private void LoadCombinedTempDbLatencyChart(List da noDataText.LabelAlignment = ScottPlot.Alignment.MiddleCenter; } - TempDbLatencyChart.Plot.Axes.DateTimeTicksBottom(); + TempDbLatencyChart.Plot.Axes.DateTimeTicksBottomDateChange(); TempDbLatencyChart.Plot.Axes.SetLimitsX(xMin, xMax); TabHelpers.SetChartYLimitsWithLegendPadding(TempDbLatencyChart); TempDbLatencyChart.Plot.YLabel("Latency (ms)"); @@ -708,7 +708,7 @@ private void LoadTempdbStatsChart(IEnumerable data, int hoursBa noDataText.LabelAlignment = ScottPlot.Alignment.MiddleCenter; } - TempdbStatsChart.Plot.Axes.DateTimeTicksBottom(); + TempdbStatsChart.Plot.Axes.DateTimeTicksBottomDateChange(); TempdbStatsChart.Plot.Axes.SetLimitsX(xMin, xMax); TempdbStatsChart.Plot.Axes.AutoScaleY(); TempdbStatsChart.Plot.YLabel("MB"); @@ -879,7 +879,7 @@ private void LoadSessionStatsChart(IEnumerable data, int hours noDataText.LabelAlignment = ScottPlot.Alignment.MiddleCenter; } - SessionStatsChart.Plot.Axes.DateTimeTicksBottom(); + SessionStatsChart.Plot.Axes.DateTimeTicksBottomDateChange(); SessionStatsChart.Plot.Axes.SetLimitsX(xMin, xMax); TabHelpers.SetChartYLimitsWithLegendPadding(SessionStatsChart); SessionStatsChart.Plot.YLabel("Session Count"); @@ -1014,7 +1014,7 @@ private void LoadFileIoChart(ScottPlot.WPF.WpfPlot chart, List? data, int hoursBac noDataText.LabelAlignment = ScottPlot.Alignment.MiddleCenter; } - PerfmonCountersChart.Plot.Axes.DateTimeTicksBottom(); + PerfmonCountersChart.Plot.Axes.DateTimeTicksBottomDateChange(); PerfmonCountersChart.Plot.Axes.SetLimitsX(xMin, xMax); TabHelpers.SetChartYLimitsWithLegendPadding(PerfmonCountersChart); PerfmonCountersChart.Plot.YLabel("Value/sec"); @@ -1816,7 +1816,7 @@ private void LoadWaitStatsDetailChart(List? data, int hoursB noDataText.LabelAlignment = ScottPlot.Alignment.MiddleCenter; } - WaitStatsDetailChart.Plot.Axes.DateTimeTicksBottom(); + WaitStatsDetailChart.Plot.Axes.DateTimeTicksBottomDateChange(); WaitStatsDetailChart.Plot.Axes.SetLimitsX(xMin, xMax); TabHelpers.SetChartYLimitsWithLegendPadding(WaitStatsDetailChart); WaitStatsDetailChart.Plot.YLabel(useAvgPerWait ? "Avg Wait Time (ms/wait)" : "Wait Time (ms/sec)"); diff --git a/Dashboard/Controls/SystemEventsContent.xaml b/Dashboard/Controls/SystemEventsContent.xaml index 3434838c..6bbfb75a 100644 --- a/Dashboard/Controls/SystemEventsContent.xaml +++ b/Dashboard/Controls/SystemEventsContent.xaml @@ -31,7 +31,7 @@ - + diff --git a/Dashboard/Controls/SystemEventsContent.xaml.cs b/Dashboard/Controls/SystemEventsContent.xaml.cs index aa3e1c20..d24a2f56 100644 --- a/Dashboard/Controls/SystemEventsContent.xaml.cs +++ b/Dashboard/Controls/SystemEventsContent.xaml.cs @@ -528,7 +528,7 @@ private void LoadCorruptionEventsCharts(List data, noDataText.LabelFontColor = ScottPlot.Colors.Gray; noDataText.LabelAlignment = ScottPlot.Alignment.MiddleCenter; } - BadPagesChart.Plot.Axes.DateTimeTicksBottom(); + BadPagesChart.Plot.Axes.DateTimeTicksBottomDateChange(); BadPagesChart.Plot.Axes.SetLimitsX(xMin, xMax); BadPagesChart.Plot.YLabel("Count"); TabHelpers.LockChartVerticalAxis(BadPagesChart); @@ -557,7 +557,7 @@ private void LoadCorruptionEventsCharts(List data, noDataText.LabelFontColor = ScottPlot.Colors.Gray; noDataText.LabelAlignment = ScottPlot.Alignment.MiddleCenter; } - DumpRequestsChart.Plot.Axes.DateTimeTicksBottom(); + DumpRequestsChart.Plot.Axes.DateTimeTicksBottomDateChange(); DumpRequestsChart.Plot.Axes.SetLimitsX(xMin, xMax); DumpRequestsChart.Plot.YLabel("Count"); TabHelpers.LockChartVerticalAxis(DumpRequestsChart); @@ -586,7 +586,7 @@ private void LoadCorruptionEventsCharts(List data, noDataText.LabelFontColor = ScottPlot.Colors.Gray; noDataText.LabelAlignment = ScottPlot.Alignment.MiddleCenter; } - AccessViolationsChart.Plot.Axes.DateTimeTicksBottom(); + AccessViolationsChart.Plot.Axes.DateTimeTicksBottomDateChange(); AccessViolationsChart.Plot.Axes.SetLimitsX(xMin, xMax); AccessViolationsChart.Plot.YLabel("Count"); TabHelpers.LockChartVerticalAxis(AccessViolationsChart); @@ -615,7 +615,7 @@ private void LoadCorruptionEventsCharts(List data, noDataText.LabelFontColor = ScottPlot.Colors.Gray; noDataText.LabelAlignment = ScottPlot.Alignment.MiddleCenter; } - WriteAccessViolationsChart.Plot.Axes.DateTimeTicksBottom(); + WriteAccessViolationsChart.Plot.Axes.DateTimeTicksBottomDateChange(); WriteAccessViolationsChart.Plot.Axes.SetLimitsX(xMin, xMax); WriteAccessViolationsChart.Plot.YLabel("Count"); TabHelpers.LockChartVerticalAxis(WriteAccessViolationsChart); @@ -656,7 +656,7 @@ private void LoadContentionEventsCharts(List data, noDataText.LabelFontColor = ScottPlot.Colors.Gray; noDataText.LabelAlignment = ScottPlot.Alignment.MiddleCenter; } - NonYieldingTasksChart.Plot.Axes.DateTimeTicksBottom(); + NonYieldingTasksChart.Plot.Axes.DateTimeTicksBottomDateChange(); NonYieldingTasksChart.Plot.Axes.SetLimitsX(xMin, xMax); NonYieldingTasksChart.Plot.YLabel("Count"); TabHelpers.LockChartVerticalAxis(NonYieldingTasksChart); @@ -685,7 +685,7 @@ private void LoadContentionEventsCharts(List data, noDataText.LabelFontColor = ScottPlot.Colors.Gray; noDataText.LabelAlignment = ScottPlot.Alignment.MiddleCenter; } - LatchWarningsChart.Plot.Axes.DateTimeTicksBottom(); + LatchWarningsChart.Plot.Axes.DateTimeTicksBottomDateChange(); LatchWarningsChart.Plot.Axes.SetLimitsX(xMin, xMax); LatchWarningsChart.Plot.YLabel("Count"); TabHelpers.LockChartVerticalAxis(LatchWarningsChart); @@ -748,7 +748,7 @@ private void LoadContentionEventsCharts(List data, noDataText.LabelFontColor = ScottPlot.Colors.Gray; noDataText.LabelAlignment = ScottPlot.Alignment.MiddleCenter; } - SickSpinlocksChart.Plot.Axes.DateTimeTicksBottom(); + SickSpinlocksChart.Plot.Axes.DateTimeTicksBottomDateChange(); SickSpinlocksChart.Plot.Axes.SetLimitsX(xMin, xMax); SickSpinlocksChart.Plot.YLabel("Backoffs"); TabHelpers.LockChartVerticalAxis(SickSpinlocksChart); @@ -798,7 +798,7 @@ private void LoadContentionEventsCharts(List data, noDataText.LabelFontColor = ScottPlot.Colors.Gray; noDataText.LabelAlignment = ScottPlot.Alignment.MiddleCenter; } - CpuComparisonChart.Plot.Axes.DateTimeTicksBottom(); + CpuComparisonChart.Plot.Axes.DateTimeTicksBottomDateChange(); CpuComparisonChart.Plot.Axes.SetLimitsX(xMin, xMax); CpuComparisonChart.Plot.Axes.SetLimitsY(0, 100); // Fixed Y-axis for CPU percentage CpuComparisonChart.Plot.YLabel("CPU %"); @@ -899,7 +899,7 @@ private void LoadSevereErrorsChart(IEnumerable data noDataText.LabelAlignment = ScottPlot.Alignment.MiddleCenter; } - SevereErrorsChart.Plot.Axes.DateTimeTicksBottom(); + SevereErrorsChart.Plot.Axes.DateTimeTicksBottomDateChange(); SevereErrorsChart.Plot.Axes.SetLimitsX(xMin, xMax); SevereErrorsChart.Plot.YLabel("Event Count"); TabHelpers.LockChartVerticalAxis(SevereErrorsChart); @@ -1058,7 +1058,7 @@ private void LoadIOIssuesChart(IEnumerable data, int ho noDataText.LabelAlignment = ScottPlot.Alignment.MiddleCenter; } - IOIssuesChart.Plot.Axes.DateTimeTicksBottom(); + IOIssuesChart.Plot.Axes.DateTimeTicksBottomDateChange(); IOIssuesChart.Plot.Axes.SetLimitsX(xMin, xMax); IOIssuesChart.Plot.YLabel("Count"); TabHelpers.LockChartVerticalAxis(IOIssuesChart); @@ -1152,7 +1152,7 @@ private void LoadLongestPendingIOChart(IEnumerable data noDataText.LabelAlignment = ScottPlot.Alignment.MiddleCenter; } - LongestPendingIOChart.Plot.Axes.DateTimeTicksBottom(); + LongestPendingIOChart.Plot.Axes.DateTimeTicksBottomDateChange(); LongestPendingIOChart.Plot.Axes.SetLimitsX(xMin, xMax); LongestPendingIOChart.Plot.YLabel("Duration (ms)"); TabHelpers.LockChartVerticalAxis(LongestPendingIOChart); @@ -1251,7 +1251,7 @@ long ParseNonYield(string? value) noDataText.LabelAlignment = ScottPlot.Alignment.MiddleCenter; } - SchedulerIssuesChart.Plot.Axes.DateTimeTicksBottom(); + SchedulerIssuesChart.Plot.Axes.DateTimeTicksBottomDateChange(); SchedulerIssuesChart.Plot.Axes.SetLimitsX(xMin, xMax); SchedulerIssuesChart.Plot.YLabel("Total Non-Yield Time (ms)"); TabHelpers.LockChartVerticalAxis(SchedulerIssuesChart); @@ -1401,7 +1401,7 @@ private void LoadMemoryConditionsChart(IEnumerable data, int h noDataText.LabelAlignment = ScottPlot.Alignment.MiddleCenter; } - CPUTasksChart.Plot.Axes.DateTimeTicksBottom(); + CPUTasksChart.Plot.Axes.DateTimeTicksBottomDateChange(); CPUTasksChart.Plot.Axes.SetLimitsX(xMin, xMax); CPUTasksChart.Plot.YLabel("Workers"); TabHelpers.LockChartVerticalAxis(CPUTasksChart); @@ -1780,7 +1780,7 @@ private void LoadMemoryBrokerChart(IEnumerable dat /* Finalize both charts */ foreach (var chart in new[] { MemoryBrokerChart, MemoryBrokerRatioChart }) { - chart.Plot.Axes.DateTimeTicksBottom(); + chart.Plot.Axes.DateTimeTicksBottomDateChange(); chart.Plot.Axes.SetLimitsX(xMin, xMax); TabHelpers.LockChartVerticalAxis(chart); chart.Refresh(); @@ -1933,7 +1933,7 @@ private void LoadMemoryNodeOOMChart(IEnumerable d noDataText.LabelAlignment = ScottPlot.Alignment.MiddleCenter; } - MemoryNodeOOMChart.Plot.Axes.DateTimeTicksBottom(); + MemoryNodeOOMChart.Plot.Axes.DateTimeTicksBottomDateChange(); MemoryNodeOOMChart.Plot.Axes.SetLimitsX(xMin, xMax); MemoryNodeOOMChart.Plot.YLabel("Event Count"); TabHelpers.LockChartVerticalAxis(MemoryNodeOOMChart); @@ -1983,7 +1983,7 @@ private void LoadMemoryNodeOOMUtilChart(IEnumerableCulture's short-date pattern with the year component removed (e.g. "M/d" en-US, "dd/MM" en-GB, "dd.MM" de-DE). + private static readonly string MonthDayPattern = BuildMonthDayPattern(); + + private static string BuildMonthDayPattern() + { + var p = CultureInfo.CurrentCulture.DateTimeFormat.ShortDatePattern; + p = Regex.Replace(p, @"y+", ""); + p = Regex.Replace(p, @"^[\s/.\-]+|[\s/.\-]+$", ""); + p = Regex.Replace(p, @"([/.\-\s])\1+", "$1"); + return string.IsNullOrWhiteSpace(p) ? "M/d" : p; + } + + /// + /// Like DateTimeTicksBottom(), but prints the date line on only the first tick + /// and on ticks where the date component changes. All other ticks show time-only. + /// Date and time formats follow the current culture. + /// + public static void DateTimeTicksBottomDateChange(this ScottPlot.AxisManager axes) + { + axes.DateTimeTicksBottom(); + if (axes.Bottom.TickGenerator is ScottPlot.TickGenerators.DateTimeAutomatic gen) + { + DateTime? lastDate = null; + var culture = CultureInfo.CurrentCulture; + gen.LabelFormatter = dt => + { + var time = dt.ToString("t", culture); + if (lastDate is null || dt.Date != lastDate.Value) + { + lastDate = dt.Date; + return $"{dt.ToString(MonthDayPattern, culture)}\n{time}"; + } + return time; + }; + } + } +} diff --git a/Dashboard/Helpers/CorrelatedCrosshairManager.cs b/Dashboard/Helpers/CorrelatedCrosshairManager.cs index c49b0a7b..80e2ec7d 100644 --- a/Dashboard/Helpers/CorrelatedCrosshairManager.cs +++ b/Dashboard/Helpers/CorrelatedCrosshairManager.cs @@ -350,7 +350,7 @@ public void Dispose() _lanes.Clear(); } - private class DataSeries + private sealed class DataSeries { public string Name { get; set; } = ""; public string? Unit { get; set; } @@ -359,7 +359,7 @@ private class DataSeries public bool IsEventBased { get; set; } } - private class LaneInfo + private sealed class LaneInfo { public ScottPlot.WPF.WpfPlot Chart { get; set; } = null!; public string Label { get; set; } = ""; diff --git a/Dashboard/Helpers/TabHelpers.cs b/Dashboard/Helpers/TabHelpers.cs index 70f6533d..61319272 100644 --- a/Dashboard/Helpers/TabHelpers.cs +++ b/Dashboard/Helpers/TabHelpers.cs @@ -200,6 +200,8 @@ public static void ApplyThemeToChart(WpfPlot chart) chart.Plot.Axes.Left.TickLabelStyle.ForeColor = textColor; chart.Plot.Axes.Bottom.Label.ForeColor = textColor; chart.Plot.Axes.Left.Label.ForeColor = textColor; + chart.Plot.Axes.Bottom.TickLabelStyle.FontSize = 13; + chart.Plot.Axes.Left.TickLabelStyle.FontSize = 13; // Set the WPF control Background to match so no white flash appears before ScottPlot's render loop fires chart.Background = new SolidColorBrush(Color.FromRgb(figureBackground.R, figureBackground.G, figureBackground.B)); @@ -234,6 +236,8 @@ public static void ReapplyAxisColors(WpfPlot chart) chart.Plot.Axes.Left.TickLabelStyle.ForeColor = textColor; chart.Plot.Axes.Bottom.Label.ForeColor = textColor; chart.Plot.Axes.Left.Label.ForeColor = textColor; + chart.Plot.Axes.Bottom.TickLabelStyle.FontSize = 13; + chart.Plot.Axes.Left.TickLabelStyle.FontSize = 13; } /// diff --git a/Dashboard/ProcedureHistoryWindow.xaml.cs b/Dashboard/ProcedureHistoryWindow.xaml.cs index 0875f951..057e07d6 100644 --- a/Dashboard/ProcedureHistoryWindow.xaml.cs +++ b/Dashboard/ProcedureHistoryWindow.xaml.cs @@ -210,7 +210,7 @@ private void UpdateChart() scatter.MarkerSize = 4; } - HistoryChart.Plot.Axes.DateTimeTicksBottom(); + HistoryChart.Plot.Axes.DateTimeTicksBottomDateChange(); Helpers.TabHelpers.ReapplyAxisColors(HistoryChart); HistoryChart.Plot.YLabel(metricLabel); HistoryChart.Plot.XLabel("Collection Time"); diff --git a/Dashboard/QueryExecutionHistoryWindow.xaml.cs b/Dashboard/QueryExecutionHistoryWindow.xaml.cs index 33f70367..dcdc277d 100644 --- a/Dashboard/QueryExecutionHistoryWindow.xaml.cs +++ b/Dashboard/QueryExecutionHistoryWindow.xaml.cs @@ -226,7 +226,7 @@ private void UpdateChart() colorIndex++; } - HistoryChart.Plot.Axes.DateTimeTicksBottom(); + HistoryChart.Plot.Axes.DateTimeTicksBottomDateChange(); Helpers.TabHelpers.ReapplyAxisColors(HistoryChart); HistoryChart.Plot.YLabel(metricLabel); HistoryChart.Plot.XLabel("Collection Time"); diff --git a/Dashboard/QueryStatsHistoryWindow.xaml.cs b/Dashboard/QueryStatsHistoryWindow.xaml.cs index 755c823d..7f428289 100644 --- a/Dashboard/QueryStatsHistoryWindow.xaml.cs +++ b/Dashboard/QueryStatsHistoryWindow.xaml.cs @@ -202,7 +202,7 @@ private void UpdateChart() scatter.MarkerSize = 4; } - HistoryChart.Plot.Axes.DateTimeTicksBottom(); + HistoryChart.Plot.Axes.DateTimeTicksBottomDateChange(); Helpers.TabHelpers.ReapplyAxisColors(HistoryChart); HistoryChart.Plot.YLabel(metricLabel); HistoryChart.Plot.XLabel("Collection Time"); diff --git a/Dashboard/ServerTab.xaml b/Dashboard/ServerTab.xaml index e6927176..5c3f601a 100644 --- a/Dashboard/ServerTab.xaml +++ b/Dashboard/ServerTab.xaml @@ -163,7 +163,7 @@ - + @@ -255,7 +255,7 @@ - + - + diff --git a/Dashboard/ServerTab.xaml.cs b/Dashboard/ServerTab.xaml.cs index 6c9f7b1f..e793be13 100644 --- a/Dashboard/ServerTab.xaml.cs +++ b/Dashboard/ServerTab.xaml.cs @@ -2253,7 +2253,7 @@ private void LoadBlockingStatsCharts(List data, int h noDataText.LabelFontColor = ScottPlot.Colors.Gray; noDataText.LabelAlignment = ScottPlot.Alignment.MiddleCenter; } - BlockingStatsBlockingEventsChart.Plot.Axes.DateTimeTicksBottom(); + BlockingStatsBlockingEventsChart.Plot.Axes.DateTimeTicksBottomDateChange(); BlockingStatsBlockingEventsChart.Plot.Axes.SetLimitsX(xMin, xMax); BlockingStatsBlockingEventsChart.Plot.YLabel("Count"); LockChartVerticalAxis(BlockingStatsBlockingEventsChart); @@ -2282,7 +2282,7 @@ private void LoadBlockingStatsCharts(List data, int h noDataText.LabelFontColor = ScottPlot.Colors.Gray; noDataText.LabelAlignment = ScottPlot.Alignment.MiddleCenter; } - BlockingStatsDurationChart.Plot.Axes.DateTimeTicksBottom(); + BlockingStatsDurationChart.Plot.Axes.DateTimeTicksBottomDateChange(); BlockingStatsDurationChart.Plot.Axes.SetLimitsX(xMin, xMax); BlockingStatsDurationChart.Plot.YLabel("Duration (ms)"); LockChartVerticalAxis(BlockingStatsDurationChart); @@ -2311,7 +2311,7 @@ private void LoadBlockingStatsCharts(List data, int h noDataText.LabelFontColor = ScottPlot.Colors.Gray; noDataText.LabelAlignment = ScottPlot.Alignment.MiddleCenter; } - BlockingStatsDeadlocksChart.Plot.Axes.DateTimeTicksBottom(); + BlockingStatsDeadlocksChart.Plot.Axes.DateTimeTicksBottomDateChange(); BlockingStatsDeadlocksChart.Plot.Axes.SetLimitsX(xMin, xMax); BlockingStatsDeadlocksChart.Plot.YLabel("Count"); LockChartVerticalAxis(BlockingStatsDeadlocksChart); @@ -2340,7 +2340,7 @@ private void LoadBlockingStatsCharts(List data, int h noDataText.LabelFontColor = ScottPlot.Colors.Gray; noDataText.LabelAlignment = ScottPlot.Alignment.MiddleCenter; } - BlockingStatsDeadlockWaitTimeChart.Plot.Axes.DateTimeTicksBottom(); + BlockingStatsDeadlockWaitTimeChart.Plot.Axes.DateTimeTicksBottomDateChange(); BlockingStatsDeadlockWaitTimeChart.Plot.Axes.SetLimitsX(xMin, xMax); BlockingStatsDeadlockWaitTimeChart.Plot.YLabel("Duration (ms)"); LockChartVerticalAxis(BlockingStatsDeadlockWaitTimeChart); @@ -2386,7 +2386,7 @@ private void UpdateCollectorDurationChart(List data) colorIndex++; } - CollectorDurationChart.Plot.Axes.DateTimeTicksBottom(); + CollectorDurationChart.Plot.Axes.DateTimeTicksBottomDateChange(); TabHelpers.ReapplyAxisColors(CollectorDurationChart); CollectorDurationChart.Plot.YLabel("Duration (ms)"); CollectorDurationChart.Plot.Axes.AutoScale(); @@ -2449,7 +2449,7 @@ private void LoadLockWaitStatsChart(List data, int hoursBack, noDataText.LabelAlignment = ScottPlot.Alignment.MiddleCenter; } - LockWaitStatsChart.Plot.Axes.DateTimeTicksBottom(); + LockWaitStatsChart.Plot.Axes.DateTimeTicksBottomDateChange(); LockWaitStatsChart.Plot.Axes.SetLimitsX(xMin, xMax); LockWaitStatsChart.Plot.YLabel("Wait Time (ms/sec)"); _legendPanels[LockWaitStatsChart] = LockWaitStatsChart.Plot.ShowLegend(ScottPlot.Edge.Bottom); @@ -2506,7 +2506,7 @@ private void LoadCurrentWaitsDurationChart(List data, int noDataText.LabelAlignment = ScottPlot.Alignment.MiddleCenter; } - CurrentWaitsDurationChart.Plot.Axes.DateTimeTicksBottom(); + CurrentWaitsDurationChart.Plot.Axes.DateTimeTicksBottomDateChange(); CurrentWaitsDurationChart.Plot.Axes.SetLimitsX(xMin, xMax); CurrentWaitsDurationChart.Plot.YLabel("Total Wait Duration (ms)"); _legendPanels[CurrentWaitsDurationChart] = CurrentWaitsDurationChart.Plot.ShowLegend(ScottPlot.Edge.Bottom); @@ -2563,7 +2563,7 @@ private void LoadCurrentWaitsBlockedChart(List data, in noDataText.LabelAlignment = ScottPlot.Alignment.MiddleCenter; } - CurrentWaitsBlockedChart.Plot.Axes.DateTimeTicksBottom(); + CurrentWaitsBlockedChart.Plot.Axes.DateTimeTicksBottomDateChange(); CurrentWaitsBlockedChart.Plot.Axes.SetLimitsX(xMin, xMax); CurrentWaitsBlockedChart.Plot.YLabel("Blocked Sessions"); _legendPanels[CurrentWaitsBlockedChart] = CurrentWaitsBlockedChart.Plot.ShowLegend(ScottPlot.Edge.Bottom); @@ -2903,7 +2903,7 @@ private void LoadResourceOverviewCpuChart(IEnumerable cpuData, int noDataText.LabelAlignment = ScottPlot.Alignment.MiddleCenter; } - ResourceOverviewCpuChart.Plot.Axes.DateTimeTicksBottom(); + ResourceOverviewCpuChart.Plot.Axes.DateTimeTicksBottomDateChange(); ResourceOverviewCpuChart.Plot.Axes.SetLimitsX(xMin, xMax); ResourceOverviewCpuChart.Plot.Axes.SetLimitsY(0, 100); ResourceOverviewCpuChart.Plot.YLabel("CPU %"); @@ -2966,7 +2966,7 @@ private void LoadResourceOverviewMemoryChart(IEnumerable memory noDataText.LabelAlignment = ScottPlot.Alignment.MiddleCenter; } - ResourceOverviewMemoryChart.Plot.Axes.DateTimeTicksBottom(); + ResourceOverviewMemoryChart.Plot.Axes.DateTimeTicksBottomDateChange(); ResourceOverviewMemoryChart.Plot.Axes.SetLimitsX(xMin, xMax); ResourceOverviewMemoryChart.Plot.YLabel("MB"); LockChartVerticalAxis(ResourceOverviewMemoryChart); @@ -3043,7 +3043,7 @@ private void LoadResourceOverviewIoChart(IEnumerable ioData, in noDataText.LabelAlignment = ScottPlot.Alignment.MiddleCenter; } - ResourceOverviewIoChart.Plot.Axes.DateTimeTicksBottom(); + ResourceOverviewIoChart.Plot.Axes.DateTimeTicksBottomDateChange(); ResourceOverviewIoChart.Plot.Axes.SetLimitsX(xMin, xMax); ResourceOverviewIoChart.Plot.Axes.AutoScaleY(); ResourceOverviewIoChart.Plot.YLabel("Latency (ms)"); @@ -3115,7 +3115,7 @@ private void LoadResourceOverviewWaitChart(IEnumerable waitD noDataText.LabelAlignment = ScottPlot.Alignment.MiddleCenter; } - ResourceOverviewWaitChart.Plot.Axes.DateTimeTicksBottom(); + ResourceOverviewWaitChart.Plot.Axes.DateTimeTicksBottomDateChange(); ResourceOverviewWaitChart.Plot.Axes.SetLimitsX(xMin, xMax); ResourceOverviewWaitChart.Plot.Axes.AutoScaleY(); ResourceOverviewWaitChart.Plot.YLabel("Wait Time (ms/sec)"); diff --git a/Dashboard/Services/BenefitScorer.cs b/Dashboard/Services/BenefitScorer.cs index 1acf26cf..94606aba 100644 --- a/Dashboard/Services/BenefitScorer.cs +++ b/Dashboard/Services/BenefitScorer.cs @@ -616,20 +616,20 @@ internal static string ClassifyWaitType(string waitType) var wt = waitType.ToUpperInvariant(); return wt switch { - _ when wt.StartsWith("PAGEIOLATCH") => "I/O", - _ when wt.Contains("IO_COMPLETION") => "I/O", - _ when wt.StartsWith("WRITELOG") => "I/O", + _ when wt.StartsWith("PAGEIOLATCH", StringComparison.Ordinal) => "I/O", + _ when wt.Contains("IO_COMPLETION", StringComparison.Ordinal) => "I/O", + _ when wt.StartsWith("WRITELOG", StringComparison.Ordinal) => "I/O", _ when wt == "SOS_SCHEDULER_YIELD" => "CPU", - _ when wt.StartsWith("CXPACKET") || wt.StartsWith("CXCONSUMER") => "Parallelism", - _ when wt.StartsWith("CXSYNC") => "Parallelism", - _ when wt.StartsWith("HT") => "Hash", + _ when wt.StartsWith("CXPACKET", StringComparison.Ordinal) || wt.StartsWith("CXCONSUMER", StringComparison.Ordinal) => "Parallelism", + _ when wt.StartsWith("CXSYNC", StringComparison.Ordinal) => "Parallelism", + _ when wt.StartsWith("HT", StringComparison.Ordinal) => "Hash", _ when wt == "BPSORT" => "Sort", _ when wt == "BMPBUILD" => "Hash", - _ when wt.StartsWith("PAGELATCH") => "Latch", - _ when wt.StartsWith("LATCH_") => "Latch", - _ when wt.StartsWith("LCK_") => "Lock", + _ when wt.StartsWith("PAGELATCH", StringComparison.Ordinal) => "Latch", + _ when wt.StartsWith("LATCH_", StringComparison.Ordinal) => "Latch", + _ when wt.StartsWith("LCK_", StringComparison.Ordinal) => "Lock", _ when wt == "ASYNC_NETWORK_IO" => "Network", - _ when wt.Contains("MEMORY_ALLOCATION") => "Memory", + _ when wt.Contains("MEMORY_ALLOCATION", StringComparison.Ordinal) => "Memory", _ when wt == "SOS_PHYS_PAGE_CACHE" => "Memory", _ => "Other" }; diff --git a/Dashboard/Services/PlanAnalyzer.cs b/Dashboard/Services/PlanAnalyzer.cs index 254246d5..1f153ee5 100644 --- a/Dashboard/Services/PlanAnalyzer.cs +++ b/Dashboard/Services/PlanAnalyzer.cs @@ -253,7 +253,7 @@ private static void AnalyzeStatement(PlanStatement stmt) if (unsnifffedParams.Count > 0) { - var hasRecompile = stmt.StatementText.Contains("RECOMPILE", StringComparison.OrdinalIgnoreCase); + var hasRecompile = (stmt.StatementText ?? "").Contains("RECOMPILE", StringComparison.OrdinalIgnoreCase); if (!hasRecompile) { var names = string.Join(", ", unsnifffedParams.Select(p => p.Name)); @@ -1099,7 +1099,7 @@ _ when nonSargableReason.StartsWith("Function call", StringComparison.OrdinalIgn // Rule 28: Row Count Spool — NOT IN with nullable column // Pattern: Row Count Spool with high rewinds, child scan has IS NULL predicate, // and statement text contains NOT IN - if (node.PhysicalOp.Contains("Row Count Spool")) + if ((node.PhysicalOp ?? "").Contains("Row Count Spool", StringComparison.Ordinal)) { var rewinds = node.HasActualStats ? (double)node.ActualRewinds : node.EstimateRewinds; if (rewinds > 10000 && HasNotInPattern(node, stmt)) @@ -1118,7 +1118,7 @@ _ when nonSargableReason.StartsWith("Function call", StringComparison.OrdinalIgn if (!(node.HasActualStats && node.ActualExecutions == 0)) foreach (var w in node.Warnings.ToList()) { - if (w.WarningType == "Implicit Conversion" && w.Message.StartsWith("Seek Plan")) + if (w.WarningType == "Implicit Conversion" && w.Message.StartsWith("Seek Plan", StringComparison.Ordinal)) { w.Severity = PlanWarningSeverity.Critical; w.Message = $"Implicit conversion prevented an index seek, forcing a scan instead. Fix the data type mismatch: ensure the parameter or variable type matches the column type exactly. {w.Message}"; @@ -1828,7 +1828,7 @@ private static bool AllocatesResources(PlanNode node) || op.EndsWith("Spool", StringComparison.OrdinalIgnoreCase); } - private record ScanImpact(double CostPct, double ElapsedPct, string? Summary); + private sealed record ScanImpact(double CostPct, double ElapsedPct, string? Summary); /// /// Builds impact details for a scan node: what % of plan time/cost it represents, diff --git a/Dashboard/Themes/CoolBreezeTheme.xaml b/Dashboard/Themes/CoolBreezeTheme.xaml index 933d65e4..36437cbc 100644 --- a/Dashboard/Themes/CoolBreezeTheme.xaml +++ b/Dashboard/Themes/CoolBreezeTheme.xaml @@ -644,6 +644,40 @@ + + + diff --git a/Dashboard/Themes/DarkTheme.xaml b/Dashboard/Themes/DarkTheme.xaml index 5e0b8324..20abf9bb 100644 --- a/Dashboard/Themes/DarkTheme.xaml +++ b/Dashboard/Themes/DarkTheme.xaml @@ -643,6 +643,40 @@ + + + diff --git a/Dashboard/Themes/LightTheme.xaml b/Dashboard/Themes/LightTheme.xaml index 903203f3..4e22f4f3 100644 --- a/Dashboard/Themes/LightTheme.xaml +++ b/Dashboard/Themes/LightTheme.xaml @@ -644,6 +644,40 @@ + + + diff --git a/Dashboard/TracePatternHistoryWindow.xaml.cs b/Dashboard/TracePatternHistoryWindow.xaml.cs index fd1de4e8..80439328 100644 --- a/Dashboard/TracePatternHistoryWindow.xaml.cs +++ b/Dashboard/TracePatternHistoryWindow.xaml.cs @@ -183,7 +183,7 @@ private void UpdateChart() scatter.MarkerSize = 4; } - HistoryChart.Plot.Axes.DateTimeTicksBottom(); + HistoryChart.Plot.Axes.DateTimeTicksBottomDateChange(); Helpers.TabHelpers.ReapplyAxisColors(HistoryChart); HistoryChart.Plot.YLabel(metricLabel); HistoryChart.Plot.XLabel("End Time"); diff --git a/Lite/Services/BenefitScorer.cs b/Lite/Services/BenefitScorer.cs index a922c9b7..7d03ae91 100644 --- a/Lite/Services/BenefitScorer.cs +++ b/Lite/Services/BenefitScorer.cs @@ -616,20 +616,20 @@ internal static string ClassifyWaitType(string waitType) var wt = waitType.ToUpperInvariant(); return wt switch { - _ when wt.StartsWith("PAGEIOLATCH") => "I/O", - _ when wt.Contains("IO_COMPLETION") => "I/O", - _ when wt.StartsWith("WRITELOG") => "I/O", + _ when wt.StartsWith("PAGEIOLATCH", StringComparison.Ordinal) => "I/O", + _ when wt.Contains("IO_COMPLETION", StringComparison.Ordinal) => "I/O", + _ when wt.StartsWith("WRITELOG", StringComparison.Ordinal) => "I/O", _ when wt == "SOS_SCHEDULER_YIELD" => "CPU", - _ when wt.StartsWith("CXPACKET") || wt.StartsWith("CXCONSUMER") => "Parallelism", - _ when wt.StartsWith("CXSYNC") => "Parallelism", - _ when wt.StartsWith("HT") => "Hash", + _ when wt.StartsWith("CXPACKET", StringComparison.Ordinal) || wt.StartsWith("CXCONSUMER", StringComparison.Ordinal) => "Parallelism", + _ when wt.StartsWith("CXSYNC", StringComparison.Ordinal) => "Parallelism", + _ when wt.StartsWith("HT", StringComparison.Ordinal) => "Hash", _ when wt == "BPSORT" => "Sort", _ when wt == "BMPBUILD" => "Hash", - _ when wt.StartsWith("PAGELATCH") => "Latch", - _ when wt.StartsWith("LATCH_") => "Latch", - _ when wt.StartsWith("LCK_") => "Lock", + _ when wt.StartsWith("PAGELATCH", StringComparison.Ordinal) => "Latch", + _ when wt.StartsWith("LATCH_", StringComparison.Ordinal) => "Latch", + _ when wt.StartsWith("LCK_", StringComparison.Ordinal) => "Lock", _ when wt == "ASYNC_NETWORK_IO" => "Network", - _ when wt.Contains("MEMORY_ALLOCATION") => "Memory", + _ when wt.Contains("MEMORY_ALLOCATION", StringComparison.Ordinal) => "Memory", _ when wt == "SOS_PHYS_PAGE_CACHE" => "Memory", _ => "Other" }; diff --git a/Lite/Services/PlanAnalyzer.cs b/Lite/Services/PlanAnalyzer.cs index 29d0a3d6..5f7284c1 100644 --- a/Lite/Services/PlanAnalyzer.cs +++ b/Lite/Services/PlanAnalyzer.cs @@ -1,6 +1,3 @@ -using System; -using System.Collections.Generic; -using System.Linq; using System.Text.RegularExpressions; using PerformanceMonitorLite.Models; @@ -253,7 +250,7 @@ private static void AnalyzeStatement(PlanStatement stmt) if (unsnifffedParams.Count > 0) { - var hasRecompile = stmt.StatementText.Contains("RECOMPILE", StringComparison.OrdinalIgnoreCase); + var hasRecompile = (stmt.StatementText ?? "").Contains("RECOMPILE", StringComparison.OrdinalIgnoreCase); if (!hasRecompile) { var names = string.Join(", ", unsnifffedParams.Select(p => p.Name)); @@ -1099,7 +1096,7 @@ _ when nonSargableReason.StartsWith("Function call", StringComparison.OrdinalIgn // Rule 28: Row Count Spool — NOT IN with nullable column // Pattern: Row Count Spool with high rewinds, child scan has IS NULL predicate, // and statement text contains NOT IN - if (node.PhysicalOp.Contains("Row Count Spool")) + if ((node.PhysicalOp ?? "").Contains("Row Count Spool", StringComparison.Ordinal)) { var rewinds = node.HasActualStats ? (double)node.ActualRewinds : node.EstimateRewinds; if (rewinds > 10000 && HasNotInPattern(node, stmt)) @@ -1118,7 +1115,7 @@ _ when nonSargableReason.StartsWith("Function call", StringComparison.OrdinalIgn if (!(node.HasActualStats && node.ActualExecutions == 0)) foreach (var w in node.Warnings.ToList()) { - if (w.WarningType == "Implicit Conversion" && w.Message.StartsWith("Seek Plan")) + if (w.WarningType == "Implicit Conversion" && w.Message.StartsWith("Seek Plan", StringComparison.Ordinal)) { w.Severity = PlanWarningSeverity.Critical; w.Message = $"Implicit conversion prevented an index seek, forcing a scan instead. Fix the data type mismatch: ensure the parameter or variable type matches the column type exactly. {w.Message}"; diff --git a/Lite/Services/RemoteCollectorService.QueryStore.cs b/Lite/Services/RemoteCollectorService.QueryStore.cs index 612653c3..9265c32e 100644 --- a/Lite/Services/RemoteCollectorService.QueryStore.cs +++ b/Lite/Services/RemoteCollectorService.QueryStore.cs @@ -222,7 +222,7 @@ ORDER BY /* Fall back to 13 (SQL 2016) if version detection fails */ } - bool isNew = productVersion > 13 || serverStatus.SqlEngineEdition == 5 || serverStatus.SqlEngineEdition == 8; + bool isNew = productVersion > 13 || serverStatus?.SqlEngineEdition == 5 || serverStatus?.SqlEngineEdition == 8; bool hasPlanType = productVersion >= 16; /* Build version-conditional column fragments for the Query Store query.