-
Notifications
You must be signed in to change notification settings - Fork 2k
[Perf] Lazy-initialize _triggerSpecificity dictionary on BindableObject #34133
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
StephaneDelcroix
merged 2 commits into
net11.0
from
dev/simonrozsival/lazy-trigger-specificity
Feb 23, 2026
+196
−3
Merged
Changes from all commits
Commits
Show all changes
2 commits
Select commit
Hold shift + click to select a range
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
192 changes: 192 additions & 0 deletions
192
src/Core/tests/Benchmarks/Benchmarks/BindableObjectAllocBenchmarker.cs
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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<TriggerBase, SetterSpecificity>. | ||
| /// 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; | ||
| } | ||
| } | ||
| } |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
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
?
Or would it still cascade to more and more changes?
There was a problem hiding this comment.
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.