dotnet / runtime

.NET is a cross-platform runtime for cloud, mobile, desktop, and IoT apps.
https://docs.microsoft.com/dotnet/core/
MIT License
15.38k stars 4.75k forks source link

[iOS] App crashes with native out of memory error due to memory leak of elements inside a Grid. #104272

Closed jgold6 closed 3 months ago

jgold6 commented 4 months ago

Description

Any contents in a Grid will slowly leak if garbage collection is allowed to run without ever being manually called with GC.Collect(). The app will eventually crash when navigating to and away from the page with the Grid; how fast depending on how big, memory-wise, the objects in the Grid are. Observing the native memory in XCode Instruments reveals garbage collection running and collecting most, but not all, native memory of the type(s) in the Grid, e.g. MauiImage or MauiLabel. If GC.Collect is called manually, however, the leak does not appear to occur and the app never crashes, nor do I see the same consistent overall memory growth as I do when the garbage collector runs on its own according to its algorithm. No managed memory leak is observed according to memory usage reported by the GC API.

Reproduction Steps

  1. Open the attached test project
  2. Deploy release configuration to iPad device (leak occurs on simulator as well, but Instruments and the simulator crash due to low system memory before the app crashes)
  3. Open Instruments app with Leaks profiles
  4. Attach to the installed test app in the iPad and record to launch and profile the app
  5. Allow app to run until it crashes

MemoryLeakTestGridLeak.zip

Expected behavior

App will continue to run indefinitely without out of memory native crash.

Actual behavior

App crashes with native out of memory error. It will take about an hour as is. Right now the Label is the only content of the Grid. Add the commented images back in and the app will crash much faster. See comments in GridLeakPage.xaml:

Wrap a large Image in a Grid, and it will crash in about 14 minutes (iPad Pro) Add one more of the same and it will crash in about 7 minutes Add 3 more and it will crash in about 3.5

Even a Label leaks. Comment out the images and comment in the Labels and the app will crash in a bit more than an hour.

If GC.Collect is called manually, leak does not occur.

Regression?

Unknown

Known Workarounds

Manually call GC.Collect() when navigating away from the page with the Grid.

Configuration

.NET 8 MAUI iOS 17.5.1 ARM64 Issue does not occur on Android using .NET 8 MAUI

Other information

I discovered that the app will run much longer if not run under Instruments, to the point where I thought perhaps Instruments was the cause. But by upping the number of Image elements to 16 from 4, the app crashed in about 2 minutes when not running under instruments.

dotnet-policy-service[bot] commented 4 months ago

Tagging subscribers to this area: @dotnet/gc See info in area-owners.md if you want to be subscribed.

dotnet-policy-service[bot] commented 4 months ago

Tagging subscribers to this area: @brzvlad See info in area-owners.md if you want to be subscribed.

BrzVlad commented 4 months ago

I investigated this in maccatalyst and it is not really a leak. If GCs are not triggered manually the app size will increase but only up to a certain point. The problem is that fully collecting the image/label is done late, after multiple GCs.

In order to futher illustrate this problem I tweaked the GridLeakPage.xaml to have the following layout:

<Grid>
        <Grid>
        <Grid>
            <Image                        
                Source="dotnet_bot_large.png" />
        </Grid>
        </Grid>
</Grid>

I logged finalizer execution and once we navigate away from the page the following objects have finalizers run. After GC1 GridLeakPage is collected, after another gc we have a LayoutView finalizer run, with 2 more objects of type LayoutView finalized after 2 more GCs. Only after that we have the finalizer for MauiImageView run (which I suspect might hold some native memory around). So in this case, the image has its finalizer run only after 5 GC collections.

In normal C# application, if you have a chain of references like FinObj1 -> FinObj2 -> FinObj3 ... and FinObj1 is eligible for finalization, then all objects are going to be finalized together and at the next collection they are all dead. However, in this case, MauiImageView, LayoutView etc are bridge objects with ObjC counterpart and I think there are references between them on ObjC side. MauiImageView is kept alive by a strong gc handle in ObjC in an UIView. This UIView object is probably referenced from some other ObjC that can only die once its C# counterpart is collected so we need 2 GC cycles. With longer chains of references crossing ObjC world we would need increasingly more GC collections to be able to reclaim all memory. I'm not sure whether this is fixable.

Seems like a reason for the memory usage is that some of these controls can end up using a lot of memory on the native side. I think the recommendation in these cases is to use the GC.AddMemoryPressure api. I'd expect this to lead to more GC collections if memory grows too much.

cc @dalexsoto @rolfbjarne @Redth

filipnavara commented 2 months ago

For the OP: You may want to read the documentation on DisconnectHandler and then look at https://github.com/AdamEssenmacher/MemoryToolkit.Maui. For practical purposes that may give you an immediate solution. There's going to be a change to the behavior in upcoming MAUI release in .NET 9.