Hello MuPDF Team, we knew about #248 fixing a ton but we've found out that the are still some leaks, at least in certain code paths we use the library for. So we've managed to pin it to 2 separate features.
Leak 1 - enumerating widgets (~0.2–0.8 KB per widget/doc)
Leak 2 - TextPage.Search (~0.5 KB per call with a hit)
Although small, a leak is a leak. An example that would process 10000 docs/hours means for us a ~200MB every hour added that eventually lead to memory exhaustion.
Minimal repro: loop opening a PDF, enumerate page.GetWidgets() (Leak 1) or run TextPage.Search for a string that matches (Leak 2), Dispose/Close each iteration, force full GC -> watch RSS (Resident Set Size) climb.
A 🤖's investigation:
Leak 1 - enumerating widgets (~0.2–0.8 KB per widget/doc), in Widget.SyncFromNative():
- Widget.FieldName calls pdf_load_field_name, whose returned char* is never freed. pdf_load_field_name2 is the freeing variant.
- The border/color reads go through Helpers.PdfObjBorrowed, which drops C# ownership of pdf_dict_get's result without calling pdf_drop_obj, leaking the kept reference each call.
Leak 2 - TextPage.Search (~0.5 KB per call with a hit), in Helpers.BorrowStextBlock:
- It allocates a native FzStextBlock via new_FzStextBlock__SWIG_2(...) but wraps it with cMemOwn: false, so delete_FzStextBlock is never called. Leaks one native block per FirstStextLinePtr call.
Suggested fixes: (1) FieldName → pdf_load_field_name2; (2) read the border/color dict entries with owning wrappers disposed after use rather than PdfObjBorrowed; (3) make BorrowStextBlock own the block and dispose it.
Environment: MuPDF.NET 3.28.2 (also 3.2.17.9) · MuPDF.NativeAssets.Linux.x64 1.28.2 · net10.0, workstation GC · Linux x64 (but applies to windows too)
Example repro below.
Use TestDocuments/.../WidgetTest/test_widget_parse.pdf for widgets, and any text PDF (e.g. TestDocuments/Demo/columns.pdf with search the) for search.
using System;
using System.IO;
using System.Linq;
using MuPDF.NET;
// Leak 1: dotnet run -- <pdf-with-form-fields> widgets
// Leak 2: dotnet run -- <pdf-with-text> search <word-that-occurs>
class Program
{
static void Main(string[] args)
{
var data = File.ReadAllBytes(args[0]);
var mode = args.Length > 1 ? args[1] : "widgets";
var needle = args.Length > 2 ? args[2] : "the";
Report(0);
for (int i = 1; i <= 3000; i++)
{
using var doc = new Document(stream: data);
for (int p = 0; p < doc.PageCount; p++)
{
var page = doc[p];
if (mode == "widgets")
{
// Leak 1: constructing each Widget retains native memory.
foreach (var w in page.GetWidgets()) { _ = w.FieldName; _ = w.Rect; }
}
else // search
{
var tp = page.GetTextPage();
_ = tp.ExtractText();
for (int k = 0; k < 20; k++)
{
var quads = TextPage.Search(tp, needle, hitMax: 1); // leaks per call that finds a hit
if (quads.Count > 0) { _ = quads[0].Rect; }
}
tp.Dispose();
}
page.Dispose();
}
doc.Close();
if (i % 500 == 0) Report(i);
}
}
// Native footprint after a forced full GC + finalizers. Managed heap stays flat; RSS keeps climbing.
// use `Process.GetCurrentProcess().WorkingSet64` on Windows
static void Report(int i)
{
GC.Collect(); GC.WaitForPendingFinalizers(); GC.Collect();
long rssKb = long.Parse(File.ReadLines("/proc/self/status")
.First(l => l.StartsWith("VmRSS:"))
.Split(' ', StringSplitOptions.RemoveEmptyEntries)[1]);
Console.WriteLine($"iter={i,-5} managedMB={GC.GetTotalMemory(true) >> 20} RSS_MB={rssKb / 1024}");
}
}
Hello MuPDF Team, we knew about #248 fixing a ton but we've found out that the are still some leaks, at least in certain code paths we use the library for. So we've managed to pin it to 2 separate features.
Leak 1 - enumerating widgets (~0.2–0.8 KB per widget/doc)
Leak 2 - TextPage.Search (~0.5 KB per call with a hit)
Although small, a leak is a leak. An example that would process 10000 docs/hours means for us a ~200MB every hour added that eventually lead to memory exhaustion.
Minimal repro: loop opening a PDF, enumerate page.GetWidgets() (Leak 1) or run TextPage.Search for a string that matches (Leak 2), Dispose/Close each iteration, force full GC -> watch RSS (Resident Set Size) climb.
A 🤖's investigation:
Environment: MuPDF.NET 3.28.2 (also 3.2.17.9) · MuPDF.NativeAssets.Linux.x64 1.28.2 · net10.0, workstation GC · Linux x64 (but applies to windows too)
Example repro below.
Use TestDocuments/.../WidgetTest/test_widget_parse.pdf for widgets, and any text PDF (e.g. TestDocuments/Demo/columns.pdf with search the) for search.