Skip to content
Merged
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
2 changes: 1 addition & 1 deletion src/Controls/src/Core/BindableObject.cs
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ public BindableObject()
}

internal ushort _triggerCount = 0;
internal Dictionary<TriggerBase, SetterSpecificity> _triggerSpecificity = new Dictionary<TriggerBase, SetterSpecificity>();
internal Dictionary<TriggerBase, SetterSpecificity> _triggerSpecificity;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would it work to add

#nullable enable

internal Dictionary<TriggerBase, SetterSpecificity>? _triggerSpecificity;

#nullable disable

?

Or would it still cascade to more and more changes?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That should work, but I don't think it has high value right now. We should let copilot annotate the whole class/codebase at some point.

readonly Dictionary<BindableProperty, BindablePropertyContext> _properties = new Dictionary<BindableProperty, BindablePropertyContext>(4);
bool _applying;
WeakReference _inheritedContext;
Expand Down
5 changes: 3 additions & 2 deletions src/Controls/src/Core/Interactivity/TriggerBase.cs
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,7 @@ internal virtual void OnAttachedTo(BindableObject bindable)
var manualSpecificity = (ushort)(SetterSpecificity.ManualTriggerBaseline + triggerIndex);
var specificity = new SetterSpecificity(0, manualSpecificity, 0, 0, 0, 0, 0, 0);

bindable._triggerSpecificity ??= new Dictionary<TriggerBase, SetterSpecificity>();
bindable._triggerSpecificity[this] = specificity;
Condition.SetUp(bindable);
}
Expand All @@ -100,7 +101,7 @@ internal virtual void OnDetachingFrom(BindableObject bindable)
if (Condition != null)
{
Condition.TearDown(bindable);
bindable._triggerSpecificity.Remove(this);
bindable._triggerSpecificity?.Remove(this);
}
}

Expand All @@ -116,7 +117,7 @@ internal virtual void OnSeal()

void OnConditionChanged(BindableObject bindable, bool oldValue, bool newValue)
{
if (!bindable._triggerSpecificity.TryGetValue(this, out var specificity))
if (bindable._triggerSpecificity?.TryGetValue(this, out var specificity) != true)
{
// this should never happen
return;
Expand Down
192 changes: 192 additions & 0 deletions src/Core/tests/Benchmarks/Benchmarks/BindableObjectAllocBenchmarker.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,192 @@
using BenchmarkDotNet.Attributes;
using Microsoft.Maui.Controls;

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()
{
var label = new Label();
for (int i = 0; i < 1_000; i++)
{
label.Text = "a";
label.Text = "b";
}
}

/// <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
var root = new VerticalStackLayout();
var current = root;
for (int depth = 0; depth < 10; depth++)
{
var child = new VerticalStackLayout();
current.Add(child);
current = child;
}

// Add 100 leaves at the bottom — each fires DescendantAdded 10 levels up
for (int i = 0; i < 100; i++)
{
current.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]);
}

for (int i = 0; i < 100; i++)
{
current.Remove(labels[i]);
}
}

// --- #34129: BindingContext propagation (WeakReference reuse + .ToArray() elimination) ---

class SimpleViewModel
{
public string Name { get; set; } = "Test";
}

/// <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]
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;
}
}

// --- #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;
}

/// <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;
}
}
}
Loading