4

In my application (.NET Framework 4.5) I'm rendering some RDLC reports (50-60) in order to export them to a single PDF.

Unfortunately there seems to be a big memory leak, basically every LocalReportnever gets disposed.

This is my code:

public void ProcessReport(ReportDataSource[] reportDS, string reportPath)
{
    const string format = "PDF";
    string deviceInfo = null;
    string encoding = String.Empty;
    string mimeType = String.Empty;
    string extension = String.Empty;
    Warning[] warnings = null;
    string[] streamIDs = null;
    Byte[] pdfArray = null;

    using (var report = new LocalReport())
    {
        report.EnableExternalImages = true;
        report.ReportEmbeddedResource = reportPath;
        report.Refresh();

        foreach (var rds in reportDS)
        {
            report.DataSources.Add(rds);
        }
        report.Refresh();

        try
        {
            pdfArray = report.Render(format, deviceInfo, out mimeType, out encoding,
                out extension, out streamIDs, out warnings);
        }
        catch (Exception ex)
        {
            Console.WriteLine(ex.InnerException.Message);
            throw;
        }

        report.ReleaseSandboxAppDomain();
        report.Dispose();

        //Add pdfArray to MemoryStream and then to PDF - Doesn't leak
    }
}

I found the memory leak just by looking to Visual Studio memory panel, every time report.Render get's called it add 20-30mb and they never go down until I close the application. I'm sure that using the MemoryStreamis not the issue because even if commented I still get 200mb-250mb in memory that never get released. This is bad because after running this application like 3-4 times it reaches >1GB until it doesn't even run anymore. I also tried to manually call the GarbageCollector but didn't work. The application is 32 bit.

What can I do to fix this ?

SilentRage47
  • 934
  • 2
  • 14
  • 31
  • What's your evidence of a memory leak? What are you measuring, what are you seeing? Are you running this as a 32 or 64 bit process? What is the nature of the process you are doing this in (is it a long-running server app, or a short-lived batch application)? – Flydog57 Nov 09 '18 at 16:39
  • 1
    Curious: why call Dispose on an object in a Using? Also, what are the actual symptoms of the issue? I read this as "it doesn't work and here is why". Can you please state what you are seeing and any error messages? – UnhandledExcepSean Nov 09 '18 at 16:40
  • Also, you state the memorystream section isn't the problem. How did you determine that? – UnhandledExcepSean Nov 09 '18 at 16:44
  • Added some more information, the main issue is that if a user calls this function like 3 times that application reach >1GB of memory and then crash. It's just some reports batch rendering, once they're rendered and they user export or print them I don't need them in memory anymore, unfortunately my code doesn't dispose them. – SilentRage47 Nov 10 '18 at 09:43
  • Have you tried the solutions posted here? https://www.codeproject.com/Questions/636950/How-to-fix-memory-leak-in-Microsoft-Report-rdlc – UnhandledExcepSean Nov 12 '18 at 19:44
  • Yes,I tried that but with NetFx40_LegacySecurityPolicy enabled I can't use dynamic and I need it. – SilentRage47 Nov 13 '18 at 10:38

1 Answers1

8

I have a real solution and can explain why!

It turns out that LocalReport here is using .NET Remoting to dynamically create a sub appdomain and run the report in order to avoid a leak internally somewhere. We then notice that, eventually, the report will release all the memory after 10 to 20 minutes. For people with a lot of PDFs being generated, this isn't going to work. However, the key here is that they are using .NET Remoting. One of the key parts to Remoting is something called "Leasing". Leasing means that it will keep that Marshal Object around for a while since Remoting is usually expensive to setup and its probably going to be used more than once. LocalReport RDLC is abusing this.

By default, the leasing time is... 10 minutes! Also, if something makes various calls into it, it adds another 2 minutes to the wait time! Thus, it can randomly be between 10 and 20 minutes depending how the calls line up. Luckily, you can change how long this timeout happens. Unluckily, you can only set this once per app domain... Thus, if you need remoting other than PDF generation, you will probably need to make another service running it so you can change the defaults. To do this, all you need to do is run these 4 lines of code at startup:

    LifetimeServices.LeaseTime = TimeSpan.FromSeconds(5);
    LifetimeServices.LeaseManagerPollTime = TimeSpan.FromSeconds(5);
    LifetimeServices.RenewOnCallTime = TimeSpan.FromSeconds(1);
    LifetimeServices.SponsorshipTimeout = TimeSpan.FromSeconds(5);

You'll see the memory use start to rise and then within a few seconds you should see the memory start coming back down. Took me days with a memory profiler to really track this down and realize what was happening.

You can't wrap ReportViewer in a using statement (Dispose crashes), but you should be able to if you use LocalReport directly. After that disposes, you can call GC.Collect() if you want to be doubly sure you are doing everything you can to free up that memory.

Hope this helps!

Edit

Apparently, you should call GC.Collect(0) after generating a PDF report or else it appears the memory use could still get high for some reason.

Daniel Lorenz
  • 4,178
  • 1
  • 32
  • 39
  • Do you mind to share where you can you add this code? I'm using ASP .net 4.5 – Kevin Ng Apr 19 '19 at 09:33
  • start of the app. either in Application_Start or program Main. It can only run once. – Daniel Lorenz Apr 19 '19 at 11:39
  • For the Dispose() crashing, one response here on SO has a solution (it worked for me): create a fake HttpContext to avoid it... See https://stackoverflow.com/questions/46177755/ssrs-reportviewer-nullreference-exception-on-disposing – mBardos Oct 12 '21 at 06:29