Widespread memory leak propagation in visual trees #21918
-
DescriptionMemory leaks in MAUI are exacerbated by orders of magnitude due to their propagation through the visual tree via held references (e.g. Parent, Content, ItemsSource, Children). This leads to entire pages and binding contexts being held in memory indefinitely (sometimes even entire apps), resulting in severe performance degradation, UI choppiness, and eventual forced application shutdowns by the operating system. I believe this is the most critically severe performance-related issue in MAUI apps today. The behavior is the same across platforms. Lots of effort has been going into fixing individual leaks, but the ROI on these efforts is muted by the all-or-nothing nature of this problem. Steps to ReproduceNo response Link to public reproduction project repositoryhttps://github.com/AdamEssenmacher/MemoryToolkit.Maui/tree/main/samples Version with bug8.0.7 SR2 Is this a regression from previous behavior?Yes, this used to work in Xamarin.Forms Last version that worked wellUnknown/Other Affected platformsiOS, Android, Windows, macOS, I was not able test on other platforms Affected platform versionsNo response Did you find any workaround?There are three distinct actions that, when applied systemically through the visual tree bottom-up (from leaf to root), can whack leaky views into non-leaking states and compartmentalize small leaks so that they remain isolated to their offending views: 1) Clear the binding context: This contributes to compartmentalization by removing the reference. It also gives the view a chance to reset itself to a near-default state where leaks are least likely to happen (some views only leak in certain states). View Lifecycle ChallengesThe workaround I've offered above might not be an ideal or permanent solution. Executing these cleanup measures requires that we have knowledge of when an app is "done with" a given view or page. MAUI currently does not really have a standardized component lifecycle, so there isn't really a central event or hook we can rely on. I have developed an approach that attempts to address this by making an educated guess about when an app is probably "done with" a given
Stopgap SolutionI have developed a behavior that detects leaky views during development using this definition of "done with", and another behavior that automatically applies the cleanup measures described earlier. I offer them as stop-gap measures pending a more robust solution. Request for AssistanceI encourage the community to provide feedback on these behaviors, and to contribute ideas for how MAUI might incorporate improved lifecycle management to prevent and contain memory leaks. Relevant log outputNo response |
Beta Was this translation helpful? Give feedback.
Replies: 18 comments 25 replies
-
Adam, I know you are very knowledgeable on this subject. I have tried to figure this out a bit myself but don't have a shred of your knowledge on it. For my part, I am creating everything through C# (no XAML). I do personally know with certainty when my objects are not needed, so I can theoretically destroy/dispose of them when needed. But I am still not sure what to do with them as there is no built in "Dispose" system. "Dispose" Protocol?I have therefore wanted to create a "Dispose" method that can destroy I think the clearest way I would be able to conceptualize the problem and solution would be. I think this will help others as well or help formulate a correct mechanism for dealing with this. I think if we had an extension for View "Dispose" that safely discarded that object (and/or its children) then this would be the best way to manage things and be sure we are not having memory leaks. Example HierarchyLet's say hypothetically you have a hierarchy like this:
This creates a hierarchy like this:
Disposing Correctly?Hypothetically, let's say you want to destroy the entire I asked this question here also but got zero replies except someone wondering the same thing as me. Based on the discussions I have seen you participating I suspect you might be the only person that knows and this would again help make clear how we are supposed to manage these things without leaking. If we have a protocol of steps to follow then we can all apply our own methods that should work as needed. Thanks for any help if you can. |
Beta Was this translation helpful? Give feedback.
-
@jonmdev great questions. You've clearly done a ton of research, but still are still unclear on an ideal approach to ensure MAUI views are cleaned up properly. This underscores my position that MAUI really needs a standard component lifecycle management mechanism. I'll try to respond to your thoughts to the best of my understanding.
A word of warning: a completely manual approach here is bound to be error-prone, and a single miss can carry a very high penalty. That said, determining 'when' views should be cleaned up is definitely a separate (but somewhat related) concern vs the 'how'. I'll keep the focus on the remainder of my response on the latter.
I think this is an excellent idea! The implementation of Currently, the method signature is
...but there's no reason it couldn't be refactored out and tweaked for more general use like:
I'm not going to presume to have a "correct" (or even complete) approach here. This is part of why I've posted the original issue--I can't help but feel that I might be working against MAUI's design intent. However, I can say that the approach I've cobbled together works well in practice 😊. The poorly-named
This approach prevents certain classes of leaks. In particular:
This won't prevent or avoid all leaks though. In these cases, this approach at least compartmentalizes leaks. I'll also note there there is some 'extra stuff' in the current implementation of
Extra stuff 1)
Extra stuff 2)The other part here is targeting a specific, known leak in MAUI's
The general approach is the same, but accomplishing part (3) (clearing references to other view) can be specific to different type of views. For example:
MAUI's views have something like a common Also, this if/else if implementation is likely incomplete.
Platform-specific leaks exist. Once a leak happens, the leak propagation problem is platform-agnostic, and so the benefit of compartmentalization is as well. Platform handlers really should be taking care of platform specific cleanup themselves. However, as we've seen in the iOS-specific |
Beta Was this translation helpful? Give feedback.
-
Thanks. That is all very helpful. It is interesting. It feels like these are things we should not need to know about as end users. Ie. There should simply be a function What are we SUPPOSED to do?The question I have next then is what are we intended to do to destroy/dispose things from Microsoft's perspective? From the reference here: https://learn.microsoft.com/en-us/dotnet/maui/user-interface/handlers/create?view=net-maui-8.0#native-view-cleanup It sounds like the intended equivalent we are supposed to use is just:
Is that supposed to be all we are theoretically supposed to do? If you have something in a hierarchy with children (or without), and you just call this method, what happens and what is supposed to happen? What about In theory, what does Microsoft/Maui intend for us to be doing here? Are we supposed to manually remove from hierarchy and then Is this supposed to remove it from the hierarchy as well? Or just dispose of the platform view inside? Can we have some directions on the intended workflow to remove/destroy/dispose things like Border/Image/Label/Layout? If we know what we are supposed to be doing we can better comment or work together on how to fix it. Minor issue?Also, I am not sure if this will be an issue for you, but your code is:
I am not sure if you will sometimes get a null Handler there depending on what |
Beta Was this translation helpful? Give feedback.
-
I think, ideally, we're not supposed to do anything. The GC is supposed to clean things up for us. Handler authors aren't supposed to rely on Put another way, I do not believe it is/was in MAUI's design intent for developers to have to manually keep track of views and tear them apart and manually dispose/disconnect them in order to prevent memory leaks. Rather, These are just educated guesses though. Regardless, doing these things is currently necessary (or at least a very good idea) due to this propagating memory leak issue, combined with the practical reality that some handlers do actually rely on
Generally in C#, calling an instance method like That aside, you are right to be suspicious of calling methods on a disposed object. Generally in C#, doing so could cause an |
Beta Was this translation helpful? Give feedback.
-
What I mean is: Take my example in OP, where we have a hierarchy we are managing by C# and intentionally want to disconnect a chunk and dispose of it. Regardless of any issues around accidental memory leaks, how does Maui intend we do this? Based on this paragraph here:
I presume the design intention is: (1) manually remove Maui view from Maui hierarchy, and then (2) run @davidortinau Is this correct? Can you or one of your team please explain what the design intention is? What are we supposed to do in C# if we have a hierarchy and want to remove and garbage collect a view or chunk of that hierarchy? If so, then @AdamEssenmacher your Whether they would want to add a VisualElement monitor like you do for appropriately triggering this method would be a second issue. ie. There are two problems: (1) proper garbage collection steps when initiated (ie. under DisconnectHandler apparently), and (2) monitoring issues for when it should happen without being requested explicitly (your Monitor solution). In other words, based on their paragraph above, the fixes you are proposing would have to go into (1) ensuring the Unloaded event is triggered when it should be, and (2) ensuring DisconnectHandler does what it's supposed to when it is called. |
Beta Was this translation helpful? Give feedback.
-
I should be extra clear that I'm not proposing any actual fixes here. I've identified a problem in the propagating memory leaks, some major contributing causes, and a workaround.
Assuming we had a reason to not want to wait around for the GC to take care of it for us, I believe the intent would just be to make sure
I do not think calling |
Beta Was this translation helpful? Give feedback.
-
I keep writing way too much but I think I can answer your question @jonmdev, maybe I won't have to redraft this one. Put simply: XAML wasn't designed for disposable objects. WPF didn't have that concept. That makes it a bit of a bad fit for MAUI, since under the hood we know native unmanaged objects are correlated with elements. I think the MAUI team expected the Handler infrastructure would separate things enough it wouldn't matter. What we're finding is it's not enough, or there are some leaks in the internals. They are NOT obvious, so I don't think the MAUI team knows about them. Their impact on smallish apps is limited, so they can go undetected. They only seem to manifest in a catastrophic way in apps intended to be used for a long time with lots of navigations. That's a very "industry or enterprise app" niche on mobile. I think those people have only started using MAUI in earnest this year due to the looming Xamarin Forms support deadline. So now they're finding these issues that could only be found by complicated apps. I'm super thankful for @AdamEssenmacher. I think he's the authority right here. He's walked backwards through a lot of MAUI code to find the issues. It's hard to overstate how much work that must have been. The memory profiling tools can only tell us "this is the graph" and not "What the heck is that thing and why is it rooting my object?" There really aren't many experts who know that kind of MAUI arcana. I hope the team sees this and adopts a lot of the code. I have a feeling there are a few patterns they'll find were bad ideas and can be replaced with better ideas. While they hunt that down, I hope they put Microsoft's excellent tech writers in charge of writing a good guide to manually handling these issues and implementing some fixes in apps. I'm curious if MAUI should be a XAML framework with a concept of disposal. As a veteran Windows Forms dev it's never sat right with me that even in WPF elements didn't implement |
Beta Was this translation helpful? Give feedback.
-
@AdamEssenmacher generally I'd say you've nailed it here with your assessment :-) in XF we aggressively deconstructed hierarchies and called Dispose on every single platform view. This led to a whole different class of issues that we spent most of the lifetime of XF trying to fix and, in some cases, never did.
So, our approach in MAUI was to try to just let the GC do what the GC does and just make sure that all of our code is collectable. Class of issues we've been fixing and users have hitMaking Handlers GC friendlyThis is largely an iOS problem (though it does happen on other platforms). You can very easily cause memory leaks on iOS when there's a circular dependency of NSObjects. This happens for us because all of our XPLAT elements have a "Parent" property, so, all of our children have references to all of our parents. Example PR: #18682 On Android and Windows this usually comes up with the classic scenario of referencing things that survive the lifetime of what you want GC'd. We don't really have that many PRs for android/windows specific leaks because it doesn't suffer from the same circular reference issue. Typically, with android/windows you don't really even need to unsubscribe from events. Managed Xplat mistakesThere have been a few places where things were getting parented when they shouldn't have IDisposable confusion and DI
What now?
At one point during NET8 development we did think about adding some hooks in handler that would just be directly tied to when a view is removed/added from the Visual tree #16292 but deemed that it would be premature to commit to this API.
|
Beta Was this translation helpful? Give feedback.
-
I agree with @PureWeen's lengthy assessment! One of the problems I had as a Xamarin.Forms customer is the proactive There was actually one interesting case where Windows had a circular reference: I believe the As for the original post:
I actually don't think the behavior is across all platforms. The bulk of the issues seem to be on iOS/Catalyst to me, and we are actively working on these. |
Beta Was this translation helpful? Give feedback.
-
Verified this issue with Visual Studio 17.10.0 Preview 3(8.0.20/8.0.7). Can repro this issue on iOS. |
Beta Was this translation helpful? Give feedback.
-
@PureWeen thank you so much for your detailed and thoughtful reply. Your first-hand account fills in some massive blanks in my own understanding of the 'why' behind this issue. I've been burned more than once by the XF problems you mention, so this explanation really resonates with me. For what it's worth, I think the design intent you've described here was absolutely the right thing for MAUI:
It seems we share a common understanding on the technicalities, driving forces, and contributing factors behind this problem. However, I'm concerned that we might not have a common understanding regarding its actual, real-world pervasiveness and impact. I believe that this issue is absolutely catastrophic when it occurs, and that the conditions which cause it to occur are extraordinarly common. If you'd like, I can offer some evidence to support these claims. Though, it's easy enough to observe if you just look. Even the OOTB "right-click -> new MAUI project" template is currently born rooted!
It's clear that this statement should be correct in an ideal state. However, currently--in practice--it couldn't be further from the truth 😢. Right now, developers of MAUI apps of even moderate complexity need to be hyper-aware of what the GC is doing (or rather, isn't doing) if they want their apps usable for more than a few minutes without crashing. |
Beta Was this translation helpful? Give feedback.
-
@PureWeen @jonathanpeppers I'm trying to see 'the forest for the trees' here. The real issue at hand isn't the half-dozen common classes of MAUI leaks, or the dozens (maybe hundreds) of actual leaks in official MAUI controls, or in the likely thousands of individual leaks in the wild caused by devs doing dumb stuff (like capturing the global dispatcher in an event loop). Leaks happen.
The real problem I mean to address in this issue is what happens when any of these leaks occurs--because it roots entire apps. In 1973, Ford released the Pinto--a car most famous now for having a propensity to explode when rear-ended. Of course, under ideal circumstances, Pintos shouldn't be getting rear-ended. However, if we accept the practical reality that a Pinto will get rear-ended every once in a while, we can probably agree that it shouldn't explode as a result, right? So, just as a fender-bender shouldn't cause a Pinto to explode, a leaky view shouldn't root an entire MAUI app. We need some fault-tolerance baked into the architecture. @jonathanpeppers this is what I mean when I say the behavior is the same across platforms. iOS is for sure the leakiest right now, but once a leak occurs on any platform, the result is the same. |
Beta Was this translation helpful? Give feedback.
-
This I've moved that issue to our May iteration for investigation. If we find other controls in that sample causing issues we'll create separate issues for that. Moved this to a discussion because it's a lot easier to have a conversation around the points being made in this format then on an issue. |
Beta Was this translation helpful? Give feedback.
-
Beta Was this translation helpful? Give feedback.
-
I'll share my experiences here - but first I need to thank @AdamEssenmacher profusely for the contributions he's made public. Without them my team would have been in a dire position. We have a large app we have been porting from Xamarin Forms for the past 15 months. A few weeks before we were set to release we discovered that we had a memory leak. As we dug into it, the enormity of the consequences became apparent. We found a few leaks within our own objects and corrected them. But the leaks in the visual controls are the linchpin to massive memory leaks. Thanks again to Adam for showing how we can pull that linchpin. With the memory toolkit we can now at least compartmentalize the memory leak and move towards our delayed release. This is a defensive measure that will likely live in our code for a long time. Perhaps we were lulled into a false sense of security while doing this port, in that these defensive techniques were not necessary in Xamarin Forms. It is not my intent to throw anyone under the bus. Memory leaks happen. But I understand what Adam is getting at when he talks about memory leak fault tolerance. It takes only one leak to deal a devastating blow to an app that supports frequent navigation and handles moderate amounts of data. It would be a great help if this built in to MAUI. When my app pops a page off the navigation stack, we are done with it. We fully expect it to go away. Or at the very least not be pinned by any visual element. And failing that, it should release any bound context. Some direct feedback to @AdamEssenmacher on how we use his tool. We couldn't really use the behavior because we have created a lifecycle around the view model. Inside that lifecycle we already disconnect behaviors. And so yours gets caught up in the process. We did take the Disconnect() method and incorporate it into our lifecycle. We did find that some elements will throw exceptions within this process. We wrapped each call in a try/catch so that even if one aspect of the Disconnect fails the others still have an opportunity to run. Without that, memory leaks quickly reappear. And finally, with memory profiling now becoming a required part of our SDLC we are searching for ways to automate the profiling. The difficult part is that the profiling should happen while running on each platform (Windows, iOS and Android). Any ideas would be greatly appreciated. |
Beta Was this translation helpful? Give feedback.
-
Hi Adam, |
Beta Was this translation helpful? Give feedback.
-
We're in the process of converting a fairly large Xamarin app to Maui. The conversion is now functionally complete, but plagued by memory leaks, like those described in this thread. I can't see how anyone can build a production ready application that doesn't randomly shutdown when memory consumption hits the limit. How is the situation with Blazor in MAUI? Is this a solution to avoid the issues with the MAUI controls? or does that have it's own memory leak issues to contend with? |
Beta Was this translation helpful? Give feedback.
-
@AdamEssenmacher @PureWeen @jonathanpeppers is there any update on this? It's been a few months, just want to check in! |
Beta Was this translation helpful? Give feedback.
I don't think this is true.
@AdamEssenmacher you must have missed it, this PR shipped in 8.0.60:
This makes any
Element.Parent
a weak reference. It should improve many general cases that would hold onto aPage
. There could be more ways to improve this, of course.We also fixed the following memory leaks: