dotnet / dotnet/maui

[leak-scan] PathFigure.Segments — shared collection strongly retains the path figure

Open
#37,584 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 `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

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

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.