fix(alc): stop static Type-keyed caches pinning collectible AssemblyLoadContexts by nickrandolph · Pull Request #23630 · unoplatform/uno · GitHub
Skip to content

fix(alc): stop static Type-keyed caches pinning collectible AssemblyLoadContexts#23630

Open
nickrandolph wants to merge 5 commits into
dev/nr/alc-weak-associated-parentfrom
dev/nr/alc-weak-type-keyed-caches
Open

fix(alc): stop static Type-keyed caches pinning collectible AssemblyLoadContexts#23630
nickrandolph wants to merge 5 commits into
dev/nr/alc-weak-associated-parentfrom
dev/nr/alc-weak-type-keyed-caches

Conversation

@nickrandolph

Copy link
Copy Markdown
Contributor

ALC pin-fix stack — PR 2 of 4 (uno). Base: dev/nr/alc-weak-associated-parent. Next: U3 (ItemsView DEBUG) stacks on this branch.

Fixes #23629

Copilot AI review requested due to automatic review settings July 3, 2026 13:15
@CLAassistant

Copy link
Copy Markdown

@mergify

mergify Bot commented Jul 3, 2026

Copy link
Copy Markdown
Contributor

Tick the box to add this pull request to the merge queue (same as @mergifyio queue).

  • Queue this pull request

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

This PR addresses issue #23629 by replacing several static, Type-keyed caches with weak-keyed alternatives to avoid pinning collectible AssemblyLoadContext instances (e.g., plugin/preview hosting) for the lifetime of the process.

Changes:

  • Replace strong Dictionary<Type, …> / HashtableEx outer maps with ConditionalWeakTable<Type, …> for routed-event flags, default style caching, and DP registries.
  • Adjust DP name-to-property caching to avoid caching entries for types loaded from non-default (collectible) ALCs.
  • Replace ContentControl’s per-type “has default template” memoization with a weak-keyed cache.

Reviewed changes

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

Show a summary per file
File Description
src/Uno.UI/UI/Xaml/UIElementGeneratedProxy.cs Switch routed-event implementation cache to weak Type keys via ConditionalWeakTable.
src/Uno.UI/UI/Xaml/Style/Style.cs Replace default-style caches with weak-keyed tables; keep provider lookups strongly keyed with explicit eviction.
src/Uno.UI/UI/Xaml/DependencyProperty.Dictionary.PropertiesRegistry.cs Make DP registry outer map weak-keyed by owner Type to avoid ALC pinning.
src/Uno.UI/UI/Xaml/DependencyProperty.Dictionary.NameToProperty.cs Skip caching composite DP lookup entries when the involved type is from a collectible ALC.
src/Uno.UI/UI/Xaml/DependencyProperty.Dictionary.FrameworkPropertiesForType.cs Make “framework properties for type” cache weak-keyed by Type.
src/Uno.UI/UI/Xaml/Controls/ContentControl/ContentControl.cs Replace strong per-type memoization for “default template exists” with a weak-keyed cache.

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

Comment thread src/Uno.UI/UI/Xaml/Controls/ContentControl/ContentControl.cs
Comment thread src/Uno.UI/UI/Xaml/Style/Style.cs
Nick Randolph and others added 5 commits July 3, 2026 23:36
…pin collectible types

ContentControl.HasDefaultTemplate is a static, process-lifetime memoized Func<Type,bool>
backed by a Dictionary<Type,bool>. It is queried with the type of every measured
ContentControl, so once a control type from a collectible AssemblyLoadContext (plugin /
preview hosting) is measured, the dictionary retains that type's RuntimeType ->
LoaderAllocator and pins the entire ALC for the process lifetime. Dump analysis showed this
single cache as the sole external root pinning previewed-app ALCs.

Back the memoization with a ConditionalWeakTable<Type, StrongBox<bool>> instead: type
identity is canonical so weak-keyed lookup is equivalent, but a collectible type is now
collectable once otherwise unreferenced. General fix (not ALC-specific): removes a
steady managed leak for any long-running host that measures transient control types.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
… types

Style._defaultStyleCache / _nativeDefaultStyleCache mapped Type -> resolved default Style in
a process-lifetime Dictionary. Queried during rendering with the type of every styled control,
they retained control types from collectible AssemblyLoadContexts (plugin / preview hosting) via
RuntimeType -> LoaderAllocator, pinning the whole ALC. The existing ClearCachesForNonDefaultAlc
eviction cannot win the race: a render after unload re-populates the entry.

Back both caches with a ConditionalWeakTable<Type, StrongBox<Style?>> (weak identity keys; the
StrongBox lets a null resolved style still be cached). Type identity is canonical so lookup is
equivalent, but a collectible type is now collectable once otherwise unreferenced — structurally
removing the pin regardless of render/unload ordering. The strongly-keyed provider lookups still
use explicit eviction.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The DependencyProperty registry (Type -> per-type property table) and the inherited-
properties-for-type cache were strong HashtableEx maps keyed by owner Type. Registering /
looking up a DependencyProperty for a control type from a collectible AssemblyLoadContext
retained that type (Type -> RuntimeType -> LoaderAllocator), pinning the whole ALC; the
existing RemoveNonDefaultAlcEntries eviction loses the race against the unloading app's
residual DP activity re-populating the entry.

Back the outer Type-keyed maps with ConditionalWeakTable<Type, ...> (weak identity keys).
Type identity is canonical so lookup is equivalent; collectible types are now collectable
once otherwise unreferenced, removing the pin structurally. Eviction is retained as eager
cleanup. Inner per-type name tables stay strong and collect with their weak type key.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…nt caches

- NameToPropertyDictionary: skip caching entries whose owner type is from a
  collectible ALC (composite key can't be weak-keyed); they re-resolve via the
  weak-keyed registry.
- UIElementGeneratedProxy: weak-key the Type -> RoutedEventFlag cache via
  ConditionalWeakTable (a miss must not be misread as no-routed-events, so keep
  caching with weak keys rather than skipping).
Addresses review feedback (#23629): avoid the null-forgiving operator by
returning false for a null type instead of calling the non-nullable
GetDefaultStyleForType with a suppressed null.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@nickrandolph nickrandolph force-pushed the dev/nr/alc-weak-type-keyed-caches branch from 1cd1a4f to 91074fb Compare July 3, 2026 13:36
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.

3 participants