Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
138 changes: 118 additions & 20 deletions Dashboard/Controls/MemoryContent.xaml.cs
Original file line number Diff line number Diff line change
Expand Up @@ -1080,31 +1080,132 @@ private void LoadMemoryPressureEventsChart(IEnumerable<MemoryPressureEventItem>
_memoryPressureEventsHover?.Clear();
TabHelpers.ApplyThemeToChart(MemoryPressureEventsChart);

// Only chart HIGH severity events
var dataList = data?.Where(d => d.Severity.Equals("HIGH", StringComparison.OrdinalIgnoreCase))
.OrderBy(d => d.SampleTime).ToList() ?? new List<MemoryPressureEventItem>();
// Count rows where SQL Server reported actual pressure (indicator >= 2 matches sp_pressuredetector).
var dataList = data?
.Where(d => d.MemoryIndicatorsProcess >= 2 || d.MemoryIndicatorsSystem >= 2)
.OrderBy(d => d.SampleTime)
.ToList() ?? new List<MemoryPressureEventItem>();

bool hasData = false;
int maxBarCount = 0;

if (dataList.Count > 0)
{
// Group by hour and count HIGH events
var grouped = dataList
.GroupBy(d => new DateTime(d.SampleTime.Year, d.SampleTime.Month, d.SampleTime.Day, d.SampleTime.Hour, 0, 0))
.OrderBy(g => g.Key)
.ToList();

if (grouped.Count > 0)
double hourWidth = 1.0 / 24.0;
double barSize = hourWidth * 0.4;
double barOffset = hourWidth * 0.22;

// Four series: SQL Server medium, SQL Server severe (stacked on top of medium),
// OS medium, OS severe. Stacking uses ValueBase so severe bars sit on top of medium.
var sqlMediumBars = new List<ScottPlot.Bar>();
var sqlSevereBars = new List<ScottPlot.Bar>();
var osMediumBars = new List<ScottPlot.Bar>();
var osSevereBars = new List<ScottPlot.Bar>();

var sqlMediumColor = ScottPlot.Color.FromHex("#FFB74D"); // orange 300
var sqlSevereColor = ScottPlot.Color.FromHex("#E65100"); // orange 900
var osMediumColor = ScottPlot.Color.FromHex("#E57373"); // red 300
var osSevereColor = ScottPlot.Color.FromHex("#B71C1C"); // red 900

foreach (var g in grouped)
{
int sqlMedium = g.Count(d => d.MemoryIndicatorsProcess == 2);
int sqlSevere = g.Count(d => d.MemoryIndicatorsProcess >= 3);
int osMedium = g.Count(d => d.MemoryIndicatorsSystem == 2);
int osSevere = g.Count(d => d.MemoryIndicatorsSystem >= 3);
Comment on lines +1117 to +1120
Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

After this PR MemoryPressureEventItem.Severity (Models/MemoryPressureEventItem.cs:13) is no longer consumed anywhere in the Dashboard — the last reader was the old d.Severity.Equals("HIGH"…) filter. DatabaseService.Memory.cs:151 still reads column ordinal 6 and populates it, so it's dead-but-still-materialised. Either drop the field and stop selecting the column, or leave a // kept for MCP/grid binding comment so the next person doesn't delete it accidentally.


Generated by Claude Code

double x = g.Key.ToOADate();

if (sqlMedium > 0)
{
sqlMediumBars.Add(new ScottPlot.Bar
{
Position = x - barOffset,
ValueBase = 0,
Value = sqlMedium,
Size = barSize,
FillColor = sqlMediumColor,
LineWidth = 0
});
}
if (sqlSevere > 0)
{
sqlSevereBars.Add(new ScottPlot.Bar
{
Position = x - barOffset,
ValueBase = sqlMedium,
Value = sqlMedium + sqlSevere,
Size = barSize,
FillColor = sqlSevereColor,
LineWidth = 0
});
}
if (osMedium > 0)
{
osMediumBars.Add(new ScottPlot.Bar
{
Position = x + barOffset,
ValueBase = 0,
Value = osMedium,
Size = barSize,
FillColor = osMediumColor,
LineWidth = 0
});
}
if (osSevere > 0)
{
osSevereBars.Add(new ScottPlot.Bar
{
Position = x + barOffset,
ValueBase = osMedium,
Value = osMedium + osSevere,
Size = barSize,
FillColor = osSevereColor,
LineWidth = 0
});
}

int sqlTotal = sqlMedium + sqlSevere;
int osTotal = osMedium + osSevere;
if (sqlTotal > maxBarCount) maxBarCount = sqlTotal;
if (osTotal > maxBarCount) maxBarCount = osTotal;
}

bool anyBars = sqlMediumBars.Count > 0 || sqlSevereBars.Count > 0
|| osMediumBars.Count > 0 || osSevereBars.Count > 0;

if (anyBars)
{
hasData = true;
var timePoints = grouped.Select(g => g.Key);
double[] highCounts = grouped.Select(g => (double)g.Count()).ToArray();

var (xs, ys) = TabHelpers.FillTimeSeriesGaps(timePoints, highCounts.Select(c => c));
var highScatter = MemoryPressureEventsChart.Plot.Add.Scatter(xs, ys);
highScatter.LineWidth = 2;
highScatter.MarkerSize = 5;
highScatter.Color = TabHelpers.ChartColors[3];
highScatter.LegendText = "High Pressure Events";
_memoryPressureEventsHover?.Add(highScatter, "High Pressure Events");
if (sqlMediumBars.Count > 0)
{
var bp = MemoryPressureEventsChart.Plot.Add.Bars(sqlMediumBars);
bp.LegendText = "SQL Server (medium)";
_memoryPressureEventsHover?.Add(bp, "SQL Server (medium)");
}
if (sqlSevereBars.Count > 0)
{
var bp = MemoryPressureEventsChart.Plot.Add.Bars(sqlSevereBars);
bp.LegendText = "SQL Server (severe)";
_memoryPressureEventsHover?.Add(bp, "SQL Server (severe)");
}
if (osMediumBars.Count > 0)
{
var bp = MemoryPressureEventsChart.Plot.Add.Bars(osMediumBars);
bp.LegendText = "Operating System (medium)";
_memoryPressureEventsHover?.Add(bp, "Operating System (medium)");
}
if (osSevereBars.Count > 0)
{
var bp = MemoryPressureEventsChart.Plot.Add.Bars(osSevereBars);
bp.LegendText = "Operating System (severe)";
_memoryPressureEventsHover?.Add(bp, "Operating System (severe)");
}

_legendPanels[MemoryPressureEventsChart] = MemoryPressureEventsChart.Plot.ShowLegend(ScottPlot.Edge.Bottom);
MemoryPressureEventsChart.Plot.Legend.FontSize = 12;
Expand All @@ -1114,19 +1215,16 @@ private void LoadMemoryPressureEventsChart(IEnumerable<MemoryPressureEventItem>
if (!hasData)
{
double xCenter = xMin + (xMax - xMin) / 2;
var noDataText = MemoryPressureEventsChart.Plot.Add.Text("No data for selected time range", xCenter, 0.5);
var noDataText = MemoryPressureEventsChart.Plot.Add.Text("No memory pressure events in selected time range", xCenter, 0.5);
noDataText.LabelFontSize = 14;
noDataText.LabelFontColor = ScottPlot.Colors.Gray;
noDataText.LabelAlignment = ScottPlot.Alignment.MiddleCenter;
}

MemoryPressureEventsChart.Plot.Axes.DateTimeTicksBottomDateChange();
MemoryPressureEventsChart.Plot.Axes.SetLimitsX(xMin, xMax);
MemoryPressureEventsChart.Plot.YLabel("Event Count");
// Fixed negative space for legend
MemoryPressureEventsChart.Plot.Axes.AutoScaleY();
var pressureLimits = MemoryPressureEventsChart.Plot.Axes.GetLimits();
MemoryPressureEventsChart.Plot.Axes.SetLimitsY(0, pressureLimits.Top * 1.05);
MemoryPressureEventsChart.Plot.YLabel("Pressure Events per Hour");
MemoryPressureEventsChart.Plot.Axes.SetLimitsY(0, Math.Max(maxBarCount * 1.1, 5.0));
Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

maxBarCount only tracks sqlTotal / osTotal separately (whichever one side is taller). Because SQL and OS bars are drawn at different X offsets (x - barOffset vs x + barOffset) they never stack on top of each other, so this is correct — but it's subtle. Worth a one-line comment next to this so a future reader doesn't "fix" it to sqlTotal + osTotal, which would over-scale the Y axis.


Generated by Claude Code


TabHelpers.LockChartVerticalAxis(MemoryPressureEventsChart);
MemoryPressureEventsChart.Refresh();
Expand Down
51 changes: 47 additions & 4 deletions Dashboard/Helpers/ChartHoverHelper.cs
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ internal sealed class ChartHoverHelper
{
private readonly ScottPlot.WPF.WpfPlot _chart;
private readonly List<(ScottPlot.Plottables.Scatter Scatter, string Label)> _scatters = new();
private readonly List<(ScottPlot.Plottables.BarPlot BarPlot, string Label)> _barPlots = new();
private readonly Popup _popup;
private readonly TextBlock _text;
private string _unit;
Expand Down Expand Up @@ -62,20 +63,28 @@ public void Dispose()
_chart.MouseLeave -= OnMouseLeave;
_popup.IsOpen = false;
_scatters.Clear();
_barPlots.Clear();
}

public void Clear() => _scatters.Clear();
public void Clear()
{
_scatters.Clear();
_barPlots.Clear();
}

public void Add(ScottPlot.Plottables.Scatter scatter, string label) =>
_scatters.Add((scatter, label));

public void Add(ScottPlot.Plottables.BarPlot barPlot, string label) =>
_barPlots.Add((barPlot, label));

/// <summary>
/// Returns the nearest series label and data-point time for the given mouse position,
/// or null if no series is close enough.
/// </summary>
public (string Label, DateTime Time)? GetNearestSeries(Point mousePos)
{
if (_scatters.Count == 0) return null;
if (_scatters.Count == 0 && _barPlots.Count == 0) return null;
try
{
var dpi = VisualTreeHelper.GetDpi(_chart);
Expand Down Expand Up @@ -106,16 +115,45 @@ public void Add(ScottPlot.Plottables.Scatter scatter, string label) =>
}
}

FindNearestBar(pixel, ref bestYDistance, ref bestPoint, ref bestLabel, ref found);

if (found)
return (bestLabel, DateTime.FromOADate(bestPoint.X));
}
catch { }
return null;
}

private void FindNearestBar(ScottPlot.Pixel pixel, ref double bestYDistance,
ref ScottPlot.DataPoint bestPoint, ref string bestLabel, ref bool found)
{
foreach (var (barPlot, label) in _barPlots)
{
foreach (var bar in barPlot.Bars)
{
var topPixel = _chart.Plot.GetPixel(new ScottPlot.Coordinates(bar.Position, bar.Value));
double halfWidthPx = Math.Abs(
_chart.Plot.GetPixel(new ScottPlot.Coordinates(bar.Position + bar.Size / 2, bar.Value)).X
- topPixel.X);
Comment on lines +134 to +137
Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Two Plot.GetPixel calls per bar, per mouse move. A 30-day window at 24h buckets × up to 4 stacked series = up to ~2,880 bars, re-computed every 30 ms while the mouse is inside the chart. halfWidthPx depends only on bar.Size (constant per series here), so hoist it to a per-series calculation outside the inner loop. Also consider pixel-space binary-searching on Position once the bars are sorted — the inner foreach is effectively an O(N) scan per mouse event.

Not a blocker for this PR, but worth a follow-up if the 30-day view feels laggy on hover.


Generated by Claude Code

double dx = Math.Abs(topPixel.X - pixel.X);
if (dx > halfWidthPx + 4) continue;
double dy = Math.Abs(topPixel.Y - pixel.Y);
if (dy < bestYDistance)
{
bestYDistance = dy;
// For stacked bars, report the segment height (Value - ValueBase), not the top coordinate
double segmentHeight = bar.Value - bar.ValueBase;
bestPoint = new ScottPlot.DataPoint(new ScottPlot.Coordinates(bar.Position, segmentHeight), 0);
bestLabel = label;
found = true;
}
}
}
}

private void OnMouseMove(object sender, MouseEventArgs e)
{
if (_scatters.Count == 0) return;
if (_scatters.Count == 0 && _barPlots.Count == 0) return;
var now = DateTime.UtcNow;
if ((now - _lastUpdate).TotalMilliseconds < 30) return;
_lastUpdate = now;
Expand Down Expand Up @@ -158,10 +196,15 @@ pick the series closest in Y (nearest line to cursor). */
}
}

FindNearestBar(pixel, ref bestYDistance, ref bestPoint, ref bestLabel, ref found);

if (found)
{
var time = ServerTimeHelper.ConvertForDisplay(DateTime.FromOADate(bestPoint.X), ServerTimeHelper.CurrentDisplayMode);
_text.Text = $"{bestLabel}\n{bestPoint.Y:N1} {_unit}\n{time:HH:mm:ss}";
string valueFormatted = (bestPoint.Y == Math.Floor(bestPoint.Y))
? bestPoint.Y.ToString("N0")
: bestPoint.Y.ToString("N1");
Comment on lines +204 to +206
Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This format switch is cross-cutting: it fires for every ChartHoverHelper consumer, not just the new bar chart. Scatter values that happen to be whole numbers (e.g. a memory trend reading of exactly 100 GB or a PLE of 1000) will now render as 100 / 1000 while neighbouring samples render as 99.7 / 1000.4 — inconsistent unit precision in the same tooltip.

If the goal is integer-looking counts for the bar chart only, guard it on _barPlots having produced the winner (e.g. track bool bestIsBar through FindNearestBar) or parameterise the helper's format at construction time. Otherwise this will likely show up as a cosmetic regression on the other memory/wait/cpu charts.


Generated by Claude Code

_text.Text = $"{bestLabel}\n{valueFormatted} {_unit}\n{time:HH:mm:ss}";
_popup.HorizontalOffset = pos.X + 15;
_popup.VerticalOffset = pos.Y + 15;
_popup.IsOpen = true;
Expand Down
44 changes: 44 additions & 0 deletions Dashboard/Mcp/McpInstructions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -217,6 +217,50 @@ You are connected to a SQL Server performance monitoring tool via Performance Mo
| `RESOURCE_SEMAPHORE` | Memory grant pressure | `get_resource_semaphore` |
| `LATCH_*` | Internal contention | `get_tempdb_trend` |

## Interpreting Memory Pressure Events

`get_memory_pressure_events` returns notifications from the `RING_BUFFER_RESOURCE_MONITOR` ring buffer. The `memory_indicators_process` and `memory_indicators_system` values are SQL Server's Resource Monitor signals. Indicator scale:

- **0-1**: normal operating state, not actionable
- **2 (medium)**: Resource Monitor has crossed a threshold and is starting to respond — trimming caches, reducing memory grants. Worth investigating if sustained or frequent.
- **3+ (severe)**: aggressive response — buffer pool pages are being evicted, plan cache entries thrown out, workspace memory starved. Always worth investigating.

The two indicators report different things:

- `memory_indicators_process` — the SQL Server *process itself* is under memory pressure. Usually workload-induced (large memory grants, plan cache bloat, buffer pool churn).
- `memory_indicators_system` — Windows is signaling low memory *system-wide*. Something on the whole box is consuming memory; SQL Server may or may not be the culprit.

### What to check when process pressure (indicator >= 2) fires

The workload is squeezing SQL Server itself. Follow-up tools:
| Signal to check | Tool |
|-----------------|------|
| Memory grant contention, workspace memory exhaustion | `get_resource_semaphore` |
| Buffer pool composition, memory clerk distribution | `get_memory_clerks` |
| Plan cache bloat (lots of single-use plans) | `get_plan_cache_bloat` |
| Page Life Expectancy, target vs total server memory | `get_memory_stats`, `get_memory_trend` |
| Queries that requested large grants during the window | `get_top_queries_by_cpu`, `get_expensive_queries` |
| `RESOURCE_SEMAPHORE` waits in the same window | `get_wait_stats`, `get_wait_trend` |

### What to check when system pressure (indicator >= 2) fires but process does not

The box is tight on memory, but SQL Server's own process is not the cause. SQL Server feels Windows' low-memory notification but isn't driving it. Typical root causes: other services on the machine (anti-virus, backup agents, monitoring agents, additional SQL instances, SSIS/SSRS, RDP sessions), oversized file system cache, or VM-host memory oversubscription. Follow-up:

| Signal to check | Tool |
|-----------------|------|
| SQL Server's memory configuration (`max server memory` vs total RAM) | `get_server_properties` |
| Is SQL Server itself actually fine? | `get_memory_stats`, `get_memory_clerks` |

Most of the diagnosis in this case is *outside* the monitored SQL instance — tell the user to check what else is running on the host.

### Patterns

- **Both process and system firing together** → real capacity problem. Add RAM, tune the workload, or reduce concurrency.
- **Process only** → workload/schema issue, not a hardware problem. Tune queries and indexes.
- **System only** → non-SQL workload on the host; SQL itself is healthy but the tenant mix is tight.
- **Bursty spikes** → correlate the pressure window with `get_running_jobs` (scheduled maintenance, index rebuilds, big reports) and `get_top_queries_by_cpu` for that period.
- **Flat-line sustained** → chronic under-provisioning; memory needs to grow or workload needs to shrink.

## Tool Relationships

- `get_wait_stats` identifies the symptom category (CPU, I/O, locks, parallelism). Other tools find the root cause.
Expand Down
12 changes: 11 additions & 1 deletion Dashboard/Mcp/McpSystemEventTools.cs
Original file line number Diff line number Diff line change
Expand Up @@ -117,7 +117,17 @@ public static async Task<string> GetTraceAnalysis(
}
}

[McpServerTool(Name = "get_memory_pressure_events"), Description("Gets memory pressure notifications from the ring buffer. Shows RESOURCE_MEMPHYSICAL_LOW, RESOURCE_MEMVIRTUAL_LOW, and other memory broker notifications with process/system indicators.")]
[McpServerTool(Name = "get_memory_pressure_events"), Description(@"Gets memory pressure notifications from the RING_BUFFER_RESOURCE_MONITOR ring buffer (same source as sp_pressuredetector). Returns RESOURCE_MEMPHYSICAL_LOW, RESOURCE_MEMVIRTUAL_LOW, RESOURCE_MEMPHYSICAL_HIGH, and RESOURCE_MEM_STEADY notifications with indicator values.

Indicator scale (applies to both memory_indicators_process and memory_indicators_system):
0-1 = normal, no pressure
2 = medium pressure (SQL Server's Resource Monitor starts trimming caches and reducing grants)
3+ = severe pressure (aggressive buffer pool / plan cache eviction)

memory_indicators_process = SQL Server process itself is under memory pressure (workload-induced).
memory_indicators_system = Windows is signaling low memory system-wide (could be other tenants on the box).

For actionable interpretation and suggested follow-up tools, see the 'Interpreting Memory Pressure Events' section of the server instructions.")]
public static async Task<string> GetMemoryPressureEvents(
ServerManager serverManager,
DatabaseServiceRegistry registry,
Expand Down
4 changes: 2 additions & 2 deletions Lite.Tests/DuckDbSchemaTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -138,8 +138,8 @@ public void SchemaStatements_MatchTableCount()
foreach (var _ in Schema.GetAllTableStatements())
tableCount++;

/* 29 tables from Schema (schema_version is created separately by DuckDbInitializer) */
Assert.Equal(29, tableCount);
/* 30 tables from Schema (schema_version is created separately by DuckDbInitializer) */
Assert.Equal(30, tableCount);
}

[Fact]
Expand Down
7 changes: 7 additions & 0 deletions Lite/Controls/ServerTab.xaml
Original file line number Diff line number Diff line change
Expand Up @@ -1176,6 +1176,13 @@
</Grid>
</TabItem>

<!-- Memory Pressure Events Sub-Tab -->
<TabItem Header="Memory Pressure Events">
<Grid>
<ScottPlot:WpfPlot x:Name="MemoryPressureEventsChart" Margin="5"/>
</Grid>
</TabItem>

</TabControl>
</TabItem>

Expand Down
Loading
Loading