[leak-scan] GeometryGroup.Children — shared collection strongly retains the geometry group
- Dominant language
- C#
- Stars
- 23.3k
- Forks
- 2k
- Avg merge
- 1d 15h
- Merged PRs (30d)
- 290
Description
> [!IMPORTANT]
> AI-generated by the **Daily Memory Leak Hunter** workflow.
## Description
Assigning a long-lived `GeometryCollection` to a transient `GeometryGroup.Children` strongly retains the group after its owner is discarded. `AttachCollection` subscribes directly to the shared collection and cleanup occurs only when `Children` is replaced.
**Retention path:** shared `GeometryCollection` -> `CollectionChanged` invocation list -> `GeometryGroup.OnChildrenCollectionChanged` -> transient group -> 1 MB payload.
Source: `src/Controls/src/Core/Shapes/GeometryGroup.cs:18-24` wires the property callback; `GeometryGroup.cs:59-72` attaches the collection; `GeometryGroup.cs:80-90` only detaches during property replacement. There is no unload/navigation teardown.
## Standalone repro
`leakprobe.csproj`:
```xml
net10.0enablefalse
```
`LeakTest.cs`:
```csharp
using System;
using System.Collections.Generic;
using System.Runtime.CompilerServices;
using Microsoft.Maui.Controls.Shapes;
using Xunit;
public class LeakTest
{
const int N = 30;
[Fact]
public void GeometryGroup_Children_Leaks()
{
var shared = new GeometryCollection();
var control = Create(shared, 0); var leaky = Create(shared, 1); var mitigation = Create(shared, 2);
ForceGc();
Assert.Equal(0, Alive(control)); Assert.Equal(N, Alive(leaky)); Assert.Equal(0, Alive(mitigation));
GC.KeepAlive(shared);
}
[MethodImpl(MethodImplOptions.NoInlining)]
static List Create(GeometryCollection shared, int scenario)
{
var refs = new List();
for (var i = 0; i < N; i++)
{
var subject = new GeometryGroup { BindingContext = new byte[1024 * 1024] };
if (scenario != 0) subject.Children = shared;
if (scenario == 2) subject.Children = new GeometryCollection();
refs.Add(new WeakReference(subject));
}
return refs;
}
static int Alive(IEnumerable refs) { var n = 0; foreach (var r in refs) if (r.IsAlive) n++; return n; }
static void ForceGc() { for (var i = 0; i < 7; i++) { GC.Collect(); GC.WaitForPendingFinalizers(); GC.Collect(); } }
}
```
Run: `dotnet test --logger "console;verbosity=normal"`
## Observed results
| Scenario | Alive | Retained payload |
|---|---:|---:|
| Control | 0/30 | 0 MB |
| Mitigation (replace `Children`) | 0/30 | 0 MB |
| Leaky (shared collection remains assigned) | 30/30 | 30 MB |
This managed path affects all platforms. It requires assigning a shared/long-lived `GeometryCollection` and not replacing it before discarding the group.
**Suggested fix:** use a weak collection-changed proxy or lifecycle cleanup. **Scope:** a framework-hardening opportunity around a supported shared collection; callers currently need non-obvious manual teardown.
> Generated by [Daily Memory Leak Hunter](https://github.com/dotnet/maui/actions/runs/32140707109) · gpt56 · 134.2 AIC · ⌖ 23.5 AIC · ⊞ 32.1K · [◷](https://github.com/search?q=repo%3Adotnet%2Fmaui+is%3Aissue+%22gh-aw-workflow-call-id%3A+dotnet%2Fmaui%2Fdaily-leak-hunter%22&type=issues)
Contributor guide
Research direction
Start with src/Controls/src/Core/Shapes/GeometryGroup.cs, especially lines 18-24, 59-72, and 80-90, to trace collection subscription and detachment. Run the standalone repro with dotnet test --logger "console;verbosity=normal" and verify that transient groups using a shared GeometryCollection are collectible without replacing Children, while the control and mitigation scenarios remain collectible.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- csharp
- Domain
- frontend
- Issue type
- Bug
- Difficulty
- 4/5
- Estimated time
- 3-5 days
- Activity status
- Active
- Clarity
- Mostly clear
- Newbie friendliness
- 52/100