@@ -352,6 +352,8 @@ private async Task DownloadExtraTestAssembliesAsync()
352352
353353 private async Task < string > CollectFrameworksDiffsAsync ( bool skipMain )
354354 {
355+ List < ExtraAssemblyDiffFailure > extraAssemblyFailures = new ( ) ;
356+
355357 try
356358 {
357359 await Task . WhenAll (
@@ -364,9 +366,11 @@ await Task.WhenAll(
364366
365367 int memoryAvailableGB = GetRemainingSystemMemoryGB ( ) ;
366368
367- await Task . WhenAll (
369+ List < ExtraAssemblyDiffFailure > [ ] failuresByBranch = await Task . WhenAll (
368370 DiffExtraProjectsAsync ( "artifacts-main" , "clr-checked-main" , DiffsMainDirectory , memoryAvailableGB ) ,
369371 DiffExtraProjectsAsync ( "artifacts-pr" , "clr-checked-pr" , DiffsPrDirectory , memoryAvailableGB ) ) ;
372+
373+ extraAssemblyFailures . AddRange ( failuresByBranch . SelectMany ( branchFailures => branchFailures ) ) ;
370374 }
371375 finally
372376 {
@@ -378,14 +382,24 @@ await Task.WhenAll(
378382 CombineAllDiffs ( DiffsMainDirectory , CombinedDasmMainDirectory ) ;
379383 CombineAllDiffs ( DiffsPrDirectory , CombinedDasmPrDirectory ) ;
380384
385+ // Drop the dasm of any assembly that failed to diff on either branch. Otherwise its one-sided
386+ // (missing / partial) dasm shows up in jit-analyze as a flood of spurious +100% / -100% diffs.
387+ RemoveFailedAssemblyDasm ( extraAssemblyFailures , CombinedDasmMainDirectory , CombinedDasmPrDirectory ) ;
388+
381389 string diffAnalyzeSummary = await JitDiffUtils . RunJitAnalyzeAsync ( this , CombinedDasmMainDirectory , CombinedDasmPrDirectory ) ;
382390
391+ // Surface any extra-assembly diff failures to the user (rendered prominently by the host) rather
392+ // than burying them inside the collapsed diff summary code block.
393+ await ReportExtraAssemblyFailuresAsync ( extraAssemblyFailures ) ;
394+
383395 PendingTasks . Enqueue ( UploadTextArtifactAsync ( "diff-frameworks.txt" , diffAnalyzeSummary ) ) ;
384396
385397 return diffAnalyzeSummary ;
386398
387- async Task DiffExtraProjectsAsync ( string coreRootFolder , string checkedClrFolder , string outputFolder , int memoryAvailableGB )
399+ async Task < List < ExtraAssemblyDiffFailure > > DiffExtraProjectsAsync ( string coreRootFolder , string checkedClrFolder , string outputFolder , int memoryAvailableGB )
388400 {
401+ var failures = new List < ExtraAssemblyDiffFailure > ( ) ;
402+
389403 string projectsRoot = ExtraProjectsDirectory ;
390404 string branch = coreRootFolder . Contains ( "main" , StringComparison . Ordinal ) ? "main" : "pr" ;
391405
@@ -413,7 +427,7 @@ async Task DiffExtraProjectsAsync(string coreRootFolder, string checkedClrFolder
413427 . OrderByDescending ( d => Directory . GetFiles ( d , "*.dll" ) . Sum ( f => new FileInfo ( f ) . Length ) ) ) ;
414428 if ( projectDirs . Count == 0 )
415429 {
416- return ;
430+ return failures ;
417431 }
418432
419433 int coreRootCopies = Math . Min ( Math . Min ( Environment . ProcessorCount , memoryAvailableGB * 2 ) , projectDirs . Count ) ;
@@ -472,13 +486,38 @@ await Parallel.ForAsync(0, coreRootCopies, async (index, _) =>
472486
473487 string [ ] assemblyPaths = testedAssemblies . Select ( a => Path . GetFullPath ( Path . Combine ( projectDir , a ) ) ) . ToArray ( ) ;
474488
489+ string projectName = Path . GetFileName ( projectDir ) ;
490+ List < string > jitDiffOutput = new ( ) ;
491+
475492 try
476493 {
477- await JitDiffUtils . RunJitDiffOnAssembliesAsync ( this , newCoreRootFolder , checkedClrFolder , outputFolder , assemblyPaths , logPrefix : $ "{ branch } { Path . GetFileName ( projectDir ) } " ) ;
494+ await JitDiffUtils . RunJitDiffOnAssembliesAsync ( this , newCoreRootFolder , checkedClrFolder , outputFolder , assemblyPaths , logPrefix : $ "{ branch } { projectName } " , output : jitDiffOutput ) ;
478495 }
479496 catch ( Exception ex )
480497 {
481- await LogAsync ( $ "Failed to diff { Path . GetFileName ( projectDir ) } : { ex . Message } ") ;
498+ await LogAsync ( $ "Failed to diff { projectName } on { branch } : { ex . Message } ") ;
499+
500+ // Prefer the specific assemblies jit-diff named as failing; fall back to all of the
501+ // project's assemblies if it failed without naming one (e.g. a crash before dasm).
502+ string [ ] failedAssemblies = JitDiffUtils . ParseFailedAssemblyNames ( jitDiffOutput )
503+ . Distinct ( StringComparer . OrdinalIgnoreCase )
504+ . ToArray ( ) ;
505+
506+ if ( failedAssemblies . Length == 0 )
507+ {
508+ failedAssemblies = testedAssemblies . Select ( Path . GetFileName ) . ToArray ( ) ! ;
509+ }
510+
511+ string [ ] errorLines = ExtractJitDiffErrorLines ( jitDiffOutput ) ;
512+ if ( errorLines . Length == 0 )
513+ {
514+ errorLines = [ ex . Message ] ;
515+ }
516+
517+ lock ( failures )
518+ {
519+ failures . Add ( new ExtraAssemblyDiffFailure ( branch , projectName , failedAssemblies , errorLines ) ) ;
520+ }
482521 }
483522 }
484523
@@ -487,6 +526,8 @@ await Parallel.ForAsync(0, coreRootCopies, async (index, _) =>
487526 try { Directory . Delete ( newCoreRootFolder , recursive : true ) ; } catch { }
488527 }
489528 } ) ;
529+
530+ return failures ;
490531 }
491532
492533 void CombineAllDiffs ( string directory , string destination )
@@ -500,6 +541,142 @@ void CombineAllDiffs(string directory, string destination)
500541 }
501542 }
502543
544+ private sealed record ExtraAssemblyDiffFailure ( string Branch , string Project , string [ ] Assemblies , string [ ] ErrorLines ) ;
545+
546+ private static readonly string [ ] s_jitDiffErrorMarkers =
547+ [
548+ "Error running" ,
549+ "errors compiling set" ,
550+ "returned with" ,
551+ "Dasm commands returned" ,
552+ "Dasm task failed" ,
553+ "Failures detected generating asm" ,
554+ "Assert failure" ,
555+ "Unhandled exception" ,
556+ ] ;
557+
558+ private static string [ ] ExtractJitDiffErrorLines ( List < string > jitDiffOutput )
559+ {
560+ return jitDiffOutput
561+ . Where ( line => s_jitDiffErrorMarkers . Any ( marker => line . Contains ( marker , StringComparison . OrdinalIgnoreCase ) ) )
562+ . Distinct ( )
563+ . ToArray ( ) ;
564+ }
565+
566+ private static void RemoveFailedAssemblyDasm ( List < ExtraAssemblyDiffFailure > failures , params string [ ] combinedDasmDirectories )
567+ {
568+ foreach ( string dasmFile in failures
569+ . SelectMany ( failure => failure . Assemblies )
570+ . Select ( assembly => $ "{ Path . GetFileNameWithoutExtension ( assembly ) } .dasm")
571+ . Distinct ( StringComparer . OrdinalIgnoreCase ) )
572+ {
573+ foreach ( string directory in combinedDasmDirectories )
574+ {
575+ try { File . Delete ( Path . Combine ( directory , dasmFile ) ) ; }
576+ catch { }
577+ }
578+ }
579+ }
580+
581+ private async Task ReportExtraAssemblyFailuresAsync ( List < ExtraAssemblyDiffFailure > failures )
582+ {
583+ if ( failures . Count == 0 )
584+ {
585+ return ;
586+ }
587+
588+ bool failedOnMain = failures . Any ( failure => failure . Branch == "main" ) ;
589+ bool failedOnPr = failures . Any ( failure => failure . Branch == "pr" ) ;
590+
591+ // Only the PR branch failing (nothing on main) points at the PR itself; failures that also happen
592+ // on main are more likely environmental/pre-existing, so they're reported but not flagged on the PR.
593+ bool likelyCausedByPr = failedOnPr && ! failedOnMain ;
594+
595+ var failedAssemblies = failures
596+ . SelectMany ( failure => failure . Assemblies . Select ( assembly => ( failure . Branch , Assembly : assembly ) ) )
597+ . GroupBy ( entry => entry . Assembly , StringComparer . OrdinalIgnoreCase )
598+ . OrderBy ( group => group . Key , StringComparer . OrdinalIgnoreCase )
599+ . ToArray ( ) ;
600+
601+ // Plain Markdown - the host owns presentation (alert block, comment, ...). The failed-assembly list
602+ // is capped so it stays readable; this concise summary is all that's posted as a comment.
603+ const int MaxListedAssemblies = 10 ;
604+
605+ StringBuilder summary = new ( ) ;
606+
607+ summary . AppendLine ( $ "**{ failedAssemblies . Length } extra test { ( failedAssemblies . Length == 1 ? "assembly" : "assemblies" ) } failed to produce JIT diffs** and { ( failedAssemblies . Length == 1 ? "was" : "were" ) } excluded from the diff analysis:") ;
608+
609+ foreach ( var group in failedAssemblies . Take ( MaxListedAssemblies ) )
610+ {
611+ string branches = string . Join ( ", " , group . Select ( entry => entry . Branch ) . Distinct ( ) . OrderBy ( branch => branch , StringComparer . Ordinal ) ) ;
612+ summary . AppendLine ( $ "- `{ group . Key } ` ({ branches } )") ;
613+ }
614+
615+ if ( failedAssemblies . Length > MaxListedAssemblies )
616+ {
617+ summary . AppendLine ( $ "- ... and { failedAssemblies . Length - MaxListedAssemblies } more") ;
618+ }
619+
620+ if ( likelyCausedByPr )
621+ {
622+ summary . AppendLine ( ) ;
623+ summary . AppendLine ( "All failures happened only on the PR branch, which likely indicates a bug introduced by the PR (e.g. a JIT assert or crash while compiling these assemblies)." ) ;
624+ }
625+
626+ string summaryMarkdown = summary . ToString ( ) . TrimEnd ( ) ;
627+
628+ // The full per-assembly output can be large, so it's only shown in the (collapsible) tracking issue
629+ // details - never in a posted comment.
630+ StringBuilder details = new ( ) ;
631+
632+ details . AppendLine ( "<details>" ) ;
633+ details . AppendLine ( "<summary>Relevant output</summary>" ) ;
634+ details . AppendLine ( ) ;
635+ details . AppendLine ( "```" ) ;
636+
637+ const int MaxOutputLines = 60 ;
638+ int outputLines = 0 ;
639+ bool truncated = false ;
640+
641+ foreach ( ExtraAssemblyDiffFailure failure in failures
642+ . OrderBy ( failure => failure . Branch , StringComparer . Ordinal )
643+ . ThenBy ( failure => failure . Project , StringComparer . OrdinalIgnoreCase ) )
644+ {
645+ foreach ( string line in failure . ErrorLines )
646+ {
647+ if ( outputLines ++ == MaxOutputLines )
648+ {
649+ truncated = true ;
650+ break ;
651+ }
652+
653+ details . AppendLine ( $ "[{ failure . Branch } { failure . Project } ] { line } ") ;
654+ }
655+
656+ if ( truncated )
657+ {
658+ break ;
659+ }
660+ }
661+
662+ if ( truncated )
663+ {
664+ details . AppendLine ( "... (truncated, see the full logs for details)" ) ;
665+ }
666+
667+ details . AppendLine ( "```" ) ;
668+ details . AppendLine ( ) ;
669+ details . AppendLine ( "</details>" ) ;
670+
671+ string detailsMarkdown = details . ToString ( ) . TrimEnd ( ) ;
672+
673+ await LogAsync ( $ "{ summaryMarkdown } \n \n { detailsMarkdown } ") ;
674+
675+ // Post a comment (summary only, no verbose output) when the PR is the likely cause; the full report
676+ // always goes to the tracking issue.
677+ await ReportUserVisibleErrorAsync ( summaryMarkdown , details : detailsMarkdown , postComment : likelyCausedByPr ) ;
678+ }
679+
503680 private async Task UploadJitDiffExamplesAsync ( string diffAnalyzeSummary , bool regressions )
504681 {
505682 var ( diffs , noisyDiffsRemoved ) = await JitDiffUtils . GetDiffMarkdownAsync (
0 commit comments