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
58 changes: 58 additions & 0 deletions Lite/Controls/ServerTab.xaml
Original file line number Diff line number Diff line change
Expand Up @@ -565,6 +565,64 @@
</Grid>
</TabItem>

<!-- Memory Clerks Sub-Tab -->
<TabItem Header="Memory Clerks">
<Grid Margin="8">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="220"/>
<ColumnDefinition Width="*"/>
</Grid.ColumnDefinitions>

<!-- Clerk Type Picker -->
<Border Grid.Column="0" Background="{StaticResource BackgroundDarkBrush}" CornerRadius="4" Padding="8" Margin="0,0,4,0">
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
<RowDefinition Height="*"/>
</Grid.RowDefinitions>
<TextBlock Grid.Row="0" Text="Select Memory Clerks" FontWeight="SemiBold" Margin="0,0,0,4"/>
<StackPanel Grid.Row="1" Orientation="Horizontal" Margin="0,0,0,4">
<Button Content="Top Clerks" Click="MemoryClerkSelectTop_Click" Padding="6,2" Margin="0,0,4,0" FontSize="11"/>
<Button Content="Clear All" Click="MemoryClerkClearAll_Click" Padding="6,2" FontSize="11"/>
<TextBlock x:Name="MemoryClerkCountText" Text="0 selected" FontSize="10" Foreground="{StaticResource ForegroundMutedBrush}" VerticalAlignment="Center" Margin="8,0,0,0"/>
</StackPanel>
<TextBox Grid.Row="2" x:Name="MemoryClerkSearchBox" TextChanged="MemoryClerkSearch_TextChanged"
Margin="0,0,0,4" Padding="4,2"/>
<ListBox Grid.Row="3" x:Name="MemoryClerksList" Background="Transparent" BorderThickness="0">
<ListBox.ItemTemplate>
<DataTemplate>
<CheckBox IsChecked="{Binding IsSelected, Mode=TwoWay}"
Checked="MemoryClerk_CheckChanged" Unchecked="MemoryClerk_CheckChanged"
Foreground="{StaticResource ForegroundBrush}" FontSize="11">
<TextBlock Text="{Binding DisplayName}"/>
</CheckBox>
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
</Grid>
</Border>

<!-- Chart + Summary -->
<Grid Grid.Column="1">
<Grid.RowDefinitions>
<RowDefinition Height="*"/>
<RowDefinition Height="Auto"/>
</Grid.RowDefinitions>
<ScottPlot:WpfPlot Grid.Row="0" x:Name="MemoryClerksChart"/>
<Border Grid.Row="1" Background="{StaticResource BackgroundDarkBrush}" CornerRadius="4" Padding="12,8" Margin="0,8,0,0">
<StackPanel Orientation="Horizontal" HorizontalAlignment="Center">
<TextBlock Text="Non-BP Total:" FontWeight="SemiBold" Foreground="{StaticResource ForegroundDimBrush}" VerticalAlignment="Center" Margin="0,0,5,0"/>
<TextBlock x:Name="MemoryClerksTotalText" Text="--" VerticalAlignment="Center" Margin="0,0,20,0"/>
<TextBlock Text="Top Non-BP Clerk:" FontWeight="SemiBold" Foreground="{StaticResource ForegroundDimBrush}" VerticalAlignment="Center" Margin="0,0,5,0"/>
<TextBlock x:Name="MemoryClerksTopText" Text="--" VerticalAlignment="Center"/>
</StackPanel>
</Border>
</Grid>
</Grid>
</TabItem>

</TabControl>
</TabItem>

Expand Down
177 changes: 176 additions & 1 deletion Lite/Controls/ServerTab.xaml.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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<SelectableItem> _memoryClerkItems = new();
private bool _isUpdatingMemoryClerkSelection;

/* Column filtering */
private Popup? _filterPopup;
Expand Down Expand Up @@ -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 */
Expand Down Expand Up @@ -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);
Expand All @@ -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);
Expand Down Expand Up @@ -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}";
Expand Down Expand Up @@ -1357,6 +1366,172 @@ private async System.Threading.Tasks.Task UpdateWaitStatsChartFromPickerAsync()
}
}

/* ========== Memory Clerks Picker ========== */

private void PopulateMemoryClerkPicker(List<string> clerkTypes)
{
var previouslySelected = new HashSet<string>(_memoryClerkItems.Where(i => i.IsSelected).Select(i => i.DisplayName));
var topClerks = previouslySelected.Count == 0 ? new HashSet<string>(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<string>(_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<SelectableItem>)?.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;
Expand Down
4 changes: 4 additions & 0 deletions Lite/Database/Schema.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -575,6 +578,7 @@ public static IEnumerable<string> GetAllIndexStatements()
yield return CreateMemoryGrantStatsIndex;
yield return CreateWaitingTasksIndex;
yield return CreateBlockedProcessReportsIndex;
yield return CreateMemoryClerksIndex;
yield return CreateDatabaseScopedConfigIndex;
yield return CreateTraceFlagsIndex;
yield return CreateRunningJobsIndex;
Expand Down
80 changes: 80 additions & 0 deletions Lite/Services/LocalDataService.Memory.cs
Original file line number Diff line number Diff line change
Expand Up @@ -108,6 +108,79 @@ FROM v_memory_stats
return items;
}

/// <summary>
/// Gets the distinct memory clerk types collected for a server, ordered by total memory descending.
/// </summary>
public async Task<List<string>> 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<string>();
using var reader = await command.ExecuteReaderAsync();
while (await reader.ReadAsync())
{
items.Add(reader.GetString(0));
}
return items;
}

/// <summary>
/// Gets memory clerk trend data for a single clerk type for charting.
/// </summary>
public async Task<List<MemoryClerkTrendPoint>> 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<MemoryClerkTrendPoint>();
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;
}

/// <summary>
/// Gets the latest memory clerk breakdown.
/// </summary>
Expand Down Expand Up @@ -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; }
}
Loading