diff --git a/README.md b/README.md index d4378d9..f7fdf79 100644 --- a/README.md +++ b/README.md @@ -618,73 +618,73 @@ dotnet run --project src/benchmarks/ReactiveUI.Primitives.Benchmarks/ReactiveUI. Short local jobs are useful for fast regression checks; rerun with a longer BenchmarkDotNet job before making release claims. Current local test coverage after this pass is 82.80% line coverage and 75.50% branch coverage from `coverage-local.cobertura.xml`; the 100% coverage target remains an active work item. -`N/A` means the current benchmark harness does not contain a matching measured row for that library. +The table below is generated from the joined BenchmarkDotNet CSV and uses `Mean / Allocated` for each cell. | Scenario | ReactiveUI.Primitives | System.Reactive | R3 | |---|---:|---:|---:| -| Return subscribe | 0.2089 ns / 0 B | 45.7505 ns / 120 B | 28.1108 ns / 72 B | -| Empty subscribe | 2.6805 ns / 40 B | 40.1536 ns / 96 B | 25.8399 ns / 48 B | -| Range subscribe | 46.0466 ns / 96 B | 2,452.1549 ns / 2,472 B | 63.6342 ns / 72 B | -| Repeat subscribe | 6.7455 ns / 0 B | 2,275.7650 ns / 2,408 B | 64.8692 ns / 72 B | -| Throw subscribe | 54.9685 ns / 120 B | 106.8594 ns / 240 B | 85.8239 ns / 192 B | -| FromEnumerable subscribe | 49.3764 ns / 40 B | 2,171.2470 ns / 2,504 B | 70.3934 ns / 80 B | -| Completed task bridge | 8.7892 ns / 88 B | 769.1087 ns / 793 B | N/A | -| Create subscribe | 43.7376 ns / 248 B | N/A | N/A | -| CreateSafe subscribe | 44.1792 ns / 248 B | N/A | N/A | -| Defer subscribe | 66.0839 ns / 240 B | N/A | N/A | -| Start subscribe | 51.7062 ns / 376 B | N/A | N/A | -| Unfold subscribe | 167.5562 ns / 736 B | N/A | N/A | -| Use subscribe | 67.7116 ns / 432 B | N/A | N/A | -| FromAsyncEnumerable subscribe | 1,921.1208 ns / 2,052 B | N/A | N/A | -| Never subscribe/dispose | 0.2163 ns / 0 B | N/A | N/A | -| Map + Keep over range | 130.0149 ns / 208 B | 2,461.1312 ns / 2,616 B | 256.9470 ns / 264 B | -| Aggregate + Any + Count | 229.9964 ns / 992 B | 5,073.0502 ns / 5,896 B | 529.5892 ns / 1,280 B | -| StartWith + Append + DefaultIfEmpty | 45.2433 ns / 184 B | 869.3671 ns / 1,257 B | 128.0685 ns / 280 B | -| SelectMany over ranges | 939.2041 ns / 712 B | 3,357.9581 ns / 3,872 B | 965.1100 ns / 1,032 B | -| Zip over ranges | 38.8399 ns / 232 B | 2,903.8925 ns / 2,976 B | 658.2362 ns / 648 B | -| Concat ranges | 67.0788 ns / 256 B | N/A | N/A | -| Merge ranges | 66.9464 ns / 256 B | N/A | N/A | -| Race ranges | 37.7646 ns / 192 B | N/A | N/A | -| Switch ranges | 797.3144 ns / 1,376 B | N/A | N/A | -| CombineLatest ranges | 95.0119 ns / 504 B | N/A | N/A | -| WithLatest ranges | 104.8306 ns / 504 B | N/A | N/A | -| ForkJoin ranges | 67.7277 ns / 480 B | N/A | N/A | -| Delay range | 3,132.9781 ns / 38,816 B | N/A | N/A | -| DelayStart range | 874.3075 ns / 25,520 B | N/A | N/A | -| Throttle burst | 2,504.3725 ns / 38,384 B | N/A | N/A | -| Sample latest | 1,045.5780 ns / 26,072 B | N/A | N/A | -| Timestamp range | 394.0507 ns / 312 B | N/A | N/A | -| TimeInterval range | 478.5383 ns / 736 B | N/A | N/A | -| Timeout never | 976.3098 ns / 25,816 B | N/A | N/A | -| ObserveOn immediate | 21.8563 ns / 96 B | N/A | N/A | -| Replay subscribe | 324.2733 ns / 320 B | 665.7033 ns / 696 B | N/A | -| BehaviorSignal 32 values | 554.5077 ns / 176 B | 581.5846 ns / 200 B | 594.3121 ns / 184 B | -| BehaviorSignal 1024 values | 15,698.1415 ns / 176 B | 15,826.6246 ns / 200 B | 15,702.7802 ns / 184 B | -| Signal emit, 32 values | 65.8803 ns / 136 B | 90.0502 ns / 136 B | 116.1177 ns / 152 B | -| Signal emit, 1024 values | 1,650.9938 ns / 136 B | 1,676.8777 ns / 136 B | 1,984.4349 ns / 152 B | -| Signal subscribe/dispose, 8 observers | 240.6380 ns / 592 B | 284.9100 ns / 1,288 B | 450.5067 ns / 840 B | -| Signal subscribe/dispose, 64 observers | 2,599.1562 ns / 3,800 B | 3,600.1331 ns / 38,472 B | 3,401.7292 ns / 6,216 B | -| Publish live connect | 125.5809 ns / 384 B | N/A | N/A | -| Share live subscribe | 225.6140 ns / 848 B | N/A | N/A | -| Replay live late subscribe | 595.9243 ns / 568 B | N/A | N/A | -| RefCount subscribe | 222.4420 ns / 848 B | N/A | N/A | -| AutoConnect subscribe | 167.9847 ns / 728 B | N/A | N/A | -| StateSignal updates | 552.4185 ns / 176 B | N/A | N/A | -| ReadOnlyState projection | 123.5420 ns / 248 B | N/A | N/A | -| TaskSignal subscribe | 2,384.2121 ns / 3,875 B | N/A | N/A | -| Command execute | 114.0456 ns / 600 B | N/A | N/A | -| Command result subscribe | 137.9245 ns / 672 B | N/A | N/A | -| CollectList range | 115.9544 ns / 688 B | N/A | N/A | -| CollectArray range | 83.1385 ns / 656 B | N/A | N/A | -| CollectArrayAsync range | 33.8094 ns / 384 B | N/A | N/A | -| FirstAsync range | 5.9320 ns / 56 B | N/A | N/A | -| ToTask range | 13.9715 ns / 192 B | N/A | N/A | -| Count(predicate) range | 55.0441 ns / 144 B | N/A | N/A | -| All + Contains range | 204.1768 ns / 1,024 B | N/A | N/A | -| Pocket dispose | 60.2873 ns / 408 B | 93.1830 ns / 512 B | N/A | -| CurrentThread schedule | 12.4912 ns / 88 B | 14.7694 ns / 88 B | N/A | -| Safe witness | 21.7079 ns / 168 B | N/A | N/A | -| Completed Spark | 0.0006 ns / 0 B | N/A | N/A | +| Return subscribe | 0.2924 ns / 0 B | 57.3971 ns / 120 B | 36.4736 ns / 80 B | +| Empty subscribe | 2.6250 ns / 40 B | 49.4400 ns / 96 B | 31.7660 ns / 56 B | +| Range subscribe | 53.4814 ns / 96 B | 2,661.3499 ns / 2,472 B | 74.1203 ns / 80 B | +| Repeat subscribe | 7.4801 ns / 0 B | 2,561.7627 ns / 2,408 B | 72.9776 ns / 80 B | +| Throw subscribe | 65.5047 ns / 120 B | 121.8558 ns / 240 B | 101.9381 ns / 200 B | +| FromEnumerable subscribe | 53.8412 ns / 40 B | 2,463.3475 ns / 2,504 B | 79.5410 ns / 88 B | +| Completed task bridge | 14.7929 ns / 88 B | 1,069.5220 ns / 793 B | 42.9666 ns / 88 B | +| Create subscribe | 71.6670 ns / 248 B | 50.1209 ns / 168 B | 64.3239 ns / 128 B | +| CreateSafe subscribe | 58.6071 ns / 248 B | 50.7736 ns / 168 B | 64.0586 ns / 128 B | +| Defer subscribe | 80.2183 ns / 240 B | 1,494.6925 ns / 1,512 B | 120.5173 ns / 152 B | +| Start subscribe | 68.4794 ns / 376 B | 884.6336 ns / 751 B | 67.0051 ns / 160 B | +| Unfold subscribe | 205.0502 ns / 736 B | 2,413.8472 ns / 2,768 B | 106.4548 ns / 128 B | +| Use subscribe | 78.5460 ns / 432 B | 90.4731 ns / 168 B | 62.4567 ns / 128 B | +| FromAsyncEnumerable subscribe | 2,572.9176 ns / 2,065 B | 2,042.2704 ns / 2,445 B | 1,375.5498 ns / 1,023 B | +| Never subscribe/dispose | 0.2897 ns / 0 B | 5.9221 ns / 40 B | 21.0021 ns / 56 B | +| Map + Keep over range | 144.9399 ns / 208 B | 2,858.9877 ns / 2,616 B | 306.8607 ns / 272 B | +| Aggregate + Any + Count | 269.0177 ns / 992 B | 5,698.2445 ns / 5,896 B | 625.4828 ns / 1,280 B | +| StartWith + Append + DefaultIfEmpty | 56.9262 ns / 184 B | 1,055.3664 ns / 1,257 B | 157.0466 ns / 288 B | +| SelectMany over ranges | 1,054.3650 ns / 712 B | 3,870.5592 ns / 3,872 B | 1,152.8382 ns / 1,040 B | +| Zip over ranges | 45.4963 ns / 232 B | 3,656.1999 ns / 2,976 B | 747.8750 ns / 656 B | +| Concat ranges | 77.7447 ns / 256 B | 2,961.9928 ns / 2,856 B | 277.4494 ns / 360 B | +| Merge ranges | 81.1404 ns / 256 B | 4,211.2274 ns / 3,952 B | 722.6829 ns / 352 B | +| Race ranges | 42.3265 ns / 192 B | 1,643.5506 ns / 1,760 B | 295.8642 ns / 360 B | +| Switch ranges | 931.0946 ns / 1,376 B | 2,259.5812 ns / 2,336 B | 789.2157 ns / 392 B | +| CombineLatest ranges | 118.2750 ns / 504 B | 3,395.1528 ns / 2,824 B | 714.2596 ns / 344 B | +| WithLatest ranges | 112.4741 ns / 504 B | 3,630.0810 ns / 2,824 B | 409.9192 ns / 248 B | +| ForkJoin ranges | 82.2519 ns / 480 B | 3,726.2327 ns / 3,136 B | 1,016.8916 ns / 504 B | +| Delay range | 3,509.9035 ns / 38,816 B | 6,600.1541 ns / 39,584 B | 2,118.7826 ns / 2,200 B | +| DelayStart range | 1,029.2875 ns / 25,520 B | 2,440.2833 ns / 26,456 B | 349.4229 ns / 552 B | +| Throttle burst | 3,351.9829 ns / 38,384 B | 2,843.6348 ns / 36,480 B | 1,729.4249 ns / 1,512 B | +| Sample latest | 1,192.1561 ns / 26,072 B | 2,187.4022 ns / 26,264 B | 388.9601 ns / 664 B | +| Timestamp range | 452.1057 ns / 312 B | 1,870.3018 ns / 1,608 B | 373.1253 ns / 152 B | +| TimeInterval range | 542.5902 ns / 736 B | 1,853.5918 ns / 1,712 B | 479.2913 ns / 160 B | +| Timeout idle | 1,157.1993 ns / 25,912 B | 1,463.0736 ns / 29,776 B | 486.4943 ns / 784 B | +| ObserveOn immediate | 27.0202 ns / 96 B | 18,308.8684 ns / 11,309 B | 998.1707 ns / 432 B | +| Replay subscribe | 369.7292 ns / 320 B | 766.3089 ns / 696 B | 461.5158 ns / 688 B | +| BehaviorSignal 32 values | 613.2438 ns / 176 B | 618.4676 ns / 200 B | 669.5095 ns / 192 B | +| BehaviorSignal 1024 values | 17,167.4906 ns / 176 B | 16,983.8969 ns / 200 B | 16,967.7836 ns / 192 B | +| Signal emit, 32 values | 79.8202 ns / 136 B | 105.0434 ns / 136 B | 168.7808 ns / 160 B | +| Signal emit, 1024 values | 2,512.2955 ns / 136 B | 1,973.1489 ns / 136 B | 2,166.3308 ns / 160 B | +| Signal subscribe/dispose, 8 observers | 271.5037 ns / 592 B | 340.2902 ns / 1,288 B | 522.6329 ns / 840 B | +| Signal subscribe/dispose, 64 observers | 3,011.0357 ns / 3,800 B | 4,617.4255 ns / 38,472 B | 4,091.2084 ns / 6,216 B | +| Publish live connect | 173.3073 ns / 384 B | 3,236.7963 ns / 2,696 B | 561.8089 ns / 368 B | +| Share live subscribe | 276.6871 ns / 848 B | 3,461.6713 ns / 2,880 B | 548.8631 ns / 488 B | +| Replay live late subscribe | 712.4733 ns / 568 B | 4,472.9059 ns / 3,408 B | 1,132.5459 ns / 1,360 B | +| RefCount subscribe | 280.1471 ns / 848 B | 3,155.4108 ns / 2,880 B | 529.4785 ns / 488 B | +| AutoConnect subscribe | 245.5960 ns / 728 B | 3,474.4862 ns / 2,736 B | 466.0376 ns / 368 B | +| StateSignal updates | 609.7935 ns / 176 B | 609.6959 ns / 200 B | 662.1553 ns / 192 B | +| ReadOnlyState projection | 150.0491 ns / 248 B | 106.9365 ns / 328 B | 199.0058 ns / 312 B | +| TaskSignal subscribe | 6,126.1940 ns / 3,853 B | 874.2303 ns / 886 B | 47.2969 ns / 160 B | +| Command execute | 144.2864 ns / 600 B | 809.1453 ns / 1,089 B | 123.6877 ns / 296 B | +| Command result subscribe | 184.0972 ns / 672 B | 46.0881 ns / 136 B | 76.0079 ns / 160 B | +| CollectList range | 138.5013 ns / 688 B | 2,916.3119 ns / 3,488 B | 189.9245 ns / 632 B | +| CollectArray range | 114.7686 ns / 656 B | 4,302.7507 ns / 3,640 B | 231.4877 ns / 784 B | +| CollectArrayAsync range | 37.9519 ns / 384 B | 3,070.6184 ns / 3,984 B | 189.4382 ns / 784 B | +| FirstAsync range | 7.4257 ns / 56 B | 2,679.5795 ns / 2,792 B | 89.9454 ns / 208 B | +| ToTask range | 16.8438 ns / 192 B | 2,778.4730 ns / 2,824 B | 104.4256 ns / 208 B | +| Count(predicate) range | 64.3218 ns / 144 B | 2,804.5286 ns / 2,520 B | 111.1174 ns / 200 B | +| All + Contains range | 250.2919 ns / 1,024 B | 5,723.2875 ns / 5,816 B | 257.2693 ns / 392 B | +| Pocket dispose | 85.2006 ns / 408 B | 107.9840 ns / 512 B | 102.8140 ns / 480 B | +| CurrentThread schedule | 15.6813 ns / 88 B | 18.6513 ns / 88 B | 34.4560 ns / 56 B | +| Safe witness | 32.4502 ns / 168 B | 16.2257 ns / 136 B | 22.0434 ns / 56 B | +| Completed Spark | 0.0000 ns / 0 B | 0.0000 ns / 0 B | 0.2869 ns / 0 B | Performance constraints used by the project: diff --git a/src/Directory.Packages.props b/src/Directory.Packages.props index 195198c..3a7cbc6 100644 --- a/src/Directory.Packages.props +++ b/src/Directory.Packages.props @@ -16,6 +16,7 @@ + diff --git a/src/benchmarks/ReactiveUI.Primitives.Benchmarks/AsyncBridgeBenchmarks.cs b/src/benchmarks/ReactiveUI.Primitives.Benchmarks/AsyncBridgeBenchmarks.cs index d88b635..03e06f9 100644 --- a/src/benchmarks/ReactiveUI.Primitives.Benchmarks/AsyncBridgeBenchmarks.cs +++ b/src/benchmarks/ReactiveUI.Primitives.Benchmarks/AsyncBridgeBenchmarks.cs @@ -43,4 +43,16 @@ public int SystemReactiveCompletedTaskBridge() using var subscription = RxObservable.FromAsync(() => CompletedTask).Subscribe(observer); return observer.LastValue; } + + /// + /// Completed task conversion in R3. + /// + /// The emitted value. + [Benchmark] + public int R3CompletedTaskBridge() + { + var observer = new IntR3Observer(); + using var subscription = R3.Observable.ToObservable(CompletedTask, configureAwait: false).Subscribe(observer); + return observer.LastValue; + } } diff --git a/src/benchmarks/ReactiveUI.Primitives.Benchmarks/BenchmarkObservers.cs b/src/benchmarks/ReactiveUI.Primitives.Benchmarks/BenchmarkObservers.cs index 0bf8563..94c729d 100644 --- a/src/benchmarks/ReactiveUI.Primitives.Benchmarks/BenchmarkObservers.cs +++ b/src/benchmarks/ReactiveUI.Primitives.Benchmarks/BenchmarkObservers.cs @@ -73,6 +73,11 @@ internal sealed class IntR3Observer : Observer /// public int NextCount { get; private set; } + /// + /// Gets the last value observed. + /// + public int LastValue { get; private set; } + /// /// Gets the number of terminal completions observed. /// @@ -91,6 +96,7 @@ protected override void OnNextCore(int value) { NextCount++; Total += value; + LastValue = value; } /// @@ -108,6 +114,67 @@ protected override void OnErrorResumeCore(Exception error) /// The completion result. protected override void OnCompletedCore(Result result) { + if (result.IsFailure) + { + ErrorCount++; + return; + } + + CompletionCount++; + } +} + +/// +/// Observer used by R3 benchmark cases that only need an item count. +/// +/// The observed value type. +internal sealed class CountingR3Observer : Observer +{ + /// + /// Gets the number of onNext calls. + /// + public int Count { get; private set; } + + /// + /// Gets the number of terminal completions observed. + /// + public int CompletionCount { get; private set; } + + /// + /// Gets the number of errors observed. + /// + public int ErrorCount { get; private set; } + + /// + /// Called for each emitted value. + /// + /// The emitted value. + protected override void OnNextCore(T value) + { + Count++; + } + + /// + /// Called when an error is observed. + /// + /// The observed exception. + protected override void OnErrorResumeCore(Exception error) + { + ErrorCount++; + } + + /// + /// Called when sequence completed. + /// + /// The completion result. + protected override void OnCompletedCore(Result result) + { + if (result.IsFailure) + { + ErrorCount++; + return; + } + CompletionCount++; } } diff --git a/src/benchmarks/ReactiveUI.Primitives.Benchmarks/ConnectableShareBenchmarks.cs b/src/benchmarks/ReactiveUI.Primitives.Benchmarks/ConnectableShareBenchmarks.cs index d754026..ab8c148 100644 --- a/src/benchmarks/ReactiveUI.Primitives.Benchmarks/ConnectableShareBenchmarks.cs +++ b/src/benchmarks/ReactiveUI.Primitives.Benchmarks/ConnectableShareBenchmarks.cs @@ -6,6 +6,8 @@ using ReactiveUI.Primitives; using ReactiveUI.Primitives.Signals; +using RxObservable = System.Reactive.Linq.Observable; + namespace ReactiveUI.Primitives.Benchmarks; /// @@ -30,6 +32,34 @@ public int PrimitivesPublishLiveConnect() return observer.Total; } + /// + /// Benchmarks publish connection using System.Reactive. + /// + /// The observed total. + [Benchmark] + public int SystemReactivePublishLiveConnect() + { + var observer = new IntSignalObserver(); + var connectable = RxObservable.Publish(RxObservable.Range(1, Count)); + using var subscription = connectable.Subscribe(observer); + using var connection = connectable.Connect(); + return observer.Total; + } + + /// + /// Benchmarks publish connection using R3. + /// + /// The observed total. + [Benchmark] + public int R3PublishLiveConnect() + { + var observer = new IntR3Observer(); + var connectable = R3.ObservableExtensions.Publish(R3.Observable.Range(1, Count)); + using var subscription = connectable.Subscribe(observer); + using var connection = connectable.Connect(); + return observer.Total; + } + /// /// Benchmarks share-live reference counting. /// @@ -42,6 +72,31 @@ public int PrimitivesShareLiveSubscribe() return observer.Total; } + /// + /// Benchmarks share/reference counting using System.Reactive publish-refcount. + /// + /// The observed total. + [Benchmark] + public int SystemReactiveShareLiveSubscribe() + { + var observer = new IntSignalObserver(); + using var subscription = RxObservable.RefCount(RxObservable.Publish(RxObservable.Range(1, Count))) + .Subscribe(observer); + return observer.Total; + } + + /// + /// Benchmarks share/reference counting using R3. + /// + /// The observed total. + [Benchmark] + public int R3ShareLiveSubscribe() + { + var observer = new IntR3Observer(); + using var subscription = R3.ObservableExtensions.Share(R3.Observable.Range(1, Count)).Subscribe(observer); + return observer.Total; + } + /// /// Benchmarks replay-live late subscription. /// @@ -56,6 +111,34 @@ public int PrimitivesReplayLiveLateSubscribe() return observer.Total; } + /// + /// Benchmarks replay late subscription using System.Reactive. + /// + /// The observed total. + [Benchmark] + public int SystemReactiveReplayLiveLateSubscribe() + { + var observer = new IntSignalObserver(); + var connectable = RxObservable.Replay(RxObservable.Range(1, Count), Count); + using var connection = connectable.Connect(); + using var subscription = connectable.Subscribe(observer); + return observer.Total; + } + + /// + /// Benchmarks replay late subscription using R3. + /// + /// The observed total. + [Benchmark] + public int R3ReplayLiveLateSubscribe() + { + var observer = new IntR3Observer(); + var connectable = R3.ObservableExtensions.Replay(R3.Observable.Range(1, Count), Count); + using var connection = connectable.Connect(); + using var subscription = connectable.Subscribe(observer); + return observer.Total; + } + /// /// Benchmarks ref-count subscription. /// @@ -68,6 +151,33 @@ public int PrimitivesRefCountSubscribe() return observer.Total; } + /// + /// Benchmarks ref-count subscription using System.Reactive. + /// + /// The observed total. + [Benchmark] + public int SystemReactiveRefCountSubscribe() + { + var observer = new IntSignalObserver(); + using var subscription = RxObservable.RefCount(RxObservable.Publish(RxObservable.Range(1, Count))) + .Subscribe(observer); + return observer.Total; + } + + /// + /// Benchmarks ref-count subscription using R3. + /// + /// The observed total. + [Benchmark] + public int R3RefCountSubscribe() + { + var observer = new IntR3Observer(); + using var subscription = R3.ObservableExtensions.RefCount( + R3.ObservableExtensions.Publish(R3.Observable.Range(1, Count))) + .Subscribe(observer); + return observer.Total; + } + /// /// Benchmarks auto-connect subscription. /// @@ -79,4 +189,31 @@ public int PrimitivesAutoConnectSubscribe() using var subscription = Signal.Range(1, Count).PublishLive().AutoConnect().Subscribe(observer); return observer.Total; } + + /// + /// Benchmarks auto-connect subscription using System.Reactive. + /// + /// The observed total. + [Benchmark] + public int SystemReactiveAutoConnectSubscribe() + { + var observer = new IntSignalObserver(); + using var subscription = RxObservable.AutoConnect(RxObservable.Publish(RxObservable.Range(1, Count))) + .Subscribe(observer); + return observer.Total; + } + + /// + /// Benchmarks auto-connect-equivalent subscription using R3 publish/connect. + /// + /// The observed total. + [Benchmark] + public int R3AutoConnectSubscribe() + { + var observer = new IntR3Observer(); + var connectable = R3.ObservableExtensions.Publish(R3.Observable.Range(1, Count)); + using var subscription = connectable.Subscribe(observer); + using var connection = connectable.Connect(); + return observer.Total; + } } diff --git a/src/benchmarks/ReactiveUI.Primitives.Benchmarks/CoreRuntimeBenchmarks.cs b/src/benchmarks/ReactiveUI.Primitives.Benchmarks/CoreRuntimeBenchmarks.cs index 6787031..19beaf9 100644 --- a/src/benchmarks/ReactiveUI.Primitives.Benchmarks/CoreRuntimeBenchmarks.cs +++ b/src/benchmarks/ReactiveUI.Primitives.Benchmarks/CoreRuntimeBenchmarks.cs @@ -56,6 +56,23 @@ public int SystemReactiveCompositeDispose() return disposed; } + /// + /// Composite disposable dispose path in R3. + /// + /// The number of disposal callbacks executed. + [Benchmark] + public int R3CompositeDispose() + { + var disposed = 0; + var pocket = new R3.CompositeDisposable( + R3.Disposable.Create(() => disposed++), + R3.Disposable.Create(() => disposed++), + R3.Disposable.Create(() => disposed++)); + + pocket.Dispose(); + return disposed; + } + /// /// Schedule and execute one action on current-thread sequencer. /// @@ -80,6 +97,18 @@ public int SystemReactiveCurrentThreadSchedule() return value; } + /// + /// Immediate dispatch through R3 return subscription. + /// + /// The executed marker value. + [Benchmark] + public int R3CurrentThreadSchedule() + { + var observer = new IntR3Observer(); + using var subscription = R3.Observable.Return(1).Subscribe(observer); + return observer.LastValue; + } + /// /// Wrap a witness with the safe witness helper. /// @@ -94,6 +123,33 @@ public int PrimitivesSafeWitness() return value; } + /// + /// Notify a System.Reactive observer created from delegates. + /// + /// The forwarded value. + [Benchmark] + public int SystemReactiveSafeWitness() + { + var value = 0; + var observer = System.Reactive.Observer.Create(x => value = x, _ => { }, () => { }); + observer.OnNext(42); + observer.OnCompleted(); + return value; + } + + /// + /// Notify an R3 observer. + /// + /// The forwarded value. + [Benchmark] + public int R3SafeWitness() + { + var observer = new IntR3Observer(); + observer.OnNext(42); + observer.OnCompleted(R3.Result.Success); + return observer.LastValue; + } + /// /// Allocating a completed spark should remain allocation efficient. /// @@ -104,4 +160,26 @@ public int PrimitivesCompletedSpark() var spark = Spark.CreateOnCompleted(); return (int)spark.Kind; } + + /// + /// Allocating a completed notification with System.Reactive. + /// + /// An integer marker extracted from kind. + [Benchmark] + public int SystemReactiveCompletedSpark() + { + var notification = System.Reactive.Notification.CreateOnCompleted(); + return (int)notification.Kind; + } + + /// + /// Allocating a completed notification with R3. + /// + /// An integer marker extracted from kind. + [Benchmark] + public int R3CompletedSpark() + { + var notification = new R3.Notification(R3.Result.Success); + return (int)notification.Kind; + } } diff --git a/src/benchmarks/ReactiveUI.Primitives.Benchmarks/FactoryAdapterExpansionBenchmarks.cs b/src/benchmarks/ReactiveUI.Primitives.Benchmarks/FactoryAdapterExpansionBenchmarks.cs index cbf5e51..2bd8107 100644 --- a/src/benchmarks/ReactiveUI.Primitives.Benchmarks/FactoryAdapterExpansionBenchmarks.cs +++ b/src/benchmarks/ReactiveUI.Primitives.Benchmarks/FactoryAdapterExpansionBenchmarks.cs @@ -3,11 +3,18 @@ // See the LICENSE file in the project root for full license information. using System.Collections.Generic; +using System.Reactive.Concurrency; +using System.Reactive.Linq; +using System.Reactive.Threading.Tasks; +using System.Threading; using BenchmarkDotNet.Attributes; using ReactiveUI.Primitives.Concurrency; using ReactiveUI.Primitives.Disposables; using ReactiveUI.Primitives.Signals; +using RxDisposable = System.Reactive.Disposables.Disposable; +using RxObservable = System.Reactive.Linq.Observable; + namespace ReactiveUI.Primitives.Benchmarks; /// @@ -36,6 +43,40 @@ public int PrimitivesCreateSubscribe() return observer.Total; } + /// + /// Benchmarks a custom create observable using System.Reactive. + /// + /// The observed total. + [Benchmark] + public int SystemReactiveCreateSubscribe() + { + var observer = new IntSignalObserver(); + using var subscription = RxObservable.Create(target => + { + target.OnNext(Value); + target.OnCompleted(); + return RxDisposable.Empty; + }).Subscribe(observer); + return observer.Total; + } + + /// + /// Benchmarks a custom create observable using R3. + /// + /// The observed total. + [Benchmark] + public int R3CreateSubscribe() + { + var observer = new IntR3Observer(); + using var subscription = R3.Observable.Create(static (R3.Observer target) => + { + target.OnNext(Value); + target.OnCompleted(R3.Result.Success); + return R3.Disposable.Empty; + }).Subscribe(observer); + return observer.Total; + } + /// /// Benchmarks a custom safe-create signal. /// @@ -53,6 +94,40 @@ public int PrimitivesCreateSafeSubscribe() return observer.Total; } + /// + /// Benchmarks the safe custom create path using System.Reactive create semantics. + /// + /// The observed total. + [Benchmark] + public int SystemReactiveCreateSafeSubscribe() + { + var observer = new IntSignalObserver(); + using var subscription = RxObservable.Create(target => + { + target.OnNext(Value); + target.OnCompleted(); + return RxDisposable.Empty; + }).Subscribe(observer); + return observer.Total; + } + + /// + /// Benchmarks the safe custom create path using R3 create semantics. + /// + /// The observed total. + [Benchmark] + public int R3CreateSafeSubscribe() + { + var observer = new IntR3Observer(); + using var subscription = R3.Observable.Create(static (R3.Observer target) => + { + target.OnNext(Value); + target.OnCompleted(R3.Result.Success); + return R3.Disposable.Empty; + }).Subscribe(observer); + return observer.Total; + } + /// /// Benchmarks deferred factory creation. /// @@ -65,6 +140,30 @@ public int PrimitivesDeferSubscribe() return observer.Total; } + /// + /// Benchmarks deferred factory creation using System.Reactive. + /// + /// The observed total. + [Benchmark] + public int SystemReactiveDeferSubscribe() + { + var observer = new IntSignalObserver(); + using var subscription = RxObservable.Defer(static () => RxObservable.Range(1, Count)).Subscribe(observer); + return observer.Total; + } + + /// + /// Benchmarks deferred factory creation using R3. + /// + /// The observed total. + [Benchmark] + public int R3DeferSubscribe() + { + var observer = new IntR3Observer(); + using var subscription = R3.Observable.Defer(static () => R3.Observable.Range(1, Count)).Subscribe(observer); + return observer.Total; + } + /// /// Benchmarks starting work on the immediate scheduler. /// @@ -77,6 +176,31 @@ public int PrimitivesStartSubscribe() return observer.Total; } + /// + /// Benchmarks starting work on the immediate scheduler using System.Reactive. + /// + /// The observed total. + [Benchmark] + public int SystemReactiveStartSubscribe() + { + var observer = new IntSignalObserver(); + using var subscription = RxObservable.Start(static () => Value, ImmediateScheduler.Instance).Subscribe(observer); + return observer.Total; + } + + /// + /// Benchmarks starting completed work using R3 async factory semantics. + /// + /// The observed total. + [Benchmark] + public int R3StartSubscribe() + { + var observer = new IntR3Observer(); + using var subscription = R3.Observable.FromAsync(static _ => new ValueTask(Value), configureAwait: false) + .Subscribe(observer); + return observer.Total; + } + /// /// Benchmarks finite unfold generation. /// @@ -90,6 +214,44 @@ public int PrimitivesUnfoldSubscribe() return observer.Total; } + /// + /// Benchmarks finite generation using System.Reactive. + /// + /// The observed total. + [Benchmark] + public int SystemReactiveUnfoldSubscribe() + { + var observer = new IntSignalObserver(); + using var subscription = RxObservable.Generate( + 0, + static state => state < Count, + static state => state + 1, + static state => state) + .Subscribe(observer); + return observer.Total; + } + + /// + /// Benchmarks finite generation using an R3 create loop. + /// + /// The observed total. + [Benchmark] + public int R3UnfoldSubscribe() + { + var observer = new IntR3Observer(); + using var subscription = R3.Observable.Create(static (R3.Observer target) => + { + for (var state = 0; state < Count; state++) + { + target.OnNext(state); + } + + target.OnCompleted(R3.Result.Success); + return R3.Disposable.Empty; + }).Subscribe(observer); + return observer.Total; + } + /// /// Benchmarks resource-scoped signal creation. /// @@ -102,6 +264,37 @@ public int PrimitivesUseSubscribe() return observer.Total; } + /// + /// Benchmarks resource-scoped observable creation using System.Reactive. + /// + /// The observed total. + [Benchmark] + public int SystemReactiveUseSubscribe() + { + var observer = new IntSignalObserver(); + using var subscription = RxObservable.Using(static () => RxDisposable.Empty, static _ => RxObservable.Return(Value)) + .Subscribe(observer); + return observer.Total; + } + + /// + /// Benchmarks resource-scoped observable creation using R3. + /// + /// The observed total. + [Benchmark] + public int R3UseSubscribe() + { + var observer = new IntR3Observer(); + using var subscription = R3.Observable.Create(static (R3.Observer target) => + { + using var resource = R3.Disposable.Empty; + target.OnNext(Value); + target.OnCompleted(R3.Result.Success); + return R3.Disposable.Empty; + }).Subscribe(observer); + return observer.Total; + } + /// /// Benchmarks async-enumerable adaptation. /// @@ -112,6 +305,41 @@ public async Task PrimitivesFromAsyncEnumerableSubscribeAsync() return (await Signal.FromAsyncEnumerable(ValuesAsync()).CollectArrayAsync().ConfigureAwait(false)).Length; } + /// + /// Benchmarks async-enumerable adaptation using System.Reactive create semantics. + /// + /// The number of values collected. + [Benchmark] + public async Task SystemReactiveFromAsyncEnumerableSubscribeAsync() + { + var values = await RxObservable.Create(static async (target, cancellationToken) => + { + await foreach (var value in ValuesAsync().WithCancellation(cancellationToken).ConfigureAwait(false)) + { + target.OnNext(value); + } + + target.OnCompleted(); + }) + .ToArray() + .ToTask() + .ConfigureAwait(false); + return values.Length; + } + + /// + /// Benchmarks async-enumerable adaptation using R3. + /// + /// The number of values collected. + [Benchmark] + public async Task R3FromAsyncEnumerableSubscribeAsync() + { + return (await R3.ObservableExtensions.ToArrayAsync( + R3.Observable.ToObservable(ValuesAsync()), + CancellationToken.None) + .ConfigureAwait(false)).Length; + } + /// /// Benchmarks subscribing and disposing a never-ending signal. /// @@ -124,6 +352,30 @@ public int PrimitivesNeverSubscribeDispose() return observer.NextCount + observer.CompletionCount + observer.ErrorCount; } + /// + /// Benchmarks subscribing and disposing a never-ending System.Reactive observable. + /// + /// The observed notification count. + [Benchmark] + public int SystemReactiveNeverSubscribeDispose() + { + var observer = new IntSignalObserver(); + using var subscription = RxObservable.Never().Subscribe(observer); + return observer.NextCount + observer.CompletionCount + observer.ErrorCount; + } + + /// + /// Benchmarks subscribing and disposing a never-ending R3 observable. + /// + /// The observed notification count. + [Benchmark] + public int R3NeverSubscribeDispose() + { + var observer = new IntR3Observer(); + using var subscription = R3.Observable.Never().Subscribe(observer); + return observer.NextCount + observer.CompletionCount + observer.ErrorCount; + } + private static async IAsyncEnumerable ValuesAsync() { await Task.Yield(); diff --git a/src/benchmarks/ReactiveUI.Primitives.Benchmarks/OperatorHigherOrderBenchmarks.cs b/src/benchmarks/ReactiveUI.Primitives.Benchmarks/OperatorHigherOrderBenchmarks.cs index 4b9399a..adee584 100644 --- a/src/benchmarks/ReactiveUI.Primitives.Benchmarks/OperatorHigherOrderBenchmarks.cs +++ b/src/benchmarks/ReactiveUI.Primitives.Benchmarks/OperatorHigherOrderBenchmarks.cs @@ -5,6 +5,9 @@ using BenchmarkDotNet.Attributes; using ReactiveUI.Primitives; using ReactiveUI.Primitives.Signals; +using System.Threading; + +using RxObservable = System.Reactive.Linq.Observable; namespace ReactiveUI.Primitives.Benchmarks; @@ -28,6 +31,32 @@ public int PrimitivesConcatRanges() return observer.Total; } + /// + /// Benchmarks concatenating ranges using System.Reactive. + /// + /// The observed total. + [Benchmark] + public int SystemReactiveConcatRanges() + { + var observer = new IntSignalObserver(); + using var subscription = RxObservable.Concat(RxObservable.Range(1, Count), RxObservable.Range(1, Count)) + .Subscribe(observer); + return observer.Total; + } + + /// + /// Benchmarks concatenating ranges using R3. + /// + /// The observed total. + [Benchmark] + public int R3ConcatRanges() + { + var observer = new IntR3Observer(); + using var subscription = R3.Observable.Concat(R3.Observable.Range(1, Count), R3.Observable.Range(1, Count)) + .Subscribe(observer); + return observer.Total; + } + /// /// Benchmarks merging inner ranges. /// @@ -40,6 +69,32 @@ public int PrimitivesMergeRanges() return observer.Total; } + /// + /// Benchmarks merging ranges using System.Reactive. + /// + /// The observed total. + [Benchmark] + public int SystemReactiveMergeRanges() + { + var observer = new IntSignalObserver(); + using var subscription = RxObservable.Merge(RxObservable.Range(1, Count), RxObservable.Range(1, Count)) + .Subscribe(observer); + return observer.Total; + } + + /// + /// Benchmarks merging ranges using R3. + /// + /// The observed total. + [Benchmark] + public int R3MergeRanges() + { + var observer = new IntR3Observer(); + using var subscription = R3.Observable.Merge(R3.Observable.Range(1, Count), R3.Observable.Range(1, Count)) + .Subscribe(observer); + return observer.Total; + } + /// /// Benchmarks racing two sources. /// @@ -52,6 +107,32 @@ public int PrimitivesRaceRanges() return observer.Total; } + /// + /// Benchmarks racing two sources using System.Reactive. + /// + /// The observed total. + [Benchmark] + public int SystemReactiveRaceRanges() + { + var observer = new IntSignalObserver(); + using var subscription = RxObservable.Amb(RxObservable.Range(1, Count), RxObservable.Range(100, Count)) + .Subscribe(observer); + return observer.Total; + } + + /// + /// Benchmarks racing two sources using R3. + /// + /// The observed total. + [Benchmark] + public int R3RaceRanges() + { + var observer = new IntR3Observer(); + using var subscription = R3.Observable.Race(R3.Observable.Range(1, Count), R3.Observable.Range(100, Count)) + .Subscribe(observer); + return observer.Total; + } + /// /// Benchmarks switching to the latest inner source. /// @@ -66,6 +147,36 @@ public int PrimitivesSwitchRanges() return observer.Total; } + /// + /// Benchmarks switching to the latest inner source using System.Reactive. + /// + /// The observed total. + [Benchmark] + public int SystemReactiveSwitchRanges() + { + var observer = new IntSignalObserver(); + using var subscription = RxObservable.Switch( + RxObservable.ToObservable(new[] { RxObservable.Range(1, Count), RxObservable.Range(100, Count) })) + .Subscribe(observer); + return observer.Total; + } + + /// + /// Benchmarks switching to the latest inner source using R3. + /// + /// The observed total. + [Benchmark] + public int R3SwitchRanges() + { + var observer = new IntR3Observer(); + using var subscription = R3.ObservableExtensions.Switch( + R3.Observable.ToObservable( + new[] { R3.Observable.Range(1, Count), R3.Observable.Range(100, Count) }, + CancellationToken.None)) + .Subscribe(observer); + return observer.Total; + } + /// /// Benchmarks combine-latest over ranges. /// @@ -81,6 +192,36 @@ public int PrimitivesCombineLatestRanges() return observer.Total; } + /// + /// Benchmarks combine-latest over ranges using System.Reactive. + /// + /// The observed total. + [Benchmark] + public int SystemReactiveCombineLatestRanges() + { + var observer = new IntSignalObserver(); + using var subscription = RxObservable.CombineLatest( + RxObservable.Range(1, Count), + RxObservable.Range(10, Count), + static (left, right) => left + right).Subscribe(observer); + return observer.Total; + } + + /// + /// Benchmarks combine-latest over ranges using R3. + /// + /// The observed total. + [Benchmark] + public int R3CombineLatestRanges() + { + var observer = new IntR3Observer(); + using var subscription = R3.Observable.CombineLatest( + R3.Observable.Range(1, Count), + R3.Observable.Range(10, Count), + static (int left, int right) => left + right).Subscribe(observer); + return observer.Total; + } + /// /// Benchmarks with-latest over ranges. /// @@ -95,6 +236,36 @@ public int PrimitivesWithLatestRanges() return observer.Total; } + /// + /// Benchmarks with-latest over ranges using System.Reactive. + /// + /// The observed total. + [Benchmark] + public int SystemReactiveWithLatestRanges() + { + var observer = new IntSignalObserver(); + using var subscription = RxObservable.WithLatestFrom( + RxObservable.Range(1, Count), + RxObservable.Range(10, Count), + static (left, right) => left + right).Subscribe(observer); + return observer.Total; + } + + /// + /// Benchmarks with-latest over ranges using R3. + /// + /// The observed total. + [Benchmark] + public int R3WithLatestRanges() + { + var observer = new IntR3Observer(); + using var subscription = R3.ObservableExtensions.WithLatestFrom( + R3.Observable.Range(1, Count), + R3.Observable.Range(10, Count), + static (int left, int right) => left + right).Subscribe(observer); + return observer.Total; + } + /// /// Benchmarks fork-join over ranges. /// @@ -109,4 +280,40 @@ public int PrimitivesForkJoinRanges() static (left, right) => left + right).Subscribe(observer); return observer.Total; } + + /// + /// Benchmarks fork-join over ranges using System.Reactive. + /// + /// The observed total. + [Benchmark] + public int SystemReactiveForkJoinRanges() + { + var observer = new IntSignalObserver(); + using var subscription = RxObservable.TakeLast( + RxObservable.CombineLatest( + RxObservable.Range(1, Count), + RxObservable.Range(10, Count), + static (left, right) => left + right), + 1) + .Subscribe(observer); + return observer.Total; + } + + /// + /// Benchmarks fork-join-equivalent last combined value over ranges using R3. + /// + /// The observed total. + [Benchmark] + public int R3ForkJoinRanges() + { + var observer = new IntR3Observer(); + using var subscription = R3.ObservableExtensions.TakeLast( + R3.Observable.CombineLatest( + R3.Observable.Range(1, Count), + R3.Observable.Range(10, Count), + static (int left, int right) => left + right), + 1) + .Subscribe(observer); + return observer.Total; + } } diff --git a/src/benchmarks/ReactiveUI.Primitives.Benchmarks/OperatorTimeSchedulerBenchmarks.cs b/src/benchmarks/ReactiveUI.Primitives.Benchmarks/OperatorTimeSchedulerBenchmarks.cs index 5b9e8c4..89547e2 100644 --- a/src/benchmarks/ReactiveUI.Primitives.Benchmarks/OperatorTimeSchedulerBenchmarks.cs +++ b/src/benchmarks/ReactiveUI.Primitives.Benchmarks/OperatorTimeSchedulerBenchmarks.cs @@ -3,9 +3,15 @@ // See the LICENSE file in the project root for full license information. using BenchmarkDotNet.Attributes; +using Microsoft.Extensions.Time.Testing; using ReactiveUI.Primitives; using ReactiveUI.Primitives.Concurrency; using ReactiveUI.Primitives.Signals; +using System.Reactive.Concurrency; +using System.Reactive.Linq; + +using RxObservable = System.Reactive.Linq.Observable; +using RxSubject = System.Reactive.Subjects.Subject; namespace ReactiveUI.Primitives.Benchmarks; @@ -31,6 +37,38 @@ public int PrimitivesDelayRange() return observer.Total; } + /// + /// Benchmarks delayed range delivery using System.Reactive. + /// + /// The observed total. + [Benchmark] + public int SystemReactiveDelayRange() + { + var scheduler = new HistoricalScheduler(); + var observer = new IntSignalObserver(); + using var subscription = RxObservable.Range(1, Count).Delay(TimeSpan.FromTicks(1), scheduler).Subscribe(observer); + scheduler.AdvanceBy(TimeSpan.FromTicks(1)); + return observer.Total; + } + + /// + /// Benchmarks delayed range delivery using R3. + /// + /// The observed total. + [Benchmark] + public int R3DelayRange() + { + var timeProvider = new FakeTimeProvider(); + var observer = new IntR3Observer(); + using var subscription = R3.ObservableExtensions.Delay( + R3.Observable.Range(1, Count), + TimeSpan.FromTicks(1), + timeProvider) + .Subscribe(observer); + timeProvider.Advance(TimeSpan.FromTicks(1)); + return observer.Total; + } + /// /// Benchmarks delayed subscription. /// @@ -45,6 +83,40 @@ public int PrimitivesDelayStartRange() return observer.Total; } + /// + /// Benchmarks delayed subscription using System.Reactive. + /// + /// The observed total. + [Benchmark] + public int SystemReactiveDelayStartRange() + { + var scheduler = new HistoricalScheduler(); + var observer = new IntSignalObserver(); + using var subscription = RxObservable.Range(1, Count) + .DelaySubscription(TimeSpan.FromTicks(1), scheduler) + .Subscribe(observer); + scheduler.AdvanceBy(TimeSpan.FromTicks(1)); + return observer.Total; + } + + /// + /// Benchmarks delayed subscription using R3. + /// + /// The observed total. + [Benchmark] + public int R3DelayStartRange() + { + var timeProvider = new FakeTimeProvider(); + var observer = new IntR3Observer(); + using var subscription = R3.ObservableExtensions.DelaySubscription( + R3.Observable.Range(1, Count), + TimeSpan.FromTicks(1), + timeProvider) + .Subscribe(observer); + timeProvider.Advance(TimeSpan.FromTicks(1)); + return observer.Total; + } + /// /// Benchmarks throttle over a burst. /// @@ -65,6 +137,47 @@ public int PrimitivesThrottleBurst() return observer.LastValue; } + /// + /// Benchmarks throttle over a burst using System.Reactive. + /// + /// The last observed value. + [Benchmark] + public int SystemReactiveThrottleBurst() + { + var scheduler = new HistoricalScheduler(); + var observer = new IntSignalObserver(); + using var source = new RxSubject(); + using var subscription = source.Throttle(TimeSpan.FromTicks(1), scheduler).Subscribe(observer); + for (var i = 0; i < Count; i++) + { + source.OnNext(i); + } + + scheduler.AdvanceBy(TimeSpan.FromTicks(1)); + return observer.LastValue; + } + + /// + /// Benchmarks debounce over a burst using R3. + /// + /// The last observed value. + [Benchmark] + public int R3ThrottleBurst() + { + var timeProvider = new FakeTimeProvider(); + var observer = new IntR3Observer(); + using var source = new R3.Subject(); + using var subscription = R3.ObservableExtensions.Debounce(source, TimeSpan.FromTicks(1), timeProvider) + .Subscribe(observer); + for (var i = 0; i < Count; i++) + { + source.OnNext(i); + } + + timeProvider.Advance(TimeSpan.FromTicks(1)); + return observer.LastValue; + } + /// /// Benchmarks sampling the latest value. /// @@ -81,6 +194,39 @@ public int PrimitivesSampleLatest() return observer.Total; } + /// + /// Benchmarks sampling the latest value using System.Reactive. + /// + /// The observed total. + [Benchmark] + public int SystemReactiveSampleLatest() + { + var scheduler = new HistoricalScheduler(); + var observer = new IntSignalObserver(); + using var source = new RxSubject(); + using var subscription = source.Sample(TimeSpan.FromTicks(1), scheduler).Subscribe(observer); + source.OnNext(Count); + scheduler.AdvanceBy(TimeSpan.FromTicks(1)); + return observer.Total; + } + + /// + /// Benchmarks sampling the latest value using R3 throttle-last semantics. + /// + /// The observed total. + [Benchmark] + public int R3SampleLatest() + { + var timeProvider = new FakeTimeProvider(); + var observer = new IntR3Observer(); + using var source = new R3.Subject(); + using var subscription = R3.ObservableExtensions.ThrottleLast(source, TimeSpan.FromTicks(1), timeProvider) + .Subscribe(observer); + source.OnNext(Count); + timeProvider.Advance(TimeSpan.FromTicks(1)); + return observer.Total; + } + /// /// Benchmarks timestamp projection. /// @@ -93,6 +239,30 @@ public int PrimitivesTimestampRange() return count; } + /// + /// Benchmarks timestamp projection using System.Reactive. + /// + /// The number of timestamps observed. + [Benchmark] + public int SystemReactiveTimestampRange() + { + var count = 0; + using var subscription = RxObservable.Range(1, Count).Timestamp(ImmediateScheduler.Instance).Subscribe(_ => count++); + return count; + } + + /// + /// Benchmarks timestamp projection using R3. + /// + /// The number of timestamps observed. + [Benchmark] + public int R3TimestampRange() + { + var observer = new CountingR3Observer<(long Timestamp, int Value)>(); + using var subscription = R3.ObservableExtensions.Timestamp(R3.Observable.Range(1, Count)).Subscribe(observer); + return observer.Count; + } + /// /// Benchmarks time-interval projection. /// @@ -106,19 +276,83 @@ public int PrimitivesTimeIntervalRange() } /// - /// Benchmarks timeout error delivery. + /// Benchmarks time-interval projection using System.Reactive. + /// + /// The number of intervals observed. + [Benchmark] + public int SystemReactiveTimeIntervalRange() + { + var count = 0; + using var subscription = RxObservable.Range(1, Count) + .TimeInterval(ImmediateScheduler.Instance) + .Subscribe(_ => count++); + return count; + } + + /// + /// Benchmarks time-interval projection using R3. + /// + /// The number of intervals observed. + [Benchmark] + public int R3TimeIntervalRange() + { + var observer = new CountingR3Observer<(TimeSpan Interval, int Value)>(); + using var subscription = R3.ObservableExtensions.TimeInterval(R3.Observable.Range(1, Count)).Subscribe(observer); + return observer.Count; + } + + /// + /// Benchmarks timeout error delivery after a source becomes idle. /// /// The number of timeout errors observed. [Benchmark] - public int PrimitivesTimeoutNever() + public int PrimitivesTimeoutIdle() { var clock = new TestClock(); var observer = new IntSignalObserver(); - using var subscription = Signal.Never().Timeout(TimeSpan.FromTicks(1), clock).Subscribe(observer); + using var source = new Signal(); + using var subscription = source.Timeout(TimeSpan.FromTicks(1), clock).Subscribe(observer); + source.OnNext(0); clock.AdvanceBy(TimeSpan.FromTicks(1)); return observer.ErrorCount; } + /// + /// Benchmarks timeout error delivery after a source becomes idle using System.Reactive. + /// + /// The number of timeout errors observed. + [Benchmark] + public int SystemReactiveTimeoutIdle() + { + var scheduler = new HistoricalScheduler(); + var observer = new IntSignalObserver(); + using var source = new RxSubject(); + using var subscription = source.Timeout(TimeSpan.FromTicks(1), scheduler).Subscribe(observer); + source.OnNext(0); + scheduler.AdvanceBy(TimeSpan.FromTicks(1)); + return observer.ErrorCount; + } + + /// + /// Benchmarks timeout error delivery after a source becomes idle using R3. + /// + /// The number of timeout errors observed. + [Benchmark] + public int R3TimeoutIdle() + { + var timeProvider = new FakeTimeProvider(); + var observer = new IntR3Observer(); + using var source = new R3.Subject(); + using var subscription = R3.ObservableExtensions.Timeout( + source, + TimeSpan.FromTicks(1), + timeProvider) + .Subscribe(observer); + source.OnNext(0); + timeProvider.Advance(TimeSpan.FromTicks(1)); + return observer.ErrorCount; + } + /// /// Benchmarks immediate observe-on dispatch. /// @@ -130,4 +364,46 @@ public int PrimitivesObserveOnImmediate() using var subscription = Signal.Range(1, Count).ObserveOn(Sequencer.Immediate).Subscribe(observer); return observer.Total; } + + /// + /// Benchmarks immediate observe-on dispatch using System.Reactive. + /// + /// The observed total. + [Benchmark] + public int SystemReactiveObserveOnImmediate() + { + var observer = new IntSignalObserver(); + using var subscription = RxObservable.Range(1, Count).ObserveOn(ImmediateScheduler.Instance).Subscribe(observer); + return observer.Total; + } + + /// + /// Benchmarks immediate observe-on dispatch using R3. + /// + /// The observed total. + [Benchmark] + public int R3ObserveOnImmediate() + { + var observer = new IntR3Observer(); + using var context = new ImmediateSynchronizationContext(); + using var subscription = R3.ObservableExtensions.ObserveOn(R3.Observable.Range(1, Count), context).Subscribe(observer); + return observer.Total; + } + + private sealed class ImmediateSynchronizationContext : SynchronizationContext, IDisposable + { + public override void Post(SendOrPostCallback d, object? state) + { + d(state); + } + + public override void Send(SendOrPostCallback d, object? state) + { + d(state); + } + + public void Dispose() + { + } + } } diff --git a/src/benchmarks/ReactiveUI.Primitives.Benchmarks/Program.cs b/src/benchmarks/ReactiveUI.Primitives.Benchmarks/Program.cs index 6cff779..befcfb1 100644 --- a/src/benchmarks/ReactiveUI.Primitives.Benchmarks/Program.cs +++ b/src/benchmarks/ReactiveUI.Primitives.Benchmarks/Program.cs @@ -109,10 +109,12 @@ private static async Task RunSmokeBenchmarksAsync() var replay = new ReplaySignalBenchmarks(); Console.WriteLine($"PrimitivesReplaySubscribe={replay.PrimitivesReplaySubscribe()}"); Console.WriteLine($"SystemReactiveReplaySubscribe={replay.SystemReactiveReplaySubscribe()}"); + Console.WriteLine($"R3ReplaySubscribe={replay.R3ReplaySubscribe()}"); var taskBridge = new AsyncBridgeBenchmarks(); Console.WriteLine($"PrimitivesCompletedTaskBridge={taskBridge.PrimitivesCompletedTaskBridge()}"); Console.WriteLine($"SystemReactiveCompletedTaskBridge={taskBridge.SystemReactiveCompletedTaskBridge()}"); + Console.WriteLine($"R3CompletedTaskBridge={taskBridge.R3CompletedTaskBridge()}"); await RunExpansionSmokeBenchmarksAsync(); @@ -123,34 +125,140 @@ private static async Task RunExpansionSmokeBenchmarksAsync() { var factoryAdapters = new FactoryAdapterExpansionBenchmarks(); Console.WriteLine($"PrimitivesCreateSubscribe={factoryAdapters.PrimitivesCreateSubscribe()}"); + Console.WriteLine($"SystemReactiveCreateSubscribe={factoryAdapters.SystemReactiveCreateSubscribe()}"); + Console.WriteLine($"R3CreateSubscribe={factoryAdapters.R3CreateSubscribe()}"); + Console.WriteLine($"PrimitivesCreateSafeSubscribe={factoryAdapters.PrimitivesCreateSafeSubscribe()}"); + Console.WriteLine($"SystemReactiveCreateSafeSubscribe={factoryAdapters.SystemReactiveCreateSafeSubscribe()}"); + Console.WriteLine($"R3CreateSafeSubscribe={factoryAdapters.R3CreateSafeSubscribe()}"); Console.WriteLine($"PrimitivesDeferSubscribe={factoryAdapters.PrimitivesDeferSubscribe()}"); + Console.WriteLine($"SystemReactiveDeferSubscribe={factoryAdapters.SystemReactiveDeferSubscribe()}"); + Console.WriteLine($"R3DeferSubscribe={factoryAdapters.R3DeferSubscribe()}"); + Console.WriteLine($"PrimitivesStartSubscribe={factoryAdapters.PrimitivesStartSubscribe()}"); + Console.WriteLine($"SystemReactiveStartSubscribe={factoryAdapters.SystemReactiveStartSubscribe()}"); + Console.WriteLine($"R3StartSubscribe={factoryAdapters.R3StartSubscribe()}"); + Console.WriteLine($"PrimitivesUnfoldSubscribe={factoryAdapters.PrimitivesUnfoldSubscribe()}"); + Console.WriteLine($"SystemReactiveUnfoldSubscribe={factoryAdapters.SystemReactiveUnfoldSubscribe()}"); + Console.WriteLine($"R3UnfoldSubscribe={factoryAdapters.R3UnfoldSubscribe()}"); + Console.WriteLine($"PrimitivesUseSubscribe={factoryAdapters.PrimitivesUseSubscribe()}"); + Console.WriteLine($"SystemReactiveUseSubscribe={factoryAdapters.SystemReactiveUseSubscribe()}"); + Console.WriteLine($"R3UseSubscribe={factoryAdapters.R3UseSubscribe()}"); Console.WriteLine( $"PrimitivesFromAsyncEnumerableSubscribe={await factoryAdapters.PrimitivesFromAsyncEnumerableSubscribeAsync()}"); + Console.WriteLine( + $"SystemReactiveFromAsyncEnumerableSubscribe={await factoryAdapters.SystemReactiveFromAsyncEnumerableSubscribeAsync()}"); + Console.WriteLine( + $"R3FromAsyncEnumerableSubscribe={await factoryAdapters.R3FromAsyncEnumerableSubscribeAsync()}"); + Console.WriteLine($"PrimitivesNeverSubscribeDispose={factoryAdapters.PrimitivesNeverSubscribeDispose()}"); + Console.WriteLine($"SystemReactiveNeverSubscribeDispose={factoryAdapters.SystemReactiveNeverSubscribeDispose()}"); + Console.WriteLine($"R3NeverSubscribeDispose={factoryAdapters.R3NeverSubscribeDispose()}"); var timeSchedulers = new OperatorTimeSchedulerBenchmarks(); Console.WriteLine($"PrimitivesDelayRange={timeSchedulers.PrimitivesDelayRange()}"); + Console.WriteLine($"SystemReactiveDelayRange={timeSchedulers.SystemReactiveDelayRange()}"); + Console.WriteLine($"R3DelayRange={timeSchedulers.R3DelayRange()}"); + Console.WriteLine($"PrimitivesDelayStartRange={timeSchedulers.PrimitivesDelayStartRange()}"); + Console.WriteLine($"SystemReactiveDelayStartRange={timeSchedulers.SystemReactiveDelayStartRange()}"); + Console.WriteLine($"R3DelayStartRange={timeSchedulers.R3DelayStartRange()}"); Console.WriteLine($"PrimitivesThrottleBurst={timeSchedulers.PrimitivesThrottleBurst()}"); - Console.WriteLine($"PrimitivesTimeoutNever={timeSchedulers.PrimitivesTimeoutNever()}"); + Console.WriteLine($"SystemReactiveThrottleBurst={timeSchedulers.SystemReactiveThrottleBurst()}"); + Console.WriteLine($"R3ThrottleBurst={timeSchedulers.R3ThrottleBurst()}"); + Console.WriteLine($"PrimitivesSampleLatest={timeSchedulers.PrimitivesSampleLatest()}"); + Console.WriteLine($"SystemReactiveSampleLatest={timeSchedulers.SystemReactiveSampleLatest()}"); + Console.WriteLine($"R3SampleLatest={timeSchedulers.R3SampleLatest()}"); + Console.WriteLine($"PrimitivesTimestampRange={timeSchedulers.PrimitivesTimestampRange()}"); + Console.WriteLine($"SystemReactiveTimestampRange={timeSchedulers.SystemReactiveTimestampRange()}"); + Console.WriteLine($"R3TimestampRange={timeSchedulers.R3TimestampRange()}"); + Console.WriteLine($"PrimitivesTimeIntervalRange={timeSchedulers.PrimitivesTimeIntervalRange()}"); + Console.WriteLine($"SystemReactiveTimeIntervalRange={timeSchedulers.SystemReactiveTimeIntervalRange()}"); + Console.WriteLine($"R3TimeIntervalRange={timeSchedulers.R3TimeIntervalRange()}"); + Console.WriteLine($"PrimitivesTimeoutIdle={timeSchedulers.PrimitivesTimeoutIdle()}"); + Console.WriteLine($"SystemReactiveTimeoutIdle={timeSchedulers.SystemReactiveTimeoutIdle()}"); + Console.WriteLine($"R3TimeoutIdle={timeSchedulers.R3TimeoutIdle()}"); + Console.WriteLine($"PrimitivesObserveOnImmediate={timeSchedulers.PrimitivesObserveOnImmediate()}"); + Console.WriteLine($"SystemReactiveObserveOnImmediate={timeSchedulers.SystemReactiveObserveOnImmediate()}"); + Console.WriteLine($"R3ObserveOnImmediate={timeSchedulers.R3ObserveOnImmediate()}"); var higherOrder = new OperatorHigherOrderBenchmarks(); Console.WriteLine($"PrimitivesConcatRanges={higherOrder.PrimitivesConcatRanges()}"); + Console.WriteLine($"SystemReactiveConcatRanges={higherOrder.SystemReactiveConcatRanges()}"); + Console.WriteLine($"R3ConcatRanges={higherOrder.R3ConcatRanges()}"); + Console.WriteLine($"PrimitivesMergeRanges={higherOrder.PrimitivesMergeRanges()}"); + Console.WriteLine($"SystemReactiveMergeRanges={higherOrder.SystemReactiveMergeRanges()}"); + Console.WriteLine($"R3MergeRanges={higherOrder.R3MergeRanges()}"); + Console.WriteLine($"PrimitivesRaceRanges={higherOrder.PrimitivesRaceRanges()}"); + Console.WriteLine($"SystemReactiveRaceRanges={higherOrder.SystemReactiveRaceRanges()}"); + Console.WriteLine($"R3RaceRanges={higherOrder.R3RaceRanges()}"); + Console.WriteLine($"PrimitivesSwitchRanges={higherOrder.PrimitivesSwitchRanges()}"); + Console.WriteLine($"SystemReactiveSwitchRanges={higherOrder.SystemReactiveSwitchRanges()}"); + Console.WriteLine($"R3SwitchRanges={higherOrder.R3SwitchRanges()}"); Console.WriteLine($"PrimitivesCombineLatestRanges={higherOrder.PrimitivesCombineLatestRanges()}"); + Console.WriteLine($"SystemReactiveCombineLatestRanges={higherOrder.SystemReactiveCombineLatestRanges()}"); + Console.WriteLine($"R3CombineLatestRanges={higherOrder.R3CombineLatestRanges()}"); + Console.WriteLine($"PrimitivesWithLatestRanges={higherOrder.PrimitivesWithLatestRanges()}"); + Console.WriteLine($"SystemReactiveWithLatestRanges={higherOrder.SystemReactiveWithLatestRanges()}"); + Console.WriteLine($"R3WithLatestRanges={higherOrder.R3WithLatestRanges()}"); Console.WriteLine($"PrimitivesForkJoinRanges={higherOrder.PrimitivesForkJoinRanges()}"); + Console.WriteLine($"SystemReactiveForkJoinRanges={higherOrder.SystemReactiveForkJoinRanges()}"); + Console.WriteLine($"R3ForkJoinRanges={higherOrder.R3ForkJoinRanges()}"); var terminalCollections = new TerminalCollectionBenchmarks(); Console.WriteLine($"PrimitivesCollectList={terminalCollections.PrimitivesCollectList()}"); + Console.WriteLine($"SystemReactiveCollectList={terminalCollections.SystemReactiveCollectList()}"); + Console.WriteLine($"R3CollectList={await terminalCollections.R3CollectList()}"); + Console.WriteLine($"PrimitivesCollectArray={terminalCollections.PrimitivesCollectArray()}"); + Console.WriteLine($"SystemReactiveCollectArray={terminalCollections.SystemReactiveCollectArray()}"); + Console.WriteLine($"R3CollectArray={await terminalCollections.R3CollectArray()}"); + Console.WriteLine($"PrimitivesCollectArrayAsync={await terminalCollections.PrimitivesCollectArrayAsync()}"); + Console.WriteLine($"SystemReactiveCollectArrayAsync={await terminalCollections.SystemReactiveCollectArrayAsync()}"); + Console.WriteLine($"R3CollectArrayAsync={await terminalCollections.R3CollectArrayAsync()}"); Console.WriteLine($"PrimitivesFirstAsync={await terminalCollections.PrimitivesFirstAsync()}"); + Console.WriteLine($"SystemReactiveFirstAsync={await terminalCollections.SystemReactiveFirstAsync()}"); + Console.WriteLine($"R3FirstAsync={await terminalCollections.R3FirstAsync()}"); + Console.WriteLine($"PrimitivesToTask={await terminalCollections.PrimitivesToTask()}"); + Console.WriteLine($"SystemReactiveToTask={await terminalCollections.SystemReactiveToTask()}"); + Console.WriteLine($"R3ToTask={await terminalCollections.R3ToTask()}"); + Console.WriteLine($"PrimitivesCountPredicate={terminalCollections.PrimitivesCountPredicate()}"); + Console.WriteLine($"SystemReactiveCountPredicate={terminalCollections.SystemReactiveCountPredicate()}"); + Console.WriteLine($"R3CountPredicate={await terminalCollections.R3CountPredicate()}"); Console.WriteLine($"PrimitivesAllContains={terminalCollections.PrimitivesAllContains()}"); + Console.WriteLine($"SystemReactiveAllContains={terminalCollections.SystemReactiveAllContains()}"); + Console.WriteLine($"R3AllContains={await terminalCollections.R3AllContains()}"); var connectableShare = new ConnectableShareBenchmarks(); Console.WriteLine($"PrimitivesPublishLiveConnect={connectableShare.PrimitivesPublishLiveConnect()}"); + Console.WriteLine($"SystemReactivePublishLiveConnect={connectableShare.SystemReactivePublishLiveConnect()}"); + Console.WriteLine($"R3PublishLiveConnect={connectableShare.R3PublishLiveConnect()}"); Console.WriteLine($"PrimitivesShareLiveSubscribe={connectableShare.PrimitivesShareLiveSubscribe()}"); + Console.WriteLine($"SystemReactiveShareLiveSubscribe={connectableShare.SystemReactiveShareLiveSubscribe()}"); + Console.WriteLine($"R3ShareLiveSubscribe={connectableShare.R3ShareLiveSubscribe()}"); Console.WriteLine($"PrimitivesReplayLiveLateSubscribe={connectableShare.PrimitivesReplayLiveLateSubscribe()}"); + Console.WriteLine($"SystemReactiveReplayLiveLateSubscribe={connectableShare.SystemReactiveReplayLiveLateSubscribe()}"); + Console.WriteLine($"R3ReplayLiveLateSubscribe={connectableShare.R3ReplayLiveLateSubscribe()}"); + Console.WriteLine($"PrimitivesRefCountSubscribe={connectableShare.PrimitivesRefCountSubscribe()}"); + Console.WriteLine($"SystemReactiveRefCountSubscribe={connectableShare.SystemReactiveRefCountSubscribe()}"); + Console.WriteLine($"R3RefCountSubscribe={connectableShare.R3RefCountSubscribe()}"); + Console.WriteLine($"PrimitivesAutoConnectSubscribe={connectableShare.PrimitivesAutoConnectSubscribe()}"); + Console.WriteLine($"SystemReactiveAutoConnectSubscribe={connectableShare.SystemReactiveAutoConnectSubscribe()}"); + Console.WriteLine($"R3AutoConnectSubscribe={connectableShare.R3AutoConnectSubscribe()}"); var stateTaskCommand = new StateTaskCommandBenchmarks(); Console.WriteLine($"PrimitivesStateSignalUpdates={stateTaskCommand.PrimitivesStateSignalUpdates()}"); + Console.WriteLine($"SystemReactiveStateSignalUpdates={stateTaskCommand.SystemReactiveStateSignalUpdates()}"); + Console.WriteLine($"R3StateSignalUpdates={stateTaskCommand.R3StateSignalUpdates()}"); + Console.WriteLine($"PrimitivesReadOnlyStateProjection={stateTaskCommand.PrimitivesReadOnlyStateProjection()}"); + Console.WriteLine( + $"SystemReactiveReadOnlyStateProjection={stateTaskCommand.SystemReactiveReadOnlyStateProjection()}"); + Console.WriteLine($"R3ReadOnlyStateProjection={stateTaskCommand.R3ReadOnlyStateProjection()}"); Console.WriteLine($"PrimitivesTaskSignalSubscribe={stateTaskCommand.PrimitivesTaskSignalSubscribe()}"); + Console.WriteLine($"SystemReactiveTaskSignalSubscribe={stateTaskCommand.SystemReactiveTaskSignalSubscribe()}"); + Console.WriteLine($"R3TaskSignalSubscribe={stateTaskCommand.R3TaskSignalSubscribe()}"); Console.WriteLine($"PrimitivesCommandExecute={await stateTaskCommand.PrimitivesCommandExecuteAsync()}"); + Console.WriteLine($"SystemReactiveCommandExecute={await stateTaskCommand.SystemReactiveCommandExecuteAsync()}"); + Console.WriteLine($"R3CommandExecute={stateTaskCommand.R3CommandExecute()}"); + Console.WriteLine( + $"PrimitivesCommandResultSubscribe={await stateTaskCommand.PrimitivesCommandResultSubscribeAsync()}"); + Console.WriteLine($"SystemReactiveCommandResultSubscribe={stateTaskCommand.SystemReactiveCommandResultSubscribe()}"); + Console.WriteLine($"R3CommandResultSubscribe={stateTaskCommand.R3CommandResultSubscribe()}"); } private static void RunCoreRuntimeSmokeBenchmarks() @@ -158,10 +266,16 @@ private static void RunCoreRuntimeSmokeBenchmarks() var coreRuntime = new CoreRuntimeBenchmarks(); Console.WriteLine($"PrimitivesPocketDispose={coreRuntime.PrimitivesPocketDispose()}"); Console.WriteLine($"SystemReactiveCompositeDispose={coreRuntime.SystemReactiveCompositeDispose()}"); + Console.WriteLine($"R3CompositeDispose={coreRuntime.R3CompositeDispose()}"); Console.WriteLine($"PrimitivesCurrentThreadSchedule={coreRuntime.PrimitivesCurrentThreadSchedule()}"); Console.WriteLine( $"SystemReactiveCurrentThreadSchedule={coreRuntime.SystemReactiveCurrentThreadSchedule()}"); + Console.WriteLine($"R3CurrentThreadSchedule={coreRuntime.R3CurrentThreadSchedule()}"); Console.WriteLine($"PrimitivesSafeWitness={coreRuntime.PrimitivesSafeWitness()}"); + Console.WriteLine($"SystemReactiveSafeWitness={coreRuntime.SystemReactiveSafeWitness()}"); + Console.WriteLine($"R3SafeWitness={coreRuntime.R3SafeWitness()}"); Console.WriteLine($"PrimitivesCompletedSpark={coreRuntime.PrimitivesCompletedSpark()}"); + Console.WriteLine($"SystemReactiveCompletedSpark={coreRuntime.SystemReactiveCompletedSpark()}"); + Console.WriteLine($"R3CompletedSpark={coreRuntime.R3CompletedSpark()}"); } } diff --git a/src/benchmarks/ReactiveUI.Primitives.Benchmarks/ReactiveUI.Primitives.Benchmarks.csproj b/src/benchmarks/ReactiveUI.Primitives.Benchmarks/ReactiveUI.Primitives.Benchmarks.csproj index 0ebd45b..742431b 100644 --- a/src/benchmarks/ReactiveUI.Primitives.Benchmarks/ReactiveUI.Primitives.Benchmarks.csproj +++ b/src/benchmarks/ReactiveUI.Primitives.Benchmarks/ReactiveUI.Primitives.Benchmarks.csproj @@ -10,11 +10,12 @@ true true false - $(NoWarn);SA1600;SA1649;SA1402;SA1518;SA1208;SA1210;SA1211;S109;S3257;CA1822;S1128;SA1200 + $(NoWarn);SA1600;SA1649;SA1402;SA1518;SA1208;SA1210;SA1211;S109;S3257;CA1822;S1128;SA1200;S4144;S6966;IDE0300;S138;CA1849 + diff --git a/src/benchmarks/ReactiveUI.Primitives.Benchmarks/ReplaySignalBenchmarks.cs b/src/benchmarks/ReactiveUI.Primitives.Benchmarks/ReplaySignalBenchmarks.cs index e201289..5f4a5fc 100644 --- a/src/benchmarks/ReactiveUI.Primitives.Benchmarks/ReplaySignalBenchmarks.cs +++ b/src/benchmarks/ReactiveUI.Primitives.Benchmarks/ReplaySignalBenchmarks.cs @@ -7,6 +7,7 @@ using ReactiveUI.Primitives.Signals; using R3; +using R3ReplaySubject = R3.ReplaySubject; using RxReplaySubject = System.Reactive.Subjects.ReplaySubject; namespace ReactiveUI.Primitives.Benchmarks; @@ -45,6 +46,20 @@ public int SystemReactiveReplaySubscribe() return observer.Total; } + /// + /// Bounded replay subscription benchmark for R3. + /// + /// The sum replayed to a late subscriber. + [Benchmark] + public int R3ReplaySubscribe() + { + var observer = new IntR3Observer(); + using var subject = new R3ReplaySubject(16); + PopulateReplaySubject(subject); + using var subscription = subject.Subscribe(observer); + return observer.Total; + } + private static void PopulateReplaySubject(ReplaySignal subject) { for (var i = 0; i < 16; i++) @@ -60,5 +75,12 @@ private static void PopulateReplaySubject(RxReplaySubject subject) subject.OnNext(i); } } -} + private static void PopulateReplaySubject(R3ReplaySubject subject) + { + for (var i = 0; i < 16; i++) + { + subject.OnNext(i); + } + } +} diff --git a/src/benchmarks/ReactiveUI.Primitives.Benchmarks/StateTaskCommandBenchmarks.cs b/src/benchmarks/ReactiveUI.Primitives.Benchmarks/StateTaskCommandBenchmarks.cs index 49349ee..020417e 100644 --- a/src/benchmarks/ReactiveUI.Primitives.Benchmarks/StateTaskCommandBenchmarks.cs +++ b/src/benchmarks/ReactiveUI.Primitives.Benchmarks/StateTaskCommandBenchmarks.cs @@ -6,6 +6,9 @@ using ReactiveUI.Primitives; using ReactiveUI.Primitives.Concurrency; using ReactiveUI.Primitives.Signals; +using System.Reactive.Concurrency; +using System.Reactive.Linq; +using System.Reactive.Threading.Tasks; namespace ReactiveUI.Primitives.Benchmarks; @@ -36,6 +39,42 @@ public int PrimitivesStateSignalUpdates() return observer.Total; } + /// + /// Benchmarks state updates using System.Reactive behavior subject. + /// + /// The observed total. + [Benchmark] + public int SystemReactiveStateSignalUpdates() + { + var observer = new IntSignalObserver(); + using var state = new System.Reactive.Subjects.BehaviorSubject(0); + using var subscription = state.Subscribe(observer); + for (var i = 0; i < Count; i++) + { + state.OnNext(i); + } + + return observer.Total; + } + + /// + /// Benchmarks state updates using R3 behavior subject. + /// + /// The observed total. + [Benchmark] + public int R3StateSignalUpdates() + { + var observer = new IntR3Observer(); + using var state = new R3.BehaviorSubject(0); + using var subscription = state.Subscribe(observer); + for (var i = 0; i < Count; i++) + { + state.OnNext(i); + } + + return observer.Total; + } + /// /// Benchmarks read-only state projection. /// @@ -49,6 +88,35 @@ public int PrimitivesReadOnlyStateProjection() return projected.Value; } + /// + /// Benchmarks read-only state projection using System.Reactive. + /// + /// The current projected value. + [Benchmark] + public int SystemReactiveReadOnlyStateProjection() + { + var current = 0; + using var state = new System.Reactive.Subjects.BehaviorSubject(Value); + using var subscription = state.Select(static value => value + 1).Subscribe(value => current = value); + state.OnNext(Value + 1); + return current; + } + + /// + /// Benchmarks read-only state projection using R3. + /// + /// The current projected value. + [Benchmark] + public int R3ReadOnlyStateProjection() + { + using var state = new R3.BehaviorSubject(Value); + using var projected = R3.ReactivePropertyExtensions.ToReadOnlyReactiveProperty( + R3.ObservableExtensions.Select(state, static (int value) => value + 1), + Value + 1); + state.OnNext(Value + 1); + return projected.CurrentValue; + } + /// /// Benchmarks task signal subscription. /// @@ -62,6 +130,34 @@ public int PrimitivesTaskSignalSubscribe() return observer.Total; } + /// + /// Benchmarks task-backed observable subscription using System.Reactive. + /// + /// The observed total. + [Benchmark] + public int SystemReactiveTaskSignalSubscribe() + { + var observer = new IntSignalObserver(); + using var subscription = System.Reactive.Linq.Observable.FromAsync( + static () => Task.FromResult(Value), + ImmediateScheduler.Instance) + .Subscribe(observer); + return observer.Total; + } + + /// + /// Benchmarks task-backed observable subscription using R3. + /// + /// The observed total. + [Benchmark] + public int R3TaskSignalSubscribe() + { + var observer = new IntR3Observer(); + using var subscription = R3.Observable.ToObservable(Task.FromResult(Value), configureAwait: false) + .Subscribe(observer); + return observer.Total; + } + /// /// Benchmarks command execution. /// @@ -73,6 +169,27 @@ public async Task PrimitivesCommandExecuteAsync() return await command.ExecuteAsync().ConfigureAwait(false); } + /// + /// Benchmarks command-like execution using System.Reactive async factory semantics. + /// + /// The command result. + [Benchmark] + public Task SystemReactiveCommandExecuteAsync() => + System.Reactive.Linq.Observable.Start(static () => Value, ImmediateScheduler.Instance).FirstAsync().ToTask(); + + /// + /// Benchmarks command execution using R3. + /// + /// The command result. + [Benchmark] + public int R3CommandExecute() + { + var result = 0; + using var command = new R3.ReactiveCommand(value => result = value); + command.Execute(Value); + return result; + } + /// /// Benchmarks command result publication. /// @@ -86,4 +203,32 @@ public async Task PrimitivesCommandResultSubscribeAsync() await command.ExecuteAsync().ConfigureAwait(false); return observer.Total; } + + /// + /// Benchmarks command-result publication using System.Reactive subject semantics. + /// + /// The observed total. + [Benchmark] + public int SystemReactiveCommandResultSubscribe() + { + var observer = new IntSignalObserver(); + using var results = new System.Reactive.Subjects.Subject(); + using var subscription = results.Subscribe(observer); + results.OnNext(Value); + return observer.Total; + } + + /// + /// Benchmarks command-result publication using R3 subject semantics. + /// + /// The observed total. + [Benchmark] + public int R3CommandResultSubscribe() + { + var observer = new IntR3Observer(); + using var results = new R3.Subject(); + using var subscription = results.Subscribe(observer); + results.OnNext(Value); + return observer.Total; + } } diff --git a/src/benchmarks/ReactiveUI.Primitives.Benchmarks/TerminalCollectionBenchmarks.cs b/src/benchmarks/ReactiveUI.Primitives.Benchmarks/TerminalCollectionBenchmarks.cs index a838b78..907b30a 100644 --- a/src/benchmarks/ReactiveUI.Primitives.Benchmarks/TerminalCollectionBenchmarks.cs +++ b/src/benchmarks/ReactiveUI.Primitives.Benchmarks/TerminalCollectionBenchmarks.cs @@ -5,6 +5,11 @@ using BenchmarkDotNet.Attributes; using ReactiveUI.Primitives; using ReactiveUI.Primitives.Signals; +using System.Reactive.Linq; +using System.Reactive.Threading.Tasks; +using System.Threading; + +using RxObservable = System.Reactive.Linq.Observable; namespace ReactiveUI.Primitives.Benchmarks; @@ -28,6 +33,29 @@ public int PrimitivesCollectList() return result; } + /// + /// Benchmarks collecting into a list using System.Reactive. + /// + /// The collected count. + [Benchmark] + public int SystemReactiveCollectList() + { + var result = 0; + using var subscription = RxObservable.Range(1, Count).ToList().Subscribe(values => result = values.Count); + return result; + } + + /// + /// Benchmarks collecting into a list using R3. + /// + /// The collected count. + [Benchmark] + public async Task R3CollectList() + { + return (await R3.ObservableExtensions.ToListAsync(R3.Observable.Range(1, Count), CancellationToken.None) + .ConfigureAwait(false)).Count; + } + /// /// Benchmarks collecting into an array signal. /// @@ -40,6 +68,29 @@ public int PrimitivesCollectArray() return result; } + /// + /// Benchmarks collecting into an array using System.Reactive. + /// + /// The collected count. + [Benchmark] + public int SystemReactiveCollectArray() + { + var result = 0; + using var subscription = RxObservable.Range(1, Count).ToArray().Subscribe(values => result = values.Length); + return result; + } + + /// + /// Benchmarks collecting into an array using R3. + /// + /// The collected count. + [Benchmark] + public async Task R3CollectArray() + { + return (await R3.ObservableExtensions.ToArrayAsync(R3.Observable.Range(1, Count), CancellationToken.None) + .ConfigureAwait(false)).Length; + } + /// /// Benchmarks asynchronous array collection. /// @@ -50,6 +101,27 @@ public async Task PrimitivesCollectArrayAsync() return (await Signal.Range(1, Count).CollectArrayAsync().ConfigureAwait(false)).Length; } + /// + /// Benchmarks asynchronous array collection using System.Reactive. + /// + /// The collected count. + [Benchmark] + public async Task SystemReactiveCollectArrayAsync() + { + return (await RxObservable.Range(1, Count).ToArray().ToTask().ConfigureAwait(false)).Length; + } + + /// + /// Benchmarks asynchronous array collection using R3. + /// + /// The collected count. + [Benchmark] + public async Task R3CollectArrayAsync() + { + return (await R3.ObservableExtensions.ToArrayAsync(R3.Observable.Range(1, Count), CancellationToken.None) + .ConfigureAwait(false)).Length; + } + /// /// Benchmarks first-value task conversion. /// @@ -58,6 +130,22 @@ public async Task PrimitivesCollectArrayAsync() public Task PrimitivesFirstAsync() => Signal.Range(1, Count).FirstAsync(); + /// + /// Benchmarks first-value task conversion using System.Reactive. + /// + /// The first value. + [Benchmark] + public Task SystemReactiveFirstAsync() => + RxObservable.Range(1, Count).FirstAsync().ToTask(); + + /// + /// Benchmarks first-value task conversion using R3. + /// + /// The first value. + [Benchmark] + public Task R3FirstAsync() => + R3.ObservableExtensions.FirstAsync(R3.Observable.Range(1, Count), CancellationToken.None); + /// /// Benchmarks last-value task conversion. /// @@ -66,6 +154,22 @@ public Task PrimitivesFirstAsync() => public Task PrimitivesToTask() => Signal.Range(1, Count).ToTask(); + /// + /// Benchmarks last-value task conversion using System.Reactive. + /// + /// The last value. + [Benchmark] + public Task SystemReactiveToTask() => + RxObservable.Range(1, Count).ToTask(); + + /// + /// Benchmarks last-value task conversion using R3. + /// + /// The last value. + [Benchmark] + public Task R3ToTask() => + R3.ObservableExtensions.LastAsync(R3.Observable.Range(1, Count), CancellationToken.None); + /// /// Benchmarks predicate count. /// @@ -78,6 +182,29 @@ public int PrimitivesCountPredicate() return observer.Total; } + /// + /// Benchmarks predicate count using System.Reactive. + /// + /// The matching count. + [Benchmark] + public int SystemReactiveCountPredicate() + { + var observer = new IntSignalObserver(); + using var subscription = RxObservable.Range(1, Count).Count(static value => value % 2 == 0).Subscribe(observer); + return observer.Total; + } + + /// + /// Benchmarks predicate count using R3. + /// + /// The matching count. + [Benchmark] + public Task R3CountPredicate() => + R3.ObservableExtensions.CountAsync( + R3.Observable.Range(1, Count), + static (int value) => value % 2 == 0, + CancellationToken.None); + /// /// Benchmarks all and contains terminal predicates. /// @@ -90,4 +217,37 @@ public int PrimitivesAllContains() using var contains = Signal.Range(1, Count).Contains(Count).Subscribe(value => result += value ? 1 : 0); return result; } + + /// + /// Benchmarks all and contains terminal predicates using System.Reactive. + /// + /// The number of true results. + [Benchmark] + public int SystemReactiveAllContains() + { + var result = 0; + using var all = RxObservable.Range(1, Count).All(static value => value > 0).Subscribe(value => result += value ? 1 : 0); + using var contains = RxObservable.Range(1, Count).Contains(Count).Subscribe(value => result += value ? 1 : 0); + return result; + } + + /// + /// Benchmarks all and contains terminal predicates using R3. + /// + /// The number of true results. + [Benchmark] + public async Task R3AllContains() + { + var all = await R3.ObservableExtensions.AllAsync( + R3.Observable.Range(1, Count), + static (int value) => value > 0, + CancellationToken.None) + .ConfigureAwait(false); + var contains = await R3.ObservableExtensions.ContainsAsync( + R3.Observable.Range(1, Count), + Count, + CancellationToken.None) + .ConfigureAwait(false); + return (all ? 1 : 0) + (contains ? 1 : 0); + } }