[leak-scan] Path.Data — shared Geometry change events retain the Path
Nobody has claimed this yet.
- Dominant language
- C#
- Stars
- 23.3k
- Forks
- 2k
- Avg merge
- 1d 14h
- Merged PRs (30d)
- 296
Description
[!IMPORTANT]
AI-generated by the Daily Memory Leak Hunter — dotnet/maui workflow. This finding was empirically confirmed against the shippedMicrosoft.Maui.Controls10.0.0 package on plainnet10.0.
Description
Assigning a shared Geometry to Path.Data strongly retains each discarded Path in the shipped package. The geometry's change notification holds the path's instance callback, so a long-lived geometry keeps the path and its managed object graph alive unless Data is explicitly cleared.
Retention path
shared LineGeometry -> PropertyChanged delegate -> Path data-change callback -> transient Path -> attached 1 MiB payload
src/Controls/src/Core/Shapes/Path.cs:29-38defines theDataproperty callbacks.src/Controls/src/Core/Shapes/Path.cs:75-94shows the corresponding notification subscription area. CurrentmainusesWeakGeometryChangedProxy; the shipped 10.0.0 package used by this repro still exhibits strong retention.
Standalone repro
leakprobe.csproj:
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net10.0</TargetFramework>
<Nullable>enable</Nullable>
<IsPackable>false</IsPackable>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.Maui.Controls" Version="10.0.0" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.11.1" />
<PackageReference Include="xunit" Version="2.9.2" />
<PackageReference Include="xunit.runner.visualstudio" Version="2.8.2" />
</ItemGroup>
</Project>
LeakTest.cs:
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;
static readonly BindableProperty PayloadProperty =
BindableProperty.CreateAttached("Payload", typeof(byte[]), typeof(LeakTest), null);
[Fact]
public void Path_Data_Leaks()
{
var shared = new LineGeometry();
var control = Allocate(shared, Scenario.Control);
var leaky = Allocate(shared, Scenario.Leaky);
var mitigation = Allocate(shared, Scenario.Mitigation);
ForceGc();
Assert.Equal(0, Alive(control));
Assert.Equal(N, Alive(leaky));
Assert.Equal(0, Alive(mitigation));
GC.KeepAlive(shared);
}
[MethodImpl(MethodImplOptions.NoInlining)]
static List<WeakReference> Allocate(LineGeometry shared, Scenario scenario)
{
var references = new List<WeakReference>(N);
for (var i = 0; i < N; i++)
{
var path = new Microsoft.Maui.Controls.Shapes.Path();
var payload = new byte[1024 * 1024];
path.SetValue(PayloadProperty, payload);
if (scenario != Scenario.Control)
path.Data = shared;
if (scenario == Scenario.Mitigation)
path.Data = null;
references.Add(new WeakReference(payload));
}
return references;
}
static int Alive(IEnumerable<WeakReference> references)
{
var alive = 0;
foreach (var reference in references)
if (reference.IsAlive)
alive++;
return alive;
}
[MethodImpl(MethodImplOptions.NoInlining)]
static void ForceGc()
{
for (var i = 0; i < 7; i++)
{
GC.Collect();
GC.WaitForPendingFinalizers();
GC.Collect();
}
}
enum Scenario { Control, Leaky, Mitigation }
}
Run:
dotnet test --logger "console;verbosity=normal"
Observed results
| Scenario | Payloads alive | Retained payload |
|---|---|---|
| Control: never assign shared geometry | 0 / 30 | 0 MiB |
Mitigation: assign, then clear Data |
0 / 30 | 0 MiB |
| Leaky: leave shared geometry assigned | 30 / 30 | 30 MiB |
Impact and suggested fix
This is purely managed behavior and affects all platforms. It requires reuse or long-lived caching of a geometry; per-view geometries that die with their path do not create this root.
Use a weak event proxy for geometry notifications and retain explicit unsubscription when Data changes. Current main appears hardened in this area, so the actionable scope is the shipped 10.0.0 package/servicing lineage rather than a new main implementation change.
Generated by Daily Memory Leak Hunter · gpt56 · 447.6 AIC · ⌖ 20.1 AIC · ⊞ 31.7K · ◷
Contributor guide
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
Research direction
Start with src/Controls/src/Core/Shapes/Path.cs, especially the Data callbacks at lines 29-38 and notification subscription area at lines 75-94, and compare current main with the shipped 10.0.0 behavior. Run the LeakTest.cs repro with dotnet test; done means the servicing lineage no longer retains discarded Path payloads while the control, leaky, and mitigation scenarios produce the expected results.
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