MemStorageBench 在鸿蒙上测量内存带宽与存储吞吐
MemStorageBench 把内存和存储测试拆成两项独立负载:内存带宽测量固定复制工作量产生的数据流量,顺序存储测量固定文件在连续读写中的吞吐。界面显示速率、耗时和校验状态,每个结果都保留对应的工作量、单位与测量方法。
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 秒并重开 → 读取 → 重开 → 写入 → 校验与清理
块宽按请求数据量画;数据量是应用的 I/O 请求,闪存的实际写入量还包含控制器写放大。
文字版
| 阶段 | 说明 | 所属区间 |
|---|---|---|
| 预热写 A | 1 × 1.5 GiB;不计入最终速率 | 主工作量 10.5 GiB |
| 读取 A | 3 × 1.5 GiB;三轮整数速率取均值 | 主工作量 10.5 GiB |
| 写入 B / C / D | 3 × 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.06 | 40,541.89 | MiB/s |
| 逻辑复制吞吐 | 20,255.03 | 20,270.94 | MiB/s |
| 顺序读取 | 3,295.00 | 3,153.00 | MiB/s |
| 顺序写入,含同步 | 3,198.00 | 3,249.33 | MiB/s |
MemStorageBench · 两组独立运行结果
SGU-AL10 · 2026-10-17 · 两图坐标尺度不同
同一天的两次独立运行,工作量相同;内存两次相差不到 0.1%,存储读取相差 4.3%、写入相差 1.6%。
数据表
| 面板 | 类别 | 记录 A / MiB/s | 记录 B / MiB/s |
|---|---|---|---|
| 内存 · 同一复制负载的两种口径 | 读写流量 | 40,510.06 | 40,541.89 |
| 内存 · 同一复制负载的两种口径 | 逻辑复制吞吐 | 20,255.03 | 20,270.94 |
| 存储 · 固定 1.5 GiB 文件 | 顺序读取 | 3,295.00 | 3,153.00 |
| 存储 · 固定 1.5 GiB 文件 | 顺序写入(含同步) | 3,198.00 | 3,249.33 |
内存与存储使用不同量级的坐标轴。内存的两个指标描述同一项复制负载;存储读写描述固定文件流程。图表只比较同一行指标。
记录 A 的存储逐轮数据如下:
| 轮次 | 读取耗时 ms | 读取 MiB/s | 写入模式 | 写入耗时 ms | 写入 MiB/s |
|---|---|---|---|---|---|
| 1 | 465.926 | 3296 | B | 474.786 | 3235 |
| 2 | 465.482 | 3299 | C | 476.521 | 3223 |
| 3 | 466.765 | 3290 | D | 489.721 | 3136 |
记录 A · 三轮读写吞吐与耗时
SGU-AL10 · 2026-10-17
每轮 24 × 64 MiB;写入计时包含 fdatasync。
数据表
| 面板 | 轮次 | 读取 A / MiB/s | 写入 B/C/D / MiB/s |
|---|---|---|---|
| 逐轮整数吞吐 | 第 1 轮 | 3296 | 3235 |
| 逐轮整数吞吐 | 第 2 轮 | 3299 | 3223 |
| 逐轮整数吞吐 | 第 3 轮 | 3290 | 3136 |
| 对应连续计时区间 | 第 1 轮 | 465.9 | 474.8 |
| 对应连续计时区间 | 第 2 轮 | 465.5 | 476.5 |
| 对应连续计时区间 | 第 3 轮 | 466.8 | 489.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 流程。它只报告这两类速率,不生成综合分数,数值也不等同于原平台上的官方成绩。工作量、计时和完成条件一起保存,一个速率才能被正确解释和重复检查。
评论
欢迎讨论,不用登录。评论会直接显示;骂人和广告会被删掉。
管理
评论
加载中……