Skip to content

Label.FormattedText leaks labels when shared FormattedString is stored in Application.Resources #35495

@AdamEssenmacher

Description

@AdamEssenmacher

Description

When multiple transient Label instances use the same long-lived FormattedString from Application.Resources, the shared FormattedString retains each label after the containing pages are popped and full GC runs.

The repro models a realistic checkout/account review flow where app-level rich disclosure text is reused across many transient pages. Each disclosure label has a row view model as its BindingContext, with a 160 KB payload representing realistic row/page state. The leak is therefore not limited to the label object: the retained label keeps its row view model and associated payload alive.

Repro project:

https://github.com/AdamEssenmacher/maui/tree/repro/formattedtext-resource-leak/src/Controls/samples/FormattedTextLeakRepro

With defaults:

Pages/run: 30
Disclosures/page: 24
Payload KB/disclosure: 160
Dwell ms/page: 25

Observed final metrics:

Platform Scenario Labels alive Row VMs alive Payload retained Managed heap delta GC heap delta
iOS Simulator, iPhone 17e, iOS 26.4 Inline control 0/720 0/720 0 B 372.6 KB 3.3 MB
iOS Simulator, iPhone 17e, iOS 26.4 Shared resource 720/720 720/720 112.5 MB 125.1 MB 135.3 MB
iOS Simulator, iPhone 17e, iOS 26.4 Mitigation 0/720 0/720 0 B 636.2 KB -1.2 MB
Android Emulator, Pixel_9a, sdk_gphone16k_arm64 Inline control 0/720 0/720 0 B 1.2 MB 1.5 MB
Android Emulator, Pixel_9a, sdk_gphone16k_arm64 Shared resource 720/720 720/720 112.5 MB 122.5 MB 130.9 MB
Android Emulator, Pixel_9a, sdk_gphone16k_arm64 Mitigation 0/720 0/720 0 B 43.2 KB 624.0 KB

The apparent retention chain is:

Application.Resources
  -> shared FormattedString
  -> PropertyChanging / PropertyChanged / SpansCollectionChanged invocation lists
  -> Label event handlers
  -> Label.BindingContext
  -> row view model payload

The mitigation scenario sets Label.FormattedText = null in OnDisappearing, which returns the weak-reference counts to zero. That suggests the leak is caused by event subscriptions from the shared FormattedString back to each label, and that detaching FormattedText releases the retained UI and binding-context graph.

Steps to Reproduce

  1. Clone the repro branch:
git clone https://github.com/AdamEssenmacher/maui.git
cd maui
git checkout repro/formattedtext-resource-leak
  1. Build the MAUI build tasks, then build the repro app:
dotnet build Microsoft.Maui.BuildTasks.slnf
dotnet build src/Controls/samples/FormattedTextLeakRepro/FormattedTextLeakRepro.csproj -f net10.0-android
dotnet build src/Controls/samples/FormattedTextLeakRepro/FormattedTextLeakRepro.csproj -f net10.0-ios -p:RuntimeIdentifier=iossimulator-arm64
  1. Launch the repro app on Android or iOS.

  2. Leave the default settings:

Pages/run: 30
Disclosures/page: 24
Payload KB/disclosure: 160
Dwell ms/page: 25
  1. Tap Run inline control.

  2. Observe that after full GC, the control releases the transient pages, labels, and row view models:

disclosure labels: 0/720
row view models: 0/720
Payload retained: 0 B
  1. Tap Run shared resource.

  2. Observe that the app-rooted shared FormattedString instances retain every transient label and row view model after the pages are popped and full GC runs:

disclosure labels: 720/720
row view models: 720/720
Payload retained: 112.5 MB
  1. Optional: tap Run mitigation.

  2. Observe that setting Label.FormattedText = null in OnDisappearing releases the graph again:

disclosure labels: 0/720
row view models: 0/720
Payload retained: 0 B

Link to public reproduction project repository

https://github.com/AdamEssenmacher/maui/tree/repro/formattedtext-resource-leak/src/Controls/samples/FormattedTextLeakRepro

Version with bug

10.0.60

Is this a regression from previous behavior?

Not sure, did not test other versions

Last version that worked well

Unknown/Other

Affected platforms

iOS, Android, I was not able test on other platforms

Affected platform versions

No response

Did you find any workaround?

No response

Relevant log output

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions