dotnet / dotnet/maui

[Perf] Reduce allocations in inherited BindingContext propagation

Open
#34,129 0 comments 0 reactions 0 assignees View on GitHub
perf/general
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

Open the contributing 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

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.