Memory problem
When working with complex reports, which use the internal script, I noticed a significant consumption of RAM. For web services this may be critical.
Why is this happening? After compilation of the report script a small library (assembly) remains in memory. It persists by the framework engine for subsequent calls. In the end, "garbage collector" will remove them, but still there is a memory consumption having place before.
The way out of this situation is to use a separate application domain for the report with the script. By using the application domain, we can remove an assembly from memory, or to be more accurate - to unload domains that contain the assembly. Thus, by isolating the report and accompanying library with the script, we can easily clear the resources simply swapping out the unnecessary domain.
The pay for this – is the necessity for creation an assembly that is loaded in the second domain and some inconveniences of calling functions of the second domain from the first one.
Using domains
Any .Net application already has one default domain. It was decided to create another domain and put the assembly with the report into it. Besides the report object, the assembly will contain functions of working with the report: show, export.
I found marshaling and proxy to be the easiest way to communicate with the second domain. Usually marshaling is used for inter-process communication, but this is also true for domains. Marshaling allows the client in the same domain invoke the object functions from another domain. We will refer to the object in the domain through a proxy.
The interaction between the domains is the same as between different processes. To access code in another domain, you should use a proxy. Proxy is an object replacement. It redirects calls from one domain to another.
Let's look at the workflow of a report with one or two domains:
The figure shows that one domain is run in a single process. And we are launching a report with a script within it. In this report, the script is compiled and loaded into the assembly domain. If we call a lot of different reports with the script, the number of such assemblies is greatly increased, resulting in the memory waste.
Now let's consider the case with the two domains:
In this case, the report is run in a separate domain but still in the same process. In this case, the assembly of the report script is loaded into the second domain. By using the proxy all functions in the second domain are available in the first domain. When we finish our work with the report and close it, the domain is unloaded together with all assemblies while freeing the memory.
Implementation
Create a Windows Forms application. This application with a standard class will be needed in order to create a second class and invoke functions from the loaded assembly in it.
Add two buttons to the form: Run report, Export report to PDF.
Add the function for creating a new domain:
1 2 3 4 5 |
public AppDomain NewDomain() { AppDomain domain = AppDomain.CreateDomain("NewDomain"); return domain; } |
And function of unload domain:
1 2 3 4 |
public void UnloadDomain(AppDomain domain) { AppDomain.Unload(domain); } |
Add a class library project to the solution. Call it a New Domain. So we have an assembly, which will be uploaded into a new domain.
Be sure to inherit the class from MarshalByRefObject. To work with FastReport .Net, we need to add a reference to FastReport.dll library in the project.
Now you can create an instance of the Report object:
public Report report1 = new Report();
We will upload the report with a script in it. To do this, create report download function:
1 2 3 4 |
public void LoadReport(string path) { report1.Load(path); } |
And functions run and export the report:
1 2 3 4 5 6 7 8 9 10 |
public void ShowReport() { report1.Show(); } public void ExportToPDF() { FastReport.Export.Pdf.PDFExport pdf = new FastReport.Export.Pdf.PDFExport(); pdf.Export(report1); } |
This is all we need to work with the report. Build the assembly and put in a folder with the application's executable file or add a reference to the assembly in the application project.
Go to the project application.
Earlier we wrote a function of adding a new domain. Now we need to download the created above assembly in this domain. To work with the assembly, we need a proxy:
1 2 3 4 5 |
public dynamic CreateProxy(AppDomain domain) { dynamic proxyOfChildDomainObject = domain.CreateInstanceFromAndUnwrap("NewDomain.dll", "NewDomain.NewDomainClass"); return proxyOfChildDomainObject; } |
Here we create an instance of a proxy class for our assembly. CreateInstanceFromAndUnwrap function creates a new instance of the specified type defined in the specified assembly file. Specify the name of the assembly file and the full class name as a parameter.
So we can create a new domain and proxy to work with the assembly. Now let's add the code for the first button:
private void button1_Click(object sender, EventArgs e)
{
AppDomain domain = NewDomain();
dynamic proxy1 = CreateProxy(domain);
proxy1.LoadReport(Environment.CurrentDirectory + "/Matrix.frx");
proxy1.ShowReport();
UnloadDomain(domain);
}
Let's take a closer look. In the first line create a new application domain using the function NewDomain(). Next, create a proxy for our assembly in the second domain. You can now work with the functions of the assembly from the second domain. Load the report. And run it in preview mode. After reviewing the report, the domain is unloaded.
Use similar code for the second button. Call the function of export the report to PDF instead of showing the report. This will display the window of export settings and save dialog box.
1 2 3 4 5 6 7 8 |
private void button2_Click(object sender, EventArgs e) { AppDomain domain = NewDomain(); dynamic proxy1 = CreateProxy(domain); proxy1.LoadReport(Environment.CurrentDirectory + "/Matrix.frx"); proxy1.ExportToPDF(); UnloadDomain(domain); } |
That's all. The application is ready.
Although using multiple domains somewhat slows down the reports, after all it can significantly save the memory when running reports with built-in script.
Script assembly will be deleted from the memory, along with the domain unloading. That can be critical for heavy systems, such as web services.
In my opinion, the most effective way to work with reports with the script is to run this report in a separate application domain with the re-creation of this domain by N reports building. With the unloading of accumulated assemblies you can adjust the load on the memory. The number N should be selected by experimentation to determine the balance of the cost of resources to create domain and memory cleaning