Skip to content

Commit 873052e

Browse files
committed
Report jitdiff errors better
1 parent f642a02 commit 873052e

3 files changed

Lines changed: 229 additions & 8 deletions

File tree

Runner/Helpers/JitDiffUtils.cs

Lines changed: 22 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -69,14 +69,32 @@ public static async Task RunJitDiffOnFrameworksAsync(JobBase job, string coreRoo
6969
await RunJitDiffAsync(job, coreRootFolder, checkedClrFolder, outputFolder, "--frameworks");
7070
}
7171

72-
public static async Task RunJitDiffOnAssembliesAsync(JobBase job, string coreRootFolder, string checkedClrFolder, string outputFolder, string[] assemblyPaths, string? logPrefix = null, CancellationToken cancellationToken = default)
72+
public static async Task RunJitDiffOnAssembliesAsync(JobBase job, string coreRootFolder, string checkedClrFolder, string outputFolder, string[] assemblyPaths, string? logPrefix = null, List<string>? output = null, CancellationToken cancellationToken = default)
7373
{
7474
ArgumentOutOfRangeException.ThrowIfZero(assemblyPaths.Length);
7575

76-
await RunJitDiffAsync(job, coreRootFolder, checkedClrFolder, outputFolder, string.Join(' ', assemblyPaths.Select(path => $"--assembly \"{path}\"")), logPrefix, cancellationToken);
76+
await RunJitDiffAsync(job, coreRootFolder, checkedClrFolder, outputFolder, string.Join(' ', assemblyPaths.Select(path => $"--assembly \"{path}\"")), logPrefix, output, cancellationToken);
7777
}
7878

79-
private static async Task RunJitDiffAsync(JobBase job, string coreRootFolder, string checkedClrFolder, string outputFolder, string frameworksOrAssembly, string? logPrefix = null, CancellationToken cancellationToken = default)
79+
// jit-diff prints a line like "Error running <corerun> on <assembly path>" for every assembly whose
80+
// dasm generation failed (see jitutils DiffTool.RunDasmTool). Extract those assembly file names so the
81+
// caller can report them and drop their (missing/one-sided) dasm from the cross-branch comparison.
82+
public static IEnumerable<string> ParseFailedAssemblyNames(List<string> jitDiffOutput)
83+
{
84+
foreach (string line in jitDiffOutput)
85+
{
86+
Match match = JitDiffAssemblyFailureRegex().Match(line);
87+
if (match.Success)
88+
{
89+
yield return Path.GetFileName(match.Groups[1].Value.Trim());
90+
}
91+
}
92+
}
93+
94+
[GeneratedRegex(@"Error running \S+ on (.+)$")]
95+
private static partial Regex JitDiffAssemblyFailureRegex();
96+
97+
private static async Task RunJitDiffAsync(JobBase job, string coreRootFolder, string checkedClrFolder, string outputFolder, string frameworksOrAssembly, string? logPrefix = null, List<string>? output = null, CancellationToken cancellationToken = default)
8098
{
8199
bool useCctors = !job.TryGetFlag("nocctors");
82100
bool useTier0 = job.TryGetFlag("tier0");
@@ -111,6 +129,7 @@ await job.RunProcessAsync("jitutils/bin/jit-diff",
111129
$"{frameworksOrAssembly} --pmi " +
112130
$"--core_root {coreRootFolder} " +
113131
$"--base {checkedClrFolder}",
132+
output: output,
114133
logPrefix: $"jit-diff {logPrefix ?? coreRootFolder}",
115134
envVars: envVars,
116135
cancellationToken: cancellationToken);

Runner/JobBase.cs

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -549,6 +549,31 @@ public async Task UploadTextArtifactAsync(string fileName, string contents)
549549
}
550550
}
551551

552+
// Well-known artifact used to tell the host about a problem that should be surfaced to the user even
553+
// though the job as a whole succeeded (see ReportUserVisibleErrorAsync). Content is plain Markdown -
554+
// the host owns the presentation (alert block, comment references, ...). Summary is always shown;
555+
// Details (if any) is appended in the tracking issue only; a comment is posted when PostComment is set.
556+
public const string UserVisibleErrorArtifactFileName = "UserVisibleError.json";
557+
558+
public sealed record UserVisibleError(string Summary, string? Details, bool PostComment);
559+
560+
/// <summary>
561+
/// Reports an error that should be surfaced to the user even when the job as a whole succeeds. Use this
562+
/// for problems that don't fail the job but that the user should still be told about (e.g. part of the
563+
/// work failing in a way that likely indicates a bug in the change being tested). Unlike a fatal error,
564+
/// this does not terminate the job. Pass plain Markdown and leave presentation to the host:
565+
/// <paramref name="summary"/> is always surfaced; <paramref name="details"/> is only added to the
566+
/// tracking issue (keep it out of anything that may be posted as a comment); set
567+
/// <paramref name="postComment"/> when the problem is likely actionable (e.g. caused by the change under
568+
/// test) so the host also posts a comment.
569+
/// </summary>
570+
public async Task ReportUserVisibleErrorAsync(string summary, string? details = null, bool postComment = false)
571+
{
572+
await LogAsync($"Surfacing user-visible error to the host (postComment: {postComment}):\n{summary}");
573+
574+
await UploadTextArtifactAsync(UserVisibleErrorArtifactFileName, JsonSerializer.Serialize(new UserVisibleError(summary, details, postComment)));
575+
}
576+
552577
public async Task UploadArtifactAsync(string path, string? fileName = null)
553578
{
554579
string name = fileName ?? Path.GetFileName(path);

Runner/Jobs/JitDiffJob.cs

Lines changed: 182 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -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

Comments
 (0)