From 9995376fbef6e6c7c050edd1a021a86e3da3de8f Mon Sep 17 00:00:00 2001 From: Erik Darling <2136037+erikdarlingdata@users.noreply.github.com> Date: Wed, 25 Feb 2026 10:27:04 -0500 Subject: [PATCH] Issue #281 Gap 1: Standardize memory grant stats collection and add charts Both apps now collect from dm_exec_query_resource_semaphores (previously Lite used dm_exec_query_memory_grants). Added two new per-pool charts to the Memory Grants sub-tab in both apps: Sizing (available/granted/used MB) and Activity (grantees/waiters/timeouts/forced grants). Dashboard: simplified collector by removing inline OUTER APPLY delta, added memory_grant_stats to the proper delta framework (calculate_deltas), removed unused warning columns, added upgrade script for 1.3.0-to-2.0.0. Lite: new DuckDB schema (v14 migration), DeltaCalculator seeding for timeout/forced counters, new Memory Grants sub-tab with two ScottPlot charts. Also includes waiting_tasks collector simplification. Co-Authored-By: Claude Opus 4.6 --- Dashboard/Controls/MemoryContent.xaml | 62 +++-- Dashboard/Controls/MemoryContent.xaml.cs | 260 ++++++++++++------ Dashboard/Mcp/McpMemoryTools.cs | 9 +- Dashboard/Models/MemoryGrantStatsItem.cs | 20 +- Dashboard/Services/DatabaseService.Memory.cs | 40 +-- Lite/Controls/ServerTab.xaml | 12 + Lite/Controls/ServerTab.xaml.cs | 98 ++++++- Lite/Database/DuckDbInitializer.cs | 13 +- Lite/Database/Schema.cs | 23 +- Lite/Mcp/McpMemoryTools.cs | 38 ++- Lite/Services/DeltaCalculator.cs | 32 +++ .../Services/LocalDataService.MemoryGrants.cs | 108 ++++---- .../RemoteCollectorService.MemoryGrants.cs | 83 +++--- install/02_create_tables.sql | 20 +- install/05_delta_framework.sql | 84 ++++++ install/06_ensure_collection_table.sql | 20 +- install/15_collect_memory_grant_stats.sql | 140 ++-------- install/37_collect_waiting_tasks.sql | 60 +--- .../01_memory_grant_stats_schema.sql | 172 ++++++++++++ upgrades/1.3.0-to-2.0.0/upgrade.txt | 1 + upgrades/README.md | 2 + 21 files changed, 828 insertions(+), 469 deletions(-) create mode 100644 upgrades/1.3.0-to-2.0.0/01_memory_grant_stats_schema.sql create mode 100644 upgrades/1.3.0-to-2.0.0/upgrade.txt diff --git a/Dashboard/Controls/MemoryContent.xaml b/Dashboard/Controls/MemoryContent.xaml index d157b62a..ce1a6f95 100644 --- a/Dashboard/Controls/MemoryContent.xaml +++ b/Dashboard/Controls/MemoryContent.xaml @@ -95,34 +95,40 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Dashboard/Controls/MemoryContent.xaml.cs b/Dashboard/Controls/MemoryContent.xaml.cs index aa8bf7f1..f3ca9334 100644 --- a/Dashboard/Controls/MemoryContent.xaml.cs +++ b/Dashboard/Controls/MemoryContent.xaml.cs @@ -64,7 +64,8 @@ public partial class MemoryContent : UserControl // Chart hover tooltips private Helpers.ChartHoverHelper? _memoryStatsOverviewHover; - private Helpers.ChartHoverHelper? _memoryGrantsHover; + private Helpers.ChartHoverHelper? _memoryGrantSizingHover; + private Helpers.ChartHoverHelper? _memoryGrantActivityHover; private Helpers.ChartHoverHelper? _memoryClerksHover; private Helpers.ChartHoverHelper? _planCacheHover; private Helpers.ChartHoverHelper? _memoryPressureEventsHover; @@ -78,7 +79,8 @@ public MemoryContent() Loaded += OnLoaded; _memoryStatsOverviewHover = new Helpers.ChartHoverHelper(MemoryStatsOverviewChart, "MB"); - _memoryGrantsHover = new Helpers.ChartHoverHelper(MemoryGrantsChart, "MB"); + _memoryGrantSizingHover = new Helpers.ChartHoverHelper(MemoryGrantSizingChart, "MB"); + _memoryGrantActivityHover = new Helpers.ChartHoverHelper(MemoryGrantActivityChart, "count"); _memoryClerksHover = new Helpers.ChartHoverHelper(MemoryClerksChart, "MB"); _planCacheHover = new Helpers.ChartHoverHelper(PlanCacheChart, "MB"); _memoryPressureEventsHover = new Helpers.ChartHoverHelper(MemoryPressureEventsChart, "events"); @@ -94,8 +96,9 @@ private void SetupChartContextMenus() // Memory Stats Overview chart TabHelpers.SetupChartContextMenu(MemoryStatsOverviewChart, "Memory_Stats_Overview", "collect.memory_stats"); - // Memory Grants chart - TabHelpers.SetupChartContextMenu(MemoryGrantsChart, "Memory_Grants", "collect.memory_grant_stats"); + // Memory Grant charts + TabHelpers.SetupChartContextMenu(MemoryGrantSizingChart, "Memory_Grant_Sizing", "collect.memory_grant_stats"); + TabHelpers.SetupChartContextMenu(MemoryGrantActivityChart, "Memory_Grant_Activity", "collect.memory_grant_stats"); // Memory Clerks chart TabHelpers.SetupChartContextMenu(MemoryClerksChart, "Memory_Clerks", "collect.memory_clerks_stats"); @@ -391,112 +394,213 @@ private void UpdateMemoryStatsSummaryPanel(List dataList) #region Memory Grants + private sealed class PoolGrantPoint + { + public DateTime CollectionTime { get; set; } + public int PoolId { get; set; } + public double AvailableMemoryMb { get; set; } + public double GrantedMemoryMb { get; set; } + public double UsedMemoryMb { get; set; } + public double GranteeCount { get; set; } + public double WaiterCount { get; set; } + public double TimeoutErrorCountDelta { get; set; } + public double ForcedGrantCountDelta { get; set; } + } + private async System.Threading.Tasks.Task RefreshMemoryGrantsAsync() { if (_databaseService == null) return; try { - // Only show loading overlay on initial load (no existing chart data) - if (!MemoryGrantsChart.Plot.GetPlottables().Any()) + if (!MemoryGrantSizingChart.Plot.GetPlottables().Any()) { - MemoryGrantsLoading.IsLoading = true; - MemoryGrantsNoDataMessage.Visibility = Visibility.Collapsed; + MemoryGrantSizingLoading.IsLoading = true; + MemoryGrantSizingNoData.Visibility = Visibility.Collapsed; + MemoryGrantActivityNoData.Visibility = Visibility.Collapsed; } var data = await _databaseService.GetMemoryGrantStatsAsync(_memoryGrantsHoursBack, _memoryGrantsFromDate, _memoryGrantsToDate); var dataList = data.ToList(); - MemoryGrantsNoDataMessage.Visibility = dataList.Count == 0 ? Visibility.Visible : Visibility.Collapsed; - LoadMemoryGrantsChart(dataList, _memoryGrantsHoursBack, _memoryGrantsFromDate, _memoryGrantsToDate); + + // Filter to rows with active grants for charting + var filtered = dataList.Where(d => (d.GrantedMemoryMb ?? 0) > 0).ToList(); + + bool hasData = filtered.Count > 0; + MemoryGrantSizingNoData.Visibility = hasData ? Visibility.Collapsed : Visibility.Visible; + MemoryGrantActivityNoData.Visibility = hasData ? Visibility.Collapsed : Visibility.Visible; + + // Aggregate across resource_semaphore_id within each pool + var aggregated = filtered + .GroupBy(d => new { d.CollectionTime, d.PoolId }) + .Select(g => new PoolGrantPoint + { + CollectionTime = g.Key.CollectionTime, + PoolId = g.Key.PoolId, + AvailableMemoryMb = g.Sum(x => (double)(x.AvailableMemoryMb ?? 0)), + GrantedMemoryMb = g.Sum(x => (double)(x.GrantedMemoryMb ?? 0)), + UsedMemoryMb = g.Sum(x => (double)(x.UsedMemoryMb ?? 0)), + GranteeCount = g.Sum(x => (double)(x.GranteeCount ?? 0)), + WaiterCount = g.Sum(x => (double)(x.WaiterCount ?? 0)), + TimeoutErrorCountDelta = g.Sum(x => (double)(x.TimeoutErrorCountDelta ?? 0)), + ForcedGrantCountDelta = g.Sum(x => (double)(x.ForcedGrantCountDelta ?? 0)) + }) + .OrderBy(d => d.CollectionTime) + .ToList(); + + LoadMemoryGrantSizingChart(aggregated, _memoryGrantsHoursBack, _memoryGrantsFromDate, _memoryGrantsToDate); + LoadMemoryGrantActivityChart(aggregated, _memoryGrantsHoursBack, _memoryGrantsFromDate, _memoryGrantsToDate); } catch (Exception ex) { Logger.Error($"Error loading memory grants: {ex.Message}"); + MemoryGrantSizingNoData.Visibility = Visibility.Visible; + MemoryGrantActivityNoData.Visibility = Visibility.Visible; } finally { - MemoryGrantsLoading.IsLoading = false; + MemoryGrantSizingLoading.IsLoading = false; + MemoryGrantActivityLoading.IsLoading = false; } } - private void LoadMemoryGrantsChart(IEnumerable data, int hoursBack, DateTime? fromDate, DateTime? toDate) + private void LoadMemoryGrantSizingChart(List aggregated, 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(MemoryGrantsChart, out var existingPanel) && existingPanel != null) - { - MemoryGrantsChart.Plot.Axes.Remove(existingPanel); - _legendPanels[MemoryGrantsChart] = null; - } - MemoryGrantsChart.Plot.Clear(); - _memoryGrantsHover?.Clear(); - TabHelpers.ApplyDarkModeToChart(MemoryGrantsChart); - - var dataList = data?.OrderBy(d => d.CollectionTime).ToList() ?? new List(); - - // Aggregate by collection_time to avoid doubling from multiple resource_semaphore_ids - // SUM granted (actual usage), MAX target (global limit) - var aggregated = dataList - .GroupBy(d => d.CollectionTime) - .Select(g => new { - CollectionTime = g.Key, - GrantedMemoryMb = g.Sum(x => x.GrantedMemoryMb ?? 0m), - TargetMemoryMb = g.Max(x => x.TargetMemoryMb ?? 0m) - }) - .OrderBy(d => d.CollectionTime) - .ToList(); - - // Granted MB series with gap filling (already aggregated, no further summing needed) - var (grantedXs, grantedYs) = TabHelpers.FillTimeSeriesGaps( - aggregated.Select(d => d.CollectionTime), - aggregated.Select(d => (double)d.GrantedMemoryMb)); - - // Target MB series with gap filling (already aggregated) - var (targetXs, targetYs) = TabHelpers.FillTimeSeriesGaps( - aggregated.Select(d => d.CollectionTime), - aggregated.Select(d => (double)d.TargetMemoryMb)); - - if (grantedXs.Length > 0) - { - var grantedScatter = MemoryGrantsChart.Plot.Add.Scatter(grantedXs, grantedYs); - grantedScatter.LineWidth = 2; - grantedScatter.MarkerSize = 5; - grantedScatter.Color = TabHelpers.ChartColors[0]; - grantedScatter.LegendText = "Granted MB"; - _memoryGrantsHover?.Add(grantedScatter, "Granted MB"); - - var targetScatter = MemoryGrantsChart.Plot.Add.Scatter(targetXs, targetYs); - targetScatter.LineWidth = 2; - targetScatter.MarkerSize = 5; - targetScatter.Color = TabHelpers.ChartColors[2]; - targetScatter.LegendText = "Target MB"; - _memoryGrantsHover?.Add(targetScatter, "Target MB"); - - _legendPanels[MemoryGrantsChart] = MemoryGrantsChart.Plot.ShowLegend(ScottPlot.Edge.Bottom); - MemoryGrantsChart.Plot.Legend.FontSize = 12; + if (_legendPanels.TryGetValue(MemoryGrantSizingChart, out var existingPanel) && existingPanel != null) + { + MemoryGrantSizingChart.Plot.Axes.Remove(existingPanel); + _legendPanels[MemoryGrantSizingChart] = null; } - else + MemoryGrantSizingChart.Plot.Clear(); + _memoryGrantSizingHover?.Clear(); + TabHelpers.ApplyDarkModeToChart(MemoryGrantSizingChart); + + var poolIds = aggregated.Select(d => d.PoolId).Distinct().OrderBy(id => id).ToList(); + int colorIndex = 0; + var colors = TabHelpers.ChartColors; + bool hasData = false; + + foreach (var poolId in poolIds) { - double xCenter = xMin + (xMax - xMin) / 2; - var noDataText = MemoryGrantsChart.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; + var poolData = aggregated.Where(d => d.PoolId == poolId).OrderBy(d => d.CollectionTime).ToList(); + if (poolData.Count == 0) continue; + hasData = true; + + var metrics = new (string Name, Func Selector)[] { + ("Available MB", d => d.AvailableMemoryMb), + ("Granted MB", d => d.GrantedMemoryMb), + ("Used MB", d => d.UsedMemoryMb) + }; + + foreach (var (metricName, selector) in metrics) + { + var (xs, ys) = TabHelpers.FillTimeSeriesGaps( + poolData.Select(d => d.CollectionTime), + poolData.Select(selector)); + + if (xs.Length > 0) + { + var scatter = MemoryGrantSizingChart.Plot.Add.Scatter(xs, ys); + scatter.LineWidth = 2; + scatter.MarkerSize = 5; + scatter.Color = colors[colorIndex % colors.Length]; + var label = $"Pool {poolId}: {metricName}"; + scatter.LegendText = label; + _memoryGrantSizingHover?.Add(scatter, label); + colorIndex++; + } + } } - MemoryGrantsChart.Plot.Axes.DateTimeTicksBottom(); - MemoryGrantsChart.Plot.Axes.SetLimitsX(xMin, xMax); - MemoryGrantsChart.Plot.YLabel("MB"); - // Fixed negative space for legend - MemoryGrantsChart.Plot.Axes.AutoScaleY(); - var grantsLimits = MemoryGrantsChart.Plot.Axes.GetLimits(); - MemoryGrantsChart.Plot.Axes.SetLimitsY(0, grantsLimits.Top * 1.05); + if (hasData) + { + _legendPanels[MemoryGrantSizingChart] = MemoryGrantSizingChart.Plot.ShowLegend(ScottPlot.Edge.Bottom); + MemoryGrantSizingChart.Plot.Legend.FontSize = 12; + } + + MemoryGrantSizingChart.Plot.Axes.DateTimeTicksBottom(); + MemoryGrantSizingChart.Plot.Axes.SetLimitsX(xMin, xMax); + MemoryGrantSizingChart.Plot.YLabel("MB"); + MemoryGrantSizingChart.Plot.Axes.AutoScaleY(); + var limits = MemoryGrantSizingChart.Plot.Axes.GetLimits(); + MemoryGrantSizingChart.Plot.Axes.SetLimitsY(0, limits.Top * 1.05); + TabHelpers.LockChartVerticalAxis(MemoryGrantSizingChart); + MemoryGrantSizingChart.Refresh(); + } + + private void LoadMemoryGrantActivityChart(List aggregated, 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(MemoryGrantActivityChart, out var existingPanel) && existingPanel != null) + { + MemoryGrantActivityChart.Plot.Axes.Remove(existingPanel); + _legendPanels[MemoryGrantActivityChart] = null; + } + MemoryGrantActivityChart.Plot.Clear(); + _memoryGrantActivityHover?.Clear(); + TabHelpers.ApplyDarkModeToChart(MemoryGrantActivityChart); + + var poolIds = aggregated.Select(d => d.PoolId).Distinct().OrderBy(id => id).ToList(); + int colorIndex = 0; + var colors = TabHelpers.ChartColors; + bool hasData = false; + + foreach (var poolId in poolIds) + { + var poolData = aggregated.Where(d => d.PoolId == poolId).OrderBy(d => d.CollectionTime).ToList(); + if (poolData.Count == 0) continue; + hasData = true; + + var metrics = new (string Name, Func Selector)[] { + ("Grantees", d => d.GranteeCount), + ("Waiters", d => d.WaiterCount), + ("Timeouts", d => d.TimeoutErrorCountDelta), + ("Forced Grants", d => d.ForcedGrantCountDelta) + }; + + foreach (var (metricName, selector) in metrics) + { + var (xs, ys) = TabHelpers.FillTimeSeriesGaps( + poolData.Select(d => d.CollectionTime), + poolData.Select(selector)); + + if (xs.Length > 0) + { + var scatter = MemoryGrantActivityChart.Plot.Add.Scatter(xs, ys); + scatter.LineWidth = 2; + scatter.MarkerSize = 5; + scatter.Color = colors[colorIndex % colors.Length]; + var label = $"Pool {poolId}: {metricName}"; + scatter.LegendText = label; + _memoryGrantActivityHover?.Add(scatter, label); + colorIndex++; + } + } + } + + if (hasData) + { + _legendPanels[MemoryGrantActivityChart] = MemoryGrantActivityChart.Plot.ShowLegend(ScottPlot.Edge.Bottom); + MemoryGrantActivityChart.Plot.Legend.FontSize = 12; + } - TabHelpers.LockChartVerticalAxis(MemoryGrantsChart); - MemoryGrantsChart.Refresh(); + MemoryGrantActivityChart.Plot.Axes.DateTimeTicksBottom(); + MemoryGrantActivityChart.Plot.Axes.SetLimitsX(xMin, xMax); + MemoryGrantActivityChart.Plot.YLabel("Count"); + MemoryGrantActivityChart.Plot.Axes.AutoScaleY(); + var limits = MemoryGrantActivityChart.Plot.Axes.GetLimits(); + MemoryGrantActivityChart.Plot.Axes.SetLimitsY(0, limits.Top * 1.05); + TabHelpers.LockChartVerticalAxis(MemoryGrantActivityChart); + MemoryGrantActivityChart.Refresh(); } #endregion diff --git a/Dashboard/Mcp/McpMemoryTools.cs b/Dashboard/Mcp/McpMemoryTools.cs index 2f64333e..c04cb0e9 100644 --- a/Dashboard/Mcp/McpMemoryTools.cs +++ b/Dashboard/Mcp/McpMemoryTools.cs @@ -199,12 +199,9 @@ public static async Task GetMemoryGrants( waiter_count = r.WaiterCount, timeout_error_count = r.TimeoutErrorCount, forced_grant_count = r.ForcedGrantCount, - granted_pct = r.GrantedPercentage, - used_pct = r.UsedPercentage, - available_memory_pressure = r.AvailableMemoryPressureWarning, - waiter_count_warning = r.WaiterCountWarning, - timeout_error_warning = r.TimeoutErrorWarning, - forced_grant_warning = r.ForcedGrantWarning + timeout_error_count_delta = r.TimeoutErrorCountDelta, + forced_grant_count_delta = r.ForcedGrantCountDelta, + sample_interval_seconds = r.SampleIntervalSeconds }); return JsonSerializer.Serialize(new diff --git a/Dashboard/Models/MemoryGrantStatsItem.cs b/Dashboard/Models/MemoryGrantStatsItem.cs index 177f2b4a..eaf9ccb2 100644 --- a/Dashboard/Models/MemoryGrantStatsItem.cs +++ b/Dashboard/Models/MemoryGrantStatsItem.cs @@ -6,6 +6,7 @@ public class MemoryGrantStatsItem { public long CollectionId { get; set; } public DateTime CollectionTime { get; set; } + public DateTime ServerStartTime { get; set; } public short ResourceSemaphoreId { get; set; } public int PoolId { get; set; } @@ -17,24 +18,15 @@ public class MemoryGrantStatsItem public decimal? GrantedMemoryMb { get; set; } public decimal? UsedMemoryMb { get; set; } - // Counts + // Point-in-time counts public int? GranteeCount { get; set; } public int? WaiterCount { get; set; } public long? TimeoutErrorCount { get; set; } public long? ForcedGrantCount { get; set; } - // Pressure warnings - public bool? AvailableMemoryPressureWarning { get; set; } - public bool? WaiterCountWarning { get; set; } - public bool? TimeoutErrorWarning { get; set; } - public bool? ForcedGrantWarning { get; set; } - - // Computed helpers - public decimal? GrantedPercentage => TargetMemoryMb > 0 - ? GrantedMemoryMb * 100.0m / TargetMemoryMb - : null; - public decimal? UsedPercentage => GrantedMemoryMb > 0 - ? UsedMemoryMb * 100.0m / GrantedMemoryMb - : null; + // Delta columns (calculated by framework) + public long? TimeoutErrorCountDelta { get; set; } + public long? ForcedGrantCountDelta { get; set; } + public int? SampleIntervalSeconds { get; set; } } } diff --git a/Dashboard/Services/DatabaseService.Memory.cs b/Dashboard/Services/DatabaseService.Memory.cs index 28a4d00e..b0ac827f 100644 --- a/Dashboard/Services/DatabaseService.Memory.cs +++ b/Dashboard/Services/DatabaseService.Memory.cs @@ -171,6 +171,7 @@ public async Task> GetMemoryGrantStatsAsync(int hours SELECT mgs.collection_id, mgs.collection_time, + mgs.server_start_time, mgs.resource_semaphore_id, mgs.pool_id, mgs.target_memory_mb, @@ -183,10 +184,9 @@ public async Task> GetMemoryGrantStatsAsync(int hours mgs.waiter_count, mgs.timeout_error_count, mgs.forced_grant_count, - mgs.available_memory_pressure_warning, - mgs.waiter_count_warning, - mgs.timeout_error_warning, - mgs.forced_grant_warning + mgs.timeout_error_count_delta, + mgs.forced_grant_count_delta, + mgs.sample_interval_seconds FROM collect.memory_grant_stats AS mgs {dateFilter} ORDER BY @@ -212,22 +212,22 @@ ORDER BY { CollectionId = reader.GetInt64(0), CollectionTime = reader.GetDateTime(1), - ResourceSemaphoreId = reader.GetInt16(2), - PoolId = reader.GetInt32(3), - TargetMemoryMb = reader.IsDBNull(4) ? null : reader.GetDecimal(4), - MaxTargetMemoryMb = reader.IsDBNull(5) ? null : reader.GetDecimal(5), - TotalMemoryMb = reader.IsDBNull(6) ? null : reader.GetDecimal(6), - AvailableMemoryMb = reader.IsDBNull(7) ? null : reader.GetDecimal(7), - GrantedMemoryMb = reader.IsDBNull(8) ? null : reader.GetDecimal(8), - UsedMemoryMb = reader.IsDBNull(9) ? null : reader.GetDecimal(9), - GranteeCount = reader.IsDBNull(10) ? null : reader.GetInt32(10), - WaiterCount = reader.IsDBNull(11) ? null : reader.GetInt32(11), - TimeoutErrorCount = reader.IsDBNull(12) ? null : reader.GetInt64(12), - ForcedGrantCount = reader.IsDBNull(13) ? null : reader.GetInt64(13), - AvailableMemoryPressureWarning = reader.IsDBNull(14) ? null : reader.GetBoolean(14), - WaiterCountWarning = reader.IsDBNull(15) ? null : reader.GetBoolean(15), - TimeoutErrorWarning = reader.IsDBNull(16) ? null : reader.GetBoolean(16), - ForcedGrantWarning = reader.IsDBNull(17) ? null : reader.GetBoolean(17) + ServerStartTime = reader.IsDBNull(2) ? DateTime.MinValue : reader.GetDateTime(2), + ResourceSemaphoreId = reader.GetInt16(3), + PoolId = reader.GetInt32(4), + TargetMemoryMb = reader.IsDBNull(5) ? null : reader.GetDecimal(5), + MaxTargetMemoryMb = reader.IsDBNull(6) ? null : reader.GetDecimal(6), + TotalMemoryMb = reader.IsDBNull(7) ? null : reader.GetDecimal(7), + AvailableMemoryMb = reader.IsDBNull(8) ? null : reader.GetDecimal(8), + GrantedMemoryMb = reader.IsDBNull(9) ? null : reader.GetDecimal(9), + UsedMemoryMb = reader.IsDBNull(10) ? null : reader.GetDecimal(10), + GranteeCount = reader.IsDBNull(11) ? null : reader.GetInt32(11), + WaiterCount = reader.IsDBNull(12) ? null : reader.GetInt32(12), + TimeoutErrorCount = reader.IsDBNull(13) ? null : reader.GetInt64(13), + ForcedGrantCount = reader.IsDBNull(14) ? null : reader.GetInt64(14), + TimeoutErrorCountDelta = reader.IsDBNull(15) ? null : reader.GetInt64(15), + ForcedGrantCountDelta = reader.IsDBNull(16) ? null : reader.GetInt64(16), + SampleIntervalSeconds = reader.IsDBNull(17) ? null : reader.GetInt32(17) }); } diff --git a/Lite/Controls/ServerTab.xaml b/Lite/Controls/ServerTab.xaml index 4d162946..d77dbb59 100644 --- a/Lite/Controls/ServerTab.xaml +++ b/Lite/Controls/ServerTab.xaml @@ -698,6 +698,18 @@ + + + + + + + + + + + + diff --git a/Lite/Controls/ServerTab.xaml.cs b/Lite/Controls/ServerTab.xaml.cs index 92e19c8a..4700c556 100644 --- a/Lite/Controls/ServerTab.xaml.cs +++ b/Lite/Controls/ServerTab.xaml.cs @@ -58,6 +58,8 @@ public partial class ServerTab : UserControl private Helpers.ChartHoverHelper? _blockingTrendHover; private Helpers.ChartHoverHelper? _deadlockTrendHover; private Helpers.ChartHoverHelper? _memoryClerksHover; + private Helpers.ChartHoverHelper? _memoryGrantSizingHover; + private Helpers.ChartHoverHelper? _memoryGrantActivityHover; /* Memory clerks picker */ private List _memoryClerkItems = new(); @@ -170,6 +172,8 @@ public ServerTab(ServerConnection server, DuckDbInitializer duckDb, CredentialSe _blockingTrendHover = new Helpers.ChartHoverHelper(BlockingTrendChart, "incidents"); _deadlockTrendHover = new Helpers.ChartHoverHelper(DeadlockTrendChart, "deadlocks"); _memoryClerksHover = new Helpers.ChartHoverHelper(MemoryClerksChart, "MB"); + _memoryGrantSizingHover = new Helpers.ChartHoverHelper(MemoryGrantSizingChart, "MB"); + _memoryGrantActivityHover = new Helpers.ChartHoverHelper(MemoryGrantActivityChart, ""); /* Initial load is triggered by MainWindow.ConnectToServer calling RefreshData() after collectors finish - no Loaded handler needed */ @@ -480,6 +484,7 @@ private async System.Threading.Tasks.Task RefreshAllDataAsync() var perfmonCountersTask = _dataService.GetDistinctPerfmonCountersAsync(_serverId, hoursBack, fromDate, toDate); var queryStoreTask = _dataService.GetQueryStoreTopQueriesAsync(_serverId, hoursBack, 50, fromDate, toDate); var memoryGrantTrendTask = _dataService.GetMemoryGrantTrendAsync(_serverId, hoursBack, fromDate, toDate); + var memoryGrantChartTask = _dataService.GetMemoryGrantChartDataAsync(_serverId, hoursBack, fromDate, toDate); var serverConfigTask = SafeQueryAsync(() => _dataService.GetLatestServerConfigAsync(_serverId)); var databaseConfigTask = SafeQueryAsync(() => _dataService.GetLatestDatabaseConfigAsync(_serverId)); var databaseScopedConfigTask = SafeQueryAsync(() => _dataService.GetLatestDatabaseScopedConfigAsync(_serverId)); @@ -493,7 +498,7 @@ await System.Threading.Tasks.Task.WhenAll( snapshotsTask, cpuTask, memoryTask, memoryTrendTask, queryStatsTask, procStatsTask, fileIoTask, fileIoTrendTask, tempDbTask, tempDbFileIoTask, deadlockTask, blockedProcessTask, waitTypesTask, memoryClerkTypesTask, perfmonCountersTask, - queryStoreTask, memoryGrantTrendTask, + queryStoreTask, memoryGrantTrendTask, memoryGrantChartTask, serverConfigTask, databaseConfigTask, databaseScopedConfigTask, traceFlagsTask, runningJobsTask, collectionHealthTask, collectionLogTask, dailySummaryTask); @@ -562,6 +567,7 @@ await System.Threading.Tasks.Task.WhenAll( UpdateProcDurationTrendChart(procDurationTrendTask.Result); UpdateQueryStoreDurationTrendChart(queryStoreDurationTrendTask.Result); UpdateExecutionCountTrendChart(executionCountTrendTask.Result); + UpdateMemoryGrantCharts(memoryGrantChartTask.Result); /* Populate pickers (preserve selections) */ PopulateWaitTypePicker(waitTypesTask.Result); @@ -728,6 +734,96 @@ private void UpdateMemoryChart(List data, List data) + { + ClearChart(MemoryGrantSizingChart); + ClearChart(MemoryGrantActivityChart); + _memoryGrantSizingHover?.Clear(); + _memoryGrantActivityHover?.Clear(); + ApplyDarkTheme(MemoryGrantSizingChart); + ApplyDarkTheme(MemoryGrantActivityChart); + + if (data.Count == 0) + { + MemoryGrantSizingChart.Refresh(); + MemoryGrantActivityChart.Refresh(); + return; + } + + var poolIds = data.Select(d => d.PoolId).Distinct().OrderBy(p => p).ToList(); + int colorIndex = 0; + + /* Chart 1: Memory Grant Sizing — Available, Granted, Used MB per pool */ + double sizingMax = 0; + var sizingMetrics = new (string Name, Func Selector)[] + { + ("Available MB", d => d.AvailableMemoryMb), + ("Granted MB", d => d.GrantedMemoryMb), + ("Used MB", d => d.UsedMemoryMb) + }; + + foreach (var poolId in poolIds) + { + var poolData = data.Where(d => d.PoolId == poolId).OrderBy(d => d.CollectionTime).ToList(); + var times = poolData.Select(d => d.CollectionTime.AddMinutes(UtcOffsetMinutes).ToOADate()).ToArray(); + + foreach (var metric in sizingMetrics) + { + var values = poolData.Select(d => metric.Selector(d)).ToArray(); + var plot = MemoryGrantSizingChart.Plot.Add.Scatter(times, values); + var label = $"Pool {poolId}: {metric.Name}"; + plot.LegendText = label; + plot.Color = ScottPlot.Color.FromHex(SeriesColors[colorIndex % SeriesColors.Length]); + _memoryGrantSizingHover?.Add(plot, label); + if (values.Length > 0) sizingMax = Math.Max(sizingMax, values.Max()); + colorIndex++; + } + } + + MemoryGrantSizingChart.Plot.Axes.DateTimeTicksBottom(); + ReapplyAxisColors(MemoryGrantSizingChart); + MemoryGrantSizingChart.Plot.YLabel("Memory (MB)"); + SetChartYLimitsWithLegendPadding(MemoryGrantSizingChart, 0, sizingMax > 0 ? sizingMax : 100); + ShowChartLegend(MemoryGrantSizingChart); + MemoryGrantSizingChart.Refresh(); + + /* Chart 2: Memory Grant Activity — Grantees, Waiters, Timeouts, Forced per pool */ + double activityMax = 0; + colorIndex = 0; + var activityMetrics = new (string Name, Func Selector)[] + { + ("Grantees", d => d.GranteeCount), + ("Waiters", d => d.WaiterCount), + ("Timeouts", d => d.TimeoutErrorCountDelta), + ("Forced Grants", d => d.ForcedGrantCountDelta) + }; + + foreach (var poolId in poolIds) + { + var poolData = data.Where(d => d.PoolId == poolId).OrderBy(d => d.CollectionTime).ToList(); + var times = poolData.Select(d => d.CollectionTime.AddMinutes(UtcOffsetMinutes).ToOADate()).ToArray(); + + foreach (var metric in activityMetrics) + { + var values = poolData.Select(d => metric.Selector(d)).ToArray(); + var plot = MemoryGrantActivityChart.Plot.Add.Scatter(times, values); + var label = $"Pool {poolId}: {metric.Name}"; + plot.LegendText = label; + plot.Color = ScottPlot.Color.FromHex(SeriesColors[colorIndex % SeriesColors.Length]); + _memoryGrantActivityHover?.Add(plot, label); + if (values.Length > 0) activityMax = Math.Max(activityMax, values.Max()); + colorIndex++; + } + } + + MemoryGrantActivityChart.Plot.Axes.DateTimeTicksBottom(); + ReapplyAxisColors(MemoryGrantActivityChart); + MemoryGrantActivityChart.Plot.YLabel("Count"); + SetChartYLimitsWithLegendPadding(MemoryGrantActivityChart, 0, activityMax > 0 ? activityMax : 10); + ShowChartLegend(MemoryGrantActivityChart); + MemoryGrantActivityChart.Refresh(); + } + private void UpdateTempDbChart(List data) { ClearChart(TempDbChart); diff --git a/Lite/Database/DuckDbInitializer.cs b/Lite/Database/DuckDbInitializer.cs index 09c2ec27..ac064de5 100644 --- a/Lite/Database/DuckDbInitializer.cs +++ b/Lite/Database/DuckDbInitializer.cs @@ -68,7 +68,7 @@ public void Dispose() /// /// Current schema version. Increment this when schema changes require table rebuilds. /// - internal const int CurrentSchemaVersion = 13; + internal const int CurrentSchemaVersion = 14; private readonly string _archivePath; @@ -452,6 +452,17 @@ Must drop/recreate because DuckDB appender writes by position. */ await ExecuteNonQueryAsync(connection, "DROP TABLE IF EXISTS procedure_stats"); await ExecuteNonQueryAsync(connection, "DROP TABLE IF EXISTS query_store_stats"); } + + if (fromVersion < 14) + { + /* v14: Switched memory_grant_stats from per-session (dm_exec_query_memory_grants) + to semaphore-level (dm_exec_query_resource_semaphores) for parity with Dashboard. + Old schema had session_id, query_text, dop, etc. New schema has + resource_semaphore_id, pool_id, and delta columns. + Must drop/recreate because column layout is completely different. */ + _logger?.LogInformation("Running migration to v14: rebuilding memory_grant_stats for resource semaphore schema"); + await ExecuteNonQueryAsync(connection, "DROP TABLE IF EXISTS memory_grant_stats"); + } } /// diff --git a/Lite/Database/Schema.cs b/Lite/Database/Schema.cs index 5bce789c..2723e099 100644 --- a/Lite/Database/Schema.cs +++ b/Lite/Database/Schema.cs @@ -380,19 +380,20 @@ CREATE TABLE IF NOT EXISTS memory_grant_stats ( collection_time TIMESTAMP NOT NULL, server_id INTEGER NOT NULL, server_name VARCHAR NOT NULL, - session_id INTEGER, - database_name VARCHAR, - query_text VARCHAR, - requested_memory_mb DECIMAL(18,2), + resource_semaphore_id SMALLINT, + pool_id INTEGER, + target_memory_mb DECIMAL(18,2), + max_target_memory_mb DECIMAL(18,2), + total_memory_mb DECIMAL(18,2), + available_memory_mb DECIMAL(18,2), granted_memory_mb DECIMAL(18,2), used_memory_mb DECIMAL(18,2), - max_used_memory_mb DECIMAL(18,2), - ideal_memory_mb DECIMAL(18,2), - required_memory_mb DECIMAL(18,2), - wait_time_ms BIGINT, - is_small_grant BOOLEAN, - dop INTEGER, - query_cost DECIMAL(18,2) + grantee_count INTEGER, + waiter_count INTEGER, + timeout_error_count BIGINT, + forced_grant_count BIGINT, + timeout_error_count_delta BIGINT, + forced_grant_count_delta BIGINT )"; public const string CreateWaitingTasksTable = @" diff --git a/Lite/Mcp/McpMemoryTools.cs b/Lite/Mcp/McpMemoryTools.cs index c485b72d..920797a8 100644 --- a/Lite/Mcp/McpMemoryTools.cs +++ b/Lite/Mcp/McpMemoryTools.cs @@ -124,13 +124,12 @@ public static async Task GetMemoryClerks( } } - [McpServerTool(Name = "get_memory_grants"), Description("Gets recently captured memory grants — queries that were granted or waiting for workspace memory at collection time. Shows requested vs granted vs used memory, wait times, and query text. High wait times or large gaps between granted and used memory indicate inefficient grants or memory pressure.")] + [McpServerTool(Name = "get_memory_grants"), Description("Gets resource semaphore statistics showing granted vs available workspace memory per resource pool, waiter counts, and timeout/forced grant deltas. High waiter counts or rising timeout deltas indicate memory grant pressure affecting query performance.")] public static async Task GetMemoryGrants( LocalDataService dataService, ServerManager serverManager, [Description("Server name or display name.")] string? server_name = null, - [Description("Hours of history. Default 1.")] int hours_back = 1, - [Description("Maximum rows. Default 20.")] int limit = 20) + [Description("Hours of history. Default 1.")] int hours_back = 1) { var resolved = ServerResolver.Resolve(serverManager, server_name); if (resolved == null) @@ -143,33 +142,32 @@ public static async Task GetMemoryGrants( var hoursError = McpHelpers.ValidateHoursBack(hours_back); if (hoursError != null) return hoursError; - var limitError = McpHelpers.ValidateTop(limit); - if (limitError != null) return limitError; + var rows = await dataService.GetMemoryGrantChartDataAsync(resolved.Value.ServerId, hours_back); + if (rows.Count == 0) + { + return "No memory grant data available."; + } - var rows = await dataService.GetMemoryGrantStatsAsync(resolved.Value.ServerId, hours_back); - var result = rows.Take(limit).Select(r => new + /* Return latest snapshot */ + var latestTime = rows.Max(r => r.CollectionTime); + var latest = rows.Where(r => r.CollectionTime == latestTime); + + var result = latest.Select(r => new { collection_time = r.CollectionTime.ToString("o"), - session_id = r.SessionId, - database_name = r.DatabaseName, - requested_memory_mb = Math.Round(r.RequestedMemoryMb, 2), + pool_id = r.PoolId, + available_memory_mb = Math.Round(r.AvailableMemoryMb, 2), granted_memory_mb = Math.Round(r.GrantedMemoryMb, 2), used_memory_mb = Math.Round(r.UsedMemoryMb, 2), - max_used_memory_mb = Math.Round(r.MaxUsedMemoryMb, 2), - ideal_memory_mb = Math.Round(r.IdealMemoryMb, 2), - required_memory_mb = Math.Round(r.RequiredMemoryMb, 2), - grant_efficiency_pct = r.GrantedMemoryMb > 0 ? Math.Round(r.MaxUsedMemoryMb / r.GrantedMemoryMb * 100, 1) : 0, - wait_time_ms = r.WaitTimeMs, - is_small_grant = r.IsSmallGrant, - dop = r.Dop, - query_cost = Math.Round(r.QueryCost, 2), - query_text = McpHelpers.Truncate(r.QueryText, 2000) + grantee_count = r.GranteeCount, + waiter_count = r.WaiterCount, + timeout_error_count_delta = r.TimeoutErrorCountDelta, + forced_grant_count_delta = r.ForcedGrantCountDelta }); return JsonSerializer.Serialize(new { server = resolved.Value.ServerName, - hours_back, grants = result }, McpHelpers.JsonOptions); } diff --git a/Lite/Services/DeltaCalculator.cs b/Lite/Services/DeltaCalculator.cs index ce0d5cbd..55ff9105 100644 --- a/Lite/Services/DeltaCalculator.cs +++ b/Lite/Services/DeltaCalculator.cs @@ -49,6 +49,7 @@ public async Task SeedFromDatabaseAsync(DuckDbInitializer duckDb) await SeedWaitStatsAsync(connection); await SeedFileIoStatsAsync(connection); await SeedPerfmonStatsAsync(connection); + await SeedMemoryGrantStatsAsync(connection); _logger?.LogInformation("Delta calculator seeded from database"); } @@ -157,4 +158,35 @@ FROM perfmon_stats if (count > 0) _logger?.LogDebug("Seeded {Count} perfmon_stats baseline rows", count); } + private async Task SeedMemoryGrantStatsAsync(DuckDBConnection connection) + { + try + { + using var cmd = connection.CreateCommand(); + cmd.CommandText = @" +SELECT server_id, pool_id, resource_semaphore_id, timeout_error_count, forced_grant_count +FROM memory_grant_stats +WHERE (server_id, collection_time) IN ( + SELECT server_id, MAX(collection_time) FROM memory_grant_stats GROUP BY server_id +)"; + using var reader = await cmd.ExecuteReaderAsync(); + var count = 0; + while (await reader.ReadAsync()) + { + var serverId = reader.GetInt32(0); + var poolId = reader.IsDBNull(1) ? 0 : reader.GetInt32(1); + var semaphoreId = reader.IsDBNull(2) ? (short)0 : reader.GetInt16(2); + var deltaKey = $"{poolId}_{semaphoreId}"; + Seed(serverId, "memory_grants_timeouts", deltaKey, reader.IsDBNull(3) ? 0 : reader.GetInt64(3)); + Seed(serverId, "memory_grants_forced", deltaKey, reader.IsDBNull(4) ? 0 : reader.GetInt64(4)); + count++; + } + if (count > 0) _logger?.LogDebug("Seeded {Count} memory_grant_stats baseline rows", count); + } + catch + { + /* Table may not exist on first run after schema migration */ + } + } + } diff --git a/Lite/Services/LocalDataService.MemoryGrants.cs b/Lite/Services/LocalDataService.MemoryGrants.cs index 77766e63..eb93033f 100644 --- a/Lite/Services/LocalDataService.MemoryGrants.cs +++ b/Lite/Services/LocalDataService.MemoryGrants.cs @@ -16,66 +16,51 @@ namespace PerformanceMonitorLite.Services; public partial class LocalDataService { /// - /// Gets the most recent memory grant snapshot for a server. + /// Gets memory grant trend — total granted MB per collection snapshot for the Memory Overview overlay. /// - public async Task> GetMemoryGrantStatsAsync(int serverId, int hoursBack = 1) + public async Task> GetMemoryGrantTrendAsync(int serverId, int hoursBack = 4, 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, - session_id, - database_name, - query_text, - requested_memory_mb, - granted_memory_mb, - used_memory_mb, - max_used_memory_mb, - ideal_memory_mb, - required_memory_mb, - wait_time_ms, - is_small_grant, - dop, - query_cost + 0 AS total_server_memory_mb, + 0 AS target_server_memory_mb, + 0 AS buffer_pool_mb, + SUM(granted_memory_mb) AS total_granted_mb FROM memory_grant_stats WHERE server_id = $1 AND collection_time >= $2 -ORDER BY collection_time DESC, granted_memory_mb DESC"; +AND collection_time <= $3 +GROUP BY collection_time +ORDER BY collection_time"; command.Parameters.Add(new DuckDBParameter { Value = serverId }); - command.Parameters.Add(new DuckDBParameter { Value = DateTime.UtcNow.AddHours(-hoursBack) }); + command.Parameters.Add(new DuckDBParameter { Value = startTime }); + command.Parameters.Add(new DuckDBParameter { Value = endTime }); - var items = new List(); + var items = new List(); using var reader = await command.ExecuteReaderAsync(); while (await reader.ReadAsync()) { - items.Add(new MemoryGrantStatsRow + items.Add(new MemoryTrendPoint { CollectionTime = reader.GetDateTime(0), - SessionId = reader.IsDBNull(1) ? 0 : reader.GetInt32(1), - DatabaseName = reader.IsDBNull(2) ? "" : reader.GetString(2), - QueryText = reader.IsDBNull(3) ? "" : reader.GetString(3), - RequestedMemoryMb = reader.IsDBNull(4) ? 0 : ToDouble(reader.GetValue(4)), - GrantedMemoryMb = reader.IsDBNull(5) ? 0 : ToDouble(reader.GetValue(5)), - UsedMemoryMb = reader.IsDBNull(6) ? 0 : ToDouble(reader.GetValue(6)), - MaxUsedMemoryMb = reader.IsDBNull(7) ? 0 : ToDouble(reader.GetValue(7)), - IdealMemoryMb = reader.IsDBNull(8) ? 0 : ToDouble(reader.GetValue(8)), - RequiredMemoryMb = reader.IsDBNull(9) ? 0 : ToDouble(reader.GetValue(9)), - WaitTimeMs = reader.IsDBNull(10) ? 0 : ToInt64(reader.GetValue(10)), - IsSmallGrant = !reader.IsDBNull(11) && reader.GetBoolean(11), - Dop = reader.IsDBNull(12) ? 0 : reader.GetInt32(12), - QueryCost = reader.IsDBNull(13) ? 0 : ToDouble(reader.GetValue(13)) + TotalGrantedMb = reader.IsDBNull(4) ? 0 : ToDouble(reader.GetValue(4)) }); } - return items; } /// - /// Gets memory grant trend — total granted MB per collection snapshot for charting. + /// Gets memory grant chart data aggregated by collection_time and pool_id + /// for the Memory Grants sub-tab charts. /// - public async Task> GetMemoryGrantTrendAsync(int serverId, int hoursBack = 4, DateTime? fromDate = null, DateTime? toDate = null) + public async Task> GetMemoryGrantChartDataAsync(int serverId, int hoursBack = 4, DateTime? fromDate = null, DateTime? toDate = null) { using var connection = await OpenConnectionAsync(); using var command = connection.CreateCommand(); @@ -85,53 +70,56 @@ public async Task> GetMemoryGrantTrendAsync(int serverId, command.CommandText = @" SELECT collection_time, - 0 AS total_server_memory_mb, - 0 AS target_server_memory_mb, - 0 AS buffer_pool_mb, - SUM(granted_memory_mb) AS total_granted_mb + pool_id, + SUM(available_memory_mb) AS available_memory_mb, + SUM(granted_memory_mb) AS granted_memory_mb, + SUM(used_memory_mb) AS used_memory_mb, + SUM(grantee_count) AS grantee_count, + SUM(waiter_count) AS waiter_count, + SUM(timeout_error_count_delta) AS timeout_error_count_delta, + SUM(forced_grant_count_delta) AS forced_grant_count_delta FROM memory_grant_stats WHERE server_id = $1 AND collection_time >= $2 AND collection_time <= $3 -GROUP BY collection_time -ORDER BY collection_time"; +AND granted_memory_mb > 0 +GROUP BY collection_time, pool_id +ORDER BY collection_time, pool_id"; 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(); + var items = new List(); using var reader = await command.ExecuteReaderAsync(); while (await reader.ReadAsync()) { - items.Add(new MemoryTrendPoint + items.Add(new MemoryGrantChartPoint { CollectionTime = reader.GetDateTime(0), - TotalGrantedMb = reader.IsDBNull(4) ? 0 : ToDouble(reader.GetValue(4)) + PoolId = reader.IsDBNull(1) ? 0 : reader.GetInt32(1), + AvailableMemoryMb = reader.IsDBNull(2) ? 0 : ToDouble(reader.GetValue(2)), + GrantedMemoryMb = reader.IsDBNull(3) ? 0 : ToDouble(reader.GetValue(3)), + UsedMemoryMb = reader.IsDBNull(4) ? 0 : ToDouble(reader.GetValue(4)), + GranteeCount = reader.IsDBNull(5) ? 0 : (int)ToInt64(reader.GetValue(5)), + WaiterCount = reader.IsDBNull(6) ? 0 : (int)ToInt64(reader.GetValue(6)), + TimeoutErrorCountDelta = reader.IsDBNull(7) ? 0 : ToInt64(reader.GetValue(7)), + ForcedGrantCountDelta = reader.IsDBNull(8) ? 0 : ToInt64(reader.GetValue(8)) }); } return items; } } -public class MemoryGrantStatsRow +public class MemoryGrantChartPoint { public DateTime CollectionTime { get; set; } - public int SessionId { get; set; } - public string DatabaseName { get; set; } = ""; - public string QueryText { get; set; } = ""; - public double RequestedMemoryMb { get; set; } + public int PoolId { get; set; } + public double AvailableMemoryMb { get; set; } public double GrantedMemoryMb { get; set; } public double UsedMemoryMb { get; set; } - public double MaxUsedMemoryMb { get; set; } - public double IdealMemoryMb { get; set; } - public double RequiredMemoryMb { get; set; } - public long WaitTimeMs { get; set; } - public bool IsSmallGrant { get; set; } - public int Dop { get; set; } - public double QueryCost { get; set; } - - public string GrantEfficiency => GrantedMemoryMb > 0 - ? $"{MaxUsedMemoryMb / GrantedMemoryMb * 100:F0}%" - : "N/A"; + public int GranteeCount { get; set; } + public int WaiterCount { get; set; } + public long TimeoutErrorCountDelta { get; set; } + public long ForcedGrantCountDelta { get; set; } } diff --git a/Lite/Services/RemoteCollectorService.MemoryGrants.cs b/Lite/Services/RemoteCollectorService.MemoryGrants.cs index dfaeab5e..53b3438a 100644 --- a/Lite/Services/RemoteCollectorService.MemoryGrants.cs +++ b/Lite/Services/RemoteCollectorService.MemoryGrants.cs @@ -21,7 +21,8 @@ namespace PerformanceMonitorLite.Services; public partial class RemoteCollectorService { /// - /// Collects memory grant statistics from sys.dm_exec_query_memory_grants. + /// Collects memory grant statistics from sys.dm_exec_query_resource_semaphores. + /// Uses the same DMV as Dashboard for parity. /// private async Task CollectMemoryGrantStatsAsync(ServerConnection server, CancellationToken cancellationToken) { @@ -29,22 +30,20 @@ private async Task CollectMemoryGrantStatsAsync(ServerConnection server, Ca SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED; SELECT - session_id = mg.session_id, - database_name = DB_NAME(st.dbid), - query_text = LEFT(st.text, 4000), - requested_memory_mb = CONVERT(decimal(18,2), mg.requested_memory_kb / 1024.0), - granted_memory_mb = CONVERT(decimal(18,2), ISNULL(mg.granted_memory_kb, 0) / 1024.0), - used_memory_mb = CONVERT(decimal(18,2), ISNULL(mg.used_memory_kb, 0) / 1024.0), - max_used_memory_mb = CONVERT(decimal(18,2), ISNULL(mg.max_used_memory_kb, 0) / 1024.0), - ideal_memory_mb = CONVERT(decimal(18,2), mg.ideal_memory_kb / 1024.0), - required_memory_mb = CONVERT(decimal(18,2), mg.required_memory_kb / 1024.0), - wait_time_ms = mg.wait_time_ms, - is_small_grant = mg.is_small, - dop = mg.dop, - query_cost = mg.query_cost -FROM sys.dm_exec_query_memory_grants AS mg -OUTER APPLY sys.dm_exec_sql_text(mg.sql_handle) AS st -WHERE mg.session_id <> @@SPID + resource_semaphore_id = deqrs.resource_semaphore_id, + pool_id = deqrs.pool_id, + target_memory_mb = CONVERT(decimal(18,2), deqrs.target_memory_kb / 1024.0), + max_target_memory_mb = CONVERT(decimal(18,2), deqrs.max_target_memory_kb / 1024.0), + total_memory_mb = CONVERT(decimal(18,2), deqrs.total_memory_kb / 1024.0), + available_memory_mb = CONVERT(decimal(18,2), deqrs.available_memory_kb / 1024.0), + granted_memory_mb = CONVERT(decimal(18,2), ISNULL(deqrs.granted_memory_kb, 0) / 1024.0), + used_memory_mb = CONVERT(decimal(18,2), ISNULL(deqrs.used_memory_kb, 0) / 1024.0), + grantee_count = deqrs.grantee_count, + waiter_count = deqrs.waiter_count, + timeout_error_count = ISNULL(deqrs.timeout_error_count, 0), + forced_grant_count = ISNULL(deqrs.forced_grant_count, 0) +FROM sys.dm_exec_query_resource_semaphores AS deqrs +WHERE deqrs.max_target_memory_kb IS NOT NULL OPTION(RECOMPILE);"; var serverId = GetServerId(server); @@ -53,10 +52,10 @@ WHERE mg.session_id <> @@SPID _lastSqlMs = 0; _lastDuckDbMs = 0; - var rows = new List<(int SessionId, string? DatabaseName, string? QueryText, - decimal RequestedMb, decimal GrantedMb, decimal UsedMb, decimal MaxUsedMb, - decimal IdealMb, decimal RequiredMb, long WaitTimeMs, bool IsSmall, - int Dop, decimal QueryCost)>(); + var rows = new List<(short ResourceSemaphoreId, int PoolId, + decimal TargetMb, decimal MaxTargetMb, decimal TotalMb, decimal AvailableMb, + decimal GrantedMb, decimal UsedMb, + int GranteeCount, int WaiterCount, long TimeoutErrorCount, long ForcedGrantCount)>(); var sqlSw = Stopwatch.StartNew(); using var sqlConnection = await CreateConnectionAsync(server, cancellationToken); @@ -67,19 +66,18 @@ WHERE mg.session_id <> @@SPID while (await reader.ReadAsync(cancellationToken)) { rows.Add(( - Convert.ToInt32(reader.GetValue(0)), - reader.IsDBNull(1) ? null : reader.GetString(1), - reader.IsDBNull(2) ? null : reader.GetString(2), + Convert.ToInt16(reader.GetValue(0)), + Convert.ToInt32(reader.GetValue(1)), + reader.IsDBNull(2) ? 0m : reader.GetDecimal(2), reader.IsDBNull(3) ? 0m : reader.GetDecimal(3), reader.IsDBNull(4) ? 0m : reader.GetDecimal(4), reader.IsDBNull(5) ? 0m : reader.GetDecimal(5), reader.IsDBNull(6) ? 0m : reader.GetDecimal(6), reader.IsDBNull(7) ? 0m : reader.GetDecimal(7), - reader.IsDBNull(8) ? 0m : reader.GetDecimal(8), - reader.IsDBNull(9) ? 0L : Convert.ToInt64(reader.GetValue(9)), - reader.IsDBNull(10) ? false : Convert.ToBoolean(reader.GetValue(10)), - reader.IsDBNull(11) ? 0 : Convert.ToInt32(reader.GetValue(11)), - reader.IsDBNull(12) ? 0m : SafeToDecimal(reader.GetValue(12)))); + reader.IsDBNull(8) ? 0 : Convert.ToInt32(reader.GetValue(8)), + reader.IsDBNull(9) ? 0 : Convert.ToInt32(reader.GetValue(9)), + reader.IsDBNull(10) ? 0L : Convert.ToInt64(reader.GetValue(10)), + reader.IsDBNull(11) ? 0L : Convert.ToInt64(reader.GetValue(11)))); } sqlSw.Stop(); @@ -93,24 +91,29 @@ WHERE mg.session_id <> @@SPID { foreach (var r in rows) { + var deltaKey = $"{r.PoolId}_{r.ResourceSemaphoreId}"; + var deltaTimeouts = _deltaCalculator.CalculateDelta(serverId, "memory_grants_timeouts", deltaKey, r.TimeoutErrorCount); + var deltaForced = _deltaCalculator.CalculateDelta(serverId, "memory_grants_forced", deltaKey, r.ForcedGrantCount); + var row = appender.CreateRow(); row.AppendValue(GenerateCollectionId()) .AppendValue(collectionTime) .AppendValue(serverId) .AppendValue(server.ServerName) - .AppendValue(r.SessionId) - .AppendValue(r.DatabaseName) - .AppendValue(r.QueryText) - .AppendValue(r.RequestedMb) + .AppendValue(r.ResourceSemaphoreId) + .AppendValue(r.PoolId) + .AppendValue(r.TargetMb) + .AppendValue(r.MaxTargetMb) + .AppendValue(r.TotalMb) + .AppendValue(r.AvailableMb) .AppendValue(r.GrantedMb) .AppendValue(r.UsedMb) - .AppendValue(r.MaxUsedMb) - .AppendValue(r.IdealMb) - .AppendValue(r.RequiredMb) - .AppendValue(r.WaitTimeMs) - .AppendValue(r.IsSmall) - .AppendValue(r.Dop) - .AppendValue(r.QueryCost) + .AppendValue(r.GranteeCount) + .AppendValue(r.WaiterCount) + .AppendValue(r.TimeoutErrorCount) + .AppendValue(r.ForcedGrantCount) + .AppendValue(deltaTimeouts) + .AppendValue(deltaForced) .EndRow(); rowsCollected++; } diff --git a/install/02_create_tables.sql b/install/02_create_tables.sql index c053fcab..05c4b492 100644 --- a/install/02_create_tables.sql +++ b/install/02_create_tables.sql @@ -681,6 +681,7 @@ BEGIN ( collection_id bigint IDENTITY NOT NULL, collection_time datetime2(7) NOT NULL DEFAULT SYSDATETIME(), + server_start_time datetime2(7) NOT NULL, resource_semaphore_id smallint NOT NULL, pool_id integer NOT NULL, target_memory_mb decimal(19,2) NULL, @@ -693,16 +694,15 @@ BEGIN waiter_count integer NULL, timeout_error_count bigint NULL, forced_grant_count bigint NULL, - /*Pressure warnings*/ - available_memory_pressure_warning bit NULL, - waiter_count_warning bit NULL, - timeout_error_warning bit NULL, - forced_grant_warning bit NULL, - CONSTRAINT - PK_memory_grant_stats - PRIMARY KEY CLUSTERED - (collection_time, collection_id) - WITH + /*Delta columns calculated by framework*/ + timeout_error_count_delta bigint NULL, + forced_grant_count_delta bigint NULL, + sample_interval_seconds integer NULL, + CONSTRAINT + PK_memory_grant_stats + PRIMARY KEY CLUSTERED + (collection_time, collection_id) + WITH (DATA_COMPRESSION = PAGE) ); diff --git a/install/05_delta_framework.sql b/install/05_delta_framework.sql index e184e705..b0e87b28 100644 --- a/install/05_delta_framework.sql +++ b/install/05_delta_framework.sql @@ -1065,6 +1065,90 @@ BEGIN OPTION(RECOMPILE, HASH JOIN, HASH GROUP);'; END; + /* + Memory Grant Stats Delta Calculation + */ + ELSE IF @table_name = N'memory_grant_stats' + BEGIN + SET @sql = N' + WITH + current_collection AS + ( + SELECT + mgs.*, + row_number = + ROW_NUMBER() OVER + ( + PARTITION BY + mgs.resource_semaphore_id, + mgs.pool_id + ORDER BY + mgs.collection_time DESC + ) + FROM collect.memory_grant_stats AS mgs + WHERE mgs.timeout_error_count_delta IS NULL + ), + previous_collection AS + ( + SELECT + mgs.collection_id, + mgs.resource_semaphore_id, + mgs.pool_id, + mgs.timeout_error_count, + mgs.forced_grant_count, + mgs.collection_time, + row_number = + ROW_NUMBER() OVER + ( + PARTITION BY + mgs.resource_semaphore_id, + mgs.pool_id + ORDER BY + mgs.collection_time DESC + ) + FROM collect.memory_grant_stats AS mgs + WHERE mgs.timeout_error_count_delta IS NOT NULL + OR mgs.collection_id IN + ( + SELECT + MIN(mgs2.collection_id) + FROM collect.memory_grant_stats AS mgs2 + GROUP BY + mgs2.resource_semaphore_id, + mgs2.pool_id + ) + ) + UPDATE + cc + SET + timeout_error_count_delta = + CASE + WHEN cc.server_start_time >= pc.collection_time + THEN cc.timeout_error_count /*Server restart*/ + WHEN cc.timeout_error_count >= pc.timeout_error_count + THEN cc.timeout_error_count - pc.timeout_error_count + ELSE cc.timeout_error_count /*Counter wrapped or restart*/ + END, + forced_grant_count_delta = + CASE + WHEN cc.server_start_time >= pc.collection_time + THEN cc.forced_grant_count /*Server restart*/ + WHEN cc.forced_grant_count >= pc.forced_grant_count + THEN cc.forced_grant_count - pc.forced_grant_count + ELSE cc.forced_grant_count /*Counter wrapped or restart*/ + END, + sample_interval_seconds = + DATEDIFF(SECOND, pc.collection_time, cc.collection_time) + FROM current_collection AS cc + LEFT JOIN previous_collection AS pc + ON cc.resource_semaphore_id = pc.resource_semaphore_id + AND cc.pool_id = pc.pool_id + AND pc.row_number = 1 + WHERE cc.row_number = 1 + AND pc.collection_id IS NOT NULL /*Exclude first collection where no previous exists*/ + OPTION(RECOMPILE, HASH JOIN, HASH GROUP);'; + END; + ELSE BEGIN RAISERROR(N'Unknown table name for delta calculation: %s', 16, 1, @table_name); diff --git a/install/06_ensure_collection_table.sql b/install/06_ensure_collection_table.sql index 0b7a9407..905e62ef 100644 --- a/install/06_ensure_collection_table.sql +++ b/install/06_ensure_collection_table.sql @@ -671,6 +671,7 @@ BEGIN ( collection_id bigint IDENTITY NOT NULL, collection_time datetime2(7) NOT NULL DEFAULT SYSDATETIME(), + server_start_time datetime2(7) NOT NULL, resource_semaphore_id smallint NOT NULL, pool_id integer NOT NULL, target_memory_mb decimal(19,2) NULL, @@ -683,16 +684,15 @@ BEGIN waiter_count integer NULL, timeout_error_count bigint NULL, forced_grant_count bigint NULL, - /*Pressure warnings*/ - available_memory_pressure_warning bit NULL, - waiter_count_warning bit NULL, - timeout_error_warning bit NULL, - forced_grant_warning bit NULL, - CONSTRAINT - PK_memory_grant_stats - PRIMARY KEY CLUSTERED - (collection_time, collection_id) - WITH + /*Delta columns calculated by framework*/ + timeout_error_count_delta bigint NULL, + forced_grant_count_delta bigint NULL, + sample_interval_seconds integer NULL, + CONSTRAINT + PK_memory_grant_stats + PRIMARY KEY CLUSTERED + (collection_time, collection_id) + WITH (DATA_COMPRESSION = PAGE) ); END; diff --git a/install/15_collect_memory_grant_stats.sql b/install/15_collect_memory_grant_stats.sql index 76b4de45..53b64aab 100644 --- a/install/15_collect_memory_grant_stats.sql +++ b/install/15_collect_memory_grant_stats.sql @@ -21,8 +21,8 @@ GO /* Memory grant statistics collector Collects memory grant semaphore data from sys.dm_exec_query_resource_semaphores -Stores MB values and calculates pressure warnings Point-in-time snapshot data for memory grant pressure monitoring +Delta calculation for cumulative counters handled by collect.calculate_deltas */ IF OBJECT_ID(N'collect.memory_grant_stats_collector', N'P') IS NULL @@ -98,12 +98,12 @@ BEGIN /* Collect memory grant semaphore statistics - Stores MB values and calculates pressure warnings - This is point-in-time state data showing current memory grant pressure + Point-in-time state data showing current memory grant pressure */ INSERT INTO collect.memory_grant_stats ( + server_start_time, resource_semaphore_id, pool_id, target_memory_mb, @@ -115,13 +115,15 @@ BEGIN grantee_count, waiter_count, timeout_error_count, - forced_grant_count, - available_memory_pressure_warning, - waiter_count_warning, - timeout_error_warning, - forced_grant_warning + forced_grant_count ) SELECT + server_start_time = + ( + SELECT + dosi.sqlserver_start_time + FROM sys.dm_os_sys_info AS dosi + ), resource_semaphore_id = deqrs.resource_semaphore_id, pool_id = deqrs.pool_id, target_memory_mb = deqrs.target_memory_kb / 1024.0, @@ -133,129 +135,19 @@ BEGIN grantee_count = deqrs.grantee_count, waiter_count = deqrs.waiter_count, timeout_error_count = ISNULL(deqrs.timeout_error_count, 0), - forced_grant_count = ISNULL(deqrs.forced_grant_count, 0), - available_memory_pressure_warning = - CASE - WHEN prev.available_memory_mb IS NOT NULL - AND (deqrs.available_memory_kb / 1024.0) < (prev.available_memory_mb * 0.80) - THEN 1 - ELSE 0 - END, - waiter_count_warning = - CASE - WHEN deqrs.waiter_count > 0 - THEN 1 - ELSE 0 - END, - timeout_error_warning = - CASE - WHEN ISNULL(deqrs.timeout_error_count, 0) > 0 - THEN 1 - ELSE 0 - END, - forced_grant_warning = - CASE - WHEN ISNULL(deqrs.forced_grant_count, 0) > 0 - THEN 1 - ELSE 0 - END + forced_grant_count = ISNULL(deqrs.forced_grant_count, 0) FROM sys.dm_exec_query_resource_semaphores AS deqrs - OUTER APPLY - ( - SELECT TOP (1) - prev.available_memory_mb - FROM collect.memory_grant_stats AS prev - WHERE prev.resource_semaphore_id = deqrs.resource_semaphore_id - AND prev.pool_id = deqrs.pool_id - ORDER BY - prev.collection_id DESC - ) AS prev WHERE deqrs.max_target_memory_kb IS NOT NULL OPTION(RECOMPILE); SET @rows_collected = ROWCOUNT_BIG(); /* - Debug output for pressure warnings + Calculate deltas for cumulative counters */ - IF @debug = 1 - BEGIN - DECLARE - @current_available_memory_mb decimal(19,2), - @previous_available_memory_mb decimal(19,2), - @current_waiter_count integer, - @current_timeout_error_count bigint, - @current_forced_grant_count bigint, - @available_warning bit, - @waiter_warning bit, - @timeout_warning bit, - @forced_warning bit; - - SELECT - @current_available_memory_mb = mgs.available_memory_mb, - @current_waiter_count = mgs.waiter_count, - @current_timeout_error_count = mgs.timeout_error_count, - @current_forced_grant_count = mgs.forced_grant_count, - @available_warning = mgs.available_memory_pressure_warning, - @waiter_warning = mgs.waiter_count_warning, - @timeout_warning = mgs.timeout_error_warning, - @forced_warning = mgs.forced_grant_warning - FROM collect.memory_grant_stats AS mgs - WHERE mgs.collection_id = - ( - SELECT - MAX(mgs2.collection_id) - FROM collect.memory_grant_stats AS mgs2 - ); - - /* - Get previous available memory for warning message - */ - SELECT TOP (1) - @previous_available_memory_mb = mgs.available_memory_mb - FROM collect.memory_grant_stats AS mgs - WHERE mgs.collection_id < - ( - SELECT - MAX(mgs2.collection_id) - FROM collect.memory_grant_stats AS mgs2 - ) - ORDER BY - mgs.collection_id DESC; - - IF @available_warning = 1 - BEGIN - DECLARE @available_msg nvarchar(500) = - N'WARNING: Available memory grant dropped from ' + - CONVERT(nvarchar(20), @previous_available_memory_mb) + N' MB to ' + - CONVERT(nvarchar(20), @current_available_memory_mb) + N' MB (>20% drop)'; - RAISERROR(@available_msg, 0, 1) WITH NOWAIT; - END; - - IF @waiter_warning = 1 - BEGIN - DECLARE @waiter_msg nvarchar(500) = - N'WARNING: Memory grant waiters detected: ' + - CONVERT(nvarchar(20), @current_waiter_count); - RAISERROR(@waiter_msg, 0, 1) WITH NOWAIT; - END; - - IF @timeout_warning = 1 - BEGIN - DECLARE @timeout_msg nvarchar(500) = - N'WARNING: Memory grant timeout errors detected: ' + - CONVERT(nvarchar(20), @current_timeout_error_count); - RAISERROR(@timeout_msg, 0, 1) WITH NOWAIT; - END; - - IF @forced_warning = 1 - BEGIN - DECLARE @forced_msg nvarchar(500) = - N'WARNING: Forced memory grants detected: ' + - CONVERT(nvarchar(20), @current_forced_grant_count); - RAISERROR(@forced_msg, 0, 1) WITH NOWAIT; - END; - END; + EXECUTE collect.calculate_deltas + @table_name = N'memory_grant_stats', + @debug = @debug; /* Log successful collection @@ -318,5 +210,5 @@ GO PRINT 'Memory grant stats collector created successfully'; PRINT 'Collects point-in-time memory grant semaphore data from sys.dm_exec_query_resource_semaphores'; -PRINT 'Stores MB values and calculates pressure warnings for memory grant monitoring'; +PRINT 'Delta calculation for timeout_error_count and forced_grant_count via collect.calculate_deltas'; GO diff --git a/install/37_collect_waiting_tasks.sql b/install/37_collect_waiting_tasks.sql index 973f0b33..7d17a9dd 100644 --- a/install/37_collect_waiting_tasks.sql +++ b/install/37_collect_waiting_tasks.sql @@ -144,53 +144,21 @@ BEGIN wait_type = wt.wait_type, wait_duration_ms = wt.wait_duration_ms, blocking_session_id = ISNULL(wt.blocking_session_id, 0), - resource_description = - CASE - WHEN LEN(wt.resource_description) > 1000 - THEN SUBSTRING(wt.resource_description, 1, 1000) + N'...' - ELSE wt.resource_description - END, - database_id = der.database_id, + resource_description = NULL, + database_id = NULL, database_name = DB_NAME(der.database_id), - query_text = - CASE - WHEN dest.text IS NULL - THEN N'(unavailable)' - WHEN LEN(dest.text) > 4000 - THEN SUBSTRING(dest.text, 1, 4000) + N'...' - ELSE dest.text - END, - statement_text = - CASE - WHEN dest.text IS NULL - THEN N'(unavailable)' - WHEN der.statement_start_offset = 0 - AND der.statement_end_offset = 0 - THEN dest.text - WHEN der.statement_end_offset = -1 - THEN SUBSTRING - ( - dest.text, - (der.statement_start_offset / 2) + 1, - 2147483647 - ) - ELSE SUBSTRING - ( - dest.text, - (der.statement_start_offset / 2) + 1, - ((der.statement_end_offset - der.statement_start_offset) / 2) + 1 - ) - END, - query_plan = qp.query_plan, - sql_handle = der.sql_handle, - plan_handle = der.plan_handle, - request_status = der.status, - command = der.command, - cpu_time_ms = der.cpu_time, - total_elapsed_time_ms = der.total_elapsed_time, - logical_reads = der.logical_reads, - writes = der.writes, - row_count = der.row_count + query_text = NULL, + statement_text = NULL, + query_plan = NULL, + sql_handle = NULL, + plan_handle = NULL, + request_status = NULL, + command = NULL, + cpu_time_ms = NULL, + total_elapsed_time_ms = NULL, + logical_reads = NULL, + writes = NULL, + row_count = NULL FROM sys.dm_os_waiting_tasks AS wt LEFT JOIN sys.dm_exec_requests AS der ON der.session_id = wt.session_id diff --git a/upgrades/1.3.0-to-2.0.0/01_memory_grant_stats_schema.sql b/upgrades/1.3.0-to-2.0.0/01_memory_grant_stats_schema.sql new file mode 100644 index 00000000..738663a5 --- /dev/null +++ b/upgrades/1.3.0-to-2.0.0/01_memory_grant_stats_schema.sql @@ -0,0 +1,172 @@ +/* +Copyright 2026 Darling Data, LLC +https://www.erikdarling.com/ + +Upgrade from 1.3.0 to 2.0.0 +Adds server_start_time and delta columns to collect.memory_grant_stats +for proper delta framework integration. +Drops unused warning columns from the inline delta approach. +*/ + +SET ANSI_NULLS ON; +SET ANSI_PADDING ON; +SET ANSI_WARNINGS ON; +SET ARITHABORT ON; +SET CONCAT_NULL_YIELDS_NULL ON; +SET QUOTED_IDENTIFIER ON; +SET NUMERIC_ROUNDABORT OFF; +SET IMPLICIT_TRANSACTIONS OFF; +SET STATISTICS TIME, IO OFF; +GO + +USE PerformanceMonitor; +GO + +/* Add server_start_time for delta framework restart detection */ +IF NOT EXISTS +( + SELECT + 1/0 + FROM sys.columns + WHERE object_id = OBJECT_ID(N'collect.memory_grant_stats') + AND name = N'server_start_time' +) +BEGIN + ALTER TABLE + collect.memory_grant_stats + ADD + server_start_time datetime2(7) NULL; + + PRINT 'Added server_start_time to collect.memory_grant_stats'; +END; +GO + +/* Add timeout_error_count_delta for delta framework */ +IF NOT EXISTS +( + SELECT + 1/0 + FROM sys.columns + WHERE object_id = OBJECT_ID(N'collect.memory_grant_stats') + AND name = N'timeout_error_count_delta' +) +BEGIN + ALTER TABLE + collect.memory_grant_stats + ADD + timeout_error_count_delta bigint NULL; + + PRINT 'Added timeout_error_count_delta to collect.memory_grant_stats'; +END; +GO + +/* Add forced_grant_count_delta for delta framework */ +IF NOT EXISTS +( + SELECT + 1/0 + FROM sys.columns + WHERE object_id = OBJECT_ID(N'collect.memory_grant_stats') + AND name = N'forced_grant_count_delta' +) +BEGIN + ALTER TABLE + collect.memory_grant_stats + ADD + forced_grant_count_delta bigint NULL; + + PRINT 'Added forced_grant_count_delta to collect.memory_grant_stats'; +END; +GO + +/* Add sample_interval_seconds for delta framework */ +IF NOT EXISTS +( + SELECT + 1/0 + FROM sys.columns + WHERE object_id = OBJECT_ID(N'collect.memory_grant_stats') + AND name = N'sample_interval_seconds' +) +BEGIN + ALTER TABLE + collect.memory_grant_stats + ADD + sample_interval_seconds integer NULL; + + PRINT 'Added sample_interval_seconds to collect.memory_grant_stats'; +END; +GO + +/* Drop unused warning columns from the old inline delta approach */ +IF EXISTS +( + SELECT + 1/0 + FROM sys.columns + WHERE object_id = OBJECT_ID(N'collect.memory_grant_stats') + AND name = N'available_memory_pressure_warning' +) +BEGIN + ALTER TABLE + collect.memory_grant_stats + DROP COLUMN + available_memory_pressure_warning; + + PRINT 'Dropped available_memory_pressure_warning from collect.memory_grant_stats'; +END; +GO + +IF EXISTS +( + SELECT + 1/0 + FROM sys.columns + WHERE object_id = OBJECT_ID(N'collect.memory_grant_stats') + AND name = N'waiter_count_warning' +) +BEGIN + ALTER TABLE + collect.memory_grant_stats + DROP COLUMN + waiter_count_warning; + + PRINT 'Dropped waiter_count_warning from collect.memory_grant_stats'; +END; +GO + +IF EXISTS +( + SELECT + 1/0 + FROM sys.columns + WHERE object_id = OBJECT_ID(N'collect.memory_grant_stats') + AND name = N'timeout_error_warning' +) +BEGIN + ALTER TABLE + collect.memory_grant_stats + DROP COLUMN + timeout_error_warning; + + PRINT 'Dropped timeout_error_warning from collect.memory_grant_stats'; +END; +GO + +IF EXISTS +( + SELECT + 1/0 + FROM sys.columns + WHERE object_id = OBJECT_ID(N'collect.memory_grant_stats') + AND name = N'forced_grant_warning' +) +BEGIN + ALTER TABLE + collect.memory_grant_stats + DROP COLUMN + forced_grant_warning; + + PRINT 'Dropped forced_grant_warning from collect.memory_grant_stats'; +END; +GO diff --git a/upgrades/1.3.0-to-2.0.0/upgrade.txt b/upgrades/1.3.0-to-2.0.0/upgrade.txt new file mode 100644 index 00000000..d4d53f95 --- /dev/null +++ b/upgrades/1.3.0-to-2.0.0/upgrade.txt @@ -0,0 +1 @@ +01_memory_grant_stats_schema.sql diff --git a/upgrades/README.md b/upgrades/README.md index fcf28827..28395114 100644 --- a/upgrades/README.md +++ b/upgrades/README.md @@ -73,4 +73,6 @@ GO - **1.2.0**: Current Configuration tabs, Default Trace DynamicResource fix, alert badge, chart tooltips, drill-down sizing - **1.3.0**: Add total_physical_memory_mb and committed_target_memory_mb to memory_stats collector +- **2.0.0**: Add server_start_time and delta columns to memory_grant_stats for delta framework; drop unused warning columns; new Memory Grants charts + Future upgrade folders will be added here as new versions are released.