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
Logo

讨论HarmonyOS开发技术,专注于API与组件、DevEco Studio、测试、元服务和应用上架分发等。

更多推荐