From 50dbf5afea26e3947e8f1c27dabe8f21a6a9cebd Mon Sep 17 00:00:00 2001
From: Glenn Watson <5834289+glennawatson@users.noreply.github.com>
Date: Mon, 14 Sep 2026 22:25:31 +1000
Subject: [PATCH 1/3] test(benchmarks): trace every benchmark through a shared
config
- Adds Interaction and contended WhenChanged benchmarks.
- The benchmark workflow runs every benchmark.
---
.github/workflows/benchmarks.yml | 23 +---
src/Directory.Packages.props | 1 +
src/benchmarks/Directory.Build.props | 33 +++++
.../Mocks/BenchmarkModeDetector.cs | 16 +++
.../ModuleInitializer.cs | 41 -------
...iveUI.Binding.Benchmarks.ReactiveUI.csproj | 17 +--
.../ReactiveUIBindingBenchmark.cs | 29 +++--
.../ReactiveUIObservationBenchmark.cs | 29 +++--
.../BindBenchmark.cs | 38 +-----
.../BindOneWayBenchmark.cs | 18 +--
.../BindToBenchmark.cs | 18 +--
.../BindTwoWayBenchmark.cs | 18 +--
.../BindingInitializer.cs | 32 -----
.../InteractionBenchmark.cs | 79 ++++++++++++
.../InvokeCommandBenchmark.cs | 18 +--
.../{ => Mocks}/BenchmarkChildViewModel.cs | 2 +-
.../{ => Mocks}/BenchmarkCommand.cs | 2 +-
.../{ => Mocks}/BenchmarkSource.cs | 2 +-
.../{ => Mocks}/BenchmarkView.cs | 2 +-
.../{ => Mocks}/BenchmarkViewModel.cs | 2 +-
.../Mocks/CountingObserver.cs | 25 ++++
.../OneWayBindBenchmark.cs | 18 +--
.../ReactiveUI.Binding.Benchmarks.csproj | 10 --
.../RxUiDynamicChainBaseline.cs | 17 +--
.../UnsafeFallbackBenchmark.cs | 27 ++---
.../WhenAnyBenchmark.cs | 18 +--
.../WhenAnyDynamicBenchmark.cs | 16 +--
.../WhenAnyObservableBenchmark.cs | 18 +--
.../WhenAnyValueBenchmark.cs | 18 +--
.../WhenChangedBenchmark.cs | 18 +--
.../WhenChangedContentionBenchmark.cs | 48 ++++++++
.../WhenChangingBenchmark.cs | 18 +--
...ding.Generator.Benchmarks.Roslyn413.csproj | 7 +-
.../GenerationBenchmarks.cs | 26 ++--
.../GeneratorCorpus.cs | 114 ------------------
.../Mocks/AddressViewModel.cs | 30 +++++
.../Mocks/PersonBindings.cs | 95 +++++++++++++++
.../Mocks/PersonView.cs | 62 ++++++++++
.../Mocks/PersonViewModel.cs | 71 +++++++++++
.../Mocks/SaveButton.cs | 19 +++
...tiveUI.Binding.Generator.Benchmarks.csproj | 9 +-
.../{ => Support}/GeneratorHarness.cs | 34 ++++--
.../Configs/NativeAotBenchmarkConfig.cs | 19 +++
.../Shared/Platforms/unix/BenchmarkConfig.cs | 22 ++++
.../Shared/Platforms/unix/ProfilerConfig.cs | 38 ++++++
.../Platforms/windows/BenchmarkConfig.cs | 23 ++++
.../Platforms/windows/ProfilerConfig.cs | 15 +++
47 files changed, 727 insertions(+), 528 deletions(-)
create mode 100644 src/benchmarks/Directory.Build.props
create mode 100644 src/benchmarks/ReactiveUI.Binding.Benchmarks.ReactiveUI/Mocks/BenchmarkModeDetector.cs
delete mode 100644 src/benchmarks/ReactiveUI.Binding.Benchmarks.ReactiveUI/ModuleInitializer.cs
delete mode 100644 src/benchmarks/ReactiveUI.Binding.Benchmarks/BindingInitializer.cs
create mode 100644 src/benchmarks/ReactiveUI.Binding.Benchmarks/InteractionBenchmark.cs
rename src/benchmarks/ReactiveUI.Binding.Benchmarks/{ => Mocks}/BenchmarkChildViewModel.cs (95%)
rename src/benchmarks/ReactiveUI.Binding.Benchmarks/{ => Mocks}/BenchmarkCommand.cs (95%)
rename src/benchmarks/ReactiveUI.Binding.Benchmarks/{ => Mocks}/BenchmarkSource.cs (97%)
rename src/benchmarks/ReactiveUI.Binding.Benchmarks/{ => Mocks}/BenchmarkView.cs (97%)
rename src/benchmarks/ReactiveUI.Binding.Benchmarks/{ => Mocks}/BenchmarkViewModel.cs (98%)
create mode 100644 src/benchmarks/ReactiveUI.Binding.Benchmarks/Mocks/CountingObserver.cs
create mode 100644 src/benchmarks/ReactiveUI.Binding.Benchmarks/WhenChangedContentionBenchmark.cs
delete mode 100644 src/benchmarks/ReactiveUI.Binding.Generator.Benchmarks/GeneratorCorpus.cs
create mode 100644 src/benchmarks/ReactiveUI.Binding.Generator.Benchmarks/Mocks/AddressViewModel.cs
create mode 100644 src/benchmarks/ReactiveUI.Binding.Generator.Benchmarks/Mocks/PersonBindings.cs
create mode 100644 src/benchmarks/ReactiveUI.Binding.Generator.Benchmarks/Mocks/PersonView.cs
create mode 100644 src/benchmarks/ReactiveUI.Binding.Generator.Benchmarks/Mocks/PersonViewModel.cs
create mode 100644 src/benchmarks/ReactiveUI.Binding.Generator.Benchmarks/Mocks/SaveButton.cs
rename src/benchmarks/ReactiveUI.Binding.Generator.Benchmarks/{ => Support}/GeneratorHarness.cs (67%)
create mode 100644 src/benchmarks/Shared/Configs/NativeAotBenchmarkConfig.cs
create mode 100644 src/benchmarks/Shared/Platforms/unix/BenchmarkConfig.cs
create mode 100644 src/benchmarks/Shared/Platforms/unix/ProfilerConfig.cs
create mode 100644 src/benchmarks/Shared/Platforms/windows/BenchmarkConfig.cs
create mode 100644 src/benchmarks/Shared/Platforms/windows/ProfilerConfig.cs
diff --git a/.github/workflows/benchmarks.yml b/.github/workflows/benchmarks.yml
index d6a5b2b6..ac642b42 100644
--- a/.github/workflows/benchmarks.yml
+++ b/.github/workflows/benchmarks.yml
@@ -2,20 +2,6 @@ name: Benchmarks
on:
workflow_dispatch:
- inputs:
- suite:
- description: 'Which benchmark suite to run'
- type: choice
- default: runtime
- options:
- - runtime
- - baseline
- - generator
- - all
- filter:
- description: 'BenchmarkDotNet filter, e.g. *WhenChanged* or *'
- type: string
- default: '*'
permissions:
contents: read
@@ -24,12 +10,9 @@ jobs:
benchmark:
uses: reactiveui/actions-common/.github/workflows/workflow-common-benchmarks.yml@main
with:
- # One line per suite the choice covers. An unselected suite leaves a blank line behind,
- # which the list skips.
projects: |
- ${{ (inputs.suite == 'runtime' || inputs.suite == 'all') && 'benchmarks/ReactiveUI.Binding.Benchmarks/ReactiveUI.Binding.Benchmarks.csproj' || '' }}
- ${{ (inputs.suite == 'baseline' || inputs.suite == 'all') && 'benchmarks/ReactiveUI.Binding.Benchmarks.ReactiveUI/ReactiveUI.Binding.Benchmarks.ReactiveUI.csproj' || '' }}
- ${{ (inputs.suite == 'generator' || inputs.suite == 'all') && 'benchmarks/ReactiveUI.Binding.Generator.Benchmarks/ReactiveUI.Binding.Generator.Benchmarks.csproj' || '' }}
- filter: ${{ inputs.filter }}
+ benchmarks/ReactiveUI.Binding.Benchmarks/ReactiveUI.Binding.Benchmarks.csproj
+ benchmarks/ReactiveUI.Binding.Benchmarks.ReactiveUI/ReactiveUI.Binding.Benchmarks.ReactiveUI.csproj
+ benchmarks/ReactiveUI.Binding.Generator.Benchmarks/ReactiveUI.Binding.Generator.Benchmarks.csproj
solutionFile: ReactiveUI.Binding.SourceGenerators.slnx
installWorkloads: true
diff --git a/src/Directory.Packages.props b/src/Directory.Packages.props
index 6c10c453..b0f74270 100644
--- a/src/Directory.Packages.props
+++ b/src/Directory.Packages.props
@@ -74,6 +74,7 @@
+
diff --git a/src/benchmarks/Directory.Build.props b/src/benchmarks/Directory.Build.props
new file mode 100644
index 00000000..27a62728
--- /dev/null
+++ b/src/benchmarks/Directory.Build.props
@@ -0,0 +1,33 @@
+
+
+
+
+
+ $([MSBuild]::NormalizeDirectory($(MSBuildProjectDirectory), '..'))
+ false
+ true
+
+
+
+ Exe
+ false
+
+ windows
+ unix
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/benchmarks/ReactiveUI.Binding.Benchmarks.ReactiveUI/Mocks/BenchmarkModeDetector.cs b/src/benchmarks/ReactiveUI.Binding.Benchmarks.ReactiveUI/Mocks/BenchmarkModeDetector.cs
new file mode 100644
index 00000000..2431bc19
--- /dev/null
+++ b/src/benchmarks/ReactiveUI.Binding.Benchmarks.ReactiveUI/Mocks/BenchmarkModeDetector.cs
@@ -0,0 +1,16 @@
+// Copyright (c) 2019-2026 ReactiveUI Association Incorporated. All rights reserved.
+// ReactiveUI Association Incorporated licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for full license information.
+
+using System.Runtime.CompilerServices;
+using Splat;
+
+namespace ReactiveUI.Binding.Benchmarks.Mocks;
+
+/// Tells ReactiveUI it is running outside an application, as a benchmark process does.
+internal sealed class BenchmarkModeDetector : IModeDetector
+{
+ ///
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public bool? InUnitTestRunner() => true;
+}
diff --git a/src/benchmarks/ReactiveUI.Binding.Benchmarks.ReactiveUI/ModuleInitializer.cs b/src/benchmarks/ReactiveUI.Binding.Benchmarks.ReactiveUI/ModuleInitializer.cs
deleted file mode 100644
index 005344a6..00000000
--- a/src/benchmarks/ReactiveUI.Binding.Benchmarks.ReactiveUI/ModuleInitializer.cs
+++ /dev/null
@@ -1,41 +0,0 @@
-// Copyright (c) 2019-2026 ReactiveUI Association Incorporated. All rights reserved.
-// ReactiveUI Association Incorporated licenses this file to you under the MIT license.
-// See the LICENSE file in the project root for full license information.
-
-using System.Runtime.CompilerServices;
-using ReactiveUI.Builder;
-using Splat;
-
-namespace ReactiveUI.Binding.Benchmarks;
-
-///
-/// One-shot initializer that configures ReactiveUI before any benchmarks run.
-/// Call from each benchmark class's static constructor.
-///
-internal static class ModuleInitializer
-{
- /// Guard flag to ensure initialization runs at most once. Uses .
- private static int _initialized;
-
- /// Ensures ReactiveUI is initialized exactly once, regardless of how many benchmark classes call this method.
- internal static void EnsureInitialized()
- {
- if (Interlocked.Exchange(ref _initialized, 1) != 0)
- {
- return;
- }
-
- ModeDetector.OverrideModeDetector(new BenchmarkModeDetector());
- _ = RxAppBuilder.CreateReactiveUIBuilder()
- .WithCoreServices()
- .BuildApp();
- }
-
- /// Mode detector for benchmark context.
- private sealed class BenchmarkModeDetector : IModeDetector
- {
- ///
- [MethodImpl(MethodImplOptions.AggressiveInlining)]
- public bool? InUnitTestRunner() => true;
- }
-}
diff --git a/src/benchmarks/ReactiveUI.Binding.Benchmarks.ReactiveUI/ReactiveUI.Binding.Benchmarks.ReactiveUI.csproj b/src/benchmarks/ReactiveUI.Binding.Benchmarks.ReactiveUI/ReactiveUI.Binding.Benchmarks.ReactiveUI.csproj
index f3704786..c1f7c07f 100644
--- a/src/benchmarks/ReactiveUI.Binding.Benchmarks.ReactiveUI/ReactiveUI.Binding.Benchmarks.ReactiveUI.csproj
+++ b/src/benchmarks/ReactiveUI.Binding.Benchmarks.ReactiveUI/ReactiveUI.Binding.Benchmarks.ReactiveUI.csproj
@@ -1,30 +1,19 @@
- Exe
net8.0;net10.0;net11.0;net462
-
- $(DefineConstants);BENCH_NETFX
- enable
- false
false
-
-
-
-
-
+
+
+
diff --git a/src/benchmarks/ReactiveUI.Binding.Benchmarks.ReactiveUI/ReactiveUIBindingBenchmark.cs b/src/benchmarks/ReactiveUI.Binding.Benchmarks.ReactiveUI/ReactiveUIBindingBenchmark.cs
index bdaf0b9b..142833b6 100644
--- a/src/benchmarks/ReactiveUI.Binding.Benchmarks.ReactiveUI/ReactiveUIBindingBenchmark.cs
+++ b/src/benchmarks/ReactiveUI.Binding.Benchmarks.ReactiveUI/ReactiveUIBindingBenchmark.cs
@@ -4,23 +4,15 @@
using System.Diagnostics;
using BenchmarkDotNet.Attributes;
-using BenchmarkDotNet.Diagnosers;
-using BenchmarkDotNet.Jobs;
+using ReactiveUI.Binding.Benchmarks.Configs;
+using ReactiveUI.Binding.Benchmarks.Mocks;
+using ReactiveUI.Builder;
+using Splat;
namespace ReactiveUI.Binding.Benchmarks;
/// ReactiveUI expression-tree binding benchmarks for comparison.
-#if BENCH_NETFX
-[SimpleJob(RuntimeMoniker.Net462)]
-#endif
-[SimpleJob(RuntimeMoniker.Net80)]
-[SimpleJob(RuntimeMoniker.Net10_0)]
-[SimpleJob(RuntimeMoniker.Net11_0)]
-[MemoryDiagnoser]
-#if !BENCH_NETFX
-[EventPipeProfiler(EventPipeProfile.GcVerbose)]
-#endif
-[MarkdownExporterAttribute.GitHub]
+[Config(typeof(BenchmarkConfig))]
[DebuggerDisplay("Expression-tree binding over {PropertyChangeCount} changes")]
public class ReactiveUIBindingBenchmark
{
@@ -33,8 +25,15 @@ public class ReactiveUIBindingBenchmark
/// The target view instance used for binding benchmarks.
private BenchmarkView _target = null!;
- /// Initializes static members of the class. Ensures ReactiveUI is configured before any benchmarks run.
- static ReactiveUIBindingBenchmark() => ModuleInitializer.EnsureInitialized();
+ /// Configures ReactiveUI for the benchmark process.
+ [GlobalSetup]
+ public static void Register()
+ {
+ ModeDetector.OverrideModeDetector(new BenchmarkModeDetector());
+ _ = RxAppBuilder.CreateReactiveUIBuilder()
+ .WithCoreServices()
+ .BuildApp();
+ }
/// Sets up fresh source and target objects before each benchmark iteration.
[IterationSetup]
diff --git a/src/benchmarks/ReactiveUI.Binding.Benchmarks.ReactiveUI/ReactiveUIObservationBenchmark.cs b/src/benchmarks/ReactiveUI.Binding.Benchmarks.ReactiveUI/ReactiveUIObservationBenchmark.cs
index 0bc96add..98f2f5f5 100644
--- a/src/benchmarks/ReactiveUI.Binding.Benchmarks.ReactiveUI/ReactiveUIObservationBenchmark.cs
+++ b/src/benchmarks/ReactiveUI.Binding.Benchmarks.ReactiveUI/ReactiveUIObservationBenchmark.cs
@@ -4,23 +4,15 @@
using System.Diagnostics;
using BenchmarkDotNet.Attributes;
-using BenchmarkDotNet.Diagnosers;
-using BenchmarkDotNet.Jobs;
+using ReactiveUI.Binding.Benchmarks.Configs;
+using ReactiveUI.Binding.Benchmarks.Mocks;
+using ReactiveUI.Builder;
+using Splat;
namespace ReactiveUI.Binding.Benchmarks;
/// ReactiveUI expression-tree WhenAnyValue benchmarks for comparison.
-#if BENCH_NETFX
-[SimpleJob(RuntimeMoniker.Net462)]
-#endif
-[SimpleJob(RuntimeMoniker.Net80)]
-[SimpleJob(RuntimeMoniker.Net10_0)]
-[SimpleJob(RuntimeMoniker.Net11_0)]
-[MemoryDiagnoser]
-#if !BENCH_NETFX
-[EventPipeProfiler(EventPipeProfile.GcVerbose)]
-#endif
-[MarkdownExporterAttribute.GitHub]
+[Config(typeof(BenchmarkConfig))]
[DebuggerDisplay("Expression-tree observation over {PropertyChangeCount} changes")]
public class ReactiveUIObservationBenchmark
{
@@ -30,8 +22,15 @@ public class ReactiveUIObservationBenchmark
/// The view model instance used for observation benchmarks.
private BenchmarkViewModel _vm = null!;
- /// Initializes static members of the class. Ensures ReactiveUI is configured before any benchmarks run.
- static ReactiveUIObservationBenchmark() => ModuleInitializer.EnsureInitialized();
+ /// Configures ReactiveUI for the benchmark process.
+ [GlobalSetup]
+ public static void Register()
+ {
+ ModeDetector.OverrideModeDetector(new BenchmarkModeDetector());
+ _ = RxAppBuilder.CreateReactiveUIBuilder()
+ .WithCoreServices()
+ .BuildApp();
+ }
/// Sets up a fresh view model before each benchmark iteration.
[IterationSetup]
diff --git a/src/benchmarks/ReactiveUI.Binding.Benchmarks/BindBenchmark.cs b/src/benchmarks/ReactiveUI.Binding.Benchmarks/BindBenchmark.cs
index 6b299ce4..221f0764 100644
--- a/src/benchmarks/ReactiveUI.Binding.Benchmarks/BindBenchmark.cs
+++ b/src/benchmarks/ReactiveUI.Binding.Benchmarks/BindBenchmark.cs
@@ -3,8 +3,8 @@
// See the LICENSE file in the project root for full license information.
using BenchmarkDotNet.Attributes;
-using BenchmarkDotNet.Diagnosers;
-using BenchmarkDotNet.Jobs;
+using ReactiveUI.Binding.Benchmarks.Configs;
+using ReactiveUI.Binding.Benchmarks.Mocks;
namespace ReactiveUI.Binding.Benchmarks;
@@ -13,19 +13,7 @@ namespace ReactiveUI.Binding.Benchmarks;
/// and applied once, and the changes it wrote are published to whoever subscribes to the binding - so the cost
/// of making one, of driving it from either side, and of watching what it did are all measured here.
///
-#if BENCH_NETFX
-[SimpleJob(RuntimeMoniker.Net462)]
-#endif
-[SimpleJob(RuntimeMoniker.Net80)]
-[SimpleJob(RuntimeMoniker.Net10_0)]
-[SimpleJob(RuntimeMoniker.Net11_0)]
-[SimpleJob(RuntimeMoniker.NativeAot10_0, id: nameof(RuntimeMoniker.NativeAot10_0))]
-[SimpleJob(RuntimeMoniker.NativeAot11_0, id: nameof(RuntimeMoniker.NativeAot11_0))]
-[MemoryDiagnoser]
-#if !BENCH_NETFX
-[EventPipeProfiler(EventPipeProfile.GcVerbose)]
-#endif
-[MarkdownExporterAttribute.GitHub]
+[Config(typeof(NativeAotBenchmarkConfig))]
public class BindBenchmark
{
/// Represents the number of property change events to be triggered during the benchmark tests.
@@ -94,24 +82,4 @@ public void WithObservedChanges()
_viewModel.Name = $"Name_{i}";
}
}
-
- /// Counts what a binding reported without allocating per change.
- private sealed class CountingObserver : IObserver
- {
- /// Gets how many changes were reported.
- public int Count { get; private set; }
-
- ///
- public void OnNext(BindingChange value) => Count++;
-
- ///
- public void OnError(Exception error)
- {
- }
-
- ///
- public void OnCompleted()
- {
- }
- }
}
diff --git a/src/benchmarks/ReactiveUI.Binding.Benchmarks/BindOneWayBenchmark.cs b/src/benchmarks/ReactiveUI.Binding.Benchmarks/BindOneWayBenchmark.cs
index 522ff139..433372a2 100644
--- a/src/benchmarks/ReactiveUI.Binding.Benchmarks/BindOneWayBenchmark.cs
+++ b/src/benchmarks/ReactiveUI.Binding.Benchmarks/BindOneWayBenchmark.cs
@@ -3,26 +3,14 @@
// See the LICENSE file in the project root for full license information.
using BenchmarkDotNet.Attributes;
-using BenchmarkDotNet.Diagnosers;
-using BenchmarkDotNet.Jobs;
+using ReactiveUI.Binding.Benchmarks.Configs;
+using ReactiveUI.Binding.Benchmarks.Mocks;
using ReactiveUI.Primitives.Concurrency;
namespace ReactiveUI.Binding.Benchmarks;
/// Source-generated BindOneWay benchmarks with and without scheduler.
-#if BENCH_NETFX
-[SimpleJob(RuntimeMoniker.Net462)]
-#endif
-[SimpleJob(RuntimeMoniker.Net80)]
-[SimpleJob(RuntimeMoniker.Net10_0)]
-[SimpleJob(RuntimeMoniker.Net11_0)]
-[SimpleJob(RuntimeMoniker.NativeAot10_0, id: nameof(RuntimeMoniker.NativeAot10_0))]
-[SimpleJob(RuntimeMoniker.NativeAot11_0, id: nameof(RuntimeMoniker.NativeAot11_0))]
-[MemoryDiagnoser]
-#if !BENCH_NETFX
-[EventPipeProfiler(EventPipeProfile.GcVerbose)]
-#endif
-[MarkdownExporterAttribute.GitHub]
+[Config(typeof(NativeAotBenchmarkConfig))]
public class BindOneWayBenchmark
{
/// Represents the number of property change events to be triggered during the benchmark tests.
diff --git a/src/benchmarks/ReactiveUI.Binding.Benchmarks/BindToBenchmark.cs b/src/benchmarks/ReactiveUI.Binding.Benchmarks/BindToBenchmark.cs
index 9091e4bd..448c5303 100644
--- a/src/benchmarks/ReactiveUI.Binding.Benchmarks/BindToBenchmark.cs
+++ b/src/benchmarks/ReactiveUI.Binding.Benchmarks/BindToBenchmark.cs
@@ -3,25 +3,13 @@
// See the LICENSE file in the project root for full license information.
using BenchmarkDotNet.Attributes;
-using BenchmarkDotNet.Diagnosers;
-using BenchmarkDotNet.Jobs;
+using ReactiveUI.Binding.Benchmarks.Configs;
+using ReactiveUI.Binding.Benchmarks.Mocks;
namespace ReactiveUI.Binding.Benchmarks;
/// Source-generated BindTo benchmarks, which write a stream's values into a target property.
-#if BENCH_NETFX
-[SimpleJob(RuntimeMoniker.Net462)]
-#endif
-[SimpleJob(RuntimeMoniker.Net80)]
-[SimpleJob(RuntimeMoniker.Net10_0)]
-[SimpleJob(RuntimeMoniker.Net11_0)]
-[SimpleJob(RuntimeMoniker.NativeAot10_0, id: nameof(RuntimeMoniker.NativeAot10_0))]
-[SimpleJob(RuntimeMoniker.NativeAot11_0, id: nameof(RuntimeMoniker.NativeAot11_0))]
-[MemoryDiagnoser]
-#if !BENCH_NETFX
-[EventPipeProfiler(EventPipeProfile.GcVerbose)]
-#endif
-[MarkdownExporterAttribute.GitHub]
+[Config(typeof(NativeAotBenchmarkConfig))]
public class BindToBenchmark
{
/// How many values each benchmark pushes through one binding.
diff --git a/src/benchmarks/ReactiveUI.Binding.Benchmarks/BindTwoWayBenchmark.cs b/src/benchmarks/ReactiveUI.Binding.Benchmarks/BindTwoWayBenchmark.cs
index 4dd20a45..db5ae204 100644
--- a/src/benchmarks/ReactiveUI.Binding.Benchmarks/BindTwoWayBenchmark.cs
+++ b/src/benchmarks/ReactiveUI.Binding.Benchmarks/BindTwoWayBenchmark.cs
@@ -4,26 +4,14 @@
using System.Runtime.CompilerServices;
using BenchmarkDotNet.Attributes;
-using BenchmarkDotNet.Diagnosers;
-using BenchmarkDotNet.Jobs;
+using ReactiveUI.Binding.Benchmarks.Configs;
+using ReactiveUI.Binding.Benchmarks.Mocks;
using ReactiveUI.Primitives.Concurrency;
namespace ReactiveUI.Binding.Benchmarks;
/// Source-generated BindTwoWay benchmarks with and without scheduler.
-#if BENCH_NETFX
-[SimpleJob(RuntimeMoniker.Net462)]
-#endif
-[SimpleJob(RuntimeMoniker.Net80)]
-[SimpleJob(RuntimeMoniker.Net10_0)]
-[SimpleJob(RuntimeMoniker.Net11_0)]
-[SimpleJob(RuntimeMoniker.NativeAot10_0, id: nameof(RuntimeMoniker.NativeAot10_0))]
-[SimpleJob(RuntimeMoniker.NativeAot11_0, id: nameof(RuntimeMoniker.NativeAot11_0))]
-[MemoryDiagnoser]
-#if !BENCH_NETFX
-[EventPipeProfiler(EventPipeProfile.GcVerbose)]
-#endif
-[MarkdownExporterAttribute.GitHub]
+[Config(typeof(NativeAotBenchmarkConfig))]
public class BindTwoWayBenchmark
{
/// Represents the number of property change events to be triggered during the benchmark tests.
diff --git a/src/benchmarks/ReactiveUI.Binding.Benchmarks/BindingInitializer.cs b/src/benchmarks/ReactiveUI.Binding.Benchmarks/BindingInitializer.cs
deleted file mode 100644
index 001ebe8b..00000000
--- a/src/benchmarks/ReactiveUI.Binding.Benchmarks/BindingInitializer.cs
+++ /dev/null
@@ -1,32 +0,0 @@
-// Copyright (c) 2019-2026 ReactiveUI Association Incorporated. All rights reserved.
-// ReactiveUI Association Incorporated licenses this file to you under the MIT license.
-// See the LICENSE file in the project root for full license information.
-
-using ReactiveUI.Binding.Builder;
-using ReactiveUI.Binding.Mixins;
-
-namespace ReactiveUI.Binding.Benchmarks;
-
-/// Builds the runtime services once per process, for the benchmarks that resolve through them.
-///
-/// The generated path needs none of this: it names every type and member it touches. Only the reflection
-/// fallback resolves through the registered services, so only the benchmarks measuring it initialise them.
-///
-internal static class BindingInitializer
-{
- /// Guards the one-shot build.
- private static int _initialized;
-
- /// Builds the core services, at most once however many benchmark classes ask.
- internal static void EnsureInitialized()
- {
- if (Interlocked.Exchange(ref _initialized, 1) != 0)
- {
- return;
- }
-
- _ = RxBindingBuilder.CreateReactiveUIBindingBuilder()
- .WithCoreServices()
- .BuildApp();
- }
-}
diff --git a/src/benchmarks/ReactiveUI.Binding.Benchmarks/InteractionBenchmark.cs b/src/benchmarks/ReactiveUI.Binding.Benchmarks/InteractionBenchmark.cs
new file mode 100644
index 00000000..8ce85416
--- /dev/null
+++ b/src/benchmarks/ReactiveUI.Binding.Benchmarks/InteractionBenchmark.cs
@@ -0,0 +1,79 @@
+// Copyright (c) 2019-2026 ReactiveUI Association Incorporated. All rights reserved.
+// ReactiveUI Association Incorporated licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for full license information.
+
+using BenchmarkDotNet.Attributes;
+using ReactiveUI.Binding.Benchmarks.Configs;
+
+namespace ReactiveUI.Binding.Benchmarks;
+
+/// Interaction benchmarks: asking a question, and registering and removing handlers.
+[Config(typeof(NativeAotBenchmarkConfig))]
+public class InteractionBenchmark
+{
+ /// How many questions each Handle benchmark asks.
+ private const int QuestionCount = 1_000;
+
+ /// How many handlers each registration benchmark adds and removes.
+ private const int RegistrationCount = 10;
+
+ /// The interaction under measurement, with three handlers registered.
+ private Interaction _interaction = null!;
+
+ /// The handler registrations, released after each iteration.
+ private IDisposable[] _registrations = null!;
+
+ /// Registers one handler that answers and two later ones that pass, so every question walks all three.
+ [IterationSetup]
+ public void Setup()
+ {
+ _interaction = new();
+ _registrations =
+ [
+ _interaction.RegisterHandler(static context => context.SetOutput(context.Input.Length)),
+ _interaction.RegisterHandler(static _ => { }),
+ _interaction.RegisterHandler(static _ => { }),
+ ];
+ }
+
+ /// Releases the handler registrations.
+ [IterationCleanup]
+ public void Cleanup()
+ {
+ for (var i = 0; i < _registrations.Length; i++)
+ {
+ _registrations[i].Dispose();
+ }
+ }
+
+ /// Asks N questions that fall through two handlers to the one that answers.
+ /// The sum of the answers.
+ [Benchmark(Description = "Handle")]
+ public async Task Handle()
+ {
+ var total = 0;
+ for (var i = 0; i < QuestionCount; i++)
+ {
+ total += await _interaction.Handle("question").ConfigureAwait(false);
+ }
+
+ return total;
+ }
+
+ /// Registers ten more handlers beside the three already registered, then removes them.
+ [Benchmark(Description = "10x Register/Dispose")]
+ public void RegisterAndDispose()
+ {
+ var registrations = new IDisposable[RegistrationCount];
+
+ for (var i = 0; i < RegistrationCount; i++)
+ {
+ registrations[i] = _interaction.RegisterHandler(static context => context.SetOutput(0));
+ }
+
+ for (var i = 0; i < RegistrationCount; i++)
+ {
+ registrations[i].Dispose();
+ }
+ }
+}
diff --git a/src/benchmarks/ReactiveUI.Binding.Benchmarks/InvokeCommandBenchmark.cs b/src/benchmarks/ReactiveUI.Binding.Benchmarks/InvokeCommandBenchmark.cs
index 4a6b3ebb..b2835cb1 100644
--- a/src/benchmarks/ReactiveUI.Binding.Benchmarks/InvokeCommandBenchmark.cs
+++ b/src/benchmarks/ReactiveUI.Binding.Benchmarks/InvokeCommandBenchmark.cs
@@ -3,25 +3,13 @@
// See the LICENSE file in the project root for full license information.
using BenchmarkDotNet.Attributes;
-using BenchmarkDotNet.Diagnosers;
-using BenchmarkDotNet.Jobs;
+using ReactiveUI.Binding.Benchmarks.Configs;
+using ReactiveUI.Binding.Benchmarks.Mocks;
namespace ReactiveUI.Binding.Benchmarks;
/// Source-generated InvokeCommand benchmarks, which run a command with each value a stream produces.
-#if BENCH_NETFX
-[SimpleJob(RuntimeMoniker.Net462)]
-#endif
-[SimpleJob(RuntimeMoniker.Net80)]
-[SimpleJob(RuntimeMoniker.Net10_0)]
-[SimpleJob(RuntimeMoniker.Net11_0)]
-[SimpleJob(RuntimeMoniker.NativeAot10_0, id: nameof(RuntimeMoniker.NativeAot10_0))]
-[SimpleJob(RuntimeMoniker.NativeAot11_0, id: nameof(RuntimeMoniker.NativeAot11_0))]
-[MemoryDiagnoser]
-#if !BENCH_NETFX
-[EventPipeProfiler(EventPipeProfile.GcVerbose)]
-#endif
-[MarkdownExporterAttribute.GitHub]
+[Config(typeof(NativeAotBenchmarkConfig))]
public class InvokeCommandBenchmark
{
/// How many values each benchmark pushes through one invocation.
diff --git a/src/benchmarks/ReactiveUI.Binding.Benchmarks/BenchmarkChildViewModel.cs b/src/benchmarks/ReactiveUI.Binding.Benchmarks/Mocks/BenchmarkChildViewModel.cs
similarity index 95%
rename from src/benchmarks/ReactiveUI.Binding.Benchmarks/BenchmarkChildViewModel.cs
rename to src/benchmarks/ReactiveUI.Binding.Benchmarks/Mocks/BenchmarkChildViewModel.cs
index 09e53360..8cbaff25 100644
--- a/src/benchmarks/ReactiveUI.Binding.Benchmarks/BenchmarkChildViewModel.cs
+++ b/src/benchmarks/ReactiveUI.Binding.Benchmarks/Mocks/BenchmarkChildViewModel.cs
@@ -5,7 +5,7 @@
using System.ComponentModel;
using System.Diagnostics;
-namespace ReactiveUI.Binding.Benchmarks;
+namespace ReactiveUI.Binding.Benchmarks.Mocks;
/// A child view model for deep chain benchmarks.
[DebuggerDisplay("Value = {Value}")]
diff --git a/src/benchmarks/ReactiveUI.Binding.Benchmarks/BenchmarkCommand.cs b/src/benchmarks/ReactiveUI.Binding.Benchmarks/Mocks/BenchmarkCommand.cs
similarity index 95%
rename from src/benchmarks/ReactiveUI.Binding.Benchmarks/BenchmarkCommand.cs
rename to src/benchmarks/ReactiveUI.Binding.Benchmarks/Mocks/BenchmarkCommand.cs
index 45eb359e..66d2b58e 100644
--- a/src/benchmarks/ReactiveUI.Binding.Benchmarks/BenchmarkCommand.cs
+++ b/src/benchmarks/ReactiveUI.Binding.Benchmarks/Mocks/BenchmarkCommand.cs
@@ -5,7 +5,7 @@
using System.Runtime.CompilerServices;
using System.Windows.Input;
-namespace ReactiveUI.Binding.Benchmarks;
+namespace ReactiveUI.Binding.Benchmarks.Mocks;
/// A command that counts what reached it, so an invocation benchmark measures the wiring and not the body.
public sealed class BenchmarkCommand : ICommand
diff --git a/src/benchmarks/ReactiveUI.Binding.Benchmarks/BenchmarkSource.cs b/src/benchmarks/ReactiveUI.Binding.Benchmarks/Mocks/BenchmarkSource.cs
similarity index 97%
rename from src/benchmarks/ReactiveUI.Binding.Benchmarks/BenchmarkSource.cs
rename to src/benchmarks/ReactiveUI.Binding.Benchmarks/Mocks/BenchmarkSource.cs
index a4c0b2cc..c3efa63d 100644
--- a/src/benchmarks/ReactiveUI.Binding.Benchmarks/BenchmarkSource.cs
+++ b/src/benchmarks/ReactiveUI.Binding.Benchmarks/Mocks/BenchmarkSource.cs
@@ -4,7 +4,7 @@
using System.Runtime.CompilerServices;
-namespace ReactiveUI.Binding.Benchmarks;
+namespace ReactiveUI.Binding.Benchmarks.Mocks;
/// A stream the benchmark drives by hand, so the value count is the measurement.
/// The value type.
diff --git a/src/benchmarks/ReactiveUI.Binding.Benchmarks/BenchmarkView.cs b/src/benchmarks/ReactiveUI.Binding.Benchmarks/Mocks/BenchmarkView.cs
similarity index 97%
rename from src/benchmarks/ReactiveUI.Binding.Benchmarks/BenchmarkView.cs
rename to src/benchmarks/ReactiveUI.Binding.Benchmarks/Mocks/BenchmarkView.cs
index b3fe470b..4b8a44f1 100644
--- a/src/benchmarks/ReactiveUI.Binding.Benchmarks/BenchmarkView.cs
+++ b/src/benchmarks/ReactiveUI.Binding.Benchmarks/Mocks/BenchmarkView.cs
@@ -5,7 +5,7 @@
using System.ComponentModel;
using System.Diagnostics;
-namespace ReactiveUI.Binding.Benchmarks;
+namespace ReactiveUI.Binding.Benchmarks.Mocks;
/// A view used for binding benchmarks. Implements to support ReactiveUI's expression-tree-based binding APIs.
[DebuggerDisplay("DisplayName = {DisplayName}, DisplayAge = {DisplayAge}")]
diff --git a/src/benchmarks/ReactiveUI.Binding.Benchmarks/BenchmarkViewModel.cs b/src/benchmarks/ReactiveUI.Binding.Benchmarks/Mocks/BenchmarkViewModel.cs
similarity index 98%
rename from src/benchmarks/ReactiveUI.Binding.Benchmarks/BenchmarkViewModel.cs
rename to src/benchmarks/ReactiveUI.Binding.Benchmarks/Mocks/BenchmarkViewModel.cs
index 5b988f85..efacfc28 100644
--- a/src/benchmarks/ReactiveUI.Binding.Benchmarks/BenchmarkViewModel.cs
+++ b/src/benchmarks/ReactiveUI.Binding.Benchmarks/Mocks/BenchmarkViewModel.cs
@@ -6,7 +6,7 @@
using System.Diagnostics;
using System.Windows.Input;
-namespace ReactiveUI.Binding.Benchmarks;
+namespace ReactiveUI.Binding.Benchmarks.Mocks;
/// A view model used for benchmarking source-generated property observation and binding.
[DebuggerDisplay("Name = {Name}, Age = {Age}")]
diff --git a/src/benchmarks/ReactiveUI.Binding.Benchmarks/Mocks/CountingObserver.cs b/src/benchmarks/ReactiveUI.Binding.Benchmarks/Mocks/CountingObserver.cs
new file mode 100644
index 00000000..f88df964
--- /dev/null
+++ b/src/benchmarks/ReactiveUI.Binding.Benchmarks/Mocks/CountingObserver.cs
@@ -0,0 +1,25 @@
+// Copyright (c) 2019-2026 ReactiveUI Association Incorporated. All rights reserved.
+// ReactiveUI Association Incorporated licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for full license information.
+
+namespace ReactiveUI.Binding.Benchmarks.Mocks;
+
+/// Counts what a binding reported without allocating per change.
+internal sealed class CountingObserver : IObserver
+{
+ /// Gets how many changes were reported.
+ public int Count { get; private set; }
+
+ ///
+ public void OnNext(BindingChange value) => Count++;
+
+ ///
+ public void OnError(Exception error)
+ {
+ }
+
+ ///
+ public void OnCompleted()
+ {
+ }
+}
diff --git a/src/benchmarks/ReactiveUI.Binding.Benchmarks/OneWayBindBenchmark.cs b/src/benchmarks/ReactiveUI.Binding.Benchmarks/OneWayBindBenchmark.cs
index fb77925c..048ada79 100644
--- a/src/benchmarks/ReactiveUI.Binding.Benchmarks/OneWayBindBenchmark.cs
+++ b/src/benchmarks/ReactiveUI.Binding.Benchmarks/OneWayBindBenchmark.cs
@@ -3,25 +3,13 @@
// See the LICENSE file in the project root for full license information.
using BenchmarkDotNet.Attributes;
-using BenchmarkDotNet.Diagnosers;
-using BenchmarkDotNet.Jobs;
+using ReactiveUI.Binding.Benchmarks.Configs;
+using ReactiveUI.Binding.Benchmarks.Mocks;
namespace ReactiveUI.Binding.Benchmarks;
/// Source-generated OneWayBind benchmarks, which read against the ReactiveUI baseline of the same name.
-#if BENCH_NETFX
-[SimpleJob(RuntimeMoniker.Net462)]
-#endif
-[SimpleJob(RuntimeMoniker.Net80)]
-[SimpleJob(RuntimeMoniker.Net10_0)]
-[SimpleJob(RuntimeMoniker.Net11_0)]
-[SimpleJob(RuntimeMoniker.NativeAot10_0, id: nameof(RuntimeMoniker.NativeAot10_0))]
-[SimpleJob(RuntimeMoniker.NativeAot11_0, id: nameof(RuntimeMoniker.NativeAot11_0))]
-[MemoryDiagnoser]
-#if !BENCH_NETFX
-[EventPipeProfiler(EventPipeProfile.GcVerbose)]
-#endif
-[MarkdownExporterAttribute.GitHub]
+[Config(typeof(NativeAotBenchmarkConfig))]
public class OneWayBindBenchmark
{
/// How many property changes each benchmark drives through one binding.
diff --git a/src/benchmarks/ReactiveUI.Binding.Benchmarks/ReactiveUI.Binding.Benchmarks.csproj b/src/benchmarks/ReactiveUI.Binding.Benchmarks/ReactiveUI.Binding.Benchmarks.csproj
index 863db79a..fe90a2e2 100644
--- a/src/benchmarks/ReactiveUI.Binding.Benchmarks/ReactiveUI.Binding.Benchmarks.csproj
+++ b/src/benchmarks/ReactiveUI.Binding.Benchmarks/ReactiveUI.Binding.Benchmarks.csproj
@@ -1,21 +1,11 @@
- Exe
net8.0;net10.0;net11.0;net462
-
- $(DefineConstants);BENCH_NETFX
- enable
- false
true
-
diff --git a/src/benchmarks/ReactiveUI.Binding.Benchmarks/RxUiDynamicChainBaseline.cs b/src/benchmarks/ReactiveUI.Binding.Benchmarks/RxUiDynamicChainBaseline.cs
index ab58df93..d5a8786f 100644
--- a/src/benchmarks/ReactiveUI.Binding.Benchmarks/RxUiDynamicChainBaseline.cs
+++ b/src/benchmarks/ReactiveUI.Binding.Benchmarks/RxUiDynamicChainBaseline.cs
@@ -8,28 +8,17 @@
using System.Linq.Expressions;
using System.Runtime.CompilerServices;
using BenchmarkDotNet.Attributes;
-using BenchmarkDotNet.Diagnosers;
-using BenchmarkDotNet.Jobs;
using ReactiveUI;
+using ReactiveUI.Binding.Benchmarks.Configs;
using ReactiveUI.Builder;
-using BenchmarkVm = ReactiveUI.Binding.Benchmarks.BenchmarkViewModel;
+using BenchmarkVm = ReactiveUI.Binding.Benchmarks.Mocks.BenchmarkViewModel;
// Outside ReactiveUI.Binding so extension lookup reaches ReactiveUI's overloads; the view model comes in by alias,
// since importing its namespace would make every call ambiguous.
namespace RxUiDynamicChain;
/// The dynamic-chain scenarios of WhenAnyDynamicBenchmark, run against ReactiveUI's own engine.
-#if BENCH_NETFX
-[SimpleJob(RuntimeMoniker.Net462)]
-#endif
-[SimpleJob(RuntimeMoniker.Net80)]
-[SimpleJob(RuntimeMoniker.Net10_0)]
-[SimpleJob(RuntimeMoniker.Net11_0)]
-[MemoryDiagnoser]
-#if !BENCH_NETFX
-[EventPipeProfiler(EventPipeProfile.GcVerbose)]
-#endif
-[MarkdownExporterAttribute.GitHub]
+[Config(typeof(BenchmarkConfig))]
#if NET8_0_OR_GREATER
[RequiresUnreferencedCode("Evaluates expression-based member chains via reflection; members may be trimmed.")]
#endif
diff --git a/src/benchmarks/ReactiveUI.Binding.Benchmarks/UnsafeFallbackBenchmark.cs b/src/benchmarks/ReactiveUI.Binding.Benchmarks/UnsafeFallbackBenchmark.cs
index 0413c917..e106886b 100644
--- a/src/benchmarks/ReactiveUI.Binding.Benchmarks/UnsafeFallbackBenchmark.cs
+++ b/src/benchmarks/ReactiveUI.Binding.Benchmarks/UnsafeFallbackBenchmark.cs
@@ -6,8 +6,9 @@
using System.Diagnostics.CodeAnalysis;
#endif
using BenchmarkDotNet.Attributes;
-using BenchmarkDotNet.Diagnosers;
-using BenchmarkDotNet.Jobs;
+using ReactiveUI.Binding.Benchmarks.Configs;
+using ReactiveUI.Binding.Benchmarks.Mocks;
+using ReactiveUI.Binding.Builder;
namespace ReactiveUI.Binding.Benchmarks;
@@ -17,17 +18,7 @@ namespace ReactiveUI.Binding.Benchmarks;
/// walk the path at run time, so an ahead-of-time publish cannot be relied on to keep the members they reach.
/// Read these against the generated benchmark of the same operator to see what the fallback costs.
///
-#if BENCH_NETFX
-[SimpleJob(RuntimeMoniker.Net462)]
-#endif
-[SimpleJob(RuntimeMoniker.Net80)]
-[SimpleJob(RuntimeMoniker.Net10_0)]
-[SimpleJob(RuntimeMoniker.Net11_0)]
-[MemoryDiagnoser]
-#if !BENCH_NETFX
-[EventPipeProfiler(EventPipeProfile.GcVerbose)]
-#endif
-[MarkdownExporterAttribute.GitHub]
+[Config(typeof(BenchmarkConfig))]
#if NET8_0_OR_GREATER
[RequiresUnreferencedCode("Evaluates expression-based member chains via reflection; members may be trimmed.")]
#endif
@@ -42,8 +33,14 @@ public class UnsafeFallbackBenchmark
/// The binding's target.
private BenchmarkView _view = null!;
- /// Builds the runtime services the reflection path resolves through, once for the process.
- static UnsafeFallbackBenchmark() => BindingInitializer.EnsureInitialized();
+ /// Registers the runtime services the reflection path resolves through.
+ [GlobalSetup]
+ public static void Register()
+ {
+ var builder = RxBindingBuilder.CreateReactiveUIBindingBuilder();
+ _ = builder.WithCoreServices();
+ _ = builder.BuildApp();
+ }
/// Builds a fresh source and target before each iteration.
[IterationSetup]
diff --git a/src/benchmarks/ReactiveUI.Binding.Benchmarks/WhenAnyBenchmark.cs b/src/benchmarks/ReactiveUI.Binding.Benchmarks/WhenAnyBenchmark.cs
index 7e244d9e..1a3223a8 100644
--- a/src/benchmarks/ReactiveUI.Binding.Benchmarks/WhenAnyBenchmark.cs
+++ b/src/benchmarks/ReactiveUI.Binding.Benchmarks/WhenAnyBenchmark.cs
@@ -3,25 +3,13 @@
// See the LICENSE file in the project root for full license information.
using BenchmarkDotNet.Attributes;
-using BenchmarkDotNet.Diagnosers;
-using BenchmarkDotNet.Jobs;
+using ReactiveUI.Binding.Benchmarks.Configs;
+using ReactiveUI.Binding.Benchmarks.Mocks;
namespace ReactiveUI.Binding.Benchmarks;
/// Source-generated WhenAny benchmarks, which hand each change to a selector as an observed change.
-#if BENCH_NETFX
-[SimpleJob(RuntimeMoniker.Net462)]
-#endif
-[SimpleJob(RuntimeMoniker.Net80)]
-[SimpleJob(RuntimeMoniker.Net10_0)]
-[SimpleJob(RuntimeMoniker.Net11_0)]
-[SimpleJob(RuntimeMoniker.NativeAot10_0, id: nameof(RuntimeMoniker.NativeAot10_0))]
-[SimpleJob(RuntimeMoniker.NativeAot11_0, id: nameof(RuntimeMoniker.NativeAot11_0))]
-[MemoryDiagnoser]
-#if !BENCH_NETFX
-[EventPipeProfiler(EventPipeProfile.GcVerbose)]
-#endif
-[MarkdownExporterAttribute.GitHub]
+[Config(typeof(NativeAotBenchmarkConfig))]
public class WhenAnyBenchmark
{
/// How many property changes each benchmark drives through one subscription.
diff --git a/src/benchmarks/ReactiveUI.Binding.Benchmarks/WhenAnyDynamicBenchmark.cs b/src/benchmarks/ReactiveUI.Binding.Benchmarks/WhenAnyDynamicBenchmark.cs
index 4238320b..732ad126 100644
--- a/src/benchmarks/ReactiveUI.Binding.Benchmarks/WhenAnyDynamicBenchmark.cs
+++ b/src/benchmarks/ReactiveUI.Binding.Benchmarks/WhenAnyDynamicBenchmark.cs
@@ -8,24 +8,14 @@
using System.Linq.Expressions;
using System.Runtime.CompilerServices;
using BenchmarkDotNet.Attributes;
-using BenchmarkDotNet.Diagnosers;
-using BenchmarkDotNet.Jobs;
+using ReactiveUI.Binding.Benchmarks.Configs;
+using ReactiveUI.Binding.Benchmarks.Mocks;
using ReactiveUI.Binding.Builder;
namespace ReactiveUI.Binding.Benchmarks;
/// Benchmarks reflection-walked observation against the generated observation of the same chain.
-#if BENCH_NETFX
-[SimpleJob(RuntimeMoniker.Net462)]
-#endif
-[SimpleJob(RuntimeMoniker.Net80)]
-[SimpleJob(RuntimeMoniker.Net10_0)]
-[SimpleJob(RuntimeMoniker.Net11_0)]
-[MemoryDiagnoser]
-#if !BENCH_NETFX
-[EventPipeProfiler(EventPipeProfile.GcVerbose)]
-#endif
-[MarkdownExporterAttribute.GitHub]
+[Config(typeof(BenchmarkConfig))]
#if NET8_0_OR_GREATER
[RequiresUnreferencedCode("Evaluates expression-based member chains via reflection; members may be trimmed.")]
#endif
diff --git a/src/benchmarks/ReactiveUI.Binding.Benchmarks/WhenAnyObservableBenchmark.cs b/src/benchmarks/ReactiveUI.Binding.Benchmarks/WhenAnyObservableBenchmark.cs
index 2c79ce27..aaf35bcc 100644
--- a/src/benchmarks/ReactiveUI.Binding.Benchmarks/WhenAnyObservableBenchmark.cs
+++ b/src/benchmarks/ReactiveUI.Binding.Benchmarks/WhenAnyObservableBenchmark.cs
@@ -3,25 +3,13 @@
// See the LICENSE file in the project root for full license information.
using BenchmarkDotNet.Attributes;
-using BenchmarkDotNet.Diagnosers;
-using BenchmarkDotNet.Jobs;
+using ReactiveUI.Binding.Benchmarks.Configs;
+using ReactiveUI.Binding.Benchmarks.Mocks;
namespace ReactiveUI.Binding.Benchmarks;
/// Source-generated WhenAnyObservable benchmarks, which switch to whichever stream a property holds.
-#if BENCH_NETFX
-[SimpleJob(RuntimeMoniker.Net462)]
-#endif
-[SimpleJob(RuntimeMoniker.Net80)]
-[SimpleJob(RuntimeMoniker.Net10_0)]
-[SimpleJob(RuntimeMoniker.Net11_0)]
-[SimpleJob(RuntimeMoniker.NativeAot10_0, id: nameof(RuntimeMoniker.NativeAot10_0))]
-[SimpleJob(RuntimeMoniker.NativeAot11_0, id: nameof(RuntimeMoniker.NativeAot11_0))]
-[MemoryDiagnoser]
-#if !BENCH_NETFX
-[EventPipeProfiler(EventPipeProfile.GcVerbose)]
-#endif
-[MarkdownExporterAttribute.GitHub]
+[Config(typeof(NativeAotBenchmarkConfig))]
public class WhenAnyObservableBenchmark
{
/// How many values each benchmark pushes through the observed stream.
diff --git a/src/benchmarks/ReactiveUI.Binding.Benchmarks/WhenAnyValueBenchmark.cs b/src/benchmarks/ReactiveUI.Binding.Benchmarks/WhenAnyValueBenchmark.cs
index 28aa2e35..8fc03904 100644
--- a/src/benchmarks/ReactiveUI.Binding.Benchmarks/WhenAnyValueBenchmark.cs
+++ b/src/benchmarks/ReactiveUI.Binding.Benchmarks/WhenAnyValueBenchmark.cs
@@ -3,25 +3,13 @@
// See the LICENSE file in the project root for full license information.
using BenchmarkDotNet.Attributes;
-using BenchmarkDotNet.Diagnosers;
-using BenchmarkDotNet.Jobs;
+using ReactiveUI.Binding.Benchmarks.Configs;
+using ReactiveUI.Binding.Benchmarks.Mocks;
namespace ReactiveUI.Binding.Benchmarks;
/// Source-generated WhenAnyValue benchmarks, which read against the ReactiveUI baseline of the same name.
-#if BENCH_NETFX
-[SimpleJob(RuntimeMoniker.Net462)]
-#endif
-[SimpleJob(RuntimeMoniker.Net80)]
-[SimpleJob(RuntimeMoniker.Net10_0)]
-[SimpleJob(RuntimeMoniker.Net11_0)]
-[SimpleJob(RuntimeMoniker.NativeAot10_0, id: nameof(RuntimeMoniker.NativeAot10_0))]
-[SimpleJob(RuntimeMoniker.NativeAot11_0, id: nameof(RuntimeMoniker.NativeAot11_0))]
-[MemoryDiagnoser]
-#if !BENCH_NETFX
-[EventPipeProfiler(EventPipeProfile.GcVerbose)]
-#endif
-[MarkdownExporterAttribute.GitHub]
+[Config(typeof(NativeAotBenchmarkConfig))]
public class WhenAnyValueBenchmark
{
/// How many property changes each benchmark drives through one subscription.
diff --git a/src/benchmarks/ReactiveUI.Binding.Benchmarks/WhenChangedBenchmark.cs b/src/benchmarks/ReactiveUI.Binding.Benchmarks/WhenChangedBenchmark.cs
index fe8a59ee..59c03282 100644
--- a/src/benchmarks/ReactiveUI.Binding.Benchmarks/WhenChangedBenchmark.cs
+++ b/src/benchmarks/ReactiveUI.Binding.Benchmarks/WhenChangedBenchmark.cs
@@ -3,25 +3,13 @@
// See the LICENSE file in the project root for full license information.
using BenchmarkDotNet.Attributes;
-using BenchmarkDotNet.Diagnosers;
-using BenchmarkDotNet.Jobs;
+using ReactiveUI.Binding.Benchmarks.Configs;
+using ReactiveUI.Binding.Benchmarks.Mocks;
namespace ReactiveUI.Binding.Benchmarks;
/// Source-generated WhenChanged benchmarks using lightweight observables.
-#if BENCH_NETFX
-[SimpleJob(RuntimeMoniker.Net462)]
-#endif
-[SimpleJob(RuntimeMoniker.Net80)]
-[SimpleJob(RuntimeMoniker.Net10_0)]
-[SimpleJob(RuntimeMoniker.Net11_0)]
-[SimpleJob(RuntimeMoniker.NativeAot10_0, id: nameof(RuntimeMoniker.NativeAot10_0))]
-[SimpleJob(RuntimeMoniker.NativeAot11_0, id: nameof(RuntimeMoniker.NativeAot11_0))]
-[MemoryDiagnoser]
-#if !BENCH_NETFX
-[EventPipeProfiler(EventPipeProfile.GcVerbose)]
-#endif
-[MarkdownExporterAttribute.GitHub]
+[Config(typeof(NativeAotBenchmarkConfig))]
public class WhenChangedBenchmark
{
/// Represents the number of property change events to be triggered during the benchmark tests.
diff --git a/src/benchmarks/ReactiveUI.Binding.Benchmarks/WhenChangedContentionBenchmark.cs b/src/benchmarks/ReactiveUI.Binding.Benchmarks/WhenChangedContentionBenchmark.cs
new file mode 100644
index 00000000..6cd4a2b6
--- /dev/null
+++ b/src/benchmarks/ReactiveUI.Binding.Benchmarks/WhenChangedContentionBenchmark.cs
@@ -0,0 +1,48 @@
+// Copyright (c) 2019-2026 ReactiveUI Association Incorporated. All rights reserved.
+// ReactiveUI Association Incorporated licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for full license information.
+
+using BenchmarkDotNet.Attributes;
+using ReactiveUI.Binding.Benchmarks.Configs;
+using ReactiveUI.Binding.Benchmarks.Mocks;
+
+namespace ReactiveUI.Binding.Benchmarks;
+
+/// WhenChanged with two threads changing the observed property at the same time.
+[Config(typeof(NativeAotBenchmarkConfig))]
+public class WhenChangedContentionBenchmark
+{
+ /// How many property changes each writer drives.
+ private const int PropertyChangeCount = 1_000;
+
+ /// The view model under observation.
+ private BenchmarkViewModel _vm = null!;
+
+ /// Builds a fresh view model before each iteration.
+ [IterationSetup]
+ public void Setup() =>
+ _vm = new() { Name = "Initial" };
+
+ /// Two writers on separate threads, each driving N changes through one subscription.
+ [Benchmark(Description = "Two Writers")]
+ public void TwoWriters()
+ {
+ var last = string.Empty;
+ using var sub = _vm.WhenChanged(x => x.Name)
+ .Subscribe(v => last = v);
+
+ Parallel.Invoke(
+ () => Write("A_"),
+ () => Write("B_"));
+ }
+
+ /// Drives N changes, each to a value the other writer never uses.
+ /// The prefix that keeps this writer's values distinct.
+ private void Write(string prefix)
+ {
+ for (var i = 0; i < PropertyChangeCount; i++)
+ {
+ _vm.Name = prefix + i;
+ }
+ }
+}
diff --git a/src/benchmarks/ReactiveUI.Binding.Benchmarks/WhenChangingBenchmark.cs b/src/benchmarks/ReactiveUI.Binding.Benchmarks/WhenChangingBenchmark.cs
index 6313741f..081d0f8f 100644
--- a/src/benchmarks/ReactiveUI.Binding.Benchmarks/WhenChangingBenchmark.cs
+++ b/src/benchmarks/ReactiveUI.Binding.Benchmarks/WhenChangingBenchmark.cs
@@ -3,25 +3,13 @@
// See the LICENSE file in the project root for full license information.
using BenchmarkDotNet.Attributes;
-using BenchmarkDotNet.Diagnosers;
-using BenchmarkDotNet.Jobs;
+using ReactiveUI.Binding.Benchmarks.Configs;
+using ReactiveUI.Binding.Benchmarks.Mocks;
namespace ReactiveUI.Binding.Benchmarks;
/// Source-generated WhenChanging benchmarks, which observe before the value is replaced.
-#if BENCH_NETFX
-[SimpleJob(RuntimeMoniker.Net462)]
-#endif
-[SimpleJob(RuntimeMoniker.Net80)]
-[SimpleJob(RuntimeMoniker.Net10_0)]
-[SimpleJob(RuntimeMoniker.Net11_0)]
-[SimpleJob(RuntimeMoniker.NativeAot10_0, id: nameof(RuntimeMoniker.NativeAot10_0))]
-[SimpleJob(RuntimeMoniker.NativeAot11_0, id: nameof(RuntimeMoniker.NativeAot11_0))]
-[MemoryDiagnoser]
-#if !BENCH_NETFX
-[EventPipeProfiler(EventPipeProfile.GcVerbose)]
-#endif
-[MarkdownExporterAttribute.GitHub]
+[Config(typeof(NativeAotBenchmarkConfig))]
public class WhenChangingBenchmark
{
/// How many property changes each benchmark drives through one subscription.
diff --git a/src/benchmarks/ReactiveUI.Binding.Generator.Benchmarks.Roslyn413/ReactiveUI.Binding.Generator.Benchmarks.Roslyn413.csproj b/src/benchmarks/ReactiveUI.Binding.Generator.Benchmarks.Roslyn413/ReactiveUI.Binding.Generator.Benchmarks.Roslyn413.csproj
index b59b477f..9fc2e56a 100644
--- a/src/benchmarks/ReactiveUI.Binding.Generator.Benchmarks.Roslyn413/ReactiveUI.Binding.Generator.Benchmarks.Roslyn413.csproj
+++ b/src/benchmarks/ReactiveUI.Binding.Generator.Benchmarks.Roslyn413/ReactiveUI.Binding.Generator.Benchmarks.Roslyn413.csproj
@@ -3,16 +3,12 @@
- Exe
net8.0;net10.0;net11.0
- enable
- false
ReactiveUI.Binding.Generator.Benchmarks.Roslyn413
ReactiveUI.Binding.Generator.Benchmarks
-
@@ -27,6 +23,9 @@
+
diff --git a/src/benchmarks/ReactiveUI.Binding.Generator.Benchmarks/GenerationBenchmarks.cs b/src/benchmarks/ReactiveUI.Binding.Generator.Benchmarks/GenerationBenchmarks.cs
index 92e87a2a..b6dd317d 100644
--- a/src/benchmarks/ReactiveUI.Binding.Generator.Benchmarks/GenerationBenchmarks.cs
+++ b/src/benchmarks/ReactiveUI.Binding.Generator.Benchmarks/GenerationBenchmarks.cs
@@ -3,30 +3,24 @@
// See the LICENSE file in the project root for full license information.
using BenchmarkDotNet.Attributes;
-using BenchmarkDotNet.Diagnosers;
using Microsoft.CodeAnalysis;
+using ReactiveUI.Binding.Benchmarks.Configs;
+using ReactiveUI.Binding.Generator.Benchmarks.Support;
namespace ReactiveUI.Binding.Generator.Benchmarks;
-/// Measures a full generation pass over a corpus of consumer code.
+/// Measures a full generation pass over the mock consumer source.
///
/// The driver is rebuilt per iteration so each measurement is a cold generation, which is what a consumer's
/// build actually pays. Reusing a primed driver would measure the incremental cache instead, and hoisting the
/// driver into setup would let one iteration's caches serve the next.
///
-[MemoryDiagnoser]
-#if !BENCH_NETFX
-[EventPipeProfiler(EventPipeProfile.GcVerbose)]
-#endif
+[Config(typeof(ProfilerConfig))]
public class GenerationBenchmarks
{
- /// The corpus compilation, built once per parameter set.
+ /// The mock consumer compilation, built once per parameter set.
private Compilation _compilation = null!;
- /// Gets or sets how many view-model and view pairs the corpus holds.
- [Params(1, 16, 64)]
- public int Pairs { get; set; }
-
///
/// Gets or sets a value indicating whether the build lists the generated namespace, which is what decides
/// between claiming each call site outright and offering an overload that competes for them all.
@@ -35,16 +29,16 @@ public class GenerationBenchmarks
public bool Intercept { get; set; }
///
- /// Builds the corpus compilation once per parameter set. Loading a framework's worth of metadata
+ /// Builds the mock consumer compilation once per parameter set. Loading a framework's worth of metadata
/// references costs far more than a generation pass and is work the host build does once, so measuring it
/// per iteration would bury what this benchmark is for.
///
[GlobalSetup]
- public void Setup() => _compilation = GeneratorHarness.BuildCompilation(GeneratorCorpus.Build(Pairs), Intercept);
+ public void Setup() => _compilation = GeneratorHarness.BuildCompilation(Intercept);
/// Runs a whole cold generation: syntax scan, extraction, and emission.
/// The number of generated characters, returned so the work cannot be optimized away.
- /// The corpus generated nothing, so there is no result to report.
+ /// The mock consumer source generated nothing, so there is no result to report.
[Benchmark]
public int Generate()
{
@@ -59,10 +53,10 @@ public int Generate()
characters += generated.SourceText.Length;
}
- // A corpus that stopped matching the APIs would generate nothing and quietly turn this into a
+ // Mock source that stopped matching the APIs would generate nothing and quietly turn this into a
// measurement of driver overhead, so refuse to report a number for it.
return characters == 0
- ? throw new InvalidOperationException("The corpus generated no source; the benchmark is measuring nothing.")
+ ? throw new InvalidOperationException("The mock consumer source generated nothing; the benchmark is measuring nothing.")
: characters;
}
}
diff --git a/src/benchmarks/ReactiveUI.Binding.Generator.Benchmarks/GeneratorCorpus.cs b/src/benchmarks/ReactiveUI.Binding.Generator.Benchmarks/GeneratorCorpus.cs
deleted file mode 100644
index c773fba6..00000000
--- a/src/benchmarks/ReactiveUI.Binding.Generator.Benchmarks/GeneratorCorpus.cs
+++ /dev/null
@@ -1,114 +0,0 @@
-// Copyright (c) 2019-2026 ReactiveUI Association Incorporated. All rights reserved.
-// ReactiveUI Association Incorporated licenses this file to you under the MIT license.
-// See the LICENSE file in the project root for full license information.
-
-using System.Runtime.CompilerServices;
-using System.Text;
-
-namespace ReactiveUI.Binding.Generator.Benchmarks;
-
-///
-/// Builds consumer source for the generator to chew on: view-model and view pairs with a spread of call sites
-/// across the observation and binding APIs.
-///
-///
-/// A corpus rather than one call site, because the emitter's cost is per invocation and per group; a single
-/// call site measures mostly driver overhead and would hide whatever the emitter itself does.
-///
-internal static class GeneratorCorpus
-{
- /// Opens the body of a corpus type.
- private const string TypeBodyOpen = " {";
-
- /// Closes the body of a corpus type.
- private const string TypeBodyClose = " }";
-
- /// Names the view model parameter and opens the view parameter of a corpus call site.
- private const string ViewModelAndViewParameters = " vm, MyView";
-
- /// Roughly how many characters one view-model and view pair contributes.
- private const int PairSourceCapacity = 2_048;
-
- /// Builds a compilation unit containing the given number of view-model and view pairs.
- /// How many view-model and view pairs to emit.
- /// The source text.
- internal static string Build(int pairCount)
- {
- var sb = new StringBuilder(pairCount * PairSourceCapacity);
-
- _ = sb.AppendLine("using System;")
- .AppendLine("using System.ComponentModel;")
- .AppendLine("using System.Windows.Input;")
- .AppendLine("using ReactiveUI.Binding;")
- .AppendLine()
- .AppendLine("namespace Corpus")
- .AppendLine("{");
-
- for (var i = 0; i < pairCount; i++)
- {
- AppendPair(sb, i);
- }
-
- return sb.AppendLine("}").ToString();
- }
-
- /// Appends one view-model, view, and usage class.
- /// The builder to append to.
- /// The index that makes the emitted names unique.
- private static void AppendPair(StringBuilder sb, int index)
- {
- AppendTypes(sb, index);
- AppendUsage(sb, index);
- }
-
- /// Appends the view model, child, button, and view for one pair.
- /// The builder to append to.
- /// The index that makes the emitted names unique.
- [MethodImpl(MethodImplOptions.AggressiveInlining)]
- private static void AppendTypes(StringBuilder sb, int index) =>
- sb.Append(" public class Child").Append(index).AppendLine(" : INotifyPropertyChanged").AppendLine(TypeBodyOpen)
- .AppendLine(" public event PropertyChangedEventHandler PropertyChanged;").AppendLine()
- .AppendLine(" public string Nested { get; set; }").AppendLine(TypeBodyClose).AppendLine()
- .Append(" public class MyViewModel").Append(index).AppendLine(" : INotifyPropertyChanged").AppendLine(TypeBodyOpen)
- .AppendLine(" public event PropertyChangedEventHandler PropertyChanged;").AppendLine()
- .AppendLine(" public string Name { get; set; }").AppendLine().AppendLine(" public int Count { get; set; }")
- .AppendLine().AppendLine(" public bool Flag { get; set; }").AppendLine().Append(" public Child").Append(index)
- .AppendLine(" Child { get; set; }").AppendLine().AppendLine(" public ICommand Save { get; set; }").AppendLine(TypeBodyClose)
- .AppendLine().Append(" public class MyButton").Append(index).AppendLine().AppendLine(TypeBodyOpen)
- .AppendLine(" public event EventHandler Click;").AppendLine(TypeBodyClose).AppendLine().Append(" public class MyView")
- .Append(index).Append(" : IViewFor").AppendLine(TypeBodyOpen)
- .Append(" public MyViewModel").Append(index).AppendLine(" ViewModel { get; set; }").AppendLine()
- .Append(" object IViewFor.ViewModel { get => ViewModel; set => ViewModel = (MyViewModel").Append(index).AppendLine(")value; }")
- .AppendLine().AppendLine(" public string NameText { get; set; }").AppendLine()
- .AppendLine(" public string CountText { get; set; }").AppendLine()
- .AppendLine(" public bool FlagValue { get; set; }").AppendLine().Append(" public MyButton").Append(index)
- .AppendLine(" SaveButton { get; set; }").AppendLine(TypeBodyClose);
-
- /// Appends the call sites for one pair, spread across the observation and binding APIs.
- /// The builder to append to.
- /// The index that makes the emitted names unique.
- [MethodImpl(MethodImplOptions.AggressiveInlining)]
- private static void AppendUsage(StringBuilder sb, int index) =>
- sb.Append(" public static class Usage").Append(index).AppendLine().AppendLine(TypeBodyOpen)
- .Append(" public static IObservable ObserveName(MyViewModel").Append(index)
- .AppendLine(" vm) => vm.WhenChanged(x => x.Name);").AppendLine()
- .Append(" public static IObservable ObserveNested(MyViewModel").Append(index)
- .AppendLine(" vm) => vm.WhenChanged(x => x.Child.Nested);").AppendLine()
- .Append(" public static IObservable<(string, int)> ObserveBoth(MyViewModel").Append(index)
- .AppendLine(" vm) => vm.WhenChanged(x => x.Name, x => x.Count);").AppendLine()
- .Append(" public static IObservable ObserveChanging(MyViewModel").Append(index)
- .AppendLine(" vm) => vm.WhenChanging(x => x.Count);").AppendLine()
- .Append(" public static IObservable AnyValue(MyViewModel").Append(index)
- .AppendLine(" vm) => vm.WhenAnyValue(x => x.Name);").AppendLine().Append(" public static IDisposable BindName(MyViewModel")
- .Append(index).Append(ViewModelAndViewParameters).Append(index).AppendLine(" view) => vm.BindOneWay(view, x => x.Name, x => x.NameText);").AppendLine()
- .Append(" public static IDisposable BindFlag(MyViewModel").Append(index).Append(ViewModelAndViewParameters).Append(index)
- .AppendLine(" view) => vm.BindTwoWay(view, x => x.Flag, x => x.FlagValue);").AppendLine()
- .Append(" public static IDisposable OneWay(MyViewModel").Append(index).Append(ViewModelAndViewParameters).Append(index)
- .AppendLine(" view) => view.OneWayBind(vm, x => x.Name, x => x.NameText);").AppendLine()
- .Append(" public static IReactiveBinding TwoWay(MyViewModel").Append(index)
- .Append(ViewModelAndViewParameters).Append(index).AppendLine(" view) => view.Bind(vm, x => x.Name, x => x.NameText);").AppendLine()
- .Append(" public static IDisposable Command(MyViewModel").Append(index).Append(ViewModelAndViewParameters).Append(index)
- .AppendLine(" view) => view.BindCommand(vm, x => x.Save, x => x.SaveButton);").AppendLine()
- .Append(" public static IDisposable ToTarget(MyViewModel").Append(index).Append(ViewModelAndViewParameters).Append(index)
- .AppendLine(" view) => vm.WhenChanged(x => x.Name).BindTo(view, x => x.NameText);").AppendLine(TypeBodyClose).AppendLine();
-}
diff --git a/src/benchmarks/ReactiveUI.Binding.Generator.Benchmarks/Mocks/AddressViewModel.cs b/src/benchmarks/ReactiveUI.Binding.Generator.Benchmarks/Mocks/AddressViewModel.cs
new file mode 100644
index 00000000..e58f5bad
--- /dev/null
+++ b/src/benchmarks/ReactiveUI.Binding.Generator.Benchmarks/Mocks/AddressViewModel.cs
@@ -0,0 +1,30 @@
+// Copyright (c) 2019-2026 ReactiveUI Association Incorporated. All rights reserved.
+// ReactiveUI Association Incorporated licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for full license information.
+
+using System.ComponentModel;
+
+namespace ReactiveUI.Binding.Generator.Benchmarks.Mocks;
+
+/// A nested view model, so the mock bindings observe a chain of two properties.
+public class AddressViewModel : INotifyPropertyChanged
+{
+ ///
+ public event PropertyChangedEventHandler? PropertyChanged;
+
+ /// Gets or sets the city.
+ public string City
+ {
+ get => field;
+ set
+ {
+ if (field == value)
+ {
+ return;
+ }
+
+ field = value;
+ PropertyChanged?.Invoke(this, new(nameof(City)));
+ }
+ } = string.Empty;
+}
diff --git a/src/benchmarks/ReactiveUI.Binding.Generator.Benchmarks/Mocks/PersonBindings.cs b/src/benchmarks/ReactiveUI.Binding.Generator.Benchmarks/Mocks/PersonBindings.cs
new file mode 100644
index 00000000..6b8bacaa
--- /dev/null
+++ b/src/benchmarks/ReactiveUI.Binding.Generator.Benchmarks/Mocks/PersonBindings.cs
@@ -0,0 +1,95 @@
+// Copyright (c) 2019-2026 ReactiveUI Association Incorporated. All rights reserved.
+// ReactiveUI Association Incorporated licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for full license information.
+
+using System;
+using System.Runtime.CompilerServices;
+
+namespace ReactiveUI.Binding.Generator.Benchmarks.Mocks;
+
+/// One call site for each observation and binding API the generator writes code for.
+public static class PersonBindings
+{
+ /// Observes the name.
+ /// The view model to observe.
+ /// The name, now and after each change.
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public static IObservable ObserveName(PersonViewModel viewModel) =>
+ viewModel.WhenChanged(x => x.Name);
+
+ /// Observes the city through the address.
+ /// The view model to observe.
+ /// The city, now and after each change to either link.
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public static IObservable ObserveCity(PersonViewModel viewModel) =>
+ viewModel.WhenChanged(x => x.Address.City);
+
+ /// Observes the name and the age together.
+ /// The view model to observe.
+ /// Both values, now and after each change to either.
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public static IObservable> ObserveNameAndAge(PersonViewModel viewModel) =>
+ viewModel.WhenChanged(x => x.Name, x => x.Age);
+
+ /// Observes the age before each change.
+ /// The view model to observe.
+ /// The age, now and before each change.
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public static IObservable ObserveAgeChanging(PersonViewModel viewModel) =>
+ viewModel.WhenChanging(x => x.Age);
+
+ /// Observes the name under the ReactiveUI-compatible name.
+ /// The view model to observe.
+ /// The name, now and after each change.
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public static IObservable ObserveNameValue(PersonViewModel viewModel) =>
+ viewModel.WhenAnyValue(x => x.Name);
+
+ /// Writes the name to the view.
+ /// The view model to read.
+ /// The view to write.
+ /// The binding.
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public static IDisposable BindName(PersonViewModel viewModel, PersonView view) =>
+ viewModel.BindOneWay(view, x => x.Name, x => x.NameText);
+
+ /// Keeps the active flag and the active box in step.
+ /// The view model to bind.
+ /// The view to bind.
+ /// The binding.
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public static IDisposable BindIsActive(PersonViewModel viewModel, PersonView view) =>
+ viewModel.BindTwoWay(view, x => x.IsActive, x => x.IsActiveValue);
+
+ /// Writes the name to the view, starting from the view.
+ /// The view model to read.
+ /// The view to write.
+ /// The binding.
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public static IDisposable OneWayBindName(PersonViewModel viewModel, PersonView view) =>
+ view.OneWayBind(viewModel, x => x.Name, x => x.NameText);
+
+ /// Keeps the name and the name text in step, starting from the view.
+ /// The view model to bind.
+ /// The view to bind.
+ /// The binding.
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public static IDisposable BindNameBothWays(PersonViewModel viewModel, PersonView view) =>
+ view.Bind(viewModel, x => x.Name, x => x.NameText);
+
+ /// Runs the save command when the save button is clicked.
+ /// The view model holding the command.
+ /// The view holding the button.
+ /// The binding.
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public static IDisposable BindSave(PersonViewModel viewModel, PersonView view) =>
+ view.BindCommand(viewModel, x => x.Save, x => x.SaveButton);
+
+ /// Writes each name the view model reports into the view.
+ /// The view model to observe.
+ /// The view to write.
+ /// The binding.
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public static IDisposable BindNameTo(PersonViewModel viewModel, PersonView view) =>
+ viewModel.WhenChanged(x => x.Name).BindTo(view, x => x.NameText);
+}
diff --git a/src/benchmarks/ReactiveUI.Binding.Generator.Benchmarks/Mocks/PersonView.cs b/src/benchmarks/ReactiveUI.Binding.Generator.Benchmarks/Mocks/PersonView.cs
new file mode 100644
index 00000000..70e30e4d
--- /dev/null
+++ b/src/benchmarks/ReactiveUI.Binding.Generator.Benchmarks/Mocks/PersonView.cs
@@ -0,0 +1,62 @@
+// Copyright (c) 2019-2026 ReactiveUI Association Incorporated. All rights reserved.
+// ReactiveUI Association Incorporated licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for full license information.
+
+using System.Collections.Generic;
+using System.ComponentModel;
+
+namespace ReactiveUI.Binding.Generator.Benchmarks.Mocks;
+
+/// A view with the controls the mock bindings write to.
+public class PersonView : IViewFor, INotifyPropertyChanged
+{
+ ///
+ public event PropertyChangedEventHandler? PropertyChanged;
+
+ ///
+ public PersonViewModel? ViewModel
+ {
+ get => field;
+ set => SetField(ref field, value, nameof(ViewModel));
+ }
+
+ ///
+ object? IViewFor.ViewModel
+ {
+ get => ViewModel;
+ set => ViewModel = (PersonViewModel?)value;
+ }
+
+ /// Gets or sets the text showing the name.
+ public string NameText
+ {
+ get => field;
+ set => SetField(ref field, value, nameof(NameText));
+ } = string.Empty;
+
+ /// Gets or sets a value indicating whether the active box is checked.
+ public bool IsActiveValue
+ {
+ get => field;
+ set => SetField(ref field, value, nameof(IsActiveValue));
+ }
+
+ /// Gets the button that runs the save command.
+ public SaveButton SaveButton { get; } = new();
+
+ /// Writes a property's value, raising the change event after the write.
+ /// The property type.
+ /// The property's backing field.
+ /// The new value.
+ /// The property being written.
+ private void SetField(ref T storage, T value, string propertyName)
+ {
+ if (EqualityComparer.Default.Equals(storage, value))
+ {
+ return;
+ }
+
+ storage = value;
+ PropertyChanged?.Invoke(this, new(propertyName));
+ }
+}
diff --git a/src/benchmarks/ReactiveUI.Binding.Generator.Benchmarks/Mocks/PersonViewModel.cs b/src/benchmarks/ReactiveUI.Binding.Generator.Benchmarks/Mocks/PersonViewModel.cs
new file mode 100644
index 00000000..e7200015
--- /dev/null
+++ b/src/benchmarks/ReactiveUI.Binding.Generator.Benchmarks/Mocks/PersonViewModel.cs
@@ -0,0 +1,71 @@
+// Copyright (c) 2019-2026 ReactiveUI Association Incorporated. All rights reserved.
+// ReactiveUI Association Incorporated licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for full license information.
+
+using System.Collections.Generic;
+using System.ComponentModel;
+using System.Windows.Input;
+
+namespace ReactiveUI.Binding.Generator.Benchmarks.Mocks;
+
+/// A view model with the property shapes the mock bindings read: text, a number, a flag, a nested object and a command.
+public class PersonViewModel : INotifyPropertyChanged, INotifyPropertyChanging
+{
+ ///
+ public event PropertyChangedEventHandler? PropertyChanged;
+
+ ///
+ public event PropertyChangingEventHandler? PropertyChanging;
+
+ /// Gets or sets the person's name.
+ public string Name
+ {
+ get => field;
+ set => SetField(ref field, value, nameof(Name));
+ } = string.Empty;
+
+ /// Gets or sets the person's age.
+ public int Age
+ {
+ get => field;
+ set => SetField(ref field, value, nameof(Age));
+ }
+
+ /// Gets or sets a value indicating whether the person is active.
+ public bool IsActive
+ {
+ get => field;
+ set => SetField(ref field, value, nameof(IsActive));
+ }
+
+ /// Gets or sets the person's address.
+ public AddressViewModel Address
+ {
+ get => field;
+ set => SetField(ref field, value, nameof(Address));
+ } = new();
+
+ /// Gets or sets the command that saves the person.
+ public ICommand? Save
+ {
+ get => field;
+ set => SetField(ref field, value, nameof(Save));
+ }
+
+ /// Writes a property's value, raising the change events around the write.
+ /// The property type.
+ /// The property's backing field.
+ /// The new value.
+ /// The property being written.
+ private void SetField(ref T storage, T value, string propertyName)
+ {
+ if (EqualityComparer.Default.Equals(storage, value))
+ {
+ return;
+ }
+
+ PropertyChanging?.Invoke(this, new(propertyName));
+ storage = value;
+ PropertyChanged?.Invoke(this, new(propertyName));
+ }
+}
diff --git a/src/benchmarks/ReactiveUI.Binding.Generator.Benchmarks/Mocks/SaveButton.cs b/src/benchmarks/ReactiveUI.Binding.Generator.Benchmarks/Mocks/SaveButton.cs
new file mode 100644
index 00000000..a496a4dc
--- /dev/null
+++ b/src/benchmarks/ReactiveUI.Binding.Generator.Benchmarks/Mocks/SaveButton.cs
@@ -0,0 +1,19 @@
+// Copyright (c) 2019-2026 ReactiveUI Association Incorporated. All rights reserved.
+// ReactiveUI Association Incorporated licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for full license information.
+
+using System;
+using System.Runtime.CompilerServices;
+
+namespace ReactiveUI.Binding.Generator.Benchmarks.Mocks;
+
+/// A button the mock command binding attaches to through its click event.
+public class SaveButton
+{
+ /// Occurs when the button is clicked.
+ public event EventHandler? Click;
+
+ /// Raises .
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public void PerformClick() => Click?.Invoke(this, EventArgs.Empty);
+}
diff --git a/src/benchmarks/ReactiveUI.Binding.Generator.Benchmarks/ReactiveUI.Binding.Generator.Benchmarks.csproj b/src/benchmarks/ReactiveUI.Binding.Generator.Benchmarks/ReactiveUI.Binding.Generator.Benchmarks.csproj
index 551d7c65..b6736990 100644
--- a/src/benchmarks/ReactiveUI.Binding.Generator.Benchmarks/ReactiveUI.Binding.Generator.Benchmarks.csproj
+++ b/src/benchmarks/ReactiveUI.Binding.Generator.Benchmarks/ReactiveUI.Binding.Generator.Benchmarks.csproj
@@ -1,15 +1,10 @@
- Exe
net8.0;net10.0;net11.0
- enable
- false
-
-
@@ -23,4 +18,8 @@
+
+
+
+
diff --git a/src/benchmarks/ReactiveUI.Binding.Generator.Benchmarks/GeneratorHarness.cs b/src/benchmarks/ReactiveUI.Binding.Generator.Benchmarks/Support/GeneratorHarness.cs
similarity index 67%
rename from src/benchmarks/ReactiveUI.Binding.Generator.Benchmarks/GeneratorHarness.cs
rename to src/benchmarks/ReactiveUI.Binding.Generator.Benchmarks/Support/GeneratorHarness.cs
index 0b5dee3d..286d9767 100644
--- a/src/benchmarks/ReactiveUI.Binding.Generator.Benchmarks/GeneratorHarness.cs
+++ b/src/benchmarks/ReactiveUI.Binding.Generator.Benchmarks/Support/GeneratorHarness.cs
@@ -7,13 +7,19 @@
using Microsoft.CodeAnalysis.CSharp;
using ReactiveUI.Binding.SourceGenerators;
-namespace ReactiveUI.Binding.Generator.Benchmarks;
+namespace ReactiveUI.Binding.Generator.Benchmarks.Support;
-/// Shared setup for the generator benchmarks: compilations and drivers over a corpus.
+/// Builds the compilation and the generator driver the generation benchmarks run.
internal static class GeneratorHarness
{
- /// The assembly name given to the throwaway compilation the generator runs against.
- private const string CompilationAssemblyName = "Corpus";
+ /// The assembly name given to the compilation the generator runs against.
+ private const string CompilationAssemblyName = "Mocks";
+
+ /// The folder beside the benchmark assembly that holds the mock consumer source.
+ private const string MocksFolder = "Mocks";
+
+ /// Matches the C# source files in the mocks folder.
+ private const string SourceFilePattern = "*.cs";
/// The feature a build lists interceptable namespaces under.
private const string InterceptorsNamespacesFeature = "InterceptorsNamespaces";
@@ -29,20 +35,28 @@ internal static class GeneratorHarness
/// The parse options.
internal static CSharpParseOptions ParseOptions(bool intercept)
{
- var parseOptions = new CSharpParseOptions(LanguageVersion.CSharp10);
+ var parseOptions = new CSharpParseOptions(LanguageVersion.Latest);
return intercept
? parseOptions.WithFeatures([new KeyValuePair(InterceptorsNamespacesFeature, InterceptorNamespace)])
: parseOptions;
}
- /// Builds a compilation over the corpus source.
- /// The corpus source text.
+ /// Builds a compilation over the mock consumer source copied beside the benchmark assembly.
/// Whether the build lists the generated namespace for interception.
/// The compilation.
- internal static CSharpCompilation BuildCompilation(string sourceText, bool intercept)
+ internal static CSharpCompilation BuildCompilation(bool intercept)
{
- var syntaxTree = CSharpSyntaxTree.ParseText(sourceText, ParseOptions(intercept));
+ var parseOptions = ParseOptions(intercept);
+ var paths = Directory.GetFiles(Path.Combine(AppContext.BaseDirectory, MocksFolder), SourceFilePattern);
+ Array.Sort(paths, StringComparer.Ordinal);
+
+ var syntaxTrees = new SyntaxTree[paths.Length];
+ for (var i = 0; i < paths.Length; i++)
+ {
+ using var reader = File.OpenText(paths[i]);
+ syntaxTrees[i] = CSharpSyntaxTree.ParseText(reader.ReadToEnd(), parseOptions, paths[i]);
+ }
var references = new List(Basic.Reference.Assemblies.Net80.References.All)
{
@@ -53,7 +67,7 @@ internal static CSharpCompilation BuildCompilation(string sourceText, bool inter
return CSharpCompilation.Create(
CompilationAssemblyName,
- [syntaxTree],
+ syntaxTrees,
references,
new(OutputKind.DynamicallyLinkedLibrary));
}
diff --git a/src/benchmarks/Shared/Configs/NativeAotBenchmarkConfig.cs b/src/benchmarks/Shared/Configs/NativeAotBenchmarkConfig.cs
new file mode 100644
index 00000000..ebab9439
--- /dev/null
+++ b/src/benchmarks/Shared/Configs/NativeAotBenchmarkConfig.cs
@@ -0,0 +1,19 @@
+// Copyright (c) 2019-2026 ReactiveUI Association Incorporated. All rights reserved.
+// ReactiveUI Association Incorporated licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for full license information.
+
+using BenchmarkDotNet.Environments;
+using BenchmarkDotNet.Jobs;
+
+namespace ReactiveUI.Binding.Benchmarks.Configs;
+
+/// Adds the NativeAOT runtimes to , for benchmarks whose code publishes ahead of time.
+public class NativeAotBenchmarkConfig : BenchmarkConfig
+{
+ /// Initializes a new instance of the class.
+ public NativeAotBenchmarkConfig()
+ {
+ _ = AddJob(new Job(nameof(RuntimeMoniker.NativeAot10_0)).WithRuntime(NativeAotRuntime.Net10_0));
+ _ = AddJob(new Job(nameof(RuntimeMoniker.NativeAot11_0)).WithRuntime(NativeAotRuntime.Net11_0));
+ }
+}
diff --git a/src/benchmarks/Shared/Platforms/unix/BenchmarkConfig.cs b/src/benchmarks/Shared/Platforms/unix/BenchmarkConfig.cs
new file mode 100644
index 00000000..876a4e41
--- /dev/null
+++ b/src/benchmarks/Shared/Platforms/unix/BenchmarkConfig.cs
@@ -0,0 +1,22 @@
+// Copyright (c) 2019-2026 ReactiveUI Association Incorporated. All rights reserved.
+// ReactiveUI Association Incorporated licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for full license information.
+
+using BenchmarkDotNet.Environments;
+using BenchmarkDotNet.Exporters;
+using BenchmarkDotNet.Jobs;
+
+namespace ReactiveUI.Binding.Benchmarks.Configs;
+
+/// Runs a benchmark on .NET 8, 10 and 11.
+public class BenchmarkConfig : ProfilerConfig
+{
+ /// Initializes a new instance of the class.
+ public BenchmarkConfig()
+ {
+ _ = AddJob(new Job().WithRuntime(CoreRuntime.Core80));
+ _ = AddJob(new Job().WithRuntime(CoreRuntime.Core10_0));
+ _ = AddJob(new Job().WithRuntime(CoreRuntime.Core11_0));
+ _ = AddExporter(MarkdownExporter.GitHub);
+ }
+}
diff --git a/src/benchmarks/Shared/Platforms/unix/ProfilerConfig.cs b/src/benchmarks/Shared/Platforms/unix/ProfilerConfig.cs
new file mode 100644
index 00000000..d8398b94
--- /dev/null
+++ b/src/benchmarks/Shared/Platforms/unix/ProfilerConfig.cs
@@ -0,0 +1,38 @@
+// Copyright (c) 2019-2026 ReactiveUI Association Incorporated. All rights reserved.
+// ReactiveUI Association Incorporated licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for full license information.
+
+using System.Diagnostics.Tracing;
+using BenchmarkDotNet.Configs;
+using BenchmarkDotNet.Diagnosers;
+using Microsoft.Diagnostics.NETCore.Client;
+using Microsoft.Diagnostics.Tracing.Parsers;
+
+namespace ReactiveUI.Binding.Benchmarks.Configs;
+
+/// Traces every benchmark with EventPipe, recording CPU samples and verbose GC events in one trace.
+///
+/// A config keeps one EventPipe profiler, and the CPU and GC profiles both enable the runtime provider. So the
+/// sample profiler is added beside the GC profile, and the runtime provider carries the keywords of both.
+///
+public class ProfilerConfig : ManualConfig
+{
+ /// The provider that samples CPU stacks.
+ private const string SampleProfilerProviderName = "Microsoft-DotNETCore-SampleProfiler";
+
+ /// The runtime keywords of both profiles: the CPU profile's defaults, which include GC and exceptions, plus GC handles.
+ private const ClrTraceEventParser.Keywords RuntimeKeywords =
+ ClrTraceEventParser.Keywords.Default | ClrTraceEventParser.Keywords.GCHandle;
+
+ /// Initializes a new instance of the class.
+ public ProfilerConfig()
+ {
+ EventPipeProvider[] providers =
+ [
+ new(SampleProfilerProviderName, EventLevel.Informational),
+ new(ClrTraceEventParser.ProviderName, EventLevel.Verbose, (long)RuntimeKeywords),
+ ];
+
+ _ = AddDiagnoser(new EventPipeProfiler(EventPipeProfile.GcVerbose, providers));
+ }
+}
diff --git a/src/benchmarks/Shared/Platforms/windows/BenchmarkConfig.cs b/src/benchmarks/Shared/Platforms/windows/BenchmarkConfig.cs
new file mode 100644
index 00000000..fe087b89
--- /dev/null
+++ b/src/benchmarks/Shared/Platforms/windows/BenchmarkConfig.cs
@@ -0,0 +1,23 @@
+// Copyright (c) 2019-2026 ReactiveUI Association Incorporated. All rights reserved.
+// ReactiveUI Association Incorporated licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for full license information.
+
+using BenchmarkDotNet.Environments;
+using BenchmarkDotNet.Exporters;
+using BenchmarkDotNet.Jobs;
+
+namespace ReactiveUI.Binding.Benchmarks.Configs;
+
+/// Runs a benchmark on .NET Framework 4.6.2 and on .NET 8, 10 and 11.
+public class BenchmarkConfig : ProfilerConfig
+{
+ /// Initializes a new instance of the class.
+ public BenchmarkConfig()
+ {
+ _ = AddJob(new Job().WithRuntime(ClrRuntime.Net462));
+ _ = AddJob(new Job().WithRuntime(CoreRuntime.Core80));
+ _ = AddJob(new Job().WithRuntime(CoreRuntime.Core10_0));
+ _ = AddJob(new Job().WithRuntime(CoreRuntime.Core11_0));
+ _ = AddExporter(MarkdownExporter.GitHub);
+ }
+}
diff --git a/src/benchmarks/Shared/Platforms/windows/ProfilerConfig.cs b/src/benchmarks/Shared/Platforms/windows/ProfilerConfig.cs
new file mode 100644
index 00000000..5b2d1df0
--- /dev/null
+++ b/src/benchmarks/Shared/Platforms/windows/ProfilerConfig.cs
@@ -0,0 +1,15 @@
+// Copyright (c) 2019-2026 ReactiveUI Association Incorporated. All rights reserved.
+// ReactiveUI Association Incorporated licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for full license information.
+
+using BenchmarkDotNet.Configs;
+using BenchmarkDotNet.Diagnostics.Windows;
+
+namespace ReactiveUI.Binding.Benchmarks.Configs;
+
+/// Traces every benchmark with ETW, which records CPU samples and GC events for .NET Framework and .NET alike.
+public class ProfilerConfig : ManualConfig
+{
+ /// Initializes a new instance of the class.
+ public ProfilerConfig() => _ = AddDiagnoser(new EtwProfiler(new EtwProfilerConfig()));
+}
From aea71ed85b2b41ccd68b0d5c3a2a1b883e7bd5e8 Mon Sep 17 00:00:00 2001
From: Glenn Watson <5834289+glennawatson@users.noreply.github.com>
Date: Mon, 14 Sep 2026 22:42:19 +1000
Subject: [PATCH 2/3] test(benchmarks): trace .NET jobs with EventPipe and .NET
Framework with ETW
---
.../Program.cs | 5 ++-
.../ReactiveUIBindingBenchmark.cs | 2 --
.../ReactiveUIObservationBenchmark.cs | 2 --
.../ReactiveUI.Binding.Benchmarks/Program.cs | 9 +++--
.../RxUiDynamicChainBaseline.cs | 2 --
.../UnsafeFallbackBenchmark.cs | 2 --
.../WhenAnyDynamicBenchmark.cs | 2 --
.../unix => Configs}/BenchmarkConfig.cs | 4 ++-
.../Configs/NativeAotBenchmarkConfig.cs | 5 +--
.../unix => Configs}/ProfilerConfig.cs | 2 +-
.../Shared/Platforms/unix/BenchmarkHost.cs | 18 ++++++++++
.../Platforms/windows/BenchmarkConfig.cs | 23 ------------
.../Shared/Platforms/windows/BenchmarkHost.cs | 22 ++++++++++++
.../Platforms/windows/NetFrameworkConfig.cs | 35 +++++++++++++++++++
.../Platforms/windows/ProfilerConfig.cs | 15 --------
15 files changed, 88 insertions(+), 60 deletions(-)
rename src/benchmarks/Shared/{Platforms/unix => Configs}/BenchmarkConfig.cs (84%)
rename src/benchmarks/Shared/{Platforms/unix => Configs}/ProfilerConfig.cs (93%)
create mode 100644 src/benchmarks/Shared/Platforms/unix/BenchmarkHost.cs
delete mode 100644 src/benchmarks/Shared/Platforms/windows/BenchmarkConfig.cs
create mode 100644 src/benchmarks/Shared/Platforms/windows/BenchmarkHost.cs
create mode 100644 src/benchmarks/Shared/Platforms/windows/NetFrameworkConfig.cs
delete mode 100644 src/benchmarks/Shared/Platforms/windows/ProfilerConfig.cs
diff --git a/src/benchmarks/ReactiveUI.Binding.Benchmarks.ReactiveUI/Program.cs b/src/benchmarks/ReactiveUI.Binding.Benchmarks.ReactiveUI/Program.cs
index ece84667..39402147 100644
--- a/src/benchmarks/ReactiveUI.Binding.Benchmarks.ReactiveUI/Program.cs
+++ b/src/benchmarks/ReactiveUI.Binding.Benchmarks.ReactiveUI/Program.cs
@@ -2,7 +2,7 @@
// ReactiveUI Association Incorporated licenses this file to you under the MIT license.
// See the LICENSE file in the project root for full license information.
-using BenchmarkDotNet.Running;
+using ReactiveUI.Binding.Benchmarks.Configs;
namespace ReactiveUI.Binding.Benchmarks.ReactiveUI;
@@ -12,6 +12,5 @@ internal static class Program
/// Runs the benchmarks selected by .
/// The command line arguments passed to the benchmark switcher.
[STAThread]
- internal static void Main(string[] args) =>
- _ = BenchmarkSwitcher.FromAssembly(typeof(Program).Assembly).Run(args);
+ internal static void Main(string[] args) => BenchmarkHost.Run(typeof(Program).Assembly, args);
}
diff --git a/src/benchmarks/ReactiveUI.Binding.Benchmarks.ReactiveUI/ReactiveUIBindingBenchmark.cs b/src/benchmarks/ReactiveUI.Binding.Benchmarks.ReactiveUI/ReactiveUIBindingBenchmark.cs
index 142833b6..5fe3a240 100644
--- a/src/benchmarks/ReactiveUI.Binding.Benchmarks.ReactiveUI/ReactiveUIBindingBenchmark.cs
+++ b/src/benchmarks/ReactiveUI.Binding.Benchmarks.ReactiveUI/ReactiveUIBindingBenchmark.cs
@@ -4,7 +4,6 @@
using System.Diagnostics;
using BenchmarkDotNet.Attributes;
-using ReactiveUI.Binding.Benchmarks.Configs;
using ReactiveUI.Binding.Benchmarks.Mocks;
using ReactiveUI.Builder;
using Splat;
@@ -12,7 +11,6 @@
namespace ReactiveUI.Binding.Benchmarks;
/// ReactiveUI expression-tree binding benchmarks for comparison.
-[Config(typeof(BenchmarkConfig))]
[DebuggerDisplay("Expression-tree binding over {PropertyChangeCount} changes")]
public class ReactiveUIBindingBenchmark
{
diff --git a/src/benchmarks/ReactiveUI.Binding.Benchmarks.ReactiveUI/ReactiveUIObservationBenchmark.cs b/src/benchmarks/ReactiveUI.Binding.Benchmarks.ReactiveUI/ReactiveUIObservationBenchmark.cs
index 98f2f5f5..588eaffe 100644
--- a/src/benchmarks/ReactiveUI.Binding.Benchmarks.ReactiveUI/ReactiveUIObservationBenchmark.cs
+++ b/src/benchmarks/ReactiveUI.Binding.Benchmarks.ReactiveUI/ReactiveUIObservationBenchmark.cs
@@ -4,7 +4,6 @@
using System.Diagnostics;
using BenchmarkDotNet.Attributes;
-using ReactiveUI.Binding.Benchmarks.Configs;
using ReactiveUI.Binding.Benchmarks.Mocks;
using ReactiveUI.Builder;
using Splat;
@@ -12,7 +11,6 @@
namespace ReactiveUI.Binding.Benchmarks;
/// ReactiveUI expression-tree WhenAnyValue benchmarks for comparison.
-[Config(typeof(BenchmarkConfig))]
[DebuggerDisplay("Expression-tree observation over {PropertyChangeCount} changes")]
public class ReactiveUIObservationBenchmark
{
diff --git a/src/benchmarks/ReactiveUI.Binding.Benchmarks/Program.cs b/src/benchmarks/ReactiveUI.Binding.Benchmarks/Program.cs
index 01ee9382..f3964fe2 100644
--- a/src/benchmarks/ReactiveUI.Binding.Benchmarks/Program.cs
+++ b/src/benchmarks/ReactiveUI.Binding.Benchmarks/Program.cs
@@ -2,16 +2,15 @@
// ReactiveUI Association Incorporated licenses this file to you under the MIT license.
// See the LICENSE file in the project root for full license information.
-using BenchmarkDotNet.Running;
+using ReactiveUI.Binding.Benchmarks.Configs;
namespace ReactiveUI.Binding.Benchmarks;
-/// Entry point that hands the command line to the BenchmarkDotNet switcher.
+/// Entry point that hands the command line to the benchmark host.
internal static class Program
{
- /// Runs the benchmark selected by the command line.
+ /// Runs the benchmarks selected by the command line.
/// The command-line arguments passed to the switcher.
[STAThread]
- internal static void Main(string[] args) =>
- _ = BenchmarkSwitcher.FromAssembly(typeof(Program).Assembly).Run(args);
+ internal static void Main(string[] args) => BenchmarkHost.Run(typeof(Program).Assembly, args);
}
diff --git a/src/benchmarks/ReactiveUI.Binding.Benchmarks/RxUiDynamicChainBaseline.cs b/src/benchmarks/ReactiveUI.Binding.Benchmarks/RxUiDynamicChainBaseline.cs
index d5a8786f..f3a37922 100644
--- a/src/benchmarks/ReactiveUI.Binding.Benchmarks/RxUiDynamicChainBaseline.cs
+++ b/src/benchmarks/ReactiveUI.Binding.Benchmarks/RxUiDynamicChainBaseline.cs
@@ -9,7 +9,6 @@
using System.Runtime.CompilerServices;
using BenchmarkDotNet.Attributes;
using ReactiveUI;
-using ReactiveUI.Binding.Benchmarks.Configs;
using ReactiveUI.Builder;
using BenchmarkVm = ReactiveUI.Binding.Benchmarks.Mocks.BenchmarkViewModel;
@@ -18,7 +17,6 @@
namespace RxUiDynamicChain;
/// The dynamic-chain scenarios of WhenAnyDynamicBenchmark, run against ReactiveUI's own engine.
-[Config(typeof(BenchmarkConfig))]
#if NET8_0_OR_GREATER
[RequiresUnreferencedCode("Evaluates expression-based member chains via reflection; members may be trimmed.")]
#endif
diff --git a/src/benchmarks/ReactiveUI.Binding.Benchmarks/UnsafeFallbackBenchmark.cs b/src/benchmarks/ReactiveUI.Binding.Benchmarks/UnsafeFallbackBenchmark.cs
index e106886b..03220a40 100644
--- a/src/benchmarks/ReactiveUI.Binding.Benchmarks/UnsafeFallbackBenchmark.cs
+++ b/src/benchmarks/ReactiveUI.Binding.Benchmarks/UnsafeFallbackBenchmark.cs
@@ -6,7 +6,6 @@
using System.Diagnostics.CodeAnalysis;
#endif
using BenchmarkDotNet.Attributes;
-using ReactiveUI.Binding.Benchmarks.Configs;
using ReactiveUI.Binding.Benchmarks.Mocks;
using ReactiveUI.Binding.Builder;
@@ -18,7 +17,6 @@ namespace ReactiveUI.Binding.Benchmarks;
/// walk the path at run time, so an ahead-of-time publish cannot be relied on to keep the members they reach.
/// Read these against the generated benchmark of the same operator to see what the fallback costs.
///
-[Config(typeof(BenchmarkConfig))]
#if NET8_0_OR_GREATER
[RequiresUnreferencedCode("Evaluates expression-based member chains via reflection; members may be trimmed.")]
#endif
diff --git a/src/benchmarks/ReactiveUI.Binding.Benchmarks/WhenAnyDynamicBenchmark.cs b/src/benchmarks/ReactiveUI.Binding.Benchmarks/WhenAnyDynamicBenchmark.cs
index 732ad126..3a19c110 100644
--- a/src/benchmarks/ReactiveUI.Binding.Benchmarks/WhenAnyDynamicBenchmark.cs
+++ b/src/benchmarks/ReactiveUI.Binding.Benchmarks/WhenAnyDynamicBenchmark.cs
@@ -8,14 +8,12 @@
using System.Linq.Expressions;
using System.Runtime.CompilerServices;
using BenchmarkDotNet.Attributes;
-using ReactiveUI.Binding.Benchmarks.Configs;
using ReactiveUI.Binding.Benchmarks.Mocks;
using ReactiveUI.Binding.Builder;
namespace ReactiveUI.Binding.Benchmarks;
/// Benchmarks reflection-walked observation against the generated observation of the same chain.
-[Config(typeof(BenchmarkConfig))]
#if NET8_0_OR_GREATER
[RequiresUnreferencedCode("Evaluates expression-based member chains via reflection; members may be trimmed.")]
#endif
diff --git a/src/benchmarks/Shared/Platforms/unix/BenchmarkConfig.cs b/src/benchmarks/Shared/Configs/BenchmarkConfig.cs
similarity index 84%
rename from src/benchmarks/Shared/Platforms/unix/BenchmarkConfig.cs
rename to src/benchmarks/Shared/Configs/BenchmarkConfig.cs
index 876a4e41..6c98eddf 100644
--- a/src/benchmarks/Shared/Platforms/unix/BenchmarkConfig.cs
+++ b/src/benchmarks/Shared/Configs/BenchmarkConfig.cs
@@ -2,18 +2,20 @@
// ReactiveUI Association Incorporated licenses this file to you under the MIT license.
// See the LICENSE file in the project root for full license information.
+using BenchmarkDotNet.Configs;
using BenchmarkDotNet.Environments;
using BenchmarkDotNet.Exporters;
using BenchmarkDotNet.Jobs;
namespace ReactiveUI.Binding.Benchmarks.Configs;
-/// Runs a benchmark on .NET 8, 10 and 11.
+/// Runs benchmarks on .NET 8, 10 and 11, traced with EventPipe.
public class BenchmarkConfig : ProfilerConfig
{
/// Initializes a new instance of the class.
public BenchmarkConfig()
{
+ Add(DefaultConfig.Instance);
_ = AddJob(new Job().WithRuntime(CoreRuntime.Core80));
_ = AddJob(new Job().WithRuntime(CoreRuntime.Core10_0));
_ = AddJob(new Job().WithRuntime(CoreRuntime.Core11_0));
diff --git a/src/benchmarks/Shared/Configs/NativeAotBenchmarkConfig.cs b/src/benchmarks/Shared/Configs/NativeAotBenchmarkConfig.cs
index ebab9439..db920f7a 100644
--- a/src/benchmarks/Shared/Configs/NativeAotBenchmarkConfig.cs
+++ b/src/benchmarks/Shared/Configs/NativeAotBenchmarkConfig.cs
@@ -2,13 +2,14 @@
// ReactiveUI Association Incorporated licenses this file to you under the MIT license.
// See the LICENSE file in the project root for full license information.
+using BenchmarkDotNet.Configs;
using BenchmarkDotNet.Environments;
using BenchmarkDotNet.Jobs;
namespace ReactiveUI.Binding.Benchmarks.Configs;
-/// Adds the NativeAOT runtimes to , for benchmarks whose code publishes ahead of time.
-public class NativeAotBenchmarkConfig : BenchmarkConfig
+/// Adds the NativeAOT runtimes, for benchmarks whose code publishes ahead of time.
+public class NativeAotBenchmarkConfig : ManualConfig
{
/// Initializes a new instance of the class.
public NativeAotBenchmarkConfig()
diff --git a/src/benchmarks/Shared/Platforms/unix/ProfilerConfig.cs b/src/benchmarks/Shared/Configs/ProfilerConfig.cs
similarity index 93%
rename from src/benchmarks/Shared/Platforms/unix/ProfilerConfig.cs
rename to src/benchmarks/Shared/Configs/ProfilerConfig.cs
index d8398b94..fdc16c3b 100644
--- a/src/benchmarks/Shared/Platforms/unix/ProfilerConfig.cs
+++ b/src/benchmarks/Shared/Configs/ProfilerConfig.cs
@@ -10,7 +10,7 @@
namespace ReactiveUI.Binding.Benchmarks.Configs;
-/// Traces every benchmark with EventPipe, recording CPU samples and verbose GC events in one trace.
+/// Traces a benchmark with EventPipe, recording CPU samples and verbose GC events in one trace.
///
/// A config keeps one EventPipe profiler, and the CPU and GC profiles both enable the runtime provider. So the
/// sample profiler is added beside the GC profile, and the runtime provider carries the keywords of both.
diff --git a/src/benchmarks/Shared/Platforms/unix/BenchmarkHost.cs b/src/benchmarks/Shared/Platforms/unix/BenchmarkHost.cs
new file mode 100644
index 00000000..cac4b6a7
--- /dev/null
+++ b/src/benchmarks/Shared/Platforms/unix/BenchmarkHost.cs
@@ -0,0 +1,18 @@
+// Copyright (c) 2019-2026 ReactiveUI Association Incorporated. All rights reserved.
+// ReactiveUI Association Incorporated licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for full license information.
+
+using System.Reflection;
+using BenchmarkDotNet.Running;
+
+namespace ReactiveUI.Binding.Benchmarks.Configs;
+
+/// Runs the benchmarks on .NET with EventPipe.
+public static class BenchmarkHost
+{
+ /// Runs the benchmarks the command line selects.
+ /// The assembly holding the benchmarks.
+ /// The command line arguments passed to the benchmark switcher.
+ public static void Run(Assembly assembly, string[] args) =>
+ _ = BenchmarkSwitcher.FromAssembly(assembly).Run(args, new BenchmarkConfig());
+}
diff --git a/src/benchmarks/Shared/Platforms/windows/BenchmarkConfig.cs b/src/benchmarks/Shared/Platforms/windows/BenchmarkConfig.cs
deleted file mode 100644
index fe087b89..00000000
--- a/src/benchmarks/Shared/Platforms/windows/BenchmarkConfig.cs
+++ /dev/null
@@ -1,23 +0,0 @@
-// Copyright (c) 2019-2026 ReactiveUI Association Incorporated. All rights reserved.
-// ReactiveUI Association Incorporated licenses this file to you under the MIT license.
-// See the LICENSE file in the project root for full license information.
-
-using BenchmarkDotNet.Environments;
-using BenchmarkDotNet.Exporters;
-using BenchmarkDotNet.Jobs;
-
-namespace ReactiveUI.Binding.Benchmarks.Configs;
-
-/// Runs a benchmark on .NET Framework 4.6.2 and on .NET 8, 10 and 11.
-public class BenchmarkConfig : ProfilerConfig
-{
- /// Initializes a new instance of the class.
- public BenchmarkConfig()
- {
- _ = AddJob(new Job().WithRuntime(ClrRuntime.Net462));
- _ = AddJob(new Job().WithRuntime(CoreRuntime.Core80));
- _ = AddJob(new Job().WithRuntime(CoreRuntime.Core10_0));
- _ = AddJob(new Job().WithRuntime(CoreRuntime.Core11_0));
- _ = AddExporter(MarkdownExporter.GitHub);
- }
-}
diff --git a/src/benchmarks/Shared/Platforms/windows/BenchmarkHost.cs b/src/benchmarks/Shared/Platforms/windows/BenchmarkHost.cs
new file mode 100644
index 00000000..3153e4a9
--- /dev/null
+++ b/src/benchmarks/Shared/Platforms/windows/BenchmarkHost.cs
@@ -0,0 +1,22 @@
+// Copyright (c) 2019-2026 ReactiveUI Association Incorporated. All rights reserved.
+// ReactiveUI Association Incorporated licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for full license information.
+
+using System.Reflection;
+using BenchmarkDotNet.Running;
+
+namespace ReactiveUI.Binding.Benchmarks.Configs;
+
+/// Runs the benchmarks on .NET with EventPipe, then on .NET Framework with ETW.
+public static class BenchmarkHost
+{
+ /// Runs the benchmarks the command line selects.
+ /// The assembly holding the benchmarks.
+ /// The command line arguments passed to the benchmark switcher.
+ public static void Run(Assembly assembly, string[] args)
+ {
+ var switcher = BenchmarkSwitcher.FromAssembly(assembly);
+ _ = switcher.Run(args, new BenchmarkConfig());
+ _ = switcher.Run(args, new NetFrameworkConfig());
+ }
+}
diff --git a/src/benchmarks/Shared/Platforms/windows/NetFrameworkConfig.cs b/src/benchmarks/Shared/Platforms/windows/NetFrameworkConfig.cs
new file mode 100644
index 00000000..edab2969
--- /dev/null
+++ b/src/benchmarks/Shared/Platforms/windows/NetFrameworkConfig.cs
@@ -0,0 +1,35 @@
+// Copyright (c) 2019-2026 ReactiveUI Association Incorporated. All rights reserved.
+// ReactiveUI Association Incorporated licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for full license information.
+
+using BenchmarkDotNet.Configs;
+using BenchmarkDotNet.Diagnostics.Windows;
+using BenchmarkDotNet.Environments;
+using BenchmarkDotNet.Exporters;
+using BenchmarkDotNet.Filters;
+using BenchmarkDotNet.Jobs;
+
+namespace ReactiveUI.Binding.Benchmarks.Configs;
+
+/// Runs benchmarks on .NET Framework 4.6.2, traced with ETW.
+///
+/// EventPipe cannot attach to .NET Framework, and a run keeps one set of profilers, so this runs apart from
+/// . The filter keeps only the .NET Framework job, dropping the NativeAOT jobs a
+/// benchmark class adds for the other run.
+///
+public class NetFrameworkConfig : ManualConfig
+{
+ /// The id of the .NET Framework job, which the filter keeps.
+ private const string NetFrameworkJobId = "Net462";
+
+ /// Initializes a new instance of the class.
+ public NetFrameworkConfig()
+ {
+ Add(DefaultConfig.Instance);
+ _ = AddJob(new Job(NetFrameworkJobId).WithRuntime(ClrRuntime.Net462));
+ _ = AddDiagnoser(new EtwProfiler(new EtwProfilerConfig()));
+ _ = AddExporter(MarkdownExporter.GitHub);
+ _ = AddFilter(new SimpleFilter(static benchmark => benchmark.Job.Id == NetFrameworkJobId));
+ _ = WithOption(ConfigOptions.DontOverwriteResults, true);
+ }
+}
diff --git a/src/benchmarks/Shared/Platforms/windows/ProfilerConfig.cs b/src/benchmarks/Shared/Platforms/windows/ProfilerConfig.cs
deleted file mode 100644
index 5b2d1df0..00000000
--- a/src/benchmarks/Shared/Platforms/windows/ProfilerConfig.cs
+++ /dev/null
@@ -1,15 +0,0 @@
-// Copyright (c) 2019-2026 ReactiveUI Association Incorporated. All rights reserved.
-// ReactiveUI Association Incorporated licenses this file to you under the MIT license.
-// See the LICENSE file in the project root for full license information.
-
-using BenchmarkDotNet.Configs;
-using BenchmarkDotNet.Diagnostics.Windows;
-
-namespace ReactiveUI.Binding.Benchmarks.Configs;
-
-/// Traces every benchmark with ETW, which records CPU samples and GC events for .NET Framework and .NET alike.
-public class ProfilerConfig : ManualConfig
-{
- /// Initializes a new instance of the class.
- public ProfilerConfig() => _ = AddDiagnoser(new EtwProfiler(new EtwProfilerConfig()));
-}
From 0f77b4287876556e1a165bf0c56261fe43231246 Mon Sep 17 00:00:00 2001
From: Glenn Watson <5834289+glennawatson@users.noreply.github.com>
Date: Mon, 14 Sep 2026 22:53:20 +1000
Subject: [PATCH 3/3] test(benchmarks): mark the benchmark entry points for
inlining
---
.../ReactiveUI.Binding.Benchmarks.ReactiveUI/Program.cs | 2 ++
src/benchmarks/ReactiveUI.Binding.Benchmarks/Program.cs | 2 ++
2 files changed, 4 insertions(+)
diff --git a/src/benchmarks/ReactiveUI.Binding.Benchmarks.ReactiveUI/Program.cs b/src/benchmarks/ReactiveUI.Binding.Benchmarks.ReactiveUI/Program.cs
index 39402147..4d9512e0 100644
--- a/src/benchmarks/ReactiveUI.Binding.Benchmarks.ReactiveUI/Program.cs
+++ b/src/benchmarks/ReactiveUI.Binding.Benchmarks.ReactiveUI/Program.cs
@@ -2,6 +2,7 @@
// ReactiveUI Association Incorporated licenses this file to you under the MIT license.
// See the LICENSE file in the project root for full license information.
+using System.Runtime.CompilerServices;
using ReactiveUI.Binding.Benchmarks.Configs;
namespace ReactiveUI.Binding.Benchmarks.ReactiveUI;
@@ -12,5 +13,6 @@ internal static class Program
/// Runs the benchmarks selected by .
/// The command line arguments passed to the benchmark switcher.
[STAThread]
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
internal static void Main(string[] args) => BenchmarkHost.Run(typeof(Program).Assembly, args);
}
diff --git a/src/benchmarks/ReactiveUI.Binding.Benchmarks/Program.cs b/src/benchmarks/ReactiveUI.Binding.Benchmarks/Program.cs
index f3964fe2..498ce3cd 100644
--- a/src/benchmarks/ReactiveUI.Binding.Benchmarks/Program.cs
+++ b/src/benchmarks/ReactiveUI.Binding.Benchmarks/Program.cs
@@ -2,6 +2,7 @@
// ReactiveUI Association Incorporated licenses this file to you under the MIT license.
// See the LICENSE file in the project root for full license information.
+using System.Runtime.CompilerServices;
using ReactiveUI.Binding.Benchmarks.Configs;
namespace ReactiveUI.Binding.Benchmarks;
@@ -12,5 +13,6 @@ internal static class Program
/// Runs the benchmarks selected by the command line.
/// The command-line arguments passed to the switcher.
[STAThread]
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
internal static void Main(string[] args) => BenchmarkHost.Run(typeof(Program).Assembly, args);
}