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
20 changes: 12 additions & 8 deletions src/PlanViewer.App/Controls/PlanViewerControl.RuntimeSummary.cs
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,8 @@ private void ShowRuntimeSummary(PlanStatement statement)
};
int rowIndex = 0;

void AddRow(string label, string value, string? brushKey = null)
// nested: the row is a detail of the row above it, so its label is indented under that row's.
void AddRow(string label, string value, string? brushKey = null, bool nested = false)
{
grid.RowDefinitions.Add(new RowDefinition(GridLength.Auto));

Expand All @@ -40,7 +41,7 @@ void AddRow(string label, string value, string? brushKey = null)
FontSize = 11,
Foreground = labelBrush,
HorizontalAlignment = HorizontalAlignment.Left,
Margin = new Thickness(0, 1, 8, 1)
Margin = new Thickness(nested ? 12 : 0, 1, 8, 1)
};
Grid.SetRow(labelText, rowIndex);
Grid.SetColumn(labelText, 0);
Expand Down Expand Up @@ -84,7 +85,8 @@ static string MemoryGrantBrushKey(double pctUsed, bool hasSpill)

var hasSpillInTree = statement.RootNode != null && HasSpillInPlanTree(statement.RootNode);

// E11: order — Elapsed → CPU:Elapsed → DOP → CPU → Compile → Memory → Used → Optimization → CE Model → Cost.
// E11: order — Elapsed → CPU:Elapsed → DOP → CPU → Compile → Memory → Used → CE Model → Optimization → Cost.
// #613 moved CE Model above Optimization, so Optimization and its nested early abort reason end the list.
// Extra Avalonia-only rows (threads, UDF, cached plan size) kept near their logical neighbors.

if (statement.QueryTimeStats != null)
Expand Down Expand Up @@ -188,13 +190,15 @@ static string MemoryGrantBrushKey(double pctUsed, bool hasSpill)
}
}

// Optimization + CE model
if (!string.IsNullOrEmpty(statement.StatementOptmLevel))
AddRow("Optimization", statement.StatementOptmLevel);
if (!string.IsNullOrEmpty(statement.StatementOptmEarlyAbortReason))
AddRow("Early abort", statement.StatementOptmEarlyAbortReason);
// CE model, then Optimization. #613: the early abort reason is part of the optimization
// result, not a fact of its own, so it sits under the Optimization row.
if (statement.CardinalityEstimationModelVersion > 0)
AddRow("CE model", statement.CardinalityEstimationModelVersion.ToString());
var hasOptimizationRow = !string.IsNullOrEmpty(statement.StatementOptmLevel);
if (hasOptimizationRow)
AddRow("Optimization", statement.StatementOptmLevel!);
if (!string.IsNullOrEmpty(statement.StatementOptmEarlyAbortReason))
AddRow("Early abort", statement.StatementOptmEarlyAbortReason, nested: hasOptimizationRow);

if (grid.Children.Count > 0)
{
Expand Down
9 changes: 5 additions & 4 deletions src/PlanViewer.Core/Output/HtmlExporter.cs
Original file line number Diff line number Diff line change
Expand Up @@ -308,8 +308,9 @@ private static void WriteRuntimeCard(StringBuilder sb, StatementResult stmt)
sb.AppendLine("<div class=\"card-body\">");

// Order per Joe (#215 E11): Elapsed → CPU:Elapsed → DOP → CPU → Compile →
// Memory → Used → Optimization → CE Model → Cost. Puts the important
// measurements on top and groups related metrics together.
// Memory → Used → CE Model → Optimization → Cost. Puts the important
// measurements on top and groups related metrics together. #613 moved CE Model
// above Optimization, the same order as the App's Runtime Summary.
if (stmt.QueryTime != null)
{
WriteRow(sb, "Elapsed", $"{stmt.QueryTime.ElapsedTimeMs:N0} ms");
Expand Down Expand Up @@ -343,10 +344,10 @@ private static void WriteRuntimeCard(StringBuilder sb, StatementResult stmt)
if (stmt.MemoryGrant.SerialRequiredKB > 0 && stmt.MemoryGrant.SerialRequiredKB != stmt.MemoryGrant.DesiredKB)
WriteRow(sb, "Serial required", FormatKB(stmt.MemoryGrant.SerialRequiredKB));
}
if (stmt.OptimizationLevel != null)
WriteRow(sb, "Optimization", Encode(stmt.OptimizationLevel));
if (stmt.CardinalityEstimationModel > 0)
WriteRow(sb, "CE Model", stmt.CardinalityEstimationModel.ToString());
if (stmt.OptimizationLevel != null)
WriteRow(sb, "Optimization", Encode(stmt.OptimizationLevel));
WriteRow(sb, "Cost", stmt.EstimatedCost.ToString("N2"));
sb.AppendLine("</div>");
sb.AppendLine("</div>");
Expand Down
15 changes: 8 additions & 7 deletions src/PlanViewer.Web/Pages/InsightsPanel.razor
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,8 @@
<div class="insight-card runtime">
<h4>@(isEstimatedRuntime ? "Predicted Runtime" : "Runtime")</h4>
<div class="insight-body">
@* Order per Joe #215 E11: Elapsed → CPU:Elapsed → DOP → CPU → Compile → Memory → Used → Optimization → CE Model → Cost *@
@* Order per Joe #215 E11: Elapsed → CPU:Elapsed → DOP → CPU → Compile → Memory → Used → CE Model → Optimization → Cost.
#613 moved CE Model above Optimization, the same order as the App's Runtime Summary. *@
@if (Stmt.QueryTime != null)
{
<div class="insight-row">
Expand Down Expand Up @@ -86,18 +87,18 @@
</div>
}
}
@if (Stmt.OptimizationLevel != null)
@if (Stmt.CardinalityEstimationModel > 0)
{
<div class="insight-row">
<span class="insight-label">Optimization</span>
<span class="insight-value">@Stmt.OptimizationLevel</span>
<span class="insight-label">CE Model</span>
<span class="insight-value">@Stmt.CardinalityEstimationModel</span>
</div>
}
@if (Stmt.CardinalityEstimationModel > 0)
@if (Stmt.OptimizationLevel != null)
{
<div class="insight-row">
<span class="insight-label">CE Model</span>
<span class="insight-value">@Stmt.CardinalityEstimationModel</span>
<span class="insight-label">Optimization</span>
<span class="insight-value">@Stmt.OptimizationLevel</span>
</div>
}
<div class="insight-row">
Expand Down
18 changes: 18 additions & 0 deletions tests/PlanViewer.Core.Tests/HtmlExporterTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -115,6 +115,24 @@ public void Export_CraftedSeverityOnAnOperator_IsMappedToo()
Assert.Contains("<div class=\"warning-item info\">", html);
}

/// <summary>#613: the runtime card lists CE Model above Optimization, the same order as the App's Runtime Summary.</summary>
[Fact]
public void Export_RuntimeCard_ListsCeModelAboveOptimization()
{
var plan = PlanTestHelper.LoadAndAnalyze("key_lookup_plan.sqlplan");
foreach (var batch in plan.Batches)
foreach (var stmt in batch.Statements)
PlanLayoutEngine.Layout(stmt);

var result = ResultMapper.Map(plan, "key_lookup_plan.sqlplan");
var html = HtmlExporter.Export(result, TextFormatter.Format(result));

var ceModel = html.IndexOf("<span class=\"label\">CE Model</span>", StringComparison.Ordinal);
var optimization = html.IndexOf("<span class=\"label\">Optimization</span>", StringComparison.Ordinal);
Assert.True(ceModel >= 0, "no CE Model row");
Assert.True(optimization > ceModel, "Optimization is not below CE Model");
}

private static string ExportWithSeverity(string? severity, bool onOperator = false)
{
var warning = new WarningResult { Severity = severity!, Type = "demo", Message = "demo" };
Expand Down
94 changes: 94 additions & 0 deletions tests/PlanViewer.Core.Tests/RuntimeSummaryEarlyAbortTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
using System.IO;
using System.Linq;
using Avalonia.Controls;
using Avalonia.LogicalTree;
using PlanViewer.App.Controls;

namespace PlanViewer.Core.Tests;

/// <summary>
/// #613: the early abort reason is part of the optimization result (StatementOptmEarlyAbortReason
/// only exists for a FULL optimization), so the Runtime Summary shows it as a detail of the
/// Optimization row: on the next row, with its label indented and its value in the same column.
/// CE model moved up above Optimization, so Optimization and its reason end the list.
/// </summary>
public class RuntimeSummaryEarlyAbortTests
{
[Fact]
public void EarlyAbort_IsNestedUnderOptimization()
{
HeadlessUi.Run(() =>
{
var viewer = LoadPlan(stripOptimizationLevel: false);
var window = new Window { Content = viewer, Width = 1400, Height = 900 };
window.Show();
window.UpdateLayout();

var grid = SummaryGrid(viewer);
var ceModel = Label(grid, "CE model");
var optimization = Label(grid, "Optimization");
var earlyAbort = Label(grid, "Early abort");

Assert.Equal(Grid.GetRow(ceModel) + 1, Grid.GetRow(optimization));
Assert.Equal(Grid.GetRow(optimization) + 1, Grid.GetRow(earlyAbort));
Assert.Equal(grid.RowDefinitions.Count - 1, Grid.GetRow(earlyAbort));
Assert.True(earlyAbort.Margin.Left > optimization.Margin.Left,
$"Early abort label is not indented: {earlyAbort.Margin.Left} vs {optimization.Margin.Left}");
Assert.Equal("GoodEnoughPlanFound", Value(grid, earlyAbort));
});
}

[Fact]
public void EarlyAbort_WithoutAnOptimizationRow_IsNotIndented()
{
HeadlessUi.Run(() =>
{
var viewer = LoadPlan(stripOptimizationLevel: true);
var window = new Window { Content = viewer, Width = 1400, Height = 900 };
window.Show();
window.UpdateLayout();

var grid = SummaryGrid(viewer);
Assert.DoesNotContain(grid.Children.OfType<TextBlock>(), t => t.Text == "Optimization");

var earlyAbort = Label(grid, "Early abort");
var elapsed = Label(grid, "Elapsed");
Assert.Equal(elapsed.Margin.Left, earlyAbort.Margin.Left);
});
}

/// <summary>
/// key_lookup_plan.sqlplan is an actual plan with StatementOptmLevel="FULL" and
/// StatementOptmEarlyAbortReason="GoodEnoughPlanFound". Removing the level leaves a reason
/// with no row to nest under.
/// </summary>
private static PlanViewerControl LoadPlan(bool stripOptimizationLevel)
{
var path = Path.Combine("Plans", "key_lookup_plan.sqlplan");
Assert.True(File.Exists(path), $"Test plan not found: {path}");
var xml = File.ReadAllText(path).Replace("encoding=\"utf-16\"", "encoding=\"utf-8\"");

const string level = "StatementOptmLevel=\"FULL\"";
Assert.Contains(level, xml);
Assert.Contains("StatementOptmEarlyAbortReason=\"GoodEnoughPlanFound\"", xml);
if (stripOptimizationLevel)
xml = xml.Replace(level, "");

var viewer = new PlanViewerControl();
Assert.True(viewer.LoadPlan(xml, "key_lookup_plan.sqlplan"), $"Plan failed to load: {viewer.LastLoadError}");
return viewer;
}

private static Grid SummaryGrid(PlanViewerControl viewer)
{
var panel = viewer.GetLogicalDescendants().OfType<StackPanel>().First(p => p.Name == "RuntimeSummaryContent");
return (Grid)panel.Children.Single();
}

private static TextBlock Label(Grid grid, string label)
=> grid.Children.OfType<TextBlock>().Single(t => Grid.GetColumn(t) == 0 && t.Text == label);

private static string Value(Grid grid, TextBlock label)
=> grid.Children.OfType<TextBlock>()
.Single(t => Grid.GetColumn(t) == 1 && Grid.GetRow(t) == Grid.GetRow(label)).Text ?? "";
}
Loading