Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 8 additions & 4 deletions src/Controls/src/Core/BindableObject.cs
Original file line number Diff line number Diff line change
Expand Up @@ -363,7 +363,10 @@ public static void SetInheritedBindingContext(BindableObject bindable, object va
}
else
{
bindable._inheritedContext = new WeakReference(value);
if (bindable._inheritedContext is not null)
bindable._inheritedContext.Target = value;
else
bindable._inheritedContext = new WeakReference(value);
bindable.ApplyBindings(fromBindingContextChanged: true);
bindable.OnBindingContextChanged();
}
Expand Down Expand Up @@ -692,11 +695,12 @@ private protected virtual void OnBindablePropertySet(BindableProperty property,

void ApplyBindings(bool fromBindingContextChanged)
{
var prop = _properties.Values.ToArray();
// Snapshot the property contexts — ApplyBinding can trigger re-entrant
// property changes (via callbacks) that modify _properties mid-iteration.
var contexts = _properties.Values.ToArray();

for (int i = 0, propLength = prop.Length; i < propLength; i++)
foreach (var context in contexts)
{
BindablePropertyContext context = prop[i];
if (ReferenceEquals(context.Property, BindingContextProperty))
{
// BindingContextProperty Binding is handled separately within SetInheritedBindingContext
Expand Down
194 changes: 39 additions & 155 deletions src/Core/tests/Benchmarks/Benchmarks/BindableObjectAllocBenchmarker.cs
Original file line number Diff line number Diff line change
Expand Up @@ -6,52 +6,21 @@ namespace Microsoft.Maui.Handlers.Benchmarks
[MemoryDiagnoser]
public class BindableObjectAllocBenchmarker
{
// --- #34092: Cached PropertyChangedEventArgs / PropertyChangingEventArgs ---

/// <summary>
/// Sets the same property repeatedly to measure PropertyChanged/Changing EventArgs allocations.
/// Before: new PropertyChangedEventArgs + new PropertyChangingEventArgs per call.
/// After: cached per BindableProperty.
/// </summary>
[Benchmark]
public void SetProperty_EventArgsAlloc()
Label _label;
Label _child;
VerticalStackLayout _deepTreeLeaf;
VerticalStackLayout _flatLayout;
object _contextA, _contextB;
bool _toggle;

[GlobalSetup]
public void Setup()
{
var label = new Label();
for (int i = 0; i < 1_000; i++)
{
label.Text = "a";
label.Text = "b";
}
}
_label = new Label();
_contextA = new object();
_contextB = new object();

/// <summary>
/// Sets multiple different properties to show caching benefit across properties.
/// </summary>
[Benchmark]
public void SetMultipleProperties_EventArgsAlloc()
{
var entry = new Entry();
for (int i = 0; i < 500; i++)
{
entry.Text = "a";
entry.Placeholder = "p";
entry.FontSize = 14 + (i % 3);
entry.Text = "b";
entry.Placeholder = "q";
}
}

// --- #34093: Reuse ElementEventArgs in tree propagation ---

/// <summary>
/// Adds children to a deep hierarchy, triggering DescendantAdded propagation.
/// Before: new ElementEventArgs at every tree level.
/// After: single ElementEventArgs reused through recursion.
/// </summary>
[Benchmark]
public void AddChildren_DeepTree_ElementEventArgs()
{
// Build a 10-level deep tree
// 10-level deep tree for DescendantAdded/Removed propagation
var root = new VerticalStackLayout();
var current = root;
for (int depth = 0; depth < 10; depth++)
Expand All @@ -60,133 +29,48 @@ public void AddChildren_DeepTree_ElementEventArgs()
current.Add(child);
current = child;
}
_child = new Label();
_deepTreeLeaf = current;

// Add 100 leaves at the bottom — each fires DescendantAdded 10 levels up
for (int i = 0; i < 100; i++)
{
current.Add(new Label());
}
// Flat layout with 200 children for BindingContext propagation
_flatLayout = new VerticalStackLayout();
for (int i = 0; i < 200; i++)
_flatLayout.Add(new Label());
}

/// <summary>
/// Removes children from a deep hierarchy, triggering DescendantRemoved propagation.
/// </summary>
[Benchmark]
public void RemoveChildren_DeepTree_ElementEventArgs()
{
var root = new VerticalStackLayout();
var current = root;
for (int depth = 0; depth < 10; depth++)
{
var child = new VerticalStackLayout();
current.Add(child);
current = child;
}

var labels = new Label[100];
for (int i = 0; i < 100; i++)
{
labels[i] = new Label();
current.Add(labels[i]);
}
// --- #34092: Cached PropertyChangedEventArgs / PropertyChangingEventArgs ---

for (int i = 0; i < 100; i++)
{
current.Remove(labels[i]);
}
[Benchmark(Description = "SetValue (EventArgs)")]
public void SetProperty_EventArgs()
{
_toggle = !_toggle;
_label.Text = _toggle ? "a" : "b";
}

// --- #34129: BindingContext propagation (WeakReference reuse + .ToArray() elimination) ---
// --- #34093: Reuse ElementEventArgs in tree propagation ---

class SimpleViewModel
[Benchmark(Description = "Add+Remove child (10-deep tree)")]
public void AddRemoveChild_DeepTree()
{
public string Name { get; set; } = "Test";
_deepTreeLeaf.Add(_child);
_deepTreeLeaf.Remove(_child);
}

/// <summary>
/// Sets BindingContext on a flat layout with many children.
/// Before: new WeakReference per child + .ToArray() on each child's _properties.
/// After: reuse WeakReference.Target + foreach on dictionary directly.
/// </summary>
[Benchmark]
// --- #34129: BindingContext propagation (WeakReference reuse + .ToArray() elimination) ---

[Benchmark(Description = "Set BindingContext (200 children)")]
public void SetBindingContext_FlatTree()
{
var layout = new VerticalStackLayout();
for (int i = 0; i < 200; i++)
{
layout.Add(new Label());
}

var vm = new SimpleViewModel();
for (int i = 0; i < 10; i++)
{
layout.BindingContext = vm;
layout.BindingContext = null;
}
}

/// <summary>
/// Sets BindingContext on a deep tree where children have bindings.
/// This is the worst-case hot path: ApplyBindings + WeakReference for every descendant.
/// </summary>
[Benchmark]
public void SetBindingContext_DeepTreeWithBindings()
{
var root = new VerticalStackLayout();
var current = root;

for (int depth = 0; depth < 5; depth++)
{
var child = new VerticalStackLayout();
current.Add(child);
current = child;
}

for (int i = 0; i < 50; i++)
{
var label = new Label();
label.SetBinding(Label.TextProperty, new Binding("Name"));
current.Add(label);
}

var vm = new SimpleViewModel();
for (int i = 0; i < 20; i++)
{
root.BindingContext = vm;
root.BindingContext = null;
}
_toggle = !_toggle;
_flatLayout.BindingContext = _toggle ? _contextA : _contextB;
}

// --- #34131: Lazy _triggerSpecificity dictionary ---

/// <summary>
/// Creates many BindableObjects (Labels) that never use triggers.
/// Before: each allocates Dictionary&lt;TriggerBase, SetterSpecificity&gt;.
/// After: dictionary is null until first trigger attachment.
/// </summary>
[Benchmark]
public Label[] CreateManyLabels_NoTriggers()
{
var labels = new Label[1_000];
for (int i = 0; i < 1_000; i++)
{
labels[i] = new Label();
}
return labels;
}
[Benchmark(Description = "new Label()")]
public Label CreateLabel() => new Label();

/// <summary>
/// Creates many Entries (more complex BindableObject) without triggers.
/// </summary>
[Benchmark]
public Entry[] CreateManyEntries_NoTriggers()
{
var entries = new Entry[500];
for (int i = 0; i < 500; i++)
{
entries[i] = new Entry();
}
return entries;
}
[Benchmark(Description = "new Entry()")]
public Entry CreateEntry() => new Entry();
}
}
Loading