dotnet / dotnet/maui

[leak-scan] GeometryGroup.Children — shared collection strongly retains the geometry group

Open
#37,582 0 comments 0 reactions 0 assignees View on GitHub
agentic-workflows perf/memory-leak 💦
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

Open the contributing 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

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.