From d543d36e97fe948f4b8b00682969987c60d878bd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Valentin=20Breu=C3=9F?= Date: Sun, 19 Apr 2026 12:18:18 +0200 Subject: [PATCH 1/2] ci: parallelize benchmarks across matrix agents Run each benchmark class on its own runner using a GitHub Actions matrix, then combine the resulting artifacts in the PR comment. - Add `BenchmarkFilter` Nuke parameter to scope a run to a single class - Upload one `Benchmarks-` artifact per matrix leg - Download all `Benchmarks-*` artifacts when building the PR comment --- .github/workflows/build.yml | 13 +++++++++++-- .github/workflows/ci.yml | 15 ++++++++++++--- Pipeline/Build.Benchmarks.cs | 7 +++++-- Pipeline/BuildExtensions.cs | 33 +++++++++++++++++++++++++++++---- 4 files changed, 57 insertions(+), 11 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 698c49ae..b1dba763 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -66,7 +66,16 @@ jobs: ./TestResults/*.trx benchmarks: - name: "Benchmarks" + name: "Benchmarks (${{ matrix.benchmark }})" + strategy: + fail-fast: false + matrix: + benchmark: + - CallbackBenchmarks + - CompleteEventBenchmarks + - CompleteIndexerBenchmarks + - CompleteMethodBenchmarks + - CompletePropertyBenchmarks runs-on: ubuntu-latest permissions: contents: write @@ -83,7 +92,7 @@ jobs: 8.0.x 10.0.x - name: Run benchmarks - run: ./build.sh Benchmarks + run: ./build.sh Benchmarks --benchmark-filter "*${{ matrix.benchmark }}*" mutation-tests: name: "Mutation tests" diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 1ce828ce..912f3459 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -90,7 +90,16 @@ jobs: ./Artifacts/* benchmarks: - name: "Benchmarks" + name: "Benchmarks (${{ matrix.benchmark }})" + strategy: + fail-fast: false + matrix: + benchmark: + - CallbackBenchmarks + - CompleteEventBenchmarks + - CompleteIndexerBenchmarks + - CompleteMethodBenchmarks + - CompletePropertyBenchmarks runs-on: ubuntu-latest env: DOTNET_NOLOGO: true @@ -105,12 +114,12 @@ jobs: 8.0.x 10.0.x - name: Run benchmarks - run: ./build.sh Benchmarks + run: ./build.sh Benchmarks --benchmark-filter "*${{ matrix.benchmark }}*" - name: Upload artifacts if: always() uses: actions/upload-artifact@v7 with: - name: Benchmarks + name: Benchmarks-${{ matrix.benchmark }} path: | ./Artifacts/* if-no-files-found: ignore diff --git a/Pipeline/Build.Benchmarks.cs b/Pipeline/Build.Benchmarks.cs index f4c32168..e3dc79fd 100644 --- a/Pipeline/Build.Benchmarks.cs +++ b/Pipeline/Build.Benchmarks.cs @@ -15,6 +15,9 @@ namespace Build; partial class Build { + [Parameter("Filter for BenchmarkDotNet - Default is '*'")] + readonly string BenchmarkFilter = "*"; + Target BenchmarkDotNet => _ => _ .Executes(() => { @@ -27,7 +30,7 @@ partial class Build .EnableNoLogo()); DotNet( - $"{Solution.Benchmarks.Mockolate_Benchmarks.Name}.dll --exporters json --filter * --artifacts \"{benchmarkDirectory}\"", + $"{Solution.Benchmarks.Mockolate_Benchmarks.Name}.dll --exporters json --filter {BenchmarkFilter} --artifacts \"{benchmarkDirectory}\"", Solution.Benchmarks.Mockolate_Benchmarks.Directory / "bin" / "Release"); }); @@ -63,7 +66,7 @@ partial class Build Target BenchmarkComment => _ => _ .Executes(async () => { - await "Benchmarks".DownloadArtifactTo(ArtifactsDirectory, GithubToken); + await "Benchmarks-".DownloadArtifactsStartingWith(ArtifactsDirectory, GithubToken); if (!Directory.Exists(ArtifactsDirectory / "Benchmarks" / "results")) { Log.Information("Skip benchmark comment, because no results directory was generated."); diff --git a/Pipeline/BuildExtensions.cs b/Pipeline/BuildExtensions.cs index b9feff0c..82fa6647 100644 --- a/Pipeline/BuildExtensions.cs +++ b/Pipeline/BuildExtensions.cs @@ -1,4 +1,5 @@ using System; +using System.IO; using System.IO.Compression; using System.Linq; using System.Net.Http; @@ -40,7 +41,17 @@ public static SonarScannerBeginSettings SetPullRequestOrBranchName( return settings.SetBranchName(gitVersion.BranchName); } - public static async Task DownloadArtifactTo(this string artifactName, string artifactsDirectory, string githubToken) + public static Task DownloadArtifactTo(this string artifactName, string artifactsDirectory, string githubToken) + => DownloadArtifactsWhere(name => name.Equals(artifactName, StringComparison.OrdinalIgnoreCase), + artifactsDirectory, githubToken); + + public static Task DownloadArtifactsStartingWith(this string artifactNamePrefix, string artifactsDirectory, + string githubToken) + => DownloadArtifactsWhere(name => name.StartsWith(artifactNamePrefix, StringComparison.OrdinalIgnoreCase), + artifactsDirectory, githubToken); + + private static async Task DownloadArtifactsWhere(Func namePredicate, string artifactsDirectory, + string githubToken) { string runId = Environment.GetEnvironmentVariable("WorkflowRunId"); if (string.IsNullOrEmpty(runId)) @@ -68,7 +79,7 @@ public static async Task DownloadArtifactTo(this string artifactName, string art foreach (JsonElement artifact in jsonDocument.RootElement.GetProperty("artifacts").EnumerateArray()) { string name = artifact.GetProperty("name").GetString()!; - if (name.Equals(artifactName, StringComparison.OrdinalIgnoreCase)) + if (namePredicate(name)) { long artifactId = artifact.GetProperty("id").GetInt64(); HttpResponseMessage fileResponse = await client.GetAsync( @@ -76,9 +87,23 @@ public static async Task DownloadArtifactTo(this string artifactName, string art if (fileResponse.IsSuccessStatusCode) { using ZipArchive archive = new(await fileResponse.Content.ReadAsStreamAsync()); - archive.ExtractToDirectory(artifactsDirectory); + foreach (ZipArchiveEntry entry in archive.Entries) + { + string destinationPath = Path.Combine(artifactsDirectory, entry.FullName); + string destinationDirectory = Path.GetDirectoryName(destinationPath); + if (!string.IsNullOrEmpty(destinationDirectory)) + { + Directory.CreateDirectory(destinationDirectory); + } + + if (!string.IsNullOrEmpty(entry.Name)) + { + entry.ExtractToFile(destinationPath, overwrite: true); + } + } + Log.Information( - $"Extracted artifact #{artifactId} with {archive.Entries.Count} entries to {artifactsDirectory}:\n - {string.Join("\n - ", archive.Entries.Select(entry => $"{entry.Name} ({entry.Length})"))}"); + $"Extracted artifact '{name}' (#{artifactId}) with {archive.Entries.Count} entries to {artifactsDirectory}:\n - {string.Join("\n - ", archive.Entries.Select(entry => $"{entry.Name} ({entry.Length})"))}"); } else { From 2b84c11859a9586737bbd5b1b2bdd7a2a0eec696 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Valentin=20Breu=C3=9F?= Date: Sun, 19 Apr 2026 12:26:15 +0200 Subject: [PATCH 2/2] Fix review issue --- Pipeline/BuildExtensions.cs | 17 +---------------- 1 file changed, 1 insertion(+), 16 deletions(-) diff --git a/Pipeline/BuildExtensions.cs b/Pipeline/BuildExtensions.cs index 82fa6647..a5584e12 100644 --- a/Pipeline/BuildExtensions.cs +++ b/Pipeline/BuildExtensions.cs @@ -1,5 +1,4 @@ using System; -using System.IO; using System.IO.Compression; using System.Linq; using System.Net.Http; @@ -87,21 +86,7 @@ private static async Task DownloadArtifactsWhere(Func namePredicat if (fileResponse.IsSuccessStatusCode) { using ZipArchive archive = new(await fileResponse.Content.ReadAsStreamAsync()); - foreach (ZipArchiveEntry entry in archive.Entries) - { - string destinationPath = Path.Combine(artifactsDirectory, entry.FullName); - string destinationDirectory = Path.GetDirectoryName(destinationPath); - if (!string.IsNullOrEmpty(destinationDirectory)) - { - Directory.CreateDirectory(destinationDirectory); - } - - if (!string.IsNullOrEmpty(entry.Name)) - { - entry.ExtractToFile(destinationPath, overwrite: true); - } - } - + archive.ExtractToDirectory(artifactsDirectory, overwriteFiles: true); Log.Information( $"Extracted artifact '{name}' (#{artifactId}) with {archive.Entries.Count} entries to {artifactsDirectory}:\n - {string.Join("\n - ", archive.Entries.Select(entry => $"{entry.Name} ({entry.Length})"))}"); }