-
Notifications
You must be signed in to change notification settings - Fork 337
Description
Pull request #308 introduced memory leak each time you serialize/deserialize layout. Affected versions: 4.60.1 and above.
This behavior is persistent across multiple runtimes net48, netcoreapp3.1, net 5.0, net 6.0.
Our projects has auto-save layout feature so memory grows pretty quickly.
Plain C# repro:
using System;
using System.IO;
using System.Xml.Serialization;
namespace Net5Sandbox
{
public class XmlRoot
{
public int Id { get; set; }
}
class Program
{
static string Serialize(XmlRoot dataObj)
{
//var serializer = new XmlSerializer(typeof(XmlRoot)); //doesn't leak
var serializer = XmlSerializer.FromTypes(new[] { typeof(XmlRoot) })[0]; //leaks
using (var stream = new StringWriter())
{
serializer.Serialize(stream, dataObj);
return stream.ToString();
}
}
static void Main(string[] args)
{
const int repeatCount = 10000;
var totalLength = 0L;
var dataObj = new XmlRoot { Id = 1 };
for (int i = 0; i < repeatCount; i++)
{
totalLength += Serialize(dataObj).Length;
}
Console.WriteLine($"Done total length = {totalLength}");
Console.WriteLine(GC.GetTotalMemory(false));
Console.WriteLine("Running GC");
for (int i = 0; i < 10; i++)
{
GC.Collect(2, GCCollectionMode.Forced, true, true);
}
Console.WriteLine("Done");
Console.WriteLine(GC.GetTotalMemory(false));
Console.ReadKey();
}
}
}Final console app memory consumption is around 500MiB depending on runtime. However GC.GetTotalMemory() shows that it is not a object allocation issue. Console output:
Done total length = 1670000
13077480
Running GC
Done
4912128
Debug output with XmlSerializer.FromTypes(Type[]) contains:
... ommited for clarity
'Net5Sandbox.exe' (CoreCLR: clrhost): Loaded 'Microsoft.GeneratedCode'.
... (10000 times)
'Net5Sandbox.exe' (CoreCLR: clrhost): Loaded 'Microsoft.GeneratedCode'.
... ommited for clarity
While debug output with XmlSerializer.ctor(Type) contains:
... ommited for clarity
'Net5Sandbox.exe' (CoreCLR: clrhost): Loaded 'Microsoft.GeneratedCode'. // exactly once
... ommited for clarity
Looks like XmlSerializer.FromTypes(Type[]) generates dynamic assembly or type with serialization logic every time we call it in runtime. Which stays there until app restart and causes memory leak.
I see a few solution to this issue:
- Get back to
XmlSerializer.ctor(Type). However this will get us back toSystem.IO.FileNotFoundExceptionwhich is still better than memory leak. - Cache
XmlSerializer.FromTypes(Type[])result. However this requires changing serialization logic. - Use Xml Serializer Generator. Never done that, especially with nuget package, might require manual
.nuspec.