-
Notifications
You must be signed in to change notification settings - Fork 2k
Fix for PathGeometry.Figures.Clear() leaks when clearing shared PathFigure instances #36057
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,94 @@ | ||
| using System; | ||
| using System.Runtime.CompilerServices; | ||
| using Microsoft.Maui.Controls.Shapes; | ||
| using Microsoft.Maui.Graphics; | ||
| using Xunit; | ||
|
|
||
| namespace Microsoft.Maui.Controls.Core.UnitTests.Shapes; | ||
|
|
||
| public class PathGeometryTests : BaseTestFixture | ||
| { | ||
| /// <summary> | ||
| /// Figures.Clear() must unsubscribe the cleared PathFigure from the PathGeometry, | ||
| /// otherwise the figure retains the geometry alive via its PropertyChanged delegate. | ||
| /// </summary> | ||
| [Fact] | ||
| public void FiguresClear_UnsubscribesFigurePropertyChangedHandler() | ||
| { | ||
| var geometry = new PathGeometry(); | ||
| var sharedFigure = new PathFigure { StartPoint = new Point(0, 0) }; | ||
| geometry.Figures.Add(sharedFigure); | ||
|
|
||
| int invalidateCount = 0; | ||
| geometry.InvalidatePathGeometryRequested += (s, e) => invalidateCount++; | ||
|
|
||
| // Sanity-check: mutating the figure before Clear should trigger invalidation. | ||
| sharedFigure.StartPoint = new Point(10, 10); | ||
| Assert.Equal(1, invalidateCount); | ||
|
|
||
| // Act - Clear() fires CollectionChanged (Reset), which itself calls Invalidate() once. | ||
| geometry.Figures.Clear(); | ||
| int countAfterClear = invalidateCount; | ||
|
|
||
| // After Clear, mutating the figure must NOT trigger any further invalidation on the geometry. | ||
| sharedFigure.StartPoint = new Point(20, 20); | ||
| Assert.Equal(countAfterClear, invalidateCount); | ||
| } | ||
|
|
||
| /// <summary> | ||
| /// Figures.Clear() must unsubscribe the cleared PathFigure's segment-invalidation event | ||
| /// from the PathGeometry. | ||
| /// </summary> | ||
| [Fact] | ||
| public void FiguresClear_UnsubscribesFigureSegmentInvalidateHandler() | ||
| { | ||
| var geometry = new PathGeometry(); | ||
| var sharedFigure = new PathFigure { StartPoint = new Point(0, 0) }; | ||
| geometry.Figures.Add(sharedFigure); | ||
|
|
||
| int invalidateCount = 0; | ||
| geometry.InvalidatePathGeometryRequested += (s, e) => invalidateCount++; | ||
|
|
||
| // Sanity-check: adding a segment before Clear should trigger invalidation. | ||
| sharedFigure.Segments.Add(new LineSegment { Point = new Point(100, 100) }); | ||
| Assert.Equal(1, invalidateCount); | ||
|
|
||
| // Act - Clear() fires CollectionChanged (Reset), which itself calls Invalidate() once. | ||
| geometry.Figures.Clear(); | ||
| int countAfterClear = invalidateCount; | ||
|
|
||
| // After Clear, adding segments to the cleared figure must NOT trigger any further invalidation. | ||
| sharedFigure.Segments.Add(new LineSegment { Point = new Point(200, 200) }); | ||
| Assert.Equal(countAfterClear, invalidateCount); | ||
| } | ||
|
|
||
| /// <summary> | ||
| /// After Figures.Clear(), the PathGeometry must be eligible for garbage collection | ||
| /// even when the cleared PathFigure is still alive (shared/rooted elsewhere). | ||
| /// </summary> | ||
| [Fact] | ||
| public void FiguresClear_AllowsPathGeometryToBeGarbageCollected() | ||
| { | ||
| var sharedFigure = new PathFigure { StartPoint = new Point(0, 0) }; | ||
| var weakRef = CreateGeometryAndClear(sharedFigure); | ||
|
|
||
| GC.Collect(); | ||
| GC.WaitForPendingFinalizers(); | ||
| GC.Collect(); | ||
|
|
||
| // If the bug is present, sharedFigure still holds the geometry alive via | ||
| // its PropertyChanged delegate chain, so TryGetTarget would return true. | ||
| Assert.False(weakRef.TryGetTarget(out _), | ||
| "PathGeometry was retained by the cleared PathFigure (event-handler leak in Figures.Clear())."); | ||
| } | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
[moderate] Regression Prevention — adjacent scenario coverage — The new tests only cover a single
|
||
|
|
||
| // Factored out so the JIT cannot inline the PathGeometry local onto the caller's frame. | ||
| [MethodImpl(MethodImplOptions.NoInlining)] | ||
| static WeakReference<PathGeometry> CreateGeometryAndClear(PathFigure figure) | ||
| { | ||
| var geometry = new PathGeometry(); | ||
| geometry.Figures.Add(figure); | ||
| geometry.Figures.Clear(); | ||
| return new WeakReference<PathGeometry>(geometry); | ||
| } | ||
| } | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
[major] Regression Prevention / Architectural Layer — This same Clear()-retention leak pattern (Reset action fires with
OldItems == null, so the old per-item-=unsubscribe loop never runs) still exists unfixed in two sibling types that use the identicalCollectionChangedwiring style:PathFigure.UpdatePathSegmentCollection/OnPathSegmentCollectionChanged(src/Controls/src/Core/Shapes/PathFigure.cs) —figure.Segments.Clear()will still leaveoldPathSegment.PropertyChanged -= OnPathSegmentPropertyChangednever called, retaining thePathFigurealive via any survivingPathSegmentreference.GeometryGroup.UpdateChildren/OnChildrenCollectionChanged(src/Controls/src/Core/Shapes/GeometryGroup.cs) —group.Children.Clear()has the same gap forGeometrychildren.Concrete failing scenario:
var seg = new LineSegment(); figure.Segments.Add(seg); figure.Segments.Clear();—figureremains reachable throughseg.PropertyChangedand aWeakReference<PathFigure>test analogous toFiguresClear_AllowsPathGeometryToBeGarbageCollectedwould fail here today. Since this PR introduces the exact fix pattern (a subscribed-items tracking list unsubscribed onReset) forPathGeometry, please confirm whetherPathFigure/GeometryGroupshould get the same fix in this PR or a tracked follow-up issue — otherwise the underlying bug class remains only partially fixed.