Skip to content

Break the reference chain so old containers can be garbage collected.#11717

Closed
gpaskaleva-msft wants to merge 3 commits into
mainfrom
pr-11657
Closed

Break the reference chain so old containers can be garbage collected.#11717
gpaskaleva-msft wants to merge 3 commits into
mainfrom
pr-11657

Conversation

@gpaskaleva-msft

@gpaskaleva-msft gpaskaleva-msft commented Jun 12, 2026

Copy link
Copy Markdown
Contributor

Fixes #11337

Description

ElementProxy.cs:

  • _peer made non-readonly + ClearPeer() method — allows DisconnectPeerFromUia to sever the strong proxy → peer reference immediately (for non-UIElement peers like DataGridItemAutomationPeer)

AutomationPeer.cs:

  1. EnsureChildren() disconnect logic — detects removed peers when _children is refreshed during UIA traversal and calls DisconnectPeerFromUia on them.
  2. DisconnectPeerFromUia changes:
  • proxy.ClearPeer() — severs proxy→peer strong reference
  • Dispatcher.BeginInvoke for UiaDisconnectProvider — defers COM disconnect to outside UIA callback context
  • peer._children = null; peer._childrenValid = false; — the critical fix that breaks the chain from disconnected peer → cached cell peers → old DataGridCell/Row containers

Regression

No

Testing

Test Environment

  • Repro app (WPFUI): DataGrid with 200k rows, ItemsSource rebound every 3 seconds
  • UIA client (WPFAutomationClient): Calls FindAll(TreeScope.Descendants, TrueCondition) every 1 second
  • Runtime: Custom-built PresentationCore.dll deployed to local .NET 11 Preview 5 SDK

Tests Performed

# Fix Applied Result Duration
1 PR #11657 only (EnsureChildren disconnect) ❌ Memory grew to ~2500 MB 20 min
2 + ClearPeer() on ElementProxy ❌ No improvement ~5 min
3 + Deferred UiaDisconnectProvider via Dispatcher.BeginInvoke ❌ Still ~6-9 MB/cycle growth ~5 min
4 + Diagnostic counters (verified disconnects fire: 45/cycle, refreshes accelerating) ❌ 911→1019 MB in 14 cycles ~1 min
5 + peer._children = null; peer._childrenValid = false; in DisconnectPeerFromUia Stable 449-534 MB 3+ min

Risk

Microsoft Reviewers: Open in CodeFlow

amarinov-msft and others added 2 commits May 21, 2026 14:25
…ualized ItemsControls

When UIA clients enumerate a virtualized ItemsControl (e.g. DataGrid),
ElementProxy CCWs are created for each automation peer. Previously,
when children were replaced (e.g. due to collection rebinding), the
old peers were never disconnected from UIA Core. The COM references
held by UIA kept the CCW ref count > 0, pinning the managed peers and
their entire visual sub-trees in memory indefinitely.

This fix calls UiaDisconnectProvider on removed children's ElementProxy
CCWs during UpdateChildrenInternal, causing UIA Core to release its
COM references. This allows the CCW ref count to drop to zero so the
managed peers can be garbage collected.

Key changes:
- Add DisconnectPeerFromUia() that disconnects a peer by calling
  UiaDisconnectProvider on its ElementProxy (non-recursive to avoid
  disconnecting shared container peers in virtualized controls)
- Move the StructureChanged event check after disconnect logic in
  UpdateChildrenInternal so disconnection happens regardless of event
  registration
- P/Invoke UiaDisconnectProvider from UIAutomationCore.dll

Note: UIA clients may observe ElementNotAvailableException when
accessing properties of stale elements after disconnection. This is
standard UIA behavior that well-behaved clients already handle.

Fixes #11337

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@gpaskaleva-msft gpaskaleva-msft changed the title Pr 11657 Break the reference chain so old containers can be garbage collected. Jun 12, 2026
@amarinov-msft amarinov-msft marked this pull request as ready for review June 15, 2026 12:45
@amarinov-msft amarinov-msft requested review from a team and Copilot June 15, 2026 12:45
@amarinov-msft amarinov-msft requested a review from a team as a code owner June 15, 2026 12:45
amarinov-msft
amarinov-msft previously approved these changes Jun 15, 2026

@amarinov-msft amarinov-msft left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

The changes are looking good. We just need to merge #11723 here before merging this PR.

Copilot AI left a comment

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.

Pull request overview

Fixes a UI Automation-related memory leak in WPF by ensuring stale AutomationPeer instances (and their cached child-peer graphs) are actively disconnected from UIA and have their strong reference chains broken, allowing old/recycled containers to be garbage collected.

Changes:

  • Adds stale-child detection to EnsureChildren() and disconnects removed peers during UIA traversal refreshes.
  • Introduces DisconnectPeerFromUia() to clear proxy→peer references, defer UiaDisconnectProvider outside UIA callback context, and sever peer→children chains.
  • Updates ElementProxy to allow clearing its peer reference via a new ClearPeer() method.

Reviewed changes

Copilot reviewed 2 out of 2 changed files in this pull request and generated 3 comments.

File Description
src/Microsoft.DotNet.Wpf/src/PresentationCore/System/Windows/Automation/Peers/AutomationPeer.cs Adds disconnect logic during child refresh, introduces deferred UIA disconnect helper, and breaks peer reference chains to enable GC.
src/Microsoft.DotNet.Wpf/src/PresentationCore/MS/internal/Automation/ElementProxy.cs Makes _peer mutable and adds ClearPeer() to sever proxy→peer strong references during disconnect.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +1865 to +1869
peer.Dispatcher.BeginInvoke(System.Windows.Threading.DispatcherPriority.Background,
new Action(() =>
{
UiaDisconnectProvider(proxy);
}));
Comment on lines +1881 to +1882
[DllImport("UIAutomationCore.dll", EntryPoint = "UiaDisconnectProvider", CharSet = CharSet.Unicode)]
private static extern int UiaDisconnectProvider(IRawElementProviderSimple provider);
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
@h3xds1nz

Copy link
Copy Markdown
Member

Not gonna lie, this adds quite some overhead to already massively heavy invocation this recursive method call is.

@amarinov-msft amarinov-msft self-requested a review June 15, 2026 16:33
@amarinov-msft

Copy link
Copy Markdown

Not gonna lie, this adds quite some overhead to already massively heavy invocation this recursive method call is.

@h3xds1nz Can you take a look at the original PR . The fix there is identical - only the hot-path cost differs, Trying to minimize the changes inside EnsureChildren(), we just capture oldChildren and add one gated loop that does nothing unless a peer was both client-exposed and removed. That's the smallest possible addition that still finds every leaked peer. I will close this PR (and the related one, introducing the AppContext switch). If you have any considerations - you can comment in #11657

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

WPF leaks ElementProxy instances when UI Automation is used

4 participants