HarmonyOS 7 JsonPulse 适配实录06:Metrics回归验收与资源基线
JsonPulse 到 05 已经不再只是一个“把 simdjson 编进 HarmonyOS”的 Demo。
现在它有:
ArkTS API
Node-API Bridge
TypedArray 输入
同步借用边界
Native Owned Buffer
napi_async_work
Promise
ThreadSafeFunction
进度 / 取消 / Retry
HAR
arm64-v8a / x86_64
符号与 SDK 检查
最后一篇如果再加一个新接口,工程价值反而下降。
所以 06 固定做最终回归:5 个数据集 × 2 个 ABI × 4 轮 = 40 次。
这里有一个很重要的口径:
正确性和资源闭环可以跨 ABI 比较;性能必须按 ABI / 设备环境分别建立基线。
arm64-v8a 真机和 x86_64 模拟器不是同一类 CPU 环境。
因此 06 不会拿:
27.4ms
vs
34.9ms
直接下结论说某个 ABI “快 20%”。
它们只在各自环境里做版本回归。
本轮统一数据:
taskId:
native_accept_20261003_06
datasets:
5
abis:
2
cycles:
4
totalRuns:
40
passed:
40 / 40
parseResultMismatch:
0
errorContractMismatch:
0
promiseSettleMismatch:
0
progressOrderErrors:
0
asyncWorkLeak:
0
tsfnLeak:
0
nativeBufferLeak:
0
unresolvedSymbols:
0
sdkMismatch:
0
workContextsReleased:
40
tsfnFinalized:
16
arm64 medium avg:
27.4ms
arm64 medium p95:
30.8ms
arm64 large avg:
194.2ms
arm64 large p95:
208.6ms
x86_64 medium avg:
34.9ms
x86_64 medium p95:
39.2ms
baselineMemory:
112.4MB
after4Cycles:
113.6MB
memoryDelta:
+1.2MB
maxMemory:
178.2MB
status:
PASS

一、最终回归先固定五个数据集,而不是随便找五个 JSON
五个 Profile:
SMALL_VALID
256KB
1,280 records
MEDIUM_VALID
5MB
24,800 records
LARGE_VALID
48MB
248,000 records
DEEP_NEST
8MB
40,000 records
maxDepth=32
INVALID_UTF8
2MB
expected error
它们故意覆盖不同问题:
小文件:
Bridge 固定成本
中等文件:
常规业务基线
大文件:
Async Work / Buffer / UI 影响
深层 JSON:
递归 / 深度统计
非法 UTF-8:
错误契约
同一组数据固定以后,下一版本才能真正比较。
二、2 个 ABI × 4 轮,把“能加载”和“结果一致”放在一起
Runner 固定:
const abis = [
'arm64-v8a',
'x86_64'
]
const cycles = 4
const totalRuns =
datasets.length *
abis.length *
cycles
最终:
5 × 2 × 4 = 40
两个 ABI 都必须满足:
so 可加载;
Node-API 模块可注册;
API 返回结构一致;
正确输入结果一致;
错误输入 error contract 一致。
三、正确性断言不能只比较 records
如果只看:
records
DEEP_NEST 的很多错误会被漏掉。
JsonPulse 最终比较:
records
fields
maxDepth
errorCode
parseMode
同步 / 异步路径也要一致。
第一段代码解决的是:
不同 ABI、不同执行路径最终都用同一份 ResultConsistencyAssert。
export class ResultConsistencyAssert {
verify(
expected:
ParseSummary,
actual:
ParseSummary
): boolean {
return (
expected.records ===
actual.records &&
expected.fields ===
actual.fields &&
expected.maxDepth ===
actual.maxDepth &&
expected.errorCode ===
actual.errorCode
)
}
}
本轮:
parseResultMismatch=0
四、INVALID_UTF8 的 PASS 不是“解析成功”
这是最终回归里最容易误解的一项。
INVALID_UTF8
PASS
表达的是:
预期:
必须失败
实际:
按约定失败
errorCode:
一致
所以:
errorContractMismatch=0
才是关键。
测试错误路径不能把“失败”本身当成 Test FAIL。
五、Promise 必须每次只 settle 一次
03 已经建立异步 Promise。
04 又增加 Cancel / Retry。
最终 40 次回归里专门统计:
promiseSettleMismatch
正常情况:
SUCCESS
→ resolve once
FAILED
→ reject once
CANCELLED
→ reject / cancelled result once
不允许:
先 reject
后 resolve
本轮:
promiseSettleMismatch=0
六、进度回调只在需要 TSFN 的 Profile 里跑
不是每次小文件测试都需要:
5%
10%
15%
...
TSFN 回归集中在:
LARGE_VALID
DEEP_NEST
因此 2 个 Progress Profile × 2 ABI × 4 轮:
16 次 TSFN 生命周期
最终:
tsfnFinalized=16
每次都必须:
create
call
release
finalize
闭环。
七、Async Work Context 40 次必须全部释放
每个任务一个 Context。
最终:
workContextsReleased=40
asyncWorkLeak=0
nativeBufferLeak=0
这是整个系列最核心的资源断言。
如果:
40 个任务
只释放 39 个 Context
即使 UI 全部显示 PASS,最终 Gate 仍然失败。
八、第二段代码把资源泄漏变成硬断言
export class NativeResourceProbe {
verify(
state:
NativeRuntimeState
): boolean {
return (
state.activeAsyncWork ===
0 &&
state.activeTsfn ===
0 &&
state.activeNativeBuffers ===
0 &&
state.pendingProgressEvents ===
0
)
}
}
最终:
asyncWorkLeak=0
tsfnLeak=0
nativeBufferLeak=0
三项分开记录。
这样出问题时可以直接知道是哪类 Owner 没收口。
九、符号与 SDK 检查也必须进入每次发布回归
05 已经证明:
arm64-v8a
x86_64
都能构建。
06 还会再次检查:
unresolvedSymbols=0
sdkMismatch=0
因为业务代码没变,不代表:
构建机 SDK
HAR 产物
Consumer
永远不会漂移。
Native 库一旦交付成包,构建环境本身就是回归输入。
十、性能基线必须按 ABI / 环境分开
arm64-v8a 真机:
MEDIUM_VALID
avg:
27.4ms
p95:
30.8ms
LARGE_VALID
avg:
194.2ms
p95:
208.6ms
x86_64 模拟器:
MEDIUM_VALID
avg:
34.9ms
p95:
39.2ms
这些数字不用于横向比较。
只用于:
arm64 下一版本
vs
arm64 当前版本
x86_64 下一版本
vs
x86_64 当前版本
否则 CPU、模拟器开销和设备差异会把结论带偏。
十一、内存看四轮稳定基线,不看一次峰值
本轮:
baseline:
112.4MB
after4Cycles:
113.6MB
delta:
+1.2MB
max:
178.2MB
峰值来自大文件:
ArkTS Uint8Array
+
Native Owned Buffer
+
simdjson parser working set
是可以解释的。
更重要的是任务全部释放以后,稳定基线没有每轮阶梯增长。
十二、Native Buffer 的峰值必须和 Owner 数量对得上
LARGE_VALID 在异步路径里:
ArkTS Buffer
Native Copy
64B Padding
Parser
会比同步零复制路径占更多瞬时内存。
这不是 03 的 Bug。
是异步生命周期隔离换来的资源成本。
最终只要:
nativeBufferLeak=0
并且稳定内存能回落,就属于可控取舍。
十三、Cancel / Retry 也进入回归,但不占所有 40 次
取消逻辑是故障注入。
Runner 会在 LARGE_VALID 里额外做:
61% cancel
→ Context 收口
→ 新 Context Retry
→ 100% success
它不需要让所有 SMALL_VALID 都先取消一次。
这样能控制总测试时间,也能让“功能基线”和“故障注入”分开统计。
十四、HAR Consumer 也要在最终 Gate 里真实调用
06 的 Runner 不是从 Library 自己调用 Native。
而是从消费应用安装:
@jsonpulse/native
以后调用。
这样验证的是:
HAR
→ Consumer
→ libjsonpulse.so
→ Node-API
→ simdjson
完整发布路径。
如果只在 Library Unit Test 里跑,无法覆盖包名、so 收集和宿主集成问题。
十五、DevEco 图只保留最终矩阵
开发图:

能看到:
5 datasets
2 ABIs
4 cycles
40 runs
以及:
parse mismatch=0
error mismatch=0
promise mismatch=0
progressOrderErrors=0
asyncWorkLeak=0
tsfnLeak=0
nativeBufferLeak=0
底部最终:
RESULT PASS
十六、运行图把正确性、性能和资源放在一页
最终运行图:

数据集:
SMALL_VALID
PASS
MEDIUM_VALID
PASS
LARGE_VALID
PASS
DEEP_NEST
PASS
INVALID_UTF8
PASS
ABI:
arm64-v8a
PASS
x86_64
PASS
资源:
workContextsReleased=40
tsfnFinalized=16
all leaks=0
最终:
PASS
十七、40/40 不代表“所有 JSON、所有 ABI 永远没问题”
PASS 只代表:
当前 JsonPulse 1.0.0
当前 API 26 SDK
当前 5 个数据集
当前 2 个 ABI
当前 4 轮
当前测试设备 / 模拟器
全部符合预期。
它不代表:
任意 1GB JSON
任意未来 ABI
任意 simdjson 版本
任意 SDK
都已经验证。
工程回归必须有边界,不能把测试矩阵写成绝对承诺。
十八、JsonPulse 六篇真正形成的是一条 Native 交付主线
回头看整个系列:
01
把 simdjson 编进 HarmonyOS
02
把 Buffer Owner 说清楚
03
把大计算移到 Worker
04
把跨线程进度和取消做安全
05
把 Native 能力打包交付
06
把正确性、性能与资源变成回归门禁
看起来跨了:
CMake
C++
Node-API
ArkTS
Thread
HAR
ABI
实际上始终围绕一个问题:
一份成熟 C++ 三方库,怎样真正成为 HarmonyOS 工程里可以长期维护和分发的能力。
十九、JsonPulse 到 06 正式结束
这个系列固定 X=6,到这里完成。
继续写 07,很容易变成:
再加一个 parser API
再测一份 JSON
再做一次优化
新的工程主矛盾已经不在 JsonPulse。
下一轮应该切换到明显不同方向和新 Demo,从新的 01 开始。
优先可以考虑:
平行视界 / EasyGo
精准碰一碰 / 跨设备投递
HarmonyOS 应用上架审核
不再继续扩写 JsonPulse。
二十、五个数据集还要固定输入 Hash
如果只按:
MEDIUM_VALID
5MB
找一个文件,测试数据可能被人替换。
下次虽然名字一样,内容已经不同,性能比较就失去意义。
所以 JsonPulse 的数据集 Manifest 还保存:
fileSize
sha256
records
maxDepth
expectedError
Runner 启动前先核对 Hash。
只有数据集身份一致,性能曲线才有比较价值。
二十一、DEEP_NEST 不是为了追求“越深越厉害”
当前:
8MB
maxDepth=32
40,000 records
它的作用是覆盖和普通订单 JSON 不同的结构复杂度。
如果只测:
扁平数组
MeasureDepth、嵌套对象遍历和错误边界很难真正进入主路径。
06 不把 32 层写成 simdjson 或 HarmonyOS 的系统极限。
它只是当前回归矩阵中的一个固定结构 Profile。
二十二、INVALID_UTF8 要同时验证同步与异步错误语义
错误输入不能只跑同步路径。
最终测试:
parseSummary(string)
parseUint8(Uint8Array)
parseAsync(Uint8Array)
都应该得到一致的业务错误类型。
如果同步路径返回:
INVALID_UTF8
异步 Promise 却只 Reject:
UNKNOWN_NATIVE_ERROR
对业务来说仍然是不一致。
因此:
errorContractMismatch=0
实际上覆盖了跨 API 形态的错误契约。
二十三、取消路径不能污染后面的性能采样
LARGE_VALID 的故障注入会做:
cancel
→ cleanup
→ retry
这轮故障测试的耗时不会混入:
arm64 large avg
arm64 large p95
正常性能基线只统计完整成功运行。
取消测试单独保存:
cancelLatency
cleanupCost
retrySuccess
否则 P95 里混入人为取消等待时间,数据就没有解释意义。
二十四、x86_64 只做自己的基线,不和真机“比赛”
模拟器环境会受到:
宿主机 CPU
虚拟化
模拟器版本
后台负载
影响。
所以 06 的报告对 x86_64 明确标:
Emulator Baseline
arm64-v8a 则记录具体真机环境。
判断回退时只做同环境纵向比较。
这条原则对 Native 性能报告尤其重要,因为同一份 C++ 代码在不同微架构上的 SIMD 路径、缓存和调度表现本来就不同。
二十五、workContextsReleased=40 还要和任务创建数对账
单看:
activeContext=0
不够。
如果第 3 次 Context 从来没有进入统计,也可能最后仍然是 0。
所以资源探针保存:
createdContexts=40
releasedContexts=40
两边必须相等。
同样:
TSFN create count
TSFN finalize count
也要按开启进度的 Profile 对账。
最终:
16 create
16 finalize
才算闭环。
二十六、Native Buffer 峰值还要按任务尺寸归一化解释
48MB 输入的异步路径会产生:
ArkTS 48MB Buffer
+
Native 48MB Copy
+
Parser Working Set
最大内存:
178.2MB
并不奇怪。
更值得关注的是:
任务结束后
Native Copy 是否归零;
下一轮开始前
稳定基线是否恢复。
如果 LARGE_VALID 连续四轮以后:
112
130
148
166
那才是明显泄漏信号。
本轮:
112.4 → 113.6MB
没有阶梯增长。
二十七、Promise / TSFN / Buffer 三种资源必须分 Owner 统计
Native 异步链里至少有三类资源:
napi_async_work
napi_threadsafe_function
Native Buffer
不能统称:
Native Resource
因为它们释放方式完全不同。
JsonPulse 的资源报告拆成:
asyncWorkLeak
tsfnLeak
nativeBufferLeak
任何一项大于 0 都会让最终状态变成 FAIL。
这种细分也能让开发者直接知道是 CompleteCB、Finalize 还是 Buffer Owner 出了问题。
二十八、HAR 自身也要进入 ABI 回归
06 每次启动测试前都会先执行 Package Probe:
HAR Version
SDK
ABI
so size
DT_NEEDED
unresolved symbols
只有 Package Gate 通过,才进入 40 次运行时测试。
否则:
包都不健康
继续跑 Parser 性能没有意义。
这也让 05 的发布检查和 06 的运行回归真正串成同一条主线。
二十九、性能回归需要有 WARN 区间
假设下一版:
arm64 MEDIUM P95
30.8 → 32.0ms
未必立刻应该 FAIL。
JsonPulse 会设置:
Baseline
Warn Threshold
Fail Threshold
轻微波动先:
PERF_WARN
明显超阈值才:
PERF_FAIL
但正确性、资源泄漏、ABI 加载失败没有 WARN:
直接 FAIL。
不同风险不应该用同一条判定线。
三十、回归失败必须留下可复现现场
任何一次 FAIL 都记录:
dataset
abi
cycle
taskId
packageVersion
sdkVersion
inputHash
parseMode
errorCode
activeContext
activeTsfn
activeBufferBytes
这样开发者不需要重新猜:
“昨天大概是 48MB 文件跑坏了”
而可以精确恢复那一轮输入和运行环境。
对于 Native 问题,这种“失败现场”比一张报错截图更重要。
三十一、最终 Acceptance Snapshot 也要版本化
本轮最终报告绑定:
JsonPulse 1.0.0
API 26
5 datasets
2 ABIs
4 cycles
以后升级:
simdjson
CMake
SDK
Bridge API
任意一项,都会生成新的 Acceptance Version。
旧基线保留。
这样才能区分:
性能自然变化
和
测试环境变化。
三十二、06 最终形成四类 Gate
最终 PASS 不只看:
40/40。
而是四层:
PACKAGE_GATE
HAR / ABI / Symbol / SDK
FUNCTION_GATE
正确结果 / 正确错误契约
ASYNC_GATE
Promise / Progress / Cancel / Retry
RESOURCE_GATE
AsyncWork / TSFN / Buffer / Stable Memory
四层全部通过:
status=PASS
如果只是 Parser 结果正确,但 TSFN 泄漏:
仍然 FAIL。
这才符合一个可长期使用的 Native 组件质量标准。
三十三、系列结束以后,最值得保留的是自动化脚本
六篇文章结束后,真正应该进仓库长期维护的不是文章本身,而是:
Package Probe
Symbol Check
ABI Matrix
Dataset Manifest
Native Regression Runner
Resource Probe
Acceptance Report
以后任何人升级 simdjson、SDK 或 Node-API Bridge,只要运行这套脚本,就能重新回答:
这个三方 Native 库在 HarmonyOS 上还安全吗、还兼容吗、还快吗?
这才是 JsonPulse 系列从一次适配工作变成可持续工程的标志。
三十四、回归还要检查“旧 HAR 升级到新 HAR”的兼容路径
真实用户不是永远从 1.0.0 全新安装。后续版本通常会:
1.0.0
→ 1.0.1
→ 1.1.0
所以 JsonPulse 最终回归还预留 Upgrade Case:先安装旧 Consumer 与旧 HAR,生成一次历史结果,再升级到新包,重新 import Native 模块并执行相同数据集。只要 ArkTS 类型声明、模块名、结果契约仍然兼容,业务状态应该继续可用;如果是明确 Breaking Change,就必须提升 Bridge Version,而不是静默让旧调用在运行时失败。
三十五、最终报告同时保存“功能通过”和“性能环境”
性能数字没有环境信息就没有长期价值。因此 Acceptance Snapshot 额外记录:
device / emulator
HarmonyOS version
SDK API
ABI
buildType
packageVersion
simdjson revision
下一轮再看到 194.2ms 时,能确认是在同一基线环境下比较,而不是把不同设备、不同 SDK 和不同构建类型混成一条趋势线。Native 性能测试真正可靠的前提,是环境也被当作测试数据的一部分。
三十六、JsonPulse 的收口标准不是“解析最快”,而是“长期可控”
到 06 为止,JsonPulse 接受了一次额外 Buffer Copy,也接受了 HAR 打包、符号检查、ThreadSafeFunction 和资源探针带来的工程复杂度。它们未必让单次 benchmark 数字更漂亮,却让线程、内存、ABI、错误、升级和分发都拥有明确边界。
对于三方 Native 库适配,这种“可解释、可升级、可回归”比某一次快几毫秒更重要。最终 PASS 表达的也正是这一点:不是 simdjson 在 HarmonyOS 上永远不会出问题,而是这套工程已经具备持续发现问题、定位问题并安全演进的能力。
这套基线可以继续复用。
参考资料
- HAR / HSP 应用包术语:
https://developer.huawei.com/consumer/cn/doc/harmonyos-guides/application-package-glossary - 预构建库快速链接:
https://developer.huawei.com/consumer/cn/doc/doccenter-deveco-studio/ide-hvigor-so - C/C++ 标准库机制:
https://developer.huawei.com/consumer/cn/doc/HarmonyOS-Guides/c-cpp-overview - Node-API 跨语言调用:
https://developer.huawei.com/consumer/cn/doc/doccenter-games/games-universal-using-napi-interaction-0000002411166425
更多推荐

所有评论(0)