Skip to content

fix(binding): settle two-way bindings and drop needless locks - #100

Merged
glennawatson merged 6 commits into
mainfrom
glennawatson/view-write-latest-value
Sep 14, 2026
Merged

glennawatson merged 6 commits into
mainfrom
glennawatson/view-write-latest-value

Conversation

@glennawatson

@glennawatson glennawatson commented Sep 14, 2026

Copy link
Copy Markdown
Contributor

What kind of change does this PR introduce?

A bug fix, with a performance improvement.

What is the new behavior?

A two-way binding settles after quick background changes, and hot paths that need no lock take none.

  • A value waiting for the view's owning thread is replaced by a newer one, so a burst of changes becomes one write.
  • Bind and BindTwoWay settle on the latest value after two quick changes from another thread.
  • The README and CLAUDE.md describe the latest-value wait, and that Unsafe bindings route writes only through registered invokers.
  • WhenChanged hands a change that arrives during another emit to that emit, so no thread waits on a lock.
  • An observer that changes the property it observes gets the change after it returns, not inside its own call.
  • Interaction keeps its handlers in an array replaced on each change, so Handle reads them without a lock or a copy.
  • Interaction and the binding change stream share one copy-on-write array helper.
  • RxBindingBuilder tracks initialization with a volatile flag instead of a lock.
  • BindingHooks reads its cached set with a volatile read.

What is the current behavior?

Two quick background changes make a two-way binding bounce values between the view model and the view forever.

  • Every waiting value is written in order, so the view's echo of an older value writes it back to the view model.
  • A thread that raises a change during another thread's WhenChanged emit waits for it, so an observer that waits on that thread deadlocks.

What might this PR break?

Nothing a binding relies on.

  • A burst of changes from another thread writes the view once with the latest value, not once per change.
  • When two threads change one property at once, WhenChanged can deliver the later value on the thread already emitting.

Checklist

  • I have read the Contribute guide
  • Tests have been added or updated (for bug fixes / features)
  • Docs have been added or updated (for bug fixes / features)
  • Changes target the main branch
  • PR title follows Conventional Commits

Additional information

- A value waiting for the view's owning thread is replaced by a newer one, so a burst of changes becomes one write.
- Bind and BindTwoWay settle on the latest value after two changes from another thread, instead of bouncing values between the view model and the view.
- The README and CLAUDE.md describe the latest-value wait, and that Unsafe bindings route writes only through registered invokers.
- InteractionBenchmark measures asking a question through three handlers, and registering and removing handlers.
- WhenChangedContentionBenchmark measures two threads changing one observed property at the same time.
- WhenChanged hands a change that arrives during another emit to that emit, so no thread waits on a lock and an observer that waits on such a thread does not deadlock.
- An observer that changes the property it observes gets the change after it returns, not inside its own call.
- Interaction keeps its handlers in an array replaced on each change, so Handle reads them without a lock or a copy.
- Interaction and the binding change stream share one copy-on-write array helper.
- RxBindingBuilder tracks initialization with a volatile flag instead of a lock.
- BindingHooks reads its cached set with a volatile read and keeps its lock for the resolve after a refresh.
- The before-change observables keep their lock, because the value must be read on the raising thread before it writes.
@glennawatson glennawatson changed the title fix(binding): write only the latest waiting value to the view fix(binding): settle two-way bindings and drop needless locks Sep 14, 2026
- The benchmark workflow takes no suite or filter and runs all three benchmark projects in full.
@codecov

codecov Bot commented Sep 14, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 98.96907% with 1 line in your changes missing coverage. Please review.
✅ Project coverage is 99.72%. Comparing base (b4968f4) to head (00bdfd4).

Files with missing lines Patch % Lines
...ctiveUI.Binding.Shared/Interactions/Interaction.cs 80.00% 1 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main     #100      +/-   ##
==========================================
- Coverage   99.73%   99.72%   -0.01%     
==========================================
  Files         288      289       +1     
  Lines       11537    11533       -4     
  Branches     1256     1259       +3     
==========================================
- Hits        11506    11501       -5     
- Misses         18       19       +1     
  Partials       13       13              

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

…mark

- Every benchmark takes its jobs and profiler from a shared config: ETW with .NET Framework 4.6.2 on Windows, EventPipe CPU samples and GC events elsewhere.
- No benchmark uses the memory diagnoser.
- The view models, views and other test data live in Mocks folders, apart from the benchmark classes.
- The generator benchmarks read compiled mock consumer source copied beside the assembly.
- src/benchmarks/Directory.Build.props holds the settings the benchmark projects share.
@glennawatson glennawatson reopened this Sep 14, 2026
…te-latest-value

# Conflicts:
#	src/benchmarks/ReactiveUI.Binding.Benchmarks.ReactiveUI/ReactiveUIBindingBenchmark.cs
#	src/benchmarks/ReactiveUI.Binding.Benchmarks.ReactiveUI/ReactiveUIObservationBenchmark.cs
#	src/benchmarks/ReactiveUI.Binding.Benchmarks/RxUiDynamicChainBaseline.cs
#	src/benchmarks/ReactiveUI.Binding.Benchmarks/UnsafeFallbackBenchmark.cs
#	src/benchmarks/ReactiveUI.Binding.Benchmarks/WhenAnyDynamicBenchmark.cs
#	src/benchmarks/Shared/Configs/NativeAotBenchmarkConfig.cs
@sonarqubecloud

Copy link
Copy Markdown

@glennawatson
glennawatson merged commit a63e97a into main Sep 14, 2026
16 of 19 checks passed
@glennawatson
glennawatson deleted the glennawatson/view-write-latest-value branch September 14, 2026 14:41
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant