diff --git a/src/Controls/src/Core/BindableObject.cs b/src/Controls/src/Core/BindableObject.cs index 60c66b8d2176..98588bfcc256 100644 --- a/src/Controls/src/Core/BindableObject.cs +++ b/src/Controls/src/Core/BindableObject.cs @@ -38,7 +38,7 @@ public BindableObject() } internal ushort _triggerCount = 0; - internal Dictionary _triggerSpecificity = new Dictionary(); + internal Dictionary _triggerSpecificity; readonly Dictionary _properties = new Dictionary(4); bool _applying; WeakReference _inheritedContext; diff --git a/src/Controls/src/Core/Interactivity/TriggerBase.cs b/src/Controls/src/Core/Interactivity/TriggerBase.cs index 979df7b16707..bf428356dbc4 100644 --- a/src/Controls/src/Core/Interactivity/TriggerBase.cs +++ b/src/Controls/src/Core/Interactivity/TriggerBase.cs @@ -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(); bindable._triggerSpecificity[this] = specificity; Condition.SetUp(bindable); } @@ -100,7 +101,7 @@ internal virtual void OnDetachingFrom(BindableObject bindable) if (Condition != null) { Condition.TearDown(bindable); - bindable._triggerSpecificity.Remove(this); + bindable._triggerSpecificity?.Remove(this); } } @@ -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; diff --git a/src/Core/tests/Benchmarks/Benchmarks/BindableObjectAllocBenchmarker.cs b/src/Core/tests/Benchmarks/Benchmarks/BindableObjectAllocBenchmarker.cs new file mode 100644 index 000000000000..078e00ec8a06 --- /dev/null +++ b/src/Core/tests/Benchmarks/Benchmarks/BindableObjectAllocBenchmarker.cs @@ -0,0 +1,192 @@ +using BenchmarkDotNet.Attributes; +using Microsoft.Maui.Controls; + +namespace Microsoft.Maui.Handlers.Benchmarks +{ + [MemoryDiagnoser] + public class BindableObjectAllocBenchmarker + { + // --- #34092: Cached PropertyChangedEventArgs / PropertyChangingEventArgs --- + + /// + /// Sets the same property repeatedly to measure PropertyChanged/Changing EventArgs allocations. + /// Before: new PropertyChangedEventArgs + new PropertyChangingEventArgs per call. + /// After: cached per BindableProperty. + /// + [Benchmark] + public void SetProperty_EventArgsAlloc() + { + var label = new Label(); + for (int i = 0; i < 1_000; i++) + { + label.Text = "a"; + label.Text = "b"; + } + } + + /// + /// Sets multiple different properties to show caching benefit across properties. + /// + [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 --- + + /// + /// Adds children to a deep hierarchy, triggering DescendantAdded propagation. + /// Before: new ElementEventArgs at every tree level. + /// After: single ElementEventArgs reused through recursion. + /// + [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()); + } + } + + /// + /// Removes children from a deep hierarchy, triggering DescendantRemoved propagation. + /// + [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"; + } + + /// + /// 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. + /// + [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; + } + } + + /// + /// Sets BindingContext on a deep tree where children have bindings. + /// This is the worst-case hot path: ApplyBindings + WeakReference for every descendant. + /// + [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 --- + + /// + /// Creates many BindableObjects (Labels) that never use triggers. + /// Before: each allocates Dictionary<TriggerBase, SetterSpecificity>. + /// After: dictionary is null until first trigger attachment. + /// + [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; + } + + /// + /// Creates many Entries (more complex BindableObject) without triggers. + /// + [Benchmark] + public Entry[] CreateManyEntries_NoTriggers() + { + var entries = new Entry[500]; + for (int i = 0; i < 500; i++) + { + entries[i] = new Entry(); + } + return entries; + } + } +}