[Perf] Reduce allocations in inherited BindingContext propagation
- Dominant language
- C#
- Stars
- 23.3k
- Forks
- 2k
- Avg merge
- 1d 15h
- Merged PRs (30d)
- 290
Description
## Summary
When a parent's `BindingContext` changes, `SetInheritedBindingContext` is called recursively on every descendant. Each call produces two avoidable heap allocations: a `new WeakReference` and a `.ToArray()` snapshot of the property dictionary. For a visual tree of N elements, this means 2N allocations per BindingContext change — significant during navigation, `CollectionView` item realization, and initial page load.
## Current behavior
### 1. `new WeakReference` per descendant (line 366)
`SetInheritedBindingContext` allocates a new `WeakReference` on every call, discarding the previous one immediately:
https://github.com/dotnet/maui/blob/2c26b34760881ae04cb3b82f865196fc96978a50/src/Controls/src/Core/BindableObject.cs#L364-L366
The old `WeakReference` becomes garbage on the same frame it was created.
### 2. `.ToArray()` snapshot in `ApplyBindings` (line 682)
`ApplyBindings` is called from `SetInheritedBindingContext` and allocates a `BindablePropertyContext[]` copy of the entire property dictionary on every invocation:
https://github.com/dotnet/maui/blob/2c26b34760881ae04cb3b82f865196fc96978a50/src/Controls/src/Core/BindableObject.cs#L680-L694
The snapshot guards against collection-modification during iteration, but the dictionary is rarely modified during binding application.
### Combined impact
A `ContentPage` with 100 visual elements (common for a form or list) triggers 100 × `new WeakReference` + 100 × `.ToArray()` = 200 allocations just from the BindingContext flowing down the tree. In `CollectionView` scenarios with hundreds of items, this multiplies further.
## Proposed changes
### Fix 1: Reuse `WeakReference` via target update
Change `_inheritedContext` from `WeakReference` to `WeakReference` and reuse it:
```csharp
// Before
bindable._inheritedContext = new WeakReference(value);
// After
if (bindable._inheritedContext is not null)
bindable._inheritedContext.SetTarget(value);
else
bindable._inheritedContext = new WeakReference(value);
```
This is safe because `SetInheritedBindingContext` is always called on the UI thread, and `WeakReference.SetTarget()` is available since .NET 6.
### Fix 2: Eliminate `.ToArray()` in `ApplyBindings`
Replace the snapshot with direct dictionary enumeration using a `foreach` loop over `_properties`. If collection modification during iteration is a concern, guard with a version check or catch `InvalidOperationException` on the rare re-entrant path — the common path (no modification) avoids the array allocation entirely.
```csharp
// Before
var prop = _properties.Values.ToArray();
for (int i = 0, propLength = prop.Length; i < propLength; i++)
{
BindablePropertyContext context = prop[i];
...
}
// After — iterate directly, no snapshot
foreach (var kvp in _properties)
{
BindablePropertyContext context = kvp.Value;
if (ReferenceEquals(context.Property, BindingContextProperty))
continue;
ApplyBinding(context, fromBindingContextChanged);
}
```
An alternative safe approach: use a pooled/rented array from `ArrayPool` if the snapshot is truly needed for correctness.
## Estimated impact
Per inherited BindingContext change, per element:
| Allocation | Current | After fix |
|---|---|---|
| `WeakReference` | 24B | 0B (reuse) |
| `.ToArray()` snapshot | 32B + 8B × property count | 0B |
For a typical element with ~10 properties: **~136B → 0B per element**, or **~13.6 KB → 0B for a 100-element tree** per BindingContext propagation.
Contributor guide
Research direction
Read src/Controls/src/Core/BindableObject.cs around lines 364-366 and 680-694, starting with SetInheritedBindingContext and ApplyBindings. Check the existing binding behavior and re-entrant collection-modification concern before choosing between direct enumeration and a pooled snapshot. Done means reducing the avoidable allocations without changing BindingContext propagation or binding application behavior.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- csharp
- Domain
- performance
- Issue type
- Refactor
- Difficulty
- 4/5
- Estimated time
- 3-5 days
- Activity status
- Stale
- Clarity
- Mostly clear
- Newbie friendliness
- 48/100