中文 · English
Android 移动端存储性能基准工具:参考 FIO 与 Iometer 的测试模型独立实现, 内置上游源码裁剪的算力/内存层次基准(CoreMark / STREAM / tinymembench), 并支持用户自备组件包(BYOB)做二进制级测试与判分。
本项目为独立实现的测量工具,与 JEDEC / SPEC / EEMBC / Google 等组织无隶属、赞助或背书关系; 采用 Apache-2.0 许可(见 LICENSE),第三方组件版权与许可见 NOTICE 与 THIRD_PARTY_LICENSES.md。
在 Android 移动平台上,随着 UFS 3.1 与 UFS 4.0 闪存的普及,连续读取带宽已突破 4000 MB/s,随机 IOPS 超越 300K。然而,现有的移动端测试工具(如 AndroBench 等)往往存在以下痛点:
- 测试引擎老旧:未能适配高版本 Linux 内核与 Android 10+ 分区存储架构;
- 缺乏多维度存取规范:无法自定义像 Iometer 那样的多队列、读写混合百分比、随机度等复杂业务模型;
- RAM 缓存污染(PageCache 干扰):未强制使用
O_DIRECT,测得的是运行内存速度而非真正的闪存裸速。
本项目结合了存储基准测试领域的两大行业标杆:
- Iometer 模型参考:移植其 Access Specifications(存取规范模型:Transfer Request Size、Percent Random/Sequential、Percent Read/Write、Queue Depth、Workers Concurrency),并在 C++ 层实现了基于
O_DIRECT的内置原生压测核心(Dynamo Native Engine)。 - FIO 调度引擎:利用行业标准 Flexible I/O Tester (FIO),通过交叉编译打包为 Android 可执行库,提供流式实时 JSON 输出与专业存储压测能力。
┌────────────────────────────────────────────────────────┐
│ Android UI (Jetpack Compose) │
│ - DashboardScreen: 实时仪表盘 (MB/s, IOPS, 延迟) │
│ - SpecificationScreen: Iometer 存取规范多维定制面板 │
│ - ReportScreen: 历史测试汇总与对比 │
└──────────────────────────┬─────────────────────────────┘
│ (Coroutines / StateFlow)
┌──────────────────────────▼─────────────────────────────┐
│ BenchmarkViewModel │
│ - 状态机管理 (IDLE -> PREPARING -> RUNNING -> DONE) │
│ - 自动 drop_caches (Root 环境) │
└─────────────┬────────────────────────────┬─────────────┘
│ │
▼ ▼
┌───────────────────────────┐ ┌──────────────────────────┐
│ Dynamo Native Engine │ │ FIO Kernel Engine │
│ - C++17 纯原生 (JNI) │ │ - 交叉编译 libfio.so │
│ - O_DIRECT 绕过 PageCache│ │ - POSIX AIO 多队列调度 │
│ - 4KB 内存对齐寻道 │ │ - 流式 JSON 状态解析 │
└───────────────────────────┘ └──────────────────────────┘
│ │
└─────────────┬──────────────┘
▼
┌──────────────────────────┐
│ Android Flash (UFS 4/3) │
│ - F2FS / EXT4 原生分区 │
└──────────────────────────┘
- 4 KB:极小文件随机读写,直接决定系统流畅度与应用冷启动响应。
- 128 KB:日常音视频流缓冲与典型文件读写。
- 1 MB / 2 MB:超大文件连续吞吐,反映 UFS 多通道硬件极限带宽。
Q1T1:单线程单队列,真实单任务绝对延迟体现。Q8T1 / Q8T4:中重度后台并发负载。Q32T16:重度多线程与深度队列,跑满 UFS 4.0 主控多通道并发极限。
- SEQ (顺序读写):峰值吞吐量。
- RND (随机寻道):IOPS 压测。
- MIX (读写混合):支持 0% ~ 100% 自由调节(内置经典 70% 读 / 30% 写 Workstation 业务模型)。
本工程为标准 Android Gradle 工程,在 Android Studio 中打开或通过命令行编译:
# Linux / macOS / WSL
./gradlew assembleDebug
# Windows
gradlew.bat assembleDebugAPK 产物位于 app/build/outputs/apk/debug/app-debug.apk。
如果需要启用 FIO 引擎,运行提供的编译脚本即可自动下载源码并通过 Android NDK 编译:
export ANDROID_NDK_HOME=/path/to/android-ndk
bash tools/build_fio_arm64.sh产物将自动放置在 app/src/main/jniLibs/arm64-v8a/libfio.so,安装时系统将自动赋予执行权限。
- 直接 I/O 绕过缓存:
所有读写句柄均开启
O_DIRECT标志,强制所有读写 I/O 从闪存主控物理芯片经过,杜绝了 RAM 缓存导致的几万 MB/s 虚假成绩。 - 规避 FUSE 损耗:
自动识别并首选内部私有存储挂载点(直接与底层的
f2fs或ext4交互),避免 Android 10+ 分区存储通过/sdcard(FUSE 用户态中转层) 带来的 15%~30% 性能衰减。 - 空间安全预分配: 在正式压测计时前,先以块对齐方式连续写入预热数据(填充防透明压缩的伪随机字节),避免测试过程中的元数据分配导致延迟抖动。
./build.sh --max-workers=4 assembleStandardRelease assembleLabRelease
| flavor | applicationId | 说明 |
|---|---|---|
| standard | com.ufs.bench | 常规版(FEATURE_ROOT_LAB=false) |
| lab | com.ufs.bench.lab | 实验室版:BYOB 二进制测试、专业模式等(FEATURE_ROOT_LAB=true) |
两者 applicationId 不同、签名相同,可同时安装。minSdk 26 / targetSdk 34 → 用 APK Signature Scheme v2 签名即可(v1 非必需)。
口令放在 keystore.properties(不入库,见 .gitignore):
storeFile=keystore/ufs-bench.jks
keyAlias=ufsbench
证书:CN=UFS Benchmark, OU=Storage Lab, O=UFSBench, L=Shenzhen, ST=Guangdong, C=CN
SHA-256:e79e1ba007e3978de51d71b9745ce940bbcd84c3a463fd8a89dcd5cc9b0acf44
换签名会导致已安装的旧包无法覆盖安装(INSTALL_FAILED_UPDATE_INCOMPATIBLE,需先卸载、会丢数据)——
真机上已出现过一次(一加 15 上旧的 standard 包)。
dist/UFSBenchmark-<版本>-standard.apk
dist/UFSBenchmark-<版本>-lab.apk
dist/SHA256SUMS.txt # sha256sum -c 校验
dist/README.md
app/src/main/jniLibs/<abi>/libbyob_run.so 是签入版本库的预编译可执行文件(Android 只允许从
lib 目录执行原生二进制),源码在本仓库(tools/sandbox_test/byob_run.cpp +
app/src/main/cpp/sandboxed_runner.cpp)。规矩:
tools/sandbox_test/build_byob_helper.sh # 从源码构建并与签入产物比对
tools/sandbox_test/build_byob_helper.sh --install # 就地更新(之后必须重出 App 并在真机复验)
本轮就靠它抓到一次真实漂移:arm64 助手与源码一致,x86_64 那份是旧的(早期改源码未同步), 已就地更新为与源码一致。注意 AGP 打包时会 strip 原生库,所以 APK/设备里的助手字节数小于 jniLibs 里的(实测 935 KB → 139 KB),这属正常,不能用"字节相等"判定漂移, 要比对的是 jniLibs 里的文件本身。
- 源图:tools/icon/source/lab.png(lab 版)、tools/icon/source/standard.jpg(standard 版),均 1280×1280;
- 重新生成:tools/icon/make_icons.sh(用 .tools/jdk-17 的 ImageIO,无需第三方图像库);
- 每个 flavor 产出 mipmap-/ic_launcher{,_round}.png、drawable-/ic_launcher_foreground.png、 mipmap-anydpi-v26/ic_launcher{,_round}.xml、values/ic_launcher_background.xml;
- 两个坑(已记档):
- 圆形遮罩必须用 setClip(Ellipse) + drawImage —— AlphaComposite.SrcIn + fill 实测不生效;
- release APK 会缩短资源文件名,核验图标要看 aapt2 dump resources 并比对两包 PNG 体积,不能按名找。
终端(实验室版)可运行用户自备的 arm64 可执行文件,两种投喂方式。
把 arm64 可执行文件(自己写的微基准、或从别处拿到的单文件程序)放进手机,用
「终端 → 导入组件包(.zip)」选中它即可:应用会自动生成最小组件包 —— 无命令行参数、
不做参考输出判分,benchmark_id 取文件名。
| 准入项 | 要求 |
|---|---|
| 架构 | aarch64(ELF e_machine = 0xB7);x86_64 / arm32 一律拒绝 |
| 依赖 | 只能是 bionic 白名单:libc.so libm.so libdl.so liblog.so libz.so libstdc++.so libc++_shared.so libandroid.so libatomic.so |
| 禁止 | glibc 名字:libc.so.6 libm.so.6 libpthread.so.0 libgcc_s.so.1 ld-linux-aarch64.so.1 等 —— 需要 glibc 的请 静态链接后再打包 |
| 权限 | 可执行位;导入流程会自动就位到应用私有目录并做 ELF 准入核验 |
zip 内即根目录,不要再套一层:
runner.json 必需
bin/<可执行文件> 必需,arm64 ELF
data/input/… 可选,输入数据集
data/output/<参考输出> 可选,判分用的参考输出
runner.json 字段:
{
"benchmark_id": "mybench",
"binary_path": "bin/mybench",
"command_args": ["--size", "1024", "data/input/in.dat"],
"stdout_file": "mybench.out",
"oracle_rule": {
"type": "numeric_tolerance",
"reference_file": "data/output/ref.out",
"abs_tolerance": 1e-7,
"rel_tolerance": 1e-4,
"ignore_line_patterns": ["runtime = ", "elapsed"]
},
"execution_profile": {
"affinity_policy": "none",
"cooldown_target_celsius": 0.0,
"max_execution_seconds": 600
}
}
command_args中以data/开头的元素会被替换为包内绝对路径(输入放data/input/时用它);oracle_rule.type = "none"表示不判分,只记录运行结果;ignore_line_patterns是子串匹配:命中的行两侧都不参与比对(耗时/内存地址这类每次运行都不同的行必须排除);stdout_file不要用程序自己会写的文件名 —— 否则运行器不会落盘捕获到的 stdout,判分会拿错文件。
逐行比对:行内按空白切 token;两侧都能解析为数字时用 |a-b| <= abs_tol + rel_tol*|b|,
否则按字符串精确比对;空行两侧都忽略。这是"与参考输出一致"的一致性判定,不代表任何官方成绩。
- 组件包必须是 arm64;x86_64(含带 ARM 翻译的模拟器)上运行会诚实失败,不会给出虚假成绩;
- 程序按裸文件名打开输入文件时(例如
100_100_130_cf_a.of),输入要放包根目录 —— 运行目录就是包根; - 耗时/分配类行不排除,正确实现也会被判"不符";
- 依赖 glibc 的二进制必须先
-static静态链接。
规范全文见 docs/byob-打包规范.md;应用内「终端」输入 spec 会原样打印同一份内容。
本项目与 SPEC® 的关系只有一条:提供通路,既不提供任何 SPEC 内容,也不产出 SPEC 成绩。
常见做法是:在 PC 上用 SPEC 官方工具链(runspec / specperl + 官方 flags)编译出官方二进制,
再用 adb push 推进手机运行,把屏幕上的数字当作"这台手机跑 SPEC 的结果"。
本项目不这样做,也不等价于这样做:
| 维度 | 官方口径(runspec + SPEC 工具链) | 本项目 BYOB 通路 |
|---|---|---|
| 编译器 / 工具链 | SPEC 官方 flags.xml 指定的编译器与选项,分 base / peak 两档 | 用户自备介质 + Android NDK/Clang,单一固定优化等级,不做 base/peak 分档 |
| 输入集 | 官方 test / train / ref 三档,规模与内容由官方给定 | 用户自备(本仓库不附带任何数据集) |
| 运行规则 | 官方 run rules:迭代控制、报告格式、可复现性、结果上报流程 | 无。只做"跑一次 + 与参考输出比对是否一致" |
| 产出指标 | SPECspeed / SPECrate / SPECratio 等官方指标 | 不产出任何 SPEC 指标;只记录墙钟耗时与"输出是否与参考一致" |
| 结果用途 | 可对外公布的官方成绩 | 仅供本机自证与回归对比,不得与任何公布的 SPEC 成绩比较 |
补充说明一个容易被误读的点:组件流水线里使用 -DSPEC 编译,目的是让程序的输出与官方
参考输出一致(否则校验对不上),这属于"输出保真",不意味着获得了官方编译口径或官方成绩。
- 不得把本项目的结果称为 SPEC 成绩、SPEC 分数、SPECratio、SPECspeed、SPECrate;
- 不得用本项目的结果与 spec.org 公布的任何数值做比较、排名或折算;
- 不得把本项目描述为"SPEC 移植版""SPEC for Android",或暗示与 SPEC 存在隶属、赞助、认证关系;
- 本项目的终端判分只有两种含义:输出与参考一致 / 不一致,与分数无关。
本项目不分发 SPEC 的源码、输入集(test/train/ref)、参考输出与官方工具链 —— 它们都是商业许可内容,
仓库 .gitignore 已排除 SPEC2017/。组件包只能由用户用自己合法持有的介质编译生成。
tools/spec2017/ 里的脚本只是"把用户自备源码编成 Android 可执行文件、并与参考输出做一致性比对"的
工具链,不构成对 SPEC 任何内容的再分发。
- 项目自身:Apache License 2.0(见 LICENSE);
- 第三方组件与上游基准的版权/许可集中列示在 NOTICE 与 THIRD_PARTY_LICENSES.md;
- 本仓库与发布 APK 均不含 fio 二进制(GPL-2.0)。如需自行编入,请先读
tools/build_fio_arm64.sh顶部的 GPL-2.0 义务警示:把 fio 打进 APK 对外分发会要求整个 APK 满足 GPL-2.0。 - 本仓库不含 SPEC 的任何源码、输入集与参考输出(商业许可内容,.gitignore 已排除)。 只提供 BYOB 通路:用户用自备介质编译组件包后导入。
| 内置基准 | 上游 | 本项目用法与口径 |
|---|---|---|
| CoreMark(1001) | EEMBC | 上游算法逐字节保留 + Android 移植包装;EEMBC 协议禁止在衍生副本上使用 CoreMark 商标,故结果表述为"基于 EEMBC CoreMark 源码的本地测量",不称官方成绩、不使用认证徽标 |
| STREAM(1002) | John D. McCalpin | 变体内核(bench_alloc / pad 哨兵 / 十进制 MB/s);上游要求修改后不得直称 "STREAM benchmark results",故界面统一写"基于 STREAM 变体内核的持续带宽测量" |
| tinymembench(1003) | Siarhei Siamashka | MIT,裁剪移植;MIT 全文见 THIRD_PARTY_LICENSES.md |
内置基准的比值统一为"相对本机自标定基线",界面同屏展示免责声明,不与任何官方公布分数比较。
UFS® 属 JEDEC;CoreMark® 属 EEMBC;SPEC® / SPEC CPU® 属 SPEC;Android™ 属 Google LLC; CrystalDiskMark 属 Noriyuki Miyazaki。本项目为独立实现,与上述组织无隶属、赞助或背书关系; 不复用任何官方徽标,界面中的相关名称仅作事实性指代。完整声明见 NOTICE 与 App 内「设置 → 关于」。