Skip to content

[WIP][Perf] Reduce BindableObject allocations (event args caching, tree propagation, lazy init) - #34132

Closed
simonrozsival wants to merge 10 commits into
net11.0from
dev/simonrozsival/bindable-object-perf
Closed

[WIP][Perf] Reduce BindableObject allocations (event args caching, tree propagation, lazy init)#34132
simonrozsival wants to merge 10 commits into
net11.0from
dev/simonrozsival/bindable-object-perf

Conversation

@simonrozsival

Copy link
Copy Markdown
Member

Note

Are you waiting for the changes in this PR to be merged?
It would be very helpful if you could test the resulting artifacts from this PR and let us know in a comment if this change resolves your issue. Thank you!

Fixes #34092
Fixes #34093
Fixes #34129
Fixes #34131

Description

Four targeted allocation reductions in the BindableObject property system — all on hot paths that fire during page load, BindingContext propagation, and layout.

1. Cache PropertyChangedEventArgs / PropertyChangingEventArgs (#34092)

Every SetValue call allocated new PropertyChangedEventArgs and PropertyChangingEventArgs. Since BindableProperty.PropertyName is immutable, these are now cached on the BindableProperty instance via lazy ??= fields.

Files: BindableProperty.cs, BindableObject.cs

2. Reuse ElementEventArgs in tree propagation (#34093)

OnDescendantAdded/OnDescendantRemoved allocated a new ElementEventArgs at every tree level during parent propagation. Now creates the args once and passes the same instance up the tree.

File: Element.cs

3. Reduce inherited BindingContext allocations (#34129)

Two fixes:

  • Reuse WeakReference: SetInheritedBindingContext now updates _inheritedContext.Target instead of allocating a new WeakReference per descendant.
  • Eliminate .ToArray() in ApplyBindings: Replaced the snapshot array allocation with direct dictionary iteration.

File: BindableObject.cs

4. Lazy-initialize _triggerSpecificity dictionary (#34131)

The Dictionary<TriggerBase, SetterSpecificity> was eagerly allocated on every BindableObject, but <5% of objects use triggers. Now lazy-initialized on first use.

Files: BindableObject.cs, TriggerBase.cs

Changes

File Lines
BindableObject.cs +24 / -9
BindableProperty.cs +9
Element.cs +22 / -5
TriggerBase.cs +3 / -3

simonrozsival and others added 10 commits January 19, 2026 21:54
Introduce EventTrigger.Create<T>() factory methods that use static lambdas
instead of reflection-based event subscription. The XAML SourceGen now emits
calls to these factory methods for AOT compatibility.

Architecture:
- Single EventTrigger class with internal strategy pattern
- ReflectionStrategy for backward compat (annotated with RUC/RDC)
- StaticStrategy<T> for AOT-safe event subscription
- No new public types - strategies are private nested classes

Backward compatibility:
- Existing new EventTrigger() constructor still works
- EventTrigger.Event property is still set by SourceGen
- All existing XAML works unchanged

Fixes #33591
The SourceGen was generating both 'new EventTrigger()' and
'EventTrigger.Create<T>()' for the same EventTrigger, resulting
in dead code. This change defers EventTrigger creation from
CreateValuesVisitor to SetPropertiesVisitor when the Event
property is present, allowing the factory method to be used
directly.

Changes:
- EventTriggerValueProvider now marks EventTrigger nodes for
  deferred creation instead of skipping them entirely
- CreateValuesVisitor registers variable names for deferred
  EventTriggers without emitting creation code
- SetEventTriggerEvent emits the full variable declaration
  with the factory call
- Added DeferredEventTriggers set to SourceGenContext to track
  nodes that need deferred creation
- Updated Maui33591SourceGenTests snapshot to verify clean output
Move EventTrigger declaration to CreateValuesVisitor to fix variable ordering.
EventTrigger children (TriggerActions) need the variable declared before they're
processed in SetPropertiesVisitor (which uses bottom-up order).

Key changes:
- CreateValuesVisitor: Emit EventTrigger.Create<T>() call directly by walking up
  the XAML tree to find the target type
- EventTriggerValueProvider: Simplified to just return existing variable name
- EventTrigger.Create: Added eventName parameter for cleaner one-line generation
- Updated snapshot test to match new output
Consolidate EventTrigger-specific code in one place:
- Add EmitDeclaration static method with all generation logic
- Add FindTargetType helper method
- CreateValuesVisitor now just calls EmitDeclaration
- CreateValuesVisitor registers the variable name
- GenerateCreateInstanceCall generates the Create<T>() or new EventTrigger() call
- Cleaner separation of concerns
CreateValuesVisitor: writer.Write($"var {varName} = ");
EventTriggerValueProvider: writer.WriteLine("EventTrigger.Create<...>(...);")
Fixes #34093

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Fixes #34129

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…bleProperty

Fixes #34092

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Fixes #34131

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@simonrozsival simonrozsival added perf/general The issue affects performance (runtime speed, memory usage, startup time, etc.) (sub: perf) copilot labels Feb 19, 2026
@simonrozsival

Copy link
Copy Markdown
Member Author

Superseded by individual PRs: #34133, #34134, #34135, #34136

@github-actions github-actions Bot locked and limited conversation to collaborators Mar 22, 2026
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

copilot perf/general The issue affects performance (runtime speed, memory usage, startup time, etc.) (sub: perf)

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant