xamarin / xamarin-macios

.NET for iOS, Mac Catalyst, macOS, and tvOS provide open-source bindings of the Apple SDKs for use with .NET managed languages such as C#
Other
2.45k stars 511 forks source link

[.NET 6] Big delays in native objects deallocation #13101

Open slluis opened 2 years ago

slluis commented 2 years ago

Steps to Reproduce

  1. Build the attached sample. The sample shows a window with a "Create Views" button. Every time the button is clicked it creates 1000 instances of a view that contains a NSTextView. The views are not stored anywhere so the expectation is that those views should be GCed.
  2. Start the sample using Instruments, with the allocation profiler
  3. In Instruments, filter by "NSTextView"
  4. Click on the "Create Views" button several times

Expected Behavior

Every time the button is clicked the NSTextView instance count should be increased by 1000 and then gradually decrease to 0 as the GC starts collecting the views.

Actual Behavior

This is what I'm experiencing in my machine:

Environment

``` Visual Studio Community 2022 for Mac Preview Version 17.0 Preview (17.0 build 5317) Installation UUID: 756b579c-523c-405c-86c8-c16aec2b68a0 Runtime .NET 6.0.0-rc.2.21470.23 (64-bit) Architecture: X64 Roslyn (Language Service) 4.0.0-5.21501.19+8e1779e16298415843e85029d8b52a1ae9bb4c30 NuGet Versión: 6.0.0.220 .NET SDK SDK: /usr/local/share/dotnet/sdk/6.0.100-rtm.21521.3/Sdks Versiones del SDK: 6.0.100-rtm.21521.3 5.0.302 5.0.301 5.0.203 5.0.202 5.0.103 5.0.101 5.0.100 5.0.100-rc.2.20479.15 5.0.100-rc.1.20452.10 5.0.100-preview.5.20279.10 3.1.413 3.1.411 3.1.410 3.1.409 3.1.408 3.1.406 3.1.404 3.1.403 3.1.402 3.1.200 3.1.101 3.1.100 3.1.100-preview3-014645 3.1.100-preview1-014459 3.0.100 3.0.100-rc1-014190 3.0.100-preview9-014004 3.0.100-preview8-013656 3.0.100-preview7-012821 2.2.203 2.2.104 2.1.811 2.1.810 2.1.701 2.1.700 2.1.603 2.1.302 2.1.4 SDK de MSBuild: /usr/local/share/dotnet/sdk/6.0.100-rtm.21521.3/Sdks .NET Core Runtime Runtime: /usr/local/share/dotnet/dotnet Versiones de tiempo de ejecución: 6.0.0-rtm.21518.12 5.0.8 5.0.7 5.0.6 5.0.5 5.0.3 5.0.1 5.0.0 5.0.0-rc.2.20475.5 5.0.0-rc.1.20451.14 5.0.0-preview.5.20278.1 3.1.19 3.1.17 3.1.16 3.1.15 3.1.14 3.1.12 3.1.10 3.1.9 3.1.8 3.1.2 3.1.1 3.1.0 3.1.0-preview3.19553.2 3.1.0-preview1.19506.1 3.0.3 3.0.2 3.0.0 3.0.0-rc1-19456-20 3.0.0-preview9-19423-09 3.0.0-preview7-27912-14 2.2.7 2.2.4 2.2.2 2.1.23 2.1.22 2.1.16 2.1.15 2.1.14 2.1.13 2.1.12 2.1.11 2.1.10 2.1.2 2.0.5 .NET 5.0 SDK SDK: 5.0.302 SDK de .NET Core 3.1 SDK: 3.1.413 Xamarin.Profiler Versión: 1.6.15.68 Ubicación: /Applications/Xamarin Profiler.app/Contents/MacOS/Xamarin Profiler Updater Versión: 11 Xamarin Designer Version: 17.1.0.1 Hash: 8777be47e Branch: remotes/origin/main Build date: 2021-10-21 17:50:43 UTC Apple Developer Tools Xcode 12.4 (17801) Build 12D4e Xamarin.Mac Version: 7.11.2.12 (Visual Studio Community) Hash: 36c9df625 Branch: registrar-never-really-static-on-macos-d16-10-preview2-bumped-mono Build date: 2021-10-07 14:55:30-0400 Xamarin.iOS Version: 15.0.0.6 (Visual Studio Community) Hash: 2771277e0 Branch: xcode13-ios Build date: 2021-09-23 10:36:08-0400 Xamarin.Android Versión: 11.3.99.54 (Visual Studio Community) "Commit": xamarin-android/main/0e5e06f Android SDK: /Users/lluis/Library/Developer/Xamarin/android-sdk-macosx Versiones de Android admitidas: 4.4 (nivel de API 19) 4.4.87 (nivel de API 20) 5.0 (nivel de API 21) 5.1 (nivel de API 22) 6.0 (nivel de API 23) 7.0 (nivel de API 24) 7.1 (nivel de API 25) 8.0 (nivel de API 26) 8.1 (nivel de API 27) Versión de SDK Tools: 26.1.1 Versión de las herramientas de plataforma del SDK: 30.0.5 Versión de las herramientas de compilación del SDK: 30.0.2 Información de compilación: Mono: c633fe9 Java.Interop: xamarin/java.interop/main@a5ed891 ProGuard: Guardsquare/proguard/v7.0.1@912d149 SQLite: xamarin/sqlite/3.35.4@85460d3 Xamarin.Android Tools: xamarin/xamarin-android-tools/main@683f375 Microsoft OpenJDK for Mobile Java SDK: /Users/lluis/Library/Developer/Xamarin/jdk/microsoft_dist_openjdk_1.8.0.25 1.8.0-25 El código EPL de Android Designer está disponible EPL: https://github.com/xamarin/AndroidDesigner.EPL Android SDK Manager Version: 17.1.0.14 Hash: 9c65d8a Branch: remotes/origin/HEAD Build date: 2021-10-21 17:50:45 UTC Android Device Manager ```

Build Logs

Example Project

Sample.zip

Therzok commented 2 years ago

This is also the case on Mono/xammac, 7.11.

rolfbjarne commented 2 years ago

I'm not sure there's much we (in Xamarin.Mac) can do here because:

  1. The rather random behavior of when objects are collected is because it depends on whenever CoreCLR decides to run the GC. If you call GC.Collect manually on every click, things get more predictable.
  2. The frozen UI is because of:
    1. The native [NSTextView dealloc] method is horribly slow. I profiled a bit with Instruments, and over 90% of the time is spent there. There's not much we can do here (except not allocate thousands of NSTextViews).
    2. We release objects on the main thread.
    3. The GC waiting a while to kick in, thus we end up with a lot of NSTextViews to release (and subsequent long freeze until they're all released).
    4. Running GC.Collect on every click helps here, because the queue ends up being smaller (thus the freeze shorter).
    5. Disposing the NSTextView when done with it also helps (because the work doesn't queue up and freeze the UI for a long time).

That said, I found a minor memory leak (https://github.com/xamarin/xamarin-macios/pull/13109), so that's good :)

Therzok commented 2 years ago

Disposing NSView subclasses is unsafe, in case you have a managed subclass and appkit keeps a view around for longer, you will end up with resurrection issues. Or you could dispose some object passed around that you don't own (i.e. something stored on a field) and all the NSObject wrappers are disposed.

So we have to rely on finalizers most of the time in this case. That way, copy (IntPtr) constructors are reserved for when AppKit copies things - like NSTextFieldCell - and we don't have to worry about resurrection.

Therzok commented 2 years ago

I've been doing a bit of digging, and it seems like performSelectorOnMainThread has some quirks to it: https://developer.apple.com/documentation/objectivec/nsobject/1414900-performselectoronmainthread

Specifically, the runloop part:

This method registers with the runloop of its current context, and depends on that runloop being run on a regular basis to perform correctly. One common context where you might call this method and end up registering with a runloop that is not automatically run on a regular basis is when being invoked by a dispatch queue. If you need this type of functionality when running on a dispatch queue, you should use dispatch_after and related methods to get the behavior you want.

Wouldn't the finalizer thread stop running the runloop until woken up by the GC? I'll try and crop up a test case comparing performSelectorOnMainThread and dispatch_async. It's possible that most of the issues I was encountering were a Mono-only problem, so have to validate that first.

rolfbjarne commented 2 years ago

Wouldn't the finalizer thread stop running the runloop until woken up by the GC?

The finalizer thread isn't running a runloop, it's just queuing stuff to be executed on the main thread's runloop.

I don't quite understand what the documentation is saying, but according to this: https://stackoverflow.com/a/9336253/183422, the main thread's runloop can run in a mode that doesn't process the items queued by performSelectorOnMainThread. That shouldn't be a problem for a normal UI app (and I've never seen it be a problem either).

Therzok commented 2 years ago

https://gist.github.com/Therzok/55aaad6aa6b85aff6e763edb4765c87e

I'm not sure if it's by chance or not, but I'm seeing quite different behaviour in processing these, with AppKit's performSelectorOnMainThread being overall slower and delayed compared to libdispatch - but not sure if that's caused by interop overhead because of the way the test is constructed.

Therzok commented 2 years ago

I've improved on the sample here: https://github.com/Therzok/TestFinalizerRunLoop

I don't think it is a problem of the GC here. If you take a look at the current implementation - which has some workarounds in play to avoid crashing - and run the code, you will see that forcing allocations on a loop here, the nested subviews go away on separate GC cycles.

Note: The workarounds are to bind the lifetime of the Window to the WindowController, otherwise the Window is released on the same GC cycle as the WindowController causing a resurrection in ViewDidMoveToWindow. It happens regardless of ReleaseWhenClosed, because the NSWindowController takes over the lifetime of the NSWindow.