Skip to content

Async Refactor#2958

Open
timcassell wants to merge 77 commits intomasterfrom
async-refactor
Open

Async Refactor#2958
timcassell wants to merge 77 commits intomasterfrom
async-refactor

Conversation

@timcassell
Copy link
Collaborator

@timcassell timcassell commented Jan 5, 2026

Core changes:

  1. Added BenchmarkRunner.RunAsync/BenchmarkSwitcher.Run*Async APIs.
  2. Added cooperative cancellation support.
  3. Async benchmarks are now properly awaited instead of synchronously blocked
  4. Interfaces changed to support async.
  5. IterationSetup/Cleanup now support async methods.
  6. IClock is passed to the WorkloadActionNoUnroll etc. benchmark methods and they pass back ClockSpan, instead of the Engine starting and stopping the clock.
    • This is to avoid measuring workload setup before the core loop and async continuations for more accurate measurements. (It also helps to simplify the jit stage.)

Behavior changes:

  1. await Task.Yield() continues on the current synchronization context if it exists, or the current task scheduler. Previously this meant it continued on a ThreadPool thread, now it means it will likely run on the same thread via the new BenchmarkSynchronizationContext. This should be benign.

Breaking changes:

  1. IHost
    • Removed unused void Write(string message)
    • Added CancellationToken CancellationToken { get; } and ValueTask Yield()
    • Changed void SendSignal(HostSignal hostSignal) to ValueTask SendSignalAsync(HostSignal hostSignal)
  2. IEngine
    • RunResults Run() changed to ValueTask<RunResults> RunAsync()
  3. EngineParameters
    • WorkloadActionNoUnroll etc. changed from Action<long> to Func<long, IClock, ValueTask<ClockSpan>>
    • GlobalSetupAction etc. changed from Action to Func<ValueTask>
  4. IExecutor
    • ExecuteResult Execute(ExecuteParameters executeParameters) changed to ValueTask<ExecuteResult> ExecuteAsync(ExecuteParameters executeParameters, CancellationToken cancellationToken)
  5. IGenerator
    • Changed GenerateResult GenerateProject(BuildPartition buildPartition, ILogger logger, string rootArtifactsFolderPath) to ValueTask<GenerateResult> GenerateProjectAsync(BuildPartition buildPartition, ILogger logger, string rootArtifactsFolderPath, CancellationToken cancellationToken)
  6. IBuilder
    • Changed BuildResult Build(GenerateResult generateResult, BuildPartition buildPartition, ILogger logger) to ValueTask<BuildResult> BuildAsync(GenerateResult generateResult, BuildPartition buildPartition, ILogger logger, CancellationToken cancellationToken)
  7. IDiagnoser
    • Changed void Handle(HostSignal signal, DiagnoserActionParameters parameters) to ValueTask HandleAsync(HostSignal signal, DiagnoserActionParameters parameters, CancellationToken cancellationToken)
  8. IDiagnoser, IToolchain, IValidator
    • Changed IEnumerable<ValidationError> Validate(ValidationParameters validationParameters) to IAsyncEnumerable<ValidationError> ValidateAsync(ValidationParameters validationParameters);
  9. IExporter
    • Removed ExportToLog and ExportToFiles
    • Added ValueTask ExportAsync(Summary summary, ILogger logger, CancellationToken cancellationToken)

Other Changes:

  1. Added support for custom awaitable returns
    • Not supported in InProcessNoEmitToolchain
    • Does not include extension await
  2. New attributes
    • [AsyncCallerType] allows users to override the type used in the async method that calls their benchmark method.
      • Not supported in InProcessNoEmitToolchain
    • [BenchmarkCancellation] allows benchmarks to respond to cancellation requests from the user.
    • [AggressivelyOptimizeMethods] instructs the assembly weaver to apply AggressiveOptimization to all methods in the annotated type and nested types.
      • This is intended for internal use, but users can also use it. It's needed because C# doesn't allow to apply attributes to compiler-generated async state machines.
  3. Simplified EngineJitStage
  4. ReturnValueValidator is now able to validate async results.
  5. Anonymous pipe IPC changed to TCP loopback.
  6. Wasm toolchain now supports IPC for all JS engines (WebSocket or StdOut+File depending on the engine; configurable).
  7. Added new analyzers and a code fixer.

Fixes #2442
Fixes #2159
Fixes #2299
Fixes #2162
Unblocks #1808

@timcassell timcassell added this to the v0.16.0 milestone Jan 5, 2026

partial class RunnableEmitter
{
// TODO: update this to support runtime-async.
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@VSadov Should we emit runtime-async methods for this? Is it supported already? If so, what's the pattern?

@timcassell
Copy link
Collaborator Author

@copilot review

This comment was marked as off-topic.

@timcassell
Copy link
Collaborator Author

I did the minimum changes required for proper async benchmarks, but I'm not sure if we want to take it further. We could make all interface methods async (IGenerator.GenerateProjectAsync, IBuilder.BuildAsync, etc), and we could maybe add CancellationToken support (I'm not sure the best way to signal cancellation to the child process, though).

@stephentoub Can we get your input here?

@stephentoub
Copy link
Member

@stephentoub Can we get your input here?

Input on what specifically?

@timcassell
Copy link
Collaborator Author

@stephentoub Can we get your input here?

Input on what specifically?

On the whole async API design. More specifically my previous comment about how much of the surface we should make async and whether it even makes sense to add cancelation token support.

# Conflicts:
#	src/BenchmarkDotNet/BenchmarkDotNet.csproj
#	src/BenchmarkDotNet/Code/CodeGenerator.cs
#	src/BenchmarkDotNet/Code/DeclarationsProvider.cs
#	src/BenchmarkDotNet/Templates/BenchmarkProgram.txt
# Conflicts:
#	src/BenchmarkDotNet/Engines/EngineJitStage.cs
#	src/BenchmarkDotNet/Engines/EngineParameters.cs
#	src/BenchmarkDotNet/Helpers/AwaitHelper.cs
#	src/BenchmarkDotNet/Running/BenchmarkCase.cs
#	src/BenchmarkDotNet/Toolchains/Executor.cs
#	src/BenchmarkDotNet/Toolchains/InProcess/Emit/Implementation/ConsumableTypeInfo.cs
#	src/BenchmarkDotNet/Toolchains/InProcess/Emit/Implementation/Emitters/RunnableEmitter.cs
#	src/BenchmarkDotNet/Toolchains/InProcess/Emit/Implementation/Runnable/RunnableProgram.cs
#	src/BenchmarkDotNet/Toolchains/InProcess/Emit/Implementation/Runnable/RunnableReflectionHelpers.cs
#	src/BenchmarkDotNet/Toolchains/InProcess/NoEmit/InProcessNoEmitRunner.cs
#	tests/BenchmarkDotNet.Tests/Mocks/MockEngine.cs
Replaced anonymous pipes with named pipe, and made IHost methods async for true async I/O.
@timcassell timcassell marked this pull request as ready for review March 16, 2026 09:34
@timcassell
Copy link
Collaborator Author

@pavelsavara Can you sanity check the wasm changes here? Claude had the genius idea to use file-based IPC for JS shells that don't support WebSocket, so I was able to use that and still keep v8 as the default engine. (I don't expect a full review, this PR is too massive.)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment