[leak-scan] PathFigure.Segments — shared collection strongly retains the path figure
- 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 `PathSegmentCollection` to a transient `PathFigure.Segments` strongly retains the figure. The figure subscribes directly to the shared collection and only unsubscribes when `Segments` is replaced.
**Retention path:** shared `PathSegmentCollection` -> `CollectionChanged` invocation list -> `PathFigure.OnPathSegmentCollectionChanged` -> transient figure -> 1 MB payload.
Source: `src/Controls/src/Core/Shapes/PathFigure.cs:27-35` wires the property callback; `PathFigure.cs:99-108` adds/removes collection handlers only during property changes. There is no owner unload 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;
using Microsoft.Maui.Controls.Shapes;
using Xunit;
public class LeakTest
{
const int N = 30;
[Fact]
public void PathFigure_Segments_Leaks()
{
var shared = new PathSegmentCollection();
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(PathSegmentCollection shared, int scenario)
{
var refs = new List();
for (var i = 0; i < N; i++)
{
var subject = new PathFigure { BindingContext = new byte[1024 * 1024] };
if (scenario != 0) subject.Segments = shared;
if (scenario == 2) subject.Segments = new PathSegmentCollection();
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 `Segments`) | 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 segment collection and not replacing it before discarding the figure.
**Suggested fix:** use a weak collection-changed subscription or lifecycle cleanup. **Scope:** a framework-hardening opportunity around shared shape data; callers otherwise need hidden 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 in src/Controls/src/Core/Shapes/PathFigure.cs, especially lines 27-35 and 99-108, to trace the Segments property callback and collection event subscription. Run the supplied LeakTest.cs reproduction with dotnet test, then verify that discarding figures assigned to a shared PathSegmentCollection no longer retains them or their payloads.
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
- 55/100