From 0e00497200071e01e05564b47f0a534f7001b996 Mon Sep 17 00:00:00 2001 From: Drew Scoggins Date: Wed, 20 May 2026 10:32:30 -0700 Subject: [PATCH 1/3] Switch all benchmark runners from BenchmarkSwitcher.Run to RunAsync BDN 0.16's sync entrypoints install BenchmarkDotNetSynchronizationContext (a single-threaded message pump) before benchmark discovery. Any code that does sync-over-async (e.g. ML.NET internals, SslStream handshakes) deadlocks against that pump. The async entrypoint avoids this by not installing the SyncCtx on the caller thread. This applies the same fix that was already made to MicroBenchmarks in c14e7c4a to all remaining real-world benchmark runners: - Akade.IndexedSet.Benchmarks - bepuphysics2 - ILLink - ImageSharp - Microsoft.ML.Benchmarks - PowerShell.Benchmarks - Roslyn Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../Akade.IndexedSet.Benchmarks/Program.cs | 7 +++++-- src/benchmarks/real-world/ILLink/Program.cs | 12 ++++++++---- src/benchmarks/real-world/ImageSharp/Program.cs | 17 ++++++++++++----- .../Microsoft.ML.Benchmarks/Program.cs | 16 +++++++++++----- .../real-world/PowerShell.Benchmarks/Program.cs | 12 ++++++++---- src/benchmarks/real-world/Roslyn/Program.cs | 9 ++++++--- .../real-world/bepuphysics2/Program.cs | 13 +++++++++---- 7 files changed, 59 insertions(+), 27 deletions(-) diff --git a/src/benchmarks/real-world/Akade.IndexedSet.Benchmarks/Program.cs b/src/benchmarks/real-world/Akade.IndexedSet.Benchmarks/Program.cs index d0a1244468f..8f8135e1d04 100644 --- a/src/benchmarks/real-world/Akade.IndexedSet.Benchmarks/Program.cs +++ b/src/benchmarks/real-world/Akade.IndexedSet.Benchmarks/Program.cs @@ -3,6 +3,9 @@ using BenchmarkDotNet.Running; using System.Collections.Immutable; -BenchmarkSwitcher.FromAssembly(typeof(Program).Assembly).Run(args, RecommendedConfig.Create( +// Use RunAsync (not Run) so BDN does not install its single-threaded +// BenchmarkDotNetSynchronizationContext on the entrypoint thread. +await BenchmarkSwitcher.FromAssembly(typeof(Program).Assembly).RunAsync(args, RecommendedConfig.Create( artifactsPath: new DirectoryInfo(Path.Combine(Path.GetDirectoryName(typeof(Program).Assembly.Location)!, "BenchmarkDotNet.Artifacts")), - mandatoryCategories: ImmutableHashSet.Create(Categories.AkadeIndexedSet))); \ No newline at end of file + mandatoryCategories: ImmutableHashSet.Create(Categories.AkadeIndexedSet))) + .ConfigureAwait(false); \ No newline at end of file diff --git a/src/benchmarks/real-world/ILLink/Program.cs b/src/benchmarks/real-world/ILLink/Program.cs index d57eb05a15a..3147069face 100644 --- a/src/benchmarks/real-world/ILLink/Program.cs +++ b/src/benchmarks/real-world/ILLink/Program.cs @@ -4,6 +4,7 @@ using System.Collections.Immutable; using System.IO; using System.Reflection; +using System.Threading.Tasks; using BenchmarkDotNet.Engines; using BenchmarkDotNet.Extensions; using BenchmarkDotNet.Jobs; @@ -13,7 +14,9 @@ namespace ILLinkBenchmarks; public class ILLinkBench { - public static int Main(string[] args) + // Use RunAsync (not Run) so BDN does not install its single-threaded + // BenchmarkDotNetSynchronizationContext on the entrypoint thread. + public static async Task Main(string[] args) { string thisAssembly = Assembly.GetExecutingAssembly().Location; string sampleProjectFile = Path.Combine(Path.GetDirectoryName(thisAssembly), "SampleProject", "HelloWorld.csproj"); @@ -29,13 +32,14 @@ public static int Main(string[] args) .WithStrategy(RunStrategy.Monitoring) .WithMaxRelativeError(0.01); - return BenchmarkSwitcher + var summaries = await BenchmarkSwitcher .FromAssembly(typeof(BasicBenchmark).Assembly) - .Run(args, RecommendedConfig.Create( + .RunAsync(args, RecommendedConfig.Create( artifactsPath: new DirectoryInfo(Path.Combine(Path.GetDirectoryName(typeof(BasicBenchmark).Assembly.Location), "BenchmarkDotNet.Artifacts")), mandatoryCategories: ImmutableHashSet.Create("ILLink"), job: job)) - .ToExitCode(); + .ConfigureAwait(false); + return summaries.ToExitCode(); } } \ No newline at end of file diff --git a/src/benchmarks/real-world/ImageSharp/Program.cs b/src/benchmarks/real-world/ImageSharp/Program.cs index f97854cce8c..c60877fdc68 100644 --- a/src/benchmarks/real-world/ImageSharp/Program.cs +++ b/src/benchmarks/real-world/ImageSharp/Program.cs @@ -5,6 +5,7 @@ using BenchmarkDotNet.Running; using System.Collections.Immutable; using System.IO; +using System.Threading.Tasks; namespace SixLabors.ImageSharp.Benchmarks { @@ -16,10 +17,16 @@ public class Program /// /// The arguments to pass to the program. /// - public static void Main(string[] args) => BenchmarkSwitcher - .FromAssembly(typeof(Program).Assembly) - .Run(args, RecommendedConfig.Create( - artifactsPath: new DirectoryInfo(Path.Combine(Path.GetDirectoryName(typeof(Program).Assembly.Location), "BenchmarkDotNet.Artifacts")), - mandatoryCategories: ImmutableHashSet.Create(Categories.ImageSharp))); + // Use RunAsync (not Run) so BDN does not install its single-threaded + // BenchmarkDotNetSynchronizationContext on the entrypoint thread. + public static async Task Main(string[] args) + { + await BenchmarkSwitcher + .FromAssembly(typeof(Program).Assembly) + .RunAsync(args, RecommendedConfig.Create( + artifactsPath: new DirectoryInfo(Path.Combine(Path.GetDirectoryName(typeof(Program).Assembly.Location), "BenchmarkDotNet.Artifacts")), + mandatoryCategories: ImmutableHashSet.Create(Categories.ImageSharp))) + .ConfigureAwait(false); + } } } diff --git a/src/benchmarks/real-world/Microsoft.ML.Benchmarks/Program.cs b/src/benchmarks/real-world/Microsoft.ML.Benchmarks/Program.cs index 619c66589ae..fdf8d6918db 100644 --- a/src/benchmarks/real-world/Microsoft.ML.Benchmarks/Program.cs +++ b/src/benchmarks/real-world/Microsoft.ML.Benchmarks/Program.cs @@ -6,6 +6,7 @@ using System.Globalization; using System.IO; using System.Threading; +using System.Threading.Tasks; using BenchmarkDotNet.Running; using BenchmarkDotNet.Extensions; @@ -17,13 +18,18 @@ class Program /// execute dotnet run -c Release and choose the benchmarks you want to run /// /// - static int Main(string[] args) - => BenchmarkSwitcher + // Use RunAsync (not Run) so BDN does not install its single-threaded + // BenchmarkDotNetSynchronizationContext on the entrypoint thread. + static async Task Main(string[] args) + { + var summaries = await BenchmarkSwitcher .FromAssembly(typeof(Program).Assembly) - .Run(args, RecommendedConfig.Create( - artifactsPath: new DirectoryInfo(Path.Combine(Path.GetDirectoryName(typeof(Program).Assembly.Location), "BenchmarkDotNet.Artifacts")), + .RunAsync(args, RecommendedConfig.Create( + artifactsPath: new DirectoryInfo(Path.Combine(Path.GetDirectoryName(typeof(Program).Assembly.Location), "BenchmarkDotNet.Artifacts")), mandatoryCategories: ImmutableHashSet.Create(Categories.MachineLearning))) - .ToExitCode(); + .ConfigureAwait(false); + return summaries.ToExitCode(); + } internal static string GetInvariantCultureDataPath(string name) { diff --git a/src/benchmarks/real-world/PowerShell.Benchmarks/Program.cs b/src/benchmarks/real-world/PowerShell.Benchmarks/Program.cs index 53f9a3ce95b..329b2d70b06 100644 --- a/src/benchmarks/real-world/PowerShell.Benchmarks/Program.cs +++ b/src/benchmarks/real-world/PowerShell.Benchmarks/Program.cs @@ -5,6 +5,7 @@ using System.Collections.Generic; using System.Collections.Immutable; using System.IO; +using System.Threading.Tasks; using BenchmarkDotNet.Running; using BenchmarkDotNet.Extensions; @@ -12,7 +13,9 @@ namespace MicroBenchmarks { public sealed class Program { - public static int Main(string[] args) + // Use RunAsync (not Run) so BDN does not install its single-threaded + // BenchmarkDotNetSynchronizationContext on the entrypoint thread. + public static async Task Main(string[] args) { var argsList = new List(args); int? partitionCount; @@ -38,9 +41,9 @@ public static int Main(string[] args) return 1; } - return BenchmarkSwitcher + var summaries = await BenchmarkSwitcher .FromAssembly(typeof(Program).Assembly) - .Run( + .RunAsync( argsList.ToArray(), RecommendedConfig.Create( artifactsPath: new DirectoryInfo(Path.Combine(Path.GetDirectoryName(typeof(Program).Assembly.Location), "BenchmarkDotNet.Artifacts")), @@ -50,7 +53,8 @@ public static int Main(string[] args) exclusionFilterValue: exclusionFilterValue, categoryExclusionFilterValue: categoryExclusionFilterValue, getDiffableDisasm: getDiffableDisasm)) - .ToExitCode(); + .ConfigureAwait(false); + return summaries.ToExitCode(); } } } diff --git a/src/benchmarks/real-world/Roslyn/Program.cs b/src/benchmarks/real-world/Roslyn/Program.cs index dcd2096b6d7..f2116956b44 100644 --- a/src/benchmarks/real-world/Roslyn/Program.cs +++ b/src/benchmarks/real-world/Roslyn/Program.cs @@ -20,11 +20,14 @@ // to communicate information is pass by environment variable Environment.SetEnvironmentVariable(Helpers.TestProjectEnvVarName, sourceDir); -return BenchmarkSwitcher +// Use RunAsync (not Run) so BDN does not install its single-threaded +// BenchmarkDotNetSynchronizationContext on the entrypoint thread. +var summaries = await BenchmarkSwitcher .FromAssembly(typeof(Helpers).Assembly) - .Run(args, RecommendedConfig.Create( + .RunAsync(args, RecommendedConfig.Create( artifactsPath: new DirectoryInfo(Path.Combine(Path.GetDirectoryName(typeof(Helpers).Assembly.Location), "BenchmarkDotNet.Artifacts")), mandatoryCategories: ImmutableHashSet.Create("Roslyn"), job: Job.Default.WithMaxRelativeError(0.01))) - .ToExitCode(); + .ConfigureAwait(false); +return summaries.ToExitCode(); diff --git a/src/benchmarks/real-world/bepuphysics2/Program.cs b/src/benchmarks/real-world/bepuphysics2/Program.cs index f09fff1f790..fd3b7e8b9a2 100644 --- a/src/benchmarks/real-world/bepuphysics2/Program.cs +++ b/src/benchmarks/real-world/bepuphysics2/Program.cs @@ -3,14 +3,19 @@ using BenchmarkDotNet.Running; using DemoBenchmarks; using System.Collections.Immutable; +using System.Threading.Tasks; public class BepuPhysics2Benchmarks { - public static void Main(string[] args) => - BenchmarkSwitcher + // Use RunAsync (not Run) so BDN does not install its single-threaded + // BenchmarkDotNetSynchronizationContext on the entrypoint thread. + public static async Task Main(string[] args) + { + await BenchmarkSwitcher .FromAssembly(typeof(BepuPhysics2Benchmarks).Assembly) - .Run(args, RecommendedConfig.Create( + .RunAsync(args, RecommendedConfig.Create( artifactsPath: new DirectoryInfo(Path.Combine(Path.GetDirectoryName(typeof(BepuPhysics2Benchmarks).Assembly.Location), "BenchmarkDotNet.Artifacts")), mandatoryCategories: ImmutableHashSet.Create(Categories.BepuPhysics))) - .ToExitCode(); + .ConfigureAwait(false); + } } From 37a562b63da4484ef3b778750ebba47a3c9260f4 Mon Sep 17 00:00:00 2001 From: Drew Scoggins Date: Wed, 20 May 2026 10:50:59 -0700 Subject: [PATCH 2/3] Propagate exit codes from RunAsync in Akade, ImageSharp, bepuphysics2 Capture the summaries returned by BenchmarkSwitcher.RunAsync and return summaries.ToExitCode() so CI can detect benchmark failures and critical validation errors via a non-zero process exit code. Addresses Copilot review comments on PR #5227. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../real-world/Akade.IndexedSet.Benchmarks/Program.cs | 5 +++-- src/benchmarks/real-world/ImageSharp/Program.cs | 5 +++-- src/benchmarks/real-world/bepuphysics2/Program.cs | 5 +++-- 3 files changed, 9 insertions(+), 6 deletions(-) diff --git a/src/benchmarks/real-world/Akade.IndexedSet.Benchmarks/Program.cs b/src/benchmarks/real-world/Akade.IndexedSet.Benchmarks/Program.cs index 8f8135e1d04..9c8cb8f3b7a 100644 --- a/src/benchmarks/real-world/Akade.IndexedSet.Benchmarks/Program.cs +++ b/src/benchmarks/real-world/Akade.IndexedSet.Benchmarks/Program.cs @@ -5,7 +5,8 @@ // Use RunAsync (not Run) so BDN does not install its single-threaded // BenchmarkDotNetSynchronizationContext on the entrypoint thread. -await BenchmarkSwitcher.FromAssembly(typeof(Program).Assembly).RunAsync(args, RecommendedConfig.Create( +var summaries = await BenchmarkSwitcher.FromAssembly(typeof(Program).Assembly).RunAsync(args, RecommendedConfig.Create( artifactsPath: new DirectoryInfo(Path.Combine(Path.GetDirectoryName(typeof(Program).Assembly.Location)!, "BenchmarkDotNet.Artifacts")), mandatoryCategories: ImmutableHashSet.Create(Categories.AkadeIndexedSet))) - .ConfigureAwait(false); \ No newline at end of file + .ConfigureAwait(false); +return summaries.ToExitCode(); \ No newline at end of file diff --git a/src/benchmarks/real-world/ImageSharp/Program.cs b/src/benchmarks/real-world/ImageSharp/Program.cs index c60877fdc68..85229ca9587 100644 --- a/src/benchmarks/real-world/ImageSharp/Program.cs +++ b/src/benchmarks/real-world/ImageSharp/Program.cs @@ -19,14 +19,15 @@ public class Program /// // Use RunAsync (not Run) so BDN does not install its single-threaded // BenchmarkDotNetSynchronizationContext on the entrypoint thread. - public static async Task Main(string[] args) + public static async Task Main(string[] args) { - await BenchmarkSwitcher + var summaries = await BenchmarkSwitcher .FromAssembly(typeof(Program).Assembly) .RunAsync(args, RecommendedConfig.Create( artifactsPath: new DirectoryInfo(Path.Combine(Path.GetDirectoryName(typeof(Program).Assembly.Location), "BenchmarkDotNet.Artifacts")), mandatoryCategories: ImmutableHashSet.Create(Categories.ImageSharp))) .ConfigureAwait(false); + return summaries.ToExitCode(); } } } diff --git a/src/benchmarks/real-world/bepuphysics2/Program.cs b/src/benchmarks/real-world/bepuphysics2/Program.cs index fd3b7e8b9a2..ff67c617193 100644 --- a/src/benchmarks/real-world/bepuphysics2/Program.cs +++ b/src/benchmarks/real-world/bepuphysics2/Program.cs @@ -9,13 +9,14 @@ public class BepuPhysics2Benchmarks { // Use RunAsync (not Run) so BDN does not install its single-threaded // BenchmarkDotNetSynchronizationContext on the entrypoint thread. - public static async Task Main(string[] args) + public static async Task Main(string[] args) { - await BenchmarkSwitcher + var summaries = await BenchmarkSwitcher .FromAssembly(typeof(BepuPhysics2Benchmarks).Assembly) .RunAsync(args, RecommendedConfig.Create( artifactsPath: new DirectoryInfo(Path.Combine(Path.GetDirectoryName(typeof(BepuPhysics2Benchmarks).Assembly.Location), "BenchmarkDotNet.Artifacts")), mandatoryCategories: ImmutableHashSet.Create(Categories.BepuPhysics))) .ConfigureAwait(false); + return summaries.ToExitCode(); } } From 5d3cbc78d96a319f899989abf8d3ffd8827898f2 Mon Sep 17 00:00:00 2001 From: Drew Scoggins Date: Thu, 21 May 2026 15:35:08 -0700 Subject: [PATCH 3/3] Clear SyncCtx in TrainSentiment to prevent BDN 0.16 deadlock BDN 0.16 installs BenchmarkDotNetSynchronizationContext in the child process that executes each benchmark. ML.NET's ApplyWordEmbedding downloads a pretrained model using sync-over-async I/O internally, which deadlocks on the single-threaded SyncCtx. Save and clear the SynchronizationContext before the ML.NET pipeline code and restore it afterward, so the async download can complete without deadlocking. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- ...sticDualCoordinateAscentClassifierBench.cs | 73 +++++++++++-------- 1 file changed, 43 insertions(+), 30 deletions(-) diff --git a/src/benchmarks/real-world/Microsoft.ML.Benchmarks/StochasticDualCoordinateAscentClassifierBench.cs b/src/benchmarks/real-world/Microsoft.ML.Benchmarks/StochasticDualCoordinateAscentClassifierBench.cs index 14ae66abee2..0708860eedc 100644 --- a/src/benchmarks/real-world/Microsoft.ML.Benchmarks/StochasticDualCoordinateAscentClassifierBench.cs +++ b/src/benchmarks/real-world/Microsoft.ML.Benchmarks/StochasticDualCoordinateAscentClassifierBench.cs @@ -4,6 +4,7 @@ using System.Collections.Generic; using System.Globalization; +using System.Threading; using BenchmarkDotNet.Attributes; using BenchmarkDotNet.Engines; using Microsoft.ML.Data; @@ -75,39 +76,51 @@ private TransformerChain