diff --git a/CONTEXT.md b/CONTEXT.md new file mode 100644 index 0000000..d6270af --- /dev/null +++ b/CONTEXT.md @@ -0,0 +1,94 @@ +# Session Context — 2026-03-26 08:15 + +## Current Task +Starting #689/#691 — Progressive server summary landing view with USE/RED/Golden Signals framing. These are being implemented together. Design sketch is on issue #691. + +## Decisions Made + +### Session-wide +- Overlay on the slicer canvas, never on ScottPlot charts on other tabs (feedback saved) +- Dots not lines for overlays — every execution gets a dot, no minimum threshold +- Drill-down window ±30 min (±15 was too narrow, missed data due to spike chart zero-baseline rendering) +- Custom date pickers must be populated on drill-down so user can explore other tabs +- `_isRefreshing` guard needed on CustomDateRange_Changed/CustomTimeCombo_Changed/TimeRangeCombo_SelectionChanged to prevent cascading refreshes during programmatic picker updates +- Chart drill-down added to all major charts, skipping Resource Metrics detail charts and Query Performance Trends pending #689/#691 consolidation +- DuckDB path migrated to `%LOCALAPPDATA%\PerformanceMonitorLite\monitor.duckdb` — the old `bin/Debug` path has stale data + +### #689/#691 Design +- Combined implementation: #691 provides framework vocabulary, #689 provides the UI +- Replaces default landing tab (Wait Stats in Lite, Resource Overview in Dashboard) +- All data sources already exist — aggregation + presentation only +- Lite first, then Dashboard port +- Investigate buttons reuse #684 pattern +- USE Method sections: CPU, Memory, Disk I/O, TempDB, Workers (each with Utilization/Saturation/Errors) +- RED Method section: Rate, Errors, Duration +- Recent Incidents section: ranked problems with navigate links +- Full design sketch posted on issue #691 + +## Work Completed This Session + +### Issues closed: +- #676 — CREATE DATABASE model DB size fix (PR #678) +- #677 — Azure SQL DB server_id collision (community PR #680) +- #681 — Slicer time range fixes + new slicers (PRs #697, #698) +- #683 — Grid-to-slicer dot overlay (PRs #699, #700, #701) +- #684 — Critical Issues investigate button (PR #702) +- #682 — Chart drill-down (PRs #705, #706, #708, #709, #711, #714, #717) +- #704 — Slicer custom range display fix (PR #707) +- #694 — Support question answered +- #695 — sp_BlitzLock debugging comment posted + +### Key PRs: +- All merged to dev via squash+admin +- Branch protection requires PRs — cannot push directly to dev + +## Work Remaining + +### Immediate: #689/#691 +- New `ServerSummaryControl` UserControl for Lite +- Data aggregation queries pulling from existing DuckDB tables +- USE/RED framework categorization +- Severity thresholds per signal +- Navigate-to-tab actions (pattern from #684) +- Dashboard port after Lite validation + +### Deferred: +- #686 — Unified query detail panel (too much work, needs #689 first) +- #685 — Inline sparklines in grids +- #687 — Before/after comparison for query grids +- #688 — Correlated timeline lanes +- #690 — Heatmap for query duration +- #692 — Dynamic baselines (foundation for #693) +- #693 — Anomaly detection +- #696 — XE sessions stay running after Lite closes (design choice) +- Dashboard port of remaining Resource Metrics drill-downs (post #689) +- Dashboard chart time display mode for ScottPlot charts (pre-existing issue) + +## Important Context + +### File paths +- Lite slicer: `Lite/Controls/TimeRangeSlicerControl.xaml.cs` +- Dashboard slicer: `Dashboard/Controls/TimeRangeSlicerControl.xaml.cs` +- Lite ServerTab: `Lite/Controls/ServerTab.xaml.cs` (~4500 lines) +- Dashboard ServerTab: `Dashboard/ServerTab.xaml.cs` (~2000 lines) +- Dashboard QueryPerformanceContent: `Dashboard/Controls/QueryPerformanceContent.xaml.cs` +- DuckDB path: `C:/Users/edarl/AppData/Local/PerformanceMonitorLite/monitor.duckdb` (NOT the bin/Debug path) + +### Timezone notes +- Server (sql2022): Pacific time (UTC-7 DST / UTC-8 standard) +- User machine: Eastern time (UTC-4 DST / UTC-5 standard) +- `ServerTimeHelper.UtcOffsetMinutes` is the SERVER's offset from UTC +- Chart X-axis data is in server local time (UTC + UtcOffsetMinutes) +- Hover helper returns server local time +- Date pickers show user's local time +- DuckDB stores collection_time in UTC + +### Git workflow +- ALWAYS use feature branches + PRs (branch protection on dev and main) +- `gh pr merge --squash --admin` to merge +- Never push directly to dev — will be rejected + +### Build issues +- `taskkill` not reliably killing processes this session — Defender or runtime holding file locks +- User has to manually close apps before rebuild +- Dashboard MCP server can also hold DLL locks diff --git a/Lite/Controls/ServerTab.xaml b/Lite/Controls/ServerTab.xaml index 679a877..a2a6407 100644 --- a/Lite/Controls/ServerTab.xaml +++ b/Lite/Controls/ServerTab.xaml @@ -129,6 +129,32 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Lite/Controls/ServerTab.xaml.cs b/Lite/Controls/ServerTab.xaml.cs index 19f47b5..cf6488c 100644 --- a/Lite/Controls/ServerTab.xaml.cs +++ b/Lite/Controls/ServerTab.xaml.cs @@ -46,6 +46,10 @@ public partial class ServerTab : UserControl private List _perfmonCounterItems = new(); private Helpers.ChartHoverHelper? _waitStatsHover; private Helpers.ChartHoverHelper? _perfmonHover; + private Helpers.ChartHoverHelper? _overviewCpuHover; + private Helpers.ChartHoverHelper? _overviewMemoryHover; + private Helpers.ChartHoverHelper? _overviewFileIoHover; + private Helpers.ChartHoverHelper? _overviewWaitStatsHover; private Helpers.ChartHoverHelper? _cpuHover; private Helpers.ChartHoverHelper? _memoryHover; private Helpers.ChartHoverHelper? _tempDbHover; @@ -193,6 +197,10 @@ public ServerTab(ServerConnection server, DuckDbInitializer duckDb, CredentialSe } /* Apply theme immediately so charts don't flash white before data loads */ + ApplyTheme(OverviewCpuChart); + ApplyTheme(OverviewMemoryChart); + ApplyTheme(OverviewFileIoChart); + ApplyTheme(OverviewWaitStatsChart); ApplyTheme(WaitStatsChart); ApplyTheme(QueryDurationTrendChart); ApplyTheme(ProcDurationTrendChart); @@ -218,6 +226,10 @@ public ServerTab(ServerConnection server, DuckDbInitializer duckDb, CredentialSe ApplyTheme(CollectorDurationChart); /* Chart hover tooltips */ + _overviewCpuHover = new Helpers.ChartHoverHelper(OverviewCpuChart, "%"); + _overviewMemoryHover = new Helpers.ChartHoverHelper(OverviewMemoryChart, "MB"); + _overviewFileIoHover = new Helpers.ChartHoverHelper(OverviewFileIoChart, "ms"); + _overviewWaitStatsHover = new Helpers.ChartHoverHelper(OverviewWaitStatsChart, "ms/sec"); _waitStatsHover = new Helpers.ChartHoverHelper(WaitStatsChart, "ms/sec"); _perfmonHover = new Helpers.ChartHoverHelper(PerfmonChart, ""); _cpuHover = new Helpers.ChartHoverHelper(CpuChart, "%"); @@ -249,6 +261,16 @@ public ServerTab(ServerConnection server, DuckDbInitializer duckDb, CredentialSe Helpers.ContextMenuHelper.SetupChartContextMenu(ProcDurationTrendChart, "Procedure_Duration_Trends"); Helpers.ContextMenuHelper.SetupChartContextMenu(QueryStoreDurationTrendChart, "QueryStore_Duration_Trends"); Helpers.ContextMenuHelper.SetupChartContextMenu(ExecutionCountTrendChart, "Execution_Count_Trends"); + /* Overview chart context menus */ + var ovCpuMenu = Helpers.ContextMenuHelper.SetupChartContextMenu(OverviewCpuChart, "Overview_CPU"); + AddChartDrillDownMenuItem(OverviewCpuChart, ovCpuMenu, _overviewCpuHover, "Show Active Queries at This Time", OnCpuDrillDown); + var ovMemMenu = Helpers.ContextMenuHelper.SetupChartContextMenu(OverviewMemoryChart, "Overview_Memory"); + AddChartDrillDownMenuItem(OverviewMemoryChart, ovMemMenu, _overviewMemoryHover, "Show Active Queries at This Time", OnMemoryDrillDown); + var ovIoMenu = Helpers.ContextMenuHelper.SetupChartContextMenu(OverviewFileIoChart, "Overview_FileIO"); + AddChartDrillDownMenuItem(OverviewFileIoChart, ovIoMenu, _overviewFileIoHover, "Show Active Queries at This Time", OnCpuDrillDown); + var ovWaitMenu = Helpers.ContextMenuHelper.SetupChartContextMenu(OverviewWaitStatsChart, "Overview_WaitStats"); + AddChartDrillDownMenuItem(OverviewWaitStatsChart, ovWaitMenu, _overviewWaitStatsHover, "Show Active Queries at This Time", OnCpuDrillDown); + var cpuMenu = Helpers.ContextMenuHelper.SetupChartContextMenu(CpuChart, "CPU_Usage"); AddChartDrillDownMenuItem(CpuChart, cpuMenu, _cpuHover, "Show Active Queries at This Time", OnCpuDrillDown); var memoryMenu = Helpers.ContextMenuHelper.SetupChartContextMenu(MemoryChart, "Memory_Usage"); @@ -673,7 +695,7 @@ private async System.Threading.Tasks.Task RefreshAllDataAsync(bool fullRefresh = { await RefreshVisibleTabAsync(hoursBack, fromDate, toDate, subTabOnly: true); /* Always keep alert badge current even when Blocking tab is not visible */ - if (MainTabControl.SelectedIndex != 7) + if (MainTabControl.SelectedIndex != 8) await RefreshAlertCountsAsync(hoursBack, fromDate, toDate); } @@ -695,19 +717,20 @@ private async System.Threading.Tasks.Task RefreshVisibleTabAsync(int hoursBack, { switch (MainTabControl.SelectedIndex) { - case 0: await RefreshWaitStatsAsync(hoursBack, fromDate, toDate); break; - case 1: await RefreshQueriesAsync(hoursBack, fromDate, toDate, subTabOnly); break; - case 2: break; // Plan Viewer — no queries - case 3: await RefreshCpuAsync(hoursBack, fromDate, toDate); break; - case 4: await RefreshMemoryAsync(hoursBack, fromDate, toDate, subTabOnly); break; - case 5: await RefreshFileIoAsync(hoursBack, fromDate, toDate); break; - case 6: await RefreshTempDbAsync(hoursBack, fromDate, toDate); break; - case 7: await RefreshBlockingAsync(hoursBack, fromDate, toDate, subTabOnly); break; - case 8: await RefreshPerfmonAsync(hoursBack, fromDate, toDate); break; - case 9: await RefreshRunningJobsAsync(hoursBack, fromDate, toDate); break; - case 10: await RefreshConfigurationAsync(hoursBack, fromDate, toDate); break; - case 11: await RefreshDailySummaryAsync(hoursBack, fromDate, toDate); break; - case 12: await RefreshCollectionHealthAsync(hoursBack, fromDate, toDate); break; + case 0: await RefreshOverviewAsync(hoursBack, fromDate, toDate); break; + case 1: await RefreshWaitStatsAsync(hoursBack, fromDate, toDate); break; + case 2: await RefreshQueriesAsync(hoursBack, fromDate, toDate, subTabOnly); break; + case 3: break; // Plan Viewer — no queries + case 4: await RefreshCpuAsync(hoursBack, fromDate, toDate); break; + case 5: await RefreshMemoryAsync(hoursBack, fromDate, toDate, subTabOnly); break; + case 6: await RefreshFileIoAsync(hoursBack, fromDate, toDate); break; + case 7: await RefreshTempDbAsync(hoursBack, fromDate, toDate); break; + case 8: await RefreshBlockingAsync(hoursBack, fromDate, toDate, subTabOnly); break; + case 9: await RefreshPerfmonAsync(hoursBack, fromDate, toDate); break; + case 10: await RefreshRunningJobsAsync(hoursBack, fromDate, toDate); break; + case 11: await RefreshConfigurationAsync(hoursBack, fromDate, toDate); break; + case 12: await RefreshDailySummaryAsync(hoursBack, fromDate, toDate); break; + case 13: await RefreshCollectionHealthAsync(hoursBack, fromDate, toDate); break; } } @@ -977,6 +1000,158 @@ await System.Threading.Tasks.Task.WhenAll( } /// Tab 3 — CPU + /// Tab 0 — Overview (4 charts: CPU, Memory, File I/O, Wait Stats) + private async System.Threading.Tasks.Task RefreshOverviewAsync(int hoursBack, DateTime? fromDate, DateTime? toDate) + { + try + { + var cpuTask = SafeQueryAsync(() => _dataService.GetCpuUtilizationAsync(_serverId, hoursBack, fromDate, toDate)); + var memoryTask = SafeQueryAsync(() => _dataService.GetMemoryTrendAsync(_serverId, hoursBack, fromDate, toDate)); + var fileIoTask = SafeQueryAsync(() => _dataService.GetFileIoLatencyTrendAsync(_serverId, hoursBack, fromDate, toDate)); + + // Get top 5 wait types then fetch trends for each + var waitStats = await SafeQueryAsync(() => _dataService.GetWaitStatsAsync(_serverId, hoursBack, fromDate, toDate)); + var topWaits = waitStats.Take(5).Select(w => w.WaitType).ToList(); + await System.Threading.Tasks.Task.WhenAll(cpuTask, memoryTask, fileIoTask); + + UpdateOverviewCpuChart(cpuTask.Result); + UpdateOverviewMemoryChart(memoryTask.Result); + UpdateOverviewFileIoChart(fileIoTask.Result); + await UpdateOverviewWaitStatsChartAsync(topWaits, hoursBack, fromDate, toDate); + } + catch (Exception ex) + { + AppLogger.Info("ServerTab", $"[{_server.DisplayName}] RefreshOverviewAsync failed: {ex.Message}"); + } + } + + private void UpdateOverviewCpuChart(List data) + { + ClearChart(OverviewCpuChart); + _overviewCpuHover?.Clear(); + ApplyTheme(OverviewCpuChart); + + if (data.Count == 0) { RefreshEmptyChart(OverviewCpuChart, "CPU Utilization", "CPU %"); return; } + + var times = data.Select(d => d.SampleTime.ToOADate()).ToArray(); + var sqlCpu = data.Select(d => (double)d.SqlServerCpu).ToArray(); + + var plot = OverviewCpuChart.Plot.Add.Scatter(times, sqlCpu); + plot.LegendText = "SQL CPU %"; + plot.Color = ScottPlot.Color.FromHex("#4FC3F7"); + _overviewCpuHover?.Add(plot, "SQL CPU %"); + + OverviewCpuChart.Plot.Axes.DateTimeTicksBottom(); + ReapplyAxisColors(OverviewCpuChart); + OverviewCpuChart.Plot.Title("CPU Utilization"); + OverviewCpuChart.Plot.YLabel("CPU %"); + OverviewCpuChart.Plot.Axes.SetLimitsY(0, 105); + ShowChartLegend(OverviewCpuChart); + OverviewCpuChart.Refresh(); + } + + private void UpdateOverviewMemoryChart(List data) + { + ClearChart(OverviewMemoryChart); + _overviewMemoryHover?.Clear(); + ApplyTheme(OverviewMemoryChart); + + if (data.Count == 0) { RefreshEmptyChart(OverviewMemoryChart, "Memory Utilization", "MB"); return; } + + var times = data.Select(d => d.CollectionTime.AddMinutes(UtcOffsetMinutes).ToOADate()).ToArray(); + var bufferPool = data.Select(d => d.BufferPoolMb).ToArray(); + var grants = data.Select(d => d.TotalGrantedMb).ToArray(); + + var bpPlot = OverviewMemoryChart.Plot.Add.Scatter(times, bufferPool); + bpPlot.LegendText = "Buffer Pool"; + bpPlot.Color = ScottPlot.Color.FromHex("#CE93D8"); + _overviewMemoryHover?.Add(bpPlot, "Buffer Pool"); + + var grantPlot = OverviewMemoryChart.Plot.Add.Scatter(times, grants); + grantPlot.LegendText = "Memory Grants"; + grantPlot.Color = ScottPlot.Color.FromHex("#FFB74D"); + _overviewMemoryHover?.Add(grantPlot, "Memory Grants"); + + OverviewMemoryChart.Plot.Axes.DateTimeTicksBottom(); + ReapplyAxisColors(OverviewMemoryChart); + OverviewMemoryChart.Plot.Title("Memory Utilization"); + OverviewMemoryChart.Plot.YLabel("MB"); + SetChartYLimitsWithLegendPadding(OverviewMemoryChart, 0, bufferPool.Max()); + ShowChartLegend(OverviewMemoryChart); + OverviewMemoryChart.Refresh(); + } + + private void UpdateOverviewFileIoChart(List data) + { + ClearChart(OverviewFileIoChart); + _overviewFileIoHover?.Clear(); + ApplyTheme(OverviewFileIoChart); + + if (data.Count == 0) { RefreshEmptyChart(OverviewFileIoChart, "File I/O Latency", "ms"); return; } + + // Aggregate across all databases/files per collection time + var grouped = data + .GroupBy(d => d.CollectionTime) + .OrderBy(g => g.Key) + .Select(g => new { Time = g.Key, ReadMs = g.Average(x => x.AvgReadLatencyMs), WriteMs = g.Average(x => x.AvgWriteLatencyMs) }) + .ToList(); + + var times = grouped.Select(d => d.Time.AddMinutes(UtcOffsetMinutes).ToOADate()).ToArray(); + var readMs = grouped.Select(d => d.ReadMs).ToArray(); + var writeMs = grouped.Select(d => d.WriteMs).ToArray(); + + var readPlot = OverviewFileIoChart.Plot.Add.Scatter(times, readMs); + readPlot.LegendText = "Read ms"; + readPlot.Color = ScottPlot.Color.FromHex("#81C784"); + _overviewFileIoHover?.Add(readPlot, "Read ms"); + + var writePlot = OverviewFileIoChart.Plot.Add.Scatter(times, writeMs); + writePlot.LegendText = "Write ms"; + writePlot.Color = ScottPlot.Color.FromHex("#FFB74D"); + _overviewFileIoHover?.Add(writePlot, "Write ms"); + + OverviewFileIoChart.Plot.Axes.DateTimeTicksBottom(); + ReapplyAxisColors(OverviewFileIoChart); + OverviewFileIoChart.Plot.Title("File I/O Latency"); + OverviewFileIoChart.Plot.YLabel("Latency (ms)"); + var maxVal = Math.Max(readMs.DefaultIfEmpty(0).Max(), writeMs.DefaultIfEmpty(0).Max()); + SetChartYLimitsWithLegendPadding(OverviewFileIoChart, 0, maxVal); + ShowChartLegend(OverviewFileIoChart); + OverviewFileIoChart.Refresh(); + } + + private async System.Threading.Tasks.Task UpdateOverviewWaitStatsChartAsync( + List topWaits, int hoursBack, DateTime? fromDate, DateTime? toDate) + { + ClearChart(OverviewWaitStatsChart); + _overviewWaitStatsHover?.Clear(); + ApplyTheme(OverviewWaitStatsChart); + + if (topWaits.Count == 0) { RefreshEmptyChart(OverviewWaitStatsChart, "Wait Statistics", "ms/sec"); return; } + + var colors = new[] { "#4FC3F7", "#81C784", "#FFB74D", "#CE93D8", "#E57373" }; + for (int i = 0; i < Math.Min(topWaits.Count, 5); i++) + { + var trend = await _dataService.GetWaitStatsTrendAsync(_serverId, topWaits[i], hoursBack, fromDate, toDate); + if (trend.Count < 2) continue; + + var times = trend.Select(d => d.CollectionTime.AddMinutes(UtcOffsetMinutes).ToOADate()).ToArray(); + var values = trend.Select(d => d.WaitTimeMsPerSecond).ToArray(); + + var plot = OverviewWaitStatsChart.Plot.Add.Scatter(times, values); + plot.LegendText = topWaits[i]; + plot.Color = ScottPlot.Color.FromHex(colors[i]); + _overviewWaitStatsHover?.Add(plot, topWaits[i]); + } + + OverviewWaitStatsChart.Plot.Axes.DateTimeTicksBottom(); + ReapplyAxisColors(OverviewWaitStatsChart); + OverviewWaitStatsChart.Plot.Title("Wait Statistics"); + OverviewWaitStatsChart.Plot.YLabel("Wait Time (ms/sec)"); + ShowChartLegend(OverviewWaitStatsChart); + OverviewWaitStatsChart.Refresh(); + } + private async System.Threading.Tasks.Task RefreshCpuAsync(int hoursBack, DateTime? fromDate, DateTime? toDate) { try @@ -2676,7 +2851,7 @@ private async void OnCpuDrillDown(DateTime time) SetDrillDownTimeRange(fromDate, toDate); // Navigate to Queries > Active Queries with ±15 min window - MainTabControl.SelectedIndex = 1; // Queries + MainTabControl.SelectedIndex = 2; // Queries QueriesSubTabControl.SelectedIndex = 1; // Active Queries var snapshots = await _dataService.GetLatestQuerySnapshotsAsync(_serverId, 0, fromDate, toDate); _querySnapshotsFilterMgr!.UpdateData(snapshots); @@ -2689,7 +2864,7 @@ private async void OnMemoryDrillDown(DateTime time) var toDate = time.AddMinutes(30); SetDrillDownTimeRange(fromDate, toDate); - MainTabControl.SelectedIndex = 1; // Queries + MainTabControl.SelectedIndex = 2; // Queries QueriesSubTabControl.SelectedIndex = 1; // Active Queries var snapshots = await _dataService.GetLatestQuerySnapshotsAsync(_serverId, 0, fromDate, toDate); _querySnapshotsFilterMgr!.UpdateData(snapshots); @@ -2703,7 +2878,7 @@ private async void OnTempDbDrillDown(DateTime time) SetDrillDownTimeRange(fromDate, toDate); // Navigate to Active Queries — TempDB spills are visible there - MainTabControl.SelectedIndex = 1; // Queries + MainTabControl.SelectedIndex = 2; // Queries QueriesSubTabControl.SelectedIndex = 1; // Active Queries var snapshots = await _dataService.GetLatestQuerySnapshotsAsync(_serverId, 0, fromDate, toDate); _querySnapshotsFilterMgr!.UpdateData(snapshots); @@ -2716,7 +2891,7 @@ private async void OnBlockingDrillDown(DateTime time) var toDate = time.AddMinutes(30); SetDrillDownTimeRange(fromDate, toDate); - MainTabControl.SelectedIndex = 7; // Blocking + MainTabControl.SelectedIndex = 8; // Blocking BlockingSubTabControl.SelectedIndex = 2; // Blocked Process Reports var bpr = await _dataService.GetRecentBlockedProcessReportsAsync(_serverId, 0, fromDate, toDate); _blockedProcessFilterMgr!.UpdateData(bpr); @@ -2728,7 +2903,7 @@ private async void OnDeadlockDrillDown(DateTime time) var toDate = time.AddMinutes(30); SetDrillDownTimeRange(fromDate, toDate); - MainTabControl.SelectedIndex = 7; // Blocking + MainTabControl.SelectedIndex = 8; // Blocking BlockingSubTabControl.SelectedIndex = 3; // Deadlocks var dlr = await _dataService.GetRecentDeadlocksAsync(_serverId, 0, fromDate, toDate); _deadlockFilterMgr!.UpdateData(DeadlockProcessDetail.ParseFromRows(dlr));