Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 3 additions & 20 deletions .github/workflows/benchmarks.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
1 change: 1 addition & 0 deletions src/Directory.Packages.props
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,7 @@
<!-- Benchmarking -->
<!-- The net11 and NativeAOT-11 runtime monikers arrive in 0.16; 0.15 tops out at 10. -->
<PackageVersion Include="BenchmarkDotNet" Version="0.16.0-preview.1"/>
<PackageVersion Include="BenchmarkDotNet.Diagnostics.Windows" Version="0.16.0-preview.1"/>

<!-- Test App Dependencies-->
<PackageVersion Include="ReactiveUI" Version="24.2.0"/>
Expand Down
33 changes: 33 additions & 0 deletions src/benchmarks/Directory.Build.props
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
<Project>
<Import Project="$([MSBuild]::GetPathOfFileAbove('Directory.Build.props', '$(MSBuildThisFileDirectory)../'))"/>

<PropertyGroup>
<!-- Only the projects directly in this folder: BenchmarkDotNet's per-job projects sit deeper, in each output folder. -->
<BenchmarkProjectParentDirectory>$([MSBuild]::NormalizeDirectory($(MSBuildProjectDirectory), '..'))</BenchmarkProjectParentDirectory>
<IsBenchmarkHostProject>false</IsBenchmarkHostProject>
<IsBenchmarkHostProject Condition="'$(BenchmarkProjectParentDirectory)' == '$(MSBuildThisFileDirectory)'">true</IsBenchmarkHostProject>
</PropertyGroup>

<PropertyGroup Condition="'$(IsBenchmarkHostProject)' == 'true'">
<OutputType>Exe</OutputType>
<IsPackable>false</IsPackable>
<!-- Pass -p:BenchmarkPlatform=windows or unix to compile the other platform's configs. -->
<BenchmarkPlatform Condition="'$(BenchmarkPlatform)' == '' and $([MSBuild]::IsOsPlatform('Windows'))">windows</BenchmarkPlatform>
<BenchmarkPlatform Condition="'$(BenchmarkPlatform)' == ''">unix</BenchmarkPlatform>
</PropertyGroup>

<ItemGroup Condition="'$(IsBenchmarkHostProject)' == 'true'">
<PackageReference Include="BenchmarkDotNet"/>
<Compile Include="$(MSBuildThisFileDirectory)Shared\Configs\**\*.cs" Link="Configs\%(RecursiveDir)%(Filename)%(Extension)"/>
<None Include="$(MSBuildThisFileDirectory)Shared\Platforms\**\*.cs" Link="Platforms\%(RecursiveDir)%(Filename)%(Extension)"/>
</ItemGroup>

<ItemGroup Condition="'$(IsBenchmarkHostProject)' == 'true' and '$(BenchmarkPlatform)' == 'windows'">
<Compile Include="$(MSBuildThisFileDirectory)Shared\Platforms\windows\**\*.cs" Link="Platforms\windows\%(RecursiveDir)%(Filename)%(Extension)"/>
<PackageReference Include="BenchmarkDotNet.Diagnostics.Windows"/>
</ItemGroup>

<ItemGroup Condition="'$(IsBenchmarkHostProject)' == 'true' and '$(BenchmarkPlatform)' == 'unix'">
<Compile Include="$(MSBuildThisFileDirectory)Shared\Platforms\unix\**\*.cs" Link="Platforms\unix\%(RecursiveDir)%(Filename)%(Extension)"/>
</ItemGroup>
</Project>
Original file line number Diff line number Diff line change
@@ -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;

/// <summary>Tells ReactiveUI it is running outside an application, as a benchmark process does.</summary>
internal sealed class BenchmarkModeDetector : IModeDetector
{
/// <inheritdoc/>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public bool? InUnitTestRunner() => true;
}

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,8 @@
// 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 System.Runtime.CompilerServices;
using ReactiveUI.Binding.Benchmarks.Configs;

namespace ReactiveUI.Binding.Benchmarks.ReactiveUI;

Expand All @@ -12,6 +13,6 @@ internal static class Program
/// <summary>Runs the benchmarks selected by <paramref name="args"/>.</summary>
/// <param name="args">The command line arguments passed to the benchmark switcher.</param>
[STAThread]
internal static void Main(string[] args) =>
_ = BenchmarkSwitcher.FromAssembly(typeof(Program).Assembly).Run(args);
[MethodImpl(MethodImplOptions.AggressiveInlining)]
internal static void Main(string[] args) => BenchmarkHost.Run(typeof(Program).Assembly, args);
}
Original file line number Diff line number Diff line change
@@ -1,30 +1,19 @@
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFrameworks>net8.0;net10.0;net11.0;net462</TargetFrameworks>
<!--
net462 compiles anywhere but only launches on Windows, so the job that measures it is declared
there alone. Declaring it also drops the EventPipe profiler, which refuses any job below
.NET Core 3.0 and whose validator stops the whole run rather than the single job.
-->
<DefineConstants Condition="$([MSBuild]::IsOsPlatform('Windows'))">$(DefineConstants);BENCH_NETFX</DefineConstants>
<Nullable>enable</Nullable>
<IsPackable>false</IsPackable>
<IsAotCompatible>false</IsAotCompatible>
</PropertyGroup>

<ItemGroup>
<PackageReference Include="BenchmarkDotNet"/>
<PackageReference Include="ReactiveUI"/>
<PackageReference Include="System.Reactive"/>
</ItemGroup>

<!-- Share view model / view types from the SourceGen benchmark project -->
<ItemGroup>
<Compile Include="..\ReactiveUI.Binding.Benchmarks\BenchmarkViewModel.cs" Link="BenchmarkViewModel.cs"/>
<Compile Include="..\ReactiveUI.Binding.Benchmarks\BenchmarkChildViewModel.cs" Link="BenchmarkChildViewModel.cs"/>
<Compile Include="..\ReactiveUI.Binding.Benchmarks\BenchmarkView.cs" Link="BenchmarkView.cs"/>
<Compile Include="..\ReactiveUI.Binding.Benchmarks\Mocks\BenchmarkViewModel.cs" Link="Mocks\BenchmarkViewModel.cs"/>
<Compile Include="..\ReactiveUI.Binding.Benchmarks\Mocks\BenchmarkChildViewModel.cs" Link="Mocks\BenchmarkChildViewModel.cs"/>
<Compile Include="..\ReactiveUI.Binding.Benchmarks\Mocks\BenchmarkView.cs" Link="Mocks\BenchmarkView.cs"/>
</ItemGroup>

</Project>
Original file line number Diff line number Diff line change
Expand Up @@ -4,23 +4,13 @@

using System.Diagnostics;
using BenchmarkDotNet.Attributes;
using BenchmarkDotNet.Diagnosers;
using BenchmarkDotNet.Jobs;
using ReactiveUI.Binding.Benchmarks.Mocks;
using ReactiveUI.Builder;
using Splat;

namespace ReactiveUI.Binding.Benchmarks;

/// <summary>ReactiveUI expression-tree binding benchmarks for comparison.</summary>
#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]
[DebuggerDisplay("Expression-tree binding over {PropertyChangeCount} changes")]
public class ReactiveUIBindingBenchmark
{
Expand All @@ -33,8 +23,15 @@ public class ReactiveUIBindingBenchmark
/// <summary>The target view instance used for binding benchmarks.</summary>
private BenchmarkView _target = null!;

/// <summary>Initializes static members of the <see cref="ReactiveUIBindingBenchmark"/> class. Ensures ReactiveUI is configured before any benchmarks run.</summary>
static ReactiveUIBindingBenchmark() => ModuleInitializer.EnsureInitialized();
/// <summary>Configures ReactiveUI for the benchmark process.</summary>
[GlobalSetup]
public static void Register()
{
ModeDetector.OverrideModeDetector(new BenchmarkModeDetector());
_ = RxAppBuilder.CreateReactiveUIBuilder()
.WithCoreServices()
.BuildApp();
}

/// <summary>Sets up fresh source and target objects before each benchmark iteration.</summary>
[IterationSetup]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,23 +4,13 @@

using System.Diagnostics;
using BenchmarkDotNet.Attributes;
using BenchmarkDotNet.Diagnosers;
using BenchmarkDotNet.Jobs;
using ReactiveUI.Binding.Benchmarks.Mocks;
using ReactiveUI.Builder;
using Splat;

namespace ReactiveUI.Binding.Benchmarks;

/// <summary>ReactiveUI expression-tree WhenAnyValue benchmarks for comparison.</summary>
#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]
[DebuggerDisplay("Expression-tree observation over {PropertyChangeCount} changes")]
public class ReactiveUIObservationBenchmark
{
Expand All @@ -30,8 +20,15 @@ public class ReactiveUIObservationBenchmark
/// <summary>The view model instance used for observation benchmarks.</summary>
private BenchmarkViewModel _vm = null!;

/// <summary>Initializes static members of the <see cref="ReactiveUIObservationBenchmark"/> class. Ensures ReactiveUI is configured before any benchmarks run.</summary>
static ReactiveUIObservationBenchmark() => ModuleInitializer.EnsureInitialized();
/// <summary>Configures ReactiveUI for the benchmark process.</summary>
[GlobalSetup]
public static void Register()
{
ModeDetector.OverrideModeDetector(new BenchmarkModeDetector());
_ = RxAppBuilder.CreateReactiveUIBuilder()
.WithCoreServices()
.BuildApp();
}

/// <summary>Sets up a fresh view model before each benchmark iteration.</summary>
[IterationSetup]
Expand Down
38 changes: 3 additions & 35 deletions src/benchmarks/ReactiveUI.Binding.Benchmarks/BindBenchmark.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand All @@ -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.
/// </summary>
#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
{
/// <summary>Represents the number of property change events to be triggered during the benchmark tests.</summary>
Expand Down Expand Up @@ -94,24 +82,4 @@ public void WithObservedChanges()
_viewModel.Name = $"Name_{i}";
}
}

/// <summary>Counts what a binding reported without allocating per change.</summary>
private sealed class CountingObserver : IObserver<BindingChange>
{
/// <summary>Gets how many changes were reported.</summary>
public int Count { get; private set; }

/// <inheritdoc/>
public void OnNext(BindingChange value) => Count++;

/// <inheritdoc/>
public void OnError(Exception error)
{
}

/// <inheritdoc/>
public void OnCompleted()
{
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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;

/// <summary>Source-generated BindOneWay benchmarks with and without scheduler.</summary>
#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
{
/// <summary>Represents the number of property change events to be triggered during the benchmark tests.</summary>
Expand Down
18 changes: 3 additions & 15 deletions src/benchmarks/ReactiveUI.Binding.Benchmarks/BindToBenchmark.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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;

/// <summary>Source-generated BindTo benchmarks, which write a stream's values into a target property.</summary>
#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
{
/// <summary>How many values each benchmark pushes through one binding.</summary>
Expand Down
Loading
Loading