-
Notifications
You must be signed in to change notification settings - Fork 63
Fix and port Memory Pressure Events feature (#865) #866
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -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); | ||
| 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; | ||
|
|
@@ -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)); | ||
|
Owner
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Generated by Claude Code |
||
|
|
||
| TabHelpers.LockChartVerticalAxis(MemoryPressureEventsChart); | ||
| MemoryPressureEventsChart.Refresh(); | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -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; | ||
|
|
@@ -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); | ||
|
|
@@ -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
Owner
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Two 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; | ||
|
|
@@ -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
Owner
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This format switch is cross-cutting: it fires for every If the goal is integer-looking counts for the bar chart only, guard it on 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; | ||
|
|
||
There was a problem hiding this comment.
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 oldd.Severity.Equals("HIGH"…)filter.DatabaseService.Memory.cs:151still 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 bindingcomment so the next person doesn't delete it accidentally.Generated by Claude Code