dotnet / dotnet/maui

[leak-scan] PathGeometry.Figures — shared collection strongly retains the path geometry

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

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

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.