Repository navigation
Description
Important
AI-generated by the Daily Memory Leak Hunter workflow. The finding below was reproduced against the shipped Microsoft.Maui.Controls 10.0.0 package.
Description
Assigning a shared or otherwise long-lived PathSegmentCollection to PathFigure.Segments keeps the PathFigure and its binding context alive after application references are dropped.
PathFigure.UpdatePathSegmentCollection subscribes the figure instance to the collection's CollectionChanged event (src/Controls/src/Core/Shapes/PathFigure.cs:97-111). It unsubscribes only when Segments is replaced (PathFigure.cs:97-104), so ordinary collection of the figure cannot begin while the shared publisher retains its delegate.
Retention path
shared PathSegmentCollection -> CollectionChanged delegate -> PathFigure.OnPathSegmentCollectionChanged target -> PathFigure.BindingContext -> transient payload
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.Linq;
using System.Runtime.CompilerServices;
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 = CreateMany(() => Create(null, false));
var leaky = CreateMany(() => Create(shared, false));
var mitigation = CreateMany(() => Create(shared, true));
for (var i = 0; i < 7; i++)
{
GC.Collect();
GC.WaitForPendingFinalizers();
GC.Collect();
}
Assert.Equal(0, control.Count(x => x.IsAlive));
Assert.Equal(N, leaky.Count(x => x.IsAlive));
Assert.Equal(0, mitigation.Count(x => x.IsAlive));
GC.KeepAlive(shared);
}
[MethodImpl(MethodImplOptions.NoInlining)]
static WeakReference[] CreateMany(Func<WeakReference> factory) =>
Enumerable.Range(0, N).Select(_ => factory()).ToArray();
[MethodImpl(MethodImplOptions.NoInlining)]
static WeakReference Create(PathSegmentCollection? shared, bool mitigate)
{
var payload = new Payload();
var figure = new PathFigure { BindingContext = payload };
if (shared is not null)
figure.Segments = shared;
if (mitigate)
figure.Segments = new PathSegmentCollection();
return new WeakReference(payload);
}
sealed class Payload
{
readonly byte[] _bytes = new byte[1024 * 1024];
}
}Run:
dotnet test --logger "console;verbosity=normal"Observed results
| Scenario | Payloads alive after full GC | Retained payload |
|---|---|---|
| Control (private default collection) | 0 / 30 | 0 MB |
Mitigation (Segments replaced) |
0 / 30 | 0 MB |
Leaky (shared PathSegmentCollection remains assigned) |
30 / 30 | 30 MB |
Scope
This is purely managed code and affects all platforms. It occurs only when a PathSegmentCollection outlives the PathFigure; the normal private default collection does not leak. Replacing Segments before release avoids retention, but this is a usage footgun the framework can harden.
Suggested fix: subscribe to the external collection through a weak collection-changed proxy while retaining the existing deterministic unsubscribe when Segments changes.
Generated by Daily Memory Leak Hunter Β· gpt56 Β· 282.5 AIC Β· β 19.2 AIC Β· β 28K Β· β·
Activity
Fix PathFigure memory leak with shared PathSegmentCollection (#38744)
Fix PathFigure memory leak with shared PathSegmentCollection (#38744)
Fix PathFigure memory leak with shared PathSegmentCollection (#38744)
Fix PathFigure memory leak with shared PathSegmentCollection (#38744)
Fix PathFigure memory leak with shared PathSegmentCollection (#38744)
Fix PathFigure memory leak with shared PathSegmentCollection (#38744)
Fix PathFigure memory leak with shared PathSegmentCollection (#38744)
Fix PathFigure memory leak with shared PathSegmentCollection (#38744)
Fix PathFigure memory leak with shared PathSegmentCollection (#38744)
Fix PathFigure memory leak with shared PathSegmentCollection (#38744)
Fix PathFigure memory leak with shared PathSegmentCollection (#38744)