From 4418a2821eed1b4d0914387a51fd2f313b162ed9 Mon Sep 17 00:00:00 2001 From: Da Shen Date: Sun, 31 May 2026 12:18:56 +0800 Subject: [PATCH 1/4] =?UTF-8?q?[0178]=20=E4=BF=AE=E5=A4=8D=20mupdf=20?= =?UTF-8?q?=E6=B8=B2=E6=9F=93=E5=99=A8=E5=86=85=E5=AD=98=E6=B3=84=E6=BC=8F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 4.7 --- devel/0178.md | 31 +++++++++++++++++++++++++++++++ 1 file changed, 31 insertions(+) create mode 100644 devel/0178.md diff --git a/devel/0178.md b/devel/0178.md new file mode 100644 index 0000000000..22cf5eb4a6 --- /dev/null +++ b/devel/0178.md @@ -0,0 +1,31 @@ + +# [0178] 修复 mupdf 渲染器内存泄漏 + +## 1 相关文档 +- [0177](0177.md) - 实现 tm_memory 运行时内存监控 + +## 2 任务描述 +基于 0177 的内存监控能力,采用 TDD 方式查找并修复 mupdf 渲染器中的内存泄漏问题。 + +## 3 内存泄漏定位 +在 `src/Plugins/MuPDF/mupdf_renderer.cpp` 的 `register_pattern` 函数中发现三处内存泄漏: + +1. **buffer processor 泄漏**:`pdf_new_buffer_processor` 创建的 processor 仅被 `pdf_close_processor` 关闭,未被 `pdf_drop_processor` 释放。 +2. **pdf_dict (xres) 泄漏**:`pdf_dict_puts(ctx, subres, "XObject", xres)` 后未调用 `pdf_drop_obj(ctx, xres)` 释放原始引用。 +3. **pdf_pattern 泄漏**:手动创建的 `pdf_pattern` 使用 `fz_malloc_struct` 分配(清零),但未初始化 `storable.drop` 函数指针。`pdf_drop_pattern` 在引用计数降至 0 时因 `drop` 为 NULL 而无法释放 `pat`、`resources` 和 `contents`。 + +## 4 修复方案 +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` 的引用计数管理接管生命周期。 + +## 5 如何测试 + +### 5.1 单元测试 +``` +xmake b mupdf_renderer_test && xmake r mupdf_renderer_test +``` + +### 5.2 运行验证 +在渲染包含 pattern brush 的文档时,使用 `get_rss()` 观察 RSS 是否稳定。 From 1a216d308431a19476bc2b9c3b717c9da89d7ab8 Mon Sep 17 00:00:00 2001 From: Da Shen Date: Sun, 31 May 2026 12:19:44 +0800 Subject: [PATCH 2/4] =?UTF-8?q?[0178]=20=E6=B7=BB=E5=8A=A0=E5=A4=8D?= =?UTF-8?q?=E7=8E=B0=20mupdf=20=E6=B8=B2=E6=9F=93=E5=99=A8=E5=86=85?= =?UTF-8?q?=E5=AD=98=E6=B3=84=E6=BC=8F=E7=9A=84=E5=8D=95=E5=85=83=E6=B5=8B?= =?UTF-8?q?=E8=AF=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 新增 mupdf_renderer_test,通过 RSS 监控检测 register_pattern 的内存泄漏 - 修复 format_picsize_string 在无 GUI 环境下调用 get_current_editor 崩溃的问题, 使测试可在 headless 环境中运行 Co-Authored-By: Claude Opus 4.7 --- src/Plugins/MuPDF/mupdf_picture.cpp | 12 ++- tests/Plugins/MuPDF/mupdf_renderer_test.cpp | 103 ++++++++++++++++++++ 2 files changed, 113 insertions(+), 2 deletions(-) create mode 100644 tests/Plugins/MuPDF/mupdf_renderer_test.cpp 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/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" From c33982a97a680f95bcd8e9534bb9b45ca4c5006d Mon Sep 17 00:00:00 2001 From: Da Shen Date: Sun, 31 May 2026 12:24:39 +0800 Subject: [PATCH 3/4] =?UTF-8?q?[0178]=20=E4=BF=AE=E5=A4=8D=20mupdf=20?= =?UTF-8?q?=E6=B8=B2=E6=9F=93=E5=99=A8=20register=5Fpattern=20=E7=9A=84?= =?UTF-8?q?=E5=86=85=E5=AD=98=E6=B3=84=E6=BC=8F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 1. 在 pdf_close_processor 后添加 pdf_drop_processor,释放 buffer processor 2. 在 pdf_dict_puts 后添加 pdf_drop_obj(ctx, xres),释放 xres 原始引用 3. 定义 mupdf_pattern_drop_imp 作为手动创建 pattern 的释放函数 4. 使用 FZ_INIT_STORABLE 初始化 pdf_pattern 的 storable.drop 5. 移除 register_pattern 中多余的 pdf_drop_pattern 调用, 让 mupdf_pattern 的引用计数管理接管生命周期 Co-Authored-By: Claude Opus 4.7 --- src/Plugins/MuPDF/mupdf_renderer.cpp | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) 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; } } From e98316a30aac6043f23e9f7a78eaa1e9ef9d02c3 Mon Sep 17 00:00:00 2001 From: Da Shen Date: Sun, 31 May 2026 12:42:12 +0800 Subject: [PATCH 4/4] =?UTF-8?q?[0178]=20=E6=9B=B4=E6=96=B0=E4=BB=BB?= =?UTF-8?q?=E5=8A=A1=E6=96=87=E6=A1=A3=EF=BC=8C=E8=A1=A5=E5=85=85=E6=99=AE?= =?UTF-8?q?=E9=80=9A=E7=94=A8=E6=88=B7=E5=8F=AF=E6=84=9F=E7=9F=A5=E7=9A=84?= =?UTF-8?q?=E4=BC=98=E5=8C=96=E8=AF=B4=E6=98=8E?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 按照 dddd.md 模板重新组织文档结构 - 在如何测试部分补充普通用户可验证的场景: 创建带有 pattern brush 的文档并反复导出 PDF, 观察内存占用是否稳定 Co-Authored-By: Claude Opus 4.7 --- devel/0178.md | 71 +++++++++++++++++++++++++++++++++++++++------------ 1 file changed, 54 insertions(+), 17 deletions(-) diff --git a/devel/0178.md b/devel/0178.md index 22cf5eb4a6..c03ba3521c 100644 --- a/devel/0178.md +++ b/devel/0178.md @@ -3,29 +3,66 @@ ## 1 相关文档 - [0177](0177.md) - 实现 tm_memory 运行时内存监控 +- [dddd.md](dddd.md) - 任务文档模板 -## 2 任务描述 -基于 0177 的内存监控能力,采用 TDD 方式查找并修复 mupdf 渲染器中的内存泄漏问题。 +## 2 任务相关的代码文件 +- `src/Plugins/MuPDF/mupdf_renderer.cpp` +- `src/Plugins/MuPDF/mupdf_picture.cpp` +- `tests/Plugins/MuPDF/mupdf_renderer_test.cpp` -## 3 内存泄漏定位 -在 `src/Plugins/MuPDF/mupdf_renderer.cpp` 的 `register_pattern` 函数中发现三处内存泄漏: +## 3 如何测试 -1. **buffer processor 泄漏**:`pdf_new_buffer_processor` 创建的 processor 仅被 `pdf_close_processor` 关闭,未被 `pdf_drop_processor` 释放。 -2. **pdf_dict (xres) 泄漏**:`pdf_dict_puts(ctx, subres, "XObject", xres)` 后未调用 `pdf_drop_obj(ctx, xres)` 释放原始引用。 -3. **pdf_pattern 泄漏**:手动创建的 `pdf_pattern` 使用 `fz_malloc_struct` 分配(清零),但未初始化 `storable.drop` 函数指针。`pdf_drop_pattern` 在引用计数降至 0 时因 `drop` 为 NULL 而无法释放 `pat`、`resources` 和 `contents`。 +### 3.1 确定性测试(单元测试) +``` +xmake b mupdf_renderer_test && xmake r mupdf_renderer_test +``` -## 4 修复方案 -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` 的引用计数管理接管生命周期。 +### 3.2 非确定性测试(用户场景验证) -## 5 如何测试 +#### 普通用户可感知的优化 -### 5.1 单元测试 -``` +本次修复针对的是 **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.2 运行验证 -在渲染包含 pattern brush 的文档时,使用 `get_rss()` 观察 RSS 是否稳定。 +## 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` 的引用计数管理接管生命周期