Skip to content

Commit b146158

Browse files
Split FinOps services and content code-behinds into partial classes
Move-only refactor; no behavior changes. Five files split: Services (partials of partials): - Dashboard/Services/DatabaseService.FinOps.cs (2924 → 24) into 7 sub-partials: Inventory, Workload, Storage, Queries, IndexAnalysis, Recommendations, Models - Lite/Services/LocalDataService.FinOps.cs (2702 → 525) into 7 sub-partials: ServerProperties, Inventory, StorageGrowth, Utilization, Workload, IndexAnalysis, Recommendations Content code-behinds (per-tab partials): - Dashboard/Controls/SystemEventsContent.xaml.cs (2263 → 363) into 10 partials: FilterPopup, SystemHealth, SevereErrors, IOIssues, SchedulerIssues, MemoryConditions, CPUTasks, MemoryBroker, MemoryNodeOOM, CopyExport - Dashboard/Controls/ResourceMetricsContent.xaml.cs (1881 → 406) into 8 partials: LatchStats, SpinlockStats, TempdbStats, SessionStats, FileIoLatency, PerfmonCounters, WaitStatsDetail, CopyExport - Dashboard/Controls/MemoryContent.xaml.cs (1356 → 256) into 6 partials: MemoryStats, MemoryGrants, MemoryClerks, PlanCache, MemoryPressure, CopyExport Build: 0 errors. Smoke-tested: 30-min Lite watch with 0 error events; collection_health snapshot 18,390 runs across 24 collectors, 100% SUCCESS. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent 95fc881 commit b146158

43 files changed

Lines changed: 11344 additions & 10357 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.
Lines changed: 149 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,149 @@
1+
/*
2+
* Copyright (c) 2026 Erik Darling, Darling Data LLC
3+
*
4+
* This file is part of the SQL Server Performance Monitor.
5+
*
6+
* Licensed under the MIT License. See LICENSE file in the project root for full license information.
7+
*/
8+
9+
using System;
10+
using System.Collections.Generic;
11+
using System.Globalization;
12+
using System.IO;
13+
using System.Linq;
14+
using System.Text;
15+
using System.Threading.Tasks;
16+
using System.Windows;
17+
using System.Windows.Controls;
18+
using System.Windows.Controls.Primitives;
19+
using System.Windows.Data;
20+
using Microsoft.Win32;
21+
using PerformanceMonitorDashboard.Helpers;
22+
using PerformanceMonitorDashboard.Models;
23+
using PerformanceMonitorDashboard.Services;
24+
25+
namespace PerformanceMonitorDashboard.Controls
26+
{
27+
public partial class MemoryContent : UserControl
28+
{
29+
#region Context Menu Handlers
30+
31+
private void CopyCell_Click(object sender, RoutedEventArgs e)
32+
{
33+
if (sender is MenuItem menuItem && menuItem.Parent is ContextMenu contextMenu)
34+
{
35+
var dataGrid = TabHelpers.FindDataGridFromContextMenu(contextMenu);
36+
if (dataGrid != null && dataGrid.CurrentCell.Item != null)
37+
{
38+
var cellContent = TabHelpers.GetCellContent(dataGrid, dataGrid.CurrentCell);
39+
if (!string.IsNullOrEmpty(cellContent))
40+
{
41+
/* Use SetDataObject with copy=false to avoid WPF's problematic Clipboard.Flush() */
42+
Clipboard.SetDataObject(cellContent, false);
43+
}
44+
}
45+
}
46+
}
47+
48+
private void CopyRow_Click(object sender, RoutedEventArgs e)
49+
{
50+
if (sender is MenuItem menuItem && menuItem.Parent is ContextMenu contextMenu)
51+
{
52+
var dataGrid = TabHelpers.FindDataGridFromContextMenu(contextMenu);
53+
if (dataGrid != null && dataGrid.SelectedItem != null)
54+
{
55+
var rowText = TabHelpers.GetRowAsText(dataGrid, dataGrid.SelectedItem);
56+
/* Use SetDataObject with copy=false to avoid WPF's problematic Clipboard.Flush() */
57+
Clipboard.SetDataObject(rowText, false);
58+
}
59+
}
60+
}
61+
62+
private void CopyAllRows_Click(object sender, RoutedEventArgs e)
63+
{
64+
if (sender is MenuItem menuItem && menuItem.Parent is ContextMenu contextMenu)
65+
{
66+
var dataGrid = TabHelpers.FindDataGridFromContextMenu(contextMenu);
67+
if (dataGrid != null && dataGrid.Items.Count > 0)
68+
{
69+
var sb = new StringBuilder();
70+
71+
// Add headers
72+
var headers = new List<string>();
73+
foreach (var column in dataGrid.Columns)
74+
{
75+
if (column is DataGridBoundColumn)
76+
{
77+
headers.Add(Helpers.DataGridClipboardBehavior.GetHeaderText(column));
78+
}
79+
}
80+
sb.AppendLine(string.Join("\t", headers));
81+
82+
// Add all rows
83+
foreach (var item in dataGrid.Items)
84+
{
85+
sb.AppendLine(TabHelpers.GetRowAsText(dataGrid, item));
86+
}
87+
88+
/* Use SetDataObject with copy=false to avoid WPF's problematic Clipboard.Flush() */
89+
Clipboard.SetDataObject(sb.ToString(), false);
90+
}
91+
}
92+
}
93+
94+
private void ExportToCsv_Click(object sender, RoutedEventArgs e)
95+
{
96+
if (sender is MenuItem menuItem && menuItem.Parent is ContextMenu contextMenu)
97+
{
98+
var dataGrid = TabHelpers.FindDataGridFromContextMenu(contextMenu);
99+
if (dataGrid != null && dataGrid.Items.Count > 0)
100+
{
101+
string prefix = "memory";
102+
103+
104+
var saveFileDialog = new SaveFileDialog
105+
{
106+
FileName = $"{prefix}_{DateTime.Now:yyyyMMdd_HHmmss}.csv",
107+
DefaultExt = ".csv",
108+
Filter = "CSV Files (*.csv)|*.csv|All Files (*.*)|*.*"
109+
};
110+
111+
if (saveFileDialog.ShowDialog() == true)
112+
{
113+
try
114+
{
115+
var sb = new StringBuilder();
116+
117+
// Add headers
118+
var headers = new List<string>();
119+
foreach (var column in dataGrid.Columns)
120+
{
121+
if (column is DataGridBoundColumn)
122+
{
123+
headers.Add(TabHelpers.EscapeCsvField(Helpers.DataGridClipboardBehavior.GetHeaderText(column), TabHelpers.CsvSeparator));
124+
}
125+
}
126+
sb.AppendLine(string.Join(TabHelpers.CsvSeparator, headers));
127+
128+
// Add all rows
129+
foreach (var item in dataGrid.Items)
130+
{
131+
var values = TabHelpers.GetRowValues(dataGrid, item);
132+
sb.AppendLine(string.Join(TabHelpers.CsvSeparator, values.Select(v => TabHelpers.EscapeCsvField(v, TabHelpers.CsvSeparator))));
133+
}
134+
135+
File.WriteAllText(saveFileDialog.FileName, sb.ToString());
136+
MessageBox.Show($"Data exported successfully to:\n{saveFileDialog.FileName}", "Export Complete", MessageBoxButton.OK, MessageBoxImage.Information);
137+
}
138+
catch (Exception ex)
139+
{
140+
MessageBox.Show($"Error exporting data:\n\n{ex.Message}", "Export Error", MessageBoxButton.OK, MessageBoxImage.Error);
141+
}
142+
}
143+
}
144+
}
145+
}
146+
147+
#endregion
148+
}
149+
}
Lines changed: 248 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,248 @@
1+
/*
2+
* Copyright (c) 2026 Erik Darling, Darling Data LLC
3+
*
4+
* This file is part of the SQL Server Performance Monitor.
5+
*
6+
* Licensed under the MIT License. See LICENSE file in the project root for full license information.
7+
*/
8+
9+
using System;
10+
using System.Collections.Generic;
11+
using System.Globalization;
12+
using System.IO;
13+
using System.Linq;
14+
using System.Text;
15+
using System.Threading.Tasks;
16+
using System.Windows;
17+
using System.Windows.Controls;
18+
using System.Windows.Controls.Primitives;
19+
using System.Windows.Data;
20+
using Microsoft.Win32;
21+
using PerformanceMonitorDashboard.Helpers;
22+
using PerformanceMonitorDashboard.Models;
23+
using PerformanceMonitorDashboard.Services;
24+
25+
namespace PerformanceMonitorDashboard.Controls
26+
{
27+
public partial class MemoryContent : UserControl
28+
{
29+
#region Memory Clerks
30+
31+
private async System.Threading.Tasks.Task RefreshMemoryClerksAsync()
32+
{
33+
if (_databaseService == null) return;
34+
35+
try
36+
{
37+
if (!MemoryClerksChart.Plot.GetPlottables().Any())
38+
{
39+
MemoryClerksLoading.IsLoading = true;
40+
MemoryClerksNoDataMessage.Visibility = Visibility.Collapsed;
41+
}
42+
43+
var clerkTypes = await _databaseService.GetDistinctMemoryClerkTypesAsync(_memoryClerksHoursBack, _memoryClerksFromDate, _memoryClerksToDate);
44+
PopulateMemoryClerkPicker(clerkTypes);
45+
await UpdateMemoryClerksChartFromPickerAsync();
46+
}
47+
catch (Exception ex)
48+
{
49+
Logger.Error($"Error loading memory clerks: {ex.Message}");
50+
}
51+
finally
52+
{
53+
MemoryClerksLoading.IsLoading = false;
54+
}
55+
}
56+
57+
private void PopulateMemoryClerkPicker(List<string> clerkTypes)
58+
{
59+
var previouslySelected = new HashSet<string>(_memoryClerkItems.Where(i => i.IsSelected).Select(i => i.DisplayName));
60+
var topClerks = previouslySelected.Count == 0 ? new HashSet<string>(clerkTypes.Take(5)) : null;
61+
_memoryClerkItems = clerkTypes.Select(c => new SelectableItem
62+
{
63+
DisplayName = c,
64+
IsSelected = previouslySelected.Contains(c) || (topClerks != null && topClerks.Contains(c))
65+
}).ToList();
66+
RefreshMemoryClerkListOrder();
67+
}
68+
69+
private void RefreshMemoryClerkListOrder()
70+
{
71+
if (_memoryClerkItems == null) return;
72+
_memoryClerkItems = _memoryClerkItems
73+
.OrderByDescending(x => x.IsSelected)
74+
.ThenBy(x => x.DisplayName)
75+
.ToList();
76+
ApplyMemoryClerkFilter();
77+
UpdateMemoryClerkCount();
78+
}
79+
80+
private void UpdateMemoryClerkCount()
81+
{
82+
if (_memoryClerkItems == null || MemoryClerkCountText == null) return;
83+
int count = _memoryClerkItems.Count(x => x.IsSelected);
84+
MemoryClerkCountText.Text = $"{count} selected";
85+
}
86+
87+
private void ApplyMemoryClerkFilter()
88+
{
89+
var search = MemoryClerkSearchBox?.Text?.Trim() ?? "";
90+
MemoryClerksList.ItemsSource = null;
91+
if (string.IsNullOrEmpty(search))
92+
MemoryClerksList.ItemsSource = _memoryClerkItems;
93+
else
94+
MemoryClerksList.ItemsSource = _memoryClerkItems.Where(i => i.DisplayName.Contains(search, StringComparison.OrdinalIgnoreCase)).ToList();
95+
}
96+
97+
private void MemoryClerkSearch_TextChanged(object sender, TextChangedEventArgs e) => ApplyMemoryClerkFilter();
98+
99+
private void MemoryClerkSelectTop_Click(object sender, RoutedEventArgs e)
100+
{
101+
_isUpdatingMemoryClerkSelection = true;
102+
var topClerks = new HashSet<string>(_memoryClerkItems.Take(5).Select(x => x.DisplayName));
103+
foreach (var item in _memoryClerkItems)
104+
item.IsSelected = topClerks.Contains(item.DisplayName);
105+
_isUpdatingMemoryClerkSelection = false;
106+
RefreshMemoryClerkListOrder();
107+
_ = UpdateMemoryClerksChartFromPickerAsync();
108+
}
109+
110+
private void MemoryClerkClearAll_Click(object sender, RoutedEventArgs e)
111+
{
112+
_isUpdatingMemoryClerkSelection = true;
113+
var visible = (MemoryClerksList.ItemsSource as IEnumerable<SelectableItem>)?.ToList() ?? _memoryClerkItems;
114+
foreach (var item in visible) item.IsSelected = false;
115+
_isUpdatingMemoryClerkSelection = false;
116+
RefreshMemoryClerkListOrder();
117+
_ = UpdateMemoryClerksChartFromPickerAsync();
118+
}
119+
120+
private void MemoryClerk_CheckChanged(object sender, RoutedEventArgs e)
121+
{
122+
if (_isUpdatingMemoryClerkSelection) return;
123+
RefreshMemoryClerkListOrder();
124+
_ = UpdateMemoryClerksChartFromPickerAsync();
125+
}
126+
127+
private async System.Threading.Tasks.Task UpdateMemoryClerksChartFromPickerAsync()
128+
{
129+
if (_databaseService == null) return;
130+
131+
try
132+
{
133+
var selected = _memoryClerkItems.Where(i => i.IsSelected).Take(20).ToList();
134+
135+
if (_legendPanels.TryGetValue(MemoryClerksChart, out var existingPanel) && existingPanel != null)
136+
{
137+
MemoryClerksChart.Plot.Axes.Remove(existingPanel);
138+
_legendPanels[MemoryClerksChart] = null;
139+
}
140+
MemoryClerksChart.Plot.Clear();
141+
_memoryClerksHover?.Clear();
142+
TabHelpers.ApplyThemeToChart(MemoryClerksChart);
143+
144+
DateTime rangeEnd = _memoryClerksToDate ?? Helpers.ServerTimeHelper.ServerNow;
145+
DateTime rangeStart = _memoryClerksFromDate ?? rangeEnd.AddHours(-_memoryClerksHoursBack);
146+
double xMin = rangeStart.ToOADate();
147+
double xMax = rangeEnd.ToOADate();
148+
149+
if (selected.Count > 0)
150+
{
151+
var selectedTypes = selected.Select(s => s.DisplayName).ToList();
152+
var data = await _databaseService.GetMemoryClerksByTypesAsync(selectedTypes, _memoryClerksHoursBack, _memoryClerksFromDate, _memoryClerksToDate);
153+
var dataList = data.ToList();
154+
155+
MemoryClerksNoDataMessage.Visibility = dataList.Count == 0 ? Visibility.Visible : Visibility.Collapsed;
156+
157+
if (dataList.Count > 0)
158+
{
159+
var colors = TabHelpers.ChartColors;
160+
int colorIndex = 0;
161+
162+
foreach (var clerkType in selectedTypes)
163+
{
164+
var clerkData = dataList.Where(d => d.ClerkType == clerkType)
165+
.OrderBy(d => d.CollectionTime)
166+
.ToList();
167+
168+
if (clerkData.Count >= 1)
169+
{
170+
var timePoints = clerkData.Select(d => d.CollectionTime);
171+
var values = clerkData.Select(d => (double)d.PagesMb);
172+
var (xs, ys) = TabHelpers.FillTimeSeriesGaps(timePoints, values);
173+
174+
var scatter = MemoryClerksChart.Plot.Add.Scatter(xs, ys);
175+
scatter.LineWidth = 2;
176+
scatter.MarkerSize = 5;
177+
scatter.Color = colors[colorIndex % colors.Length];
178+
var label = clerkType.Length > 20 ? clerkType.Substring(0, 20) + "..." : clerkType;
179+
scatter.LegendText = label;
180+
_memoryClerksHover?.Add(scatter, label);
181+
colorIndex++;
182+
}
183+
}
184+
185+
_legendPanels[MemoryClerksChart] = MemoryClerksChart.Plot.ShowLegend(ScottPlot.Edge.Bottom);
186+
MemoryClerksChart.Plot.Legend.FontSize = 12;
187+
}
188+
189+
UpdateMemoryClerksSummaryPanel(dataList);
190+
}
191+
else
192+
{
193+
MemoryClerksNoDataMessage.Visibility = Visibility.Collapsed;
194+
MemoryClerksTotalText.Text = "N/A";
195+
MemoryClerksTopText.Text = "N/A";
196+
}
197+
198+
MemoryClerksChart.Plot.Axes.DateTimeTicksBottomDateChange();
199+
MemoryClerksChart.Plot.Axes.SetLimitsX(xMin, xMax);
200+
MemoryClerksChart.Plot.YLabel("MB");
201+
MemoryClerksChart.Plot.Axes.AutoScaleY();
202+
var clerksLimits = MemoryClerksChart.Plot.Axes.GetLimits();
203+
MemoryClerksChart.Plot.Axes.SetLimitsY(0, clerksLimits.Top * 1.05);
204+
TabHelpers.LockChartVerticalAxis(MemoryClerksChart);
205+
MemoryClerksChart.Refresh();
206+
}
207+
catch (Exception ex)
208+
{
209+
Logger.Error($"Error updating memory clerks chart: {ex.Message}");
210+
}
211+
}
212+
213+
private void UpdateMemoryClerksSummaryPanel(List<MemoryClerksItem> dataList)
214+
{
215+
if (dataList == null || dataList.Count == 0)
216+
{
217+
MemoryClerksTotalText.Text = "N/A";
218+
MemoryClerksTopText.Text = "N/A";
219+
return;
220+
}
221+
222+
var latestTime = dataList.Max(d => d.CollectionTime);
223+
var latestData = dataList
224+
.Where(d => d.CollectionTime == latestTime)
225+
.Where(d => d.ClerkType == null || !d.ClerkType.Contains("BUFFERPOOL", StringComparison.OrdinalIgnoreCase))
226+
.ToList();
227+
228+
var totalMb = latestData.Sum(d => d.PagesMb);
229+
MemoryClerksTotalText.Text = string.Format(CultureInfo.CurrentCulture, "{0:N0} MB", totalMb);
230+
231+
var topClerk = latestData.OrderByDescending(d => d.PagesMb).FirstOrDefault();
232+
if (topClerk != null)
233+
{
234+
var name = topClerk.ClerkType ?? "Unknown";
235+
if (name.StartsWith("MEMORYCLERK_", StringComparison.OrdinalIgnoreCase))
236+
name = name.Substring(12);
237+
if (name.Length > 20) name = name.Substring(0, 20) + "...";
238+
MemoryClerksTopText.Text = string.Format(CultureInfo.CurrentCulture, "{0} ({1:N0} MB)", name, topClerk.PagesMb);
239+
}
240+
else
241+
{
242+
MemoryClerksTopText.Text = "N/A";
243+
}
244+
}
245+
246+
#endregion
247+
}
248+
}

0 commit comments

Comments
 (0)