@@ -4959,6 +4959,109 @@ private async System.Threading.Tasks.Task ShowBlockedProcessPlanAsync(object sen
49594959 }
49604960 }
49614961
4962+ // ── Deadlock process plan lookup ──
4963+
4964+ /* Deadlock graph XML puts sqlhandle/stmtstart/stmtend directly on the
4965+ <process> node, with optional <executionStack><frame sqlhandle=...>
4966+ children for the call stack. Try process-level first, then walk frames
4967+ top-down like sp_HumanEventsBlockViewer does for BPRs. */
4968+ private async void ViewDeadlockProcessPlan_Click ( object sender , RoutedEventArgs e )
4969+ {
4970+ if ( sender is not MenuItem menuItem ) return ;
4971+ var grid = FindParentDataGrid ( menuItem ) ;
4972+ if ( grid ? . CurrentItem is not DeadlockProcessDetail row ) return ;
4973+
4974+ var sideLabel = row . IsVictim ? "Victim" : "Deadlocker" ;
4975+ var label = $ "Est Plan - { sideLabel } SPID { row . Spid } ";
4976+
4977+ var frames = ExtractDeadlockProcessFrames ( row . DeadlockGraphXml , row . ProcessId ) ;
4978+ if ( frames . Count == 0 )
4979+ {
4980+ MessageBox . Show (
4981+ $ "The process has no resolvable sql_handle in the deadlock graph. " +
4982+ "This usually means the query ran as dynamic SQL or a system context — " +
4983+ "SQL Server records a zero handle in that case and the plan can't be recovered." ,
4984+ "No Plan Available" , MessageBoxButton . OK , MessageBoxImage . Information ) ;
4985+ return ;
4986+ }
4987+
4988+ string ? planXml = null ;
4989+ try
4990+ {
4991+ var connStr = _server . GetConnectionString ( _credentialService ) ;
4992+ foreach ( var f in frames )
4993+ {
4994+ planXml = await LocalDataService . FetchPlanBySqlHandleAsync (
4995+ connStr , row . DatabaseName , f . SqlHandle , f . StmtStart , f . StmtEnd ) ;
4996+ if ( ! string . IsNullOrEmpty ( planXml ) ) break ;
4997+ }
4998+ }
4999+ catch { }
5000+
5001+ if ( ! string . IsNullOrEmpty ( planXml ) )
5002+ {
5003+ OpenPlanTab ( planXml , label , row . SqlText ) ;
5004+ PlanViewerTabItem . IsSelected = true ;
5005+ }
5006+ else
5007+ {
5008+ MessageBox . Show (
5009+ $ "The plan for this { sideLabel . ToLowerInvariant ( ) } process is no longer in the plan cache on { _server . ServerName } . " +
5010+ "Deadlock graphs only give us a sql_handle — if that plan has been evicted, we can't recover it." ,
5011+ "No Plan Available" , MessageBoxButton . OK , MessageBoxImage . Information ) ;
5012+ }
5013+ }
5014+
5015+ private static IReadOnlyList < ( string SqlHandle , int StmtStart , int StmtEnd ) > ExtractDeadlockProcessFrames (
5016+ string graphXml , string processId )
5017+ {
5018+ var empty = Array . Empty < ( string , int , int ) > ( ) ;
5019+ if ( string . IsNullOrWhiteSpace ( graphXml ) || string . IsNullOrWhiteSpace ( processId ) ) return empty ;
5020+ try
5021+ {
5022+ var doc = System . Xml . Linq . XElement . Parse ( graphXml ) ;
5023+ var process = doc . Descendants ( "process" )
5024+ . FirstOrDefault ( p => string . Equals ( p . Attribute ( "id" ) ? . Value , processId , StringComparison . OrdinalIgnoreCase ) ) ;
5025+ if ( process == null ) return empty ;
5026+
5027+ var frames = new List < ( string , int , int ) > ( ) ;
5028+
5029+ /* Try process-level sqlhandle first — deadlock graphs frequently put it on <process>. */
5030+ var procHandle = process . Attribute ( "sqlhandle" ) ? . Value ;
5031+ if ( ! string . IsNullOrWhiteSpace ( procHandle ) &&
5032+ ! string . Equals ( procHandle , ZeroSqlHandle , StringComparison . OrdinalIgnoreCase ) )
5033+ {
5034+ int ps = 0 , pe = - 1 ;
5035+ int . TryParse ( process . Attribute ( "stmtstart" ) ? . Value , out ps ) ;
5036+ if ( int . TryParse ( process . Attribute ( "stmtend" ) ? . Value , out var peParsed ) ) pe = peParsed ;
5037+ frames . Add ( ( procHandle ! , ps , pe ) ) ;
5038+ }
5039+
5040+ /* Then walk the executionStack frames. */
5041+ var stack = process . Element ( "executionStack" ) ;
5042+ if ( stack != null )
5043+ {
5044+ foreach ( var frame in stack . Elements ( "frame" ) )
5045+ {
5046+ var handle = frame . Attribute ( "sqlhandle" ) ? . Value ;
5047+ if ( string . IsNullOrWhiteSpace ( handle ) ) continue ;
5048+ if ( string . Equals ( handle , ZeroSqlHandle , StringComparison . OrdinalIgnoreCase ) ) continue ;
5049+
5050+ int fs = 0 , fe = - 1 ;
5051+ int . TryParse ( frame . Attribute ( "stmtstart" ) ? . Value , out fs ) ;
5052+ if ( int . TryParse ( frame . Attribute ( "stmtend" ) ? . Value , out var feParsed ) ) fe = feParsed ;
5053+ frames . Add ( ( handle ! , fs , fe ) ) ;
5054+ }
5055+ }
5056+
5057+ return frames ;
5058+ }
5059+ catch
5060+ {
5061+ return empty ;
5062+ }
5063+ }
5064+
49625065 // ── Active Queries Slicer ──
49635066
49645067 private async System . Threading . Tasks . Task LoadActiveQueriesSlicerAsync ( )
0 commit comments