[leak-scan] PathGeometry.Figures — shared collection strongly retains the path geometry
- Dominant language
- C#
- Stars
- 23.3k
- Forks
- 2k
- Avg merge
- 1d 10h
- Merged PRs (30d)
- 297
Description
> [!IMPORTANT]
> AI-generated by the **Daily Memory Leak Hunter** workflow.
## Description
Assigning a long-lived `PathFigureCollection` to a transient `PathGeometry.Figures` strongly retains the geometry. The geometry directly subscribes to the shared collection and only detaches when the property is replaced.
**Retention path:** shared `PathFigureCollection` -> `CollectionChanged` invocation list -> `PathGeometry.OnPathFigureCollectionChanged` -> transient geometry -> 1 MB payload.
Source: `src/Controls/src/Core/Shapes/PathGeometry.cs:230-241` removes the old collection handler and directly adds the new handler; no unload/navigation teardown exists.
## Standalone repro
`leakprobe.csproj`:
```xml
net10.0enablefalse
```
`LeakTest.cs`:
```csharp
using System;
using System.Collections.Generic;
using System.Runtime.CompilerServices;
using Microsoft.Maui.Controls;
using Microsoft.Maui.Controls.Shapes;
using Xunit;
public class LeakTest
{
const int N = 30;
[Fact]
public void PathGeometry_Figures_Leaks()
{
var shared = new PathFigureCollection();
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(PathFigureCollection shared, int scenario)
{
var refs = new List();
for (var i = 0; i < N; i++)
{
var subject = new PathGeometry { BindingContext = new byte[1024 * 1024] };
if (scenario != 0) subject.Figures = shared;
if (scenario == 2) subject.Figures = new PathFigureCollection();
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 `Figures`) | 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 figure collection and not replacing it before discarding the geometry.
**Suggested fix:** use a weak collection-changed proxy or lifecycle cleanup. **Scope:** a framework-hardening opportunity around supported shared shape collections; callers currently need non-obvious 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 at src/Controls/src/Core/Shapes/PathGeometry.cs:230-241 and review how the Figures collection-change handler is attached and removed. Run the standalone LeakTest.cs repro with dotnet test to confirm the retention behavior. Done means the shared collection no longer keeps discarded PathGeometry instances and the repro's leaky scenario reports zero retained geometries.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- csharp
- Domain
- performance
- Issue type
- Bug
- Difficulty
- 4/5
- Estimated time
- 3-5 days
- Activity status
- Active
- Clarity
- Mostly clear
- Newbie friendliness
- 45/100