大量密文标记连续处理时,如何降低内存峰值
本文基于当前项目中的一组可复现测试结果,讨论在大量密文标记连续处理时,如何控制内存峰值。
当一次任务里包含大量矩形区域,系统需要反复完成以下动作:
- 在页面上创建密文标记
- 执行密文应用
- 继续处理下一批标记
在本次测试模型下,如果中间数据全部保留在内存中,进程峰值内存会明显上升;启用文件流后,峰值内存会明显下降,同时会产生临时缓存文件并增加处理耗时。
问题边界
从这次测试结果看,影响内存峰值的关键变量主要是:
- 一次任务内有多少条密文标记
- 每条标记之后是否立即执行密文应用
- 中间处理数据是保留在内存中,还是转写到临时文件
本次验证使用的是一组固定工作负载:
- 测试文档页数:10 页
- 密文矩形记录数:729 条
- 处理方式:每读到一条矩形记录,就执行一次密文标记,并立刻执行一次密文应用
这与“先全部标记、最后统一处理一次”的调用方式不同。本文只讨论这一组连续处理模型下的结果。
相关接口
以 C++ 项目中处理密文功能为例,可以重点关注以下接口:
foxit::addon::Redaction:密文功能类MarkRedactAnnot:用于为指定区域创建密文标记。Apply:适合一次性完成密文应用。StartApply:会返回渐进式处理对象,适合需要进度控制或长任务处理的场景。
核心思路
根据本次测试结果,要降低这类连续处理模型下的内存峰值,可以考虑将密文应用过程中的中间数据写入临时文件。
在本次测试中,这样做带来的结果是:
- 峰值内存下降
- 临时磁盘占用上升
- 总耗时增加
从结果上看,这是一种用临时磁盘空间换取更低内存峰值的处理方式。
C++ 示例
下面示例展示了典型做法:
cpp
#include "fsdk.h"
#include "addon/fs_redaction.h"
using namespace foxit;
using namespace foxit::addon;
using namespace foxit::common;
using namespace foxit::pdf;
class MyApplyRedactionCallback : public ApplyRedactionCallback {
public:
void Release() override {
delete this;
}
bool NeedToGenerateStreamFile() override {
return true;
}
};
void ApplyRedactionWithFileStream(PDFDoc& doc,
const std::vector<std::array<float, 5>>& rect_data) {
Redaction redaction(doc);
redaction.EnableFileStream(L"D:/tmp/redaction_cache",
new MyApplyRedactionCallback());
for (const auto& item : rect_data) {
int page_index = static_cast<int>(item[0]);
PDFPage page = doc.GetPage(page_index);
page.StartParse(PDFPage::e_ParsePageNormal, nullptr, false);
RectFArray rects;
rects.Add(RectF(item[1], item[2], item[3], item[4]));
redaction.MarkRedactAnnot(page, rects);
Progressive progressive = redaction.StartApply();
while (progressive.GetRateOfProgress() < 100) {
if (progressive.Continue() == Progressive::e_Error) {
throw std::runtime_error("StartApply failed.");
}
}
}
doc.SaveAs(L"redaction_result.pdf", PDFDoc::e_SaveFlagNoOriginal);
}
如果任务不需要渐进式控制,也可以在每次标记后直接调用 Apply。本文同时给出了 StartApply 和 Apply 两种调用方式的测试结果。
实测结果
下面展示在同一组测试输入下,开启与关闭文件流时的结果对比。
开启文件流后
| 场景 | 耗时 | 峰值内存增量 | RSS 增量 | 临时缓存 |
|---|---|---|---|---|
StartApply + 文件流 | 27.075s | +25.4 MB | +26.4 MB | 2149.90 MB / 2828 个文件 |
Apply + 文件流 | 33.978s | +24.6 MB | +25.2 MB | 2149.90 MB / 2828 个文件 |
关闭文件流后
| 场景 | 耗时 | 峰值内存增量 | RSS 增量 | 临时缓存 |
|---|---|---|---|---|
StartApply + 不落盘 | 10.725s | +2086.7 MB | +2088.5 MB | 0 |
Apply + 不落盘 | 10.131s | +2192.4 MB | +2194.1 MB | 0 |
- 在这组工作负载下,开启文件流后,峰值内存增量约为
+24.6 MB到+25.4 MB。 - 在同一组工作负载下,不启用文件流时,峰值内存增量约为
+2086.7 MB到+2192.4 MB。 - 开启文件流后,测试中记录到的临时缓存规模约为
2149.90 MB,文件数为2828。 - 在这组测试中,开启文件流后的耗时高于不落盘方式。
从这组结果看,文件流方式的价值主要体现在降低内存峰值;对应的代价是处理耗时增加,并引入额外的临时缓存文件。
什么时候优先考虑这种方式
如果你的业务与本文测试模型接近,可以优先评估文件流方案:
- 一次任务中需要处理大量密文矩形
- 需要连续执行多次密文应用
- 任务运行在服务端,内存预算比磁盘预算更紧张
- 运行环境对单任务峰值内存更敏感
如果任务规模较小,或者运行环境对磁盘 I/O 更敏感,也可以继续使用纯内存方式。
使用建议
- 为临时缓存目录预留足够空间,并确保目录可写。
- 如果要在生产环境启用文件流,建议先评估磁盘吞吐和临时目录清理策略。
- 如果需要渐进式处理控制,可以使用
StartApply。 - 如果只需要直接完成处理,可以使用
Apply。