Skip to content

大量密文标记连续处理时,如何降低内存峰值

本文基于当前项目中的一组可复现测试结果,讨论在大量密文标记连续处理时,如何控制内存峰值。

当一次任务里包含大量矩形区域,系统需要反复完成以下动作:

  1. 在页面上创建密文标记
  2. 执行密文应用
  3. 继续处理下一批标记

在本次测试模型下,如果中间数据全部保留在内存中,进程峰值内存会明显上升;启用文件流后,峰值内存会明显下降,同时会产生临时缓存文件并增加处理耗时。

问题边界

从这次测试结果看,影响内存峰值的关键变量主要是:

  • 一次任务内有多少条密文标记
  • 每条标记之后是否立即执行密文应用
  • 中间处理数据是保留在内存中,还是转写到临时文件

本次验证使用的是一组固定工作负载:

  • 测试文档页数:10 页
  • 密文矩形记录数:729 条
  • 处理方式:每读到一条矩形记录,就执行一次密文标记,并立刻执行一次密文应用

这与“先全部标记、最后统一处理一次”的调用方式不同。本文只讨论这一组连续处理模型下的结果。

相关接口

以 C++ 项目中处理密文功能为例,可以重点关注以下接口:

核心思路

根据本次测试结果,要降低这类连续处理模型下的内存峰值,可以考虑将密文应用过程中的中间数据写入临时文件

在本次测试中,这样做带来的结果是:

  • 峰值内存下降
  • 临时磁盘占用上升
  • 总耗时增加

从结果上看,这是一种用临时磁盘空间换取更低内存峰值的处理方式。

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。本文同时给出了 StartApplyApply 两种调用方式的测试结果。

实测结果

下面展示在同一组测试输入下,开启与关闭文件流时的结果对比。

开启文件流后

场景耗时峰值内存增量RSS 增量临时缓存
StartApply + 文件流27.075s+25.4 MB+26.4 MB2149.90 MB / 2828 个文件
Apply + 文件流33.978s+24.6 MB+25.2 MB2149.90 MB / 2828 个文件

关闭文件流后

场景耗时峰值内存增量RSS 增量临时缓存
StartApply + 不落盘10.725s+2086.7 MB+2088.5 MB0
Apply + 不落盘10.131s+2192.4 MB+2194.1 MB0
  • 在这组工作负载下,开启文件流后,峰值内存增量约为 +24.6 MB+25.4 MB
  • 在同一组工作负载下,不启用文件流时,峰值内存增量约为 +2086.7 MB+2192.4 MB
  • 开启文件流后,测试中记录到的临时缓存规模约为 2149.90 MB,文件数为 2828
  • 在这组测试中,开启文件流后的耗时高于不落盘方式。

从这组结果看,文件流方式的价值主要体现在降低内存峰值;对应的代价是处理耗时增加,并引入额外的临时缓存文件。

什么时候优先考虑这种方式

如果你的业务与本文测试模型接近,可以优先评估文件流方案:

  • 一次任务中需要处理大量密文矩形
  • 需要连续执行多次密文应用
  • 任务运行在服务端,内存预算比磁盘预算更紧张
  • 运行环境对单任务峰值内存更敏感

如果任务规模较小,或者运行环境对磁盘 I/O 更敏感,也可以继续使用纯内存方式。

使用建议

  • 为临时缓存目录预留足够空间,并确保目录可写。
  • 如果要在生产环境启用文件流,建议先评估磁盘吞吐和临时目录清理策略。
  • 如果需要渐进式处理控制,可以使用 StartApply
  • 如果只需要直接完成处理,可以使用 Apply