diff --git a/.gitignore b/.gitignore
index 7fe9da932b..1e724ffb9d 100644
--- a/.gitignore
+++ b/.gitignore
@@ -179,3 +179,6 @@ BundleArtifacts/
Package.StoreAssociation.xml
_pkginfo.txt
*.appx
+
+# BenchmarkDotNet artifacts (extensionless files included; benchmarks/baselines is covered by benchmarks/.gitignore)
+Rx.NET/Source/BenchmarkDotNet.Artifacts/
diff --git a/Rx.NET/Source/System.Reactive.slnx b/Rx.NET/Source/System.Reactive.slnx
index 589326009d..db3a6971e5 100644
--- a/Rx.NET/Source/System.Reactive.slnx
+++ b/Rx.NET/Source/System.Reactive.slnx
@@ -17,9 +17,8 @@
-
-
-
+
+
diff --git a/Rx.NET/Source/benchmarks/.gitignore b/Rx.NET/Source/benchmarks/.gitignore
new file mode 100644
index 0000000000..70b25eb5d0
--- /dev/null
+++ b/Rx.NET/Source/benchmarks/.gitignore
@@ -0,0 +1,8 @@
+# Benchmark run outputs (results, baselines, traces) are NOT committed to the repo.
+# Keep them locally or publish them as CI artifacts. See notes/benchmark-expansion-plan.md.
+baselines/
+traces/
+BenchmarkDotNet.Artifacts/
+
+# Local planning / guideline docs kept alongside the suite but not committed.
+notes/
diff --git a/Rx.NET/Source/benchmarks/Benchmarks.System.Reactive/AppendPrependBenchmark.cs b/Rx.NET/Source/benchmarks/Benchmarks.System.Reactive/AppendPrependBenchmark.cs
deleted file mode 100644
index 8623d1f225..0000000000
--- a/Rx.NET/Source/benchmarks/Benchmarks.System.Reactive/AppendPrependBenchmark.cs
+++ /dev/null
@@ -1,89 +0,0 @@
-// Licensed to the .NET Foundation under one or more agreements.
-// The .NET Foundation licenses this file to you under the MIT License.
-// See the LICENSE file in the project root for more information.
-
-#if (CURRENT)
-using System;
-using System.Collections.Generic;
-using System.Reactive.Linq;
-using System.Threading;
-using BenchmarkDotNet.Attributes;
-
-namespace Benchmarks.System.Reactive
-{
- [MemoryDiagnoser]
- public class AppendPrependBenchmark
- {
- [Params(1, 10, 100, 1000, 10000)]
- public int N;
-
- private int _store;
-
- [Benchmark(Baseline = true)]
- public void StartWithArray()
- {
- var array = new int[2 * N];
- var max = 2 * N - 1;
-
- for (var i = 0; i < N; i++)
- {
- array[i] = i;
- array[max - i] = i;
- }
-
- Observable
- .Empty()
- .StartWith(array)
- .Subscribe(v => Volatile.Write(ref _store, v));
- }
-
- [Benchmark]
- public void StartWithList()
- {
- var list = new List();
-
- for (var i = 0; i < N; i++)
- {
- list.Insert(i, 0);
- list.Add(i);
- }
-
- Observable
- .Empty()
- .StartWith(list)
- .Subscribe(v => Volatile.Write(ref _store, v));
- }
-
- [Benchmark]
- public void StartWithLinkedList()
- {
- var list = new LinkedList();
-
- for (var i = 0; i < N; i++)
- {
- list.AddFirst(i);
- list.AddLast(i);
- }
-
- Observable
- .Empty()
- .StartWith(list)
- .Subscribe(v => Volatile.Write(ref _store, v));
- }
-
- [Benchmark]
- public void AppendPrepend()
- {
- var obs = Observable.Empty();
-
- for (var i = 0; i < N; i++)
- {
- obs = obs.Prepend(i);
- obs = obs.Append(i);
- }
-
- obs.Subscribe(v => Volatile.Write(ref _store, v));
- }
- }
-}
-#endif
diff --git a/Rx.NET/Source/benchmarks/Benchmarks.System.Reactive/Benchmarks.System.Reactive.csproj b/Rx.NET/Source/benchmarks/Benchmarks.System.Reactive/Benchmarks.System.Reactive.csproj
index 85bffd9d23..2df158821d 100644
--- a/Rx.NET/Source/benchmarks/Benchmarks.System.Reactive/Benchmarks.System.Reactive.csproj
+++ b/Rx.NET/Source/benchmarks/Benchmarks.System.Reactive/Benchmarks.System.Reactive.csproj
@@ -1,18 +1,22 @@
-
+Exe
- net472
+ net472;net8.0;net9.0;net10.0truefalse
- Current Sources;Rx.net 3.1.1;Rx.net 4.0
+
+
-
7.0-none7.0-none
@@ -23,41 +27,30 @@
-
-
- $(DefineConstants);RX3_1_1
-
-
-
- $(DefineConstants);RX4_0
-
-
-
- $(DefineConstants);CURRENT
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
-
+
+
-
+
+
+
diff --git a/Rx.NET/Source/benchmarks/Benchmarks.System.Reactive/BufferCountBenchmark.cs b/Rx.NET/Source/benchmarks/Benchmarks.System.Reactive/BufferCountBenchmark.cs
deleted file mode 100644
index 3eedca0887..0000000000
--- a/Rx.NET/Source/benchmarks/Benchmarks.System.Reactive/BufferCountBenchmark.cs
+++ /dev/null
@@ -1,42 +0,0 @@
-// Licensed to the .NET Foundation under one or more agreements.
-// The .NET Foundation licenses this file to you under the MIT License.
-// See the LICENSE file in the project root for more information.
-
-using System;
-using System.Collections.Generic;
-using System.Reactive.Linq;
-using System.Threading;
-using BenchmarkDotNet.Attributes;
-
-namespace Benchmarks.System.Reactive
-{
- [MemoryDiagnoser]
- public class BufferCountBenchmark
- {
- private IList _store;
-
- [Benchmark]
- public void Exact()
- {
- Observable.Range(1, 1000)
- .Buffer(1)
- .Subscribe(v => Volatile.Write(ref _store, v));
- }
-
- [Benchmark]
- public void Skip()
- {
- Observable.Range(1, 1000)
- .Buffer(1, 2)
- .Subscribe(v => Volatile.Write(ref _store, v));
- }
-
- [Benchmark]
- public void Overlap()
- {
- Observable.Range(1, 1000)
- .Buffer(2, 1)
- .Subscribe(v => Volatile.Write(ref _store, v));
- }
- }
-}
diff --git a/Rx.NET/Source/benchmarks/Benchmarks.System.Reactive/CombineLatestBenchmark.cs b/Rx.NET/Source/benchmarks/Benchmarks.System.Reactive/CombineLatestBenchmark.cs
deleted file mode 100644
index 2babf600fd..0000000000
--- a/Rx.NET/Source/benchmarks/Benchmarks.System.Reactive/CombineLatestBenchmark.cs
+++ /dev/null
@@ -1,105 +0,0 @@
-// Licensed to the .NET Foundation under one or more agreements.
-// The .NET Foundation licenses this file to you under the MIT License.
-// See the LICENSE file in the project root for more information.
-
-using BenchmarkDotNet.Attributes;
-using ReactiveTests.Tests;
-
-namespace Benchmarks.System.Reactive
-{
- [MemoryDiagnoser]
- public class CombineLatestBenchmark
- {
- private readonly CombineLatestTest _zipTest = new();
-
- [Benchmark]
- public void CombineLatest_Typical2()
- {
- _zipTest.CombineLatest_Typical2();
- }
-
- [Benchmark]
- public void CombineLatest_Typical3()
- {
- _zipTest.CombineLatest_Typical3();
- }
-
- [Benchmark]
- public void CombineLatest_Typical4()
- {
- _zipTest.CombineLatest_Typical4();
- }
-
- [Benchmark]
- public void CombineLatest_Typical5()
- {
- _zipTest.CombineLatest_Typical5();
- }
-
- [Benchmark]
- public void CombineLatest_Typical6()
- {
- _zipTest.CombineLatest_Typical6();
- }
-
- [Benchmark]
- public void CombineLatest_Typical7()
- {
- _zipTest.CombineLatest_Typical7();
- }
-
- [Benchmark]
- public void CombineLatest_Typical8()
- {
- _zipTest.CombineLatest_Typical8();
- }
-
- [Benchmark]
- public void CombineLatest_Typical9()
- {
- _zipTest.CombineLatest_Typical9();
- }
-
- [Benchmark]
- public void CombineLatest_Typical10()
- {
- _zipTest.CombineLatest_Typical10();
- }
-
- [Benchmark]
- public void CombineLatest_Typical11()
- {
- _zipTest.CombineLatest_Typical11();
- }
-
- [Benchmark]
- public void CombineLatest_Typical12()
- {
- _zipTest.CombineLatest_Typical12();
- }
-
- [Benchmark]
- public void CombineLatest_Typical13()
- {
- _zipTest.CombineLatest_Typical13();
- }
-
- [Benchmark]
- public void CombineLatest_Typical14()
- {
- _zipTest.CombineLatest_Typical14();
- }
-
- [Benchmark]
- public void CombineLatest_Typical15()
- {
- _zipTest.CombineLatest_Typical15();
- }
-
- [Benchmark]
- public void CombineLatest_Typical16()
- {
- _zipTest.CombineLatest_Typical16();
- }
- }
-}
diff --git a/Rx.NET/Source/benchmarks/Benchmarks.System.Reactive/ComparisonAsyncBenchmark.cs b/Rx.NET/Source/benchmarks/Benchmarks.System.Reactive/ComparisonAsyncBenchmark.cs
deleted file mode 100644
index 24fb752f29..0000000000
--- a/Rx.NET/Source/benchmarks/Benchmarks.System.Reactive/ComparisonAsyncBenchmark.cs
+++ /dev/null
@@ -1,87 +0,0 @@
-// Licensed to the .NET Foundation under one or more agreements.
-// The .NET Foundation licenses this file to you under the MIT License.
-// See the LICENSE file in the project root for more information.
-
-using System;
-using System.Linq;
-using System.Reactive.Concurrency;
-using System.Reactive.Linq;
-using System.Threading;
-using BenchmarkDotNet.Attributes;
-
-namespace Benchmarks.System.Reactive
-{
- [MemoryDiagnoser]
- public class ComparisonAsyncBenchmark
- {
- [Params(1, 10, 100, 1000, 10000, 100000, 1000000)]
- public int N;
- private int _store;
-
- private IScheduler _scheduler1;
- private IScheduler _scheduler2;
-
- [GlobalSetup]
- public void Setup()
- {
- _scheduler1 = new EventLoopScheduler();
- _scheduler2 = new EventLoopScheduler();
- }
-
- [Benchmark]
- public void ObserveOn()
- {
- var cde = new CountdownEvent(1);
-
- Observable.Range(1, N).ObserveOn(_scheduler1)
- .Subscribe(v => Volatile.Write(ref _store, v), () => cde.Signal());
-
- if (N <= 1000)
- {
- while (cde.CurrentCount != 0) ;
- }
- else
- {
- cde.Wait();
- }
- }
-
- [Benchmark]
- public void SubscribeOn()
- {
- var cde = new CountdownEvent(1);
-
- Observable.Range(1, N).SubscribeOn(_scheduler1)
- .Subscribe(v => Volatile.Write(ref _store, v), () => cde.Signal());
-
- if (N <= 1000)
- {
- while (cde.CurrentCount != 0) ;
- }
- else
- {
- cde.Wait();
- }
- }
-
- [Benchmark]
- public void SubscribeOnObserveOn()
- {
- var cde = new CountdownEvent(1);
-
- Observable.Range(1, N)
- .SubscribeOn(_scheduler1)
- .ObserveOn(_scheduler2)
- .Subscribe(v => Volatile.Write(ref _store, v), () => cde.Signal());
-
- if (N <= 1000)
- {
- while (cde.CurrentCount != 0) ;
- }
- else
- {
- cde.Wait();
- }
- }
- }
-}
diff --git a/Rx.NET/Source/benchmarks/Benchmarks.System.Reactive/ComparisonBenchmark.cs b/Rx.NET/Source/benchmarks/Benchmarks.System.Reactive/ComparisonBenchmark.cs
deleted file mode 100644
index 7080c8e6ef..0000000000
--- a/Rx.NET/Source/benchmarks/Benchmarks.System.Reactive/ComparisonBenchmark.cs
+++ /dev/null
@@ -1,306 +0,0 @@
-// Licensed to the .NET Foundation under one or more agreements.
-// The .NET Foundation licenses this file to you under the MIT License.
-// See the LICENSE file in the project root for more information.
-
-using System;
-using System.Collections.Generic;
-using System.Linq;
-using System.Reactive.Concurrency;
-using System.Reactive.Disposables;
-using System.Reactive.Linq;
-using System.Reactive.Subjects;
-using System.Threading;
-using BenchmarkDotNet.Attributes;
-
-namespace Benchmarks.System.Reactive
-{
- [MemoryDiagnoser]
- public class ComparisonBenchmark
- {
- [Params(1, 10, 100, 1000, 10000, 100000, 1000000)]
- public int N;
- private int _store;
-
- [Benchmark]
- public void ForLoopBaseLine()
- {
- var n = N;
- for (var i = 0; i < n; i++)
- {
- Volatile.Write(ref _store, i);
- }
- }
-
- [Benchmark]
- public void EnumerableBaseLine()
- {
- foreach (var v in Enumerable.Range(1, N))
- {
- Volatile.Write(ref _store, v);
- }
- }
-
- [Benchmark]
- public void Return()
- {
- Observable.Return(1).Subscribe(v => Volatile.Write(ref _store, v));
- }
-
- [Benchmark]
- public void Range()
- {
- Observable.Range(1, N).Subscribe(v => Volatile.Write(ref _store, v));
- }
-
- [Benchmark]
- public void Select()
- {
- Observable.Range(1, N)
- .Select(v => v + 1)
- .Subscribe(v => Volatile.Write(ref _store, v));
- }
-
- [Benchmark]
- public void SelectSelect()
- {
- Observable.Range(1, N)
- .Select(v => v + 1)
- .Select(v => v + 1)
- .Subscribe(v => Volatile.Write(ref _store, v));
- }
-
- [Benchmark]
- public void Where()
- {
- Observable.Range(1, 2 * N)
- .Where(v => (v & 1) != 0)
- .Subscribe(v => Volatile.Write(ref _store, v));
- }
-
- [Benchmark]
- public void WhereWhere()
- {
- Observable.Range(1, 4 * N)
- .Where(v => (v & 1) != 0)
- .Where(v => (v & 2) != 0)
- .Subscribe(v => Volatile.Write(ref _store, v));
- }
-
- [Benchmark]
- public void Take()
- {
- Observable.Range(1, 2 * N)
- .Take(N)
- .Subscribe(v => Volatile.Write(ref _store, v));
- }
-
- [Benchmark]
- public void Skip()
- {
- Observable.Range(1, 2 * N)
- .Skip(N)
- .Subscribe(v => Volatile.Write(ref _store, v));
- }
-
- [Benchmark]
- public void TakeUntil()
- {
- Observable.Range(1, N)
- .TakeUntil(Observable.Never())
- .Subscribe(v => Volatile.Write(ref _store, v));
- }
-
- [Benchmark]
- public void ToObservable()
- {
- Enumerable.Range(1, N)
- .ToObservable()
- .Subscribe(v => Volatile.Write(ref _store, v));
- }
-
- [Benchmark]
- public void Concat()
- {
- var M = N - N / 2;
-
- Observable.Concat(
- Observable.Range(1, N),
- Observable.Range(1, M)
- )
- .Subscribe(v => Volatile.Write(ref _store, v));
- }
-
- [Benchmark]
- public void ConcatCrossMap()
- {
- var M = 1000 * 1000 / N;
-
- Observable.Concat(Observable.Range(1, N).Select(v => Observable.Range(v, M)))
- .Subscribe(v => Volatile.Write(ref _store, v));
- }
-
- [Benchmark]
- public void SelectManyCrossMap()
- {
- var M = 1000 * 1000 / N;
-
- Observable.Range(1, N).SelectMany(v => Observable.Range(v, M))
- .Subscribe(v => Volatile.Write(ref _store, v));
- }
-
- [Benchmark]
- public void MergeCrossMap()
- {
- var M = 1000 * 1000 / N;
-
- Observable.Merge(Observable.Range(1, N)
- .Select(v => Observable.Range(v, M))
- )
- .Subscribe(v => Volatile.Write(ref _store, v));
- }
-
- [Benchmark]
- public void AsyncSubjectPush()
- {
- var subj = new AsyncSubject();
- subj.Subscribe(v => Volatile.Write(ref _store, v));
-
- var n = N;
- for (var i = 0; i < N; i++)
- {
- subj.OnNext(i);
- }
- subj.OnCompleted();
- }
-
- [Benchmark]
- public void SubjectPush()
- {
- var subj = new Subject();
- subj.Subscribe(v => Volatile.Write(ref _store, v));
-
- var n = N;
- for (var i = 0; i < N; i++)
- {
- subj.OnNext(i);
- }
- subj.OnCompleted();
- }
-
- [Benchmark]
- public void AmbTwo()
- {
- Observable.Never().Amb(Observable.Range(1, N))
- .Subscribe(v => Volatile.Write(ref _store, v));
- }
-
- [Benchmark]
- public void AmbThree()
- {
- Observable.Amb(Observable.Never(), Observable.Never(), Observable.Range(1, N))
- .Subscribe(v => Volatile.Write(ref _store, v));
- }
-
- [Benchmark]
- public void Timeout()
- {
- Observable.Range(1, N)
- .Timeout(TimeSpan.FromHours(1))
- .Subscribe(v => Volatile.Write(ref _store, v));
- }
-
-#pragma warning disable CS0618 // Type or member is obsolete
- [Benchmark]
- public void First()
- {
- Volatile.Write(ref _store, Observable.Range(1, N)
- .First());
- }
-
- [Benchmark]
- public void Last()
- {
- Volatile.Write(ref _store, Observable.Range(1, N)
- .Last());
- }
-#pragma warning restore CS0618 // Type or member is obsolete
-
- private IList _bufferStore;
-
- [Benchmark]
- public void Buffer_Exact()
- {
- Observable.Range(1, 1000)
- .Buffer(1)
- .Subscribe(v => Volatile.Write(ref _bufferStore, v));
- }
-
- [Benchmark]
- public void Buffer_Skip()
- {
- Observable.Range(1, 1000)
- .Buffer(1, 2)
- .Subscribe(v => Volatile.Write(ref _bufferStore, v));
- }
-
- [Benchmark]
- public void Buffer_Overlap()
- {
- Observable.Range(1, 1000)
- .Buffer(2, 1)
- .Subscribe(v => Volatile.Write(ref _bufferStore, v));
- }
-
- [Benchmark]
- public void CurrentThreadSchedulerRepeated()
- {
- var n = N;
- var scheduler = CurrentThreadScheduler.Instance;
- for (var i = 0; i < n; i++)
- {
- scheduler.Schedule(i, (_, v) =>
- {
- Volatile.Write(ref _store, v);
- return Disposable.Empty;
- });
- }
- }
-
- [Benchmark]
- public void TakeLast()
- {
- Observable.Range(1, 2 * N).TakeLast(N)
- .Subscribe(v => Volatile.Write(ref _store, v));
- }
-
- [Benchmark]
- public void Repeat()
- {
- Observable.Repeat(1, N)
- .Subscribe(v => Volatile.Write(ref _store, v));
- }
-
- [Benchmark]
- public void ToList()
- {
- Observable.Repeat(1, N).ToList()
- .Subscribe(v => Volatile.Write(ref _bufferStore, v));
- }
-
- [Benchmark]
- public void Generate()
- {
- Observable.Generate(0, s => s < N, s => s + 1, s => s)
- .Subscribe(v => Volatile.Write(ref _store, v));
- }
-
- [Benchmark]
- public void Collect()
- {
- foreach (var v in Observable.Range(1, N).Collect(() => new List(), (a, b) => { a.Add(b); return a; }))
- {
- Volatile.Write(ref _bufferStore, v);
- }
- }
- }
-}
diff --git a/Rx.NET/Source/benchmarks/Benchmarks.System.Reactive/GroupByCompletion.cs b/Rx.NET/Source/benchmarks/Benchmarks.System.Reactive/GroupByCompletion.cs
deleted file mode 100644
index 9ecba76c5d..0000000000
--- a/Rx.NET/Source/benchmarks/Benchmarks.System.Reactive/GroupByCompletion.cs
+++ /dev/null
@@ -1,71 +0,0 @@
-// Licensed to the .NET Foundation under one or more agreements.
-// The .NET Foundation licenses this file to you under the MIT License.
-// See the LICENSE file in the project root for more information.
-
-using System;
-using System.Reactive.Linq;
-
-using BenchmarkDotNet.Attributes;
-
-namespace Benchmarks.System.Reactive
-{
- ///
- /// Completion of a wide fan-out/in scenario.
- ///
- ///
- ///
- /// This was added to address https://github.com/dotnet/reactive/issues/2005 in which completion
- /// takes longer and longer to handle as the number of groups increases.
- ///
- ///
- /// The queries in this benchmark represent the common 'fan out/in' pattern in Rx. It is often
- /// useful to split a stream into groups to enable per-group processing, and then to recombine
- /// the data back into a single stream. These benchmarks don't do any per-group processing, so
- /// they might look pointless, but we're trying to measure the minimum unavoidable overhead
- /// that any code using this technique will encounter.
- ///
- ///
- [MemoryDiagnoser]
- public class GroupByCompletion
- {
- private IObservable observable;
-
- [Params(200_000, 1_000_000)]
- public int NumberOfSamples { get; set; }
-
- [Params(10, 100, 1_000, 10_000, 100_000, 150_000, 200_000)]
- public int NumberOfGroups { get; set; }
-
- [GlobalSetup]
- public void GlobalSetup()
- {
- var data = new int[NumberOfSamples];
- for (var i = 0; i < data.Length; ++i)
- {
- data[i] = i;
- }
-
- observable = data.ToObservable();
- }
-
- [Benchmark]
- public void GroupBySelectMany()
- {
- var numberOfGroups = NumberOfGroups;
-
- observable!.GroupBy(value => value % numberOfGroups)
- .SelectMany(groupOfInts => groupOfInts)
- .Subscribe(intValue => { });
- }
-
- [Benchmark]
- public void GroupByMerge()
- {
- var numberOfGroups = NumberOfGroups;
-
- observable!.GroupBy(value => value % numberOfGroups)
- .Merge()
- .Subscribe(intValue => { });
- }
- }
-}
diff --git a/Rx.NET/Source/benchmarks/Benchmarks.System.Reactive/Infrastructure/BenchmarkBase.cs b/Rx.NET/Source/benchmarks/Benchmarks.System.Reactive/Infrastructure/BenchmarkBase.cs
new file mode 100644
index 0000000000..a00e85b24f
--- /dev/null
+++ b/Rx.NET/Source/benchmarks/Benchmarks.System.Reactive/Infrastructure/BenchmarkBase.cs
@@ -0,0 +1,21 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT License.
+// See the LICENSE file in the project root for more information.
+
+using BenchmarkDotNet.Engines;
+
+namespace Benchmarks.System.Reactive.Infrastructure
+{
+ ///
+ /// The shared root for the benchmark base classes: a single sink whose
+ /// dead-code-elimination contract is defined in one place. The throughput ()
+ /// and temporal () families each add their own [Params] N sweep on
+ /// top — those sweeps deliberately differ (temporal is capped because every element is a scheduler-queue
+ /// entry), so they cannot share a single N. This type is abstract so auto-discovery skips it.
+ ///
+ public abstract class BenchmarkBase
+ {
+ /// The BenchmarkDotNet consumer used to defeat dead-code elimination.
+ protected readonly Consumer Consumer = new();
+ }
+}
diff --git a/Rx.NET/Source/benchmarks/Benchmarks.System.Reactive/Infrastructure/FaultingSource.cs b/Rx.NET/Source/benchmarks/Benchmarks.System.Reactive/Infrastructure/FaultingSource.cs
new file mode 100644
index 0000000000..76302833bc
--- /dev/null
+++ b/Rx.NET/Source/benchmarks/Benchmarks.System.Reactive/Infrastructure/FaultingSource.cs
@@ -0,0 +1,33 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT License.
+// See the LICENSE file in the project root for more information.
+
+using System;
+using System.Reactive.Linq;
+
+namespace Benchmarks.System.Reactive.Infrastructure
+{
+ ///
+ /// Faulting sources for exercising operator error pathways. A single shared exception instance is used
+ /// so the error itself never allocates per invocation.
+ ///
+ public static class FaultingSource
+ {
+ /// The single exception instance used by every faulting source.
+ public static readonly Exception BenchmarkError = new InvalidOperationException("benchmark fault");
+
+ /// Emits 1..n then terminates with (OnError).
+ public static IObservable Faulting(int n) =>
+ Observable.Range(1, n).Concat(Observable.Throw(BenchmarkError));
+
+ ///
+ /// Faults the first subscriptions (each emitting 1..n first), then completes
+ /// normally — so Retry/RetryWhen genuinely resubscribe that many times before succeeding.
+ ///
+ public static IObservable FaultThenSucceed(int n, int faults)
+ {
+ var attempt = 0;
+ return Observable.Defer(() => attempt++ < faults ? Faulting(n) : Observable.Range(1, n));
+ }
+ }
+}
diff --git a/Rx.NET/Source/benchmarks/Benchmarks.System.Reactive/Infrastructure/OperatorBenchmarkBase.cs b/Rx.NET/Source/benchmarks/Benchmarks.System.Reactive/Infrastructure/OperatorBenchmarkBase.cs
new file mode 100644
index 0000000000..412b81c8c3
--- /dev/null
+++ b/Rx.NET/Source/benchmarks/Benchmarks.System.Reactive/Infrastructure/OperatorBenchmarkBase.cs
@@ -0,0 +1,25 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT License.
+// See the LICENSE file in the project root for more information.
+
+using BenchmarkDotNet.Attributes;
+
+namespace Benchmarks.System.Reactive.Infrastructure
+{
+ ///
+ /// Base class for throughput-style (S1) operator benchmarks: a standard element-count sweep and the
+ /// shared sink. BenchmarkDotNet honours [Params]/[Benchmark]
+ /// declared on base classes; this type is abstract so auto-discovery skips it.
+ ///
+ ///
+ /// N is not directly comparable across classes. For linear operators it is the element/source
+ /// count (work = O(N)); but the cross-map fan-out classes (SelectMany/Switch/Merge) hold total emitted work at
+ /// ~1,000,000 so N is the inner-subscription count, and the two-source combiners build two sources of N.
+ /// Read the Ratio/absolute numbers within a class, not across classes at the same N.
+ ///
+ public abstract class OperatorBenchmarkBase : BenchmarkBase
+ {
+ [Params(1, 10, 100, 1_000, 10_000, 100_000, 1_000_000)]
+ public int N;
+ }
+}
diff --git a/Rx.NET/Source/benchmarks/Benchmarks.System.Reactive/Infrastructure/PerElementColumn.cs b/Rx.NET/Source/benchmarks/Benchmarks.System.Reactive/Infrastructure/PerElementColumn.cs
new file mode 100644
index 0000000000..618ad16e03
--- /dev/null
+++ b/Rx.NET/Source/benchmarks/Benchmarks.System.Reactive/Infrastructure/PerElementColumn.cs
@@ -0,0 +1,119 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT License.
+// See the LICENSE file in the project root for more information.
+
+using System.Globalization;
+
+using BenchmarkDotNet.Columns;
+using BenchmarkDotNet.Reports;
+using BenchmarkDotNet.Running;
+
+namespace Benchmarks.System.Reactive.Infrastructure
+{
+ ///
+ /// Display-only summary columns showing per-element cost: Mean/N (ns per element) and
+ /// AllocatedBytes/N (bytes per element), where N is the [Params] element count.
+ /// OperationsPerInvoke cannot express this because it must be a compile-time constant.
+ ///
+ ///
+ /// For fan-out classes (SelectMany/Switch/Merge and friends) N is the inner-subscription count
+ /// with total work pinned, so there the figure reads as per-subscription rather than per-element.
+ /// The columns never enter the JSON export — comparison tooling recomputes per-element values
+ /// from the raw Statistics.Mean and Memory.BytesAllocatedPerOperation fields.
+ ///
+ public sealed class PerElementColumn : IColumn
+ {
+ public static readonly IColumn Time = new PerElementColumn(
+ "TimePerElement", "ns/N", "Mean time divided by the N parameter (ns per element; per-subscription for fan-out classes)", isTime: true);
+
+ public static readonly IColumn Allocated = new PerElementColumn(
+ "AllocatedPerElement", "B/N", "Allocated bytes per op divided by the N parameter (bytes per element; per-subscription for fan-out classes)", isTime: false);
+
+ private readonly bool _isTime;
+
+ private PerElementColumn(string id, string columnName, string legend, bool isTime)
+ {
+ Id = id;
+ ColumnName = columnName;
+ Legend = legend;
+ _isTime = isTime;
+ }
+
+ public string Id { get; }
+
+ public string ColumnName { get; }
+
+ public string Legend { get; }
+
+ public bool AlwaysShow => false;
+
+ public ColumnCategory Category => ColumnCategory.Custom;
+
+ public int PriorityInCategory => _isTime ? 0 : 1;
+
+ public bool IsNumeric => true;
+
+ // Dimensionless on purpose: the value is self-formatted with the unit in the column name,
+ // sidestepping BenchmarkDotNet's time-unit auto-scaling.
+ public UnitType UnitType => UnitType.Dimensionless;
+
+ public bool IsDefault(Summary summary, BenchmarkCase benchmarkCase) => false;
+
+ public bool IsAvailable(Summary summary)
+ {
+ foreach (var benchmarkCase in summary.BenchmarksCases)
+ {
+ if (TryGetN(benchmarkCase, out _))
+ {
+ return true;
+ }
+ }
+
+ return false;
+ }
+
+ public string GetValue(Summary summary, BenchmarkCase benchmarkCase) =>
+ GetValue(summary, benchmarkCase, summary.Style);
+
+ public string GetValue(Summary summary, BenchmarkCase benchmarkCase, SummaryStyle style)
+ {
+ if (!TryGetN(benchmarkCase, out var n) || n <= 0)
+ {
+ return "-";
+ }
+
+ var report = summary[benchmarkCase];
+ if (report == null)
+ {
+ return "-";
+ }
+
+ double? total = _isTime
+ ? report.ResultStatistics?.Mean // ns
+ : report.GcStats.GetBytesAllocatedPerOperation(benchmarkCase); // null without MemoryDiagnoser
+
+ return total is double value
+ ? (value / n).ToString("N2", CultureInfo.InvariantCulture)
+ : "-";
+ }
+
+ private static bool TryGetN(BenchmarkCase benchmarkCase, out int n)
+ {
+ // Scan Items rather than use the string indexer: the indexer's behaviour for a missing
+ // name is undocumented, and classes without an N param must yield "-" rather than throw.
+ foreach (var parameter in benchmarkCase.Parameters.Items)
+ {
+ if (parameter.Name == "N" && parameter.Value is int value)
+ {
+ n = value;
+ return true;
+ }
+ }
+
+ n = 0;
+ return false;
+ }
+
+ public override string ToString() => ColumnName;
+ }
+}
diff --git a/Rx.NET/Source/benchmarks/Benchmarks.System.Reactive/Infrastructure/PeriodicVirtualScheduler.cs b/Rx.NET/Source/benchmarks/Benchmarks.System.Reactive/Infrastructure/PeriodicVirtualScheduler.cs
new file mode 100644
index 0000000000..7a6976669c
--- /dev/null
+++ b/Rx.NET/Source/benchmarks/Benchmarks.System.Reactive/Infrastructure/PeriodicVirtualScheduler.cs
@@ -0,0 +1,89 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT License.
+// See the LICENSE file in the project root for more information.
+
+using System;
+using System.Reactive.Concurrency;
+using System.Reactive.Disposables;
+
+namespace Benchmarks.System.Reactive.Infrastructure
+{
+ ///
+ /// A that also implements , so the
+ /// periodic operators (Interval, Timer(dueTime, period), Sample(TimeSpan),
+ /// Buffer(TimeSpan), Window(TimeSpan)) exercise Rx's real SchedulePeriodic fast
+ /// path in virtual time, instead of the stopwatch-emulated fallback they hit on a plain
+ /// .
+ ///
+ ///
+ /// Scheduler.AsPeriodic() resolves the service via IServiceProvider.GetService, which
+ /// VirtualTimeSchedulerBase routes through its virtual GetService — implementing the
+ /// interface alone is never discovered, hence the override below.
+ ///
+ public sealed class PeriodicVirtualScheduler : HistoricalScheduler, ISchedulerPeriodic
+ {
+ protected override object GetService(Type serviceType) =>
+ serviceType == typeof(ISchedulerPeriodic) ? this : base.GetService(serviceType);
+
+ public IDisposable SchedulePeriodic(TState state, TimeSpan period, Func action)
+ {
+ if (action == null)
+ {
+ throw new ArgumentNullException(nameof(action));
+ }
+
+ // Scheduler.SchedulePeriodic permits a zero period, but in virtual time that would make
+ // Start() spin forever at a fixed clock. Fail fast — this is benchmark infrastructure only.
+ if (period <= TimeSpan.Zero)
+ {
+ throw new ArgumentOutOfRangeException(nameof(period));
+ }
+
+ return new PeriodicallyScheduledWorkItem(this, state, period, action);
+ }
+
+ private sealed class PeriodicallyScheduledWorkItem : IDisposable
+ {
+ private readonly PeriodicVirtualScheduler _scheduler;
+ private readonly TimeSpan _period;
+ private readonly Func _action;
+ private readonly SerialDisposable _run = new();
+ private TState _state;
+ private DateTimeOffset _next; // absolute due time: exact tick spacing, immune to Sleep()
+ private bool _disposed; // virtual schedulers are single-threaded; no locking needed
+
+ public PeriodicallyScheduledWorkItem(PeriodicVirtualScheduler scheduler, TState state, TimeSpan period, Func action)
+ {
+ _scheduler = scheduler;
+ _state = state;
+ _period = period;
+ _action = action;
+
+ // ISchedulerPeriodic contract: the first tick fires one period after the call.
+ _next = scheduler.Now + period;
+ _run.Disposable = scheduler.ScheduleAbsolute(this, _next, static (_, self) => self.Tick());
+ }
+
+ private IDisposable Tick()
+ {
+ _state = _action(_state);
+
+ // The action may have disposed us re-entrantly (e.g. Interval(...).Take(n) completing
+ // inside OnNext). Only reschedule afterwards, otherwise Start() would never terminate.
+ if (!_disposed)
+ {
+ _next += _period;
+ _run.Disposable = _scheduler.ScheduleAbsolute(this, _next, static (_, self) => self.Tick());
+ }
+
+ return Disposable.Empty;
+ }
+
+ public void Dispose()
+ {
+ _disposed = true;
+ _run.Dispose(); // cancels the pending ScheduledItem so GetNext() skips it
+ }
+ }
+ }
+}
diff --git a/Rx.NET/Source/benchmarks/Benchmarks.System.Reactive/Infrastructure/RateWindowBenchmarkBase.cs b/Rx.NET/Source/benchmarks/Benchmarks.System.Reactive/Infrastructure/RateWindowBenchmarkBase.cs
new file mode 100644
index 0000000000..741e1d8047
--- /dev/null
+++ b/Rx.NET/Source/benchmarks/Benchmarks.System.Reactive/Infrastructure/RateWindowBenchmarkBase.cs
@@ -0,0 +1,56 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT License.
+// See the LICENSE file in the project root for more information.
+
+using System;
+
+using BenchmarkDotNet.Attributes;
+
+namespace Benchmarks.System.Reactive.Infrastructure
+{
+ ///
+ /// Relationship between an operator's time-window and the source's inter-arrival period. One knob
+ /// captures the hot-path variation for rate/window operators without exploding the benchmark matrix.
+ ///
+ public enum Regime
+ {
+ /// Window < period — elements are spaced further apart than the window (e.g. Throttle: all pass).
+ Sparse,
+
+ /// Window ≈ period — the boundary case.
+ Boundary,
+
+ /// Window ≫ period — elements arrive far faster than the window (e.g. Throttle: constant cancel+reschedule).
+ Dense,
+ }
+
+ ///
+ /// Base class for rate/window temporal operators (Throttle, Sample, Buffer(time), Window(time)) whose hot
+ /// path depends on how the operator's relates to the source's .
+ /// Adds the knob and computes the window once per parameter combination.
+ ///
+ public abstract class RateWindowBenchmarkBase : TemporalBenchmarkBase
+ {
+ [Params(Regime.Sparse, Regime.Boundary, Regime.Dense)]
+ public Regime Density;
+
+ /// The source inter-arrival period.
+ protected TimeSpan Period { get; private set; }
+
+ /// The operator's time window, derived from relative to .
+ protected TimeSpan Window { get; private set; }
+
+ [GlobalSetup]
+ public void RateWindowSetup()
+ {
+ Period = TimeSpan.FromTicks(PeriodTicks);
+ Window = Density switch
+ {
+ Regime.Sparse => TimeSpan.FromTicks(PeriodTicks / 2), // window < period → every element passes
+ Regime.Boundary => TimeSpan.FromTicks(PeriodTicks), // window ≈ period
+ Regime.Dense => TimeSpan.FromTicks(PeriodTicks * N), // window ≫ period → arrives far faster than the window
+ _ => TimeSpan.FromTicks(PeriodTicks),
+ };
+ }
+ }
+}
diff --git a/Rx.NET/Source/benchmarks/Benchmarks.System.Reactive/Infrastructure/RxBenchmarkConfig.cs b/Rx.NET/Source/benchmarks/Benchmarks.System.Reactive/Infrastructure/RxBenchmarkConfig.cs
new file mode 100644
index 0000000000..f55d700745
--- /dev/null
+++ b/Rx.NET/Source/benchmarks/Benchmarks.System.Reactive/Infrastructure/RxBenchmarkConfig.cs
@@ -0,0 +1,33 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT License.
+// See the LICENSE file in the project root for more information.
+
+using BenchmarkDotNet.Configs;
+using BenchmarkDotNet.Diagnosers;
+using BenchmarkDotNet.Exporters.Json;
+using BenchmarkDotNet.Order;
+
+namespace Benchmarks.System.Reactive.Infrastructure
+{
+ ///
+ /// The shared BenchmarkDotNet configuration for the whole suite. Wired once in Program,
+ /// so individual benchmark classes do not need to repeat [MemoryDiagnoser].
+ ///
+ ///
+ /// Memory is a first-class output: surfaces Gen0/1/2 and Allocated
+ /// bytes/op, and the full JSON + CSV exporters emit the machine-readable artifacts the
+ /// performance-improvement plan is generated from. Opt-in profilers (ETW / EventPipe / disassembly /
+ /// threading) are enabled per-run via CLI switches, layered on top of this config.
+ ///
+ public static class RxBenchmarkConfig
+ {
+ public static IConfig Create() =>
+ ManualConfig.Create(DefaultConfig.Instance)
+ .AddDiagnoser(MemoryDiagnoser.Default)
+ // DefaultConfig already provides the GitHub markdown + CSV exporters; only add the full JSON
+ // exporter, which is the machine-readable artifact the perf-improvement plan is generated from.
+ .AddExporter(JsonExporter.Full)
+ .AddColumn(PerElementColumn.Time, PerElementColumn.Allocated)
+ .WithOrderer(new DefaultOrderer(SummaryOrderPolicy.FastestToSlowest));
+ }
+}
diff --git a/Rx.NET/Source/benchmarks/Benchmarks.System.Reactive/Infrastructure/Sinks.cs b/Rx.NET/Source/benchmarks/Benchmarks.System.Reactive/Infrastructure/Sinks.cs
new file mode 100644
index 0000000000..413a9641fa
--- /dev/null
+++ b/Rx.NET/Source/benchmarks/Benchmarks.System.Reactive/Infrastructure/Sinks.cs
@@ -0,0 +1,72 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT License.
+// See the LICENSE file in the project root for more information.
+
+using System;
+using System.Threading;
+
+using BenchmarkDotNet.Engines;
+
+namespace Benchmarks.System.Reactive.Infrastructure
+{
+ ///
+ /// An that funnels every OnNext into a BenchmarkDotNet
+ /// so results can't be eliminated as dead code. OnError is consumed (not swallowed), so error-path
+ /// benchmarks measure real work and never rethrow into the benchmark.
+ ///
+ public sealed class ConsumingObserver : IObserver
+ {
+ private readonly Consumer _consumer;
+
+ public ConsumingObserver(Consumer consumer) => _consumer = consumer;
+
+ public void OnNext(T value) => _consumer.Consume(value);
+
+ public void OnError(Exception error) => _consumer.Consume(error);
+
+ public void OnCompleted() { }
+ }
+
+ ///
+ /// A that also blocks until the sequence terminates — for asynchronous
+ /// pipelines (e.g. ObserveOn/SubscribeOn) whose completion happens on another thread.
+ ///
+ public sealed class BlockingObserver : IObserver, IDisposable
+ {
+ private readonly Consumer _consumer;
+ private readonly ManualResetEventSlim _done = new(false);
+
+ public BlockingObserver(Consumer consumer) => _consumer = consumer;
+
+ public void OnNext(T value) => _consumer.Consume(value);
+
+ public void OnError(Exception error)
+ {
+ _consumer.Consume(error);
+ _done.Set();
+ }
+
+ public void OnCompleted() => _done.Set();
+
+ public void Wait() => _done.Wait();
+
+ public void Dispose() => _done.Dispose();
+ }
+
+ public static class SinkExtensions
+ {
+ /// Subscribes a that feeds every value (and any error) to .
+ public static IDisposable SubscribeConsume(this IObservable source, Consumer consumer) =>
+ source.Subscribe(new ConsumingObserver(consumer));
+
+ /// Subscribes, consumes every value, and blocks until the sequence terminates. For async pipelines.
+ public static void SubscribeBlocking(this IObservable source, Consumer consumer)
+ {
+ using var observer = new BlockingObserver(consumer);
+ using (source.Subscribe(observer))
+ {
+ observer.Wait();
+ }
+ }
+ }
+}
diff --git a/Rx.NET/Source/benchmarks/Benchmarks.System.Reactive/Infrastructure/TemporalBenchmarkBase.cs b/Rx.NET/Source/benchmarks/Benchmarks.System.Reactive/Infrastructure/TemporalBenchmarkBase.cs
new file mode 100644
index 0000000000..ff2d6ab682
--- /dev/null
+++ b/Rx.NET/Source/benchmarks/Benchmarks.System.Reactive/Infrastructure/TemporalBenchmarkBase.cs
@@ -0,0 +1,26 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT License.
+// See the LICENSE file in the project root for more information.
+
+using BenchmarkDotNet.Attributes;
+
+namespace Benchmarks.System.Reactive.Infrastructure
+{
+ ///
+ /// Base class for temporal (S3) operator benchmarks driven by virtual time. is capped
+ /// well below the throughput sweep because every element becomes a scheduler-queue entry (cost ~N·logN).
+ /// This type is abstract so auto-discovery skips it.
+ ///
+ ///
+ /// Operators whose hot path depends on the window-vs-arrival relationship (Throttle, Sample, Buffer(time),
+ /// Window(time)) derive from instead, which adds the density knob.
+ ///
+ public abstract class TemporalBenchmarkBase : BenchmarkBase
+ {
+ /// Fixed source inter-arrival period, in virtual ticks.
+ protected const long PeriodTicks = 10;
+
+ [Params(100, 1_000, 10_000, 100_000)]
+ public int N;
+ }
+}
diff --git a/Rx.NET/Source/benchmarks/Benchmarks.System.Reactive/Infrastructure/VirtualTimeSource.cs b/Rx.NET/Source/benchmarks/Benchmarks.System.Reactive/Infrastructure/VirtualTimeSource.cs
new file mode 100644
index 0000000000..8ae6539ad7
--- /dev/null
+++ b/Rx.NET/Source/benchmarks/Benchmarks.System.Reactive/Infrastructure/VirtualTimeSource.cs
@@ -0,0 +1,38 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT License.
+// See the LICENSE file in the project root for more information.
+
+using System;
+using System.Reactive.Concurrency;
+using System.Reactive.Linq;
+
+namespace Benchmarks.System.Reactive.Infrastructure
+{
+ ///
+ /// Builders that emit "N elements spread across a virtual timeline" for temporal (S3) benchmarks.
+ ///
+ ///
+ /// Uses
+ /// rather than the recording TestScheduler.CreateHotObservable, so the only allocations in the
+ /// measured region belong to the operator under test (Generate keeps a single self-rescheduling item).
+ /// Driven by a virtual scheduler (e.g. ), Start() drains the whole
+ /// timeline synchronously with zero wall-clock waiting.
+ ///
+ public static class VirtualTimeSource
+ {
+ ///
+ /// Emits ascending integers on , each spaced
+ /// of virtual time apart, followed by OnCompleted.
+ ///
+ public static IObservable Timed(IScheduler scheduler, int n, TimeSpan step) =>
+ Observable.Generate(0, i => i < n, i => i + 1, i => i, _ => step, scheduler);
+
+ ///
+ /// As but the first element is delayed by
+ /// (subsequent elements apart) — used to interleave two
+ /// sources on a shared clock.
+ ///
+ public static IObservable Timed(IScheduler scheduler, int n, TimeSpan step, TimeSpan offset) =>
+ Observable.Generate(0, i => i < n, i => i + 1, i => i, i => i == 0 ? offset : step, scheduler);
+ }
+}
diff --git a/Rx.NET/Source/benchmarks/Benchmarks.System.Reactive/Operators/Aggregates/AggregateBenchmarks.cs b/Rx.NET/Source/benchmarks/Benchmarks.System.Reactive/Operators/Aggregates/AggregateBenchmarks.cs
new file mode 100644
index 0000000000..4e89ee3ea9
--- /dev/null
+++ b/Rx.NET/Source/benchmarks/Benchmarks.System.Reactive/Operators/Aggregates/AggregateBenchmarks.cs
@@ -0,0 +1,20 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT License.
+// See the LICENSE file in the project root for more information.
+
+using System.Reactive.Linq;
+
+using BenchmarkDotNet.Attributes;
+
+using Benchmarks.System.Reactive.Infrastructure;
+
+namespace Benchmarks.System.Reactive.Operators.Aggregates
+{
+ /// S1: Aggregate folds the whole stream into a single value emitted on completion.
+ [BenchmarkCategory("Aggregates")]
+ public class AggregateBenchmarks : OperatorBenchmarkBase
+ {
+ [Benchmark]
+ public void Aggregate() => Observable.Range(1, N).Aggregate(0L, static (acc, v) => acc + v).SubscribeConsume(Consumer);
+ }
+}
diff --git a/Rx.NET/Source/benchmarks/Benchmarks.System.Reactive/Operators/Aggregates/CountQuantifierBenchmarks.cs b/Rx.NET/Source/benchmarks/Benchmarks.System.Reactive/Operators/Aggregates/CountQuantifierBenchmarks.cs
new file mode 100644
index 0000000000..ea5e533e01
--- /dev/null
+++ b/Rx.NET/Source/benchmarks/Benchmarks.System.Reactive/Operators/Aggregates/CountQuantifierBenchmarks.cs
@@ -0,0 +1,32 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT License.
+// See the LICENSE file in the project root for more information.
+
+using System.Reactive.Linq;
+
+using BenchmarkDotNet.Attributes;
+
+using Benchmarks.System.Reactive.Infrastructure;
+
+namespace Benchmarks.System.Reactive.Operators.Aggregates
+{
+ ///
+ /// S1: Count and the quantifiers. Predicates are chosen so each must scan the whole stream
+ /// (worst case): Any/Contains never match, All always holds.
+ ///
+ [BenchmarkCategory("Aggregates")]
+ public class CountQuantifierBenchmarks : OperatorBenchmarkBase
+ {
+ [Benchmark]
+ public void Count() => Observable.Range(1, N).Count().SubscribeConsume(Consumer);
+
+ [Benchmark]
+ public void Any() => Observable.Range(1, N).Any(static v => v == int.MaxValue).SubscribeConsume(Consumer);
+
+ [Benchmark]
+ public void All() => Observable.Range(1, N).All(static v => v > 0).SubscribeConsume(Consumer);
+
+ [Benchmark]
+ public void Contains() => Observable.Range(1, N).Contains(int.MaxValue).SubscribeConsume(Consumer);
+ }
+}
diff --git a/Rx.NET/Source/benchmarks/Benchmarks.System.Reactive/Operators/Aggregates/ElementAtBenchmarks.cs b/Rx.NET/Source/benchmarks/Benchmarks.System.Reactive/Operators/Aggregates/ElementAtBenchmarks.cs
new file mode 100644
index 0000000000..a06bef2919
--- /dev/null
+++ b/Rx.NET/Source/benchmarks/Benchmarks.System.Reactive/Operators/Aggregates/ElementAtBenchmarks.cs
@@ -0,0 +1,23 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT License.
+// See the LICENSE file in the project root for more information.
+
+using System.Reactive.Linq;
+
+using BenchmarkDotNet.Attributes;
+
+using Benchmarks.System.Reactive.Infrastructure;
+
+namespace Benchmarks.System.Reactive.Operators.Aggregates
+{
+ /// S1: ElementAt/ElementAtOrDefault the last index, counting through the whole stream first.
+ [BenchmarkCategory("Aggregates")]
+ public class ElementAtBenchmarks : OperatorBenchmarkBase
+ {
+ [Benchmark]
+ public void ElementAt() => Observable.Range(1, N).ElementAt(N - 1).SubscribeConsume(Consumer);
+
+ [Benchmark]
+ public void ElementAtOrDefault() => Observable.Range(1, N).ElementAtOrDefault(N - 1).SubscribeConsume(Consumer);
+ }
+}
diff --git a/Rx.NET/Source/benchmarks/Benchmarks.System.Reactive/Operators/Aggregates/FirstLastSingleAsyncBenchmarks.cs b/Rx.NET/Source/benchmarks/Benchmarks.System.Reactive/Operators/Aggregates/FirstLastSingleAsyncBenchmarks.cs
new file mode 100644
index 0000000000..203916cdb2
--- /dev/null
+++ b/Rx.NET/Source/benchmarks/Benchmarks.System.Reactive/Operators/Aggregates/FirstLastSingleAsyncBenchmarks.cs
@@ -0,0 +1,39 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT License.
+// See the LICENSE file in the project root for more information.
+
+using System.Reactive.Linq;
+
+using BenchmarkDotNet.Attributes;
+
+using Benchmarks.System.Reactive.Infrastructure;
+
+namespace Benchmarks.System.Reactive.Operators.Aggregates
+{
+ ///
+ /// S1: the non-blocking *Async element selectors (each returns a single-value observable).
+ /// First* short-circuits on the first element; Last* drains the stream; Single* uses a
+ /// predicate matching exactly one element (so it must scan the whole stream to confirm uniqueness).
+ ///
+ [BenchmarkCategory("Aggregates")]
+ public class FirstLastSingleAsyncBenchmarks : OperatorBenchmarkBase
+ {
+ [Benchmark]
+ public void FirstAsync() => Observable.Range(1, N).FirstAsync().SubscribeConsume(Consumer);
+
+ [Benchmark]
+ public void FirstOrDefaultAsync() => Observable.Range(1, N).FirstOrDefaultAsync().SubscribeConsume(Consumer);
+
+ [Benchmark]
+ public void LastAsync() => Observable.Range(1, N).LastAsync().SubscribeConsume(Consumer);
+
+ [Benchmark]
+ public void LastOrDefaultAsync() => Observable.Range(1, N).LastOrDefaultAsync().SubscribeConsume(Consumer);
+
+ [Benchmark]
+ public void SingleAsync() => Observable.Range(1, N).SingleAsync(static v => v == 1).SubscribeConsume(Consumer);
+
+ [Benchmark]
+ public void SingleOrDefaultAsync() => Observable.Range(1, N).SingleOrDefaultAsync(static v => v == 1).SubscribeConsume(Consumer);
+ }
+}
diff --git a/Rx.NET/Source/benchmarks/Benchmarks.System.Reactive/Operators/Aggregates/IsEmptyLongCountBenchmarks.cs b/Rx.NET/Source/benchmarks/Benchmarks.System.Reactive/Operators/Aggregates/IsEmptyLongCountBenchmarks.cs
new file mode 100644
index 0000000000..47bbcaddce
--- /dev/null
+++ b/Rx.NET/Source/benchmarks/Benchmarks.System.Reactive/Operators/Aggregates/IsEmptyLongCountBenchmarks.cs
@@ -0,0 +1,23 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT License.
+// See the LICENSE file in the project root for more information.
+
+using System.Reactive.Linq;
+
+using BenchmarkDotNet.Attributes;
+
+using Benchmarks.System.Reactive.Infrastructure;
+
+namespace Benchmarks.System.Reactive.Operators.Aggregates
+{
+ /// S1: IsEmpty (short-circuits on the first element) and LongCount (drains the stream).
+ [BenchmarkCategory("Aggregates")]
+ public class IsEmptyLongCountBenchmarks : OperatorBenchmarkBase
+ {
+ [Benchmark]
+ public void IsEmpty() => Observable.Range(1, N).IsEmpty().SubscribeConsume(Consumer);
+
+ [Benchmark]
+ public void LongCount() => Observable.Range(1, N).LongCount().SubscribeConsume(Consumer);
+ }
+}
diff --git a/Rx.NET/Source/benchmarks/Benchmarks.System.Reactive/Operators/Aggregates/MinByMaxByBenchmarks.cs b/Rx.NET/Source/benchmarks/Benchmarks.System.Reactive/Operators/Aggregates/MinByMaxByBenchmarks.cs
new file mode 100644
index 0000000000..2ce3fee2c7
--- /dev/null
+++ b/Rx.NET/Source/benchmarks/Benchmarks.System.Reactive/Operators/Aggregates/MinByMaxByBenchmarks.cs
@@ -0,0 +1,26 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT License.
+// See the LICENSE file in the project root for more information.
+
+using System.Reactive.Linq;
+
+using BenchmarkDotNet.Attributes;
+
+using Benchmarks.System.Reactive.Infrastructure;
+
+namespace Benchmarks.System.Reactive.Operators.Aggregates
+{
+ ///
+ /// S1: MinBy/MaxBy keep a running list of the elements sharing the current best key (a keyed
+ /// key-selector plus list churn), returning an IList on completion.
+ ///
+ [BenchmarkCategory("Aggregates")]
+ public class MinByMaxByBenchmarks : OperatorBenchmarkBase
+ {
+ [Benchmark]
+ public void MinBy() => Observable.Range(1, N).MinBy(static v => v % 256).SubscribeConsume(Consumer);
+
+ [Benchmark]
+ public void MaxBy() => Observable.Range(1, N).MaxBy(static v => v % 256).SubscribeConsume(Consumer);
+ }
+}
diff --git a/Rx.NET/Source/benchmarks/Benchmarks.System.Reactive/Operators/Aggregates/MinMaxAverageBenchmarks.cs b/Rx.NET/Source/benchmarks/Benchmarks.System.Reactive/Operators/Aggregates/MinMaxAverageBenchmarks.cs
new file mode 100644
index 0000000000..c4cf8f6747
--- /dev/null
+++ b/Rx.NET/Source/benchmarks/Benchmarks.System.Reactive/Operators/Aggregates/MinMaxAverageBenchmarks.cs
@@ -0,0 +1,29 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT License.
+// See the LICENSE file in the project root for more information.
+
+using System.Reactive.Linq;
+
+using BenchmarkDotNet.Attributes;
+
+using Benchmarks.System.Reactive.Infrastructure;
+
+namespace Benchmarks.System.Reactive.Operators.Aggregates
+{
+ ///
+ /// S1: the numeric aggregates. Streaming OnNext accumulators, so — as noted in the plan — they are
+ /// not SIMD candidates (vectorization would need a materialized array); a useful reference point.
+ ///
+ [BenchmarkCategory("Aggregates")]
+ public class MinMaxAverageBenchmarks : OperatorBenchmarkBase
+ {
+ [Benchmark]
+ public void Min() => Observable.Range(1, N).Min().SubscribeConsume(Consumer);
+
+ [Benchmark]
+ public void Max() => Observable.Range(1, N).Max().SubscribeConsume(Consumer);
+
+ [Benchmark]
+ public void Average() => Observable.Range(1, N).Average().SubscribeConsume(Consumer);
+ }
+}
diff --git a/Rx.NET/Source/benchmarks/Benchmarks.System.Reactive/Operators/Aggregates/SequenceEqualBenchmarks.cs b/Rx.NET/Source/benchmarks/Benchmarks.System.Reactive/Operators/Aggregates/SequenceEqualBenchmarks.cs
new file mode 100644
index 0000000000..dc306e545a
--- /dev/null
+++ b/Rx.NET/Source/benchmarks/Benchmarks.System.Reactive/Operators/Aggregates/SequenceEqualBenchmarks.cs
@@ -0,0 +1,21 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT License.
+// See the LICENSE file in the project root for more information.
+
+using System.Reactive.Linq;
+
+using BenchmarkDotNet.Attributes;
+
+using Benchmarks.System.Reactive.Infrastructure;
+
+namespace Benchmarks.System.Reactive.Operators.Aggregates
+{
+ /// S1: SequenceEqual compares two streams element-by-element; the equal inputs force a full scan.
+ [BenchmarkCategory("Aggregates")]
+ public class SequenceEqualBenchmarks : OperatorBenchmarkBase
+ {
+ [Benchmark]
+ public void SequenceEqual() =>
+ Observable.Range(1, N).SequenceEqual(Observable.Range(1, N)).SubscribeConsume(Consumer);
+ }
+}
diff --git a/Rx.NET/Source/benchmarks/Benchmarks.System.Reactive/Operators/Aggregates/SumBenchmarks.cs b/Rx.NET/Source/benchmarks/Benchmarks.System.Reactive/Operators/Aggregates/SumBenchmarks.cs
new file mode 100644
index 0000000000..34a689ad8c
--- /dev/null
+++ b/Rx.NET/Source/benchmarks/Benchmarks.System.Reactive/Operators/Aggregates/SumBenchmarks.cs
@@ -0,0 +1,24 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT License.
+// See the LICENSE file in the project root for more information.
+
+using System.Reactive.Linq;
+
+using BenchmarkDotNet.Attributes;
+
+using Benchmarks.System.Reactive.Infrastructure;
+
+namespace Benchmarks.System.Reactive.Operators.Aggregates
+{
+ ///
+ /// Aggregates-category exemplar (S1): Sum — a scalar streaming accumulate that emits on completion.
+ /// Projected to long to avoid int overflow at large N. (A reference point for the SIMD
+ /// discussion: streaming accumulators cannot be vectorized.)
+ ///
+ [BenchmarkCategory("Aggregates")]
+ public class SumBenchmarks : OperatorBenchmarkBase
+ {
+ [Benchmark]
+ public void Sum() => Observable.Range(1, N).Sum(x => (long)x).SubscribeConsume(Consumer);
+ }
+}
diff --git a/Rx.NET/Source/benchmarks/Benchmarks.System.Reactive/Operators/Aggregates/ToDictionaryToLookupBenchmarks.cs b/Rx.NET/Source/benchmarks/Benchmarks.System.Reactive/Operators/Aggregates/ToDictionaryToLookupBenchmarks.cs
new file mode 100644
index 0000000000..5199c6dd86
--- /dev/null
+++ b/Rx.NET/Source/benchmarks/Benchmarks.System.Reactive/Operators/Aggregates/ToDictionaryToLookupBenchmarks.cs
@@ -0,0 +1,26 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT License.
+// See the LICENSE file in the project root for more information.
+
+using System.Reactive.Linq;
+
+using BenchmarkDotNet.Attributes;
+
+using Benchmarks.System.Reactive.Infrastructure;
+
+namespace Benchmarks.System.Reactive.Operators.Aggregates
+{
+ ///
+ /// S1 materialization into keyed collections: ToDictionary (one entry per unique key) and
+ /// ToLookup (grouped by key). Both buffer the whole stream — allocation candidates.
+ ///
+ [BenchmarkCategory("Aggregates")]
+ public class ToDictionaryToLookupBenchmarks : OperatorBenchmarkBase
+ {
+ [Benchmark]
+ public void ToDictionary() => Observable.Range(1, N).ToDictionary(static v => v).SubscribeConsume(Consumer);
+
+ [Benchmark]
+ public void ToLookup() => Observable.Range(1, N).ToLookup(static v => v % 8).SubscribeConsume(Consumer);
+ }
+}
diff --git a/Rx.NET/Source/benchmarks/Benchmarks.System.Reactive/Operators/Aggregates/ToListToArrayBenchmarks.cs b/Rx.NET/Source/benchmarks/Benchmarks.System.Reactive/Operators/Aggregates/ToListToArrayBenchmarks.cs
new file mode 100644
index 0000000000..ad26a3c19b
--- /dev/null
+++ b/Rx.NET/Source/benchmarks/Benchmarks.System.Reactive/Operators/Aggregates/ToListToArrayBenchmarks.cs
@@ -0,0 +1,26 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT License.
+// See the LICENSE file in the project root for more information.
+
+using System.Reactive.Linq;
+
+using BenchmarkDotNet.Attributes;
+
+using Benchmarks.System.Reactive.Infrastructure;
+
+namespace Benchmarks.System.Reactive.Operators.Aggregates
+{
+ ///
+ /// S1 materialization: ToList and ToArray buffer the whole stream. Prime zero-allocation
+ /// candidates — ToArray in particular does List-doubling growth plus a final exact-size copy.
+ ///
+ [BenchmarkCategory("Aggregates")]
+ public class ToListToArrayBenchmarks : OperatorBenchmarkBase
+ {
+ [Benchmark]
+ public void ToList() => Observable.Range(1, N).ToList().SubscribeConsume(Consumer);
+
+ [Benchmark]
+ public void ToArray() => Observable.Range(1, N).ToArray().SubscribeConsume(Consumer);
+ }
+}
diff --git a/Rx.NET/Source/benchmarks/Benchmarks.System.Reactive/Operators/Async/AsyncFactoryBenchmarks.cs b/Rx.NET/Source/benchmarks/Benchmarks.System.Reactive/Operators/Async/AsyncFactoryBenchmarks.cs
new file mode 100644
index 0000000000..206b625770
--- /dev/null
+++ b/Rx.NET/Source/benchmarks/Benchmarks.System.Reactive/Operators/Async/AsyncFactoryBenchmarks.cs
@@ -0,0 +1,38 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT License.
+// See the LICENSE file in the project root for more information.
+
+using System.Reactive.Linq;
+using System.Threading.Tasks;
+
+using BenchmarkDotNet.Attributes;
+using BenchmarkDotNet.Engines;
+
+using Benchmarks.System.Reactive.Infrastructure;
+
+namespace Benchmarks.System.Reactive.Operators.Async
+{
+ ///
+ /// The async factory bridges (single-shot, so no N sweep — this measures the subscription + async-completion
+ /// cost): FromAsync, Start, StartAsync, ToAsync. (FromAsyncPattern relies on
+ /// delegate BeginInvoke, which is unsupported on modern .NET, so it is not benchmarked.)
+ ///
+ [SimpleJob(RunStrategy.Monitoring, launchCount: 1, warmupCount: 3, iterationCount: 10)] // async completion latency → repeatable job, not the auto-tuned default
+ [BenchmarkCategory("Async")]
+ public class AsyncFactoryBenchmarks
+ {
+ private readonly Consumer _consumer = new();
+
+ [Benchmark]
+ public void FromAsync() => Observable.FromAsync(static () => Task.FromResult(1)).SubscribeBlocking(_consumer);
+
+ [Benchmark]
+ public void Start() => Observable.Start(static () => 1).SubscribeBlocking(_consumer);
+
+ [Benchmark]
+ public void StartAsync() => Observable.StartAsync(static () => Task.FromResult(1)).SubscribeBlocking(_consumer);
+
+ [Benchmark]
+ public void ToAsync() => Observable.ToAsync(static () => 1)().SubscribeBlocking(_consumer);
+ }
+}
diff --git a/Rx.NET/Source/benchmarks/Benchmarks.System.Reactive/Operators/Async/FromAsyncPatternBenchmarks.cs b/Rx.NET/Source/benchmarks/Benchmarks.System.Reactive/Operators/Async/FromAsyncPatternBenchmarks.cs
new file mode 100644
index 0000000000..7de77f2bdd
--- /dev/null
+++ b/Rx.NET/Source/benchmarks/Benchmarks.System.Reactive/Operators/Async/FromAsyncPatternBenchmarks.cs
@@ -0,0 +1,57 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT License.
+// See the LICENSE file in the project root for more information.
+
+using System;
+using System.Reactive.Linq;
+using System.Threading;
+
+using BenchmarkDotNet.Attributes;
+using BenchmarkDotNet.Engines;
+
+using Benchmarks.System.Reactive.Infrastructure;
+
+#pragma warning disable CS0618 // FromAsyncPattern is obsolete; benchmarked intentionally with a hand-rolled IAsyncResult.
+
+namespace Benchmarks.System.Reactive.Operators.Async
+{
+ ///
+ /// FromAsyncPattern bridges the APM (Begin/End) pattern to an observable. The common
+ /// delegate-BeginInvoke form is unsupported on modern .NET, so this uses a hand-rolled, synchronously
+ /// completing (which works on every target framework).
+ ///
+ [BenchmarkCategory("Async")]
+ public class FromAsyncPatternBenchmarks
+ {
+ private readonly Consumer _consumer = new();
+ private Func> _invoke = default!;
+
+ [GlobalSetup]
+ public void Setup() => _invoke = Observable.FromAsyncPattern(BeginSynchronously, static _ => 1);
+
+ [Benchmark]
+ public void FromAsyncPattern() => _invoke().SubscribeConsume(_consumer);
+
+ private static IAsyncResult BeginSynchronously(AsyncCallback callback, object state)
+ {
+ var result = new ImmediateAsyncResult(state);
+ callback?.Invoke(result);
+ return result;
+ }
+
+ private sealed class ImmediateAsyncResult : IAsyncResult
+ {
+ private ManualResetEvent _handle;
+
+ public ImmediateAsyncResult(object state) => AsyncState = state;
+
+ public object AsyncState { get; }
+
+ public WaitHandle AsyncWaitHandle => _handle ??= new ManualResetEvent(true);
+
+ public bool CompletedSynchronously => true;
+
+ public bool IsCompleted => true;
+ }
+ }
+}
diff --git a/Rx.NET/Source/benchmarks/Benchmarks.System.Reactive/Operators/Awaiter/AwaiterBenchmarks.cs b/Rx.NET/Source/benchmarks/Benchmarks.System.Reactive/Operators/Awaiter/AwaiterBenchmarks.cs
new file mode 100644
index 0000000000..6733d1ccf5
--- /dev/null
+++ b/Rx.NET/Source/benchmarks/Benchmarks.System.Reactive/Operators/Awaiter/AwaiterBenchmarks.cs
@@ -0,0 +1,27 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT License.
+// See the LICENSE file in the project root for more information.
+
+using System.Reactive.Linq;
+using System.Threading;
+
+using BenchmarkDotNet.Attributes;
+
+using Benchmarks.System.Reactive.Infrastructure;
+
+namespace Benchmarks.System.Reactive.Operators.Awaiter
+{
+ ///
+ /// Await support: RunAsync and GetAwaiter both project the sequence to its final element via an
+ /// AsyncSubject; blocking on the result drains the whole stream.
+ ///
+ [BenchmarkCategory("Awaiter")]
+ public class AwaiterBenchmarks : OperatorBenchmarkBase
+ {
+ [Benchmark]
+ public int RunAsync() => Observable.Range(1, N).RunAsync(CancellationToken.None).GetAwaiter().GetResult();
+
+ [Benchmark]
+ public int GetAwaiter() => Observable.Range(1, N).GetAwaiter().GetResult();
+ }
+}
diff --git a/Rx.NET/Source/benchmarks/Benchmarks.System.Reactive/Operators/Binding/LateSubscriberBenchmarks.cs b/Rx.NET/Source/benchmarks/Benchmarks.System.Reactive/Operators/Binding/LateSubscriberBenchmarks.cs
new file mode 100644
index 0000000000..857a4c1a98
--- /dev/null
+++ b/Rx.NET/Source/benchmarks/Benchmarks.System.Reactive/Operators/Binding/LateSubscriberBenchmarks.cs
@@ -0,0 +1,67 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT License.
+// See the LICENSE file in the project root for more information.
+
+using System.Reactive.Linq;
+using System.Reactive.Subjects;
+
+using BenchmarkDotNet.Attributes;
+using BenchmarkDotNet.Engines;
+
+using Benchmarks.System.Reactive.Infrastructure;
+
+namespace Benchmarks.System.Reactive.Operators.Binding
+{
+ ///
+ /// Multicast scenarios the "all-subscribers-before-Connect" benchmarks miss: a Replay subscriber that
+ /// attaches after the buffer has filled (so it exercises catch-up replay — Replay's whole point), and a
+ /// RefCount subscribe/dispose/re-subscribe churn (0→1→0→1 connect/disconnect).
+ ///
+ [BenchmarkCategory("Binding")]
+ public class LateSubscriberBenchmarks
+ {
+ [Params(1_000, 10_000, 100_000)]
+ public int N;
+
+ private readonly Consumer _consumer = new();
+
+ [Benchmark]
+ public void Replay_LateSubscriber()
+ {
+ var source = new Subject();
+ var replayed = source.Replay();
+ using var connection = replayed.Connect();
+
+ var half = N / 2;
+ for (var i = 0; i < half; i++)
+ {
+ source.OnNext(i); // fills the replay buffer while there are no subscribers
+ }
+
+ using (replayed.SubscribeConsume(_consumer)) // late subscriber → replays the buffered half, then goes live
+ {
+ for (var i = half; i < N; i++)
+ {
+ source.OnNext(i);
+ }
+
+ source.OnCompleted();
+ }
+ }
+
+ [Benchmark]
+ public void RefCount_Churn()
+ {
+ var source = new Subject();
+ var shared = source.Publish().RefCount();
+
+ for (var round = 0; round < N; round++)
+ {
+ using (shared.SubscribeConsume(_consumer)) // first subscriber connects; dispose disconnects
+ {
+ source.OnNext(round);
+ }
+ }
+ }
+ }
+}
diff --git a/Rx.NET/Source/benchmarks/Benchmarks.System.Reactive/Operators/Binding/MulticastAutoConnectBenchmarks.cs b/Rx.NET/Source/benchmarks/Benchmarks.System.Reactive/Operators/Binding/MulticastAutoConnectBenchmarks.cs
new file mode 100644
index 0000000000..8de1cd4915
--- /dev/null
+++ b/Rx.NET/Source/benchmarks/Benchmarks.System.Reactive/Operators/Binding/MulticastAutoConnectBenchmarks.cs
@@ -0,0 +1,35 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT License.
+// See the LICENSE file in the project root for more information.
+
+using System;
+using System.Reactive.Linq;
+using System.Reactive.Subjects;
+
+using BenchmarkDotNet.Attributes;
+
+using Benchmarks.System.Reactive.Infrastructure;
+
+namespace Benchmarks.System.Reactive.Operators.Binding
+{
+ ///
+ /// S2: Multicast shares a source through an explicit subject; AutoConnect connects a published
+ /// source automatically once the required number of subscribers attach.
+ ///
+ [BenchmarkCategory("Binding")]
+ public class MulticastAutoConnectBenchmarks : OperatorBenchmarkBase
+ {
+ [Benchmark]
+ public void Multicast()
+ {
+ var connectable = Observable.Range(1, N).Multicast(new Subject());
+ using (connectable.SubscribeConsume(Consumer))
+ using (connectable.Connect())
+ {
+ }
+ }
+
+ [Benchmark]
+ public void AutoConnect() => Observable.Range(1, N).Publish().AutoConnect(1).SubscribeConsume(Consumer);
+ }
+}
diff --git a/Rx.NET/Source/benchmarks/Benchmarks.System.Reactive/Operators/Binding/PublishBenchmarks.cs b/Rx.NET/Source/benchmarks/Benchmarks.System.Reactive/Operators/Binding/PublishBenchmarks.cs
new file mode 100644
index 0000000000..632e18512b
--- /dev/null
+++ b/Rx.NET/Source/benchmarks/Benchmarks.System.Reactive/Operators/Binding/PublishBenchmarks.cs
@@ -0,0 +1,50 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT License.
+// See the LICENSE file in the project root for more information.
+
+using System;
+using System.Reactive.Linq;
+
+using BenchmarkDotNet.Attributes;
+using BenchmarkDotNet.Engines;
+
+using Benchmarks.System.Reactive.Infrastructure;
+
+namespace Benchmarks.System.Reactive.Operators.Binding
+{
+ ///
+ /// Multicast-category exemplar (S2): Publish fans a single cold source out to M subscribers via
+ /// Connect(). Sweeps subscriber count to expose the multicast dispatch cost.
+ ///
+ [BenchmarkCategory("Binding")]
+ public class PublishBenchmarks
+ {
+ [Params(1_000, 10_000, 100_000)]
+ public int N;
+
+ [Params(1, 2, 5)]
+ public int Subscribers;
+
+ private readonly Consumer _consumer = new();
+
+ [Benchmark]
+ public void Publish()
+ {
+ var connectable = Observable.Range(1, N).Publish();
+
+ var subscriptions = new IDisposable[Subscribers];
+ for (var i = 0; i < Subscribers; i++)
+ {
+ subscriptions[i] = connectable.SubscribeConsume(_consumer);
+ }
+
+ using (connectable.Connect()) // drives the source synchronously to all subscribers
+ {
+ foreach (var subscription in subscriptions)
+ {
+ subscription.Dispose();
+ }
+ }
+ }
+ }
+}
diff --git a/Rx.NET/Source/benchmarks/Benchmarks.System.Reactive/Operators/Binding/PublishLastBenchmarks.cs b/Rx.NET/Source/benchmarks/Benchmarks.System.Reactive/Operators/Binding/PublishLastBenchmarks.cs
new file mode 100644
index 0000000000..6034a798b3
--- /dev/null
+++ b/Rx.NET/Source/benchmarks/Benchmarks.System.Reactive/Operators/Binding/PublishLastBenchmarks.cs
@@ -0,0 +1,27 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT License.
+// See the LICENSE file in the project root for more information.
+
+using System.Reactive.Linq;
+
+using BenchmarkDotNet.Attributes;
+
+using Benchmarks.System.Reactive.Infrastructure;
+
+namespace Benchmarks.System.Reactive.Operators.Binding
+{
+ /// S2: PublishLast (AsyncSubject-backed) multicasts only the final element on completion.
+ [BenchmarkCategory("Binding")]
+ public class PublishLastBenchmarks : OperatorBenchmarkBase
+ {
+ [Benchmark]
+ public void PublishLast()
+ {
+ var connectable = Observable.Range(1, N).PublishLast();
+ using (connectable.SubscribeConsume(Consumer))
+ using (connectable.Connect())
+ {
+ }
+ }
+ }
+}
diff --git a/Rx.NET/Source/benchmarks/Benchmarks.System.Reactive/Operators/Binding/RefCountBenchmarks.cs b/Rx.NET/Source/benchmarks/Benchmarks.System.Reactive/Operators/Binding/RefCountBenchmarks.cs
new file mode 100644
index 0000000000..677b892ca8
--- /dev/null
+++ b/Rx.NET/Source/benchmarks/Benchmarks.System.Reactive/Operators/Binding/RefCountBenchmarks.cs
@@ -0,0 +1,23 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT License.
+// See the LICENSE file in the project root for more information.
+
+using System.Reactive.Linq;
+
+using BenchmarkDotNet.Attributes;
+
+using Benchmarks.System.Reactive.Infrastructure;
+
+namespace Benchmarks.System.Reactive.Operators.Binding
+{
+ ///
+ /// S2: Publish().RefCount() connects on the first subscription and disconnects when the last drops.
+ /// The single synchronous subscription connects, drains, and auto-disconnects on completion.
+ ///
+ [BenchmarkCategory("Binding")]
+ public class RefCountBenchmarks : OperatorBenchmarkBase
+ {
+ [Benchmark]
+ public void RefCount() => Observable.Range(1, N).Publish().RefCount().SubscribeConsume(Consumer);
+ }
+}
diff --git a/Rx.NET/Source/benchmarks/Benchmarks.System.Reactive/Operators/Binding/ReplayBenchmarks.cs b/Rx.NET/Source/benchmarks/Benchmarks.System.Reactive/Operators/Binding/ReplayBenchmarks.cs
new file mode 100644
index 0000000000..799014c84b
--- /dev/null
+++ b/Rx.NET/Source/benchmarks/Benchmarks.System.Reactive/Operators/Binding/ReplayBenchmarks.cs
@@ -0,0 +1,54 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT License.
+// See the LICENSE file in the project root for more information.
+
+using System;
+using System.Reactive.Linq;
+
+using BenchmarkDotNet.Attributes;
+using BenchmarkDotNet.Engines;
+
+using Benchmarks.System.Reactive.Infrastructure;
+
+namespace Benchmarks.System.Reactive.Operators.Binding
+{
+ ///
+ /// S2 multicast with replay: Replay buffers past elements for late subscribers. A prime allocation
+ /// candidate (the replay buffer). BufferSize == 0 replays the entire stream; otherwise a bounded window.
+ ///
+ [BenchmarkCategory("Binding")]
+ public class ReplayBenchmarks
+ {
+ [Params(1_000, 10_000, 100_000)]
+ public int N;
+
+ [Params(1, 5)]
+ public int Subscribers;
+
+ [Params(0, 64)]
+ public int BufferSize;
+
+ private readonly Consumer _consumer = new();
+
+ [Benchmark]
+ public void Replay()
+ {
+ var source = Observable.Range(1, N);
+ var connectable = BufferSize == 0 ? source.Replay() : source.Replay(BufferSize);
+
+ var subscriptions = new IDisposable[Subscribers];
+ for (var i = 0; i < Subscribers; i++)
+ {
+ subscriptions[i] = connectable.SubscribeConsume(_consumer);
+ }
+
+ using (connectable.Connect())
+ {
+ foreach (var subscription in subscriptions)
+ {
+ subscription.Dispose();
+ }
+ }
+ }
+ }
+}
diff --git a/Rx.NET/Source/benchmarks/Benchmarks.System.Reactive/Operators/Blocking/BlockingBenchmarks.cs b/Rx.NET/Source/benchmarks/Benchmarks.System.Reactive/Operators/Blocking/BlockingBenchmarks.cs
new file mode 100644
index 0000000000..580fa83ce9
--- /dev/null
+++ b/Rx.NET/Source/benchmarks/Benchmarks.System.Reactive/Operators/Blocking/BlockingBenchmarks.cs
@@ -0,0 +1,23 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT License.
+// See the LICENSE file in the project root for more information.
+
+using System.Reactive.Linq;
+
+using BenchmarkDotNet.Attributes;
+
+using Benchmarks.System.Reactive.Infrastructure;
+
+namespace Benchmarks.System.Reactive.Operators.Blocking
+{
+ ///
+ /// Blocking-category exemplar: Wait blocks the calling thread until the (finite) sequence completes
+ /// and returns its last element. Driven by a synchronous Range, so it completes without real waiting.
+ ///
+ [BenchmarkCategory("Blocking")]
+ public class BlockingBenchmarks : OperatorBenchmarkBase
+ {
+ [Benchmark]
+ public int Wait() => Observable.Range(1, N).Wait();
+ }
+}
diff --git a/Rx.NET/Source/benchmarks/Benchmarks.System.Reactive/Operators/Blocking/BlockingPullBenchmarks.cs b/Rx.NET/Source/benchmarks/Benchmarks.System.Reactive/Operators/Blocking/BlockingPullBenchmarks.cs
new file mode 100644
index 0000000000..f14155b145
--- /dev/null
+++ b/Rx.NET/Source/benchmarks/Benchmarks.System.Reactive/Operators/Blocking/BlockingPullBenchmarks.cs
@@ -0,0 +1,46 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT License.
+// See the LICENSE file in the project root for more information.
+
+using System.Reactive.Linq;
+
+using BenchmarkDotNet.Attributes;
+
+using Benchmarks.System.Reactive.Infrastructure;
+
+#pragma warning disable CS0618 // The blocking operators are obsolete (callers are steered to async); benchmarked intentionally.
+
+namespace Benchmarks.System.Reactive.Operators.Blocking
+{
+ ///
+ /// The remaining blocking gate/pull operators: Single (predicate matching exactly one, so it scans the
+ /// whole stream), FirstOrDefault/LastOrDefault, and the pull GetEnumerator. (Latest/
+ /// MostRecent/Next/Chunkify are designed for hot sources and are exercised via the workload
+ /// harness rather than here.)
+ ///
+ [BenchmarkCategory("Blocking")]
+ public class BlockingPullBenchmarks : OperatorBenchmarkBase
+ {
+ [Benchmark]
+ public int Single() => Observable.Range(1, N).Single(static v => v == 1);
+
+ [Benchmark]
+ public int SingleOrDefault() => Observable.Range(1, N).SingleOrDefault(static v => v == 1);
+
+ [Benchmark]
+ public int FirstOrDefault() => Observable.Range(1, N).FirstOrDefault();
+
+ [Benchmark]
+ public int LastOrDefault() => Observable.Range(1, N).LastOrDefault();
+
+ [Benchmark]
+ public void GetEnumerator()
+ {
+ using var enumerator = Observable.Range(1, N).GetEnumerator();
+ while (enumerator.MoveNext())
+ {
+ Consumer.Consume(enumerator.Current);
+ }
+ }
+ }
+}
diff --git a/Rx.NET/Source/benchmarks/Benchmarks.System.Reactive/Operators/Blocking/FirstLastBenchmarks.cs b/Rx.NET/Source/benchmarks/Benchmarks.System.Reactive/Operators/Blocking/FirstLastBenchmarks.cs
new file mode 100644
index 0000000000..95d00e68b0
--- /dev/null
+++ b/Rx.NET/Source/benchmarks/Benchmarks.System.Reactive/Operators/Blocking/FirstLastBenchmarks.cs
@@ -0,0 +1,28 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT License.
+// See the LICENSE file in the project root for more information.
+
+using System.Reactive.Linq;
+
+using BenchmarkDotNet.Attributes;
+
+using Benchmarks.System.Reactive.Infrastructure;
+
+#pragma warning disable CS0618 // The blocking First/Last are obsolete (callers are steered to *Async); benchmarked intentionally.
+
+namespace Benchmarks.System.Reactive.Operators.Blocking
+{
+ ///
+ /// Blocking: First (returns as soon as the first element arrives) and Last (drains the whole
+ /// sequence). Both block the calling thread; the synchronous source resolves them without real waiting.
+ ///
+ [BenchmarkCategory("Blocking")]
+ public class FirstLastBenchmarks : OperatorBenchmarkBase
+ {
+ [Benchmark]
+ public int First() => Observable.Range(1, N).First();
+
+ [Benchmark]
+ public int Last() => Observable.Range(1, N).Last();
+ }
+}
diff --git a/Rx.NET/Source/benchmarks/Benchmarks.System.Reactive/Operators/Blocking/ForEachBenchmarks.cs b/Rx.NET/Source/benchmarks/Benchmarks.System.Reactive/Operators/Blocking/ForEachBenchmarks.cs
new file mode 100644
index 0000000000..a01a731c15
--- /dev/null
+++ b/Rx.NET/Source/benchmarks/Benchmarks.System.Reactive/Operators/Blocking/ForEachBenchmarks.cs
@@ -0,0 +1,22 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT License.
+// See the LICENSE file in the project root for more information.
+
+using System.Reactive.Linq;
+
+using BenchmarkDotNet.Attributes;
+
+using Benchmarks.System.Reactive.Infrastructure;
+
+#pragma warning disable CS0618 // Observable.ForEach is obsolete (callers are steered to async); benchmarked intentionally.
+
+namespace Benchmarks.System.Reactive.Operators.Blocking
+{
+ /// Blocking: ForEach invokes a callback per element and blocks until the sequence completes.
+ [BenchmarkCategory("Blocking")]
+ public class ForEachBenchmarks : OperatorBenchmarkBase
+ {
+ [Benchmark]
+ public void ForEach() => Observable.Range(1, N).ForEach(v => Consumer.Consume(v));
+ }
+}
diff --git a/Rx.NET/Source/benchmarks/Benchmarks.System.Reactive/Operators/Blocking/HotPullBenchmarks.cs b/Rx.NET/Source/benchmarks/Benchmarks.System.Reactive/Operators/Blocking/HotPullBenchmarks.cs
new file mode 100644
index 0000000000..97ab4b2a4a
--- /dev/null
+++ b/Rx.NET/Source/benchmarks/Benchmarks.System.Reactive/Operators/Blocking/HotPullBenchmarks.cs
@@ -0,0 +1,100 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT License.
+// See the LICENSE file in the project root for more information.
+
+using System.Reactive.Linq;
+using System.Reactive.Subjects;
+using System.Threading.Tasks;
+
+using BenchmarkDotNet.Attributes;
+using BenchmarkDotNet.Engines;
+
+namespace Benchmarks.System.Reactive.Operators.Blocking
+{
+ ///
+ /// The hot-source blocking-pull operators, driven by a pumped alongside the pull.
+ /// Latest/MostRecent/Chunkify pull deterministically on the producing thread; Next
+ /// requires a concurrent producer (it only captures values that arrive while a pull is blocked). These are
+ /// inherently timing-dependent, so expect higher variance than the cold-source benchmarks. N is capped.
+ ///
+ [SimpleJob(RunStrategy.Monitoring, launchCount: 1, warmupCount: 3, iterationCount: 10)] // timing-dependent (Next races a producer) → repeatable job
+ [BenchmarkCategory("Blocking")]
+ public class HotPullBenchmarks
+ {
+ [Params(1_000, 10_000)]
+ public int N;
+
+ private readonly Consumer _consumer = new();
+
+ [Benchmark]
+ public void Latest()
+ {
+ var subject = new Subject();
+ using var enumerator = subject.Latest().GetEnumerator();
+ for (var i = 0; i < N; i++)
+ {
+ subject.OnNext(i);
+ enumerator.MoveNext();
+ _consumer.Consume(enumerator.Current);
+ }
+
+ subject.OnCompleted();
+ }
+
+ [Benchmark]
+ public void MostRecent()
+ {
+ var subject = new Subject();
+ using var enumerator = subject.MostRecent(0).GetEnumerator();
+ for (var i = 0; i < N; i++)
+ {
+ subject.OnNext(i);
+ enumerator.MoveNext();
+ _consumer.Consume(enumerator.Current);
+ }
+
+ subject.OnCompleted();
+ }
+
+ [Benchmark]
+ public void Chunkify()
+ {
+ var subject = new Subject();
+ using var enumerator = subject.Chunkify().GetEnumerator();
+ for (var i = 0; i < N; i++)
+ {
+ subject.OnNext(i);
+ if ((i & 15) == 0)
+ {
+ enumerator.MoveNext();
+ _consumer.Consume(enumerator.Current);
+ }
+ }
+
+ subject.OnCompleted();
+ }
+
+ [Benchmark]
+ public void Next()
+ {
+ var subject = new Subject();
+ var n = N;
+ var pump = Task.Run(() =>
+ {
+ for (var i = 0; i < n; i++)
+ {
+ subject.OnNext(i);
+ }
+
+ subject.OnCompleted();
+ });
+
+ foreach (var value in subject.Next())
+ {
+ _consumer.Consume(value);
+ }
+
+ pump.Wait();
+ }
+ }
+}
diff --git a/Rx.NET/Source/benchmarks/Benchmarks.System.Reactive/Operators/Concurrency/ObserveOnBenchmarks.cs b/Rx.NET/Source/benchmarks/Benchmarks.System.Reactive/Operators/Concurrency/ObserveOnBenchmarks.cs
new file mode 100644
index 0000000000..fb9a4e64bf
--- /dev/null
+++ b/Rx.NET/Source/benchmarks/Benchmarks.System.Reactive/Operators/Concurrency/ObserveOnBenchmarks.cs
@@ -0,0 +1,52 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT License.
+// See the LICENSE file in the project root for more information.
+
+using System.Reactive.Concurrency;
+using System.Reactive.Linq;
+
+using BenchmarkDotNet.Attributes;
+using BenchmarkDotNet.Engines;
+
+using Benchmarks.System.Reactive.Infrastructure;
+
+namespace Benchmarks.System.Reactive.Operators.Concurrency
+{
+ ///
+ /// Concurrency-category exemplar (async S1): ObserveOn marshals every element onto another scheduler.
+ /// The benchmark blocks until completion via SubscribeBlocking. N is capped because each element
+ /// crosses a thread boundary.
+ ///
+ [SimpleJob(RunStrategy.Monitoring, launchCount: 1, warmupCount: 3, iterationCount: 10)] // thread-crossing → repeatable job, not the auto-tuned default
+ [BenchmarkCategory("Concurrency")]
+ public class ObserveOnBenchmarks
+ {
+ [Params(1_000, 10_000, 100_000)]
+ public int N;
+
+ private readonly Consumer _consumer = new();
+ private EventLoopScheduler _scheduler = default!;
+ private EventLoopScheduler _scheduler2 = default!;
+
+ [GlobalSetup]
+ public void Setup()
+ {
+ _scheduler = new EventLoopScheduler();
+ _scheduler2 = new EventLoopScheduler();
+ }
+
+ [GlobalCleanup]
+ public void Cleanup()
+ {
+ _scheduler.Dispose();
+ _scheduler2.Dispose();
+ }
+
+ [Benchmark]
+ public void ObserveOn() => Observable.Range(1, N).ObserveOn(_scheduler).SubscribeBlocking(_consumer);
+
+ [Benchmark]
+ public void SubscribeOn_ObserveOn() =>
+ Observable.Range(1, N).SubscribeOn(_scheduler).ObserveOn(_scheduler2).SubscribeBlocking(_consumer);
+ }
+}
diff --git a/Rx.NET/Source/benchmarks/Benchmarks.System.Reactive/Operators/Concurrency/SubscribeOnBenchmarks.cs b/Rx.NET/Source/benchmarks/Benchmarks.System.Reactive/Operators/Concurrency/SubscribeOnBenchmarks.cs
new file mode 100644
index 0000000000..97c2b6f574
--- /dev/null
+++ b/Rx.NET/Source/benchmarks/Benchmarks.System.Reactive/Operators/Concurrency/SubscribeOnBenchmarks.cs
@@ -0,0 +1,38 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT License.
+// See the LICENSE file in the project root for more information.
+
+using System.Reactive.Concurrency;
+using System.Reactive.Linq;
+
+using BenchmarkDotNet.Attributes;
+using BenchmarkDotNet.Engines;
+
+using Benchmarks.System.Reactive.Infrastructure;
+
+namespace Benchmarks.System.Reactive.Operators.Concurrency
+{
+ ///
+ /// Concurrency (async S1): SubscribeOn moves the subscription (and thus the synchronous source's
+ /// execution) onto another scheduler. Blocks until completion via SubscribeBlocking.
+ ///
+ [SimpleJob(RunStrategy.Monitoring, launchCount: 1, warmupCount: 3, iterationCount: 10)] // thread-crossing → repeatable job, not the auto-tuned default
+ [BenchmarkCategory("Concurrency")]
+ public class SubscribeOnBenchmarks
+ {
+ [Params(1_000, 10_000, 100_000)]
+ public int N;
+
+ private readonly Consumer _consumer = new();
+ private EventLoopScheduler _scheduler = default!;
+
+ [GlobalSetup]
+ public void Setup() => _scheduler = new EventLoopScheduler();
+
+ [GlobalCleanup]
+ public void Cleanup() => _scheduler.Dispose();
+
+ [Benchmark]
+ public void SubscribeOn() => Observable.Range(1, N).SubscribeOn(_scheduler).SubscribeBlocking(_consumer);
+ }
+}
diff --git a/Rx.NET/Source/benchmarks/Benchmarks.System.Reactive/Operators/Concurrency/SynchronizeBenchmarks.cs b/Rx.NET/Source/benchmarks/Benchmarks.System.Reactive/Operators/Concurrency/SynchronizeBenchmarks.cs
new file mode 100644
index 0000000000..74955d9b32
--- /dev/null
+++ b/Rx.NET/Source/benchmarks/Benchmarks.System.Reactive/Operators/Concurrency/SynchronizeBenchmarks.cs
@@ -0,0 +1,20 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT License.
+// See the LICENSE file in the project root for more information.
+
+using System.Reactive.Linq;
+
+using BenchmarkDotNet.Attributes;
+
+using Benchmarks.System.Reactive.Infrastructure;
+
+namespace Benchmarks.System.Reactive.Operators.Concurrency
+{
+ /// Concurrency (S1): Synchronize serializes notifications behind a gate — a per-notification lock cost.
+ [BenchmarkCategory("Concurrency")]
+ public class SynchronizeBenchmarks : OperatorBenchmarkBase
+ {
+ [Benchmark]
+ public void Synchronize() => Observable.Range(1, N).Synchronize().SubscribeConsume(Consumer);
+ }
+}
diff --git a/Rx.NET/Source/benchmarks/Benchmarks.System.Reactive/Operators/Conversions/ToEnumerableBenchmarks.cs b/Rx.NET/Source/benchmarks/Benchmarks.System.Reactive/Operators/Conversions/ToEnumerableBenchmarks.cs
new file mode 100644
index 0000000000..80bf70617a
--- /dev/null
+++ b/Rx.NET/Source/benchmarks/Benchmarks.System.Reactive/Operators/Conversions/ToEnumerableBenchmarks.cs
@@ -0,0 +1,29 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT License.
+// See the LICENSE file in the project root for more information.
+
+using System.Reactive.Linq;
+
+using BenchmarkDotNet.Attributes;
+
+using Benchmarks.System.Reactive.Infrastructure;
+
+namespace Benchmarks.System.Reactive.Operators.Conversions
+{
+ ///
+ /// Conversion-category exemplar: ToEnumerable converts a push sequence to a blocking pull sequence;
+ /// the benchmark iterates it to completion.
+ ///
+ [BenchmarkCategory("Conversions")]
+ public class ToEnumerableBenchmarks : OperatorBenchmarkBase
+ {
+ [Benchmark]
+ public void ToEnumerable()
+ {
+ foreach (var value in Observable.Range(1, N).ToEnumerable())
+ {
+ Consumer.Consume(value);
+ }
+ }
+ }
+}
diff --git a/Rx.NET/Source/benchmarks/Benchmarks.System.Reactive/Operators/Conversions/ToEventBenchmarks.cs b/Rx.NET/Source/benchmarks/Benchmarks.System.Reactive/Operators/Conversions/ToEventBenchmarks.cs
new file mode 100644
index 0000000000..5db14e6b2c
--- /dev/null
+++ b/Rx.NET/Source/benchmarks/Benchmarks.System.Reactive/Operators/Conversions/ToEventBenchmarks.cs
@@ -0,0 +1,48 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT License.
+// See the LICENSE file in the project root for more information.
+
+using System;
+using System.Linq;
+using System.Reactive;
+using System.Reactive.Linq;
+
+using BenchmarkDotNet.Attributes;
+
+using Benchmarks.System.Reactive.Infrastructure;
+
+namespace Benchmarks.System.Reactive.Operators.Conversions
+{
+ ///
+ /// Conversions bridging to .NET events and pull sources: ToEvent / ToEventPattern expose an
+ /// observable as an event (attaching the handler drives the synchronous source), and the
+ /// IEnumerable.Subscribe overload pushes a pull sequence to an observer.
+ ///
+ [BenchmarkCategory("Conversions")]
+ public class ToEventBenchmarks : OperatorBenchmarkBase
+ {
+ [Benchmark]
+ public void ToEvent()
+ {
+ var source = Observable.Range(1, N).ToEvent();
+ void Handler(int value) => Consumer.Consume(value);
+ source.OnNext += Handler; // attaching subscribes; the synchronous source runs to completion here
+ source.OnNext -= Handler;
+ }
+
+ [Benchmark]
+ public void ToEventPattern()
+ {
+ var source = Observable.Range(1, N)
+ .Select(_ => new EventPattern(this, EventArgs.Empty))
+ .ToEventPattern();
+ void Handler(object sender, EventArgs e) => Consumer.Consume(e);
+ source.OnNext += Handler;
+ source.OnNext -= Handler;
+ }
+
+ [Benchmark]
+ public void EnumerableSubscribe() =>
+ Enumerable.Range(1, N).Subscribe(new ConsumingObserver(Consumer));
+ }
+}
diff --git a/Rx.NET/Source/benchmarks/Benchmarks.System.Reactive/Operators/Conversions/ToObservableBenchmarks.cs b/Rx.NET/Source/benchmarks/Benchmarks.System.Reactive/Operators/Conversions/ToObservableBenchmarks.cs
new file mode 100644
index 0000000000..dd6d4c157b
--- /dev/null
+++ b/Rx.NET/Source/benchmarks/Benchmarks.System.Reactive/Operators/Conversions/ToObservableBenchmarks.cs
@@ -0,0 +1,21 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT License.
+// See the LICENSE file in the project root for more information.
+
+using System.Linq;
+using System.Reactive.Linq;
+
+using BenchmarkDotNet.Attributes;
+
+using Benchmarks.System.Reactive.Infrastructure;
+
+namespace Benchmarks.System.Reactive.Operators.Conversions
+{
+ /// S1: ToObservable bridges a pull sequence (IEnumerable) into a push sequence.
+ [BenchmarkCategory("Conversions")]
+ public class ToObservableBenchmarks : OperatorBenchmarkBase
+ {
+ [Benchmark]
+ public void ToObservable() => Enumerable.Range(1, N).ToObservable().SubscribeConsume(Consumer);
+ }
+}
diff --git a/Rx.NET/Source/benchmarks/Benchmarks.System.Reactive/Operators/Conversions/ToTaskBenchmarks.cs b/Rx.NET/Source/benchmarks/Benchmarks.System.Reactive/Operators/Conversions/ToTaskBenchmarks.cs
new file mode 100644
index 0000000000..2d884e6464
--- /dev/null
+++ b/Rx.NET/Source/benchmarks/Benchmarks.System.Reactive/Operators/Conversions/ToTaskBenchmarks.cs
@@ -0,0 +1,24 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT License.
+// See the LICENSE file in the project root for more information.
+
+using System.Reactive.Linq;
+using System.Reactive.Threading.Tasks;
+
+using BenchmarkDotNet.Attributes;
+
+using Benchmarks.System.Reactive.Infrastructure;
+
+namespace Benchmarks.System.Reactive.Operators.Conversions
+{
+ ///
+ /// S1: ToTask bridges a sequence to a Task of its final element. The synchronous source
+ /// completes immediately, so the task is already resolved when awaited.
+ ///
+ [BenchmarkCategory("Conversions")]
+ public class ToTaskBenchmarks : OperatorBenchmarkBase
+ {
+ [Benchmark]
+ public int ToTask() => Observable.Range(1, N).ToTask().GetAwaiter().GetResult();
+ }
+}
diff --git a/Rx.NET/Source/benchmarks/Benchmarks.System.Reactive/Operators/Creation/CreateBenchmarks.cs b/Rx.NET/Source/benchmarks/Benchmarks.System.Reactive/Operators/Creation/CreateBenchmarks.cs
new file mode 100644
index 0000000000..94f8144428
--- /dev/null
+++ b/Rx.NET/Source/benchmarks/Benchmarks.System.Reactive/Operators/Creation/CreateBenchmarks.cs
@@ -0,0 +1,34 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT License.
+// See the LICENSE file in the project root for more information.
+
+using System.Reactive.Disposables;
+using System.Reactive.Linq;
+
+using BenchmarkDotNet.Attributes;
+
+using Benchmarks.System.Reactive.Infrastructure;
+
+namespace Benchmarks.System.Reactive.Operators.Creation
+{
+ /// S1: Observable.Create — the primitive factory, pushing N elements from a user callback.
+ [BenchmarkCategory("Creation")]
+ public class CreateBenchmarks : OperatorBenchmarkBase
+ {
+ [Benchmark]
+ public void Create()
+ {
+ var n = N;
+ Observable.Create(observer =>
+ {
+ for (var i = 0; i < n; i++)
+ {
+ observer.OnNext(i);
+ }
+
+ observer.OnCompleted();
+ return Disposable.Empty;
+ }).SubscribeConsume(Consumer);
+ }
+ }
+}
diff --git a/Rx.NET/Source/benchmarks/Benchmarks.System.Reactive/Operators/Creation/DeferAsyncBenchmarks.cs b/Rx.NET/Source/benchmarks/Benchmarks.System.Reactive/Operators/Creation/DeferAsyncBenchmarks.cs
new file mode 100644
index 0000000000..d7bebd3339
--- /dev/null
+++ b/Rx.NET/Source/benchmarks/Benchmarks.System.Reactive/Operators/Creation/DeferAsyncBenchmarks.cs
@@ -0,0 +1,25 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT License.
+// See the LICENSE file in the project root for more information.
+
+using System.Reactive.Linq;
+using System.Threading.Tasks;
+
+using BenchmarkDotNet.Attributes;
+
+using Benchmarks.System.Reactive.Infrastructure;
+
+namespace Benchmarks.System.Reactive.Operators.Creation
+{
+ /// S1: DeferAsync builds the sequence lazily via an async factory, once per subscription.
+ [BenchmarkCategory("Creation")]
+ public class DeferAsyncBenchmarks : OperatorBenchmarkBase
+ {
+ [Benchmark]
+ public void DeferAsync()
+ {
+ var n = N;
+ Observable.DeferAsync(_ => Task.FromResult(Observable.Range(1, n))).SubscribeConsume(Consumer);
+ }
+ }
+}
diff --git a/Rx.NET/Source/benchmarks/Benchmarks.System.Reactive/Operators/Creation/DeferBenchmarks.cs b/Rx.NET/Source/benchmarks/Benchmarks.System.Reactive/Operators/Creation/DeferBenchmarks.cs
new file mode 100644
index 0000000000..4e72673cfe
--- /dev/null
+++ b/Rx.NET/Source/benchmarks/Benchmarks.System.Reactive/Operators/Creation/DeferBenchmarks.cs
@@ -0,0 +1,24 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT License.
+// See the LICENSE file in the project root for more information.
+
+using System.Reactive.Linq;
+
+using BenchmarkDotNet.Attributes;
+
+using Benchmarks.System.Reactive.Infrastructure;
+
+namespace Benchmarks.System.Reactive.Operators.Creation
+{
+ /// S1: Defer builds the underlying sequence lazily, once per subscription.
+ [BenchmarkCategory("Creation")]
+ public class DeferBenchmarks : OperatorBenchmarkBase
+ {
+ [Benchmark]
+ public void Defer()
+ {
+ var n = N;
+ Observable.Defer(() => Observable.Range(1, n)).SubscribeConsume(Consumer);
+ }
+ }
+}
diff --git a/Rx.NET/Source/benchmarks/Benchmarks.System.Reactive/Operators/Creation/GenerateBenchmarks.cs b/Rx.NET/Source/benchmarks/Benchmarks.System.Reactive/Operators/Creation/GenerateBenchmarks.cs
new file mode 100644
index 0000000000..38b41141aa
--- /dev/null
+++ b/Rx.NET/Source/benchmarks/Benchmarks.System.Reactive/Operators/Creation/GenerateBenchmarks.cs
@@ -0,0 +1,24 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT License.
+// See the LICENSE file in the project root for more information.
+
+using System.Reactive.Linq;
+
+using BenchmarkDotNet.Attributes;
+
+using Benchmarks.System.Reactive.Infrastructure;
+
+namespace Benchmarks.System.Reactive.Operators.Creation
+{
+ /// S1: the synchronous (untimed) Generate unfold. (The timed overload lives under Operators/Time.)
+ [BenchmarkCategory("Creation")]
+ public class GenerateBenchmarks : OperatorBenchmarkBase
+ {
+ [Benchmark]
+ public void Generate()
+ {
+ var n = N;
+ Observable.Generate(0, i => i < n, static i => i + 1, static i => i).SubscribeConsume(Consumer);
+ }
+ }
+}
diff --git a/Rx.NET/Source/benchmarks/Benchmarks.System.Reactive/Operators/Creation/RangeBenchmarks.cs b/Rx.NET/Source/benchmarks/Benchmarks.System.Reactive/Operators/Creation/RangeBenchmarks.cs
new file mode 100644
index 0000000000..be1e921581
--- /dev/null
+++ b/Rx.NET/Source/benchmarks/Benchmarks.System.Reactive/Operators/Creation/RangeBenchmarks.cs
@@ -0,0 +1,20 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT License.
+// See the LICENSE file in the project root for more information.
+
+using System.Reactive.Linq;
+
+using BenchmarkDotNet.Attributes;
+
+using Benchmarks.System.Reactive.Infrastructure;
+
+namespace Benchmarks.System.Reactive.Operators.Creation
+{
+ /// Creation-category exemplar (S1): Observable.Range — the canonical synchronous source.
+ [BenchmarkCategory("Creation")]
+ public class RangeBenchmarks : OperatorBenchmarkBase
+ {
+ [Benchmark]
+ public void Range() => Observable.Range(1, N).SubscribeConsume(Consumer);
+ }
+}
diff --git a/Rx.NET/Source/benchmarks/Benchmarks.System.Reactive/Operators/Creation/RepeatBenchmarks.cs b/Rx.NET/Source/benchmarks/Benchmarks.System.Reactive/Operators/Creation/RepeatBenchmarks.cs
new file mode 100644
index 0000000000..8ada78bf0f
--- /dev/null
+++ b/Rx.NET/Source/benchmarks/Benchmarks.System.Reactive/Operators/Creation/RepeatBenchmarks.cs
@@ -0,0 +1,20 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT License.
+// See the LICENSE file in the project root for more information.
+
+using System.Reactive.Linq;
+
+using BenchmarkDotNet.Attributes;
+
+using Benchmarks.System.Reactive.Infrastructure;
+
+namespace Benchmarks.System.Reactive.Operators.Creation
+{
+ /// S1: Repeat(value, count) emits a constant N times.
+ [BenchmarkCategory("Creation")]
+ public class RepeatBenchmarks : OperatorBenchmarkBase
+ {
+ [Benchmark]
+ public void Repeat() => Observable.Repeat(1, N).SubscribeConsume(Consumer);
+ }
+}
diff --git a/Rx.NET/Source/benchmarks/Benchmarks.System.Reactive/Operators/Creation/ScalarCreationBenchmarks.cs b/Rx.NET/Source/benchmarks/Benchmarks.System.Reactive/Operators/Creation/ScalarCreationBenchmarks.cs
new file mode 100644
index 0000000000..bcc875ba08
--- /dev/null
+++ b/Rx.NET/Source/benchmarks/Benchmarks.System.Reactive/Operators/Creation/ScalarCreationBenchmarks.cs
@@ -0,0 +1,35 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT License.
+// See the LICENSE file in the project root for more information.
+
+using System;
+using System.Reactive.Linq;
+
+using BenchmarkDotNet.Attributes;
+using BenchmarkDotNet.Engines;
+
+using Benchmarks.System.Reactive.Infrastructure;
+
+namespace Benchmarks.System.Reactive.Operators.Creation
+{
+ ///
+ /// Subscription-cost (S4) exemplars for the scalar factories, which ignore N: Return (one element),
+ /// Empty (immediate completion), Throw (immediate error). Measures the per-subscription
+ /// construct + terminate cost.
+ ///
+ [BenchmarkCategory("Creation")]
+ public class ScalarCreationBenchmarks
+ {
+ private readonly Consumer _consumer = new();
+ private readonly Exception _error = new InvalidOperationException("benchmark");
+
+ [Benchmark]
+ public void Return() => Observable.Return(1).SubscribeConsume(_consumer);
+
+ [Benchmark]
+ public void Empty() => Observable.Empty().SubscribeConsume(_consumer);
+
+ [Benchmark]
+ public void Throw() => Observable.Throw(_error).SubscribeConsume(_consumer);
+ }
+}
diff --git a/Rx.NET/Source/benchmarks/Benchmarks.System.Reactive/Operators/Creation/UsingBenchmarks.cs b/Rx.NET/Source/benchmarks/Benchmarks.System.Reactive/Operators/Creation/UsingBenchmarks.cs
new file mode 100644
index 0000000000..f623bfe62c
--- /dev/null
+++ b/Rx.NET/Source/benchmarks/Benchmarks.System.Reactive/Operators/Creation/UsingBenchmarks.cs
@@ -0,0 +1,35 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT License.
+// See the LICENSE file in the project root for more information.
+
+using System.Reactive.Disposables;
+using System.Reactive.Linq;
+
+using BenchmarkDotNet.Attributes;
+
+using Benchmarks.System.Reactive.Infrastructure;
+
+namespace Benchmarks.System.Reactive.Operators.Creation
+{
+ ///
+ /// Using binds a disposable resource's lifetime to the subscription. Using_Error drives a faulting
+ /// source so the resource is disposed on the error path.
+ ///
+ [BenchmarkCategory("Creation")]
+ public class UsingBenchmarks : OperatorBenchmarkBase
+ {
+ [Benchmark]
+ public void Using()
+ {
+ var n = N;
+ Observable.Using(static () => Disposable.Empty, _ => Observable.Range(1, n)).SubscribeConsume(Consumer);
+ }
+
+ [Benchmark]
+ public void Using_Error()
+ {
+ var n = N;
+ Observable.Using(static () => Disposable.Empty, _ => FaultingSource.Faulting(n)).SubscribeConsume(Consumer);
+ }
+ }
+}
diff --git a/Rx.NET/Source/benchmarks/Benchmarks.System.Reactive/Operators/Events/FromEventBenchmarks.cs b/Rx.NET/Source/benchmarks/Benchmarks.System.Reactive/Operators/Events/FromEventBenchmarks.cs
new file mode 100644
index 0000000000..de45c15526
--- /dev/null
+++ b/Rx.NET/Source/benchmarks/Benchmarks.System.Reactive/Operators/Events/FromEventBenchmarks.cs
@@ -0,0 +1,52 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT License.
+// See the LICENSE file in the project root for more information.
+
+using System;
+using System.Reactive.Concurrency;
+using System.Reactive.Linq;
+
+using BenchmarkDotNet.Attributes;
+
+using Benchmarks.System.Reactive.Infrastructure;
+
+namespace Benchmarks.System.Reactive.Operators.Events
+{
+ ///
+ /// Event bridging: FromEvent / FromEventPattern expose a .NET event as an observable. The
+ /// immediate scheduler is used so the handler is attached synchronously, then the event is raised N times.
+ ///
+ [BenchmarkCategory("Events")]
+ public class FromEventBenchmarks : OperatorBenchmarkBase
+ {
+ private event Action Plain;
+
+ private event EventHandler Pattern;
+
+ [Benchmark]
+ public void FromEvent()
+ {
+ using (Observable.FromEvent(h => Plain += h, h => Plain -= h, ImmediateScheduler.Instance).SubscribeConsume(Consumer))
+ {
+ var n = N;
+ for (var i = 0; i < n; i++)
+ {
+ Plain?.Invoke(i);
+ }
+ }
+ }
+
+ [Benchmark]
+ public void FromEventPattern()
+ {
+ using (Observable.FromEventPattern(h => Pattern += h, h => Pattern -= h, ImmediateScheduler.Instance).SubscribeConsume(Consumer))
+ {
+ var n = N;
+ for (var i = 0; i < n; i++)
+ {
+ Pattern?.Invoke(this, i);
+ }
+ }
+ }
+ }
+}
diff --git a/Rx.NET/Source/benchmarks/Benchmarks.System.Reactive/Operators/Hot/HotSourceBenchmarks.cs b/Rx.NET/Source/benchmarks/Benchmarks.System.Reactive/Operators/Hot/HotSourceBenchmarks.cs
new file mode 100644
index 0000000000..218ebdc31b
--- /dev/null
+++ b/Rx.NET/Source/benchmarks/Benchmarks.System.Reactive/Operators/Hot/HotSourceBenchmarks.cs
@@ -0,0 +1,97 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT License.
+// See the LICENSE file in the project root for more information.
+
+using System.Reactive.Linq;
+using System.Reactive.Subjects;
+
+using BenchmarkDotNet.Attributes;
+using BenchmarkDotNet.Engines;
+
+using Benchmarks.System.Reactive.Infrastructure;
+
+namespace Benchmarks.System.Reactive.Operators.Hot
+{
+ ///
+ /// Hot-push (S2) variants of the core transformation operators — driven by pumping a
+ /// rather than a cold Observable.Range. This exercises the real push path (per-element OnNext
+ /// dispatch, re-entrancy) with none of Range's synchronous fast-path.
+ ///
+ [BenchmarkCategory("Hot")]
+ public class HotSourceBenchmarks
+ {
+ [Params(1_000, 10_000, 100_000)]
+ public int N;
+
+ private readonly Consumer _consumer = new();
+
+ [Benchmark]
+ public void Select_Hot()
+ {
+ var source = new Subject();
+ source.Select(static v => v + 1).SubscribeConsume(_consumer);
+ Pump(source);
+ }
+
+ [Benchmark]
+ public void Where_Hot()
+ {
+ var source = new Subject();
+ source.Where(static v => (v & 1) == 0).SubscribeConsume(_consumer);
+ Pump(source);
+ }
+
+ [Benchmark]
+ public void SelectMany_Hot()
+ {
+ var source = new Subject();
+ source.SelectMany(static v => Observable.Return(v)).SubscribeConsume(_consumer);
+ Pump(source);
+ }
+
+ [Benchmark]
+ public void Scan_Hot()
+ {
+ var source = new Subject();
+ source.Scan(0L, static (acc, v) => acc + v).SubscribeConsume(_consumer);
+ Pump(source);
+ }
+
+ [Benchmark]
+ public void GroupBy_Hot()
+ {
+ var source = new Subject();
+ source.GroupBy(static v => v % 8).SelectMany(static g => g).SubscribeConsume(_consumer);
+ Pump(source);
+ }
+
+ [Benchmark]
+ public void Merge_Hot()
+ {
+ var a = new Subject();
+ var b = new Subject();
+ Observable.Merge(a, b).SubscribeConsume(_consumer);
+
+ var half = N / 2;
+ for (var i = 0; i < half; i++)
+ {
+ a.OnNext(i);
+ b.OnNext(i);
+ }
+
+ a.OnCompleted();
+ b.OnCompleted();
+ }
+
+ private void Pump(Subject source)
+ {
+ var n = N;
+ for (var i = 0; i < n; i++)
+ {
+ source.OnNext(i);
+ }
+
+ source.OnCompleted();
+ }
+ }
+}
diff --git a/Rx.NET/Source/benchmarks/Benchmarks.System.Reactive/Operators/Hot/SubjectBenchmarks.cs b/Rx.NET/Source/benchmarks/Benchmarks.System.Reactive/Operators/Hot/SubjectBenchmarks.cs
new file mode 100644
index 0000000000..80002df090
--- /dev/null
+++ b/Rx.NET/Source/benchmarks/Benchmarks.System.Reactive/Operators/Hot/SubjectBenchmarks.cs
@@ -0,0 +1,67 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT License.
+// See the LICENSE file in the project root for more information.
+
+using System;
+using System.Reactive.Subjects;
+
+using BenchmarkDotNet.Attributes;
+using BenchmarkDotNet.Engines;
+
+using Benchmarks.System.Reactive.Infrastructure;
+
+namespace Benchmarks.System.Reactive.Operators.Hot
+{
+ ///
+ /// S2: raw subject push across the subject variants, with M concurrent subscribers
+ /// (0 = dispatch to nobody, the cheapest path; 5 = the multi-subscriber array dispatch).
+ /// Complements , which pumps operators through a single subject.
+ ///
+ [BenchmarkCategory("Hot")]
+ public class SubjectBenchmarks
+ {
+ [Params(1_000, 10_000, 100_000)]
+ public int N;
+
+ [Params(0, 1, 5)]
+ public int M;
+
+ private readonly Consumer _consumer = new();
+
+ [Benchmark]
+ public void Subject_Push() => Push(new Subject());
+
+ [Benchmark]
+ public void AsyncSubject_Push() => Push(new AsyncSubject());
+
+ [Benchmark]
+ public void BehaviorSubject_Push() => Push(new BehaviorSubject(-1));
+
+ [Benchmark]
+ public void ReplaySubject_Push() => Push(new ReplaySubject());
+
+ private void Push(TSubject subject)
+ where TSubject : ISubject
+ {
+ var m = M;
+ var subscriptions = new IDisposable[m];
+ for (var i = 0; i < m; i++)
+ {
+ subscriptions[i] = subject.SubscribeConsume(_consumer);
+ }
+
+ var n = N;
+ for (var i = 0; i < n; i++)
+ {
+ subject.OnNext(i);
+ }
+
+ subject.OnCompleted();
+
+ for (var i = 0; i < m; i++)
+ {
+ subscriptions[i].Dispose();
+ }
+ }
+ }
+}
diff --git a/Rx.NET/Source/benchmarks/Benchmarks.System.Reactive/Operators/Imperative/CaseForEachAsyncBenchmarks.cs b/Rx.NET/Source/benchmarks/Benchmarks.System.Reactive/Operators/Imperative/CaseForEachAsyncBenchmarks.cs
new file mode 100644
index 0000000000..7349daf072
--- /dev/null
+++ b/Rx.NET/Source/benchmarks/Benchmarks.System.Reactive/Operators/Imperative/CaseForEachAsyncBenchmarks.cs
@@ -0,0 +1,35 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT License.
+// See the LICENSE file in the project root for more information.
+
+using System;
+using System.Collections.Generic;
+using System.Reactive.Linq;
+
+using BenchmarkDotNet.Attributes;
+using BenchmarkDotNet.Engines;
+
+using Benchmarks.System.Reactive.Infrastructure;
+
+namespace Benchmarks.System.Reactive.Operators.Imperative
+{
+ ///
+ /// Imperative: Case selects a source from a key→source map at subscription time; ForEachAsync is
+ /// the Task-returning blocking iteration (which is why this class uses a repeatable Monitoring job).
+ ///
+ [SimpleJob(RunStrategy.Monitoring, launchCount: 1, warmupCount: 3, iterationCount: 10)]
+ [BenchmarkCategory("Imperative")]
+ public class CaseForEachAsyncBenchmarks : OperatorBenchmarkBase
+ {
+ private Dictionary> _cases = default!;
+
+ [GlobalSetup]
+ public void Setup() => _cases = new Dictionary> { [0] = Observable.Range(1, N) };
+
+ [Benchmark]
+ public void Case() => Observable.Case(static () => 0, _cases).SubscribeConsume(Consumer);
+
+ [Benchmark]
+ public void ForEachAsync() => Observable.Range(1, N).ForEachAsync(v => Consumer.Consume(v)).GetAwaiter().GetResult();
+ }
+}
diff --git a/Rx.NET/Source/benchmarks/Benchmarks.System.Reactive/Operators/Imperative/IfBenchmarks.cs b/Rx.NET/Source/benchmarks/Benchmarks.System.Reactive/Operators/Imperative/IfBenchmarks.cs
new file mode 100644
index 0000000000..6eb3f66d6f
--- /dev/null
+++ b/Rx.NET/Source/benchmarks/Benchmarks.System.Reactive/Operators/Imperative/IfBenchmarks.cs
@@ -0,0 +1,23 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT License.
+// See the LICENSE file in the project root for more information.
+
+using System.Reactive.Linq;
+
+using BenchmarkDotNet.Attributes;
+
+using Benchmarks.System.Reactive.Infrastructure;
+
+namespace Benchmarks.System.Reactive.Operators.Imperative
+{
+ ///
+ /// Imperative-combinator-category exemplar (S1): Observable.If chooses a source from a predicate at
+ /// subscription time, then relays it.
+ ///
+ [BenchmarkCategory("Imperative")]
+ public class IfBenchmarks : OperatorBenchmarkBase
+ {
+ [Benchmark]
+ public void If() => Observable.If(static () => true, Observable.Range(1, N)).SubscribeConsume(Consumer);
+ }
+}
diff --git a/Rx.NET/Source/benchmarks/Benchmarks.System.Reactive/Operators/Imperative/ImperativeBenchmarks.cs b/Rx.NET/Source/benchmarks/Benchmarks.System.Reactive/Operators/Imperative/ImperativeBenchmarks.cs
new file mode 100644
index 0000000000..feb051f22f
--- /dev/null
+++ b/Rx.NET/Source/benchmarks/Benchmarks.System.Reactive/Operators/Imperative/ImperativeBenchmarks.cs
@@ -0,0 +1,41 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT License.
+// See the LICENSE file in the project root for more information.
+
+using System.Linq;
+using System.Reactive.Linq;
+
+using BenchmarkDotNet.Attributes;
+
+using Benchmarks.System.Reactive.Infrastructure;
+
+namespace Benchmarks.System.Reactive.Operators.Imperative
+{
+ ///
+ /// S1 imperative combinators: For (concatenate a projected sequence per source item), and the
+ /// condition-driven loops While / DoWhile. (If is covered by IfBenchmarks.)
+ ///
+ [BenchmarkCategory("Imperative")]
+ public class ImperativeBenchmarks : OperatorBenchmarkBase
+ {
+ [Benchmark]
+ public void For() =>
+ Observable.For(Enumerable.Range(1, N), static i => Observable.Return(i)).SubscribeConsume(Consumer);
+
+ [Benchmark]
+ public void While()
+ {
+ var i = 0;
+ var n = N;
+ Observable.While(() => i++ < n, Observable.Return(1)).SubscribeConsume(Consumer);
+ }
+
+ [Benchmark]
+ public void DoWhile()
+ {
+ var i = 0;
+ var n = N;
+ Observable.DoWhile(Observable.Return(1), () => ++i < n).SubscribeConsume(Consumer);
+ }
+ }
+}
diff --git a/Rx.NET/Source/benchmarks/Benchmarks.System.Reactive/Operators/Joins/AndThenWhenBenchmarks.cs b/Rx.NET/Source/benchmarks/Benchmarks.System.Reactive/Operators/Joins/AndThenWhenBenchmarks.cs
new file mode 100644
index 0000000000..370ffd2e12
--- /dev/null
+++ b/Rx.NET/Source/benchmarks/Benchmarks.System.Reactive/Operators/Joins/AndThenWhenBenchmarks.cs
@@ -0,0 +1,34 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT License.
+// See the LICENSE file in the project root for more information.
+
+using System.Reactive.Linq;
+
+using BenchmarkDotNet.Attributes;
+using BenchmarkDotNet.Engines;
+
+namespace Benchmarks.System.Reactive.Operators.Joins
+{
+ ///
+ /// Join-patterns-category exemplar (S1): And/Then/When (the Rx join calculus) pairs
+ /// elements from two sources. N is capped because unmatched elements are queued (O(N) memory).
+ ///
+ [BenchmarkCategory("Joins")]
+ public class AndThenWhenBenchmarks
+ {
+ [Params(100, 1_000, 10_000, 100_000)]
+ public int N;
+
+ private readonly Consumer _consumer = new();
+
+ [Benchmark]
+ public void When()
+ {
+ var left = Observable.Range(1, N);
+ var right = Observable.Range(1, N);
+
+ Observable.When(left.And(right).Then(static (a, b) => a + b))
+ .Subscribe(new Infrastructure.ConsumingObserver(_consumer));
+ }
+ }
+}
diff --git a/Rx.NET/Source/benchmarks/Benchmarks.System.Reactive/Operators/Lifecycle/ConstructionBenchmarks.cs b/Rx.NET/Source/benchmarks/Benchmarks.System.Reactive/Operators/Lifecycle/ConstructionBenchmarks.cs
new file mode 100644
index 0000000000..2aa2c6e9a0
--- /dev/null
+++ b/Rx.NET/Source/benchmarks/Benchmarks.System.Reactive/Operators/Lifecycle/ConstructionBenchmarks.cs
@@ -0,0 +1,37 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT License.
+// See the LICENSE file in the project root for more information.
+
+using System;
+using System.Reactive.Linq;
+
+using BenchmarkDotNet.Attributes;
+
+using Benchmarks.System.Reactive.Infrastructure;
+
+namespace Benchmarks.System.Reactive.Operators.Lifecycle
+{
+ ///
+ /// Decomposes a pipeline's cost into construction vs subscription+drain (the legacy Prepend_Create/_Subscribe
+ /// technique). _Full (baseline) does everything; _Create only builds the operator chain (returns
+ /// it unsubscribed); _Subscribe subscribes and drains a pipeline pre-built in [GlobalSetup]. The
+ /// Ratio isolates producer-construction from per-subscription sink allocation + per-element cost.
+ ///
+ [BenchmarkCategory("Lifecycle")]
+ public class ConstructionBenchmarks : OperatorBenchmarkBase
+ {
+ private IObservable _prebuilt = default!;
+
+ [GlobalSetup]
+ public void Setup() => _prebuilt = Observable.Range(1, N).Select(static v => v + 1);
+
+ [Benchmark(Baseline = true)]
+ public void Select_Full() => Observable.Range(1, N).Select(static v => v + 1).SubscribeConsume(Consumer);
+
+ [Benchmark]
+ public IObservable Select_Create() => Observable.Range(1, N).Select(static v => v + 1);
+
+ [Benchmark]
+ public void Select_Subscribe() => _prebuilt.SubscribeConsume(Consumer);
+ }
+}
diff --git a/Rx.NET/Source/benchmarks/Benchmarks.System.Reactive/Operators/Lifecycle/DisposeBenchmarks.cs b/Rx.NET/Source/benchmarks/Benchmarks.System.Reactive/Operators/Lifecycle/DisposeBenchmarks.cs
new file mode 100644
index 0000000000..29fde6064a
--- /dev/null
+++ b/Rx.NET/Source/benchmarks/Benchmarks.System.Reactive/Operators/Lifecycle/DisposeBenchmarks.cs
@@ -0,0 +1,116 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT License.
+// See the LICENSE file in the project root for more information.
+
+using System;
+using System.Reactive.Linq;
+using System.Reactive.Subjects;
+
+using BenchmarkDotNet.Attributes;
+using BenchmarkDotNet.Engines;
+
+using Benchmarks.System.Reactive.Infrastructure;
+
+namespace Benchmarks.System.Reactive.Operators.Lifecycle
+{
+ ///
+ /// Lifecycle: tears down a still-live subscription mid-stream — the hot Subject sources never
+ /// complete, so the usingDispose() at method end lands on an active subscription. This exercises
+ /// the unsubscribe/cleanup path of operators with non-trivial disposal, which every cold-source benchmark
+ /// misses (their sources complete synchronously before Dispose is ever reached).
+ ///
+ [BenchmarkCategory("Lifecycle")]
+ public class DisposeBenchmarks
+ {
+ [Params(1_000, 10_000, 100_000)]
+ public int N;
+
+ private readonly Consumer _consumer = new();
+
+ [Benchmark]
+ public void Merge_Dispose()
+ {
+ var a = new Subject();
+ var b = new Subject();
+ using var subscription = Observable.Merge(a, b).SubscribeConsume(_consumer);
+
+ var half = N / 2;
+ for (var i = 0; i < half; i++)
+ {
+ a.OnNext(i);
+ b.OnNext(i);
+ }
+ }
+
+ [Benchmark]
+ public void CombineLatest_Dispose()
+ {
+ var a = new Subject();
+ var b = new Subject();
+ using var subscription = Observable.CombineLatest(a, b, static (x, y) => x + y).SubscribeConsume(_consumer);
+
+ var half = N / 2;
+ for (var i = 0; i < half; i++)
+ {
+ a.OnNext(i);
+ b.OnNext(i);
+ }
+ }
+
+ [Benchmark]
+ public void Switch_Dispose()
+ {
+ var outer = new Subject>();
+ var inner = new Subject();
+ using var subscription = outer.Switch().SubscribeConsume(_consumer);
+ outer.OnNext(inner);
+
+ var half = N / 2;
+ for (var i = 0; i < half; i++)
+ {
+ inner.OnNext(i);
+ }
+ }
+
+ [Benchmark]
+ public void GroupBy_Dispose()
+ {
+ var source = new Subject();
+ using var subscription = source.GroupBy(static v => v % 8).SelectMany(static g => g).SubscribeConsume(_consumer);
+
+ var half = N / 2;
+ for (var i = 0; i < half; i++)
+ {
+ source.OnNext(i);
+ }
+ }
+
+ [Benchmark]
+ public void Window_Dispose()
+ {
+ var source = new Subject();
+ using var subscription = source.Window(16).SelectMany(static w => w).SubscribeConsume(_consumer);
+
+ var half = N / 2;
+ for (var i = 0; i < half; i++)
+ {
+ source.OnNext(i);
+ }
+ }
+
+ [Benchmark]
+ public void Publish_Dispose()
+ {
+ var source = new Subject();
+ var published = source.Publish();
+ using var subscription = published.SubscribeConsume(_consumer);
+ using var connection = published.Connect();
+
+ var half = N / 2;
+ for (var i = 0; i < half; i++)
+ {
+ source.OnNext(i);
+ }
+ }
+ }
+}
diff --git a/Rx.NET/Source/benchmarks/Benchmarks.System.Reactive/StableCompositeDisposableBenchmark.cs b/Rx.NET/Source/benchmarks/Benchmarks.System.Reactive/Operators/Lifecycle/StableCompositeDisposableBenchmarks.cs
similarity index 81%
rename from Rx.NET/Source/benchmarks/Benchmarks.System.Reactive/StableCompositeDisposableBenchmark.cs
rename to Rx.NET/Source/benchmarks/Benchmarks.System.Reactive/Operators/Lifecycle/StableCompositeDisposableBenchmarks.cs
index 7bdd5e2b06..681c68ed98 100644
--- a/Rx.NET/Source/benchmarks/Benchmarks.System.Reactive/StableCompositeDisposableBenchmark.cs
+++ b/Rx.NET/Source/benchmarks/Benchmarks.System.Reactive/Operators/Lifecycle/StableCompositeDisposableBenchmarks.cs
@@ -1,19 +1,23 @@
-// Licensed to the .NET Foundation under one or more agreements.
+// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT License.
// See the LICENSE file in the project root for more information.
using System;
using System.Collections.Generic;
-using System.Linq;
using System.Reactive.Disposables;
-using System.Reactive.Linq;
using System.Threading;
+
using BenchmarkDotNet.Attributes;
-namespace Benchmarks.System.Reactive
+namespace Benchmarks.System.Reactive.Operators.Lifecycle
{
- [MemoryDiagnoser]
- public class StableCompositeDisposableBenchmark
+ ///
+ /// Construction and disposal cost of over array and list inputs.
+ /// N is the number of inner disposables (small values dominate real operator usage; 100 probes the
+ /// scaling of the copy). Benchmarks return the created object so it is consumed, not eliminated.
+ ///
+ [BenchmarkCategory("Lifecycle")]
+ public class StableCompositeDisposableBenchmarks
{
[Params(3, 4, 5, 6, 7, 8, 9, 10, 100)]
public int N;
@@ -30,6 +34,7 @@ public void Setup()
{
_array[i] = Disposable.Empty;
}
+
_list = new List(_array);
}
@@ -68,7 +73,7 @@ public object Dispose_List()
}
[Benchmark]
- public object Dispose_Trused_Array()
+ public object Dispose_Trusted_Array()
{
var scd = CreateTrusted(_array);
scd.Dispose();
diff --git a/Rx.NET/Source/benchmarks/Benchmarks.System.Reactive/Operators/Multiple/AmbBenchmarks.cs b/Rx.NET/Source/benchmarks/Benchmarks.System.Reactive/Operators/Multiple/AmbBenchmarks.cs
new file mode 100644
index 0000000000..40025c5d0d
--- /dev/null
+++ b/Rx.NET/Source/benchmarks/Benchmarks.System.Reactive/Operators/Multiple/AmbBenchmarks.cs
@@ -0,0 +1,20 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT License.
+// See the LICENSE file in the project root for more information.
+
+using System.Reactive.Linq;
+
+using BenchmarkDotNet.Attributes;
+
+using Benchmarks.System.Reactive.Infrastructure;
+
+namespace Benchmarks.System.Reactive.Operators.Multiple
+{
+ /// S1: Amb races two sources; the synchronous Range wins over Never.
+ [BenchmarkCategory("Multiple")]
+ public class AmbBenchmarks : OperatorBenchmarkBase
+ {
+ [Benchmark]
+ public void Amb() => Observable.Range(1, N).Amb(Observable.Never()).SubscribeConsume(Consumer);
+ }
+}
diff --git a/Rx.NET/Source/benchmarks/Benchmarks.System.Reactive/Operators/Multiple/CatchBenchmarks.cs b/Rx.NET/Source/benchmarks/Benchmarks.System.Reactive/Operators/Multiple/CatchBenchmarks.cs
new file mode 100644
index 0000000000..11a90b658c
--- /dev/null
+++ b/Rx.NET/Source/benchmarks/Benchmarks.System.Reactive/Operators/Multiple/CatchBenchmarks.cs
@@ -0,0 +1,36 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT License.
+// See the LICENSE file in the project root for more information.
+
+using System.Reactive.Linq;
+
+using BenchmarkDotNet.Attributes;
+
+using Benchmarks.System.Reactive.Infrastructure;
+
+namespace Benchmarks.System.Reactive.Operators.Multiple
+{
+ ///
+ /// Error-continuation. The plain methods run the happy path (source completes, so the handler/second source is
+ /// never needed); the _Error methods drive an OnError through, so the handler is actually
+ /// subscribed — the path that matters for these operators.
+ ///
+ [BenchmarkCategory("Multiple")]
+ public class CatchBenchmarks : OperatorBenchmarkBase
+ {
+ [Benchmark]
+ public void Catch() => Observable.Range(1, N).Catch(Observable.Range(1, N)).SubscribeConsume(Consumer);
+
+ [Benchmark]
+ public void OnErrorResumeNext() =>
+ Observable.OnErrorResumeNext(Observable.Range(1, N), Observable.Range(1, N)).SubscribeConsume(Consumer);
+
+ [Benchmark]
+ public void Catch_Error() =>
+ FaultingSource.Faulting(N).Catch(Observable.Range(1, N)).SubscribeConsume(Consumer);
+
+ [Benchmark]
+ public void OnErrorResumeNext_Error() =>
+ Observable.OnErrorResumeNext(FaultingSource.Faulting(N), Observable.Range(1, N)).SubscribeConsume(Consumer);
+ }
+}
diff --git a/Rx.NET/Source/benchmarks/Benchmarks.System.Reactive/Operators/Multiple/CombineLatestBenchmarks.cs b/Rx.NET/Source/benchmarks/Benchmarks.System.Reactive/Operators/Multiple/CombineLatestBenchmarks.cs
new file mode 100644
index 0000000000..01342108fa
--- /dev/null
+++ b/Rx.NET/Source/benchmarks/Benchmarks.System.Reactive/Operators/Multiple/CombineLatestBenchmarks.cs
@@ -0,0 +1,24 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT License.
+// See the LICENSE file in the project root for more information.
+
+using System.Reactive.Linq;
+
+using BenchmarkDotNet.Attributes;
+
+using Benchmarks.System.Reactive.Infrastructure;
+
+namespace Benchmarks.System.Reactive.Operators.Multiple
+{
+ ///
+ /// S1: CombineLatest emits the combination whenever either source produces, once both have a value.
+ /// (Reimplemented cleanly — the old benchmark delegated to a unit test and measured its assertions.)
+ ///
+ [BenchmarkCategory("Multiple")]
+ public class CombineLatestBenchmarks : OperatorBenchmarkBase
+ {
+ [Benchmark]
+ public void CombineLatest() =>
+ Observable.CombineLatest(Observable.Range(1, N), Observable.Range(1, N), static (a, b) => a + b).SubscribeConsume(Consumer);
+ }
+}
diff --git a/Rx.NET/Source/benchmarks/Benchmarks.System.Reactive/Operators/Multiple/CombineLatestManyBenchmarks.cs b/Rx.NET/Source/benchmarks/Benchmarks.System.Reactive/Operators/Multiple/CombineLatestManyBenchmarks.cs
new file mode 100644
index 0000000000..620fdfcbcf
--- /dev/null
+++ b/Rx.NET/Source/benchmarks/Benchmarks.System.Reactive/Operators/Multiple/CombineLatestManyBenchmarks.cs
@@ -0,0 +1,47 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT License.
+// See the LICENSE file in the project root for more information.
+
+using System;
+using System.Collections.Generic;
+using System.Reactive.Linq;
+
+using BenchmarkDotNet.Attributes;
+using BenchmarkDotNet.Engines;
+
+using Benchmarks.System.Reactive.Infrastructure;
+
+namespace Benchmarks.System.Reactive.Operators.Multiple
+{
+ ///
+ /// S1: N-ary CombineLatest across a varying number of cold sources — the variadic
+ /// IList-selector overload, whose sink tracks a per-source "has latest" array (a distinct code path
+ /// from the specialised binary overload in ). Total emitted work is held
+ /// ~constant (each source emits 1,000,000 / Sources elements) so the sweep isolates the per-source
+ /// bookkeeping cost rather than raw element volume.
+ ///
+ [BenchmarkCategory("Multiple")]
+ public class CombineLatestManyBenchmarks
+ {
+ [Params(2, 4, 8, 16)]
+ public int Sources;
+
+ private readonly Consumer _consumer = new();
+ private IObservable[] _sources = default!;
+
+ [GlobalSetup]
+ public void Setup()
+ {
+ var per = 1_000_000 / Sources;
+ _sources = new IObservable[Sources];
+ for (var i = 0; i < Sources; i++)
+ {
+ _sources[i] = Observable.Range(i * per, per);
+ }
+ }
+
+ [Benchmark]
+ public void CombineLatest() =>
+ Observable.CombineLatest(_sources, static (IList values) => values.Count).SubscribeConsume(_consumer);
+ }
+}
diff --git a/Rx.NET/Source/benchmarks/Benchmarks.System.Reactive/Operators/Multiple/ConcatBenchmarks.cs b/Rx.NET/Source/benchmarks/Benchmarks.System.Reactive/Operators/Multiple/ConcatBenchmarks.cs
new file mode 100644
index 0000000000..e5f4659a82
--- /dev/null
+++ b/Rx.NET/Source/benchmarks/Benchmarks.System.Reactive/Operators/Multiple/ConcatBenchmarks.cs
@@ -0,0 +1,21 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT License.
+// See the LICENSE file in the project root for more information.
+
+using System.Reactive.Linq;
+
+using BenchmarkDotNet.Attributes;
+
+using Benchmarks.System.Reactive.Infrastructure;
+
+namespace Benchmarks.System.Reactive.Operators.Multiple
+{
+ /// S1: Concat relays two sources one after the other.
+ [BenchmarkCategory("Multiple")]
+ public class ConcatBenchmarks : OperatorBenchmarkBase
+ {
+ [Benchmark]
+ public void Concat() =>
+ Observable.Concat(Observable.Range(1, N), Observable.Range(1, N)).SubscribeConsume(Consumer);
+ }
+}
diff --git a/Rx.NET/Source/benchmarks/Benchmarks.System.Reactive/Operators/Multiple/InterleavedCombiningBenchmarks.cs b/Rx.NET/Source/benchmarks/Benchmarks.System.Reactive/Operators/Multiple/InterleavedCombiningBenchmarks.cs
new file mode 100644
index 0000000000..cb4fde4fbf
--- /dev/null
+++ b/Rx.NET/Source/benchmarks/Benchmarks.System.Reactive/Operators/Multiple/InterleavedCombiningBenchmarks.cs
@@ -0,0 +1,88 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT License.
+// See the LICENSE file in the project root for more information.
+
+using System;
+using System.Reactive.Concurrency;
+using System.Reactive.Linq;
+
+using BenchmarkDotNet.Attributes;
+
+using Benchmarks.System.Reactive.Infrastructure;
+
+namespace Benchmarks.System.Reactive.Operators.Multiple
+{
+ ///
+ /// The combining operators over two sources that genuinely alternate on one virtual clock (left at
+ /// step, 2·step, …; right offset by step/2), instead of the cold-Range versions elsewhere where one
+ /// source drains fully before the other emits. This exercises the real interleaving path — e.g.
+ /// CombineLatest actually re-combines on each side's change rather than overwriting an unused value.
+ ///
+ [BenchmarkCategory("Multiple")]
+ public class InterleavedCombiningBenchmarks : TemporalBenchmarkBase
+ {
+ private IObservable Left(IScheduler scheduler) =>
+ VirtualTimeSource.Timed(scheduler, N, TimeSpan.FromTicks(PeriodTicks));
+
+ private IObservable Right(IScheduler scheduler) =>
+ VirtualTimeSource.Timed(scheduler, N, TimeSpan.FromTicks(PeriodTicks), TimeSpan.FromTicks(PeriodTicks / 2));
+
+ [Benchmark]
+ public void Zip_Interleaved()
+ {
+ var scheduler = new HistoricalScheduler();
+ Observable.Zip(Left(scheduler), Right(scheduler), static (a, b) => a + b).SubscribeConsume(Consumer);
+ scheduler.Start();
+ }
+
+ [Benchmark]
+ public void CombineLatest_Interleaved()
+ {
+ var scheduler = new HistoricalScheduler();
+ Observable.CombineLatest(Left(scheduler), Right(scheduler), static (a, b) => a + b).SubscribeConsume(Consumer);
+ scheduler.Start();
+ }
+
+ [Benchmark]
+ public void WithLatestFrom_Interleaved()
+ {
+ var scheduler = new HistoricalScheduler();
+ Left(scheduler).WithLatestFrom(Right(scheduler), static (a, b) => a + b).SubscribeConsume(Consumer);
+ scheduler.Start();
+ }
+
+ [Benchmark]
+ public void Merge_Interleaved()
+ {
+ var scheduler = new HistoricalScheduler();
+ Observable.Merge(Left(scheduler), Right(scheduler)).SubscribeConsume(Consumer);
+ scheduler.Start();
+ }
+
+ [Benchmark]
+ public void Amb_Interleaved()
+ {
+ var scheduler = new HistoricalScheduler();
+ Left(scheduler).Amb(Right(scheduler)).SubscribeConsume(Consumer); // both live; right (earlier offset) wins
+ scheduler.Start();
+ }
+
+ [Benchmark]
+ public void Switch_Interleaved()
+ {
+ var scheduler = new HistoricalScheduler();
+ var n = N;
+ var period = TimeSpan.FromTicks(PeriodTicks);
+ var innerStep = TimeSpan.FromTicks(PeriodTicks / 2);
+
+ // Each inner spans ~2 outer periods, so a new inner arrives while the previous is still emitting →
+ // Switch cancels it mid-flight (its defining behaviour), unlike the synchronous cold version.
+ VirtualTimeSource.Timed(scheduler, n, period)
+ .Select(_ => VirtualTimeSource.Timed(scheduler, 4, innerStep))
+ .Switch()
+ .SubscribeConsume(Consumer);
+
+ scheduler.Start();
+ }
+ }
+}
diff --git a/Rx.NET/Source/benchmarks/Benchmarks.System.Reactive/Operators/Multiple/MergeBenchmarks.cs b/Rx.NET/Source/benchmarks/Benchmarks.System.Reactive/Operators/Multiple/MergeBenchmarks.cs
new file mode 100644
index 0000000000..7b73bb0c05
--- /dev/null
+++ b/Rx.NET/Source/benchmarks/Benchmarks.System.Reactive/Operators/Multiple/MergeBenchmarks.cs
@@ -0,0 +1,43 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT License.
+// See the LICENSE file in the project root for more information.
+
+using System;
+using System.Reactive.Linq;
+
+using BenchmarkDotNet.Attributes;
+using BenchmarkDotNet.Engines;
+
+using Benchmarks.System.Reactive.Infrastructure;
+
+namespace Benchmarks.System.Reactive.Operators.Multiple
+{
+ ///
+ /// Combining-category exemplar (S1): Merge across a varying number of cold sources. Total emitted
+ /// work is held ~constant (each source emits 1,000,000 / Sources elements) so the sweep isolates the
+ /// per-source subscription/bookkeeping cost.
+ ///
+ [BenchmarkCategory("Multiple")]
+ public class MergeBenchmarks
+ {
+ [Params(2, 10, 100, 1000)]
+ public int Sources;
+
+ private readonly Consumer _consumer = new();
+ private IObservable[] _sources = default!;
+
+ [GlobalSetup]
+ public void Setup()
+ {
+ var per = 1_000_000 / Sources;
+ _sources = new IObservable[Sources];
+ for (var i = 0; i < Sources; i++)
+ {
+ _sources[i] = Observable.Range(i * per, per);
+ }
+ }
+
+ [Benchmark]
+ public void Merge() => Observable.Merge(_sources).SubscribeConsume(_consumer);
+ }
+}
diff --git a/Rx.NET/Source/benchmarks/Benchmarks.System.Reactive/Operators/Multiple/MergeMaxConcurrencyBenchmarks.cs b/Rx.NET/Source/benchmarks/Benchmarks.System.Reactive/Operators/Multiple/MergeMaxConcurrencyBenchmarks.cs
new file mode 100644
index 0000000000..63ada5d64c
--- /dev/null
+++ b/Rx.NET/Source/benchmarks/Benchmarks.System.Reactive/Operators/Multiple/MergeMaxConcurrencyBenchmarks.cs
@@ -0,0 +1,30 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT License.
+// See the LICENSE file in the project root for more information.
+
+using System.Reactive.Linq;
+
+using BenchmarkDotNet.Attributes;
+
+using Benchmarks.System.Reactive.Infrastructure;
+
+namespace Benchmarks.System.Reactive.Operators.Multiple
+{
+ ///
+ /// S1: the Merge overload with a concurrency cap subscribes to at most 4 inner sequences at a time,
+ /// queueing the rest — a distinct code path from the unbounded array/nested Merge. Inner size precomputed in
+ /// [GlobalSetup] to avoid a per-invocation closure.
+ ///
+ [BenchmarkCategory("Multiple")]
+ public class MergeMaxConcurrencyBenchmarks : OperatorBenchmarkBase
+ {
+ private int _innerSize;
+
+ [GlobalSetup]
+ public void Setup() => _innerSize = 1_000_000 / N;
+
+ [Benchmark]
+ public void Merge_MaxConcurrency() =>
+ Observable.Merge(Observable.Range(1, N).Select(v => Observable.Range(v, _innerSize)), 4).SubscribeConsume(Consumer);
+ }
+}
diff --git a/Rx.NET/Source/benchmarks/Benchmarks.System.Reactive/Operators/Multiple/SkipUntilObservableBenchmarks.cs b/Rx.NET/Source/benchmarks/Benchmarks.System.Reactive/Operators/Multiple/SkipUntilObservableBenchmarks.cs
new file mode 100644
index 0000000000..c3128be4d0
--- /dev/null
+++ b/Rx.NET/Source/benchmarks/Benchmarks.System.Reactive/Operators/Multiple/SkipUntilObservableBenchmarks.cs
@@ -0,0 +1,37 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT License.
+// See the LICENSE file in the project root for more information.
+
+using System;
+using System.Reactive.Concurrency;
+using System.Reactive.Linq;
+
+using BenchmarkDotNet.Attributes;
+
+using Benchmarks.System.Reactive.Infrastructure;
+
+namespace Benchmarks.System.Reactive.Operators.Multiple
+{
+ ///
+ /// S3: the observable-triggered SkipUntil (skip until another sequence emits), complementing the
+ /// absolute-time overload under Operators/Time. The trigger fires at the stream midpoint under virtual time.
+ ///
+ [BenchmarkCategory("Multiple")]
+ public class SkipUntilObservableBenchmarks : TemporalBenchmarkBase
+ {
+ [Benchmark]
+ public void SkipUntil_Observable()
+ {
+ var scheduler = new HistoricalScheduler();
+ var n = N;
+ var period = TimeSpan.FromTicks(PeriodTicks);
+ var trigger = Observable.Timer(TimeSpan.FromTicks(PeriodTicks * (n / 2)), scheduler);
+
+ VirtualTimeSource.Timed(scheduler, n, period)
+ .SkipUntil(trigger)
+ .SubscribeConsume(Consumer);
+
+ scheduler.Start();
+ }
+ }
+}
diff --git a/Rx.NET/Source/benchmarks/Benchmarks.System.Reactive/Operators/Multiple/SwitchBenchmarks.cs b/Rx.NET/Source/benchmarks/Benchmarks.System.Reactive/Operators/Multiple/SwitchBenchmarks.cs
new file mode 100644
index 0000000000..1c36ee8e80
--- /dev/null
+++ b/Rx.NET/Source/benchmarks/Benchmarks.System.Reactive/Operators/Multiple/SwitchBenchmarks.cs
@@ -0,0 +1,30 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT License.
+// See the LICENSE file in the project root for more information.
+
+using System.Reactive.Linq;
+
+using BenchmarkDotNet.Attributes;
+
+using Benchmarks.System.Reactive.Infrastructure;
+
+namespace Benchmarks.System.Reactive.Operators.Multiple
+{
+ ///
+ /// S1: Switch flattens a stream of inner sequences, always following the most recent. Cross-mapped so
+ /// total emitted work stays ~constant across N (inner size precomputed to avoid a closure). Note the inners
+ /// here are synchronous; the mid-inner cancellation path is covered by InterleavedCombiningBenchmarks.
+ ///
+ [BenchmarkCategory("Multiple")]
+ public class SwitchBenchmarks : OperatorBenchmarkBase
+ {
+ private int _innerSize;
+
+ [GlobalSetup]
+ public void Setup() => _innerSize = 1_000_000 / N;
+
+ [Benchmark]
+ public void Switch() =>
+ Observable.Range(1, N).Select(v => Observable.Range(v, _innerSize)).Switch().SubscribeConsume(Consumer);
+ }
+}
diff --git a/Rx.NET/Source/benchmarks/Benchmarks.System.Reactive/Operators/Multiple/WithLatestFromBenchmarks.cs b/Rx.NET/Source/benchmarks/Benchmarks.System.Reactive/Operators/Multiple/WithLatestFromBenchmarks.cs
new file mode 100644
index 0000000000..a559364abc
--- /dev/null
+++ b/Rx.NET/Source/benchmarks/Benchmarks.System.Reactive/Operators/Multiple/WithLatestFromBenchmarks.cs
@@ -0,0 +1,21 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT License.
+// See the LICENSE file in the project root for more information.
+
+using System.Reactive.Linq;
+
+using BenchmarkDotNet.Attributes;
+
+using Benchmarks.System.Reactive.Infrastructure;
+
+namespace Benchmarks.System.Reactive.Operators.Multiple
+{
+ /// S1: WithLatestFrom combines each element of the first source with the latest of the second.
+ [BenchmarkCategory("Multiple")]
+ public class WithLatestFromBenchmarks : OperatorBenchmarkBase
+ {
+ [Benchmark]
+ public void WithLatestFrom() =>
+ Observable.Range(1, N).WithLatestFrom(Observable.Range(1, N), static (a, b) => a + b).SubscribeConsume(Consumer);
+ }
+}
diff --git a/Rx.NET/Source/benchmarks/Benchmarks.System.Reactive/Operators/Multiple/ZipBenchmarks.cs b/Rx.NET/Source/benchmarks/Benchmarks.System.Reactive/Operators/Multiple/ZipBenchmarks.cs
new file mode 100644
index 0000000000..5f91f22b4b
--- /dev/null
+++ b/Rx.NET/Source/benchmarks/Benchmarks.System.Reactive/Operators/Multiple/ZipBenchmarks.cs
@@ -0,0 +1,25 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT License.
+// See the LICENSE file in the project root for more information.
+
+using System.Reactive.Linq;
+
+using BenchmarkDotNet.Attributes;
+
+using Benchmarks.System.Reactive.Infrastructure;
+
+namespace Benchmarks.System.Reactive.Operators.Multiple
+{
+ ///
+ /// S1: Zip pairs elements positionally from two sources. (Reimplemented cleanly — the old benchmark
+ /// delegated to a unit test and measured its assertions.) The first source's elements queue until the
+ /// second produces its match, so this also exercises Zip's internal queueing.
+ ///
+ [BenchmarkCategory("Multiple")]
+ public class ZipBenchmarks : OperatorBenchmarkBase
+ {
+ [Benchmark]
+ public void Zip() =>
+ Observable.Zip(Observable.Range(1, N), Observable.Range(1, N), static (a, b) => a + b).SubscribeConsume(Consumer);
+ }
+}
diff --git a/Rx.NET/Source/benchmarks/Benchmarks.System.Reactive/Operators/Multiple/ZipManyBenchmarks.cs b/Rx.NET/Source/benchmarks/Benchmarks.System.Reactive/Operators/Multiple/ZipManyBenchmarks.cs
new file mode 100644
index 0000000000..4a23796aa1
--- /dev/null
+++ b/Rx.NET/Source/benchmarks/Benchmarks.System.Reactive/Operators/Multiple/ZipManyBenchmarks.cs
@@ -0,0 +1,47 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT License.
+// See the LICENSE file in the project root for more information.
+
+using System;
+using System.Collections.Generic;
+using System.Reactive.Linq;
+
+using BenchmarkDotNet.Attributes;
+using BenchmarkDotNet.Engines;
+
+using Benchmarks.System.Reactive.Infrastructure;
+
+namespace Benchmarks.System.Reactive.Operators.Multiple
+{
+ ///
+ /// S1: N-ary Zip across a varying number of cold sources — the variadic IList-selector overload,
+ /// whose sink keeps a per-source queue (a distinct code path from the specialised binary overload in
+ /// ). Each source emits the same count, so every position produces one combined
+ /// result; total emitted work is held ~constant (each source emits 1,000,000 / Sources elements) so the
+ /// sweep isolates the per-source queueing/bookkeeping cost.
+ ///
+ [BenchmarkCategory("Multiple")]
+ public class ZipManyBenchmarks
+ {
+ [Params(2, 4, 8, 16)]
+ public int Sources;
+
+ private readonly Consumer _consumer = new();
+ private IObservable[] _sources = default!;
+
+ [GlobalSetup]
+ public void Setup()
+ {
+ var per = 1_000_000 / Sources;
+ _sources = new IObservable[Sources];
+ for (var i = 0; i < Sources; i++)
+ {
+ _sources[i] = Observable.Range(i * per, per);
+ }
+ }
+
+ [Benchmark]
+ public void Zip() =>
+ Observable.Zip(_sources, static (IList values) => values.Count).SubscribeConsume(_consumer);
+ }
+}
diff --git a/Rx.NET/Source/benchmarks/Benchmarks.System.Reactive/Operators/Overhead/ComparisonBenchmarks.cs b/Rx.NET/Source/benchmarks/Benchmarks.System.Reactive/Operators/Overhead/ComparisonBenchmarks.cs
new file mode 100644
index 0000000000..d0075402a1
--- /dev/null
+++ b/Rx.NET/Source/benchmarks/Benchmarks.System.Reactive/Operators/Overhead/ComparisonBenchmarks.cs
@@ -0,0 +1,258 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT License.
+// See the LICENSE file in the project root for more information.
+
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Reactive.Concurrency;
+using System.Reactive.Disposables;
+using System.Reactive.Linq;
+using System.Reactive.Subjects;
+
+using BenchmarkDotNet.Attributes;
+
+using Benchmarks.System.Reactive.Infrastructure;
+
+namespace Benchmarks.System.Reactive.Operators.Overhead
+{
+ ///
+ /// The historic broad comparison grid (predates the per-operator suite), retained for baseline
+ /// continuity: a raw for loop and LINQ-to-Objects against a wide spread of Rx pipelines in
+ /// one table. For focused per-operator measurements prefer the dedicated classes; the cross-map
+ /// variants here (ConcatCrossMap/SelectManyCrossMap/MergeCrossMap) pin total
+ /// work at ~1M elements, so for those N reads as fan-out width, not element count.
+ ///
+ [BenchmarkCategory("Overhead")]
+ public class ComparisonBenchmarks : OperatorBenchmarkBase
+ {
+ [Benchmark(Baseline = true)]
+ public void ForLoopBaseLine()
+ {
+ var n = N;
+ for (var i = 0; i < n; i++)
+ {
+ Consumer.Consume(i);
+ }
+ }
+
+ [Benchmark]
+ public void EnumerableBaseLine()
+ {
+ foreach (var v in Enumerable.Range(1, N))
+ {
+ Consumer.Consume(v);
+ }
+ }
+
+ [Benchmark]
+ public void Return() => Observable.Return(1).SubscribeConsume(Consumer);
+
+ [Benchmark]
+ public void Range() => Observable.Range(1, N).SubscribeConsume(Consumer);
+
+ [Benchmark]
+ public void Select() =>
+ Observable.Range(1, N)
+ .Select(static v => v + 1)
+ .SubscribeConsume(Consumer);
+
+ [Benchmark]
+ public void SelectSelect() =>
+ Observable.Range(1, N)
+ .Select(static v => v + 1)
+ .Select(static v => v + 1)
+ .SubscribeConsume(Consumer);
+
+ [Benchmark]
+ public void Where() =>
+ Observable.Range(1, 2 * N)
+ .Where(static v => (v & 1) != 0)
+ .SubscribeConsume(Consumer);
+
+ [Benchmark]
+ public void WhereWhere() =>
+ Observable.Range(1, 4 * N)
+ .Where(static v => (v & 1) != 0)
+ .Where(static v => (v & 2) != 0)
+ .SubscribeConsume(Consumer);
+
+ [Benchmark]
+ public void Take() =>
+ Observable.Range(1, 2 * N)
+ .Take(N)
+ .SubscribeConsume(Consumer);
+
+ [Benchmark]
+ public void Skip() =>
+ Observable.Range(1, 2 * N)
+ .Skip(N)
+ .SubscribeConsume(Consumer);
+
+ [Benchmark]
+ public void TakeUntil() =>
+ Observable.Range(1, N)
+ .TakeUntil(Observable.Never())
+ .SubscribeConsume(Consumer);
+
+ [Benchmark]
+ public void ToObservable() =>
+ Enumerable.Range(1, N)
+ .ToObservable()
+ .SubscribeConsume(Consumer);
+
+ [Benchmark]
+ public void Concat()
+ {
+ var m = N - N / 2;
+
+ Observable.Concat(
+ Observable.Range(1, N),
+ Observable.Range(1, m))
+ .SubscribeConsume(Consumer);
+ }
+
+ [Benchmark]
+ public void ConcatCrossMap()
+ {
+ var m = 1000 * 1000 / N;
+
+ Observable.Concat(Observable.Range(1, N).Select(v => Observable.Range(v, m)))
+ .SubscribeConsume(Consumer);
+ }
+
+ [Benchmark]
+ public void SelectManyCrossMap()
+ {
+ var m = 1000 * 1000 / N;
+
+ Observable.Range(1, N).SelectMany(v => Observable.Range(v, m))
+ .SubscribeConsume(Consumer);
+ }
+
+ [Benchmark]
+ public void MergeCrossMap()
+ {
+ var m = 1000 * 1000 / N;
+
+ Observable.Merge(Observable.Range(1, N).Select(v => Observable.Range(v, m)))
+ .SubscribeConsume(Consumer);
+ }
+
+ [Benchmark]
+ public void AsyncSubjectPush()
+ {
+ var subj = new AsyncSubject();
+ subj.SubscribeConsume(Consumer);
+
+ var n = N;
+ for (var i = 0; i < n; i++)
+ {
+ subj.OnNext(i);
+ }
+
+ subj.OnCompleted();
+ }
+
+ [Benchmark]
+ public void SubjectPush()
+ {
+ var subj = new Subject();
+ subj.SubscribeConsume(Consumer);
+
+ var n = N;
+ for (var i = 0; i < n; i++)
+ {
+ subj.OnNext(i);
+ }
+
+ subj.OnCompleted();
+ }
+
+ [Benchmark]
+ public void AmbTwo() =>
+ Observable.Never().Amb(Observable.Range(1, N))
+ .SubscribeConsume(Consumer);
+
+ [Benchmark]
+ public void AmbThree() =>
+ Observable.Amb(Observable.Never(), Observable.Never(), Observable.Range(1, N))
+ .SubscribeConsume(Consumer);
+
+ [Benchmark]
+ public void Timeout() =>
+ Observable.Range(1, N)
+ .Timeout(TimeSpan.FromHours(1))
+ .SubscribeConsume(Consumer);
+
+#pragma warning disable CS0618 // Type or member is obsolete
+ [Benchmark]
+ public void First() => Consumer.Consume(Observable.Range(1, N).First());
+
+ [Benchmark]
+ public void Last() => Consumer.Consume(Observable.Range(1, N).Last());
+#pragma warning restore CS0618 // Type or member is obsolete
+
+ [Benchmark]
+ public void Buffer_Exact() =>
+ Observable.Range(1, 1000)
+ .Buffer(1)
+ .SubscribeConsume(Consumer);
+
+ [Benchmark]
+ public void Buffer_Skip() =>
+ Observable.Range(1, 1000)
+ .Buffer(1, 2)
+ .SubscribeConsume(Consumer);
+
+ [Benchmark]
+ public void Buffer_Overlap() =>
+ Observable.Range(1, 1000)
+ .Buffer(2, 1)
+ .SubscribeConsume(Consumer);
+
+ [Benchmark]
+ public void CurrentThreadSchedulerRepeated()
+ {
+ var n = N;
+ var scheduler = CurrentThreadScheduler.Instance;
+ for (var i = 0; i < n; i++)
+ {
+ scheduler.Schedule(i, (_, v) =>
+ {
+ Consumer.Consume(v);
+ return Disposable.Empty;
+ });
+ }
+ }
+
+ [Benchmark]
+ public void TakeLast() =>
+ Observable.Range(1, 2 * N).TakeLast(N)
+ .SubscribeConsume(Consumer);
+
+ [Benchmark]
+ public void Repeat() =>
+ Observable.Repeat(1, N)
+ .SubscribeConsume(Consumer);
+
+ [Benchmark]
+ public void ToList() =>
+ Observable.Repeat(1, N).ToList()
+ .SubscribeConsume(Consumer);
+
+ [Benchmark]
+ public void Generate() =>
+ Observable.Generate(0, s => s < N, static s => s + 1, static s => s)
+ .SubscribeConsume(Consumer);
+
+ [Benchmark]
+ public void Collect()
+ {
+ foreach (var v in Observable.Range(1, N).Collect(static () => new List(), static (a, b) => { a.Add(b); return a; }))
+ {
+ Consumer.Consume(v);
+ }
+ }
+ }
+}
diff --git a/Rx.NET/Source/benchmarks/Benchmarks.System.Reactive/Operators/Overhead/OverheadBenchmarks.cs b/Rx.NET/Source/benchmarks/Benchmarks.System.Reactive/Operators/Overhead/OverheadBenchmarks.cs
new file mode 100644
index 0000000000..48a4ed7866
--- /dev/null
+++ b/Rx.NET/Source/benchmarks/Benchmarks.System.Reactive/Operators/Overhead/OverheadBenchmarks.cs
@@ -0,0 +1,45 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT License.
+// See the LICENSE file in the project root for more information.
+
+using System.Linq;
+using System.Reactive.Linq;
+
+using BenchmarkDotNet.Attributes;
+
+using Benchmarks.System.Reactive.Infrastructure;
+
+namespace Benchmarks.System.Reactive.Operators.Overhead
+{
+ ///
+ /// The suite's reference point: the same "increment each element" work done as a raw for loop
+ /// (baseline), as LINQ-to-Objects, and as an Rx Select pipeline. The Ratio column then shows the
+ /// per-element cost of Rx relative to a hand loop and to IEnumerable — the "cost of Rx" figure the
+ /// per-operator absolute numbers can't give on their own.
+ ///
+ [BenchmarkCategory("Overhead")]
+ public class OverheadBenchmarks : OperatorBenchmarkBase
+ {
+ [Benchmark(Baseline = true)]
+ public void ForLoop()
+ {
+ var n = N;
+ for (var i = 1; i <= n; i++)
+ {
+ Consumer.Consume(i + 1);
+ }
+ }
+
+ [Benchmark]
+ public void Enumerable_Select()
+ {
+ foreach (var v in Enumerable.Range(1, N).Select(static v => v + 1))
+ {
+ Consumer.Consume(v);
+ }
+ }
+
+ [Benchmark]
+ public void Rx_Select() => Observable.Range(1, N).Select(static v => v + 1).SubscribeConsume(Consumer);
+ }
+}
diff --git a/Rx.NET/Source/benchmarks/Benchmarks.System.Reactive/Operators/Single/AppendPrependStartWithBenchmarks.cs b/Rx.NET/Source/benchmarks/Benchmarks.System.Reactive/Operators/Single/AppendPrependStartWithBenchmarks.cs
new file mode 100644
index 0000000000..f851849a4c
--- /dev/null
+++ b/Rx.NET/Source/benchmarks/Benchmarks.System.Reactive/Operators/Single/AppendPrependStartWithBenchmarks.cs
@@ -0,0 +1,26 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT License.
+// See the LICENSE file in the project root for more information.
+
+using System.Reactive.Linq;
+
+using BenchmarkDotNet.Attributes;
+
+using Benchmarks.System.Reactive.Infrastructure;
+
+namespace Benchmarks.System.Reactive.Operators.Single
+{
+ /// S1: Append / Prepend a single element, and StartWith a leading value.
+ [BenchmarkCategory("Single")]
+ public class AppendPrependStartWithBenchmarks : OperatorBenchmarkBase
+ {
+ [Benchmark]
+ public void Append() => Observable.Range(1, N).Append(0).SubscribeConsume(Consumer);
+
+ [Benchmark]
+ public void Prepend() => Observable.Range(1, N).Prepend(0).SubscribeConsume(Consumer);
+
+ [Benchmark]
+ public void StartWith() => Observable.Range(1, N).StartWith(0).SubscribeConsume(Consumer);
+ }
+}
diff --git a/Rx.NET/Source/benchmarks/Benchmarks.System.Reactive/Operators/Single/AsObservableBenchmarks.cs b/Rx.NET/Source/benchmarks/Benchmarks.System.Reactive/Operators/Single/AsObservableBenchmarks.cs
new file mode 100644
index 0000000000..bfdb04cef5
--- /dev/null
+++ b/Rx.NET/Source/benchmarks/Benchmarks.System.Reactive/Operators/Single/AsObservableBenchmarks.cs
@@ -0,0 +1,20 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT License.
+// See the LICENSE file in the project root for more information.
+
+using System.Reactive.Linq;
+
+using BenchmarkDotNet.Attributes;
+
+using Benchmarks.System.Reactive.Infrastructure;
+
+namespace Benchmarks.System.Reactive.Operators.Single
+{
+ /// S1: AsObservable hides the source's concrete type behind a thin wrapper.
+ [BenchmarkCategory("Single")]
+ public class AsObservableBenchmarks : OperatorBenchmarkBase
+ {
+ [Benchmark]
+ public void AsObservable() => Observable.Range(1, N).AsObservable().SubscribeConsume(Consumer);
+ }
+}
diff --git a/Rx.NET/Source/benchmarks/Benchmarks.System.Reactive/Operators/Single/BufferCountBenchmarks.cs b/Rx.NET/Source/benchmarks/Benchmarks.System.Reactive/Operators/Single/BufferCountBenchmarks.cs
new file mode 100644
index 0000000000..6b12302839
--- /dev/null
+++ b/Rx.NET/Source/benchmarks/Benchmarks.System.Reactive/Operators/Single/BufferCountBenchmarks.cs
@@ -0,0 +1,26 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT License.
+// See the LICENSE file in the project root for more information.
+
+using System.Reactive.Linq;
+
+using BenchmarkDotNet.Attributes;
+
+using Benchmarks.System.Reactive.Infrastructure;
+
+namespace Benchmarks.System.Reactive.Operators.Single
+{
+ ///
+ /// S1: count-based Buffer — a List per batch (an allocation candidate). The skip variant keeps
+ /// multiple in-flight buffers.
+ ///
+ [BenchmarkCategory("Single")]
+ public class BufferCountBenchmarks : OperatorBenchmarkBase
+ {
+ [Benchmark]
+ public void Buffer_Count() => Observable.Range(1, N).Buffer(16).SubscribeConsume(Consumer);
+
+ [Benchmark]
+ public void Buffer_Count_Skip() => Observable.Range(1, N).Buffer(16, 8).SubscribeConsume(Consumer);
+ }
+}
diff --git a/Rx.NET/Source/benchmarks/Benchmarks.System.Reactive/Operators/Single/DistinctUntilChangedBenchmarks.cs b/Rx.NET/Source/benchmarks/Benchmarks.System.Reactive/Operators/Single/DistinctUntilChangedBenchmarks.cs
new file mode 100644
index 0000000000..d82fd892b0
--- /dev/null
+++ b/Rx.NET/Source/benchmarks/Benchmarks.System.Reactive/Operators/Single/DistinctUntilChangedBenchmarks.cs
@@ -0,0 +1,20 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT License.
+// See the LICENSE file in the project root for more information.
+
+using System.Reactive.Linq;
+
+using BenchmarkDotNet.Attributes;
+
+using Benchmarks.System.Reactive.Infrastructure;
+
+namespace Benchmarks.System.Reactive.Operators.Single
+{
+ /// S1: DistinctUntilChanged compares adjacent elements; the ascending source keeps every element.
+ [BenchmarkCategory("Single")]
+ public class DistinctUntilChangedBenchmarks : OperatorBenchmarkBase
+ {
+ [Benchmark]
+ public void DistinctUntilChanged() => Observable.Range(1, N).DistinctUntilChanged().SubscribeConsume(Consumer);
+ }
+}
diff --git a/Rx.NET/Source/benchmarks/Benchmarks.System.Reactive/Operators/Single/DoFinallyBenchmarks.cs b/Rx.NET/Source/benchmarks/Benchmarks.System.Reactive/Operators/Single/DoFinallyBenchmarks.cs
new file mode 100644
index 0000000000..bafecd7acb
--- /dev/null
+++ b/Rx.NET/Source/benchmarks/Benchmarks.System.Reactive/Operators/Single/DoFinallyBenchmarks.cs
@@ -0,0 +1,32 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT License.
+// See the LICENSE file in the project root for more information.
+
+using System.Reactive.Linq;
+
+using BenchmarkDotNet.Attributes;
+
+using Benchmarks.System.Reactive.Infrastructure;
+
+namespace Benchmarks.System.Reactive.Operators.Single
+{
+ ///
+ /// Side-effect callbacks: Do (per-element / on-error) and Finally (on-termination). The
+ /// _Error variants drive a faulting source so the onError / error-termination callbacks fire.
+ ///
+ [BenchmarkCategory("Single")]
+ public class DoFinallyBenchmarks : OperatorBenchmarkBase
+ {
+ [Benchmark]
+ public void Do() => Observable.Range(1, N).Do(static _ => { }).SubscribeConsume(Consumer);
+
+ [Benchmark]
+ public void Finally() => Observable.Range(1, N).Finally(static () => { }).SubscribeConsume(Consumer);
+
+ [Benchmark]
+ public void Do_Error() => FaultingSource.Faulting(N).Do(static _ => { }, static _ => { }).SubscribeConsume(Consumer);
+
+ [Benchmark]
+ public void Finally_Error() => FaultingSource.Faulting(N).Finally(static () => { }).SubscribeConsume(Consumer);
+ }
+}
diff --git a/Rx.NET/Source/benchmarks/Benchmarks.System.Reactive/Operators/Single/IgnoreElementsBenchmarks.cs b/Rx.NET/Source/benchmarks/Benchmarks.System.Reactive/Operators/Single/IgnoreElementsBenchmarks.cs
new file mode 100644
index 0000000000..5aec0e07d1
--- /dev/null
+++ b/Rx.NET/Source/benchmarks/Benchmarks.System.Reactive/Operators/Single/IgnoreElementsBenchmarks.cs
@@ -0,0 +1,20 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT License.
+// See the LICENSE file in the project root for more information.
+
+using System.Reactive.Linq;
+
+using BenchmarkDotNet.Attributes;
+
+using Benchmarks.System.Reactive.Infrastructure;
+
+namespace Benchmarks.System.Reactive.Operators.Single
+{
+ /// S1: IgnoreElements drops every value and forwards only the terminal notification.
+ [BenchmarkCategory("Single")]
+ public class IgnoreElementsBenchmarks : OperatorBenchmarkBase
+ {
+ [Benchmark]
+ public void IgnoreElements() => Observable.Range(1, N).IgnoreElements().SubscribeConsume(Consumer);
+ }
+}
diff --git a/Rx.NET/Source/benchmarks/Benchmarks.System.Reactive/Operators/Single/MaterializeBenchmarks.cs b/Rx.NET/Source/benchmarks/Benchmarks.System.Reactive/Operators/Single/MaterializeBenchmarks.cs
new file mode 100644
index 0000000000..0a95393e46
--- /dev/null
+++ b/Rx.NET/Source/benchmarks/Benchmarks.System.Reactive/Operators/Single/MaterializeBenchmarks.cs
@@ -0,0 +1,31 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT License.
+// See the LICENSE file in the project root for more information.
+
+using System.Reactive.Linq;
+
+using BenchmarkDotNet.Attributes;
+
+using Benchmarks.System.Reactive.Infrastructure;
+
+namespace Benchmarks.System.Reactive.Operators.Single
+{
+ ///
+ /// Materialize wraps each notification in a reference-type Notification<T> — a per-element heap
+ /// allocation and a prime zero-allocation candidate. Dematerialize unwraps it. Materialize_Error
+ /// materializes a faulting source, so the OnError→Notification.OnError path is exercised too.
+ ///
+ [BenchmarkCategory("Single")]
+ public class MaterializeBenchmarks : OperatorBenchmarkBase
+ {
+ [Benchmark]
+ public void Materialize() => Observable.Range(1, N).Materialize().SubscribeConsume(Consumer);
+
+ [Benchmark]
+ public void MaterializeDematerialize() =>
+ Observable.Range(1, N).Materialize().Dematerialize().SubscribeConsume(Consumer);
+
+ [Benchmark]
+ public void Materialize_Error() => FaultingSource.Faulting(N).Materialize().SubscribeConsume(Consumer);
+ }
+}
diff --git a/Rx.NET/Source/benchmarks/Benchmarks.System.Reactive/Operators/Single/RepeatWhenRetryWhenBenchmarks.cs b/Rx.NET/Source/benchmarks/Benchmarks.System.Reactive/Operators/Single/RepeatWhenRetryWhenBenchmarks.cs
new file mode 100644
index 0000000000..d2d32955c9
--- /dev/null
+++ b/Rx.NET/Source/benchmarks/Benchmarks.System.Reactive/Operators/Single/RepeatWhenRetryWhenBenchmarks.cs
@@ -0,0 +1,31 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT License.
+// See the LICENSE file in the project root for more information.
+
+using System.Reactive.Linq;
+
+using BenchmarkDotNet.Attributes;
+
+using Benchmarks.System.Reactive.Infrastructure;
+
+namespace Benchmarks.System.Reactive.Operators.Single
+{
+ ///
+ /// Signal-driven resubscription. RepeatWhen re-runs on the handler's signal (here once → source runs
+ /// twice). RetryWhen (happy path) never errors, so the identity handler drains the source exactly once;
+ /// RetryWhen_Error drives a source that faults 3× before succeeding, with a handler that permits 3 retries.
+ ///
+ [BenchmarkCategory("Single")]
+ public class RepeatWhenRetryWhenBenchmarks : OperatorBenchmarkBase
+ {
+ [Benchmark]
+ public void RepeatWhen() => Observable.Range(1, N).RepeatWhen(static signals => signals.Take(1)).SubscribeConsume(Consumer);
+
+ [Benchmark]
+ public void RetryWhen() => Observable.Range(1, N).RetryWhen(static errors => errors).SubscribeConsume(Consumer);
+
+ [Benchmark]
+ public void RetryWhen_Error() =>
+ FaultingSource.FaultThenSucceed(N, 3).RetryWhen(static errors => errors.Take(3)).SubscribeConsume(Consumer);
+ }
+}
diff --git a/Rx.NET/Source/benchmarks/Benchmarks.System.Reactive/Operators/Single/ResetExceptionDispatchStateBenchmarks.cs b/Rx.NET/Source/benchmarks/Benchmarks.System.Reactive/Operators/Single/ResetExceptionDispatchStateBenchmarks.cs
new file mode 100644
index 0000000000..33d057502f
--- /dev/null
+++ b/Rx.NET/Source/benchmarks/Benchmarks.System.Reactive/Operators/Single/ResetExceptionDispatchStateBenchmarks.cs
@@ -0,0 +1,29 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT License.
+// See the LICENSE file in the project root for more information.
+
+using System.Reactive.Linq;
+
+using BenchmarkDotNet.Attributes;
+
+using Benchmarks.System.Reactive.Infrastructure;
+
+namespace Benchmarks.System.Reactive.Operators.Single
+{
+ ///
+ /// ResetExceptionDispatchState only acts on the OnError path (clearing accumulated dispatch state
+ /// so a re-thrown exception doesn't grow its stack trace). The plain method is a pass-through (wrapper cost);
+ /// _Error routes repeated faults through it across a Retry — the scenario it exists for.
+ ///
+ [BenchmarkCategory("Single")]
+ public class ResetExceptionDispatchStateBenchmarks : OperatorBenchmarkBase
+ {
+ [Benchmark]
+ public void ResetExceptionDispatchState() =>
+ Observable.Range(1, N).ResetExceptionDispatchState().SubscribeConsume(Consumer);
+
+ [Benchmark]
+ public void ResetExceptionDispatchState_Error() =>
+ FaultingSource.FaultThenSucceed(N, 3).ResetExceptionDispatchState().Retry().SubscribeConsume(Consumer);
+ }
+}
diff --git a/Rx.NET/Source/benchmarks/Benchmarks.System.Reactive/Operators/Single/RetryRepeatBenchmarks.cs b/Rx.NET/Source/benchmarks/Benchmarks.System.Reactive/Operators/Single/RetryRepeatBenchmarks.cs
new file mode 100644
index 0000000000..a2a6b2aab6
--- /dev/null
+++ b/Rx.NET/Source/benchmarks/Benchmarks.System.Reactive/Operators/Single/RetryRepeatBenchmarks.cs
@@ -0,0 +1,29 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT License.
+// See the LICENSE file in the project root for more information.
+
+using System.Reactive.Linq;
+
+using BenchmarkDotNet.Attributes;
+
+using Benchmarks.System.Reactive.Infrastructure;
+
+namespace Benchmarks.System.Reactive.Operators.Single
+{
+ ///
+ /// Resubscription. Repeat(count) re-runs the source. Retry (happy path) never errors so runs once;
+ /// Retry_Error drives a source that faults 3× before succeeding, so Retry actually resubscribes.
+ ///
+ [BenchmarkCategory("Single")]
+ public class RetryRepeatBenchmarks : OperatorBenchmarkBase
+ {
+ [Benchmark]
+ public void Retry() => Observable.Range(1, N).Retry().SubscribeConsume(Consumer);
+
+ [Benchmark]
+ public void Repeat() => Observable.Range(1, N).Repeat(2).SubscribeConsume(Consumer);
+
+ [Benchmark]
+ public void Retry_Error() => FaultingSource.FaultThenSucceed(N, 3).Retry().SubscribeConsume(Consumer);
+ }
+}
diff --git a/Rx.NET/Source/benchmarks/Benchmarks.System.Reactive/Operators/Single/ScanBenchmarks.cs b/Rx.NET/Source/benchmarks/Benchmarks.System.Reactive/Operators/Single/ScanBenchmarks.cs
new file mode 100644
index 0000000000..d6068aabdb
--- /dev/null
+++ b/Rx.NET/Source/benchmarks/Benchmarks.System.Reactive/Operators/Single/ScanBenchmarks.cs
@@ -0,0 +1,23 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT License.
+// See the LICENSE file in the project root for more information.
+
+using System.Reactive.Linq;
+
+using BenchmarkDotNet.Attributes;
+
+using Benchmarks.System.Reactive.Infrastructure;
+
+namespace Benchmarks.System.Reactive.Operators.Single
+{
+ ///
+ /// Single-sequence-category exemplar (S1): Scan — an O(1)-state running accumulate that emits per
+ /// element. A long accumulator avoids overflow at large N.
+ ///
+ [BenchmarkCategory("Single")]
+ public class ScanBenchmarks : OperatorBenchmarkBase
+ {
+ [Benchmark]
+ public void Scan() => Observable.Range(1, N).Scan(0L, static (acc, v) => acc + v).SubscribeConsume(Consumer);
+ }
+}
diff --git a/Rx.NET/Source/benchmarks/Benchmarks.System.Reactive/Operators/Single/TakeLastSkipLastBenchmarks.cs b/Rx.NET/Source/benchmarks/Benchmarks.System.Reactive/Operators/Single/TakeLastSkipLastBenchmarks.cs
new file mode 100644
index 0000000000..c51d9b9220
--- /dev/null
+++ b/Rx.NET/Source/benchmarks/Benchmarks.System.Reactive/Operators/Single/TakeLastSkipLastBenchmarks.cs
@@ -0,0 +1,29 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT License.
+// See the LICENSE file in the project root for more information.
+
+using System.Reactive.Linq;
+
+using BenchmarkDotNet.Attributes;
+
+using Benchmarks.System.Reactive.Infrastructure;
+
+namespace Benchmarks.System.Reactive.Operators.Single
+{
+ ///
+ /// S1: the count-based last-N family. Each keeps a fixed-capacity sliding Queue — a clean zero-allocation
+ /// candidate (the capacity is known up front, so a ring buffer / pooled array would eliminate the churn).
+ ///
+ [BenchmarkCategory("Single")]
+ public class TakeLastSkipLastBenchmarks : OperatorBenchmarkBase
+ {
+ [Benchmark]
+ public void TakeLast_Count() => Observable.Range(1, N).TakeLast(16).SubscribeConsume(Consumer);
+
+ [Benchmark]
+ public void SkipLast_Count() => Observable.Range(1, N).SkipLast(16).SubscribeConsume(Consumer);
+
+ [Benchmark]
+ public void TakeLastBuffer_Count() => Observable.Range(1, N).TakeLastBuffer(16).SubscribeConsume(Consumer);
+ }
+}
diff --git a/Rx.NET/Source/benchmarks/Benchmarks.System.Reactive/Operators/Single/WindowCountBenchmarks.cs b/Rx.NET/Source/benchmarks/Benchmarks.System.Reactive/Operators/Single/WindowCountBenchmarks.cs
new file mode 100644
index 0000000000..194506a230
--- /dev/null
+++ b/Rx.NET/Source/benchmarks/Benchmarks.System.Reactive/Operators/Single/WindowCountBenchmarks.cs
@@ -0,0 +1,21 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT License.
+// See the LICENSE file in the project root for more information.
+
+using System.Reactive.Linq;
+
+using BenchmarkDotNet.Attributes;
+
+using Benchmarks.System.Reactive.Infrastructure;
+
+namespace Benchmarks.System.Reactive.Operators.Single
+{
+ /// S1: count-based Window produces a sub-observable per batch; the inner windows are flattened.
+ [BenchmarkCategory("Single")]
+ public class WindowCountBenchmarks : OperatorBenchmarkBase
+ {
+ [Benchmark]
+ public void Window_Count() =>
+ Observable.Range(1, N).Window(16).SelectMany(static w => w).SubscribeConsume(Consumer);
+ }
+}
diff --git a/Rx.NET/Source/benchmarks/Benchmarks.System.Reactive/Operators/StandardSequence/BoxingBenchmarks.cs b/Rx.NET/Source/benchmarks/Benchmarks.System.Reactive/Operators/StandardSequence/BoxingBenchmarks.cs
new file mode 100644
index 0000000000..3288d935d8
--- /dev/null
+++ b/Rx.NET/Source/benchmarks/Benchmarks.System.Reactive/Operators/StandardSequence/BoxingBenchmarks.cs
@@ -0,0 +1,28 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT License.
+// See the LICENSE file in the project root for more information.
+
+using System.Reactive.Linq;
+
+using BenchmarkDotNet.Attributes;
+
+using Benchmarks.System.Reactive.Infrastructure;
+
+namespace Benchmarks.System.Reactive.Operators.StandardSequence
+{
+ ///
+ /// Isolates the cost of boxing: the same pipeline over value-type int (Unboxed, baseline) vs one
+ /// routed through IObservable<object> (Boxed — a box on the way in and an unbox on the way out
+ /// per element). The Ratio + Allocated columns quantify the per-element box/unbox tax directly.
+ ///
+ [BenchmarkCategory("StandardSequence")]
+ public class BoxingBenchmarks : OperatorBenchmarkBase
+ {
+ [Benchmark(Baseline = true)]
+ public void Unboxed() => Observable.Range(1, N).Select(static v => v + 1).SubscribeConsume(Consumer);
+
+ [Benchmark]
+ public void Boxed() =>
+ Observable.Range(1, N).Select(static v => (object)v).Select(static o => (int)o + 1).SubscribeConsume(Consumer);
+ }
+}
diff --git a/Rx.NET/Source/benchmarks/Benchmarks.System.Reactive/Operators/StandardSequence/CastOfTypeBenchmarks.cs b/Rx.NET/Source/benchmarks/Benchmarks.System.Reactive/Operators/StandardSequence/CastOfTypeBenchmarks.cs
new file mode 100644
index 0000000000..6986cca50a
--- /dev/null
+++ b/Rx.NET/Source/benchmarks/Benchmarks.System.Reactive/Operators/StandardSequence/CastOfTypeBenchmarks.cs
@@ -0,0 +1,26 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT License.
+// See the LICENSE file in the project root for more information.
+
+using System.Reactive.Linq;
+
+using BenchmarkDotNet.Attributes;
+
+using Benchmarks.System.Reactive.Infrastructure;
+
+namespace Benchmarks.System.Reactive.Operators.StandardSequence
+{
+ ///
+ /// S1: Cast and OfType over a boxed (object) source. The boxing in the source is
+ /// inherent to having an IObservable<object>; the benchmark measures the cast/type-test per element.
+ ///
+ [BenchmarkCategory("StandardSequence")]
+ public class CastOfTypeBenchmarks : OperatorBenchmarkBase
+ {
+ [Benchmark]
+ public void Cast() => Observable.Range(1, N).Select(static v => (object)v).Cast().SubscribeConsume(Consumer);
+
+ [Benchmark]
+ public void OfType() => Observable.Range(1, N).Select(static v => (object)v).OfType().SubscribeConsume(Consumer);
+ }
+}
diff --git a/Rx.NET/Source/benchmarks/Benchmarks.System.Reactive/Operators/StandardSequence/DefaultIfEmptyBenchmarks.cs b/Rx.NET/Source/benchmarks/Benchmarks.System.Reactive/Operators/StandardSequence/DefaultIfEmptyBenchmarks.cs
new file mode 100644
index 0000000000..21213f3f09
--- /dev/null
+++ b/Rx.NET/Source/benchmarks/Benchmarks.System.Reactive/Operators/StandardSequence/DefaultIfEmptyBenchmarks.cs
@@ -0,0 +1,20 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT License.
+// See the LICENSE file in the project root for more information.
+
+using System.Reactive.Linq;
+
+using BenchmarkDotNet.Attributes;
+
+using Benchmarks.System.Reactive.Infrastructure;
+
+namespace Benchmarks.System.Reactive.Operators.StandardSequence
+{
+ /// S1: DefaultIfEmpty passes the stream through (the non-empty source substitutes nothing).
+ [BenchmarkCategory("StandardSequence")]
+ public class DefaultIfEmptyBenchmarks : OperatorBenchmarkBase
+ {
+ [Benchmark]
+ public void DefaultIfEmpty() => Observable.Range(1, N).DefaultIfEmpty().SubscribeConsume(Consumer);
+ }
+}
diff --git a/Rx.NET/Source/benchmarks/Benchmarks.System.Reactive/Operators/StandardSequence/DistinctBenchmarks.cs b/Rx.NET/Source/benchmarks/Benchmarks.System.Reactive/Operators/StandardSequence/DistinctBenchmarks.cs
new file mode 100644
index 0000000000..41bd89cde5
--- /dev/null
+++ b/Rx.NET/Source/benchmarks/Benchmarks.System.Reactive/Operators/StandardSequence/DistinctBenchmarks.cs
@@ -0,0 +1,26 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT License.
+// See the LICENSE file in the project root for more information.
+
+using System.Reactive.Linq;
+
+using BenchmarkDotNet.Attributes;
+
+using Benchmarks.System.Reactive.Infrastructure;
+
+namespace Benchmarks.System.Reactive.Operators.StandardSequence
+{
+ ///
+ /// S1: Distinct over an all-unique stream, so its internal HashSet grows to N — a bounded
+ /// allocation candidate for the modernization spike.
+ ///
+ [BenchmarkCategory("StandardSequence")]
+ public class DistinctBenchmarks : OperatorBenchmarkBase
+ {
+ [Benchmark]
+ public void Distinct() => Observable.Range(1, N).Distinct().SubscribeConsume(Consumer);
+
+ [Benchmark]
+ public void Distinct_KeySelector() => Observable.Range(1, N).Distinct(static v => v % 1000).SubscribeConsume(Consumer);
+ }
+}
diff --git a/Rx.NET/Source/benchmarks/Benchmarks.System.Reactive/Operators/StandardSequence/GroupByBenchmarks.cs b/Rx.NET/Source/benchmarks/Benchmarks.System.Reactive/Operators/StandardSequence/GroupByBenchmarks.cs
new file mode 100644
index 0000000000..1ae57718d3
--- /dev/null
+++ b/Rx.NET/Source/benchmarks/Benchmarks.System.Reactive/Operators/StandardSequence/GroupByBenchmarks.cs
@@ -0,0 +1,24 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT License.
+// See the LICENSE file in the project root for more information.
+
+using System.Reactive.Linq;
+
+using BenchmarkDotNet.Attributes;
+
+using Benchmarks.System.Reactive.Infrastructure;
+
+namespace Benchmarks.System.Reactive.Operators.StandardSequence
+{
+ ///
+ /// S1: GroupBy partitions the stream into keyed sub-observables (allocating a group per key), then
+ /// the groups are flattened. Fixed at 8 keys so each group carries a meaningful share of the stream.
+ ///
+ [BenchmarkCategory("StandardSequence")]
+ public class GroupByBenchmarks : OperatorBenchmarkBase
+ {
+ [Benchmark]
+ public void GroupBy() =>
+ Observable.Range(1, N).GroupBy(static v => v % 8).SelectMany(static g => g).SubscribeConsume(Consumer);
+ }
+}
diff --git a/Rx.NET/Source/benchmarks/Benchmarks.System.Reactive/Operators/StandardSequence/GroupByUntilBenchmarks.cs b/Rx.NET/Source/benchmarks/Benchmarks.System.Reactive/Operators/StandardSequence/GroupByUntilBenchmarks.cs
new file mode 100644
index 0000000000..e1e6dc218e
--- /dev/null
+++ b/Rx.NET/Source/benchmarks/Benchmarks.System.Reactive/Operators/StandardSequence/GroupByUntilBenchmarks.cs
@@ -0,0 +1,27 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT License.
+// See the LICENSE file in the project root for more information.
+
+using System.Reactive.Linq;
+
+using BenchmarkDotNet.Attributes;
+
+using Benchmarks.System.Reactive.Infrastructure;
+
+namespace Benchmarks.System.Reactive.Operators.StandardSequence
+{
+ ///
+ /// S1: GroupByUntil partitions into keyed groups that each close when a per-group duration fires
+ /// (here after a few elements), so groups are repeatedly opened and closed. Groups are flattened.
+ ///
+ [BenchmarkCategory("StandardSequence")]
+ public class GroupByUntilBenchmarks : OperatorBenchmarkBase
+ {
+ [Benchmark]
+ public void GroupByUntil() =>
+ Observable.Range(1, N)
+ .GroupByUntil(static v => v % 8, static g => g.Skip(4))
+ .SelectMany(static g => g)
+ .SubscribeConsume(Consumer);
+ }
+}
diff --git a/Rx.NET/Source/benchmarks/Benchmarks.System.Reactive/Operators/StandardSequence/JoinGroupJoinBenchmarks.cs b/Rx.NET/Source/benchmarks/Benchmarks.System.Reactive/Operators/StandardSequence/JoinGroupJoinBenchmarks.cs
new file mode 100644
index 0000000000..59d325abf0
--- /dev/null
+++ b/Rx.NET/Source/benchmarks/Benchmarks.System.Reactive/Operators/StandardSequence/JoinGroupJoinBenchmarks.cs
@@ -0,0 +1,61 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT License.
+// See the LICENSE file in the project root for more information.
+
+using System;
+using System.Reactive.Concurrency;
+using System.Reactive.Linq;
+
+using BenchmarkDotNet.Attributes;
+
+using Benchmarks.System.Reactive.Infrastructure;
+
+namespace Benchmarks.System.Reactive.Operators.StandardSequence
+{
+ ///
+ /// The window-join operators, run under virtual time so the overlap is controlled (each element's window
+ /// spans ~2 arrivals → ~O(N) pairs, not the O(N²) that open-ended windows on cold sources would produce).
+ ///
+ [BenchmarkCategory("StandardSequence")]
+ public class JoinGroupJoinBenchmarks : TemporalBenchmarkBase
+ {
+ [Benchmark]
+ public void Join()
+ {
+ var scheduler = new HistoricalScheduler();
+ var n = N;
+ var period = TimeSpan.FromTicks(PeriodTicks);
+ var window = TimeSpan.FromTicks(PeriodTicks * 2);
+
+ VirtualTimeSource.Timed(scheduler, n, period)
+ .Join(
+ VirtualTimeSource.Timed(scheduler, n, period),
+ _ => Observable.Timer(window, scheduler),
+ _ => Observable.Timer(window, scheduler),
+ static (l, r) => l + r)
+ .SubscribeConsume(Consumer);
+
+ scheduler.Start();
+ }
+
+ [Benchmark]
+ public void GroupJoin()
+ {
+ var scheduler = new HistoricalScheduler();
+ var n = N;
+ var period = TimeSpan.FromTicks(PeriodTicks);
+ var window = TimeSpan.FromTicks(PeriodTicks * 2);
+
+ VirtualTimeSource.Timed(scheduler, n, period)
+ .GroupJoin(
+ VirtualTimeSource.Timed(scheduler, n, period),
+ _ => Observable.Timer(window, scheduler),
+ _ => Observable.Timer(window, scheduler),
+ static (l, rights) => rights)
+ .SelectMany(static rights => rights)
+ .SubscribeConsume(Consumer);
+
+ scheduler.Start();
+ }
+ }
+}
diff --git a/Rx.NET/Source/benchmarks/Benchmarks.System.Reactive/Operators/StandardSequence/ReferenceTypeElementBenchmarks.cs b/Rx.NET/Source/benchmarks/Benchmarks.System.Reactive/Operators/StandardSequence/ReferenceTypeElementBenchmarks.cs
new file mode 100644
index 0000000000..7497bae085
--- /dev/null
+++ b/Rx.NET/Source/benchmarks/Benchmarks.System.Reactive/Operators/StandardSequence/ReferenceTypeElementBenchmarks.cs
@@ -0,0 +1,40 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT License.
+// See the LICENSE file in the project root for more information.
+
+using System.Linq;
+using System.Reactive.Linq;
+
+using BenchmarkDotNet.Attributes;
+
+using Benchmarks.System.Reactive.Infrastructure;
+
+namespace Benchmarks.System.Reactive.Operators.StandardSequence
+{
+ ///
+ /// The suite is otherwise all int pipelines; this runs the GC-sensitive operators over a
+ /// reference-type (string) element, so behaviour with GC-tracked payloads — the internal
+ /// HashSet/List/Queue holding object references, equality via EqualityComparer — is
+ /// measured rather than the value-type fast paths.
+ ///
+ [BenchmarkCategory("StandardSequence")]
+ public class ReferenceTypeElementBenchmarks : OperatorBenchmarkBase
+ {
+ private string[] _strings = default!;
+
+ [GlobalSetup]
+ public void Setup() => _strings = Enumerable.Range(1, N).Select(static i => (i % 1000).ToString()).ToArray();
+
+ [Benchmark]
+ public void Distinct() => _strings.ToObservable().Distinct().SubscribeConsume(Consumer);
+
+ [Benchmark]
+ public void DistinctUntilChanged() => _strings.ToObservable().DistinctUntilChanged().SubscribeConsume(Consumer);
+
+ [Benchmark]
+ public void GroupBy() => _strings.ToObservable().GroupBy(static s => s.Length).SelectMany(static g => g).SubscribeConsume(Consumer);
+
+ [Benchmark]
+ public void Buffer() => _strings.ToObservable().Buffer(16).SubscribeConsume(Consumer);
+ }
+}
diff --git a/Rx.NET/Source/benchmarks/Benchmarks.System.Reactive/Operators/StandardSequence/SelectBenchmarks.cs b/Rx.NET/Source/benchmarks/Benchmarks.System.Reactive/Operators/StandardSequence/SelectBenchmarks.cs
new file mode 100644
index 0000000000..f205142098
--- /dev/null
+++ b/Rx.NET/Source/benchmarks/Benchmarks.System.Reactive/Operators/StandardSequence/SelectBenchmarks.cs
@@ -0,0 +1,25 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT License.
+// See the LICENSE file in the project root for more information.
+
+using System.Reactive.Linq;
+
+using BenchmarkDotNet.Attributes;
+
+using Benchmarks.System.Reactive.Infrastructure;
+
+namespace Benchmarks.System.Reactive.Operators.StandardSequence
+{
+ ///
+ /// S1 (throughput) exemplar: pushes N elements synchronously through Select and consumes
+ /// them. Sweeps N from 1 to 1,000,000 so subscription cost (small N) and per-element cost
+ /// (large N) are both visible, with allocations reported by the shared MemoryDiagnoser.
+ ///
+ [BenchmarkCategory("StandardSequence")]
+ public class SelectBenchmarks : OperatorBenchmarkBase
+ {
+ [Benchmark]
+ public void Select() =>
+ Observable.Range(1, N).Select(v => v + 1).SubscribeConsume(Consumer);
+ }
+}
diff --git a/Rx.NET/Source/benchmarks/Benchmarks.System.Reactive/Operators/StandardSequence/SelectManyBenchmarks.cs b/Rx.NET/Source/benchmarks/Benchmarks.System.Reactive/Operators/StandardSequence/SelectManyBenchmarks.cs
new file mode 100644
index 0000000000..76dc7a0608
--- /dev/null
+++ b/Rx.NET/Source/benchmarks/Benchmarks.System.Reactive/Operators/StandardSequence/SelectManyBenchmarks.cs
@@ -0,0 +1,31 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT License.
+// See the LICENSE file in the project root for more information.
+
+using System.Reactive.Linq;
+
+using BenchmarkDotNet.Attributes;
+
+using Benchmarks.System.Reactive.Infrastructure;
+
+namespace Benchmarks.System.Reactive.Operators.StandardSequence
+{
+ ///
+ /// S1 fan-out: SelectMany projects each element to an inner sequence and flattens. The cross-map inner
+ /// size (1,000,000 / N) holds total emitted work ~constant, isolating the per-inner-subscription cost.
+ /// The inner size is precomputed in [GlobalSetup] so the selector captures a field (no per-invocation
+ /// display-class closure).
+ ///
+ [BenchmarkCategory("StandardSequence")]
+ public class SelectManyBenchmarks : OperatorBenchmarkBase
+ {
+ private int _innerSize;
+
+ [GlobalSetup]
+ public void Setup() => _innerSize = 1_000_000 / N;
+
+ [Benchmark]
+ public void SelectMany() =>
+ Observable.Range(1, N).SelectMany(v => Observable.Range(v, _innerSize)).SubscribeConsume(Consumer);
+ }
+}
diff --git a/Rx.NET/Source/benchmarks/Benchmarks.System.Reactive/Operators/StandardSequence/TakeSkipBenchmarks.cs b/Rx.NET/Source/benchmarks/Benchmarks.System.Reactive/Operators/StandardSequence/TakeSkipBenchmarks.cs
new file mode 100644
index 0000000000..29621a3d96
--- /dev/null
+++ b/Rx.NET/Source/benchmarks/Benchmarks.System.Reactive/Operators/StandardSequence/TakeSkipBenchmarks.cs
@@ -0,0 +1,37 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT License.
+// See the LICENSE file in the project root for more information.
+
+using System.Reactive.Linq;
+
+using BenchmarkDotNet.Attributes;
+
+using Benchmarks.System.Reactive.Infrastructure;
+
+namespace Benchmarks.System.Reactive.Operators.StandardSequence
+{
+ /// S1: the count/predicate-based Take/Skip family (the time-based variants live under Operators/Time).
+ [BenchmarkCategory("StandardSequence")]
+ public class TakeSkipBenchmarks : OperatorBenchmarkBase
+ {
+ [Benchmark]
+ public void Take() => Observable.Range(1, 2 * N).Take(N).SubscribeConsume(Consumer);
+
+ [Benchmark]
+ public void Skip() => Observable.Range(1, 2 * N).Skip(N).SubscribeConsume(Consumer);
+
+ [Benchmark]
+ public void TakeWhile()
+ {
+ var n = N;
+ Observable.Range(1, 2 * N).TakeWhile(v => v <= n).SubscribeConsume(Consumer);
+ }
+
+ [Benchmark]
+ public void SkipWhile()
+ {
+ var n = N;
+ Observable.Range(1, 2 * N).SkipWhile(v => v <= n).SubscribeConsume(Consumer);
+ }
+ }
+}
diff --git a/Rx.NET/Source/benchmarks/Benchmarks.System.Reactive/Operators/StandardSequence/WhereBenchmarks.cs b/Rx.NET/Source/benchmarks/Benchmarks.System.Reactive/Operators/StandardSequence/WhereBenchmarks.cs
new file mode 100644
index 0000000000..a151ef79b7
--- /dev/null
+++ b/Rx.NET/Source/benchmarks/Benchmarks.System.Reactive/Operators/StandardSequence/WhereBenchmarks.cs
@@ -0,0 +1,20 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT License.
+// See the LICENSE file in the project root for more information.
+
+using System.Reactive.Linq;
+
+using BenchmarkDotNet.Attributes;
+
+using Benchmarks.System.Reactive.Infrastructure;
+
+namespace Benchmarks.System.Reactive.Operators.StandardSequence
+{
+ /// S1: Where filters a stream; the predicate here keeps roughly half the elements.
+ [BenchmarkCategory("StandardSequence")]
+ public class WhereBenchmarks : OperatorBenchmarkBase
+ {
+ [Benchmark]
+ public void Where() => Observable.Range(1, N).Where(static v => (v & 1) == 0).SubscribeConsume(Consumer);
+ }
+}
diff --git a/Rx.NET/Source/benchmarks/Benchmarks.System.Reactive/Operators/Time/BufferTimeBenchmarks.cs b/Rx.NET/Source/benchmarks/Benchmarks.System.Reactive/Operators/Time/BufferTimeBenchmarks.cs
new file mode 100644
index 0000000000..09de1e72ab
--- /dev/null
+++ b/Rx.NET/Source/benchmarks/Benchmarks.System.Reactive/Operators/Time/BufferTimeBenchmarks.cs
@@ -0,0 +1,36 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT License.
+// See the LICENSE file in the project root for more information.
+
+using System.Reactive.Linq;
+
+using BenchmarkDotNet.Attributes;
+
+using Benchmarks.System.Reactive.Infrastructure;
+
+namespace Benchmarks.System.Reactive.Operators.Time
+{
+ ///
+ /// S3: time-based Buffer collects elements into an IList per window. A prime zero-allocation
+ /// candidate for the modernization spike (a List per window; the emitted list escapes to the
+ /// consumer). The Density knob controls how many elements land in each buffer.
+ ///
+ [BenchmarkCategory("Time")]
+ public class BufferTimeBenchmarks : RateWindowBenchmarkBase
+ {
+ [Benchmark]
+ public void Buffer_Time()
+ {
+ var scheduler = new PeriodicVirtualScheduler(); // Buffer(TimeSpan) runs on the real periodic path
+ var n = N;
+ var window = Window;
+ var period = Period;
+
+ VirtualTimeSource.Timed(scheduler, n, period)
+ .Buffer(window, scheduler)
+ .SubscribeConsume(Consumer); // consumes each IList batch
+
+ scheduler.Start();
+ }
+ }
+}
diff --git a/Rx.NET/Source/benchmarks/Benchmarks.System.Reactive/Operators/Time/BufferWindowBoundaryBenchmarks.cs b/Rx.NET/Source/benchmarks/Benchmarks.System.Reactive/Operators/Time/BufferWindowBoundaryBenchmarks.cs
new file mode 100644
index 0000000000..16a1aa4651
--- /dev/null
+++ b/Rx.NET/Source/benchmarks/Benchmarks.System.Reactive/Operators/Time/BufferWindowBoundaryBenchmarks.cs
@@ -0,0 +1,53 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT License.
+// See the LICENSE file in the project root for more information.
+
+using System;
+using System.Reactive.Concurrency;
+using System.Reactive.Linq;
+
+using BenchmarkDotNet.Attributes;
+
+using Benchmarks.System.Reactive.Infrastructure;
+
+namespace Benchmarks.System.Reactive.Operators.Time
+{
+ ///
+ /// S3: the observable-boundary Buffer / Window (boundaries close each batch), complementing the
+ /// count and time shapes. Boundaries fire every 16 periods under virtual time.
+ ///
+ [BenchmarkCategory("Time")]
+ public class BufferWindowBoundaryBenchmarks : TemporalBenchmarkBase
+ {
+ [Benchmark]
+ public void Buffer_Boundary()
+ {
+ var scheduler = new HistoricalScheduler();
+ var n = N;
+ var period = TimeSpan.FromTicks(PeriodTicks);
+ var boundaries = Observable.Interval(TimeSpan.FromTicks(PeriodTicks * 16), scheduler);
+
+ VirtualTimeSource.Timed(scheduler, n, period)
+ .Buffer(boundaries)
+ .SubscribeConsume(Consumer);
+
+ scheduler.Start();
+ }
+
+ [Benchmark]
+ public void Window_Boundary()
+ {
+ var scheduler = new HistoricalScheduler();
+ var n = N;
+ var period = TimeSpan.FromTicks(PeriodTicks);
+ var boundaries = Observable.Interval(TimeSpan.FromTicks(PeriodTicks * 16), scheduler);
+
+ VirtualTimeSource.Timed(scheduler, n, period)
+ .Window(boundaries)
+ .SelectMany(static w => w)
+ .SubscribeConsume(Consumer);
+
+ scheduler.Start();
+ }
+ }
+}
diff --git a/Rx.NET/Source/benchmarks/Benchmarks.System.Reactive/Operators/Time/DelayBenchmarks.cs b/Rx.NET/Source/benchmarks/Benchmarks.System.Reactive/Operators/Time/DelayBenchmarks.cs
new file mode 100644
index 0000000000..d9505019ef
--- /dev/null
+++ b/Rx.NET/Source/benchmarks/Benchmarks.System.Reactive/Operators/Time/DelayBenchmarks.cs
@@ -0,0 +1,52 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT License.
+// See the LICENSE file in the project root for more information.
+
+using System;
+using System.Reactive.Concurrency;
+using System.Reactive.Linq;
+
+using BenchmarkDotNet.Attributes;
+
+using Benchmarks.System.Reactive.Infrastructure;
+
+namespace Benchmarks.System.Reactive.Operators.Time
+{
+ ///
+ /// S3: Delay time-shifts every element (density-independent, so N + a fixed delay). DelaySubscription
+ /// shifts only the subscription, then relays elements untouched.
+ ///
+ [BenchmarkCategory("Time")]
+ public class DelayBenchmarks : TemporalBenchmarkBase
+ {
+ [Benchmark]
+ public void Delay()
+ {
+ var scheduler = new HistoricalScheduler();
+ var n = N;
+ var period = TimeSpan.FromTicks(PeriodTicks);
+ var delay = TimeSpan.FromTicks(PeriodTicks * 4);
+
+ VirtualTimeSource.Timed(scheduler, n, period)
+ .Delay(delay, scheduler)
+ .SubscribeConsume(Consumer);
+
+ scheduler.Start();
+ }
+
+ [Benchmark]
+ public void DelaySubscription()
+ {
+ var scheduler = new HistoricalScheduler();
+ var n = N;
+ var period = TimeSpan.FromTicks(PeriodTicks);
+ var delay = TimeSpan.FromTicks(PeriodTicks * 4);
+
+ VirtualTimeSource.Timed(scheduler, n, period)
+ .DelaySubscription(delay, scheduler)
+ .SubscribeConsume(Consumer);
+
+ scheduler.Start();
+ }
+ }
+}
diff --git a/Rx.NET/Source/benchmarks/Benchmarks.System.Reactive/Operators/Time/SampleBenchmarks.cs b/Rx.NET/Source/benchmarks/Benchmarks.System.Reactive/Operators/Time/SampleBenchmarks.cs
new file mode 100644
index 0000000000..be3f8956fb
--- /dev/null
+++ b/Rx.NET/Source/benchmarks/Benchmarks.System.Reactive/Operators/Time/SampleBenchmarks.cs
@@ -0,0 +1,35 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT License.
+// See the LICENSE file in the project root for more information.
+
+using System.Reactive.Linq;
+
+using BenchmarkDotNet.Attributes;
+
+using Benchmarks.System.Reactive.Infrastructure;
+
+namespace Benchmarks.System.Reactive.Operators.Time
+{
+ ///
+ /// S3: Sample emits the most recent element on each tick of its sampling window. The Density
+ /// knob varies how many source elements fall into each sampling interval.
+ ///
+ [BenchmarkCategory("Time")]
+ public class SampleBenchmarks : RateWindowBenchmarkBase
+ {
+ [Benchmark]
+ public void Sample()
+ {
+ var scheduler = new PeriodicVirtualScheduler(); // Sample(TimeSpan) runs on the real periodic path
+ var n = N;
+ var window = Window;
+ var period = Period;
+
+ VirtualTimeSource.Timed(scheduler, n, period)
+ .Sample(window, scheduler)
+ .SubscribeConsume(Consumer);
+
+ scheduler.Start();
+ }
+ }
+}
diff --git a/Rx.NET/Source/benchmarks/Benchmarks.System.Reactive/Operators/Time/SkipTakeTimeBenchmarks.cs b/Rx.NET/Source/benchmarks/Benchmarks.System.Reactive/Operators/Time/SkipTakeTimeBenchmarks.cs
new file mode 100644
index 0000000000..024a35b1a3
--- /dev/null
+++ b/Rx.NET/Source/benchmarks/Benchmarks.System.Reactive/Operators/Time/SkipTakeTimeBenchmarks.cs
@@ -0,0 +1,86 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT License.
+// See the LICENSE file in the project root for more information.
+
+using System;
+using System.Reactive.Concurrency;
+using System.Reactive.Linq;
+
+using BenchmarkDotNet.Attributes;
+
+using Benchmarks.System.Reactive.Infrastructure;
+
+namespace Benchmarks.System.Reactive.Operators.Time
+{
+ ///
+ /// S3: the time-based Skip/Take family. The boundary is the midpoint of the stream, so each operator does
+ /// meaningful work over time. SkipLast/TakeLast keep a time-bounded queue — allocation
+ /// candidates for the modernization spike. Stays on plain : these operators
+ /// only use one-shot Schedule, never ISchedulerPeriodic.
+ ///
+ [BenchmarkCategory("Time")]
+ public class SkipTakeTimeBenchmarks : TemporalBenchmarkBase
+ {
+ private TimeSpan Midpoint => TimeSpan.FromTicks(PeriodTicks * (N / 2));
+
+ [Benchmark]
+ public void Skip_Time()
+ {
+ var scheduler = new HistoricalScheduler();
+ var n = N;
+ var period = TimeSpan.FromTicks(PeriodTicks);
+ var duration = Midpoint;
+
+ VirtualTimeSource.Timed(scheduler, n, period)
+ .Skip(duration, scheduler)
+ .SubscribeConsume(Consumer);
+
+ scheduler.Start();
+ }
+
+ [Benchmark]
+ public void Take_Time()
+ {
+ var scheduler = new HistoricalScheduler();
+ var n = N;
+ var period = TimeSpan.FromTicks(PeriodTicks);
+ var duration = Midpoint;
+
+ VirtualTimeSource.Timed(scheduler, n, period)
+ .Take(duration, scheduler)
+ .SubscribeConsume(Consumer);
+
+ scheduler.Start();
+ }
+
+ [Benchmark]
+ public void SkipLast_Time()
+ {
+ var scheduler = new HistoricalScheduler();
+ var n = N;
+ var period = TimeSpan.FromTicks(PeriodTicks);
+ var duration = Midpoint;
+
+ VirtualTimeSource.Timed(scheduler, n, period)
+ .SkipLast(duration, scheduler)
+ .SubscribeConsume(Consumer);
+
+ scheduler.Start();
+ }
+
+ [Benchmark]
+ public void TakeLast_Time()
+ {
+ var scheduler = new HistoricalScheduler();
+ var n = N;
+ var period = TimeSpan.FromTicks(PeriodTicks);
+ var duration = Midpoint;
+
+ VirtualTimeSource.Timed(scheduler, n, period)
+ .TakeLast(duration, scheduler)
+ .SubscribeConsume(Consumer);
+
+ scheduler.Start();
+ }
+ }
+}
diff --git a/Rx.NET/Source/benchmarks/Benchmarks.System.Reactive/Operators/Time/SkipUntilTakeUntilBenchmarks.cs b/Rx.NET/Source/benchmarks/Benchmarks.System.Reactive/Operators/Time/SkipUntilTakeUntilBenchmarks.cs
new file mode 100644
index 0000000000..d855193506
--- /dev/null
+++ b/Rx.NET/Source/benchmarks/Benchmarks.System.Reactive/Operators/Time/SkipUntilTakeUntilBenchmarks.cs
@@ -0,0 +1,55 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT License.
+// See the LICENSE file in the project root for more information.
+
+using System;
+using System.Reactive.Concurrency;
+using System.Reactive.Linq;
+
+using BenchmarkDotNet.Attributes;
+
+using Benchmarks.System.Reactive.Infrastructure;
+
+namespace Benchmarks.System.Reactive.Operators.Time
+{
+ ///
+ /// S3: the absolute-time SkipUntil / TakeUntil (DateTimeOffset) overloads. The boundary is
+ /// the midpoint of the stream. The virtual is keyed on DateTimeOffset,
+ /// so the boundary is expressed relative to its MinValue start clock.
+ ///
+ [BenchmarkCategory("Time")]
+ public class SkipUntilTakeUntilBenchmarks : TemporalBenchmarkBase
+ {
+ private DateTimeOffset Midpoint => DateTimeOffset.MinValue + TimeSpan.FromTicks(PeriodTicks * (N / 2));
+
+ [Benchmark]
+ public void SkipUntil_Time()
+ {
+ var scheduler = new HistoricalScheduler();
+ var n = N;
+ var period = TimeSpan.FromTicks(PeriodTicks);
+ var boundary = Midpoint;
+
+ VirtualTimeSource.Timed(scheduler, n, period)
+ .SkipUntil(boundary, scheduler)
+ .SubscribeConsume(Consumer);
+
+ scheduler.Start();
+ }
+
+ [Benchmark]
+ public void TakeUntil_Time()
+ {
+ var scheduler = new HistoricalScheduler();
+ var n = N;
+ var period = TimeSpan.FromTicks(PeriodTicks);
+ var boundary = Midpoint;
+
+ VirtualTimeSource.Timed(scheduler, n, period)
+ .TakeUntil(boundary, scheduler)
+ .SubscribeConsume(Consumer);
+
+ scheduler.Start();
+ }
+ }
+}
diff --git a/Rx.NET/Source/benchmarks/Benchmarks.System.Reactive/Operators/Time/ThrottleBenchmarks.cs b/Rx.NET/Source/benchmarks/Benchmarks.System.Reactive/Operators/Time/ThrottleBenchmarks.cs
new file mode 100644
index 0000000000..65872da460
--- /dev/null
+++ b/Rx.NET/Source/benchmarks/Benchmarks.System.Reactive/Operators/Time/ThrottleBenchmarks.cs
@@ -0,0 +1,39 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT License.
+// See the LICENSE file in the project root for more information.
+
+using System.Reactive.Concurrency;
+using System.Reactive.Linq;
+
+using BenchmarkDotNet.Attributes;
+
+using Benchmarks.System.Reactive.Infrastructure;
+
+namespace Benchmarks.System.Reactive.Operators.Time
+{
+ ///
+ /// S3 (temporal / "data over time") flagship benchmark. N elements are emitted across a virtual
+ /// timeline (spaced PeriodTicks apart) and run to completion under a ;
+ /// Start() drains the whole timeline synchronously, so even a huge Dense window costs
+ /// microseconds of wall-clock. The Density knob varies the window-vs-arrival relationship that
+ /// drives Throttle's hot path (dense arrivals → constant cancel+reschedule).
+ ///
+ [BenchmarkCategory("Time")]
+ public class ThrottleBenchmarks : RateWindowBenchmarkBase
+ {
+ [Benchmark]
+ public void Throttle()
+ {
+ var scheduler = new HistoricalScheduler(); // fresh per iteration (Clock/queue are instance state)
+ var n = N; // hoist params into locals → no field-capture closures
+ var window = Window;
+ var period = Period;
+
+ VirtualTimeSource.Timed(scheduler, n, period)
+ .Throttle(window, scheduler) // MUST pass the virtual scheduler, or it waits in real time
+ .SubscribeConsume(Consumer); // Consumer defeats DCE without allocating a closure
+
+ scheduler.Start(); // drains the whole timeline synchronously
+ }
+ }
+}
diff --git a/Rx.NET/Source/benchmarks/Benchmarks.System.Reactive/Operators/Time/TimeOverloadGapsBenchmarks.cs b/Rx.NET/Source/benchmarks/Benchmarks.System.Reactive/Operators/Time/TimeOverloadGapsBenchmarks.cs
new file mode 100644
index 0000000000..fbd3d03716
--- /dev/null
+++ b/Rx.NET/Source/benchmarks/Benchmarks.System.Reactive/Operators/Time/TimeOverloadGapsBenchmarks.cs
@@ -0,0 +1,84 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT License.
+// See the LICENSE file in the project root for more information.
+
+using System;
+using System.Reactive.Linq;
+
+using BenchmarkDotNet.Attributes;
+
+using Benchmarks.System.Reactive.Infrastructure;
+
+namespace Benchmarks.System.Reactive.Operators.Time
+{
+ ///
+ /// S3: the remaining temporal overload shapes — Sample by observable sampler, Delay and
+ /// Timeout by per-element selector, and the timed TakeLastBuffer — complementing the primary
+ /// TimeSpan overloads elsewhere in Operators/Time.
+ ///
+ [BenchmarkCategory("Time")]
+ public class TimeOverloadGapsBenchmarks : TemporalBenchmarkBase
+ {
+ [Benchmark]
+ public void Sample_Observable()
+ {
+ // The sampler is Observable.Interval, which hits SchedulePeriodic — use the periodic scheduler.
+ var scheduler = new PeriodicVirtualScheduler();
+ var n = N;
+ var period = TimeSpan.FromTicks(PeriodTicks);
+ var sampler = Observable.Interval(TimeSpan.FromTicks(PeriodTicks * 4), scheduler);
+
+ VirtualTimeSource.Timed(scheduler, n, period)
+ .Sample(sampler)
+ .SubscribeConsume(Consumer);
+
+ scheduler.Start();
+ }
+
+ [Benchmark]
+ public void Delay_Selector()
+ {
+ // One-shot Timer only (no periodic path); same scheduler type keeps the file uniform.
+ var scheduler = new PeriodicVirtualScheduler();
+ var n = N;
+ var period = TimeSpan.FromTicks(PeriodTicks);
+ var delay = TimeSpan.FromTicks(PeriodTicks * 4);
+
+ VirtualTimeSource.Timed(scheduler, n, period)
+ .Delay(_ => Observable.Timer(delay, scheduler))
+ .SubscribeConsume(Consumer);
+
+ scheduler.Start();
+ }
+
+ [Benchmark]
+ public void Timeout_Selector()
+ {
+ var scheduler = new PeriodicVirtualScheduler();
+ var n = N;
+ var period = TimeSpan.FromTicks(PeriodTicks);
+ var timeout = TimeSpan.FromTicks(PeriodTicks * 4); // longer than the gap → never fires
+
+ VirtualTimeSource.Timed(scheduler, n, period)
+ .Timeout(_ => Observable.Timer(timeout, scheduler))
+ .SubscribeConsume(Consumer);
+
+ scheduler.Start();
+ }
+
+ [Benchmark]
+ public void TakeLastBuffer_Time()
+ {
+ var scheduler = new PeriodicVirtualScheduler();
+ var n = N;
+ var period = TimeSpan.FromTicks(PeriodTicks);
+ var duration = TimeSpan.FromTicks(PeriodTicks * (n / 2));
+
+ VirtualTimeSource.Timed(scheduler, n, period)
+ .TakeLastBuffer(duration, scheduler)
+ .SubscribeConsume(Consumer);
+
+ scheduler.Start();
+ }
+ }
+}
diff --git a/Rx.NET/Source/benchmarks/Benchmarks.System.Reactive/Operators/Time/TimeoutBenchmarks.cs b/Rx.NET/Source/benchmarks/Benchmarks.System.Reactive/Operators/Time/TimeoutBenchmarks.cs
new file mode 100644
index 0000000000..42790fa243
--- /dev/null
+++ b/Rx.NET/Source/benchmarks/Benchmarks.System.Reactive/Operators/Time/TimeoutBenchmarks.cs
@@ -0,0 +1,73 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT License.
+// See the LICENSE file in the project root for more information.
+
+using System;
+using System.Reactive.Concurrency;
+using System.Reactive.Linq;
+
+using BenchmarkDotNet.Attributes;
+
+using Benchmarks.System.Reactive.Infrastructure;
+
+namespace Benchmarks.System.Reactive.Operators.Time
+{
+ ///
+ /// S3: Timeout in three modes. NeverTrips (timeout > period) measures per-element timer-reset
+ /// bookkeeping. Trips fires and switches to a fallback (completes). Throws fires with no fallback,
+ /// so a real TimeoutException is propagated as OnError — the error path.
+ ///
+ [BenchmarkCategory("Time")]
+ public class TimeoutBenchmarks : TemporalBenchmarkBase
+ {
+ [Benchmark]
+ public void Timeout_NeverTrips()
+ {
+ var scheduler = new HistoricalScheduler();
+ var n = N;
+ var period = TimeSpan.FromTicks(PeriodTicks);
+ var timeout = TimeSpan.FromTicks(PeriodTicks * 4); // always longer than the gap → never fires
+
+ VirtualTimeSource.Timed(scheduler, n, period)
+ .Timeout(timeout, scheduler)
+ .SubscribeConsume(Consumer);
+
+ scheduler.Start();
+ }
+
+ [Benchmark]
+ public void Timeout_Trips()
+ {
+ var scheduler = new HistoricalScheduler();
+ var n = N;
+ var period = TimeSpan.FromTicks(PeriodTicks);
+ var timeout = TimeSpan.FromTicks(PeriodTicks * 2); // longer than the gap → all N elements reset the timer...
+
+ // ...then the stream goes silent (Never), so the timer finally elapses after the last element and
+ // Timeout trips, switching to the fallback. A timeout shorter than the first element's arrival would
+ // instead fire at subscription and measure zero-element, N-independent work.
+ VirtualTimeSource.Timed(scheduler, n, period).Concat(Observable.Never())
+ .Timeout(timeout, Observable.Empty(), scheduler)
+ .SubscribeConsume(Consumer);
+
+ scheduler.Start();
+ }
+
+ [Benchmark]
+ public void Timeout_Throws()
+ {
+ var scheduler = new HistoricalScheduler();
+ var n = N;
+ var period = TimeSpan.FromTicks(PeriodTicks);
+ var timeout = TimeSpan.FromTicks(PeriodTicks * 2); // longer than the gap → all N elements reset the timer...
+
+ // ...then silence trips the timer after the last element; with no fallback this surfaces as
+ // OnError(TimeoutException) — the error path — instead of firing at subscription on zero elements.
+ VirtualTimeSource.Timed(scheduler, n, period).Concat(Observable.Never())
+ .Timeout(timeout, scheduler)
+ .SubscribeConsume(Consumer);
+
+ scheduler.Start();
+ }
+ }
+}
diff --git a/Rx.NET/Source/benchmarks/Benchmarks.System.Reactive/Operators/Time/TimerIntervalBenchmarks.cs b/Rx.NET/Source/benchmarks/Benchmarks.System.Reactive/Operators/Time/TimerIntervalBenchmarks.cs
new file mode 100644
index 0000000000..1a9f977e9d
--- /dev/null
+++ b/Rx.NET/Source/benchmarks/Benchmarks.System.Reactive/Operators/Time/TimerIntervalBenchmarks.cs
@@ -0,0 +1,79 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT License.
+// See the LICENSE file in the project root for more information.
+
+using System;
+using System.Reactive.Concurrency;
+using System.Reactive.Linq;
+
+using BenchmarkDotNet.Attributes;
+
+using Benchmarks.System.Reactive.Infrastructure;
+
+namespace Benchmarks.System.Reactive.Operators.Time
+{
+ ///
+ /// S3: the timed source factories. Interval and Timer use ISchedulerPeriodic, which
+ /// implements, so they run Rx's real periodic fast path in virtual
+ /// time. Interval_EmulatedPeriodic keeps the plain to quantify the
+ /// stopwatch-emulated fallback's overhead. Generate (timed) is the self-rescheduling source the
+ /// suite uses to drive the other temporal operators.
+ ///
+ [BenchmarkCategory("Time")]
+ public class TimerIntervalBenchmarks : TemporalBenchmarkBase
+ {
+ [Benchmark]
+ public void Interval()
+ {
+ var scheduler = new PeriodicVirtualScheduler();
+ var n = N;
+ var period = TimeSpan.FromTicks(PeriodTicks);
+
+ Observable.Interval(period, scheduler).Take(n)
+ .SubscribeConsume(Consumer);
+
+ scheduler.Start();
+ }
+
+ [Benchmark]
+ public void Interval_EmulatedPeriodic()
+ {
+ // HistoricalScheduler has no ISchedulerPeriodic, so Rx falls back to the stopwatch-emulated
+ // periodic path (AutoResetEvent + host-lifecycle registration per subscription).
+ var scheduler = new HistoricalScheduler();
+ var n = N;
+ var period = TimeSpan.FromTicks(PeriodTicks);
+
+ Observable.Interval(period, scheduler).Take(n)
+ .SubscribeConsume(Consumer);
+
+ scheduler.Start();
+ }
+
+ [Benchmark]
+ public void Timer()
+ {
+ var scheduler = new PeriodicVirtualScheduler();
+ var n = N;
+ var period = TimeSpan.FromTicks(PeriodTicks);
+
+ Observable.Timer(period, period, scheduler).Take(n)
+ .SubscribeConsume(Consumer);
+
+ scheduler.Start();
+ }
+
+ [Benchmark]
+ public void Generate_Timed()
+ {
+ var scheduler = new HistoricalScheduler();
+ var n = N;
+ var period = TimeSpan.FromTicks(PeriodTicks);
+
+ Observable.Generate(0, i => i < n, i => i + 1, i => i, _ => period, scheduler)
+ .SubscribeConsume(Consumer);
+
+ scheduler.Start();
+ }
+ }
+}
diff --git a/Rx.NET/Source/benchmarks/Benchmarks.System.Reactive/Operators/Time/TimestampTimeIntervalBenchmarks.cs b/Rx.NET/Source/benchmarks/Benchmarks.System.Reactive/Operators/Time/TimestampTimeIntervalBenchmarks.cs
new file mode 100644
index 0000000000..09dc9405cf
--- /dev/null
+++ b/Rx.NET/Source/benchmarks/Benchmarks.System.Reactive/Operators/Time/TimestampTimeIntervalBenchmarks.cs
@@ -0,0 +1,51 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT License.
+// See the LICENSE file in the project root for more information.
+
+using System;
+using System.Reactive.Concurrency;
+using System.Reactive.Linq;
+
+using BenchmarkDotNet.Attributes;
+
+using Benchmarks.System.Reactive.Infrastructure;
+
+namespace Benchmarks.System.Reactive.Operators.Time
+{
+ ///
+ /// S3: Timestamp and TimeInterval annotate each element with scheduler time. Both project into
+ /// value-type wrappers (Timestamped<T> / TimeInterval<T>), so — unlike the reference-type
+ /// Notification<T> path — they should not allocate per element; the benchmark confirms that.
+ ///
+ [BenchmarkCategory("Time")]
+ public class TimestampTimeIntervalBenchmarks : TemporalBenchmarkBase
+ {
+ [Benchmark]
+ public void Timestamp()
+ {
+ var scheduler = new HistoricalScheduler();
+ var n = N;
+ var period = TimeSpan.FromTicks(PeriodTicks);
+
+ VirtualTimeSource.Timed(scheduler, n, period)
+ .Timestamp(scheduler)
+ .SubscribeConsume(Consumer);
+
+ scheduler.Start();
+ }
+
+ [Benchmark]
+ public void TimeInterval()
+ {
+ var scheduler = new HistoricalScheduler();
+ var n = N;
+ var period = TimeSpan.FromTicks(PeriodTicks);
+
+ VirtualTimeSource.Timed(scheduler, n, period)
+ .TimeInterval(scheduler)
+ .SubscribeConsume(Consumer);
+
+ scheduler.Start();
+ }
+ }
+}
diff --git a/Rx.NET/Source/benchmarks/Benchmarks.System.Reactive/Operators/Time/WindowTimeBenchmarks.cs b/Rx.NET/Source/benchmarks/Benchmarks.System.Reactive/Operators/Time/WindowTimeBenchmarks.cs
new file mode 100644
index 0000000000..055ac6c6a7
--- /dev/null
+++ b/Rx.NET/Source/benchmarks/Benchmarks.System.Reactive/Operators/Time/WindowTimeBenchmarks.cs
@@ -0,0 +1,36 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT License.
+// See the LICENSE file in the project root for more information.
+
+using System.Reactive.Linq;
+
+using BenchmarkDotNet.Attributes;
+
+using Benchmarks.System.Reactive.Infrastructure;
+
+namespace Benchmarks.System.Reactive.Operators.Time
+{
+ ///
+ /// S3: time-based Window produces a sub-observable per window. The inner windows are flattened with
+ /// SelectMany so the benchmark measures element delivery, not just window-open cost.
+ ///
+ [BenchmarkCategory("Time")]
+ public class WindowTimeBenchmarks : RateWindowBenchmarkBase
+ {
+ [Benchmark]
+ public void Window_Time()
+ {
+ var scheduler = new PeriodicVirtualScheduler(); // Window(TimeSpan) runs on the real periodic path
+ var n = N;
+ var window = Window;
+ var period = Period;
+
+ VirtualTimeSource.Timed(scheduler, n, period)
+ .Window(window, scheduler)
+ .SelectMany(static w => w) // realise inner elements, else only window-open cost is measured
+ .SubscribeConsume(Consumer);
+
+ scheduler.Start();
+ }
+ }
+}
diff --git a/Rx.NET/Source/benchmarks/Benchmarks.System.Reactive/PrependVsStartWtihBenchmark.cs b/Rx.NET/Source/benchmarks/Benchmarks.System.Reactive/PrependVsStartWtihBenchmark.cs
deleted file mode 100644
index 6d1cc93605..0000000000
--- a/Rx.NET/Source/benchmarks/Benchmarks.System.Reactive/PrependVsStartWtihBenchmark.cs
+++ /dev/null
@@ -1,74 +0,0 @@
-// Licensed to the .NET Foundation under one or more agreements.
-// The .NET Foundation licenses this file to you under the MIT License.
-// See the LICENSE file in the project root for more information.
-
-#if (CURRENT)
-using System;
-using System.Collections.Generic;
-using System.Reactive.Linq;
-using System.Threading;
-using BenchmarkDotNet.Attributes;
-
-namespace Benchmarks.System.Reactive
-{
- [MemoryDiagnoser]
- public class PrependVsStartWtihBenchmark
- {
- private int _store;
-#pragma warning disable IDE0052 // (Remove unread private members.) We want to store results to prevent the benchmarked code from being optimized out of existence.
- private IObservable _obsStore;
-#pragma warning restore IDE0052
-
- [Benchmark(Baseline = true)]
- public void Prepend()
- {
- Observable
- .Empty()
- .Prepend(0)
- .Subscribe(v => Volatile.Write(ref _store, v));
- }
-
- [Benchmark]
- public void Prepend_Create()
- {
- _obsStore = Observable
- .Empty()
- .Prepend(0);
- }
-
-
- private static readonly IObservable _prependObservable = Observable.Empty().Prepend(0);
- [Benchmark]
- public void Prepend_Subscribe()
- {
- _prependObservable
- .Subscribe(v => Volatile.Write(ref _store, v));
- }
-
- [Benchmark]
- public void StartWith()
- {
- Observable
- .Empty()
- .StartWith(0)
- .Subscribe(v => Volatile.Write(ref _store, v));
- }
-
- [Benchmark]
- public void StartWith_Create()
- {
- _obsStore = Observable
- .Empty()
- .StartWith(0);
- }
-
- private static readonly IObservable _startWithObservable = Observable.Empty().StartWith(0);
- [Benchmark]
- public void StartWith_Subscribe()
- {
- _startWithObservable
- .Subscribe(v => Volatile.Write(ref _store, v));
- }
- }
-}
-#endif
diff --git a/Rx.NET/Source/benchmarks/Benchmarks.System.Reactive/Program.cs b/Rx.NET/Source/benchmarks/Benchmarks.System.Reactive/Program.cs
index ca80b785c3..575ae65711 100644
--- a/Rx.NET/Source/benchmarks/Benchmarks.System.Reactive/Program.cs
+++ b/Rx.NET/Source/benchmarks/Benchmarks.System.Reactive/Program.cs
@@ -1,46 +1,28 @@
-// Licensed to the .NET Foundation under one or more agreements.
+// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT License.
// See the LICENSE file in the project root for more information.
using System;
using System.Reactive.Linq;
-using BenchmarkDotNet.Running;
-using Microsoft.VisualStudio.TestTools.UnitTesting;
+using BenchmarkDotNet.Running;
-[assembly:DoNotParallelize] // Not really a test project, but the build tools think we are, and complain if we don't state our parallelization policy.
+using Benchmarks.System.Reactive.Infrastructure;
namespace Benchmarks.System.Reactive
{
internal class Program
{
- private static void Main()
+ private static void Main(string[] args)
{
Console.WriteLine("Effective Rx-version: " + typeof(Observable).Assembly.GetName().Version);
- var switcher = new BenchmarkSwitcher([
- typeof(ZipBenchmark),
- typeof(CombineLatestBenchmark),
- typeof(SwitchBenchmark),
- typeof(BufferCountBenchmark),
- typeof(RangeBenchmark),
- typeof(ToObservableBenchmark),
- typeof(RepeatBenchmark),
- typeof(ComparisonBenchmark),
- typeof(ComparisonAsyncBenchmark),
- typeof(ScalarScheduleBenchmark),
- typeof(StableCompositeDisposableBenchmark),
- typeof(SubjectBenchmark),
- typeof(ComparisonAsyncBenchmark),
- typeof(GroupByCompletion)
-#if (CURRENT)
- ,typeof(AppendPrependBenchmark)
- ,typeof(PrependVsStartWtihBenchmark)
-#endif
- ]);
-
- switcher.Run();
- Console.ReadLine();
+ // Auto-discover every public, non-abstract class with [Benchmark] methods in this assembly.
+ // Profiling / runtime selection is driven by CLI args (e.g. --filter, -f/--runtimes,
+ // --profiler ETW|EP, --disasm), layered on top of the shared RxBenchmarkConfig.
+ BenchmarkSwitcher
+ .FromAssembly(typeof(Program).Assembly)
+ .Run(args, RxBenchmarkConfig.Create());
}
}
}
diff --git a/Rx.NET/Source/benchmarks/Benchmarks.System.Reactive/RangeBenchmark.cs b/Rx.NET/Source/benchmarks/Benchmarks.System.Reactive/RangeBenchmark.cs
deleted file mode 100644
index 320e3469e1..0000000000
--- a/Rx.NET/Source/benchmarks/Benchmarks.System.Reactive/RangeBenchmark.cs
+++ /dev/null
@@ -1,25 +0,0 @@
-// Licensed to the .NET Foundation under one or more agreements.
-// The .NET Foundation licenses this file to you under the MIT License.
-// See the LICENSE file in the project root for more information.
-
-using System;
-using System.Reactive.Linq;
-using System.Threading;
-using BenchmarkDotNet.Attributes;
-
-namespace Benchmarks.System.Reactive
-{
- [MemoryDiagnoser]
- public class RangeBenchmark
- {
- [Params(1, 10, 100, 1000, 10000, 100000, 1000000)]
- public int N;
- private int _store;
-
- [Benchmark]
- public void Range()
- {
- Observable.Range(1, N).Subscribe(v => Volatile.Write(ref _store, v));
- }
- }
-}
diff --git a/Rx.NET/Source/benchmarks/Benchmarks.System.Reactive/RepeatBenchmark.cs b/Rx.NET/Source/benchmarks/Benchmarks.System.Reactive/RepeatBenchmark.cs
deleted file mode 100644
index 229756df38..0000000000
--- a/Rx.NET/Source/benchmarks/Benchmarks.System.Reactive/RepeatBenchmark.cs
+++ /dev/null
@@ -1,32 +0,0 @@
-// Licensed to the .NET Foundation under one or more agreements.
-// The .NET Foundation licenses this file to you under the MIT License.
-// See the LICENSE file in the project root for more information.
-
-using System;
-using System.Reactive.Linq;
-using System.Threading;
-using BenchmarkDotNet.Attributes;
-
-namespace Benchmarks.System.Reactive
-{
- [MemoryDiagnoser]
- public class RepeatBenchmark
- {
- [Params(1, 10, 100, 1000, 10000, 100000, 1000000)]
- public int N;
-
- public int _store;
-
- [Benchmark]
- public void Repeat_Infinite()
- {
- Observable.Repeat(1).Take(N).Subscribe(v => Volatile.Write(ref _store, v));
- }
-
- [Benchmark]
- public void Repeat_Finite()
- {
- Observable.Repeat(1, N).Subscribe(v => Volatile.Write(ref _store, v));
- }
- }
-}
diff --git a/Rx.NET/Source/benchmarks/Benchmarks.System.Reactive/ScalarScheduleBenchmark.cs b/Rx.NET/Source/benchmarks/Benchmarks.System.Reactive/ScalarScheduleBenchmark.cs
deleted file mode 100644
index 5c6fa9578c..0000000000
--- a/Rx.NET/Source/benchmarks/Benchmarks.System.Reactive/ScalarScheduleBenchmark.cs
+++ /dev/null
@@ -1,179 +0,0 @@
-// Licensed to the .NET Foundation under one or more agreements.
-// The .NET Foundation licenses this file to you under the MIT License.
-// See the LICENSE file in the project root for more information.
-
-using System;
-using System.Reactive.Concurrency;
-using System.Reactive.Linq;
-using System.Threading;
-using BenchmarkDotNet.Attributes;
-
-namespace Benchmarks.System.Reactive
-{
- [MemoryDiagnoser]
- public class ScalarScheduleBenchmark
- {
- private int _store;
- private Exception _exceptionStore;
-
- private IScheduler _eventLoop;
-
- private Exception _exception;
-
- [GlobalSetup]
- public void Setup()
- {
- _eventLoop = new EventLoopScheduler();
- _exception = new Exception();
- }
-
- private void BlockingConsume(IObservable source)
- {
- var cde = new CountdownEvent(1);
-
- source.Subscribe(v => Volatile.Write(ref _store, v),
- e =>
- {
- Volatile.Write(ref _exceptionStore, e);
- cde.Signal();
- },
- () => cde.Signal()
- );
-
- // spin-wait will result in faster completion detection
- // because it takes 5 microseconds to resume a blocked thread
- // for me on Windows
- while (cde.CurrentCount != 0) ;
- }
-
- private void ConsumeSync(IObservable source)
- {
- source.Subscribe(v => Volatile.Write(ref _store, v), e => Volatile.Write(ref _exceptionStore, e));
- }
-
- [Benchmark]
- public void Return_Immediate()
- {
- ConsumeSync(Observable.Return(1, ImmediateScheduler.Instance));
- }
-
- [Benchmark]
- public void Return_CurrentThread()
- {
- ConsumeSync(Observable.Return(1, CurrentThreadScheduler.Instance));
- }
-
- [Benchmark]
- public void Return_EventLoop()
- {
- BlockingConsume(Observable.Return(1, _eventLoop));
- }
-
- [Benchmark]
- public void Return_TaskPool()
- {
- BlockingConsume(Observable.Return(1, TaskPoolScheduler.Default));
- }
-
- [Benchmark]
- public void Return_ThreadPool()
- {
- BlockingConsume(Observable.Return(1, ThreadPoolScheduler.Instance));
- }
-
- [Benchmark]
- public void Throw_Immediate()
- {
- ConsumeSync(Observable.Throw(_exception, ImmediateScheduler.Instance));
- }
-
- [Benchmark]
- public void Throw_CurrentThread()
- {
- ConsumeSync(Observable.Throw(_exception, CurrentThreadScheduler.Instance));
- }
-
- [Benchmark]
- public void Throw_EventLoop()
- {
- BlockingConsume(Observable.Throw(_exception, _eventLoop));
- }
-
- [Benchmark]
- public void Throw_TaskPool()
- {
- BlockingConsume(Observable.Throw(_exception, TaskPoolScheduler.Default));
- }
-
- [Benchmark]
- public void Throw_ThreadPool()
- {
- BlockingConsume(Observable.Throw(_exception, ThreadPoolScheduler.Instance));
- }
-
-#if CURRENT
-
- [Benchmark]
- public void Prepend_Immediate()
- {
- ConsumeSync(Observable.Return(1, ImmediateScheduler.Instance).Prepend(0, ImmediateScheduler.Instance));
- }
-
-
- [Benchmark]
- public void Prepend_CurrentThread()
- {
- ConsumeSync(Observable.Return(1, CurrentThreadScheduler.Instance).Prepend(0, CurrentThreadScheduler.Instance));
- }
-
- [Benchmark]
- public void Prepend_EventLoop()
- {
- BlockingConsume(Observable.Return(1, _eventLoop).Prepend(0, _eventLoop));
- }
-
- [Benchmark]
- public void Prepend_TaskPool()
- {
- BlockingConsume(Observable.Return(1, TaskPoolScheduler.Default).Prepend(0, TaskPoolScheduler.Default));
- }
-
- [Benchmark]
- public void Prepend_ThreadPool()
- {
- BlockingConsume(Observable.Return(1, ThreadPoolScheduler.Instance).Prepend(0, ThreadPoolScheduler.Instance));
- }
-
- [Benchmark]
- public void Append_Immediate()
- {
- ConsumeSync(Observable.Return(1, ImmediateScheduler.Instance).Append(0, ImmediateScheduler.Instance));
- }
-
-
- [Benchmark]
- public void Append_CurrentThread()
- {
- ConsumeSync(Observable.Return(1, CurrentThreadScheduler.Instance).Append(0, CurrentThreadScheduler.Instance));
- }
-
- [Benchmark]
- public void Append_EventLoop()
- {
- BlockingConsume(Observable.Return(1, _eventLoop).Append(0, _eventLoop));
- }
-
- [Benchmark]
- public void Append_TaskPool()
- {
- BlockingConsume(Observable.Return(1, TaskPoolScheduler.Default).Append(0, TaskPoolScheduler.Default));
- }
-
- [Benchmark]
- public void Append_ThreadPool()
- {
- BlockingConsume(Observable.Return(1, ThreadPoolScheduler.Instance).Append(0, ThreadPoolScheduler.Instance));
- }
-#endif
- }
-}
diff --git a/Rx.NET/Source/benchmarks/Benchmarks.System.Reactive/SubjectBenchmark.cs b/Rx.NET/Source/benchmarks/Benchmarks.System.Reactive/SubjectBenchmark.cs
deleted file mode 100644
index 22b4317a53..0000000000
--- a/Rx.NET/Source/benchmarks/Benchmarks.System.Reactive/SubjectBenchmark.cs
+++ /dev/null
@@ -1,108 +0,0 @@
-// Licensed to the .NET Foundation under one or more agreements.
-// The .NET Foundation licenses this file to you under the MIT License.
-// See the LICENSE file in the project root for more information.
-
-using System;
-using System.Reactive.Linq;
-using System.Reactive.Subjects;
-using System.Threading;
-using BenchmarkDotNet.Attributes;
-
-namespace Benchmarks.System.Reactive
-{
- [MemoryDiagnoser]
- public class SubjectBenchmark
- {
- [Params(1, 10, 100, 1000, 10000, 100000, 1000000)]
- public int N;
-
- [Params(0, 1, 2, 3, 4, 5)]
- public int M;
-
- private int _store;
-
- [Benchmark]
- public object SubjectPush()
- {
- var subj = new Subject();
- var consumers = new IDisposable[M];
- var m = M;
- for (var i = 0; i < m; i++)
- {
- consumers[i] = subj.Subscribe(v => Volatile.Write(ref _store, v));
- }
-
- var n = N;
- for (var i = 0; i < n; i++)
- {
- subj.OnNext(i);
- }
- subj.OnCompleted();
-
- return consumers;
- }
-
- [Benchmark]
- public object AsyncSubjectPush()
- {
- var subj = new AsyncSubject();
- var consumers = new IDisposable[M];
- var m = M;
- for (var i = 0; i < m; i++)
- {
- consumers[i] = subj.Subscribe(v => Volatile.Write(ref _store, v));
- }
-
- var n = N;
- for (var i = 0; i < n; i++)
- {
- subj.OnNext(i);
- }
- subj.OnCompleted();
-
- return consumers;
- }
-
- [Benchmark]
- public object BehaviorSubjectPush()
- {
- var subj = new BehaviorSubject(-1);
- var consumers = new IDisposable[M];
- var m = M;
- for (var i = 0; i < m; i++)
- {
- consumers[i] = subj.Subscribe(v => Volatile.Write(ref _store, v));
- }
-
- var n = N;
- for (var i = 0; i < n; i++)
- {
- subj.OnNext(i);
- }
- subj.OnCompleted();
-
- return consumers;
- }
-
- [Benchmark]
- public object ReplaySubjectPush()
- {
- var subj = new ReplaySubject();
- var consumers = new IDisposable[M];
- var m = M;
- for (var i = 0; i < m; i++)
- {
- consumers[i] = subj.Subscribe(v => Volatile.Write(ref _store, v));
- }
-
- var n = N;
- for (var i = 0; i < n; i++)
- {
- subj.OnNext(i);
- }
- subj.OnCompleted();
-
- return consumers;
- }
- }
-}
diff --git a/Rx.NET/Source/benchmarks/Benchmarks.System.Reactive/SwitchBenchmark.cs b/Rx.NET/Source/benchmarks/Benchmarks.System.Reactive/SwitchBenchmark.cs
deleted file mode 100644
index 57258d7e21..0000000000
--- a/Rx.NET/Source/benchmarks/Benchmarks.System.Reactive/SwitchBenchmark.cs
+++ /dev/null
@@ -1,25 +0,0 @@
-// Licensed to the .NET Foundation under one or more agreements.
-// The .NET Foundation licenses this file to you under the MIT License.
-// See the LICENSE file in the project root for more information.
-
-using System.Reactive.Linq;
-using BenchmarkDotNet.Attributes;
-using System.Reactive.Threading.Tasks;
-using System.Threading.Tasks;
-
-namespace Benchmarks.System.Reactive
-{
- [MemoryDiagnoser]
- public class SwitchBenchmark
- {
- [Benchmark]
- public async Task Switch_10000_Sources()
- {
- await Observable
- .Range(1, 10000)
- .Select(x => Observable.Return(x))
- .Switch()
- .ToTask();
- }
- }
-}
diff --git a/Rx.NET/Source/benchmarks/Benchmarks.System.Reactive/ToObservableBenchmark.cs b/Rx.NET/Source/benchmarks/Benchmarks.System.Reactive/ToObservableBenchmark.cs
deleted file mode 100644
index 9a5469659d..0000000000
--- a/Rx.NET/Source/benchmarks/Benchmarks.System.Reactive/ToObservableBenchmark.cs
+++ /dev/null
@@ -1,29 +0,0 @@
-// Licensed to the .NET Foundation under one or more agreements.
-// The .NET Foundation licenses this file to you under the MIT License.
-// See the LICENSE file in the project root for more information.
-
-using System;
-using System.Linq;
-using System.Reactive.Linq;
-using System.Threading;
-using BenchmarkDotNet.Attributes;
-
-namespace Benchmarks.System.Reactive
-{
- [MemoryDiagnoser]
- public class ToObservableBenchmark
- {
- [Params(1, 10, 100, 1000, 10000, 100000, 1000000)]
- public int N;
-
- private int _store;
-
- [Benchmark]
- public void Exact()
- {
- Enumerable.Range(1, N)
- .ToObservable()
- .Subscribe(v => Volatile.Write(ref _store, v));
- }
- }
-}
diff --git a/Rx.NET/Source/benchmarks/Benchmarks.System.Reactive/ZipBenchmark.cs b/Rx.NET/Source/benchmarks/Benchmarks.System.Reactive/ZipBenchmark.cs
deleted file mode 100644
index a551b5b4af..0000000000
--- a/Rx.NET/Source/benchmarks/Benchmarks.System.Reactive/ZipBenchmark.cs
+++ /dev/null
@@ -1,135 +0,0 @@
-// Licensed to the .NET Foundation under one or more agreements.
-// The .NET Foundation licenses this file to you under the MIT License.
-// See the LICENSE file in the project root for more information.
-
-using BenchmarkDotNet.Attributes;
-using ReactiveTests.Tests;
-
-namespace Benchmarks.System.Reactive
-{
- [MemoryDiagnoser]
- public class ZipBenchmark
- {
- private readonly ZipTest _zipTest = new();
-
- [Benchmark]
- public void Zip_NAry_Asymmetric()
- {
- _zipTest.Zip_NAry_Asymmetric();
- }
-
- [Benchmark]
- public void Zip_NAry_Asymmetric_Selector()
- {
- _zipTest.Zip_NAry_Asymmetric_Selector();
- }
-
- [Benchmark]
- public void Zip_NAry_Symmetric()
- {
- _zipTest.Zip_NAry_Symmetric();
- }
-
- [Benchmark]
- public void Zip_NAry_Symmetric_Selector()
- {
- _zipTest.Zip_NAry_Symmetric_Selector();
- }
-
- [Benchmark]
- public void Zip_NAry_Enumerable_Simple()
- {
- _zipTest.Zip_NAry_Enumerable_Simple();
- }
-
- [Benchmark]
- public void Zip_AllCompleted2()
- {
- _zipTest.Zip_AllCompleted2();
- }
-
- [Benchmark]
- public void Zip_AllCompleted3()
- {
- _zipTest.Zip_AllCompleted3();
- }
-
- [Benchmark]
- public void Zip_AllCompleted4()
- {
- _zipTest.Zip_AllCompleted4();
- }
-
- [Benchmark]
- public void Zip_AllCompleted5()
- {
- _zipTest.Zip_AllCompleted5();
- }
-
- [Benchmark]
- public void Zip_AllCompleted6()
- {
- _zipTest.Zip_AllCompleted6();
- }
-
- [Benchmark]
- public void Zip_AllCompleted7()
- {
- _zipTest.Zip_AllCompleted7();
- }
-
- [Benchmark]
- public void Zip_AllCompleted8()
- {
- _zipTest.Zip_AllCompleted8();
- }
-
- [Benchmark]
- public void Zip_AllCompleted9()
- {
- _zipTest.Zip_AllCompleted9();
- }
-
- [Benchmark]
- public void Zip_AllCompleted10()
- {
- _zipTest.Zip_AllCompleted10();
- }
-
- [Benchmark]
- public void Zip_AllCompleted11()
- {
- _zipTest.Zip_AllCompleted11();
- }
-
- [Benchmark]
- public void Zip_AllCompleted12()
- {
- _zipTest.Zip_AllCompleted12();
- }
-
- [Benchmark]
- public void Zip_AllCompleted13()
- {
- _zipTest.Zip_AllCompleted13();
- }
-
- [Benchmark]
- public void Zip_AllCompleted14()
- {
- _zipTest.Zip_AllCompleted14();
- }
-
- [Benchmark]
- public void Zip_AllCompleted15()
- {
- _zipTest.Zip_AllCompleted15();
- }
-
- [Benchmark]
- public void Zip_AllCompleted16()
- {
- _zipTest.Zip_AllCompleted16();
- }
- }
-}
diff --git a/Rx.NET/Source/benchmarks/README.md b/Rx.NET/Source/benchmarks/README.md
new file mode 100644
index 0000000000..cb16b583c7
--- /dev/null
+++ b/Rx.NET/Source/benchmarks/README.md
@@ -0,0 +1,106 @@
+# Rx.NET benchmark suite
+
+Infrastructure for measuring Rx.NET performance, built to support before/after comparisons during
+performance work (e.g. the .NET 10 modernization spike).
+
+| Folder | Purpose |
+|---|---|
+| `Benchmarks.System.Reactive/` | The BenchmarkDotNet microbenchmark suite (~100 operator classes under `Operators//`, shared infrastructure under `Infrastructure/`). |
+| `Rx.WorkloadHarness/` | A long-running console host for system-level telemetry (`dotnet-counters` / `dotnet-trace` / `dotnet-monitor`) — the steady-state GC/thread-pool view microbenchmarks can't give. |
+| `tools/` | `capture-baseline.ps1` and `compare-results.ps1` — the A/B workflow (see below). |
+| `baselines/`, `traces/` | Local run outputs. **Gitignored** — keep them locally or publish as CI artifacts. |
+
+## Scenario archetypes
+
+Every benchmark class follows one of four shapes:
+
+- **S1 throughput** — cold synchronous pipelines (`Observable.Range`), swept over `N` = 1…1,000,000 elements (`OperatorBenchmarkBase`).
+- **S2 hot push** — a pumped `Subject` exercising the per-element `OnNext` dispatch path.
+- **S3 temporal** — time-based operators driven in *virtual time* (`HistoricalScheduler` /
+ `PeriodicVirtualScheduler`), so even dense timelines complete in microseconds of wall clock with
+ zero timing noise. Operators with a periodic path (`Interval`, `Timer`, `Sample`, `Buffer(time)`,
+ `Window(time)`) use `PeriodicVirtualScheduler`, which implements `ISchedulerPeriodic` so Rx's real
+ periodic fast path is measured rather than the stopwatch-emulated fallback.
+- **S4 subscription/lifecycle** — construction, subscription, and disposal costs in isolation.
+
+Results are consumed through a shared `Consumer` sink (defeating dead-code elimination); async
+pipelines block on real completion (`SubscribeBlocking`). `MemoryDiagnoser` is applied globally, so
+allocation is a first-class column everywhere, alongside per-element `ns/N` (mean time / N) and
+`B/N` (allocated bytes / N) columns for classes with an `N` parameter.
+
+Reading the per-element columns:
+
+- Small-N rows are dominated by fixed subscription/setup cost, so `ns/N` and `B/N` only converge
+ to the true steady-state per-element cost as N grows (e.g. `Select`: 544 `B/N` at N=1 flattening
+ to a constant 64 `B/N` from N≈1,000 up). Read the large-N rows for per-element cost; read the
+ small-N rows for per-subscription overhead.
+- A `B/N` that stays flat as N grows means the operator allocates *per element* — the primary
+ zero-allocation target the columns exist to surface.
+- For fan-out classes (SelectMany/Merge/Switch cross-map variants), `N` is the fan-out width with
+ total work pinned, so the figures read as per-subscription rather than per-element.
+- The columns are display-only; the exported JSON keeps raw statistics, and `compare-results.ps1`
+ recomputes per-element values itself.
+
+## Running
+
+```powershell
+cd Benchmarks.System.Reactive
+
+# Everything (long!) on .NET 10
+dotnet run -c Release -f net10.0 -- --filter *
+
+# One class / one category
+dotnet run -c Release -f net10.0 -- --filter *SelectBenchmarks*
+dotnet run -c Release -f net10.0 -- --anyCategories Time
+
+# Cross-runtime comparison in one table
+dotnet run -c Release -f net10.0 -- --filter *SelectBenchmarks* --runtimes net472 net8.0 net10.0
+
+# Fast sanity pass (no statistics — just checks benchmarks execute)
+dotnet run -c Release -f net10.0 -- --filter * --job Dry
+```
+
+Opt-in profiling (layered on the shared config per run):
+
+```powershell
+dotnet run -c Release -f net10.0 -- --filter *Merge* --profiler ETW # Windows, elevated: ETW traces
+dotnet run -c Release -f net10.0 -- --filter *Merge* --profiler EP # cross-platform EventPipe
+dotnet run -c Release -f net10.0 -- --filter *Select* --disasm # JIT disassembly
+```
+
+Note: `System.Reactive` currently tops out at `net8.0`, so net9/net10 runs execute the net8-compiled
+assembly on the newer runtime — they show JIT/runtime gains only. Product code-path changes need a
+same-TFM before/after comparison, which is what the tools below provide.
+
+## Before/after workflow (A/B)
+
+```powershell
+# 1. On the baseline commit:
+tools\capture-baseline.ps1 -Filter * -Framework net10.0
+
+# 2. Check out the candidate commit (or apply your change), then:
+tools\capture-baseline.ps1 -Filter * -Framework net10.0
+
+# 3. Compare the two stamped folders:
+tools\compare-results.ps1 -Baseline baselines\2026-07-01-abc1234 -Candidate baselines\2026-07-02-def5678
+```
+
+`capture-baseline.ps1` runs the suite, stamps results into `baselines/-[-label]/results/`
+(full JSON + GitHub markdown + CSV per class), and records the environment (commit, branch,
+`dotnet --info`) alongside. It refuses to stamp a dirty working tree unless `-AllowDirty` is passed.
+
+`compare-results.ps1` matches benchmarks by full name (including parameter values) across the two
+result sets, classifies each as regression / improvement / unchanged using a ratio threshold
+(default 2%) plus the reported confidence intervals, and writes a markdown report sorted
+worst-regression-first.
+
+## Workload harness (steady-state telemetry)
+
+```powershell
+cd Rx.WorkloadHarness
+dotnet run -c Release -f net10.0 -- buffer --rate-ms 1 # runs until Ctrl+C, prints its PID
+dotnet-counters monitor -p System.Runtime # from a second terminal
+```
+
+Workloads: `buffer` (default; allocates a `List` per batch as a visible GC signal), `select`,
+`merge`, `groupby`. Use `--seconds N` for a bounded run.
diff --git a/Rx.NET/Source/benchmarks/Rx.WorkloadHarness/Program.cs b/Rx.NET/Source/benchmarks/Rx.WorkloadHarness/Program.cs
new file mode 100644
index 0000000000..c11ecf6031
--- /dev/null
+++ b/Rx.NET/Source/benchmarks/Rx.WorkloadHarness/Program.cs
@@ -0,0 +1,91 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT License.
+// See the LICENSE file in the project root for more information.
+
+using System;
+using System.Collections.Generic;
+using System.Reactive.Linq;
+using System.Threading;
+
+namespace Rx.WorkloadHarness
+{
+ ///
+ /// A long-running host that runs a sustained Rx pipeline so live runtime telemetry can be collected
+ /// with dotnet-counters / dotnet-trace / dotnet-monitor — the system-level view
+ /// (steady-state GC, memory growth, thread-pool saturation, contention) that the microbenchmarks
+ /// cannot show. Prints its PID and runs until Ctrl+C.
+ ///
+ ///
+ /// Usage: dotnet run -c Release -f net10.0 -- [buffer|select|merge|groupby] [--rate-ms N].
+ /// Then, in another shell: dotnet-counters monitor -p <pid> System.Runtime.
+ ///
+ internal static class Program
+ {
+ private static void Main(string[] args)
+ {
+ var workload = args.Length > 0 && !args[0].StartsWith("--", StringComparison.Ordinal)
+ ? args[0].ToLowerInvariant()
+ : "buffer";
+ var rateMs = GetIntOption(args, "--rate-ms", 1);
+ var seconds = GetIntOption(args, "--seconds", 0); // 0 = run until Ctrl+C; > 0 = auto-stop (handy for CI/smoke runs)
+ if (rateMs < 1)
+ {
+ Console.Error.WriteLine("--rate-ms must be >= 1.");
+ Environment.Exit(1);
+ }
+
+ if (seconds < 0)
+ {
+ Console.Error.WriteLine("--seconds must be >= 0 (0 = run until Ctrl+C).");
+ Environment.Exit(1);
+ }
+
+ var period = TimeSpan.FromMilliseconds(rateMs);
+
+ var pid = Environment.ProcessId;
+ Console.WriteLine($"Rx.WorkloadHarness PID={pid} workload='{workload}' rate={rateMs}ms");
+ Console.WriteLine($" dotnet-counters monitor -p {pid} System.Runtime");
+ Console.WriteLine($" dotnet-trace collect -p {pid}");
+ Console.WriteLine(seconds > 0 ? $"Auto-stops after {seconds}s (or press Ctrl+C)." : "Press Ctrl+C to stop.");
+
+ var pipeline = BuildPipeline(workload, Observable.Interval(period));
+
+ using var stop = new ManualResetEventSlim(false);
+ Console.CancelKeyPress += (_, e) => { e.Cancel = true; stop.Set(); };
+ using var autoStop = seconds > 0
+ ? new Timer(_ => stop.Set(), null, TimeSpan.FromSeconds(seconds), Timeout.InfiniteTimeSpan)
+ : null;
+
+ var observed = 0L;
+ using (pipeline.Subscribe(_ => Interlocked.Increment(ref observed)))
+ {
+ stop.Wait();
+ }
+
+ Console.WriteLine($"Stopped. Observed {Volatile.Read(ref observed)} notifications.");
+ }
+
+ // A handful of representative sustained pipelines. "buffer" allocates a list per batch, which is a
+ // deliberately visible GC signal; the others exercise transform / multicast / grouping paths.
+ private static IObservable