Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
68 changes: 68 additions & 0 deletions devel/0178.md
Original file line number Diff line number Diff line change
@@ -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` 的引用计数管理接管生命周期
12 changes: 10 additions & 2 deletions src/Plugins/MuPDF/mupdf_picture.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down
12 changes: 11 additions & 1 deletion src/Plugins/MuPDF/mupdf_renderer.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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
******************************************************************************/
Expand Down Expand Up @@ -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);
Expand All @@ -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 */);
Expand All @@ -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;
Expand All @@ -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;
}
}
Expand Down
103 changes: 103 additions & 0 deletions tests/Plugins/MuPDF/mupdf_renderer_test.cpp
Original file line number Diff line number Diff line change
@@ -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 <QDir>
#include <QImage>
#include <QtTest/QtTest>

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<test_mupdf_renderer_rep> ();
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"
Loading