cancel
Showing results for 
Search instead for 
Did you mean: 

WPF Viewer memory leak in ReportAlbum

tomas_homola
Explorer
0 Kudos

Hello,

ReportAlbum instance is held by CommandManager and it cannot be collected by GC. See screenshot from .NET Memory Profiler.

I've analysed that the problem is ReportAlbum constructor that adds CommandBinding throught RegisterClassCommandBinding into CommandManager and passes instance method TabCloseExecuted.

Accepted Solutions (1)

Accepted Solutions (1)

tomas_homola
Explorer
0 Kudos

Hi,

I post my comment as an answer.

Workaround to fix the memory leak:

After disposing CrystalReportsViewer

var reportAlbum = (ReportAlbum)this.crystalReportsViewer.ViewerCore.GetType()<br>  .GetField("reportAlbum", BindingFlags.NonPublic | BindingFlags.Public | BindingFlags.Instance | BindingFlags.FlattenHierarchy)<br>  .GetValue(this.crystalReportsViewer.ViewerCore);<br>  this.crystalReportsViewer.Dispose();<br>  this.ViewerWorkaroundRemoveEventHandler(reportAlbum);

a I remove the CommandBinding and event handler.

        private void ViewerWorkaroundRemoveEventHandler(ReportAlbum reportAlbum)
        {
            var handler = reportAlbum.GetType().GetMethod("TabCloseExecuted", BindingFlags.NonPublic | BindingFlags.Public | BindingFlags.Instance | BindingFlags.FlattenHierarchy);
            var @delegate = Delegate.CreateDelegate(typeof(ExecutedRoutedEventHandler), reportAlbum, handler);

            var field = typeof(CommandManager).GetField("_classCommandBindings", BindingFlags.NonPublic | BindingFlags.Static);
            var classCommandBindings = (HybridDictionary)field.GetValue(null);
            var bindings = classCommandBindings[typeof(TabItem)] as CommandBindingCollection;
            if (bindings != null)
            {
                for (int i = 0; i < bindings.Count; i++)
                {
                    var binding = bindings[i];
                    var ev = binding.GetType().GetEvent("Executed");

                    var evField = (Delegate)binding.GetType().GetField("Executed",
                        BindingFlags.Public |
                        BindingFlags.NonPublic |
                        BindingFlags.Instance |
                        BindingFlags.FlattenHierarchy |
                        BindingFlags.Static).GetValue(binding);

                    if (evField.Target == reportAlbum)
                    {
                        ev.RemoveEventHandler(binding, @delegate);
                        bindings.RemoveAt(i);
                        break;
                    }
                }
            }
        }

Tomas

Answers (6)

Answers (6)

gareththom
Discoverer
0 Kudos

Hi,

We have been trying to track down a memory leak in our WPF application and have eventually tracked it down to this exact issue reported by Tomas Homola (dotMemory gives the exact same reference paths). His suggested workaround also worked for us, although it was very unhelpfully hidden behind a "Show all" button at the bottom of the comments and I almost missed it! Even liking the comment does not seem to un-hide it.

It's hard to believe that we are more than 5 years and 10 service packs on from when this bug was first reported, plus another year since Tomas fed back his workaround and this is still not fixed.

Can this please be escalated to be fixed in SP 33?

Can the "Best Answer" also be changed to Tomas Homola's workaround in the meantime, so other people trying to find a solution to this bug can locate it easier?

Thanks,

Gareth

tomas_homola
Explorer

Hi

I've added the workaround as answer.

Tomas

0 Kudos

Hi Thomas,

Could you please briefly describe your approach? I have the same problem with memory leaks 😞

I use diagnostics tools in Visual Studio 2017. There I can see the memory leaks very well. Even in a very simple test application that only contains the CrystalReportsViewer.

Thank you in advance for your help!

Kind regards.

Andreas

tomas_homola
Explorer

Hi Andreas,

After disposing CrystalReportsViewer

var reportAlbum = (ReportAlbum)this.crystalReportsViewer.ViewerCore.GetType()
.GetField("reportAlbum", BindingFlags.NonPublic | BindingFlags.Public | BindingFlags.Instance | BindingFlags.FlattenHierarchy)
.GetValue(this.crystalReportsViewer.ViewerCore);
this.crystalReportsViewer.Dispose();
this.ViewerWorkaroundRemoveEventHandler(reportAlbum);

a I remove the CommandBinding and event handler.

        private void ViewerWorkaroundRemoveEventHandler(ReportAlbum reportAlbum)
        {
            var handler = reportAlbum.GetType().GetMethod("TabCloseExecuted", BindingFlags.NonPublic | BindingFlags.Public | BindingFlags.Instance | BindingFlags.FlattenHierarchy);
            var @delegate = Delegate.CreateDelegate(typeof(ExecutedRoutedEventHandler), reportAlbum, handler);

            var field = typeof(CommandManager).GetField("_classCommandBindings", BindingFlags.NonPublic | BindingFlags.Static);
            var classCommandBindings = (HybridDictionary)field.GetValue(null);
            var bindings = classCommandBindings[typeof(TabItem)] as CommandBindingCollection;
            if (bindings != null)
            {
                for (int i = 0; i < bindings.Count; i++)
                {
                    var binding = bindings[i];
                    var ev = binding.GetType().GetEvent("Executed");

                    var evField = (Delegate)binding.GetType().GetField("Executed",
                        BindingFlags.Public |
                        BindingFlags.NonPublic |
                        BindingFlags.Instance |
                        BindingFlags.FlattenHierarchy |
                        BindingFlags.Static).GetValue(binding);

                    if (evField.Target == reportAlbum)
                    {
                        ev.RemoveEventHandler(binding, @delegate);
                        bindings.RemoveAt(i);
                        break;
                    }
                }
            }
        }

Tomas

0 Kudos

Hi Tomas,

thank you for the fast answer.

This is really interesting. I've just made an experiment with the test application. Your solution seems to be working correctly, the improvement is clearly visible (see screenshots in the attachment.)

Thanks!

Andreas

Screenshot 1. Without workaround.

Screenshot 2. With workaround.

0 Kudos

Cool,

Just be aware if you run into resource issues I won't be able to escalate to DEV.

Also, I just escalated possibly a similar issue to DEV also, CPU seems to consume 1 ->5 % when the report is loaded, may be the same animation causing the problem.

Set for SP 22.

Don

0 Kudos

The CR Engine will only allow 3 reports to be processed at any one instance, it will work up to 75 ish because not all reports are executed at the exact same time. So depending on the report load you may be able to possible get 75 jobs running at one time.

For high Report processing applications this is not the right way to use it. You will run into all sorts of resource issues and nothing we can do about it, it's by design.

CR for VS simply was not designed nor is it capable to be used in a high work load environment.

All I can suggest then is possibly don't use the WPF viewer and use the CR Windows Form viewer, it may help.

What you really need to use is CR Server or the full BOE Servers that can handle 1000's of reports in any given time depending on licensing and other options, assuming you give it enough resources and add multiple Report Servers you will be able to manage multiple reports.

Be aware each users instance of a report consumes one license.

Here's a link for more info in CR Server:

https://www.sap.com/products/crystal-server.html

It's a simplified version of the full BOE Product, limited to one PC but can have up to 4 Report Application Servers on it. RAS is running as a service and not an inproc version you may be/are using.

Don

tomas_homola
Explorer
0 Kudos

Hi Don,

Thanks for your explanation. I've tested that up to 75 viewers can be used in one time.

I've created a workaround that removes the CommandBinding from CommandManager through reflection and the WPF viewer is fine now with memory.

Tomas

0 Kudos

Hi Tomas,

So all you are doing is loading 1000 instances of the WPF viewer... WHY, makes no sense to do that, you cannot load 1000 reports, engine won't allow it, 3 max.

Load a report, when done close the report and load a new one in the same instance of the viewer. The single instance leaks 316 bytes, nothing to be concerned about.

Don

tomas_homola
Explorer
0 Kudos

Hi Don,

The engine allows me to have multiple viewers in same time. There is no limit.

We have users that browse many reports during a day and they require to see them in one time.

The leak is bigger because the CrystalReportsViewer is placed on a Window. This window contains more UI controls than single CrystalReportsViewer. Plus ViewModels...

Tomas

0 Kudos

Where does it show that it's the WPF viewer that is the cause of the leak?

How much of a leak are you seeing?

Have you ruled out all of Microsoft's dll's don't leak?

tomas_homola
Explorer
0 Kudos

Here is example:

<!-- MainWindow.xaml -->
<Window x:Class="WpfApp2.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        Title="MainWindow" Height="350" Width="525">
    <Grid>
        <Button Content="OpenCrystal" Click="Button_Click" />
    </Grid>
</Window>

/* MainWindow.xaml.cs */
using System;
using System.Windows;

namespace WpfApp2
{
    /// <summary>
    /// Interaction logic for MainWindow.xaml
    /// </summary>
    public partial class MainWindow : Window
    {
        public MainWindow()
        {
            InitializeComponent();
        }

        private void Button_Click(object sender, RoutedEventArgs e)
        {
            for (int i = 0; i < 1000; i++)
            {
                var wnd = new CrystalReportsWindow();
                wnd.Show();
                wnd.Close();
            }
        }
    }
}

<!-- CrystalReportsWindow.xaml -->
<Window 
    x:Class="WpfApp2.CrystalReportsWindow"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:crview="clr-namespace:SAPBusinessObjects.WPF.Viewer;assembly=SAPBusinessObjects.WPF.Viewer"
    Title="CrystalReportsWindow" Height="600" Width="800">
    <crview:CrystalReportsViewer x:Name="crystalReportsViewer" />
</Window>

/* CrystalReportsWindow.xaml.cs */
using System;
using System.Windows;

namespace WpfApp2
{
    /// <summary>
    /// Interaction logic for CrystalReportsWindow.xaml
    /// </summary>
    public partial class CrystalReportsWindow : Window
    {
        public CrystalReportsWindow()
        {
            InitializeComponent();
        }
    }
}


The leak is everytime the SAPBusinessObjects.WPF.Viewer.CrystalReportsViewer is created. It prevents the closed CrystalReportsWindow to be collected.

The ReportAlbum instances are held by CommandManager.

After 1000 crystal reports viewer windows shown&closed this simple example takes 789464 KB of memory.