← All articles

MemStorageBench: measuring memory bandwidth and storage throughput on HarmonyOS

MemStorageBench splits memory and storage testing into two independent workloads: memory bandwidth measures the data traffic produced by a fixed copy workload, and sequential storage measures the throughput of a fixed file under continuous reads and writes. The UI shows rate, elapsed time and validation status, and every result keeps its workload, units and measurement method.

This article has no English version yet; the original Chinese text follows. Chart labels are in Chinese as well.

Contents
  1. 从界面到原生负载
  2. 内存带宽的两种流量口径
  3. 顺序存储的固定工作量
  4. 一轮读写怎样计时
  5. 校验与清理决定结果能否完成
  6. 真机读写结果
  7. 保存原始结果与生成网站图表

MemStorageBench 把内存和存储测试拆成两项独立负载:内存带宽测量固定复制工作量产生的数据流量,顺序存储测量固定文件在连续读写中的吞吐。界面显示速率、耗时和校验状态,每个结果都保留对应的工作量、单位与测量方法。

一个速率由分子和分母共同决定。复制了多少数据、一次复制算一份还是两份流量、写入是否等待同步、读取时是否同时检查每个字节,都会改变数值。我们把这些规则落实在原生执行器里,再由 ArkTS 管理测试、停止和历史记录,让界面上的数字可以追溯到实际执行过程。

从界面到原生负载

应用采用 ArkUI 界面、ArkTS 测试服务和 NAPI 原生模块的分层结构。界面只提交“内存带宽”或“顺序存储”请求;测试服务串行调度任务、匹配运行标识、接收结果并保存历史;原生层负责缓冲区、文件描述符、计时和资源释放。

内存测试复用固定版本原生引擎中的 UID12 复制负载。存储测试则按 UID18/19 的同步工作量,在鸿蒙 SDK 的文件接口上实现读写流程。两者的集成方式不同:前者保留原内存引擎,后者保留明确的文件规模、轮次、数据模式和计时规则,并使用本平台的 I/O 实现。

下面是服务层的精简控制流程。runOne 表示应用自己的异步原生调用与结果保存过程;代码用于说明任务关系,省略了进度订阅和错误对象处理。

async function runSelected(kinds: BenchKind[]): Promise<void> {
  if (state.busy || state.blocked) return;
  state.busy = true;
  stopRequested = false;
  try {
    for (const kind of kinds) {
      if (stopRequested || state.blocked) break;
      await runOne(kind);
    }
  } finally {
    if (!state.blocked) state.busy = false;
  }
}

串行执行使内存测试和文件测试不会同时竞争带宽。busy 阻止重复启动;blocked 表示原生层上一轮的缓冲或文件还没有释放完毕,这时不启动下一轮,即使上一轮的 Promise 已经以异常结束。每项任务还使用独立运行标识,返回结果必须与当前请求匹配,迟到的进度不能覆盖另一轮测试。

内存带宽的两种流量口径

内存负载使用两个 worker,每个 worker 有 256 MiB 源缓冲和 256 MiB 目的缓冲,同时分配总量为 1 GiB。复制阶段的最短运行要求为一秒。应用读取引擎给出的有效速率,不用界面从按下按钮到结束的总时间反算内存带宽。

对一次逻辑上的 N 字节复制,源端需要读取 N 字节,目的端需要写入 N 字节。因此同一负载可以表达为两种速率:

指标计数方式用途
逻辑复制吞吐N / 时间表达每秒完成多少复制数据
读写流量2N / 时间表达源读与目的写的合计流量

两者的二倍关系来自计数定义。这里统计的是复制负载的逻辑流量,不是 DDR 总线上的事务计数。

界面与数据接口统一保存 MiB/s。切换成十进制 MB/s 时,应用只转换显示值,保留同一份原始结果:

function displayRate(mibPerSecond, unit) {
  if (mibPerSecond === null || !Number.isFinite(mibPerSecond)
      || mibPerSecond < 0) return null;
  return unit === 'MB/s' ? mibPerSecond * 1.048576 : mibPerSecond;
}

null 表示没有有效速率,界面显示空缺或状态说明。把失败转换成 0 MB/s 会把执行错误混进性能数据,因此失败和停止使用独立状态。

顺序存储的固定工作量

存储测试使用 1.5 GiB 文件,每轮分成 24 个 64 MiB 块。测试先预分配完整文件,写入 A 模式完成预热,等待至少两秒,随后重新打开文件并读三轮 A;再重新打开文件,依次写入 B、C、D 三种模式。六轮正式测量结束后,额外完整读取最终的 D 文件并检查内容,最后关闭文件、删除本次临时目录。

顺序存储的固定工作量

预分配 1.5 GiB → 预热 → 等待至少 2 秒并重开 → 读取 → 重开 → 写入 → 校验与清理

预热写 A1 × 1.5 GiB不计入最终速率读取 A3 × 1.5 GiB三轮整数速率取均值写入 B / C / D3 × 1.5 GiB含 fdatasync最终 D校验1 × 1.5 GiB全部测量完成后执行主工作量 10.5 GiB计时外校验 1.5 GiB总请求 12 GiB,其中写入合计 6 GiB

块宽按请求数据量画;数据量是应用的 I/O 请求,闪存的实际写入量还包含控制器写放大。

文字版
阶段说明所属区间
预热写 A1 × 1.5 GiB;不计入最终速率主工作量 10.5 GiB
读取 A3 × 1.5 GiB;三轮整数速率取均值主工作量 10.5 GiB
写入 B / C / D3 × 1.5 GiB;含 fdatasync主工作量 10.5 GiB
最终 D 校验1 × 1.5 GiB;全部测量完成后执行计时外校验 1.5 GiB

图中的块按各阶段的请求数据量画,不按时间。预热写入 1.5 GiB,正式读取共 4.5 GiB,正式写入共 4.5 GiB,计时外校验再读取 1.5 GiB。主工作量共 10.5 GiB,加上校验后总请求为 12 GiB,其中写入合计 6 GiB。这些是应用请求的字节数;闪存上的实际写入量还包含控制器的写放大。

文件以 O_DIRECT 打开,读写缓冲按要求对齐,每轮独立分配并释放。预分配或直接 I/O 失败时,本项报错,不缩小文件,也不改走缓存路径。直接 I/O 绕过了页缓存;控制器、文件系统与硬件自身的缓存仍在路径上,因此结果是文件系统层的顺序吞吐。

原流程里的 F2FS pin 请求只在探测到 F2FS 时对测试文件执行,并记录返回值;本文的记录来自未触发这一分支的运行。

一轮读写怎样计时

计时边界包含每块的绝对定位与同步读写。写入轮次还把 fdatasync 包在同一个连续时间区间内,避免只统计数据进入软件缓冲的时间。缓冲分配、写入模式填充、进度通知和最终数据扫描放在这个区间之外。

下面摘出单轮执行的关键结构。fd 必须属于本次测试,buffer 是调用方准备的 64 MiB 对齐缓冲;readWallSeconds 使用 gettimeofday 读取秒值。完整教学示例见 measure_pass.h,它只展示单轮计时,不包含应用的文件所有权、预分配和历史服务。

const double begin = readWallSeconds();
for (unsigned block = 0; block < 24; ++block) {
    if (stop.load()) return Stopped;
    const off_t offset = off_t(block) * blockBytes;
    if (lseek(fd, offset, SEEK_SET) != offset) return Failed;
    const ssize_t done = writing ? write(fd, buffer, blockBytes)
                                 : read(fd, buffer, blockBytes);
    if (done != ssize_t(blockBytes)) return Failed;
}
if (writing && fdatasync(fd) != 0) return Failed;
const double elapsed = readWallSeconds() - begin;

短读、短写和系统调用错误直接使本轮失败。这里没有用循环补齐短传输后继续声称原轮次完成,因为那会改变固定调用序列。取消请求在块边界合作检查,已经进入的同步系统调用和 fdatasync 会等待自身返回。

单轮采用 gettimeofday,与原工作量的计时定义一致;整体任务和预热等待另用单调时钟。计算速率前检查区间有限且大于零。计时器是测量方法的一部分,换用别的时钟就记为另一种方法标识。

每轮先将速率向零截断为整数 MiB/s,再对三轮整数求算术平均:

const roundMiBps = Math.trunc(fileBytes / 1048576 / elapsedSeconds);
const finalMiBps = (round0MiBps + round1MiBps + round2MiBps) / 3;

这与“总字节数除以三轮总时间”是不同的聚合方法。网站图表和历史页面使用保存的每轮值及均值,不在展示时改用另一种算法。配套 rate-math.mjs 提供了带有效性检查的纯计算函数。

校验与清理决定结果能否完成

测量中的每次 I/O 都检查完整返回长度,六轮测量完成后再全量校验最终的 D 文件。这样既确认最终文件的完整内容,又让正式读取的计时不包含逐字节比较。最终 D 校验覆盖最终文件;此前读取的 A 数据和已被覆盖的 B、C 内容,由各轮的完整返回长度检查保证。

结果完成还需要满足字节计数、轮数、资源释放与清理条件。应用同时检查 168 次主工作量块定位、24 次校验定位、七次主缓冲分配和释放、一次校验缓冲分配和释放,以及两次阶段重开。临时文件位于本次独占目录,打开、重开与清理时核对文件身份,避免将其他文件纳入操作。

内存原引擎没有提供可安全打断当前复制阶段的接口,因此点击停止会等待当前内存项完成,并取消后续项目。存储项则在块边界或等待阶段响应停止,清理结束后才允许继续。两个执行器共享同一条原则:运行是否结束,由原生任务与资源状态决定。

真机读写结果

下面两组记录均来自 SGU-AL10,采用本文所述工作量,是同一天的两次独立运行。A 记录保留完整逐轮计时和计数;B 记录来自同日最终速率界面的串行测试。两次运行的内存速率相差不到 0.1%,顺序读取相差 4.3%,顺序写入相差 1.6%。

指标记录 A记录 B单位
内存读写流量40,510.0640,541.89MiB/s
逻辑复制吞吐20,255.0320,270.94MiB/s
顺序读取3,295.003,153.00MiB/s
顺序写入,含同步3,198.003,249.33MiB/s

MemStorageBench · 两组独立运行结果

SGU-AL10 · 2026-10-17 · 两图坐标尺度不同

内存 · 同一复制负载的两种口径010k20k30k40k50kMiB/s读写流量逻辑复制吞吐40,510.0620,255.0340,541.8920,270.94存储 · 固定 1.5 GiB 文件05001k1.5k2k2.5k3k3.5kMiB/s顺序读取顺序写入(含同步)3,295.003,198.003,153.003,249.33

同一天的两次独立运行,工作量相同;内存两次相差不到 0.1%,存储读取相差 4.3%、写入相差 1.6%。

数据表
面板类别记录 A / MiB/s记录 B / MiB/s
内存 · 同一复制负载的两种口径读写流量40,510.0640,541.89
内存 · 同一复制负载的两种口径逻辑复制吞吐20,255.0320,270.94
存储 · 固定 1.5 GiB 文件顺序读取3,295.003,153.00
存储 · 固定 1.5 GiB 文件顺序写入(含同步)3,198.003,249.33

内存与存储使用不同量级的坐标轴。内存的两个指标描述同一项复制负载;存储读写描述固定文件流程。图表只比较同一行指标。

记录 A 的存储逐轮数据如下:

轮次读取耗时 ms读取 MiB/s写入模式写入耗时 ms写入 MiB/s
1465.9263296B474.7863235
2465.4823299C476.5213223
3466.7653290D489.7213136

记录 A · 三轮读写吞吐与耗时

SGU-AL10 · 2026-10-17

逐轮整数吞吐310031503200325033003350第 1 轮第 2 轮第 3 轮轮次对应连续计时区间460.0465.0470.0475.0480.0485.0490.0495.0第 1 轮第 2 轮第 3 轮轮次

每轮 24 × 64 MiB;写入计时包含 fdatasync。

数据表
面板轮次读取 A / MiB/s写入 B/C/D / MiB/s
逐轮整数吞吐第 1 轮32963235
逐轮整数吞吐第 2 轮32993223
逐轮整数吞吐第 3 轮32903136
对应连续计时区间第 1 轮465.9474.8
对应连续计时区间第 2 轮465.5476.5
对应连续计时区间第 3 轮466.8489.7

该次预热等待为 2007.793 ms,最终校验耗时 1139.910 ms,总请求与实际返回均为 12 GiB;完整校验和清理通过。整体任务约 6.679 秒,包含准备、等待、校验及清理;表中的读写速率只按各轮自己的计时区间计算。

停止路径也有独立实测:在预热等待阶段停止后,累计只传输了 1.5 GiB,正式读写轮次均未执行,速率字段保持空值,缓冲与临时文件清理成功。这条记录作为停止状态保存,不加入完整性能结果。

保存原始结果与生成网站图表

历史记录保留原始 JSON,并将方法标识、单位、状态、字节数和校验信息与结果一起保存。应用最多保留 50 条记录;界面使用字段白名单展示速率和测量信息,底层引擎的内部评分不作为指标展示。冷启动读取存档,不重新运行负载。

显示单位改变时,历史原值不变。测试方法发生变化时,记录使用各自的方法标识和参数,不同计时规则的成绩不会混在一起。

配套 chart-data.json 包含两组汇总、A 记录的六个逐轮数据和阶段字节数;数据说明 列出与三张图的对应关系。数值保留原精度,图片只在标签处取小数,网站直接使用 JSON 或 CSV 绘图。流程图的数据量属于方法配置,吞吐图和逐轮图属于设备测量,两类字段分开保存。

MemStorageBench 的结果对应这里定义的内存复制与同步文件 I/O 流程。它只报告这两类速率,不生成综合分数,数值也不等同于原平台上的官方成绩。工作量、计时和完成条件一起保存,一个速率才能被正确解释和重复检查。

Comments

Discussion welcome, no login needed. Comments show up right away; abuse and ads get removed.

Comments

    Loading…