You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
load() spends minutes and GBs on the AcroForm while holding the GIL (signed PDFs) #399
load() spends minutes and several GB processing the document's AcroForm, all while holding the GIL. On digitally signed PDFs (~80–90 pages, 84–112 form fields incl. /Sig widgets) this freezes every other Python thread for ~2 min, and one of them runs out of memory (> 9 GB) before a single page is decoded.
Removing only the catalog's /AcroForm makes the problem disappear:
91-page PDF
Load
Conversion
Peak memory
Longest event loop freeze
as is
111 s
232 s
6.6 GB
117 s
/AcroForm removed
0.0 s
116 s
2.7 GB
0.0 s
The 80-page PDF goes from OOM at 9 GB to 77 s / 2.9 GB. The Markdown output is byte-identical with and without /AcroForm.
Other observations:
load(..., page_range=(1, 1)) still takes 111 s / 3.7 GB, so the cost is document-level.
Every 10-page slice of the same PDF (pages copied into a new file) loads in 0.0 s.
Removing page /Annots changes nothing; parser_threads=1 and release_native_memory_every_n_pages=8 don't help.
faulthandler shows the thread stuck on docling_parse/pdf_parser.py:1598 (self._parser.load_document_from_bytesio(...)) for the whole time; file path input behaves the same.
Extract the AcroForm lazily (only when get_annotations() is called) or behind an option, and avoid whatever makes it scale this badly. My guess: widget /P references back to pages get expanded repeatedly into the raw dict.
Release the GIL during the native load (py::gil_scoped_release).
docling 2.132.0 · docling-parse 7.22.1 · Python 3.14 · Linux
changed the title [-]Document load holds the GIL (~1 s/page), freezing other Python threads[/-][+]load() spends minutes and GBs on the AcroForm while holding the GIL (signed PDFs)[/+]on Oct 9, 2026
Thanks for the precise report; it made this quick to pin down. I could reproduce it with a synthetic file, and the fix is in #410.
What happens.load() builds the document annotations eagerly, and that serialises /AcroForm to JSON (to_json() in src/parse/qpdf/to_json.h). Your signature dictionaries carry /Reference[*]/Data, which Acrobat and iText point at the catalog for DocMDP and FieldMDP signatures. From the catalog the serialiser walks the page tree and the structure tree, and its cycle guard only looks at the current path, so every structure element's /Pg expands its page again, fonts included. That is why the cost is document-level, why page_range and /Annots make no difference, and why page slices load instantly: a copied slice has neither the catalog's /AcroForm nor its /StructTreeRoot. The widget /P entries are not the route; those are already skipped.
Numbers on a 33-page tagged PDF from the regression corpus (15.6k structure elements, 6.4k /Pg references), DoclingThreadedPdfParser().load(path, page_range=(1, 1)), peak RSS:
before
after
untouched
0.02 s, 125 MB
0.02 s, 85 MB
+ DocMDP signature with /Data → catalog
48.6 s, 2.0 GB
0.06 s, 85 MB
+ same signature, /StructTreeRoot removed
0.12 s
–
The fix (#410): to_json() stops at catalog, page-tree and structure-tree dictionaries and writes a reference instead; the cycle guard keys on object ids instead of unparsed text; and the annotations are built on the first get_annotations() call instead of at load, so a pages-only conversion never runs this at all.
Releasing the GIL during the native load is not in that PR. After the fix the load is qpdf's parse only, and releasing the GIL there needs a mutex around the per-key decoder maps against concurrent load() calls on one parser. If it still matters for your event loop once the fix is out, it is worth its own issue.
If you can check that your files take the same route, that would confirm it:
Activity
[-]Document load holds the GIL (~1 s/page), freezing other Python threads[/-][+]load() spends minutes and GBs on the AcroForm while holding the GIL (signed PDFs)[/+]wittjeff commentedon Oct 16, 2026
Thanks for the precise report; it made this quick to pin down. I could reproduce it with a synthetic file, and the fix is in #410.
What happens.
load()builds the document annotations eagerly, and that serialises/AcroFormto JSON (to_json()insrc/parse/qpdf/to_json.h). Your signature dictionaries carry/Reference[*]/Data, which Acrobat and iText point at the catalog for DocMDP and FieldMDP signatures. From the catalog the serialiser walks the page tree and the structure tree, and its cycle guard only looks at the current path, so every structure element's/Pgexpands its page again, fonts included. That is why the cost is document-level, whypage_rangeand/Annotsmake no difference, and why page slices load instantly: a copied slice has neither the catalog's/AcroFormnor its/StructTreeRoot. The widget/Pentries are not the route; those are already skipped.Numbers on a 33-page tagged PDF from the regression corpus (15.6k structure elements, 6.4k
/Pgreferences),DoclingThreadedPdfParser().load(path, page_range=(1, 1)), peak RSS:/Data→ catalog/StructTreeRootremovedThe fix (#410):
to_json()stops at catalog, page-tree and structure-tree dictionaries and writes a reference instead; the cycle guard keys on object ids instead of unparsed text; and the annotations are built on the firstget_annotations()call instead of at load, so a pages-only conversion never runs this at all.Releasing the GIL during the native load is not in that PR. After the fix the load is qpdf's parse only, and releasing the GIL there needs a mutex around the per-key decoder maps against concurrent
load()calls on one parser. If it still matters for your event loop once the fix is out, it is worth its own issue.If you can check that your files take the same route, that would confirm it: