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.47k stars 514 forks source link

Using HttpClient synchronously results in memory leak #8350

Open prollin opened 4 years ago

prollin commented 4 years ago

Calling HttpClient.GetAsync(url).Result in a loop causes process memory to grow continuously:

Task.Run(() =>
{
    var httpClient = new HttpClient();
    httpClient.BaseAddress = new Uri("https://www.google.com/", UriKind.Absolute);
    var requestUri = "images/branding/googlelogo/1x/googlelogo_color_272x92dp.png";
    while (true)
    {
        using (var httpResponseMessage = httpClient.GetAsync(requestUri).Result)
        {
            Console.WriteLine(httpResponseMessage.Content.Headers.ContentLength);
        }
        Task.Delay(10);
    }
});

Using async/await seem to solve the issue (but unless I missed something obvious, the synchronous version shouldn't leak):

Task.Run(async () =>
{
   var httpClient = new HttpClient();
   httpClient.BaseAddress = new Uri("https://www.google.com/", UriKind.Absolute);
   var requestUri = "images/branding/googlelogo/1x/googlelogo_color_272x92dp.png";
   while (true)
   {
        using (var httpResponseMessage = await httpClient.GetAsync(requestUri).ConfigureAwait(false))
        {
            Console.WriteLine(httpResponseMessage.Content.Headers.ContentLength);
        }
        await Task.Delay(10).ConfigureAwait(false);
    }
});

Steps to Reproduce

  1. Compile and deploy attached project to device
  2. Launch app through the "Activity" instrument

Expected Behavior

Process memory doesn't increase continuously

Actual Behavior

Process memory increases continuously

Environment

Microsoft Visual Studio Professional 2019
Version 16.5.3
VisualStudio.16.Release/16.5.3+30002.166
Microsoft .NET Framework
Version 4.8.03752

Installed Version: Professional

Visual C++ 2019   00435-60000-00000-AA532
Microsoft Visual C++ 2019

.NET Memory Profiler   5.6
.NET Memory Profiler Visual Studio Integration Package

ADL Tools Service Provider   1.0
This package contains services used by Data Lake tools

ASA Service Provider   1.0

ASP.NET and Web Tools 2019   16.5.236.49856
ASP.NET and Web Tools 2019

ASP.NET Web Frameworks and Tools 2019   16.5.236.49856
For additional information, visit https://www.asp.net/

Azure App Service Tools v3.0.0   16.5.236.49856
Azure App Service Tools v3.0.0

Azure Data Lake Node   1.0
This package contains the Data Lake integration nodes for Server Explorer.

Azure Data Lake Tools for Visual Studio   2.4.5000.0
Microsoft Azure Data Lake Tools for Visual Studio

Azure Functions and Web Jobs Tools   16.5.236.49856
Azure Functions and Web Jobs Tools

Azure Stream Analytics Tools for Visual Studio   2.4.5000.0
Microsoft Azure Stream Analytics Tools for Visual Studio

C# Tools   3.5.0-beta4-20153-05+20b9af913f1b8ce0a62f72bea9e75e4aa3cf6b0e
C# components used in the IDE. Depending on your project type and settings, a different version of the compiler may be used.

Common Azure Tools   1.10
Provides common services for use by Azure Mobile Services and Microsoft Azure Tools.

Extensibility Message Bus   1.2.0 (d16-2@8b56e20)
Provides common messaging-based MEF services for loosely coupled Visual Studio extension components communication and integration.

Fabric.DiagnosticEvents   1.0
Fabric Diagnostic Events

IntelliCode Extension   1.0
IntelliCode Visual Studio Extension Detailed Info

Microsoft Azure HDInsight Azure Node   2.4.5000.0
HDInsight Node under Azure Node

Microsoft Azure Hive Query Language Service   2.4.5000.0
Language service for Hive query

Microsoft Azure Service Fabric Tools for Visual Studio   16.0
Microsoft Azure Service Fabric Tools for Visual Studio

Microsoft Azure Stream Analytics Language Service   2.4.5000.0
Language service for Azure Stream Analytics

Microsoft Azure Stream Analytics Node   1.0
Azure Stream Analytics Node under Azure Node

Microsoft Azure Tools   2.9
Microsoft Azure Tools for Microsoft Visual Studio 2019 - v2.9.30207.1

Microsoft Continuous Delivery Tools for Visual Studio   0.4
Simplifying the configuration of Azure DevOps pipelines from within the Visual Studio IDE.

Microsoft JVM Debugger   1.0
Provides support for connecting the Visual Studio debugger to JDWP compatible Java Virtual Machines

Microsoft Library Manager   2.1.25+gdacdb9b7a1
Install client-side libraries easily to any web project

Microsoft MI-Based Debugger   1.0
Provides support for connecting Visual Studio to MI compatible debuggers

Microsoft Visual C++ Wizards   1.0
Microsoft Visual C++ Wizards

Microsoft Visual Studio Tools for Containers   1.1
Develop, run, validate your ASP.NET Core applications in the target environment. F5 your application directly into a container with debugging, or CTRL + F5 to edit & refresh your app without having to rebuild the container.

Microsoft Visual Studio VC Package   1.0
Microsoft Visual Studio VC Package

Mono Debugging for Visual Studio   16.5.514 (c4f36a9)
Support for debugging Mono processes with Visual Studio.

NuGet Package Manager   5.5.0
NuGet Package Manager in Visual Studio. For more information about NuGet, visit https://docs.nuget.org/

OptionsPagePackage Extension   1.0
OptionsPagePackage Visual Studio Extension Detailed Info

ProjectServicesPackage Extension   1.0
ProjectServicesPackage Visual Studio Extension Detailed Info

SQL Server Data Tools   16.0.62003.05170
Microsoft SQL Server Data Tools

ToolWindowHostedEditor   1.0
Hosting json editor into a tool window

TypeScript Tools   16.0.20225.2001
TypeScript Tools for Microsoft Visual Studio

Visual Basic Tools   3.5.0-beta4-20153-05+20b9af913f1b8ce0a62f72bea9e75e4aa3cf6b0e
Visual Basic components used in the IDE. Depending on your project type and settings, a different version of the compiler may be used.

Visual F# Tools 10.8.0.0 for F# 4.7   16.5.0-beta.20181.6+85af456066acd4e76d2bc7821b44a325e46f2fca
Microsoft Visual F# Tools 10.8.0.0 for F# 4.7

Visual Studio Code Debug Adapter Host Package   1.0
Interop layer for hosting Visual Studio Code debug adapters in Visual Studio

Visual Studio Container Tools Extensions (Preview)   1.0
View, manage, and diagnose containers within Visual Studio.

Visual Studio Tools for Containers   1.0
Visual Studio Tools for Containers

Visual Studio Tools for Kubernetes   1.0
Visual Studio Tools for Kubernetes

VisualStudio.DeviceLog   1.0
Information about my package

VisualStudio.Foo   1.0
Information about my package

VisualStudio.Mac   1.0
Mac Extension for Visual Studio

Xamarin   16.5.000.533 (d16-5@9152e1b)
Visual Studio extension to enable development for Xamarin.iOS and Xamarin.Android.

Xamarin Designer   16.5.0.470 (remotes/origin/d16-5@681de3fd6)
Visual Studio extension to enable Xamarin Designer tools in Visual Studio.

Xamarin Templates   16.5.49 (0904f41)
Templates for building iOS, Android, and Windows apps with Xamarin and Xamarin.Forms.

Xamarin.Android SDK   10.2.0.100 (d16-5/988c811)
Xamarin.Android Reference Assemblies and MSBuild support.
    Mono: c0c5c78
    Java.Interop: xamarin/java.interop/d16-5@fc18c54
    ProGuard: xamarin/proguard/master@905836d
    SQLite: xamarin/sqlite/3.28.0@46204c4
    Xamarin.Android Tools: xamarin/xamarin-android-tools/d16-5@9f4ed4b

Xamarin.iOS and Xamarin.Mac SDK   13.16.0.13 (b75deaf)
Xamarin.iOS and Xamarin.Mac Reference Assemblies and MSBuild support.

Build Logs

build.log

Example Project (If Possible)

HttpClientMemoryLeak.zip

Therzok commented 4 years ago

Just a note, unlike the async version, the version which uses .Result not sleep for 10 milliseconds on each run. Replacing that with Task.Delay with Thread.Sleep(10) changes the rate at which allocations are done.

mandel-macaque commented 4 years ago

Clearly there is something fishy, but might not be directly related to the handler, since it will just returns the task, nevertheless I'll do some investigation.

prollin commented 4 years ago

@mandel-macaque I did some more investigation and It looks like it is leaking memory allocated in SecCertificateCreateWithData and SecCertificateCreateWithBytes, both downstream from System.Mono.AppleTls.AppleTslContext.ProcessHandshake

liamcharmer commented 4 years ago

@prollin any clear indication of a method to remove this or avoid this from happening?

prollin commented 4 years ago

Unfortunately I haven't. It seems to be an issue deeply rooted in iOS. My up to date guess is that the app is getting "charged" for the memory used by the SSL certificate cache managed by the OS. This cache will grow up to a certain size but it is possible that there is 1 cache per session (we do use a few httpclient in our app). Note that there is still quite a bit of speculation in my findings.

@mandel-macaque any chance on your end?

mandel-macaque commented 4 years ago

I'm taking a look atm, sorry I was side tracked by other issues. Will try to share some light ASAP.

mandel-macaque commented 4 years ago

The following are some technical details of the investigation which will later be added in the PR with the fix.

Issue

I have ran the sample application with the following configurations (images attached as well as the profiler files to prove the issue)

HttpClientHandler (Managed implementation)

There is no memory leak as you can see in the profiler image (all profiling files are added at the end of the comment in a .zip

ManagedSyncProfiled

CFNetworkHandler

Same as with the managed implementation there is no leak.

CFNetworkSyncProfiled

NSUrlSession

There is a bigger memory leak in the sync implementation than there is in the async one.

Async profiled:

NSUrlSessionAsyncProfiled

Sync profiled:

NSUrlSessionSyncProfiled

Conclusion: Issue confirmed.

Investigation

The stack traces of the leak objects are the same, which means that clearly some threading was involved in the issue, on top of that @Therzok comment points to an issue with Tasks delays and threads. That comment already made be suspect of line https://github.com/xamarin/xamarin-macios/blob/master/src/Foundation/NSUrlSessionHandler.cs#L690

Once you see that line, and compare the memory that is leaked (it is a memory stream from the HttpContent) clearly points to the fact that the callback for the HACK is the one leaking memory. The lambda (in this case a closure since it captures the variable) is keeping the captured variable alive and the GC is not collecting it :/

Fix

The issue resides in the fact that we are using a boolean in the inflight data to identify if the response was set, and that is wrong. The tcs already handles this correctly and the Try* methods are safe. This allows the removal of the lock, which was the one making the TrySetResult block. There is no need to set the boolean since it can be removed and we can use the tcs methods to assert if the value was set.

Results

The following are the results of profiling both implementations, as you can see, with the fix both the memory in the sync and the async versions of the app grow the same.

Sync profiled:

NSUrlSessionSyncFixed

Async profiled

NSUrlSessionAsyncFix

The fix is proposed but since @prollin has done such an amazing job I though it would be nice to add a nice explanation of what was going on :)

Extras

The following are the profiling sessions showing that the fix does what I claim

Broken:

Broken.zip

Fix: Fix.zip

Other handlers: other.zip

PS: Thanks to @Therzok the Task.Delay to Thread.Sleep already gave me a good indication where to look.

prollin commented 4 years ago

@mandel-macaque this is an amazing investigation! Thank you.

What is interesting is that I did test with the different handlers initially and could still see memory usage growing (unfortunately I do not have access to Xamarin Profiler). I just re-tested my repro case with CFNetworkHandler and I still see memory going up (in activity monitor instrument) but at a much lower rate than with NSURLSessionHandler. This could just be the SSL cache I mentioned earlier.

prollin commented 4 years ago

The test with CFNetworkHandler was flawed since the server I tested all returned error status (505 or 400). Is CFNetworkHandler fully deprecated now?

@mandel-macaque any idea when this fix will be released? There seem to be no simple workaround to this issue in the meantime since the other handlers do not work.

mandel-macaque commented 4 years ago

@prollin as soon as the PR is approved and merged I will do a cherry-pick so that the fix is in d16-7.

The CFNetwork is not deprecated, if it is failing, can you please create an issue, AFAIK it should work. I wont be adding new features to CFNetwork but will fix bugs.

The smaller leak can be due to several things, one is that the HttpClient does keep connections open to reuse them, the other is that if you are using caching, there is a NSData that I cannot dispose, if you are not, it should be disposed (and is an enhancement for NSUrlSessionHandler that I'll be working on https://github.com/xamarin/xamarin-macios/issues/8558).

mandel-macaque commented 4 years ago

@prollin one thing, since NSUrlSessionHandler is opensource, you can always copy the fix and add it to your project ASAP.

mandel-macaque commented 4 years ago

@rolfbjarne brought to my attention that I uploaded the wrong profile for the broken version, here is the correct one:

https://microsofteur-my.sharepoint.com/:u:/g/personal/mandel_microsoft_com/ETaiYkLNOeVLgcYz6lDSlLQBCOPLY0sgfDCFmT8rFmOgCQ?e=aCPoTh

prollin commented 4 years ago

@mandel-macaque quick update: since copying the fix directly opened another can of worms, I tried to use NSUrlSession API directly; I was still seeing memory usage growing significantly with a test similar to the one above:

Task.Run(() =>
{
    while (true)
    {
            using (var url = new NSUrl("https://www.google.com/images/branding/googlelogo/1x/googlelogo_color_272x92dp.png"))
            {
                using (var dataTask = this.nsUrlSession.CreateDataTask(url, (data, response, e) => { sem.Release(); }))
                {
                    dataTask.Resume();
                    sem.WaitOne();
                 }
            }
    }
});

Looking at Instrument, the leaked object were all iOS Core Foundation types like CFString and CFHTTPMessage prompting me to believe that it could be an autorelease pool issue. After some research I stumbled on this old thread: https://forums.xamarin.com/discussion/6404/memory-leaks-and-nsautoreleasepool that seem to indicate that API that cache tasks could leak. Adding an NSAutoreleasePool to the test above does indeed solves the issue:

Task.Run(() =>
{
    while (true)
    {
        using (var autoreleasePool = new NSAutoreleasePool())
        {
            using (var url = new NSUrl("https://www.google.com/images/branding/googlelogo/1x/googlelogo_color_272x92dp.png"))
            {
                using (var dataTask = this.nsUrlSession.CreateDataTask(url, (data, response, e) => { sem.Release(); }))
                {
                    dataTask.Resume();
                    sem.WaitOne();
                 }
            }
        }
    }
});

After this I simply added the NSAutoreleasePool in our production code that was causing issues (no other changes) and ran it overnight: memory did grow a bit (probably from the NSUrlSessionHandler leak you fixed + other small issues we have) but never came close to what it was before.

mandel-macaque commented 4 years ago

@prollin So we were leaking in two diff places! Ok, I'll deal with that part too.