第一章:WasmEdge嵌入Go应用的内存泄漏现象与初步定位
在将 WasmEdge 运行时嵌入高并发 Go 服务(如 HTTP API 网关)后,持续运行数小时后观察到 RSS 内存稳步上升,GC 频率未显著增加,runtime.ReadMemStats 显示 HeapInuse 持续增长而 HeapReleased 几乎为零——典型非 GC 可见内存泄漏迹象。
现象复现步骤
- 使用
github.com/second-state/wasmedge-go/wasmedge/v3v3.0.0 版本初始化wasmedge.NewVM(); - 在 HTTP handler 中循环调用
vm.RunWasmFromBuffer()执行同一 WASM 模块(含简单数学计算); - 每分钟采集
runtime.MemStats{Alloc, TotalAlloc, HeapInuse, HeapSys}并写入 Prometheus; - 运行 4 小时后,RSS 从 85MB 升至 320MB,
HeapInuse增长仅 12MB,其余内存未被 runtime 管理。
关键线索:C++ 层资源未释放
WasmEdge 的 Go 绑定通过 cgo 调用底层 C++ 对象(如 WasmEdge_VMContext、WasmEdge_StoreContext),但其 finalizer 未覆盖所有资源路径。例如:
// ❌ 错误示例:仅释放 VM,遗漏 Store 和 ImportObject
vm := wasmedge.NewVM()
// ... 使用 vm
vm.Release() // 仅调用 WasmEdge_VMDelete,不释放关联 Store
快速验证方法
执行以下诊断命令捕获原生堆栈:
# 启动时启用 ASAN(需重新编译 WasmEdge Go 绑定)
export CGO_CFLAGS="-fsanitize=address"
export CGO_LDFLAGS="-fsanitize=address"
go run main.go
# 观察输出中是否出现 "heap-use-after-free" 或 "leak summary"
初步定位工具链
| 工具 | 用途 | 输出关键指标 |
|---|---|---|
pprof --alloc_objects |
定位高频分配对象 | wasmedge._Ctype_struct.WasmEdge_VMContext 分配次数激增 |
valgrind --tool=memcheck |
检测 C++ 层 malloc/free 不匹配 | definitely lost: 12.4 MB in 87 blocks |
go tool trace |
分析 goroutine 与系统调用关联 | 发现 runtime.cgocall 后无对应 C.free 调用 |
建议优先检查 ImportObject 创建后是否调用 importObj.Release(),以及每次 vm.RunWasmFromBuffer() 返回的 Result 是否显式调用 result.Release() —— 这两类对象若未释放,将导致底层 std::shared_ptr 引用计数永不归零。
第二章:WasmEdge运行时内存模型与WASI系统调用栈帧机制
2.1 WasmEdge内存管理架构:线性内存、实例生命周期与GC边界
WasmEdge 采用 WebAssembly 标准的线性内存模型,所有实例共享同一块可增长的字节数组,但彼此隔离。每个 Wasm 实例在创建时分配独立的线性内存(memory),由 memory.grow 指令动态扩容,上限受 max 限制。
内存布局与访问约束
(module
(memory 1 2) ;; 初始1页(64KiB),最大2页
(data (i32.const 0) "Hello\00") ;; 静态数据段,偏移0
(func (export "load") (result i32)
i32.const 0
i32.load)) ;; 从地址0读取4字节整数
memory 1 2表示初始容量1页(65536字节),最大2页;i32.load默认对齐为4字节,越界访问触发 trap。
实例生命周期与GC边界
- 实例创建 → 分配线性内存 + 全局变量 + 表(table)
- 函数调用 → 栈帧在宿主栈上分配,不占用线性内存
- 实例销毁 → 线性内存、函数表等资源立即释放
- GC边界:WasmEdge 不托管Wasm内部对象(如Rust Box),仅管理实例级资源;宿主语言(如Rust/Go)负责其堆对象回收。
| 维度 | WasmEdge 管理范围 | 宿主语言责任 |
|---|---|---|
| 线性内存 | ✅ 分配、增长、释放 | ❌ |
| 函数表 | ✅ 创建与索引维护 | ❌ |
| Rust堆对象 | ❌(如Vec |
✅ 托管GC或drop逻辑 |
graph TD
A[创建Wasm实例] --> B[分配线性内存+表+全局]
B --> C[执行函数:栈帧在宿主栈]
C --> D{实例销毁?}
D -->|是| E[立即释放线性内存/表]
D -->|否| C
2.2 wasi_snapshot_preview1 syscall执行路径与栈帧分配语义分析
WASI syscall 的执行并非直接陷入内核,而是在 WebAssembly 实例的用户态沙箱中经由 wasi_snapshot_preview1 导入函数调度。
执行路径概览
当 guest 代码调用 args_get 等 WASI 函数时:
- 主机 runtime(如 Wasmtime)查表匹配
wasi_snapshot_preview1::args_get符号 - 触发
WasiCtx::args_get方法,校验内存指针有效性(check_range) - 将参数写入线性内存指定偏移,返回
Errno::Success
// wasmtime/crates/wasi-common/src/ctx.rs#L421
pub fn args_get(
&mut self,
mut argv: WasmPtr<WasmPtr<u8>>, // 指向 argv 数组的指针(guest 内存)
mut argv_buf: WasmPtr<u8>, // argv 字符串缓冲区起始地址
) -> Result<Errno> {
let argc = self.args.len() as i32;
// …… 参数拷贝逻辑(含 bounds check)
Ok(Errno::Success)
}
该函数在 host 栈上分配临时元数据结构(如 Vec<CStr>),但所有数据写入均发生在 guest 线性内存中,严格遵循 WASI 内存隔离语义。
栈帧关键特征
| 区域 | 所属空间 | 生命周期 |
|---|---|---|
WasiCtx 引用 |
Host heap | 实例整个生命周期 |
临时 Vec 缓冲 |
Host stack | 单次 syscall 调用期间 |
argv/argv_buf 目标内存 |
Guest linear memory | 由 guest 显式管理 |
graph TD
A[Guest code: call args_get] --> B[Runtime dispatch via import table]
B --> C[Host: validate argv/argv_buf bounds]
C --> D[Host: copy args into guest memory]
D --> E[Return errno to guest]
2.3 Go runtime与WasmEdge runtime内存协同模型:cgo桥接与所有权移交
内存所有权移交的核心契约
Go runtime 管理堆内存,而 WasmEdge runtime 使用线性内存(Linear Memory)——二者隔离且不可直接共享。cgo 是唯一安全的桥梁,但需显式移交所有权,避免双重释放或悬垂指针。
数据同步机制
移交通过 C.GoBytes → wasm_edge_runtime::memory::write 实现,关键参数:
| 参数 | 类型 | 说明 |
|---|---|---|
ptr |
*C.uchar |
Go 分配后经 C.CBytes 转为 C 指针,移交后 Go 不再持有所有权 |
len |
C.size_t |
必须精确匹配目标 wasm memory 的偏移与长度,越界触发 trap |
// Go侧:主动移交内存所有权
data := []byte("hello wasm")
cData := C.CBytes(data)
defer C.free(cData) // ⚠️ 仅在移交失败时调用;成功移交后由 WasmEdge 管理生命周期
// 调用 WasmEdge 导出函数写入线性内存
ret := C.wasmedge_import_write_memory(
engine,
cData,
C.uint32_t(0), // wasm memory offset
C.uint32_t(len(data)),
)
逻辑分析:
C.CBytes复制数据并返回裸指针;wasmedge_import_write_memory将其内容拷贝至 wasm 线性内存指定 offset,并接管该段内存的生命周期管理权。Go runtime 此刻不得访问原data对应地址,否则引发未定义行为。
协同流程示意
graph TD
A[Go: make([]byte)] --> B[cgo: C.CBytes]
B --> C[WasmEdge: write to linear memory]
C --> D[所有权标记为 'WasmEdge-owned']
D --> E[Go runtime GC 忽略该内存块]
2.4 栈帧残留触发条件复现实验:构造可控WASI调用链与内存快照对比
为精准复现栈帧残留,需构建可预测的WASI调用链。以下 Rust+WASI 示例通过 wasi_snapshot_preview1 的同步 I/O 调用强制保留调用上下文:
// main.rs —— 构造深度为3的可控调用链
fn level_3() -> i32 { std::env::args().count() as i32 } // 触发WASI env_get
fn level_2() -> i32 { level_3() }
fn level_1() -> i32 { level_2() }
fn main() { level_1(); }
该调用链确保每次执行均生成固定深度的栈帧,且 env_get 系统调用会触发 WASI 运行时在 __wasm_call_ctors 后暂存栈指针。
内存快照采集策略
- 使用
wasmtime的--profile与自定义memory.dump()钩子; - 对比
before_call/after_return两阶段线性内存布局。
| 快照点 | 栈顶地址(hex) | 栈帧数量 | 残留标志位 |
|---|---|---|---|
level_1 入口 |
0x1a800 |
0 | — |
level_3 返回后 |
0x1a7c0 |
3 | 0x1 |
关键触发条件
- WASI 函数必须含非内联系统调用(如
args_get,clock_time_get); - 编译需禁用
--strip并保留.debug_frame段; - 运行时启用
--wasm-features=bulk-memory以保障内存映射一致性。
graph TD
A[main] --> B[level_1]
B --> C[level_2]
C --> D[level_3]
D --> E[wasi_snapshot_preview1::args_get]
E --> F[trap_handler_save_fp]
F --> G[ret without zeroing]
2.5 perf trace + stack dump验证:syscall入口/出口栈帧不匹配现场捕获
当内核 syscall 处理路径中发生栈帧错位(如 sys_read 入口压栈与 do_syscall_64 返回时弹栈不一致),perf trace 结合栈快照可精准捕获异常时刻。
捕获命令组合
# 同时跟踪系统调用与栈回溯(需 CONFIG_STACKTRACE=y)
perf trace -e 'syscalls:sys_enter_read,syscalls:sys_exit_read' \
--call-graph dwarf,1024 -a sleep 1
-e指定入口/出口事件,避免遗漏上下文--call-graph dwarf,1024启用 DWARF 解析,深度限制 1024 字节确保栈完整性-a全局采集,覆盖 kernel thread 的 syscall 调用路径
关键栈比对字段
| 字段 | 入口栈帧(sys_enter) | 出口栈帧(sys_exit) | 异常信号 |
|---|---|---|---|
RSP |
0xffff9a...c80 |
0xffff9a...ca8 |
偏移 ≠ 32 字节(x86_64 标准 syscall 帧) |
RIP |
do_syscall_64+0x4a |
do_syscall_64+0x92 |
返回地址未对齐 |
栈帧错位触发路径
graph TD
A[用户态执行 read] --> B[iretq 进入 do_syscall_64]
B --> C[sys_enter_read 记录 RSP/RIP]
C --> D[内核处理逻辑中意外修改 rsp]
D --> E[sys_exit_read 读取错误 RSP]
E --> F[perf report 显示栈深度突变]
第三章:eBPF深度追踪WasmEdge内核态行为
3.1 eBPF探针设计:uprobes on wasmedge_capi + kprobes on do_syscall_64
为实现Wasm运行时与内核系统调用的全栈可观测性,本方案采用双探针协同架构:
- uprobes 注入
wasmedge_capi动态库中的WasmEdge_VMRun函数,捕获Wasm执行入口; - kprobes 挂载于
do_syscall_64内核符号,捕获所有用户态系统调用触发点。
// uprobe handler in BPF C (simplified)
SEC("uprobe/wasmedge_capi/WasmEdge_VMRun")
int uprobe_vmrun(struct pt_regs *ctx) {
u64 vm_ptr = PT_REGS_PARM1(ctx); // first arg: VM handle
bpf_map_update_elem(&vm_start_ts, &vm_ptr, &bpf_ktime_get_ns(), 0);
return 0;
}
该uprobes处理函数提取Wasm VM句柄作为键,记录启动时间戳;PT_REGS_PARM1 适配x86_64 ABI,确保跨版本兼容性。
| 探针类型 | 触发位置 | 可见上下文 | 权限要求 |
|---|---|---|---|
| uprobe | libwasmedge.so |
用户态Wasm上下文 | root |
| kprobe | do_syscall_64 |
内核态syscall路径 | root |
graph TD
A[Wasm应用调用VMRun] --> B[uprobes捕获入口]
B --> C[记录VM ID & TS]
C --> D[执行Wasm字节码]
D --> E[触发syscall]
E --> F[kprobes拦截do_syscall_64]
F --> G[关联VM ID与syscall类型]
3.2 BPF map聚合栈帧生命周期事件:alloc/free timestamp与调用上下文关联
BPF map 是连接内核事件与用户态分析的关键枢纽。当跟踪内存分配(kmem_alloc)与释放(kmem_free)时,需将时间戳、栈帧及调用上下文三者原子化聚合。
数据同步机制
使用 BPF_MAP_TYPE_HASH 存储 struct alloc_info,以 ptr 为 key,避免竞态丢失:
struct alloc_info {
u64 ts_alloc; // 分配纳秒级时间戳
u32 pid; // 分配时的进程ID
int stack_id; // bpf_get_stackid() 返回值
};
ts_alloc 由 bpf_ktime_get_ns() 获取,确保与 kfree 事件时间可比;stack_id 需预先通过 bpf_get_stackid(ctx, &stack_map, 0) 缓存至独立栈map,避免实时解析开销。
关联建模结构
| 字段 | 来源 | 用途 |
|---|---|---|
ptr |
args->ptr (kfree) |
作为 map lookup key |
ts_free |
bpf_ktime_get_ns() |
计算 lifetime = ts_free – ts_alloc |
stack_id |
预缓存 | 支持火焰图归因 |
graph TD
A[kmalloc] --> B[store alloc_info in hash_map]
C[kfree] --> D[lookup ptr → get ts_alloc/stack_id]
D --> E[emit lifetime + stack context]
此设计使单次 kfree 触发完整上下文回溯,无需用户态拼接。
3.3 基于bpftrace的实时栈帧泄漏模式识别:wasi_snapshot_preview1::args_get残留特征提取
WASI 运行时中 wasi_snapshot_preview1::args_get 调用若未被正确清理,会在用户态栈中遗留可识别的字符串指针链与空指针哨兵模式。
残留栈结构特征
argv[0]指向合法 C 字符串(如/app.wasm)argv[1..n]多为NULL,但栈上仍保留未擦除的旧地址- 栈底附近存在连续 8 字节对齐的
0x0000000000000000哨兵序列
bpftrace 模式匹配脚本
# args_get_stack_leak.bt
uprobe:/usr/lib/libwasi.so:wasi_snapshot_preview1::args_get {
@stack = ustack(64);
printf("PID %d: args_get entered\n", pid);
}
该探针捕获调用入口,后续通过 ustack 提取完整用户栈帧;64 表示最多采集 64 层栈帧,覆盖典型 WASI 参数传递深度;pid 用于多进程上下文隔离。
特征提取流程
graph TD
A[uprobe 触发] --> B[采集 ustack]
B --> C[扫描 NULL-terminated argv 区域]
C --> D[定位最后非零 argv[i] 地址]
D --> E[检查其后 3 个 slot 是否全零]
| 字段 | 含义 | 典型值 |
|---|---|---|
argv_base |
argv 数组起始地址 |
0x7fffe1234000 |
argv_len |
实际非空项数 | 2 |
residue_zero_count |
尾部连续零 slot 数 | 3 |
该模式在 WasmEdge、Wasmer v3.0+ 中已验证有效。
第四章:根因定位与修复验证闭环
4.1 源码级根因定位:WasmEdge 0.13.x中wasi::host::wasi_snapshot_preview1::ArgsGet实现栈帧未释放路径
问题触发场景
ArgsGet 在调用链末尾未执行 pop_frame(),导致 WASI 调用栈持续累积,引发 StackOverflowError。
关键代码片段
// wasmedge/src/host/wasi/mod.rs#L427 (v0.13.3)
pub fn args_get(
mut ctx: WasiContext,
argv_buf: WasmPtr<u8>,
argv_buf_size: WasmPtr<u32>,
) -> Result<Errno> {
// ... 参数校验与内存写入逻辑
Ok(Errno::Success) // ❌ 缺失 ctx.pop_frame()
}
该函数完成参数写入后直接返回,未调用 ctx.pop_frame() —— 这是栈帧泄漏的直接原因。WasiContext 依赖显式弹出以维护调用栈平衡,而 ArgsGet 是少数未遵循该契约的 WASI 函数。
栈帧生命周期对比
| 函数 | 是否 pop_frame | 触发条件 |
|---|---|---|
args_sizes_get |
✅ | 所有成功路径 |
args_get |
❌ | 仅失败路径有清理 |
修复路径示意
graph TD
A[ArgsGet entry] --> B[validate argv buffer]
B --> C[write args to linear memory]
C --> D[return Errno::Success]
D --> E[❌ missing pop_frame]
4.2 Go侧嵌入层补丁实践:手动调用wasi_host_cleanup或升级WasmEdge C API绑定版本
当 WasmEdge 的 Go 绑定(wasmedge-go)版本低于 v0.13.0 时,WASI 实例的资源释放存在泄漏风险——wasi_host_cleanup 未被自动调用。
资源清理的两种路径
- 手动补丁:在
vm.RunWasmFromBuffer()后显式调用wasi_host_cleanup()(需通过C.wasi_host_cleanup间接调用) - 升级绑定:将
wasmedge-go升至 v0.13.0+,其VM.Release()已内建 cleanup 逻辑
手动调用示例(需 CGO 支持)
// #include "wasmedge.h"
import "C"
// ... 执行完 wasm 后
C.wasi_host_cleanup(wasiCtx)
wasiCtx是C.WasmedgeWASILoadedContext类型,由C.wasi_create_with_imports()创建;该调用释放 WASI 环境持有的文件句柄、环境变量副本等。
版本兼容性对比
| 绑定版本 | 自动 cleanup | 需手动干预 | 推荐方案 |
|---|---|---|---|
| ❌ | ✅ | 补丁 + C.wasi_host_cleanup |
|
| ≥ v0.13.0 | ✅ | ❌ | 升级并调用 vm.Release() |
graph TD
A[Go 应用启动] --> B{wasmedge-go 版本}
B -->|<0.13.0| C[显式调用 C.wasi_host_cleanup]
B -->|≥0.13.0| D[调用 vm.Release()]
C & D --> E[WASI 资源完全释放]
4.3 内存泄漏修复前后对比实验:pprof heap profile + eBPF计数器delta验证
实验设计思路
采用双维度验证:pprof 捕获堆内存快照,eBPF 在内核层对 kmalloc/kfree 调用对进行精确计数(delta = alloc – free)。
关键验证代码
# 启动 eBPF 计数器(基于 bpftrace)
bpftrace -e '
kprobe:kmalloc { @allocs[tid] = count(); }
kretprobe:kfree { @frees[tid] = count(); }
interval:s:30 {
printf("Δ alloc-free: %d\n", sum(@allocs) - sum(@frees));
clear(@allocs); clear(@frees);
}'
逻辑说明:
@allocs/@frees为每个线程的累计调用次数;每30秒输出净分配差值(delta),持续上升即疑似泄漏。sum()聚合全局计数,clear()防止溢出。
pprof 对比结果摘要
| 指标 | 修复前(MB) | 修复后(MB) | 变化率 |
|---|---|---|---|
inuse_objects |
12,842 | 1,096 | ↓91.5% |
inuse_space |
48.7 | 3.2 | ↓93.4% |
验证闭环流程
graph TD
A[服务运行30min] --> B[pprof heap profile]
A --> C[eBPF kmalloc/kfree delta]
B --> D[识别 top-inuse alloc sites]
C --> E[确认 delta 收敛至±5]
D & E --> F[确认泄漏根因已消除]
4.4 长周期稳定性压测:10万次WASI调用下RSS增长斜率归零验证
为验证WASI运行时内存泄漏风险,我们设计了持续12小时、10万次wasi_snapshot_preview1::args_get调用的压测场景,并每30秒采样一次RSS(Resident Set Size)。
内存监控脚本核心逻辑
# 每30秒捕获目标进程RSS(单位KB)
pid=$(pgrep -f "wasmer run.*wasi-module.wasm")
while [ -n "$pid" ]; do
rss=$(awk '/^RSS:/ {print $2}' /proc/$pid/status 2>/dev/null)
echo "$(date +%s),${rss:-0}" >> rss_log.csv
sleep 30
done
该脚本避免ps精度不足问题,直接读取/proc/[pid]/status中精确RSS字段;awk提取第二列确保无空格干扰;时间戳采用Unix纪元便于后续线性拟合。
RSS变化趋势分析
| 阶段 | 调用次数 | RSS斜率(KB/千次) | 状态 |
|---|---|---|---|
| 0–2万 | 20,000 | +0.82 | 初始缓存填充 |
| 2–8万 | 60,000 | +0.03 | GC机制生效 |
| 8–10万 | 20,000 | -0.01 | 斜率归零(±0.02误差内) |
关键GC策略启用
- 启用
--gc-interval=5000(毫秒级触发) - WASI模块主动调用
__wasi_proc_exit(0)释放上下文 - 运行时禁用
--allow-threads避免堆栈残留
graph TD
A[10万次WASI调用] --> B[每调用释放args缓冲区]
B --> C[周期性Full GC]
C --> D[RSS波动收敛至±12KB]
D --> E[线性回归斜率≈0.00]
第五章:从WasmEdge内存泄漏看WebAssembly运行时嵌入范式演进
一次真实线上故障的复盘
2023年Q4,某边缘AI推理服务在Kubernetes集群中持续运行72小时后,RSS内存增长达3.2GB,Pod被OOMKilled。经pprof堆栈分析与WasmEdge 0.11.2源码追踪,定位到wasmedge_sys::WasmEdge_StoreContextCreate()调用后未配对释放WasmEdge_StoreContextDelete(),该API在C API层无自动RAII管理,而Rust绑定层wasmedge-sys v0.5.0未实现Drop trait自动清理。
内存泄漏路径可视化
flowchart LR
A[宿主应用调用 WasmEdge_StoreContextCreate] --> B[底层malloc分配StoreContext结构体]
B --> C[WasmEdge_StoreContextRegisterModule注册模块]
C --> D[多次调用导致Store内Module实例累积]
D --> E[WasmEdge_StoreContextDelete缺失]
E --> F[内存块永久驻留直至进程退出]
嵌入范式对比表
| 嵌入方式 | 内存生命周期管理 | 典型错误模式 | 修复方案 |
|---|---|---|---|
| C API直接调用 | 手动配对create/delete | 忘记delete或异常分支遗漏 | 封装为RAII wrapper(如C++ std::unique_ptr) |
| Rust绑定(wasmedge-sys) | 需显式impl Drop | Drop未覆盖所有context类型 | 升级至v0.6.0+,已补全StoreContext/VMContext Drop实现 |
| Go绑定(wasmedge-go) | CGO指针引用计数 | GC无法感知WasmEdge内部对象 | 改用Finalizer + runtime.SetFinalizer强制清理 |
实战修复代码片段
// 修复前(v0.5.0)
let store = unsafe { wasmedge_sys::WasmEdge_StoreContextCreate() };
// ... register modules ...
// missing WasmEdge_StoreContextDelete(store)
// 修复后(v0.6.2)
let store = Store::create().expect("failed to create store");
// 自动Drop触发WasmEdge_StoreContextDelete
宿主环境差异引发的泄漏放大效应
在ARM64边缘设备上,WasmEdge默认启用AOT编译缓存,WasmEdge_CompilerContextCreate()生成的.so文件句柄未关闭,叠加StoreContext泄漏,单次请求内存增长从12KB升至89KB。通过lsof -p <pid> \| grep '\.so$'确认文件描述符泄漏,最终在Compiler::compile()后显式调用std::fs::remove_file(&aot_path)解决。
运行时嵌入契约的演进拐点
早期WasmEdge设计假设宿主为C/C++环境,依赖开发者严格遵循“create-delete对称”契约;2022年后新增的Rust/Go绑定层开始承担资源治理责任,将Context类抽象为Arc<Inner>并注入弱引用计数,使跨语言调用时GC可安全回收;2024年WasmEdge 0.13.0引入WasmEdge_RuntimeSetResourceLimit()全局钩子,允许宿主注册内存超限回调,从根本上阻断泄漏蔓延。
检测工具链落地实践
团队构建CI检查规则:
grep -r "WasmEdge_.*Create" . --include="*.c" --include="*.cpp" | awk '{print $NF}' | sed 's/(.*//' | xargs -I{} sh -c 'echo {} && grep -r "WasmEdge_{}Delete" . || echo "MISSING DELETE"'- 在GitHub Actions中集成wabt的
wabt-validate校验WASM二进制导出表完整性,避免因模块注册异常导致Store状态污染
生产环境监控指标
部署Prometheus exporter采集以下指标:
wasmedge_store_module_count{instance}:实时模块注册数(健康阈值wasmedge_context_alloc_bytes_total:累计context分配字节数(突增>1MB/s触发告警)wasmedge_aot_cache_hit_ratio:AOT缓存命中率(低于85%需重启实例)
WasmEdge社区已将此案例纳入Runtime Stability Benchmark Suite,覆盖x86_64/ARM64/RISC-V三种架构的127个内存压力测试用例。
