Repository navigation
fix(translate): stop the sidecar's RAM climbing with every translated page - #136
Merged
Merged
Conversation
PyMuPDF 1.27, which the translation memory fix needs, ignores
TEXT_DEHYPHENATE in every output mode. Without a replacement, chat
indexed 1,129 words of a 346-page book as halves ("trans- action") that
neither keyword nor semantic search matches.
ingest._join_broken_words reproduces the flag's rule exactly: a line
ending in "-" loses it and runs into the next, unless the line is only
the dash. On PyMuPDF 1.25.5 the ingestor's chunks are byte-identical
before and after this change on all seven text documents of the RAG
benchmark, so CHUNKER_VERSION stays and no index is rebuilt.
A variant that spared compounds (joining only lowercase continuations)
was measured and lost two questions on BERT, whose tables use "-" for
empty cells; the parity test's expected text is what 1.25.5 indexed.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Before 1.27.2, PyMuPDF's C++ helper behind annot_xrefs leaks a list on every call, and insert_pdf copies every page's links through it. rebuild_rolling_pdf re-inserts the whole document after each page, so a translation leaked ~2,120 lists (~150 KB) per page on a 346-page book: O(pages^2) for a whole document, and invisible to gc, since each list carries one extra reference and no referrer. Measured over 129 pages of that book after a ~40-page warm-up: +1.25 MB/page on 1.25.5, +0.18 on 1.27.2.3, live objects flat. 1.26.7 still leaks; 1.27.2.3 and 1.28.2 don't. <1.28 because 1.28 deprecates the `fitz` name every module imports. The GIL property the shared fonts rely on was re-checked on 1.27.2.3 (no PyEval_SaveThread import). The new test rebuilds a linked document's rolling PDF 20 times: 1,279 objects stay alive on 1.25.5; on 1.27.2.3 fewer than the threshold of one per rebuild. Pages without links never leaked, which is why the older rebuild tests could not catch it. 1.27 also ignores TEXT_DEHYPHENATE (handled in the previous commit) and its find_tables reads ResNet's architecture figure as tables; together with the dehyphenation fix, retrieval-only hit@1 is 0.848 against 0.857 on 1.25.5 (one question, McNemar p = 1.0). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
glibc keeps what Python frees inside its arenas and returns memory on its own only from the top of a heap. On PyMuPDF 1.27, a 129-page Argos run ended ~420 MB above what malloc_trim brings it back to; a forced gc.collect() freed next to nothing, since the memory was already free. The consumer loop in _process_with_babeldoc now ends each finished page with utils.memory.release_freed_memory, off the event loop: malloc_trim(0) on glibc, a no-op on Windows, macOS and musl. It runs after the rolling rebuild, so that document is freed too, and after chunk_ready, so the viewer's swap never waits on it. No gc.collect(): ~277 ms a page for nothing. Measured on this branch over the same 129 pages, run alone: -0.19 MB/page after warm-up (main: +1.25), 1,656 MB at the end (main: 2,118), a final forced trim frees 2 MB (main: 408), 4.50 s/page. The trim costs 8-22 ms a page. The helper and its tests are ported from fix/release-freed-memory (dc4c1be), whose prose blamed glibc for the 7 GB out-of-memory that was really BabelDOC's fonts (fixed by font_cache.py); the numbers here are re-measured. Windows is still to be measured (#134). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The replacement for the flag only knew "-". Checked one by one on PyMuPDF 1.25.5, the flag also took U+00AD (soft hyphen), U+2010 and U+2011 for a line-break hyphen, and kept a lone U+2010 line the way it keeps a lone "-". Word processors export these; the benchmark corpus has none, which is why the chunk-for-chunk parity check missed them. Without this, "trans" / "action" was indexed with the soft hyphen in the middle (NFKC keeps it). The U+2010/U+2011 cases join the parity table, their expected text read off main's ingestor on 1.25.5. U+00AD is tested on the helper itself: a base-14 font reads it back as "-" and PyMuPDF's built-in fonts drop it, so no portable PDF fixture can produce it; the expected text is what 1.25.5 indexed for a page set in DejaVu Sans. Chunks of all seven benchmark documents are still byte-identical to main on 1.25.5. Also says in data-and-chat.md why CHUNKER_VERSION stays although 1.27's find_tables changes some chunks. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Review found the same quantities quoted differently. Each now names what it was measured on: - leaked objects: ~2,120 lists per rebuild while the book is still untranslated, ~1,690 live objects a page averaged over the 129-page run (translated pages carry no links). Two lists per link, not one: the regression test leaves 1,278 objects after 20 rebuilds on 1.25.5, against 2 lists x 32 links (40, less the 8 on its two translated pages) x 20 = 1,280, and 0 on 1.27.2.3. - freed heap: ~410 MB at the end of the main run, ~420 MB on 1.27. - the slope is far larger than the lists' own ~150 KB; the notes now say the causal link is the before/after measurement of the bump, and that how each list pins memory around it is not measured. Also turns the two causes into prose, so Markdown no longer runs them into the font section's list. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
3 of 4 tasks
Merged
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Why
During a translation the sidecar's RAM climbed with every page, about +1.25 MB a page after warm-up on a 346-page book. That held even after #135 fixed the fonts. Measuring the real pipeline after each page found two causes:
_extra.JM_get_annot_xref_listleaks a list per call, andinsert_pdfcopies every link through it (two lists per link).rebuild_rolling_pdfre-inserts the whole document after every page. On the book that leaked ~2,120 lists per rebuild while it was untranslated, and ~1,690 live objects a page averaged over a run.malloc_trimhanded back ~410 MB. A forcedgc.collect()freed nothing: the memory was already free, just never returned.What changes
fix(chat): join line-break hyphens without PyMuPDF's flagTEXT_DEHYPHENATE. Without a replacement, 1,129 words of the book were indexed as halves (trans- action).ingest._join_broken_wordsreproduces the flag's rule exactly.fix(translate): requirePyMuPDF>=1.27.2,<1.28fix(translate):malloc_trim(0)after every finished pageutils/memory.py, run off the event loop, after the rebuild and afterchunk_ready. A no-op on Windows, macOS and musl. Ported from the unmergedfix/release-freed-memory, whose root-cause prose was wrong (that 7 GB OOM was the fonts).fix(chat): the Unicode hyphens toodocs(translate): reconcile the figuresNo chat index is rebuilt. On PyMuPDF 1.25.5 the ingestor's chunks are byte-identical to
mainon all 7 text documents of the RAG benchmark, soCHUNKER_VERSIONis unchanged. A "smarter" join that kept compounds apart (Bose-/Einstein) was measured and dropped: it lost 2 questions.Results
129 text pages of the book, Argos, 2 pages in flight, measured after warm-up:
main(PyMuPDF 1.25.5)Speed was compared A-B-B-A, sequentially, 46 pages each:
mainThat's no difference beyond
main's own run-to-run spread. The trim costs 8–22 ms a page, and the 129-page run on this branch did 4.50 s/page.Behaviour changes worth knowing
Chat retrieval (retrieval-only, 131 questions), hit@1:
main, 1.25.5The one question still lost (
p10, ResNet) comes from 1.27'sfind_tables. It now reads ResNet's architecture figure as half-empty two-column tables (121 → 133 chunks on that paper). Tightening the ingestor's mostly-empty-grid guard could be a follow-up; therag-qualitygate is 0.80.find_tablesprints a one-linepymupdf_layoutnotice to stdout once per process; it goes to shell.log.Translation output: it differs from
mainon a few pages' wording, but no more than two runs ofmaindiffer from each other. Argos's batching depends on timing.Test plan
pytest tests: 1440 passed (1425 before, plus 5 trim, 1 leak and 9 dehyphenation cases);ruffclean.main's ingestor on 1.25.5), never from the new code.pm1255,pm1272,pm1272-dehyph,pm1272-mupdfrule, compared withcompare.py.processor.py. The call site has no unit test, so this is the check../build-sidecar.sh(Linux):pytest -m smoke21 passed, frozen half included.donein 30 s, 0 failed paragraphs, Vietnamese output.cancelled0.1 s after the request.resnet.pdf:done, 133 chunks.frozen-sidecarworkflow dispatched (Windows + Ubuntu): https://github.com/TranBaDat2607/PDFusion/actions/runs/39939620038Found along the way, not fixed here
cannot import name 'NDArray' from partially initialized module 'numpy._typing'. ChromaDB's import ran while theengine-warmthread was still importing numpy through BabelDOC. Two reruns, one immediately and one after 60 s, passed. Probably onmainas well, since neither import path changes here, but not verified there.main("contains no paragraphs"). The fix sits on the unmergedfix/skip-textless-pages.🤖 Generated with Claude Code