I have a simple application converting spreadsheets to multiple images by calling this method 15 times:
public static void ConvertFile(string inputPath, string outputDirectory, int i)
{
var spreadSheet = ExcelFile.Load(inputPath, new XlsxLoadOptions
{
StreamingMode = LoadStreamingMode.Read
});
var pages = spreadSheet.GetPaginator(new PaginatorOptions { SelectionType = SelectionType.EntireFile }).Pages;
for (var index = 0; index < pages.Count; index++)
{
var fileName = $"Spreadsheet-{i}_Page-{index}.bmp";
var page = pages[index];
using var stream = new FileStream(Path.Combine(outputDirectory, fileName), FileMode.Create);
page.Save(stream, new ImageSaveOptions(ImageSaveFormat.Bmp));
}
}
When I run this code in a .net472 console application, the memory consumption is the following:
I updates Gembox.Spreadsheet to 49.0.1557-hotfix and changed the target framework from “.net6.0” to “.net6.0-windows” and indeed the behaviour was not reproducible.
The memory consumption:
As I plan to use .net6.0 in my application to deploy for Linux OS though I cannot change my target framework in production.
Can you reproduce the issue with using target framework “net6.0” and an excel document that has a random value in the cell AV:125?
Best regards,
Sarah
Additional note: My original document also included a larger amount of cells with background colours and some merged and centered cells with texts in it, maybe this increases the effect. The last cell is AV:125.
Hi Mario, I created a small GitHub Repo with my repro code: GitHub - SarahAmagno/ReproOOM
I hope it is reproducible with this, otherwise I am not sure what setting in my local environment could cause this behaviour.
Sorry for the delayed answer. It took us some time to analyze this scenario.
It seems that memory increase is happening because GemBox.Spreadsheet uses SkiaSharp for rendering and SkiaSharp underneath uses the native Skia library.
It seems that either SkiaSharp or, more probably, native Skia has some caching implemented in the unmanaged memory and the size of that cache stabilizes around 1 GB.
Here is a code that we used to reproduce this behavior:
GCSettings.LargeObjectHeapCompactionMode = GCLargeObjectHeapCompactionMode.CompactOnce;
GCSettings.LatencyMode = GCLatencyMode.Batch;
var workbook = new ExcelFile();
workbook.Worksheets.Add("Sheet1").Cells["A1"].Value = "Hello world";
for (int i = 0; i < 10_000; ++i)
{
using var stream = new MemoryStream();
workbook.Save(stream, new ImageSaveOptions(ImageSaveFormat.Png) { RenderingMode = RenderingMode.Skia });
if (i % 50 == 0)
{
GC.Collect(GC.MaxGeneration, GCCollectionMode.Forced, blocking: true, compacting: true);
Console.WriteLine("GC collect " + i);
}
}
The memory increases to 1.1 GB (almost 5 minutes of runtime on my machine) and then drops to 0.9 GB. After that, it increases again to 1.1 GB and then drops again to 0.9 GB. This behavior repeats indefinitely. Forcing garbage collection doesn’t have any effect, so I conclude that this memory is occupied by the unmanaged code.
Here is a screenshot of the Visual Studio Performance Profiler:
thank you for the detailed explanation.
I understand that this is no memory leak, nevertheless the memory consumption of over 1 GB for the conversion of multiple files is quite a lot - is there a possibility, to reduce it?
Can the internal cache be cleared or limited, for example?
The Skia library offers the GRContext for operations like this, but from “outside” of the GemBox package I cannot access the created image’s GRContext.
After further investigation, we were able to identify the root cause of the issue - font cache in Skia.
Here is an updated code snippet that shows how the Skia font cache increases over time and how memory usage drops when the SkiaSharp.SKGraphics.PurgeFontCache method is called:
GCSettings.LargeObjectHeapCompactionMode = GCLargeObjectHeapCompactionMode.CompactOnce;
GCSettings.LatencyMode = GCLatencyMode.Batch;
var workbook = new ExcelFile();
workbook.Worksheets.Add("Sheet1").Cells["A1"].Value = "Hello world";
for (int i = 0; i < 10_000; ++i)
{
using var stream = new MemoryStream();
workbook.Save(stream, new ImageSaveOptions(ImageSaveFormat.Png) { RenderingMode = RenderingMode.Skia });
Console.WriteLine("SKGraphics.FontCacheUsed/FontCacheLimit = {0}/{1}", SkiaSharp.SKGraphics.GetFontCacheUsed(), SkiaSharp.SKGraphics.GetFontCacheLimit());
if (i % 20 == 0)
{
SkiaSharp.SKGraphics.PurgeFontCache();
Console.WriteLine("SKGraphics.PurgeFontCache " + i);
}
if (i % 50 == 0)
{
GC.Collect(GC.MaxGeneration, GCCollectionMode.Forced, blocking: true, compacting: true);
Console.WriteLine("GC collect " + i);
}
}
We will also implement a quick-fix in which we plan to globally cache SKTypeface instances because it seems that the Skia font cache size is increased with each new SKTypeface instance. We will notify you when we implement this quick-fix so you can test it.