diff --git a/devel/0178.md b/devel/0178.md new file mode 100644 index 0000000000..c03ba3521c --- /dev/null +++ b/devel/0178.md @@ -0,0 +1,68 @@ + +# [0178] 修复 mupdf 渲染器内存泄漏 + +## 1 相关文档 +- [0177](0177.md) - 实现 tm_memory 运行时内存监控 +- [dddd.md](dddd.md) - 任务文档模板 + +## 2 任务相关的代码文件 +- `src/Plugins/MuPDF/mupdf_renderer.cpp` +- `src/Plugins/MuPDF/mupdf_picture.cpp` +- `tests/Plugins/MuPDF/mupdf_renderer_test.cpp` + +## 3 如何测试 + +### 3.1 确定性测试(单元测试) +``` +xmake b mupdf_renderer_test && xmake r mupdf_renderer_test +``` + +### 3.2 非确定性测试(用户场景验证) + +#### 普通用户可感知的优化 + +本次修复针对的是 **PDF 导出/预览时包含 pattern brush(图案笔刷)的文档** 的内存泄漏问题。普通用户可通过以下方式验证: + +1. **创建带有图案背景的文档** + - 在文档中插入一个带有 pattern brush 填充的图形(例如:使用带有纹理或背景图案的画笔填充的矩形) + - 或使用包含背景图案/水印的复杂文档 + +2. **反复导出为 PDF 或反复预览** + - 多次执行 "File -> Export -> PDF" 导出操作 + - 或反复切换包含 pattern 的页面进行预览/渲染 + +3. **观察系统内存占用** + - 使用系统监控工具(如 Linux 的 `top`、`htop` 或系统监视器)观察 Mogan 进程的内存占用 + - **修复前**:每次导出/渲染后,RSS 内存持续增长,不会回落,长时间使用后可能导致软件卡顿甚至崩溃 + - **修复后**:内存占用在导出/渲染完成后保持稳定,不会随操作次数持续攀升 + +## 4 如何提交 + +提交前执行以下最少步骤: + +```bash +gf fmt --changed-since=main +xmake b mupdf_renderer_test && xmake r mupdf_renderer_test +``` + +## 5 What + +修复 mupdf 渲染器中 `register_pattern` 函数的三处内存泄漏: + +1. `pdf_new_buffer_processor` 创建的 processor 仅被 `pdf_close_processor` 关闭,未被 `pdf_drop_processor` 释放 +2. `pdf_dict_puts(ctx, subres, "XObject", xres)` 后未调用 `pdf_drop_obj(ctx, xres)` 释放原始引用 +3. 手动创建的 `pdf_pattern` 使用 `fz_malloc_struct` 分配(清零),但未初始化 `storable.drop` 函数指针,`pdf_drop_pattern` 在引用计数降至 0 时因 `drop` 为 NULL 而无法释放 `pat`、`resources` 和 `contents` + +## 6 Why + +在渲染或导出包含 pattern brush(如背景图案、纹理填充)的文档时,`register_pattern` 会被反复调用。由于上述三处内存泄漏,每次调用都会累积未释放的内存,导致: +- 长时间使用 Mogan 编辑/导出文档时内存占用持续增加 +- 频繁操作带有 pattern 的文档时可能触发 OOM 或导致软件响应变慢 +- 影响使用 MuPDF 渲染路径的所有平台的用户体验 + +## 7 How + +1. 在 `pdf_close_processor` 后添加 `pdf_drop_processor` +2. 在 `pdf_dict_puts` 后添加 `pdf_drop_obj(ctx, xres)` +3. 定义 `mupdf_pattern_drop_imp` 作为手动创建 pattern 的释放函数,并使用 `FZ_INIT_STORABLE` 初始化 `pat` +4. 移除 `register_pattern` 中多余的 `pdf_drop_pattern(ctx, pat)` 调用,让 `mupdf_pattern` 的引用计数管理接管生命周期 diff --git a/src/Plugins/MuPDF/mupdf_picture.cpp b/src/Plugins/MuPDF/mupdf_picture.cpp index 31bc9bfb91..4787186597 100644 --- a/src/Plugins/MuPDF/mupdf_picture.cpp +++ b/src/Plugins/MuPDF/mupdf_picture.cpp @@ -477,11 +477,19 @@ get_real_size_from_dpi (int px, int dpi) { static void format_picsize_string (index_type px, index_type dpi, int& w, int& h, string* out_wcm_pointer, string* out_hcm_pointer) { + double dwcm= get_real_size_from_dpi (px.x1, dpi.x1); + double dhcm= get_real_size_from_dpi (px.x2, dpi.x2); + + if (!has_current_view ()) { + // Fallback for headless/test environments: 1cm = 28.3464567pt + w= (int) (dwcm * 28.3464567 + 0.5); + h= (int) (dhcm * 28.3464567 + 0.5); + return; + } + SI cm = get_current_editor ()->as_length ("1cm"); SI pt = get_current_editor ()->as_length ("1pt"); SI par = get_current_editor ()->as_length ("1par"); - double dwcm= get_real_size_from_dpi (px.x1, dpi.x1); - double dhcm= get_real_size_from_dpi (px.x2, dpi.x2); w = dwcm * cm / pt; h = dhcm * cm / pt; diff --git a/src/Plugins/MuPDF/mupdf_renderer.cpp b/src/Plugins/MuPDF/mupdf_renderer.cpp index f5f3fa01a1..2c54534479 100644 --- a/src/Plugins/MuPDF/mupdf_renderer.cpp +++ b/src/Plugins/MuPDF/mupdf_renderer.cpp @@ -123,6 +123,14 @@ class mupdf_pattern { CONCRETE_NULL_CODE (mupdf_pattern); +static void +mupdf_pattern_drop_imp (fz_context* ctx, fz_storable* storable) { + pdf_pattern* pat= (pdf_pattern*) storable; + pdf_drop_obj (ctx, pat->resources); + pdf_drop_obj (ctx, pat->contents); + fz_free (ctx, pat); +} + /****************************************************************************** * pdf fonts ******************************************************************************/ @@ -486,6 +494,7 @@ mupdf_renderer_rep::register_pattern (brush br, SI pixel) { pdf_obj* ref = pdf_add_image (ctx, doc, image_pdf->img); pdf_dict_puts (ctx, xres, "pattern-image", ref); pdf_dict_puts (ctx, subres, "XObject", xres); + pdf_drop_obj (ctx, xres); pdf_drop_obj (ctx, ref); fz_buffer* buf= fz_new_buffer (ctx, 0); @@ -497,6 +506,7 @@ mupdf_renderer_rep::register_pattern (brush br, SI pixel) { pout->op_Do_image (ctx, pout, "pattern-image", NULL); pout->op_Q (ctx, pout); pdf_close_processor (ctx, pout); + pdf_drop_processor (ctx, pout); } pdf_obj* contents= pdf_add_stream (ctx, doc, buf, NULL /* dict */, 0 /* compress */); @@ -513,6 +523,7 @@ mupdf_renderer_rep::register_pattern (brush br, SI pixel) { // const float matrix[]= { scale_x, 0, 0, scale_y, (float) sx, (float) sy }; pdf_pattern* pat= fz_malloc_struct (ctx, pdf_pattern); + FZ_INIT_STORABLE (pat, 0, mupdf_pattern_drop_imp); pat->document = doc; pat->id = 0; // pdf_to_num (ctx, dict); pat->ismask= 0; // pdf_dict_get_int(ctx, dict, PDF_NAME(PaintType)) == 2; @@ -536,7 +547,6 @@ mupdf_renderer_rep::register_pattern (brush br, SI pixel) { // debug_convert << " " << w << ", " << h << LF; mupdf_pattern p_pdf (pat); - pdf_drop_pattern (ctx, pat); pattern_pool (p)= p_pdf; } } diff --git a/tests/Plugins/MuPDF/mupdf_renderer_test.cpp b/tests/Plugins/MuPDF/mupdf_renderer_test.cpp new file mode 100644 index 0000000000..ae92b492fe --- /dev/null +++ b/tests/Plugins/MuPDF/mupdf_renderer_test.cpp @@ -0,0 +1,103 @@ + +/****************************************************************************** + * MODULE : mupdf_renderer_test.cpp + * DESCRIPTION: Memory leak regression test for mupdf_renderer::register_pattern + * COPYRIGHT : (C) 2026 Darcy Shen + ******************************************************************************/ + +#include "mupdf_renderer.hpp" +#include "base.hpp" +#include "tm_memory.hpp" +#include +#include +#include + +extern void del_obj_mupdf_renderer (void); + +class test_mupdf_renderer_rep : public mupdf_renderer_rep { +public: + void test_register_pattern (brush br, SI pixel) { + register_pattern (br, pixel); + } +}; + +class TestMupdfRenderer : public QObject { + Q_OBJECT + +private slots: + void init () { init_lolly (); } + void test_register_pattern_does_not_leak (); +}; + +void +TestMupdfRenderer::test_register_pattern_does_not_leak () { +#ifndef __linux__ + QSKIP ("RSS-based leak test is Linux-only"); +#endif + + // Create a minimal temporary image to use as pattern + QString temp_path= QDir::temp ().filePath ("mupdf_test_pattern.png"); + QImage img (10, 10, QImage::Format_RGB32); + img.fill (Qt::white); + QVERIFY (img.save (temp_path)); + + QByteArray path_bytes= temp_path.toUtf8 (); + string path_str = string (path_bytes.constData ()); + url test_url = url_system (path_str); + + qDebug () << "temp_path:" << temp_path; + c_string _path_str (path_str); + qDebug () << "path_str:" << QString::fromUtf8 ((char*) _path_str); + c_string _test_url_str (as_string (test_url)); + qDebug () << "test_url as_string:" << QString::fromUtf8 ((char*) _test_url_str); + qDebug () << "is_none:" << is_none (test_url); + qDebug () << "is_atomic test_url:" << is_atomic (test_url); + + // Prepare renderer with a dummy pixmap + test_mupdf_renderer_rep* ren= tm_new (); + fz_context* ctx= mupdf_context (); + fz_pixmap* pix= fz_new_pixmap (ctx, fz_device_rgb (ctx), 100, 100, NULL, 1); + ren->begin (pix); + + // Build a pattern brush that references the temp image + tree pattern= tree (moebius::PATTERN, as_string (test_url), "10", "10", "white"); + c_string _pattern_str (as_string (pattern)); + qDebug () << "pattern tree:" << QString::fromUtf8 ((char*) _pattern_str); + brush br= brush (pattern); + qDebug () << "brush type:" << br->get_type (); + c_string _brush_url_str (as_string (br->get_pattern_url ())); + qDebug () << "brush pattern url:" << QString::fromUtf8 ((char*) _brush_url_str); + + // Warm up: first call loads image into caches + ren->test_register_pattern (br, PIXEL); + del_obj_mupdf_renderer (); + + long before= get_rss (); + + // Repeatedly register the same pattern and clear caches so that + // register_pattern runs its full allocation path every iteration. + const int iterations= 2000; + for (int i= 0; i < iterations; i++) { + ren->test_register_pattern (br, PIXEL); + del_obj_mupdf_renderer (); + } + + long after= get_rss (); + + // Cleanup + ren->end (); + fz_drop_pixmap (ctx, pix); + tm_delete (ren); + + long delta_kb= after - before; + // Allow some RSS noise; a real leak of ~3-5 KB per iteration would + // produce tens of MB after 2000 iterations, so 5 MB is a safe ceiling. + QVERIFY2 (delta_kb < 5120, + qPrintable (QString ("RSS grew by %1 KB after %2 iterations, " + "indicating a memory leak") + .arg (delta_kb) + .arg (iterations))); +} + +QTEST_MAIN (TestMupdfRenderer) +#include "mupdf_renderer_test.moc"