From c961b1f9c1bb1b0151abb62f17b06959d7bc0abb Mon Sep 17 00:00:00 2001 From: Erik Darling <2136037+erikdarlingdata@users.noreply.github.com> Date: Thu, 5 Mar 2026 12:11:24 -0500 Subject: [PATCH] Add Server Context pane to Plan Insights and Advice for Humans - New Server Context column in Plan Insights showing server name, edition, hardware, MAXDOP, cost threshold, max memory, and database - Only visible when connected (query editor or Run Repro Script) - Run Repro Script now fetches server metadata so Advice for Humans includes the === Server Context === block - File-opened plan Advice buttons now pass viewer metadata through Closes #22 Closes #23 Co-Authored-By: Claude Opus 4.6 --- .../Controls/PlanViewerControl.axaml | 29 +++++-- .../Controls/PlanViewerControl.axaml.cs | 86 +++++++++++++++++++ .../Controls/QuerySessionControl.axaml.cs | 1 + src/PlanViewer.App/MainWindow.axaml.cs | 16 +++- 4 files changed, 125 insertions(+), 7 deletions(-) diff --git a/src/PlanViewer.App/Controls/PlanViewerControl.axaml b/src/PlanViewer.App/Controls/PlanViewerControl.axaml index 9366b28..b050af6 100644 --- a/src/PlanViewer.App/Controls/PlanViewerControl.axaml +++ b/src/PlanViewer.App/Controls/PlanViewerControl.axaml @@ -60,14 +60,33 @@ + - - + + + + + + + + + + + @@ -81,7 +100,7 @@ - - - diff --git a/src/PlanViewer.App/Controls/PlanViewerControl.axaml.cs b/src/PlanViewer.App/Controls/PlanViewerControl.axaml.cs index 47c6187..55de492 100644 --- a/src/PlanViewer.App/Controls/PlanViewerControl.axaml.cs +++ b/src/PlanViewer.App/Controls/PlanViewerControl.axaml.cs @@ -57,6 +57,7 @@ public partial class PlanViewerControl : UserControl private ParsedPlan? _currentPlan; private PlanStatement? _currentStatement; private string? _queryText; + private ServerMetadata? _serverMetadata; private double _zoomLevel = 1.0; private const double ZoomStep = 0.15; private const double MinZoom = 0.1; @@ -146,6 +147,20 @@ public PlanViewerControl() /// public string? QueryText => _queryText; + /// + /// Server metadata for advice generation and Plan Insights display. + /// + public ServerMetadata? Metadata + { + get => _serverMetadata; + set + { + _serverMetadata = value; + if (_currentStatement != null) + ShowServerContext(); + } + } + public void LoadPlan(string planXml, string label, string? queryText = null) { _label = label; @@ -2327,6 +2342,77 @@ void AddRow(string label, string value) AddRow("Early abort", statement.StatementOptmEarlyAbortReason); RuntimeSummaryContent.Children.Add(grid); + ShowServerContext(); + } + + private void ShowServerContext() + { + ServerContextContent.Children.Clear(); + if (_serverMetadata == null) + { + ServerContextBorder.IsVisible = false; + return; + } + + var m = _serverMetadata; + var fgColor = "#E4E6EB"; + + var grid = new Grid { ColumnDefinitions = new ColumnDefinitions("Auto,*") }; + int rowIndex = 0; + + void AddRow(string label, string value) + { + grid.RowDefinitions.Add(new RowDefinition(GridLength.Auto)); + var lb = new TextBlock + { + Text = label, FontSize = 11, + Foreground = new SolidColorBrush(Color.Parse(fgColor)), + HorizontalAlignment = HorizontalAlignment.Left, + Margin = new Thickness(0, 1, 8, 1) + }; + Grid.SetRow(lb, rowIndex); + Grid.SetColumn(lb, 0); + grid.Children.Add(lb); + + var vb = new TextBlock + { + Text = value, FontSize = 11, + Foreground = new SolidColorBrush(Color.Parse(fgColor)), + Margin = new Thickness(0, 1, 0, 1) + }; + Grid.SetRow(vb, rowIndex); + Grid.SetColumn(vb, 1); + grid.Children.Add(vb); + rowIndex++; + } + + // Server name + edition + var edition = m.Edition; + if (edition != null) + { + var idx = edition.IndexOf(" (64-bit)"); + if (idx > 0) edition = edition[..idx]; + } + var serverLine = m.ServerName ?? "Unknown"; + if (edition != null) serverLine += $" ({edition})"; + if (m.ProductVersion != null) serverLine += $", {m.ProductVersion}"; + AddRow("Server", serverLine); + + // Hardware + if (m.CpuCount > 0) + AddRow("Hardware", $"{m.CpuCount} CPUs, {m.PhysicalMemoryMB:N0} MB RAM"); + + // Instance settings + AddRow("MAXDOP", m.MaxDop.ToString()); + AddRow("Cost threshold", m.CostThresholdForParallelism.ToString()); + AddRow("Max memory", $"{m.MaxServerMemoryMB:N0} MB"); + + // Database + if (m.Database != null) + AddRow("Database", $"{m.Database.Name} (compat {m.Database.CompatibilityLevel})"); + + ServerContextContent.Children.Add(grid); + ServerContextBorder.IsVisible = true; } private void UpdateInsightsHeader() diff --git a/src/PlanViewer.App/Controls/QuerySessionControl.axaml.cs b/src/PlanViewer.App/Controls/QuerySessionControl.axaml.cs index 7bc9cfa..7311a70 100644 --- a/src/PlanViewer.App/Controls/QuerySessionControl.axaml.cs +++ b/src/PlanViewer.App/Controls/QuerySessionControl.axaml.cs @@ -642,6 +642,7 @@ private void AddPlanTab(string planXml, string queryText, bool estimated, string var label = labelOverride ?? (estimated ? $"Est Plan {_planCounter}" : $"Plan {_planCounter}"); var viewer = new PlanViewerControl(); + viewer.Metadata = _serverMetadata; viewer.LoadPlan(planXml, label, queryText); // Build tab header with close button and right-click rename diff --git a/src/PlanViewer.App/MainWindow.axaml.cs b/src/PlanViewer.App/MainWindow.axaml.cs index 726e80a..2c39362 100644 --- a/src/PlanViewer.App/MainWindow.axaml.cs +++ b/src/PlanViewer.App/MainWindow.axaml.cs @@ -442,14 +442,14 @@ private DockPanel CreatePlanTabContent(PlanViewerControl viewer) humanBtn.Click += (_, _) => { if (viewer.CurrentPlan == null) return; - var analysis = ResultMapper.Map(viewer.CurrentPlan, "file"); + var analysis = ResultMapper.Map(viewer.CurrentPlan, "file", viewer.Metadata); ShowAdviceWindow("Advice for Humans", TextFormatter.Format(analysis)); }; robotBtn.Click += (_, _) => { if (viewer.CurrentPlan == null) return; - var analysis = ResultMapper.Map(viewer.CurrentPlan, "file"); + var analysis = ResultMapper.Map(viewer.CurrentPlan, "file", viewer.Metadata); var json = JsonSerializer.Serialize(analysis, new JsonSerializerOptions { WriteIndented = true }); ShowAdviceWindow("Advice for Robots", json); }; @@ -1024,6 +1024,17 @@ private async Task GetActualPlanFromFile(PlanViewerControl viewer) try { + // Fetch server metadata for advice and Plan Insights + ServerMetadata? metadata = null; + try + { + metadata = await ServerMetadataService.FetchServerMetadataAsync( + connectionString, isAzure); + metadata.Database = await ServerMetadataService.FetchDatabaseMetadataAsync( + connectionString, metadata.SupportsScopedConfigs); + } + catch { /* Non-fatal — advice will just lack server context */ } + var cts = new System.Threading.CancellationTokenSource(); var sw = System.Diagnostics.Stopwatch.StartNew(); @@ -1042,6 +1053,7 @@ private async Task GetActualPlanFromFile(PlanViewerControl viewer) // Add a new tab with the actual plan var actualViewer = new PlanViewerControl(); + actualViewer.Metadata = metadata; actualViewer.LoadPlan(actualPlanXml, "Actual Plan", queryText); var content = CreatePlanTabContent(actualViewer);