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
31 changes: 30 additions & 1 deletion Lite/Controls/ServerTab.xaml
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,30 @@
</MenuItem>
</ContextMenu>

<!-- Context menu for Deadlocks: one row per process, so "View Plan"
resolves to THIS row's process via sql_handle in the graph XML.
No Get Actual Plan — re-running a mid-transaction query that just
deadlocked is a foot-gun. -->
<ContextMenu x:Key="DeadlockContextMenu">
<MenuItem Header="Copy Cell" Click="CopyCell_Click">
<MenuItem.Icon><TextBlock Text="&#x1F4CB;"/></MenuItem.Icon>
</MenuItem>
<MenuItem Header="Copy Row" Click="CopyRow_Click">
<MenuItem.Icon><TextBlock Text="&#x1F4C4;"/></MenuItem.Icon>
</MenuItem>
<MenuItem Header="Copy All Rows" Click="CopyAllRows_Click">
<MenuItem.Icon><TextBlock Text="&#x1F4D1;"/></MenuItem.Icon>
</MenuItem>
<Separator/>
<MenuItem Header="Export to CSV..." Click="ExportToCsv_Click">
<MenuItem.Icon><TextBlock Text="&#x1F4CA;"/></MenuItem.Icon>
</MenuItem>
<Separator/>
<MenuItem Header="View Plan" Click="ViewDeadlockProcessPlan_Click">
<MenuItem.Icon><TextBlock Text="&#x1F50D;"/></MenuItem.Icon>
</MenuItem>
</ContextMenu>

<Style x:Key="GridRowStyle" TargetType="DataGridRow">
<Setter Property="ContextMenu" Value="{StaticResource DataGridContextMenu}"/>
</Style>
Expand All @@ -71,6 +95,11 @@
</Style.Triggers>
</Style>

<!-- Row style for deadlock grid - View Plan context menu -->
<Style x:Key="DeadlockRowStyle" TargetType="DataGridRow">
<Setter Property="ContextMenu" Value="{StaticResource DeadlockContextMenu}"/>
</Style>

<!-- Row style for wait stats - highlight high-wait types -->
<Style x:Key="WaitStatsRowStyle" TargetType="DataGridRow" BasedOn="{StaticResource GridRowStyle}">
<Style.Triggers>
Expand Down Expand Up @@ -1421,7 +1450,7 @@
<controls:TimeRangeSlicerControl x:Name="DeadlockSlicer" Grid.Row="0"/>
<DataGrid x:Name="DeadlockGrid" Grid.Row="1"
AutoGenerateColumns="False" IsReadOnly="True"
RowStyle="{StaticResource GridRowStyle}"
RowStyle="{StaticResource DeadlockRowStyle}"
HeadersVisibility="Column" GridLinesVisibility="Horizontal"
HorizontalScrollBarVisibility="Auto" VerticalScrollBarVisibility="Auto">
<DataGrid.Columns>
Expand Down
103 changes: 103 additions & 0 deletions Lite/Controls/ServerTab.xaml.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4959,6 +4959,109 @@ private async System.Threading.Tasks.Task ShowBlockedProcessPlanAsync(object sen
}
}

// ── Deadlock process plan lookup ──

/* Deadlock graph XML puts sqlhandle/stmtstart/stmtend directly on the
<process> node, with optional <executionStack><frame sqlhandle=...>
children for the call stack. Try process-level first, then walk frames
top-down like sp_HumanEventsBlockViewer does for BPRs. */
private async void ViewDeadlockProcessPlan_Click(object sender, RoutedEventArgs e)
{
if (sender is not MenuItem menuItem) return;
var grid = FindParentDataGrid(menuItem);
if (grid?.CurrentItem is not DeadlockProcessDetail row) return;

var sideLabel = row.IsVictim ? "Victim" : "Deadlocker";
var label = $"Est Plan - {sideLabel} SPID {row.Spid}";

var frames = ExtractDeadlockProcessFrames(row.DeadlockGraphXml, row.ProcessId);
if (frames.Count == 0)
{
MessageBox.Show(
$"The process has no resolvable sql_handle in the deadlock graph. " +
"This usually means the query ran as dynamic SQL or a system context — " +
"SQL Server records a zero handle in that case and the plan can't be recovered.",
"No Plan Available", MessageBoxButton.OK, MessageBoxImage.Information);
return;
}

string? planXml = null;
try
{
var connStr = _server.GetConnectionString(_credentialService);
foreach (var f in frames)
{
planXml = await LocalDataService.FetchPlanBySqlHandleAsync(
connStr, row.DatabaseName, f.SqlHandle, f.StmtStart, f.StmtEnd);
if (!string.IsNullOrEmpty(planXml)) break;
}
}
catch { }

if (!string.IsNullOrEmpty(planXml))
{
OpenPlanTab(planXml, label, row.SqlText);
PlanViewerTabItem.IsSelected = true;
}
else
{
MessageBox.Show(
$"The plan for this {sideLabel.ToLowerInvariant()} process is no longer in the plan cache on {_server.ServerName}. " +
"Deadlock graphs only give us a sql_handle — if that plan has been evicted, we can't recover it.",
"No Plan Available", MessageBoxButton.OK, MessageBoxImage.Information);
}
}

private static IReadOnlyList<(string SqlHandle, int StmtStart, int StmtEnd)> ExtractDeadlockProcessFrames(
string graphXml, string processId)
{
var empty = Array.Empty<(string, int, int)>();
if (string.IsNullOrWhiteSpace(graphXml) || string.IsNullOrWhiteSpace(processId)) return empty;
try
{
var doc = System.Xml.Linq.XElement.Parse(graphXml);
var process = doc.Descendants("process")
.FirstOrDefault(p => string.Equals(p.Attribute("id")?.Value, processId, StringComparison.OrdinalIgnoreCase));
if (process == null) return empty;

var frames = new List<(string, int, int)>();

/* Try process-level sqlhandle first — deadlock graphs frequently put it on <process>. */
var procHandle = process.Attribute("sqlhandle")?.Value;
if (!string.IsNullOrWhiteSpace(procHandle) &&
!string.Equals(procHandle, ZeroSqlHandle, StringComparison.OrdinalIgnoreCase))
{
int ps = 0, pe = -1;
int.TryParse(process.Attribute("stmtstart")?.Value, out ps);
if (int.TryParse(process.Attribute("stmtend")?.Value, out var peParsed)) pe = peParsed;
frames.Add((procHandle!, ps, pe));
}

/* Then walk the executionStack frames. */
var stack = process.Element("executionStack");
if (stack != null)
{
foreach (var frame in stack.Elements("frame"))
{
var handle = frame.Attribute("sqlhandle")?.Value;
if (string.IsNullOrWhiteSpace(handle)) continue;
if (string.Equals(handle, ZeroSqlHandle, StringComparison.OrdinalIgnoreCase)) continue;

int fs = 0, fe = -1;
int.TryParse(frame.Attribute("stmtstart")?.Value, out fs);
if (int.TryParse(frame.Attribute("stmtend")?.Value, out var feParsed)) fe = feParsed;
frames.Add((handle!, fs, fe));
}
}

return frames;
}
catch
{
return empty;
}
}

// ── Active Queries Slicer ──

private async System.Threading.Tasks.Task LoadActiveQueriesSlicerAsync()
Expand Down
Loading