diff --git a/Dashboard/Controls/DefaultTraceContent.xaml b/Dashboard/Controls/DefaultTraceContent.xaml
index f85534d1..eb68731b 100644
--- a/Dashboard/Controls/DefaultTraceContent.xaml
+++ b/Dashboard/Controls/DefaultTraceContent.xaml
@@ -6,118 +6,46 @@
mc:Ignorable="d"
d:DesignHeight="450" d:DesignWidth="800"
Loaded="OnLoaded">
-
-
-
-
-
-
-
-
+
+
+
+
+
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
-
+
-
-
+
+
-
+
@@ -125,68 +53,86 @@
-
+
-
+
+
+
+
+
+
+
+
+
-
+
-
+
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
-
+
-
-
+
+
-
+
-
-
+
+
-
+
-
-
+
+
-
-
+
+
-
-
+
+
-
-
-
-
-
-
-
+
+
-
-
-
-
+
+
+
diff --git a/Dashboard/Controls/DefaultTraceContent.xaml.cs b/Dashboard/Controls/DefaultTraceContent.xaml.cs
index 44a68229..fae44462 100644
--- a/Dashboard/Controls/DefaultTraceContent.xaml.cs
+++ b/Dashboard/Controls/DefaultTraceContent.xaml.cs
@@ -28,21 +28,13 @@ public partial class DefaultTraceContent : UserControl
private DateTime? _defaultTraceEventsToDate;
private string? _defaultTraceEventsFilter;
- private int _traceAnalysisHoursBack = 24;
- private DateTime? _traceAnalysisFromDate;
- private DateTime? _traceAnalysisToDate;
-
- // Popup filter state (shared popup, per-grid filter dictionaries)
+ // Popup filter state
private Popup? _filterPopup;
private ColumnFilterPopup? _filterPopupContent;
- private string? _activeFilterGrid;
private readonly Dictionary _defaultTraceFilters = new();
private List? _defaultTraceUnfilteredData;
- private readonly Dictionary _traceAnalysisFilters = new();
- private List? _traceAnalysisUnfilteredData;
-
public DefaultTraceContent()
{
InitializeComponent();
@@ -58,28 +50,19 @@ public void SetTimeRange(int hoursBack, DateTime? fromDate = null, DateTime? toD
_defaultTraceEventsHoursBack = hoursBack;
_defaultTraceEventsFromDate = fromDate;
_defaultTraceEventsToDate = toDate;
-
- _traceAnalysisHoursBack = hoursBack;
- _traceAnalysisFromDate = fromDate;
- _traceAnalysisToDate = toDate;
}
public async Task RefreshAllDataAsync()
{
if (_databaseService == null) return;
- await Task.WhenAll(
- RefreshDefaultTraceEventsAsync(),
- RefreshTraceAnalysisAsync()
- );
+ await RefreshDefaultTraceEventsAsync();
}
private void OnLoaded(object sender, RoutedEventArgs e)
{
TabHelpers.AutoSizeColumnMinWidths(DefaultTraceEventsDataGrid);
- TabHelpers.AutoSizeColumnMinWidths(TraceAnalysisDataGrid);
TabHelpers.FreezeColumns(DefaultTraceEventsDataGrid, 1);
- TabHelpers.FreezeColumns(TraceAnalysisDataGrid, 1);
}
#region Default Trace Events
@@ -130,45 +113,14 @@ private async Task RefreshDefaultTraceEventsAsync()
#endregion
- #region Trace Analysis
-
- private async Task RefreshTraceAnalysisAsync()
- {
- if (_databaseService == null) return;
-
- try
- {
- var data = await _databaseService.GetTraceAnalysisAsync(_traceAnalysisHoursBack, _traceAnalysisFromDate, _traceAnalysisToDate);
- _traceAnalysisUnfilteredData = data;
- _traceAnalysisFilters.Clear();
- TraceAnalysisDataGrid.ItemsSource = data;
- TraceAnalysisNoDataMessage.Visibility = data.Count == 0 ? Visibility.Visible : Visibility.Collapsed;
- UpdateFilterButtonStyles(TraceAnalysisDataGrid, _traceAnalysisFilters);
- }
- catch (Exception ex)
- {
- Logger.Error($"Error loading trace analysis: {ex.Message}");
- }
- }
-
- #endregion
-
#region Popup Filter Infrastructure
private void DefaultTraceFilter_Click(object sender, RoutedEventArgs e)
{
- _activeFilterGrid = "DefaultTrace";
if (sender is Button button && button.Tag is string columnName)
ShowFilterPopup(button, columnName, _defaultTraceFilters);
}
- private void TraceAnalysisFilter_Click(object sender, RoutedEventArgs e)
- {
- _activeFilterGrid = "TraceAnalysis";
- if (sender is Button button && button.Tag is string columnName)
- ShowFilterPopup(button, columnName, _traceAnalysisFilters);
- }
-
private void ShowFilterPopup(Button button, string columnName, Dictionary filters)
{
if (_filterPopup == null)
@@ -197,26 +149,12 @@ private void FilterPopup_FilterApplied(object? sender, FilterAppliedEventArgs e)
if (_filterPopup != null)
_filterPopup.IsOpen = false;
- switch (_activeFilterGrid)
- {
- case "DefaultTrace":
- if (e.FilterState.IsActive)
- _defaultTraceFilters[e.FilterState.ColumnName] = e.FilterState;
- else
- _defaultTraceFilters.Remove(e.FilterState.ColumnName);
- ApplyFilters(_defaultTraceFilters, _defaultTraceUnfilteredData, DefaultTraceEventsDataGrid, DefaultTraceEventsNoDataMessage);
- UpdateFilterButtonStyles(DefaultTraceEventsDataGrid, _defaultTraceFilters);
- break;
-
- case "TraceAnalysis":
- if (e.FilterState.IsActive)
- _traceAnalysisFilters[e.FilterState.ColumnName] = e.FilterState;
- else
- _traceAnalysisFilters.Remove(e.FilterState.ColumnName);
- ApplyFilters(_traceAnalysisFilters, _traceAnalysisUnfilteredData, TraceAnalysisDataGrid, TraceAnalysisNoDataMessage);
- UpdateFilterButtonStyles(TraceAnalysisDataGrid, _traceAnalysisFilters);
- break;
- }
+ if (e.FilterState.IsActive)
+ _defaultTraceFilters[e.FilterState.ColumnName] = e.FilterState;
+ else
+ _defaultTraceFilters.Remove(e.FilterState.ColumnName);
+ ApplyFilters(_defaultTraceFilters, _defaultTraceUnfilteredData, DefaultTraceEventsDataGrid, DefaultTraceEventsNoDataMessage);
+ UpdateFilterButtonStyles(DefaultTraceEventsDataGrid, _defaultTraceFilters);
}
private void FilterPopup_FilterCleared(object? sender, EventArgs e)
diff --git a/Dashboard/Controls/MemoryContent.xaml b/Dashboard/Controls/MemoryContent.xaml
index ce1a6f95..41808264 100644
--- a/Dashboard/Controls/MemoryContent.xaml
+++ b/Dashboard/Controls/MemoryContent.xaml
@@ -133,28 +133,66 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/Dashboard/Controls/MemoryContent.xaml.cs b/Dashboard/Controls/MemoryContent.xaml.cs
index f3ca9334..f78b0ab0 100644
--- a/Dashboard/Controls/MemoryContent.xaml.cs
+++ b/Dashboard/Controls/MemoryContent.xaml.cs
@@ -62,6 +62,10 @@ public partial class MemoryContent : UserControl
// Legend panel references for edge-based legends (ScottPlot issue #4717 workaround)
private Dictionary _legendPanels = new();
+ // Memory Clerks picker state
+ private List _memoryClerkItems = new();
+ private bool _isUpdatingMemoryClerkSelection;
+
// Chart hover tooltips
private Helpers.ChartHoverHelper? _memoryStatsOverviewHover;
private Helpers.ChartHoverHelper? _memoryGrantSizingHover;
@@ -423,15 +427,12 @@ private async System.Threading.Tasks.Task RefreshMemoryGrantsAsync()
var data = await _databaseService.GetMemoryGrantStatsAsync(_memoryGrantsHoursBack, _memoryGrantsFromDate, _memoryGrantsToDate);
var dataList = data.ToList();
- // Filter to rows with active grants for charting
- var filtered = dataList.Where(d => (d.GrantedMemoryMb ?? 0) > 0).ToList();
-
- bool hasData = filtered.Count > 0;
+ bool hasData = dataList.Count > 0;
MemoryGrantSizingNoData.Visibility = hasData ? Visibility.Collapsed : Visibility.Visible;
MemoryGrantActivityNoData.Visibility = hasData ? Visibility.Collapsed : Visibility.Visible;
// Aggregate across resource_semaphore_id within each pool
- var aggregated = filtered
+ var aggregated = dataList
.GroupBy(d => new { d.CollectionTime, d.PoolId })
.Select(g => new PoolGrantPoint
{
@@ -613,17 +614,15 @@ private async System.Threading.Tasks.Task RefreshMemoryClerksAsync()
try
{
- // Only show loading overlay on initial load (no existing chart data)
if (!MemoryClerksChart.Plot.GetPlottables().Any())
{
MemoryClerksLoading.IsLoading = true;
MemoryClerksNoDataMessage.Visibility = Visibility.Collapsed;
}
- var data = await _databaseService.GetMemoryClerksTopNAsync(5, _memoryClerksHoursBack, _memoryClerksFromDate, _memoryClerksToDate);
- var dataList = data.ToList();
- MemoryClerksNoDataMessage.Visibility = dataList.Count == 0 ? Visibility.Visible : Visibility.Collapsed;
- LoadMemoryClerksChart(dataList, _memoryClerksHoursBack, _memoryClerksFromDate, _memoryClerksToDate);
+ var clerkTypes = await _databaseService.GetDistinctMemoryClerkTypesAsync(_memoryClerksHoursBack, _memoryClerksFromDate, _memoryClerksToDate);
+ PopulateMemoryClerkPicker(clerkTypes);
+ await UpdateMemoryClerksChartFromPickerAsync();
}
catch (Exception ex)
{
@@ -635,85 +634,160 @@ private async System.Threading.Tasks.Task RefreshMemoryClerksAsync()
}
}
- private void LoadMemoryClerksChart(List data, int hoursBack, DateTime? fromDate, DateTime? toDate)
+ private void PopulateMemoryClerkPicker(List clerkTypes)
{
- DateTime rangeEnd = toDate ?? Helpers.ServerTimeHelper.ServerNow;
- DateTime rangeStart = fromDate ?? rangeEnd.AddHours(-hoursBack);
- double xMin = rangeStart.ToOADate();
- double xMax = rangeEnd.ToOADate();
-
- if (_legendPanels.TryGetValue(MemoryClerksChart, out var existingPanel) && existingPanel != null)
+ 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
{
- MemoryClerksChart.Plot.Axes.Remove(existingPanel);
- _legendPanels[MemoryClerksChart] = null;
- }
- MemoryClerksChart.Plot.Clear();
- _memoryClerksHover?.Clear();
- TabHelpers.ApplyDarkModeToChart(MemoryClerksChart);
+ DisplayName = c,
+ IsSelected = previouslySelected.Contains(c) || (topClerks != null && topClerks.Contains(c))
+ }).ToList();
+ RefreshMemoryClerkListOrder();
+ }
- var dataList = data ?? new List();
- if (dataList.Count > 0)
+ 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()
+ {
+ if (_databaseService == null) return;
+
+ try
{
- // Get all unique time points for gap filling
- // Get top 5 clerk types by total pages
- var topClerks = dataList.GroupBy(d => d.ClerkType)
- .Select(g => new { ClerkType = g.Key, TotalPages = g.Sum(x => x.PagesKb ?? 0) })
- .OrderByDescending(x => x.TotalPages)
- .Take(5)
- .Select(x => x.ClerkType)
- .ToList();
+ var selected = _memoryClerkItems.Where(i => i.IsSelected).Take(20).ToList();
- var colors = TabHelpers.ChartColors;
- int colorIndex = 0;
+ if (_legendPanels.TryGetValue(MemoryClerksChart, out var existingPanel) && existingPanel != null)
+ {
+ MemoryClerksChart.Plot.Axes.Remove(existingPanel);
+ _legendPanels[MemoryClerksChart] = null;
+ }
+ MemoryClerksChart.Plot.Clear();
+ _memoryClerksHover?.Clear();
+ TabHelpers.ApplyDarkModeToChart(MemoryClerksChart);
- foreach (var clerkType in topClerks)
+ DateTime rangeEnd = _memoryClerksToDate ?? Helpers.ServerTimeHelper.ServerNow;
+ DateTime rangeStart = _memoryClerksFromDate ?? rangeEnd.AddHours(-_memoryClerksHoursBack);
+ double xMin = rangeStart.ToOADate();
+ double xMax = rangeEnd.ToOADate();
+
+ if (selected.Count > 0)
{
- var clerkData = dataList.Where(d => d.ClerkType == clerkType)
- .OrderBy(d => d.CollectionTime)
- .ToList();
+ var selectedTypes = selected.Select(s => s.DisplayName).ToList();
+ var data = await _databaseService.GetMemoryClerksByTypesAsync(selectedTypes, _memoryClerksHoursBack, _memoryClerksFromDate, _memoryClerksToDate);
+ var dataList = data.ToList();
+
+ MemoryClerksNoDataMessage.Visibility = dataList.Count == 0 ? Visibility.Visible : Visibility.Collapsed;
- if (clerkData.Count >= 1)
+ if (dataList.Count > 0)
{
- var timePoints = clerkData.Select(d => d.CollectionTime);
- var values = clerkData.Select(d => (double)d.PagesMb);
- var (xs, ys) = TabHelpers.FillTimeSeriesGaps(timePoints, values);
+ var colors = TabHelpers.ChartColors;
+ int colorIndex = 0;
- var scatter = MemoryClerksChart.Plot.Add.Scatter(xs, ys);
- scatter.LineWidth = 2;
- scatter.MarkerSize = 5;
- scatter.Color = colors[colorIndex % colors.Length];
- var label = clerkType.Length > 20 ? clerkType.Substring(0, 20) + "..." : clerkType;
- scatter.LegendText = label;
- _memoryClerksHover?.Add(scatter, label);
- colorIndex++;
+ foreach (var clerkType in selectedTypes)
+ {
+ var clerkData = dataList.Where(d => d.ClerkType == clerkType)
+ .OrderBy(d => d.CollectionTime)
+ .ToList();
+
+ if (clerkData.Count >= 1)
+ {
+ var timePoints = clerkData.Select(d => d.CollectionTime);
+ var values = clerkData.Select(d => (double)d.PagesMb);
+ var (xs, ys) = TabHelpers.FillTimeSeriesGaps(timePoints, values);
+
+ var scatter = MemoryClerksChart.Plot.Add.Scatter(xs, ys);
+ scatter.LineWidth = 2;
+ scatter.MarkerSize = 5;
+ scatter.Color = colors[colorIndex % colors.Length];
+ var label = clerkType.Length > 20 ? clerkType.Substring(0, 20) + "..." : clerkType;
+ scatter.LegendText = label;
+ _memoryClerksHover?.Add(scatter, label);
+ colorIndex++;
+ }
+ }
+
+ _legendPanels[MemoryClerksChart] = MemoryClerksChart.Plot.ShowLegend(ScottPlot.Edge.Bottom);
+ MemoryClerksChart.Plot.Legend.FontSize = 12;
}
+
+ UpdateMemoryClerksSummaryPanel(dataList);
+ }
+ else
+ {
+ MemoryClerksNoDataMessage.Visibility = Visibility.Collapsed;
+ MemoryClerksTotalText.Text = "N/A";
+ MemoryClerksTopText.Text = "N/A";
}
- _legendPanels[MemoryClerksChart] = MemoryClerksChart.Plot.ShowLegend(ScottPlot.Edge.Bottom);
- MemoryClerksChart.Plot.Legend.FontSize = 12;
+ MemoryClerksChart.Plot.Axes.DateTimeTicksBottom();
+ MemoryClerksChart.Plot.Axes.SetLimitsX(xMin, xMax);
+ MemoryClerksChart.Plot.YLabel("MB");
+ MemoryClerksChart.Plot.Axes.AutoScaleY();
+ var clerksLimits = MemoryClerksChart.Plot.Axes.GetLimits();
+ MemoryClerksChart.Plot.Axes.SetLimitsY(0, clerksLimits.Top * 1.05);
+ TabHelpers.LockChartVerticalAxis(MemoryClerksChart);
+ MemoryClerksChart.Refresh();
}
- else
+ catch (Exception ex)
{
- double xCenter = xMin + (xMax - xMin) / 2;
- var noDataText = MemoryClerksChart.Plot.Add.Text("No data for selected time range", xCenter, 0.5);
- noDataText.LabelFontSize = 14;
- noDataText.LabelFontColor = ScottPlot.Colors.Gray;
- noDataText.LabelAlignment = ScottPlot.Alignment.MiddleCenter;
+ Logger.Error($"Error updating memory clerks chart: {ex.Message}");
}
-
- MemoryClerksChart.Plot.Axes.DateTimeTicksBottom();
- MemoryClerksChart.Plot.Axes.SetLimitsX(xMin, xMax);
- MemoryClerksChart.Plot.YLabel("MB");
- // Fixed negative space for legend
- MemoryClerksChart.Plot.Axes.AutoScaleY();
- var clerksLimits = MemoryClerksChart.Plot.Axes.GetLimits();
- MemoryClerksChart.Plot.Axes.SetLimitsY(0, clerksLimits.Top * 1.05);
-
- TabHelpers.LockChartVerticalAxis(MemoryClerksChart);
- MemoryClerksChart.Refresh();
-
- // Update summary panel
- UpdateMemoryClerksSummaryPanel(dataList);
}
private void UpdateMemoryClerksSummaryPanel(List dataList)
@@ -725,23 +799,19 @@ private void UpdateMemoryClerksSummaryPanel(List dataList)
return;
}
- // Get the latest collection time's data, excluding buffer pool
var latestTime = dataList.Max(d => d.CollectionTime);
var latestData = dataList
.Where(d => d.CollectionTime == latestTime)
.Where(d => d.ClerkType == null || !d.ClerkType.Contains("BUFFERPOOL", StringComparison.OrdinalIgnoreCase))
.ToList();
- // Total non-buffer pool memory
var totalMb = latestData.Sum(d => d.PagesMb);
MemoryClerksTotalText.Text = string.Format(CultureInfo.CurrentCulture, "{0:N0} MB", totalMb);
- // Top non-buffer pool clerk by size
var topClerk = latestData.OrderByDescending(d => d.PagesMb).FirstOrDefault();
if (topClerk != null)
{
var name = topClerk.ClerkType ?? "Unknown";
- // Remove MEMORYCLERK_ prefix for readability
if (name.StartsWith("MEMORYCLERK_", StringComparison.OrdinalIgnoreCase))
name = name.Substring(12);
if (name.Length > 20) name = name.Substring(0, 20) + "...";
diff --git a/Dashboard/Controls/QueryPerformanceContent.xaml.cs b/Dashboard/Controls/QueryPerformanceContent.xaml.cs
index 5e610547..0a887bc8 100644
--- a/Dashboard/Controls/QueryPerformanceContent.xaml.cs
+++ b/Dashboard/Controls/QueryPerformanceContent.xaml.cs
@@ -812,6 +812,16 @@ private async void ViewEstimatedPlan_Click(object sender, RoutedEventArgs e)
break;
}
+ if (planXml == null && item is LongRunningQueryPatternItem)
+ {
+ MessageBox.Show(
+ "Query trace patterns are aggregated data with no cached plan. Use 'Get Actual Plan' to generate one.",
+ "No Cached Plan",
+ MessageBoxButton.OK,
+ MessageBoxImage.Information);
+ return;
+ }
+
if (planXml != null)
{
ViewPlanRequested?.Invoke(planXml, label, queryText);
@@ -875,6 +885,11 @@ private async void GetActualPlan_Click(object sender, RoutedEventArgs e)
planXml = reg.QueryPlanXml;
label = $"Actual Plan - QS {reg.QueryId}";
break;
+ case LongRunningQueryPatternItem lrq:
+ queryText = lrq.SampleQueryText;
+ databaseName = lrq.DatabaseName;
+ label = $"Actual Plan - Pattern";
+ break;
}
if (string.IsNullOrWhiteSpace(queryText))
diff --git a/Dashboard/Models/DefaultTraceEventItem.cs b/Dashboard/Models/DefaultTraceEventItem.cs
index 7fab7eba..0140ea65 100644
--- a/Dashboard/Models/DefaultTraceEventItem.cs
+++ b/Dashboard/Models/DefaultTraceEventItem.cs
@@ -28,5 +28,11 @@ public class DefaultTraceEventItem
public long? EventSequence { get; set; }
public bool? IsSystem { get; set; }
public int? RequestId { get; set; }
+ public long? DurationUs { get; set; }
+ public DateTime? EndTime { get; set; }
+
+ // Display helpers
+ public decimal? DurationMs => DurationUs.HasValue ? DurationUs.Value / 1000.0m : null;
+ public decimal? GrowthMb => IntegerData.HasValue ? IntegerData.Value * 8.0m / 1024.0m : null;
}
}
diff --git a/Dashboard/Models/SelectableItem.cs b/Dashboard/Models/SelectableItem.cs
new file mode 100644
index 00000000..84210a50
--- /dev/null
+++ b/Dashboard/Models/SelectableItem.cs
@@ -0,0 +1,8 @@
+namespace PerformanceMonitorDashboard.Models
+{
+ public class SelectableItem
+ {
+ public string DisplayName { get; set; } = "";
+ public bool IsSelected { get; set; }
+ }
+}
diff --git a/Dashboard/Services/DatabaseService.Memory.cs b/Dashboard/Services/DatabaseService.Memory.cs
index b0ac827f..fed3cf45 100644
--- a/Dashboard/Services/DatabaseService.Memory.cs
+++ b/Dashboard/Services/DatabaseService.Memory.cs
@@ -463,6 +463,150 @@ ORDER BY
});
}
+ return items;
+ }
+
+ ///
+ /// Gets distinct memory clerk types ordered by total pages descending.
+ ///
+ public async Task> GetDistinctMemoryClerkTypesAsync(int hoursBack = 24, DateTime? fromDate = null, DateTime? toDate = null)
+ {
+ var items = new List();
+
+ await using var tc = await OpenThrottledConnectionAsync();
+ var connection = tc.Connection;
+
+ string dateFilter = fromDate.HasValue && toDate.HasValue
+ ? "WHERE mcs.collection_time >= @fromDate AND mcs.collection_time <= @toDate"
+ : "WHERE mcs.collection_time >= DATEADD(HOUR, -@hoursBack, SYSDATETIME())";
+
+ string query = $@"SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED;
+
+ SELECT
+ mcs.clerk_type
+ FROM collect.memory_clerks_stats AS mcs
+ {dateFilter}
+ GROUP BY
+ mcs.clerk_type
+ ORDER BY
+ SUM(mcs.pages_kb) DESC;";
+
+ using var command = new SqlCommand(query, connection);
+ command.CommandTimeout = 120;
+
+ if (fromDate.HasValue && toDate.HasValue)
+ {
+ command.Parameters.Add(new SqlParameter("@fromDate", SqlDbType.DateTime2) { Value = fromDate.Value });
+ command.Parameters.Add(new SqlParameter("@toDate", SqlDbType.DateTime2) { Value = toDate.Value });
+ }
+ else
+ {
+ command.Parameters.Add(new SqlParameter("@hoursBack", SqlDbType.Int) { Value = hoursBack });
+ }
+
+ using var reader = await command.ExecuteReaderAsync();
+ while (await reader.ReadAsync())
+ {
+ items.Add(reader.GetString(0));
+ }
+
+ return items;
+ }
+
+ ///
+ /// Gets memory clerk stats for specific clerk types.
+ ///
+ public async Task> GetMemoryClerksByTypesAsync(List clerkTypes, int hoursBack = 24, DateTime? fromDate = null, DateTime? toDate = null)
+ {
+ var items = new List();
+ if (clerkTypes == null || clerkTypes.Count == 0) return items;
+
+ await using var tc = await OpenThrottledConnectionAsync();
+ var connection = tc.Connection;
+
+ string dateFilter = fromDate.HasValue && toDate.HasValue
+ ? "mcs.collection_time >= @fromDate AND mcs.collection_time <= @toDate"
+ : "mcs.collection_time >= DATEADD(HOUR, -@hoursBack, SYSDATETIME())";
+
+ // Build parameterized IN list
+ var paramNames = new List();
+ for (int i = 0; i < clerkTypes.Count; i++)
+ paramNames.Add($"@ct{i}");
+
+ string query = $@"SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED;
+
+ SELECT
+ mcs.collection_id,
+ mcs.collection_time,
+ mcs.clerk_type,
+ mcs.memory_node_id,
+ mcs.pages_kb,
+ mcs.virtual_memory_reserved_kb,
+ mcs.virtual_memory_committed_kb,
+ mcs.awe_allocated_kb,
+ mcs.shared_memory_reserved_kb,
+ mcs.shared_memory_committed_kb,
+ mcs.pages_kb_delta,
+ mcs.virtual_memory_reserved_kb_delta,
+ mcs.virtual_memory_committed_kb_delta,
+ mcs.awe_allocated_kb_delta,
+ mcs.shared_memory_reserved_kb_delta,
+ mcs.shared_memory_committed_kb_delta,
+ mcs.sample_interval_seconds,
+ percent_of_total = CONVERT(decimal(5,2), 0),
+ concern_level = N'NORMAL',
+ clerk_description = N''
+ FROM collect.memory_clerks_stats AS mcs
+ WHERE {dateFilter}
+ AND mcs.clerk_type IN ({string.Join(", ", paramNames)})
+ ORDER BY
+ mcs.collection_time DESC,
+ mcs.pages_kb DESC;";
+
+ using var command = new SqlCommand(query, connection);
+ command.CommandTimeout = 120;
+
+ if (fromDate.HasValue && toDate.HasValue)
+ {
+ command.Parameters.Add(new SqlParameter("@fromDate", SqlDbType.DateTime2) { Value = fromDate.Value });
+ command.Parameters.Add(new SqlParameter("@toDate", SqlDbType.DateTime2) { Value = toDate.Value });
+ }
+ else
+ {
+ command.Parameters.Add(new SqlParameter("@hoursBack", SqlDbType.Int) { Value = hoursBack });
+ }
+
+ for (int i = 0; i < clerkTypes.Count; i++)
+ command.Parameters.Add(new SqlParameter($"@ct{i}", SqlDbType.NVarChar, 256) { Value = clerkTypes[i] });
+
+ using var reader = await command.ExecuteReaderAsync();
+ while (await reader.ReadAsync())
+ {
+ items.Add(new MemoryClerksItem
+ {
+ CollectionId = reader.GetInt64(0),
+ CollectionTime = reader.GetDateTime(1),
+ ClerkType = reader.GetString(2),
+ MemoryNodeId = reader.GetInt16(3),
+ PagesKb = reader.IsDBNull(4) ? null : reader.GetInt64(4),
+ VirtualMemoryReservedKb = reader.IsDBNull(5) ? null : reader.GetInt64(5),
+ VirtualMemoryCommittedKb = reader.IsDBNull(6) ? null : reader.GetInt64(6),
+ AweAllocatedKb = reader.IsDBNull(7) ? null : reader.GetInt64(7),
+ SharedMemoryReservedKb = reader.IsDBNull(8) ? null : reader.GetInt64(8),
+ SharedMemoryCommittedKb = reader.IsDBNull(9) ? null : reader.GetInt64(9),
+ PagesKbDelta = reader.IsDBNull(10) ? null : reader.GetInt64(10),
+ VirtualMemoryReservedKbDelta = reader.IsDBNull(11) ? null : reader.GetInt64(11),
+ VirtualMemoryCommittedKbDelta = reader.IsDBNull(12) ? null : reader.GetInt64(12),
+ AweAllocatedKbDelta = reader.IsDBNull(13) ? null : reader.GetInt64(13),
+ SharedMemoryReservedKbDelta = reader.IsDBNull(14) ? null : reader.GetInt64(14),
+ SharedMemoryCommittedKbDelta = reader.IsDBNull(15) ? null : reader.GetInt64(15),
+ SampleIntervalSeconds = reader.IsDBNull(16) ? null : reader.GetInt32(16),
+ PercentOfTotal = reader.IsDBNull(17) ? null : reader.GetDecimal(17),
+ ConcernLevel = reader.GetString(18),
+ ClerkDescription = reader.GetString(19)
+ });
+ }
+
return items;
}
diff --git a/Dashboard/Services/DatabaseService.QueryPerformance.cs b/Dashboard/Services/DatabaseService.QueryPerformance.cs
index 561992e1..88e90457 100644
--- a/Dashboard/Services/DatabaseService.QueryPerformance.cs
+++ b/Dashboard/Services/DatabaseService.QueryPerformance.cs
@@ -1455,7 +1455,7 @@ ELSE N'INFO'
WHEN avg_writes > 100000 THEN N'High write volume - review update/delete patterns'
ELSE N'Review execution plan for optimization opportunities'
END,
- sample_query_text = CONVERT(nvarchar(500), sample_query_text),
+ sample_query_text,
last_execution
FROM query_patterns
WHERE executions > 1
diff --git a/Dashboard/Services/DatabaseService.SystemEvents.cs b/Dashboard/Services/DatabaseService.SystemEvents.cs
index 114dfe1b..d326d31b 100644
--- a/Dashboard/Services/DatabaseService.SystemEvents.cs
+++ b/Dashboard/Services/DatabaseService.SystemEvents.cs
@@ -64,7 +64,9 @@ public async Task> GetDefaultTraceEventsAsync(int ho
dte.state,
dte.event_sequence,
dte.is_system,
- dte.request_id
+ dte.request_id,
+ dte.duration_us,
+ dte.end_time
FROM collect.default_trace_events AS dte
{dateFilter}{eventFilter}
ORDER BY
@@ -116,7 +118,9 @@ ORDER BY
State = reader.IsDBNull(20) ? null : reader.GetInt32(20),
EventSequence = reader.IsDBNull(21) ? null : reader.GetInt64(21),
IsSystem = reader.IsDBNull(22) ? null : reader.GetBoolean(22),
- RequestId = reader.IsDBNull(23) ? null : reader.GetInt32(23)
+ RequestId = reader.IsDBNull(23) ? null : reader.GetInt32(23),
+ DurationUs = reader.IsDBNull(24) ? null : reader.GetInt64(24),
+ EndTime = reader.IsDBNull(25) ? null : reader.GetDateTime(25)
});
}
diff --git a/Lite/Services/LocalDataService.MemoryGrants.cs b/Lite/Services/LocalDataService.MemoryGrants.cs
index eb93033f..d9d50686 100644
--- a/Lite/Services/LocalDataService.MemoryGrants.cs
+++ b/Lite/Services/LocalDataService.MemoryGrants.cs
@@ -82,7 +82,6 @@ FROM memory_grant_stats
WHERE server_id = $1
AND collection_time >= $2
AND collection_time <= $3
-AND granted_memory_mb > 0
GROUP BY collection_time, pool_id
ORDER BY collection_time, pool_id";
diff --git a/install/02_create_tables.sql b/install/02_create_tables.sql
index 9907bc09..4c2cb619 100644
--- a/install/02_create_tables.sql
+++ b/install/02_create_tables.sql
@@ -619,11 +619,13 @@ BEGIN
event_sequence bigint NULL,
is_system bit NULL,
request_id integer NULL,
- CONSTRAINT
- PK_default_trace_events
+ duration_us bigint NULL,
+ end_time datetime2(7) NULL,
+ CONSTRAINT
+ PK_default_trace_events
PRIMARY KEY
- (collection_time, event_id)
- WITH
+ (collection_time, event_id)
+ WITH
(DATA_COMPRESSION = PAGE)
);
diff --git a/install/06_ensure_collection_table.sql b/install/06_ensure_collection_table.sql
index fd0a2c8a..024d693a 100644
--- a/install/06_ensure_collection_table.sql
+++ b/install/06_ensure_collection_table.sql
@@ -620,7 +620,9 @@ BEGIN
event_sequence bigint NULL,
is_system bit NULL,
request_id integer NULL,
- CONSTRAINT PK_default_trace_events
+ duration_us bigint NULL,
+ end_time datetime2(7) NULL,
+ CONSTRAINT PK_default_trace_events
PRIMARY KEY CLUSTERED
(collection_time, event_id) WITH (DATA_COMPRESSION = PAGE)
);
diff --git a/install/29_collect_default_trace.sql b/install/29_collect_default_trace.sql
index d889e744..9760fab6 100644
--- a/install/29_collect_default_trace.sql
+++ b/install/29_collect_default_trace.sql
@@ -31,6 +31,9 @@ ALTER PROCEDURE
@include_memory_events bit = 1, /*include Server Memory Change events*/
@include_autogrow_events bit = 1, /*include Database Auto Grow/Shrink events*/
@include_config_events bit = 1, /*include configuration change events*/
+ @include_errorlog_events bit = 1, /*include ErrorLog events*/
+ @include_object_events bit = 1, /*include Object Created/Altered/Deleted events*/
+ @include_audit_events bit = 1, /*include Security Audit events*/
@debug bit = 0 /*prints additional diagnostic information*/
)
WITH RECOMPILE
@@ -321,7 +324,9 @@ BEGIN
state,
event_sequence,
is_system,
- request_id
+ request_id,
+ duration_us,
+ end_time
)
SELECT
collection_time = @collection_start_time,
@@ -347,7 +352,9 @@ BEGIN
state = ft.State,
event_sequence = ft.EventSequence,
is_system = ft.IsSystem,
- request_id = ft.RequestID
+ request_id = ft.RequestID,
+ duration_us = ft.Duration,
+ end_time = ft.EndTime
FROM sys.traces AS st
CROSS APPLY sys.fn_trace_gettable
(
@@ -370,19 +377,36 @@ BEGIN
/*Server Memory Change events*/
(@include_memory_events = 1 AND te.name LIKE N'%Server Memory Change%')
OR
- /*Database Auto Grow/Shrink events*/
- (@include_autogrow_events = 1 AND
- (te.name LIKE N'%Data File Auto Grow%' OR
+ /*Database Auto Grow/Shrink events (only collect if duration > 1 second)*/
+ (@include_autogrow_events = 1 AND
+ ISNULL(ft.Duration, 0) > 1000000 AND
+ (te.name LIKE N'%Data File Auto Grow%' OR
te.name LIKE N'%Log File Auto Grow%' OR
- te.name LIKE N'%Data File Auto Shrink%' OR
+ te.name LIKE N'%Data File Auto Shrink%' OR
te.name LIKE N'%Log File Auto Shrink%'))
OR
/*Configuration change events*/
- (@include_config_events = 1 AND
+ (@include_config_events = 1 AND
(te.name LIKE N'%Server Configuration Change%' OR
te.name LIKE N'%Database Configuration Change%' OR
te.name LIKE N'%Alter Database%'))
-
+ OR
+ /*ErrorLog events*/
+ (@include_errorlog_events = 1 AND te.name = N'ErrorLog')
+ OR
+ /*Object DDL events (exclude tempdb and auto-stats)*/
+ (@include_object_events = 1 AND
+ ISNULL(ft.DatabaseID, 0) <> 2 AND
+ ISNULL(ft.ObjectName, N'') NOT LIKE N'[_]WA[_]%' AND
+ (te.name = N'Object:Created' OR
+ te.name = N'Object:Altered' OR
+ te.name = N'Object:Deleted'))
+ OR
+ /*Security Audit events*/
+ (@include_audit_events = 1 AND
+ (te.name = N'Audit Change Audit Event' OR
+ te.name = N'Audit DBCC Event' OR
+ te.name = N'Audit Server Alter Trace Event'))
)
/*
Avoid duplicates by checking if we've already processed this event
@@ -443,6 +467,9 @@ BEGIN
include_memory_events = @include_memory_events,
include_autogrow_events = @include_autogrow_events,
include_config_events = @include_config_events,
+ include_errorlog_events = @include_errorlog_events,
+ include_object_events = @include_object_events,
+ include_audit_events = @include_audit_events,
success = 1;
END TRY
diff --git a/upgrades/1.3.0-to-2.0.0/03_default_trace_schema.sql b/upgrades/1.3.0-to-2.0.0/03_default_trace_schema.sql
new file mode 100644
index 00000000..f75d0979
--- /dev/null
+++ b/upgrades/1.3.0-to-2.0.0/03_default_trace_schema.sql
@@ -0,0 +1,60 @@
+/*
+Copyright 2026 Darling Data, LLC
+https://www.erikdarling.com/
+
+Upgrade from 1.3.0 to 2.0.0
+Adds duration_us and end_time columns to collect.default_trace_events
+for autogrow duration tracking and event completion times.
+*/
+
+SET ANSI_NULLS ON;
+SET ANSI_PADDING ON;
+SET ANSI_WARNINGS ON;
+SET ARITHABORT ON;
+SET CONCAT_NULL_YIELDS_NULL ON;
+SET QUOTED_IDENTIFIER ON;
+SET NUMERIC_ROUNDABORT OFF;
+SET IMPLICIT_TRANSACTIONS OFF;
+SET STATISTICS TIME, IO OFF;
+GO
+
+USE PerformanceMonitor;
+GO
+
+/* Add duration_us for autogrow/shrink I/O stall duration (microseconds) */
+IF NOT EXISTS
+(
+ SELECT
+ 1/0
+ FROM sys.columns
+ WHERE object_id = OBJECT_ID(N'collect.default_trace_events')
+ AND name = N'duration_us'
+)
+BEGIN
+ ALTER TABLE
+ collect.default_trace_events
+ ADD
+ duration_us bigint NULL;
+
+ PRINT 'Added duration_us to collect.default_trace_events';
+END;
+GO
+
+/* Add end_time for event completion timestamp */
+IF NOT EXISTS
+(
+ SELECT
+ 1/0
+ FROM sys.columns
+ WHERE object_id = OBJECT_ID(N'collect.default_trace_events')
+ AND name = N'end_time'
+)
+BEGIN
+ ALTER TABLE
+ collect.default_trace_events
+ ADD
+ end_time datetime2(7) NULL;
+
+ PRINT 'Added end_time to collect.default_trace_events';
+END;
+GO
diff --git a/upgrades/1.3.0-to-2.0.0/upgrade.txt b/upgrades/1.3.0-to-2.0.0/upgrade.txt
index 4b7bdf58..7568d3c7 100644
--- a/upgrades/1.3.0-to-2.0.0/upgrade.txt
+++ b/upgrades/1.3.0-to-2.0.0/upgrade.txt
@@ -1,2 +1,3 @@
01_memory_grant_stats_schema.sql
02_session_wait_stats_cleanup.sql
+03_default_trace_schema.sql