Air 系列应用交流/AirBench(自研基准模型)· 鸿蒙 PC 适配全记录

AirBench(自研基准模型)· 鸿蒙 PC 适配全记录

【Air 系列 · 鸿蒙 PC 适配全记录】05 / 56

■ 应用:AirBench(AirBench)
■ 定位:设备性能基准测试(参考GB7模型)
■ 上游:自研基准模型 | 路线:纯ArkTS+native | 版本:1.2.0
■ 市场状态:已上架 AGC 应用页: https://appgallery.huawei.com/app/6917616870374369358
■ 官网(免费下载): https://www.shunidc.cn/

————————————————————
开源鸿蒙 PC 社区: https://harmonypc.csdn.net/

PC 社区项目申请: https://atomgit.com/OpenHarmonyPCDeveloper

适配开源地址:(AtomGit 仓库建立后回填)

环境搭建文章:https://blog.csdn.net/weixin_52908342/article/details/161343743

▍一、为什么要适配 AirBench

评估一台鸿蒙 PC 设备的真实性能,目前缺一把好尺子。系统参数页只给型号与内存,第三方评测数据分散在不同口径里,而应用开发者真正关心的单核吞吐、多核扩展、内存带宽与持续频率行为,都需要在设备上现场测量。AirBench 就是为此而生的自研基准应用:包名 com.airbench.app,当前版本 1.1.0,deviceTypes 覆盖 phone、tablet、2in1,module.json5 未声明任何系统权限。

这个工程的特别之处在于它的方法论。团队没有把某个安卓跑分应用搬过来,而是先对 Geekbench 7(XAPK build 703419)做完整逆向:PLXR 资源容器格式、16 项 CPU 负载的算法与物料、计分公式与 K 常数全部反推落盘为规格书(docs/GB7_NATIVE_SPEC.md),然后以 C++ 在鸿蒙 NDK 上重实现这套负载库。PORTING_STATUS.md 同时记载过一条"官方引擎直调链"路线(对闭源引擎做入口推导直调),该路线现已从当前代码树移除,工程收敛为自研负载库这一条计分链路,逆向成果保留为规格与物料。

当前应用由两层构成:ArkTS 原生界面(首页、历史、设置三页,HdsTabs 双形态壳)与 libabnative.so 原生负载库(24 项负载:单核 16 项、多核 8 项)。负载物料 airbench-workload.plxr 随 HAP 的 rawfile 分发,设备侧解包后执行。

▍二、先划清边界:不把编译通过当成适配完成

AirBench 属于自研类工程,边界问题换了一种问法:逆向规格的成果用到哪一层,重实现与原版在哪些地方存在已知差距,哪些能力明确不伪装。下表逐层给出答案,每一条都来自 docs 规格书或代码注释。

层次 原项目实现(或传统桌面方案) 鸿蒙侧当前方案
基准口径 Geekbench 7 官方闭源引擎 逆向规格 + C++ 重实现(libabnative.so),work 定义与 K 常数对齐,实现吞吐存在已知差距
负载物料 geekbench-workload.plxr(1294 个文件) airbench-workload.plxr 随 HAP rawfile 携带,设备侧 plxr.cpp 解包缓存
执行模型 Android 多进程 JNI fork 子进程跑分(沙箱禁 execve),事件 pipe → 父进程泵 → TSFN 回传
界面层 Android View 体系 纯 ArkTS 原生(HdsTabs、GridRow 断点、暗黑资源色)
计分 runtime=mean(4 次计时) × rate × K,几何平均 同公式重实现,K' 按自家 work 定义一次折算
GPU Compute 引擎内非官方口径遗留 当前 UI 未纳入(物料目录保留)
频率口径 无对应概念 簇频率遥测 + 压制判定标红 + 单核段 factor 补偿(clamp 1..1.3)

边界里最重要的一句是口径声明:逆向重实现的负载语义全绿,但绝对分数与官方存在实现吞吐差,可比性以"同设备分项比值"与频率口径标注为前提。应用不为凑数做隐藏补偿,factor 补偿只在检测到频率墙的单核段生效且无墙设备恒等 1.0。

▍三、鸿蒙版的整体架构

应用是标准的 ArkTS 单 Ability 结构,native 侧一个负载库加两个按需 dlopen 的专用库。首页是全应用唯一的计分入口:按钮经 registerRun 钩子驱动 NativeCard 控制器,控制器调用 nbStart 后,负载在 fork 子进程里执行,事件流经 TSFN 回传 ArkTS 渲染;跑分期间遥测自动降载,测量结束释放常亮。

text
Index.ets(HdsTabs 三页壳:首页 / 历史 / 设置;≥960vp 左侧竖栏,窄屏底部悬浮胶囊)
└── HomePane(全 App 唯一"开始基准测试"按钮)
├── NativeCard(无 UI 控制器:registerRun / registerStop 钩子注册)
│ └── libabnative.so(nbStart/nbStop,TSFN JSON 事件流)
│ ├── fork 子进程(崩溃隔离;事件 pipe → 父进程 poll 泵)
│ ├── plxr.cpp(airbench-workload.plxr 解包 → files 缓存)
│ ├── bench 框架(warmup×1 + 计时×4 + K 计分 + 几何平均)
│ ├── 16 个负载实现(wl_*.cpp,注册表分发)
│ ├── Clang 负载 → dlopen libabclang.so(LLVM 子集独立成库)
│ └── PDF 负载 → libpdfium.so
├── FreqCard(簇频率实时曲线 + 压制告警 + 补偿说明)
├── ScoreCard / WlDetailCard(总分与 24 项分项,含单点冲刺分)
└── DeviceCard / CheckCard(设备信息 + 硬件自检九探针)
HistoryPane → HistoryBook(preferences JSON,上限 30 条)


主要目录如下:

text
AirBench/
├── AppScope/app.json5 # 包名 com.airbench.app、版本 1.1.0
├── docs/
│ ├── PORTING_STATUS.md # 逐轮状态、真机分数与部署纪律
│ ├── GB7_NATIVE_SPEC.md # 逆向规格书(容器/负载/计分/工具链)
│ ├── GB7_REF_KIRIN9020.md # 麒麟 9020 分项参考表
│ ├── FREQ_WALL_DEEP_DIVE.md # 频率墙深调报告
│ └── evidence/ # 各轮证据归档
└── entry/src/main/
├── ets/
│ ├── pages/Index.ets # 双形态壳与沉浸式布局
│ ├── components/ # 9 个卡片组件(HomePane 等)
│ ├── core/History.ets # 历史存储与修订号广播
│ ├── sysinfo/SysInfo.ets # 设备快照与实时遥测
│ └── native/libabnative.d.ts # nbStart/nbStop 接口声明
├── cpp/nativebench/ # 负载库源码(nb_main + wl_* + plxr)
├── libs/arm64-v8a/ # libabnative/libabclang/libpdfium 等预编译库
└── resources/rawfile/bench/ # airbench-workload.plxr 物料


▍四、核心适配过程

### 1. 逆向规格:先把尺子的定义搞清楚

docs/GB7_NATIVE_SPEC.md 是全工程的基石。容器侧,PLXR 格式被完整破解:0x14 字节头(data_end 与尾表长度),nfiles 个首尾相接的数据 blob,尾部 24 字节定长条目(flag/id/offset/size),目录清单是 64 字节哈希槽,. 与 .. 以 id 指针构成树。负载侧,16 项 CPU 负载的实现引擎与物料逐项钉死:文件压缩用 zlib、HTML5 用 litehtml、PDF 用 pdfium、照片库串 sqlite3/libjpeg/libjxl/tflite、Clang 是真编译 Lua 5.4 源码、文本处理跑嵌入式 CPython 3.13、光线追踪对应 Cycles XML 场景等。计分侧反推出 score = rate × K 与每项负载的 K 常数表,单核多核双测一致。

### 2. libabnative:设备侧的重实现

entry/src/main/cpp/nativebench/nb_main.cpp 实现 nbStart(assetsDir, optsJson, eventsCb):fork 子进程跑分保证崩溃隔离,父进程 poll 泵转发事件,TSFN 把 JSON 事件(log/progress/done/error)送回 JS 线程。16 个负载各自成文件(wl_file_compression.cpp 到 wl_video_player.cpp),静态链 zlib/zstd/opus/cmark/sqlite3/jpeg-turbo/litehtml/jolt/aom/tflite/jxl;Clang 负载因 LLVM 子集体积独立为 libabclang.so 按需 dlopen;PDF 渲染复用 libpdfium.so。停止请求经 nbStop 写跨 fork 共享标志,在负载边界与迭代间生效。字体资源以内嵌数组随库携带:hmos_sans_ttf.h 把 HarmonyOS Sans TTF 编译为 C 数组,HTML5 负载的 litehtml 文档容器经 FreeType 的 FT_New_Memory_Face 直接从内存载入,渲染不依赖系统字体目录。

### 3. 执行通道:fork、QoS 与混合路由

沙箱 MAC 策略拒绝应用数据文件的 execve,执行模型从设计上就是 fork 而非 exec。频率遥测发现 QoS 提频对部分负载有效后,工程做了混合路由:kQosRouteIds = {101,102,103,104}(文件压缩、导航、HTML5、PDF 这四个计算受限项)走主进程 inproc+QoS 通道,其余保持 fork(新子进程的 dfc 记账最宽松),且 fork 批次先跑,保证 QoS 线程不污染测量子进程。这条路由是在 Pura X 上逐负载实测的结论:四项增益 5%-15%,而内存受限项走 QoS 会损失 15%-44%。

### 4. 遥测与自检:跑分应用的仪表盘

SysInfo.ets 维护设备快照(营销名表过滤 /proc/cpuinfo 的垃圾 Hardware 文本、GPU 表补全 Maleoon 系列)与实时采样(每 2 秒轻采样,每 10 拍带一次 GPU 内存/温度/电量重探针);FreqCard 展示按簇分组的频率曲线与峰值柱状图,检测到峰值低于标称 90% 时分数标红并注明受限;HardwareCheck.ets 跑九项自检(CPU/RAM 可见性、物料落盘空间、GPU 内存遥测、引擎导出矩阵、物料清单完整性、电池/温控/性能模式),全部降级为警告不阻断跑分。跑分期间首页遥测降为半频轻采样,避免 16 次 cpufreq 读取与 32 温度区扫描抢主线程。

### 5. PC 形态与数据留存

壳层按 2in1/PC 适配:窗口宽度大于等于 960vp 时切换左侧 96vp 竖向侧栏,窄屏用底部悬浮胶囊(barOverlap 沉浸 + 系统材质探测降级),free 窗口拖宽即时切换;暗黑模式全量资源色(dark/ 变体),跑分中 setWindowKeepScreenOn 防挂起。历史经 HistoryBook 落 preferences JSON,最新在前上限 30 条,含功耗墙档位(cap/capMc)字段,修订号经 AppStorage 广播刷新。

▍五、适配中遇到的几个难点

### 难点一:沙箱禁 execve 与跑分进程的崩溃隔离

基准负载本质是不可信的重计算代码,官方引擎用多进程隔离。鸿蒙沙箱直接拒绝 execve 应用数据文件,工程改为 fork:子进程不执行新文件,复制父进程映像后直接进入负载入口,崩溃由 waitpid 回收,主进程与已出分数据不受连坐。事件经 pipe 回传,诊断面再补一条纪律——设备上 fork 子进程的 stderr 不可读,落盘诊断由 want 参数通道下发(nativeFilter/nativeClangOpt 等经 EntryAbility 写入)。

### 难点二:频率墙的发现与承认

Mate 80 Pro Max 上分数比参考低 30%,三轮复测排除温度与电源模式后,深调报告(docs/FREQ_WALL_DEEP_DIVE.md)给出结论:墙的 owner 是内核私有 dfc_cpu_driver,prime 簇硬件频率表含 2470-2750 四档但对三方负载最高只选 2340 档,scaling_governor/min/max_freq 节点在该驱动下不存在,shell 域满载同样被压。工程穷尽了应用侧手段(QoS、perfHint、120Hz 真渲染、触控注入、温度仿真、行为模拟共 20 项闭环)全部无效后,选择把结论写进产品:FreqCard 展示逐簇频率与压制告警,历史记录功耗墙档位,单核段按 clamp(标称/持续中位频率, 1, 1.3) 补偿,无墙设备恒等 1.0。

### 难点三:inproc 模式的堆毒化

为绕开 fork 子进程 QoS EPERM 曾引入进程内模式,随后出现"跑完出分应用必死":三轮取证定位为引擎在每轮跑分中毒化主线程 jemalloc tcache,UI 刷新触发 skia 段析构时解引用脏指针 SIGSEGV,触发点与轮次无关(堆布局轮盘赌)。musl RW 指针去毒修不了堆与 TLS 层,工程恢复 fork 为默认,保留纵深防御:saveHistory 提前到 digest 后立即执行、启动时 recoverOrphanResult 从 result 文件兜底恢复、跑分中遥测降载。这套"实验开关 + 默认安全路径"的组合后来复用到了负载库自身(AIRBENCH_NATIVE_INPROC 保留为实验通道)。

### 难点四:Clang 负载的保真度

Clang 负载第一版分数虚高约 16 倍。逐条对照官方引擎行为后修正了三个语义缺陷:ljumptab.h 被当成独立编译单元(实际只该作 include 路径);-fsyntax-only 每行成本比真代码生成低约 60 倍(引擎串实证原版是 -emit-obj 真出目标码);多核记账声称 4 遍实跑 1 遍。终版实现 7820 行确定性前缀切片、-emit-obj 写 /dev/null、显式 ohos triple、多核单队列打散、单 TU 失败即报错。顺带踩掉静态 LLVM 后端不注册的坑(AArch64 注册是显式函数,需 --whole-archive 归档)。

### 难点五:部署假成功与同版本换库

性能调优期反复出现"安装成功但设备跑旧库":同版本号覆盖安装时,换掉 .so 后不卸载重装不生效。工程把部署纪律固化为:重要轮次 uninstall 全卸重装,验证以行为签名锚点(新日志行存在)为准而非安装返回码。同类基建坑还包括 power-shell timeout 的单位是毫秒、devecocli ui layout 输出可能是多段 JSON——这些最终都写进了 PORTING_STATUS 的工具坑清单。

▍六、编译、安装与启动

Native 负载库按规格书记载的工具链交叉编译:aarch64-unknown-linux-ohos-clang++ 配 NDK sysroot,第三方依赖一律静态 .a 链入 libabnative.so(Clang 负载单独构建 libabclang.so)。当前树内 cpp/nativebench/ 保留负载库源码,hvigor 不参与 native 编译,预编译库直接放入 entry/libs/arm64-v8a/(libabnative、libabclang、libpdfium、libqos、libc++_shared)随 HAP 携带。

ArkTS 侧标准 hvigor 构建:

bash
export JAVA_HOME=/Applications/DevEco-Studio.app/Contents/jbr/Contents/Home
export DEVECO_SDK_HOME=/Applications/DevEco-Studio.app/Contents/sdk

hvigorw --mode module -p module=entry assembleHap --no-daemon


签名产物位于 entry/build/default/outputs/default/entry-default-signed.hap。更换 native 库后的验证轮按部署纪律执行 uninstall 重装。安装与启动:

bash
hdc install -r entry/build/default/outputs/default/entry-default-signed.hap
hdc shell aa start -a EntryAbility -b com.airbench.app


▍七、当前功能边界

代码与 docs 记录支持的已覆盖能力:

- 24 项负载(单核 16 + 多核 8)一轮约 7 分钟,warmup×1 + 计时×4 + K 计分 + 几何平均,事件流进度与逐项分项(含单点冲刺分,WlTypes.ets 提供中文名映射);
- fork 隔离执行 + 混合通道路由(四个计算受限负载走 inproc+QoS,其余 fork),跨 fork 停止协议;
- 簇频率遥测、压制判定标红、单核段 factor 补偿(无墙设备恒等 1.0);
- 硬件自检九探针、设备信息营销名与 GPU 表、跑分期间遥测降载与屏幕常亮;
- 历史 30 条 preferences 留存(含功耗墙档位),暗黑资源色与 2in1/窄屏双形态布局;
- 真机实测记录(PORTING_STATUS.md):Mate60Pro 冷机轮 1101/2871 对齐参考 1064/2815;Pura X SC 1273 超 Android 移植线 1236;Mate80PM 补偿后终版 1520/4922。

尚未覆盖或规划中的能力(均有文档依据):

- GPU Compute 未纳入 UI(物料目录保留,规格书注明其属 GB6 遗留非官方口径);
- AI 基准无对应实现(规格盘点结论:原引擎 native 层无符号,无逻辑可移植);
- 官方引擎直调链已从代码树移除,仅在 docs 留档;
- Clang 负载与官方的工具链差距(clang17 对 clang22、musl 单锁 malloc 争用)作为已知口径注明;
- 逆向重实现与官方绝对分的实现吞吐差整体存在,当前以"同设备分项比值 + 频率口径标注"为可比性前提。

因此,当前版本更准确的定位是"鸿蒙原生自研基准第一版":口径透明、过程可复现、结论可解释,它给出的每个分数都附带频率与受限状态,不为好看而隐藏测量条件。

▍八、总结

AirBench 的价值不在分数本身,而在它把"测量"这件事做成了可审计的:负载定义、计分常数、频率环境、补偿规则全部公开在 docs 里,任何一个分数都能沿着事件流与遥测曲线回溯到测量条件。逆向规格先行、重实现随后、真机口径校准收尾的三段式,也让它区别于直接搬运引擎的移植——这条路径产出的规格书与负载库,对其他需要性能基线的鸿蒙工程是直接可复用的。

后续工作的重心在 PORTING_STATUS 的待办里:轮 6 引入的 factor 补偿还需在无墙设备上回归验证恒等 1.0,preferences 落盘待验证,GPU Compute 的 UI 取舍与更多机型的冷机基线仍在清单上。对一个基准工具而言,这条路线没有终点——设备在变,固件的调度策略也在变,尺子必须跟着每次测量重新校准自己。
楼主 8 天前
想参与讨论?登录 或 30 秒注册。