diff --git a/Lite/Controls/ServerTab.xaml b/Lite/Controls/ServerTab.xaml
index 689218c2..e3cea39a 100644
--- a/Lite/Controls/ServerTab.xaml
+++ b/Lite/Controls/ServerTab.xaml
@@ -565,6 +565,64 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/Lite/Controls/ServerTab.xaml.cs b/Lite/Controls/ServerTab.xaml.cs
index 03725364..6a561f46 100644
--- a/Lite/Controls/ServerTab.xaml.cs
+++ b/Lite/Controls/ServerTab.xaml.cs
@@ -53,6 +53,11 @@ public partial class ServerTab : UserControl
private Helpers.ChartHoverHelper? _lockWaitTrendHover;
private Helpers.ChartHoverHelper? _blockingTrendHover;
private Helpers.ChartHoverHelper? _deadlockTrendHover;
+ private Helpers.ChartHoverHelper? _memoryClerksHover;
+
+ /* Memory clerks picker */
+ private List _memoryClerkItems = new();
+ private bool _isUpdatingMemoryClerkSelection;
/* Column filtering */
private Popup? _filterPopup;
@@ -163,6 +168,7 @@ public ServerTab(ServerConnection server, DuckDbInitializer duckDb, CredentialSe
_lockWaitTrendHover = new Helpers.ChartHoverHelper(LockWaitTrendChart, "ms/sec");
_blockingTrendHover = new Helpers.ChartHoverHelper(BlockingTrendChart, "incidents");
_deadlockTrendHover = new Helpers.ChartHoverHelper(DeadlockTrendChart, "deadlocks");
+ _memoryClerksHover = new Helpers.ChartHoverHelper(MemoryClerksChart, "MB");
/* Initial load is triggered by MainWindow.ConnectToServer calling RefreshData()
after collectors finish - no Loaded handler needed */
@@ -469,6 +475,7 @@ private async System.Threading.Tasks.Task RefreshAllDataAsync()
var deadlockTask = _dataService.GetRecentDeadlocksAsync(_serverId, hoursBack, fromDate, toDate);
var blockedProcessTask = _dataService.GetRecentBlockedProcessReportsAsync(_serverId, hoursBack, fromDate, toDate);
var waitTypesTask = _dataService.GetDistinctWaitTypesAsync(_serverId, hoursBack, fromDate, toDate);
+ var memoryClerkTypesTask = _dataService.GetDistinctMemoryClerkTypesAsync(_serverId, hoursBack, fromDate, toDate);
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);
@@ -483,7 +490,7 @@ private async System.Threading.Tasks.Task RefreshAllDataAsync()
await System.Threading.Tasks.Task.WhenAll(
snapshotsTask, cpuTask, memoryTask, memoryTrendTask,
queryStatsTask, procStatsTask, fileIoTask, fileIoTrendTask, tempDbTask, tempDbFileIoTask,
- deadlockTask, blockedProcessTask, waitTypesTask, perfmonCountersTask,
+ deadlockTask, blockedProcessTask, waitTypesTask, memoryClerkTypesTask, perfmonCountersTask,
queryStoreTask, memoryGrantTrendTask,
serverConfigTask, databaseConfigTask, databaseScopedConfigTask, traceFlagsTask,
runningJobsTask, collectionHealthTask, collectionLogTask);
@@ -547,10 +554,12 @@ await System.Threading.Tasks.Task.WhenAll(
/* Populate pickers (preserve selections) */
PopulateWaitTypePicker(waitTypesTask.Result);
+ PopulateMemoryClerkPicker(memoryClerkTypesTask.Result);
PopulatePerfmonPicker(perfmonCountersTask.Result);
/* Update picker-driven charts */
await UpdateWaitStatsChartFromPickerAsync();
+ await UpdateMemoryClerksChartFromPickerAsync();
await UpdatePerfmonChartFromPickerAsync();
ConnectionStatusText.Text = $"{_server.ServerName} - Last refresh: {DateTime.Now:HH:mm:ss}";
@@ -1357,6 +1366,172 @@ private async System.Threading.Tasks.Task UpdateWaitStatsChartFromPickerAsync()
}
}
+ /* ========== Memory Clerks Picker ========== */
+
+ private void PopulateMemoryClerkPicker(List clerkTypes)
+ {
+ var previouslySelected = new HashSet(_memoryClerkItems.Where(i => i.IsSelected).Select(i => i.DisplayName));
+ var topClerks = previouslySelected.Count == 0 ? new HashSet(clerkTypes.Take(5)) : null;
+ _memoryClerkItems = clerkTypes.Select(c => new SelectableItem
+ {
+ DisplayName = c,
+ IsSelected = previouslySelected.Contains(c) || (topClerks != null && topClerks.Contains(c))
+ }).ToList();
+ RefreshMemoryClerkListOrder();
+ }
+
+ private void RefreshMemoryClerkListOrder()
+ {
+ if (_memoryClerkItems == null) return;
+ _memoryClerkItems = _memoryClerkItems
+ .OrderByDescending(x => x.IsSelected)
+ .ThenBy(x => x.DisplayName)
+ .ToList();
+ ApplyMemoryClerkFilter();
+ UpdateMemoryClerkCount();
+ }
+
+ private void UpdateMemoryClerkCount()
+ {
+ if (_memoryClerkItems == null || MemoryClerkCountText == null) return;
+ int count = _memoryClerkItems.Count(x => x.IsSelected);
+ MemoryClerkCountText.Text = $"{count} selected";
+ }
+
+ private void ApplyMemoryClerkFilter()
+ {
+ var search = MemoryClerkSearchBox?.Text?.Trim() ?? "";
+ MemoryClerksList.ItemsSource = null;
+ if (string.IsNullOrEmpty(search))
+ MemoryClerksList.ItemsSource = _memoryClerkItems;
+ else
+ MemoryClerksList.ItemsSource = _memoryClerkItems.Where(i => i.DisplayName.Contains(search, StringComparison.OrdinalIgnoreCase)).ToList();
+ }
+
+ private void MemoryClerkSearch_TextChanged(object sender, TextChangedEventArgs e) => ApplyMemoryClerkFilter();
+
+ private void MemoryClerkSelectTop_Click(object sender, RoutedEventArgs e)
+ {
+ _isUpdatingMemoryClerkSelection = true;
+ var topClerks = new HashSet(_memoryClerkItems.Take(5).Select(x => x.DisplayName));
+ foreach (var item in _memoryClerkItems)
+ {
+ item.IsSelected = topClerks.Contains(item.DisplayName);
+ }
+ _isUpdatingMemoryClerkSelection = false;
+ RefreshMemoryClerkListOrder();
+ _ = UpdateMemoryClerksChartFromPickerAsync();
+ }
+
+ private void MemoryClerkClearAll_Click(object sender, RoutedEventArgs e)
+ {
+ _isUpdatingMemoryClerkSelection = true;
+ var visible = (MemoryClerksList.ItemsSource as IEnumerable)?.ToList() ?? _memoryClerkItems;
+ foreach (var item in visible) item.IsSelected = false;
+ _isUpdatingMemoryClerkSelection = false;
+ RefreshMemoryClerkListOrder();
+ _ = UpdateMemoryClerksChartFromPickerAsync();
+ }
+
+ private void MemoryClerk_CheckChanged(object sender, RoutedEventArgs e)
+ {
+ if (_isUpdatingMemoryClerkSelection) return;
+ RefreshMemoryClerkListOrder();
+ _ = UpdateMemoryClerksChartFromPickerAsync();
+ }
+
+ private async System.Threading.Tasks.Task UpdateMemoryClerksChartFromPickerAsync()
+ {
+ try
+ {
+ var selected = _memoryClerkItems.Where(i => i.IsSelected).Take(20).ToList();
+
+ ClearChart(MemoryClerksChart);
+ ApplyDarkTheme(MemoryClerksChart);
+ _memoryClerksHover?.Clear();
+
+ if (selected.Count == 0)
+ {
+ MemoryClerksTotalText.Text = "--";
+ MemoryClerksTopText.Text = "--";
+ MemoryClerksChart.Refresh();
+ return;
+ }
+
+ var hoursBack = GetHoursBack();
+ DateTime? fromDate = null;
+ DateTime? toDate = null;
+ if (IsCustomRange)
+ {
+ var fromLocal = GetDateTimeFromPickers(FromDatePicker!, FromHourCombo, FromMinuteCombo);
+ var toLocal = GetDateTimeFromPickers(ToDatePicker!, ToHourCombo, ToMinuteCombo);
+ if (fromLocal.HasValue && toLocal.HasValue)
+ {
+ fromDate = ServerTimeHelper.LocalToServerTime(fromLocal.Value);
+ toDate = ServerTimeHelper.LocalToServerTime(toLocal.Value);
+ }
+ }
+
+ double globalMax = 0;
+ double nonBpTotal = 0;
+ string topNonBpClerk = "";
+ double topNonBpMb = 0;
+
+ for (int i = 0; i < selected.Count; i++)
+ {
+ var trend = await _dataService.GetMemoryClerkTrendAsync(_serverId, selected[i].DisplayName, hoursBack, fromDate, toDate);
+ if (trend.Count == 0) continue;
+
+ var times = trend.Select(t => t.CollectionTime.AddMinutes(UtcOffsetMinutes).ToOADate()).ToArray();
+ var values = trend.Select(t => t.MemoryMb).ToArray();
+
+ var plot = MemoryClerksChart.Plot.Add.Scatter(times, values);
+ plot.LegendText = selected[i].DisplayName;
+ plot.Color = ScottPlot.Color.FromHex(SeriesColors[i % SeriesColors.Length]);
+ _memoryClerksHover?.Add(plot, selected[i].DisplayName);
+
+ if (values.Length > 0) globalMax = Math.Max(globalMax, values.Max());
+
+ /* Summary: use latest value, exclude buffer pool */
+ var latestMb = values.Last();
+ if (!selected[i].DisplayName.Contains("BUFFERPOOL", StringComparison.OrdinalIgnoreCase))
+ {
+ nonBpTotal += latestMb;
+ if (latestMb > topNonBpMb)
+ {
+ topNonBpMb = latestMb;
+ topNonBpClerk = selected[i].DisplayName;
+ }
+ }
+ }
+
+ MemoryClerksChart.Plot.Axes.DateTimeTicksBottom();
+ ReapplyAxisColors(MemoryClerksChart);
+ MemoryClerksChart.Plot.YLabel("Memory (MB)");
+ SetChartYLimitsWithLegendPadding(MemoryClerksChart, 0, globalMax > 0 ? globalMax : 100);
+ ShowChartLegend(MemoryClerksChart);
+ MemoryClerksChart.Refresh();
+
+ /* Update summary panel */
+ MemoryClerksTotalText.Text = nonBpTotal >= 1024 ? $"{nonBpTotal / 1024:F1} GB" : $"{nonBpTotal:N0} MB";
+ if (!string.IsNullOrEmpty(topNonBpClerk))
+ {
+ var name = topNonBpClerk;
+ if (name.StartsWith("MEMORYCLERK_", StringComparison.OrdinalIgnoreCase))
+ name = name.Substring(12);
+ MemoryClerksTopText.Text = topNonBpMb >= 1024 ? $"{name} ({topNonBpMb / 1024:F1} GB)" : $"{name} ({topNonBpMb:N0} MB)";
+ }
+ else
+ {
+ MemoryClerksTopText.Text = "--";
+ }
+ }
+ catch
+ {
+ /* Ignore chart update errors */
+ }
+ }
+
/* ========== Perfmon Picker ========== */
private bool _isUpdatingPerfmonSelection;
diff --git a/Lite/Database/Schema.cs b/Lite/Database/Schema.cs
index 7b3961f5..4336e8b7 100644
--- a/Lite/Database/Schema.cs
+++ b/Lite/Database/Schema.cs
@@ -460,6 +460,9 @@ is_optimized_locking_on BOOLEAN
public const string CreateBlockedProcessReportsIndex = @"
CREATE INDEX IF NOT EXISTS idx_blocked_process_reports_time ON blocked_process_reports(server_id, collection_time)";
+ public const string CreateMemoryClerksIndex = @"
+CREATE INDEX IF NOT EXISTS idx_memory_clerks_time ON memory_clerks(server_id, collection_time)";
+
public const string CreateDatabaseScopedConfigTable = @"
CREATE TABLE IF NOT EXISTS database_scoped_config (
config_id BIGINT PRIMARY KEY,
@@ -575,6 +578,7 @@ public static IEnumerable GetAllIndexStatements()
yield return CreateMemoryGrantStatsIndex;
yield return CreateWaitingTasksIndex;
yield return CreateBlockedProcessReportsIndex;
+ yield return CreateMemoryClerksIndex;
yield return CreateDatabaseScopedConfigIndex;
yield return CreateTraceFlagsIndex;
yield return CreateRunningJobsIndex;
diff --git a/Lite/Services/LocalDataService.Memory.cs b/Lite/Services/LocalDataService.Memory.cs
index 55869c47..3b581927 100644
--- a/Lite/Services/LocalDataService.Memory.cs
+++ b/Lite/Services/LocalDataService.Memory.cs
@@ -108,6 +108,79 @@ FROM v_memory_stats
return items;
}
+ ///
+ /// Gets the distinct memory clerk types collected for a server, ordered by total memory descending.
+ ///
+ public async Task> GetDistinctMemoryClerkTypesAsync(int serverId, int hoursBack = 24, 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
+ clerk_type
+FROM v_memory_clerks
+WHERE server_id = $1
+AND collection_time >= $2
+AND collection_time <= $3
+GROUP BY clerk_type
+ORDER BY SUM(memory_mb) DESC";
+
+ 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();
+ using var reader = await command.ExecuteReaderAsync();
+ while (await reader.ReadAsync())
+ {
+ items.Add(reader.GetString(0));
+ }
+ return items;
+ }
+
+ ///
+ /// Gets memory clerk trend data for a single clerk type for charting.
+ ///
+ public async Task> GetMemoryClerkTrendAsync(int serverId, string clerkType, int hoursBack = 24, 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,
+ memory_mb
+FROM v_memory_clerks
+WHERE server_id = $1
+AND clerk_type = $2
+AND collection_time >= $3
+AND collection_time <= $4
+ORDER BY collection_time";
+
+ command.Parameters.Add(new DuckDBParameter { Value = serverId });
+ command.Parameters.Add(new DuckDBParameter { Value = clerkType });
+ command.Parameters.Add(new DuckDBParameter { Value = startTime });
+ command.Parameters.Add(new DuckDBParameter { Value = endTime });
+
+ var items = new List();
+ using var reader = await command.ExecuteReaderAsync();
+ while (await reader.ReadAsync())
+ {
+ items.Add(new MemoryClerkTrendPoint
+ {
+ CollectionTime = reader.GetDateTime(0),
+ MemoryMb = reader.IsDBNull(1) ? 0 : ToDouble(reader.GetValue(1))
+ });
+ }
+
+ return items;
+ }
+
///
/// Gets the latest memory clerk breakdown.
///
@@ -172,3 +245,10 @@ public class MemoryClerkRow
public double MemoryMb { get; set; }
public string MemoryFormatted => MemoryMb >= 1024 ? $"{MemoryMb / 1024:F1} GB" : $"{MemoryMb:F1} MB";
}
+
+public class MemoryClerkTrendPoint
+{
+ public DateTime CollectionTime { get; set; }
+ public string ClerkType { get; set; } = "";
+ public double MemoryMb { get; set; }
+}