run_case.sh 标准用例规格

轻重载内存压力下"申请一块匿名内存"的耗时测量 —— iOS / Linux 同一步骤 · 输出格式强约束

纯 POSIX sh无 awk/bc 依赖key=value 记录 pre_* 申请前快照配对iOS(dash) / Linux / macOS 实测

用例目标

  • 测什么:申请一块匿名内存的耗时 —— touch 模式 = mmap + 首触缺页(纯申请); memset 模式 = mmap + 整块写入(申请 + 写入)。
  • 怎么对比:轻载与重载的唯一差异是 -b ballast 参数;重载时 harness 每轮独立完整加压:建 ballast → 等填充完成与压力稳定 → 测一次 → 释放并恢复 → 下一轮重新来过 —— 各轮初始条件一致(这是 v3 协议的核心修正:旧"单次加压多轮测量" 会让压缩器逐轮吞噬 ballast,轮与轮不可比)。
  • 关键约束:每轮测量前先快照系统内存,申请耗时与申请时刻前的内存统计值逐轮配对成 一条 REC 记录 —— 耗时永远可以溯源到"当时系统处于什么状态"。
  • 跨平台:iOS(越狱)与 Linux 用完全相同的步骤;harness 为纯 POSIX sh (bash 3.2 / zsh / dash / Ubuntu sh 实测通过),不依赖 awk/bc/python。

环境准备

平台编译部署要点
Linuxmake产物与 run_case.sh 放同一目录即可
macOS (调试用)make同上;与 iOS 共用 Darwin 分支 (vm_stat)
iOS (越狱)make iosscp 到设备后设备端 /usr/bin/ldid -S<ents.plist> 签名 + chmod +x(Mac 侧签名会 SIGKILL)

运行依赖:sh、date、uname、kill、 内存源(Darwin: vm_stat + sysctl;Linux: /proc/meminfo)。

用例步骤(标准序列)

标准四步size 按需替换,此处 200M
# ① 轻载基线(系统空闲态)
sh ./run_case.sh -s 200M -m touch  -r 5          > case-light-touch.txt
sh ./run_case.sh -s 200M -m memset -r 5          > case-light-memset.txt

# ② 重载(每轮独立: 建压→稳定→测一次→释放恢复, 共 r 轮)
sh ./run_case.sh -s 200M -m touch  -r 5 -b 3600  > case-heavy-touch.txt
sh ./run_case.sh -s 200M -m memset -r 5 -b 3600  > case-heavy-memset.txt

# iOS 远程执行(例):
ssh -p 2222 root@localhost 'cd /var/mobile/ios_memslice && sh ./run_case.sh -s 200M -m touch -r 5 -b 3600'

参数

参数默认含义
-s200M申请大小,仅整 MiB(如 64M / 200M / 500M)
-mtouchtouch = 纯申请(首触缺页);memset = 申请 + 写入
-r5轮数;每轮独立配对一次申请前内存快照
-b0ballast(MiB);>0 时自动建压/释放(mem_stress -P 50)
-w20每轮建压后稳态判定的最短等待秒数( settled = free 与 swap_comp 双稳定, 上限 120s 封顶)
-t自动每轮 ballast 死开关(秒)—— 默认自适应 = 填充预算+稳态上限+恢复上限+60s(≈510s),SSH 失联也能自愈
ballast 取值:物理内存的 45%~60%(6GB 手机 ≈ 2700~3600)。 必须用 -P 50 半随机填充(压缩比 ~2:1,压缩器有余量)——不可压缩的全随机填充会塞满 XNU 压缩器导致 watchdog 内核 panic(实测事故见主页实验报告)。
内置安全机制(五层):① 限速填充 —— ballast 以 4MiB 步进填充,系统 free 低于地板 (2000 页 ≈ 31MiB)即暂停,回收跟得上才继续,free 永不砸穿(防 jetsam 风暴 / watchdog panic); ② 孤儿防护 —— 每轮建压前扫描存活 mem_stress,发现即 exit 5 拒绝双重加压; ③ 死开关 —— 每轮 ballast 自带到期自杀(SSH/父进程失联也自愈); ④ 中止线 —— 填充/稳态期 free < 16MiB 或 ballast 异常退出立即中止(exit 3); ⑤ 全路径清理 —— trap EXIT/INT/TERM/HUP(含 SSH 断连)保证任何路径下释放 ballast。
这套防护来自一次真实事故:旧协议的 SSH 通道超时重试 在孤儿 ballast 上叠加了第二个加压进程,双重 7.2GB 触发 watchdog panic(内核 90 秒无 checkin), 设备重启丢失越狱。事故日志与 panic 报告已归档。

输出格式约束

输出为行式记录:每行一类,行首是记录类型标签,后接空格分隔的 key=value 令牌。 除 ERROR 行可含自由文本外,所有值不含空格。

记录类型

行类型时机作用
CASE每次调用开头,恰好一行用例元数据(平台/内核/规模/模式/轮数/ballast)
PHASE阶段切换生命周期与阶段级内存快照(pre_;释放用 post_)
REC每轮测量一行申请耗时 + 缺页次数 + 申请前/驻留时内存统计(核心记录)
SUMMARY测量结束fill_ms 的 avg/min/max(可由 REC 重算)
ERROR异常ERROR code=<名称> ...;退出码 2=参数/环境,3=ballast 安全中止,4=测量/解析失败

REC 字段

key单位含义
run—轮次,1..runs
size_mib / ballast_mibMiB申请大小 / 当前 ballast 总量(轻载=0)
mode—touch / memset
mmap_usµs建映射耗时(XNU 预留免费,与压力无关,用于交叉验证)
fill_msms申请耗时主体:touch=首触全部页;memset=整块写入
unit + unit_label—touch → ns/page(单页首触成本);memset → MiB/s(写入吞吐)
unmap_msmsmunmap 释放耗时
rss_mibMiB本轮峰值驻留(应为 size+页表开销)
faults次申请窗口内总缺页次数(mmap→fill→读回全程;Darwin 取 task_events_info,其余 getrusage);-1=平台不可用
majflt次其中需磁盘 I/O 的主缺页(匿名首触通常全为 0;出现非零说明已在换页)
pre_*MiB申请时刻前的系统内存快照(见下表)
post_*MiB申请驻留时快照:fill 完成后 mem_bench 保持驻留 3s(不计入耗时),驻留期采样;与 pre_* 同键集

pre_* / post_* 内存指标统一口径(同键集,平台映射)

统一 key含义Darwin (vm_stat)Linux (/proc/meminfo)
pre_total物理内存总量sysctl hw.memsizeMemTotal
pre_free空闲Pages freeMemFree
pre_avail可分配(含可回收)-1MemAvailable
pre_free_spec空闲+投机页Pages free + speculative-1
pre_file文件缓存File-backed pagesCached
pre_anon匿名驻留Anonymous pagesAnonPages
pre_swap_comp换出/已压缩数据量Pages stored in compressorSwapTotal − SwapFree
pre_comp_occ压缩器物理占用Pages occupied by compressor-1

不变量(消费方可以依赖)

消费示例

parse.py逐轮提取 耗时 + 申请前 free
import sys
for line in open(sys.argv[1]):
    if line.startswith("REC "):
        rec = dict(kv.split("=", 1) for kv in line.split()[1:])
        print(rec["run"], rec["fill_ms"], rec["pre_free"], rec["unit"], rec["unit_label"])
# 重载首触成本:
#   awk: grep '^REC' case-heavy-touch.txt | tr ' ' '\n' | grep -A0 '^unit='

从网页到对比表 —— 完整走一遍

核心逻辑:格式由 run_case.sh 约束,不依赖设备 —— 任何人在任何 iOS / Linux 机器上执行第②步, 得到的 .txt 都是同一 key=value 格式;对比 = 把任意两份 case 文件喂给 compare_cases.py(第一个文件自动作为基准)。文件可任意组合:轻 vs 重、 iOS vs Linux、改动前后、不同内核版本。

① 取件(用例全部材料,均可在本页"脚本源码"区下载)

材料来源去向
mem_stress.c / mem_bench.c / Makefile本页 · 脚本源码区编译(见环境准备),产物与脚本同目录
run_case.sh本页 · 脚本源码区(下载按钮)被测设备任意目录,chmod +x
compare_cases.py本页 · 脚本源码区(下载按钮)任意电脑(对比在采集侧做,不要求设备有 Python)

② 采集(被测设备上执行,生成四个 case 文件)

在被测设备上iOS 与 Linux 命令完全相同
sh ./run_case.sh -s 200M -m touch  -r 5          > case-light-touch.txt
sh ./run_case.sh -s 200M -m memset -r 5          > case-light-memset.txt
sh ./run_case.sh -s 200M -m touch  -r 5 -b 3600  > case-heavy-touch.txt
sh ./run_case.sh -s 200M -m memset -r 5 -b 3600  > case-heavy-memset.txt
# iOS 远程执行同一命令, 例:
#   ssh -p 2222 root@device 'cd /var/mobile/ios_memslice && sh ./run_case.sh -s 200M -m touch -r 5 -b 3600'
# 采集完把 .txt 拷回电脑 (scp / sftp), 或在设备上直接查看

③ 对比(电脑上执行)

compare_cases.py第一个文件 = 基准
# 同平台 轻载 vs 重载 (纯申请):
python3 compare_cases.py case-light-touch.txt case-heavy-touch.txt

# 同平台 轻载 vs 重载 (申请+写入):
python3 compare_cases.py case-light-memset.txt case-heavy-memset.txt

# 跨平台 同状态对比 (重命名区分平台后):
python3 compare_cases.py case-ios-light-touch.txt case-linux-light-touch.txt

④ 输出(以下为真实数据,非演示)

iOS · 轻载 vs 重载(touch,200M) —— 耗时表(含缺页) + pre/post 状态表 + 驻留开销表 + DELTA 汇总行:

compare-example-touch.txt 下载
COMPARE files=2 base=case-ios-light-touch.txt size_mib=200 mode=touch

[耗时表]
file                          ballast runs  fill_ms avg/min/max                  unit  mmap_us unmap_ms   faults
----------------------------------------------------------------------------------------------------------------
case-ios-light-touch.txt            0    5   37.97 /  35.87 /  38.88      2966.2 ns/page     12.8     13.4    12800
case-ios-heavy-touch.txt         3600    5  139.49 / 103.55 / 158.34     10897.8 ns/page     11.8     20.1    12800

[pre_* 申请前快照] (每轮申请时刻前均值, MiB; N/A=平台无此指标)
指标             case-ios-light-touch.txt  case-ios-heavy-touch.txt         Δ
---------------------------------------------------------------------------
total                            5735.0                    5735.0     +0.0%
free                             3643.6                      43.2    -98.8%
avail                               N/A                       N/A         -
free_spec                        3645.4                      54.0    -98.5%
file                              633.0                     637.0     +0.6%
anon                              409.6                    3970.4   +869.3%
swap_comp                         740.2                     776.4     +4.9%
comp_occ                          272.0                     291.8     +7.3%

[post_* 申请驻留时快照] (fill 后驻留期均值, MiB)
指标             case-ios-light-touch.txt  case-ios-heavy-touch.txt         Δ
---------------------------------------------------------------------------
total                            5735.0                    5735.0     +0.0%
free                             3441.8                      39.8    -98.8%
avail                               N/A                       N/A         -
free_spec                        3443.2                      40.0    -98.8%
file                              633.0                     627.2     -0.9%
anon                              611.6                    3774.4   +517.1%
swap_comp                         740.2                    1169.4    +58.0%
comp_occ                          272.0                     504.6    +85.5%

[驻留开销 post−pre] (申请 SIZE 对系统状态的净影响, MiB)
指标             case-ios-light-touch.txt  case-ios-heavy-touch.txt
-----------------------------------------------------------------
total                              +0.0                      +0.0
free                             -201.8                      -3.4
avail                                 -                         -
free_spec                        -202.2                     -14.0
file                               +0.0                      -9.8
anon                             +202.0                    -196.0
swap_comp                          +0.0                    +393.0
comp_occ                           +0.0                    +212.8

DELTA case-ios-heavy-touch.txt     vs base: fill_ms=+267.4% unit=+267.4% unmap_ms=+50.0% faults=+0.0% pre_free=-98.8%

# 机器可读行 (key=value, 与用例记录同风格):
ROW file=case-ios-light-touch.txt size_mib=200 mode=touch ballast_mib=0 runs=5 fill_ms_avg=37.968 fill_ms_min=35.873 fill_ms_max=38.881 unit_avg=2966.2 unit_label=ns/page mmap_us_avg=12.8 unmap_ms_avg=13.368 faults_avg=12800.0 majflt_avg=0.0 pre_total_avg=5735.0 pre_free_avg=3643.6 pre_avail_avg=-1 pre_free_spec_avg=3645.4 pre_file_avg=633.0 pre_anon_avg=409.6 pre_swap_comp_avg=740.2 pre_comp_occ_avg=272.0 post_total_avg=5735.0 post_free_avg=3441.8 post_avail_avg=-1 post_free_spec_avg=3443.2 post_file_avg=633.0 post_anon_avg=611.6 post_swap_comp_avg=740.2 post_comp_occ_avg=272.0
ROW file=case-ios-heavy-touch.txt size_mib=200 mode=touch ballast_mib=3600 runs=5 fill_ms_avg=139.492 fill_ms_min=103.546 fill_ms_max=158.335 unit_avg=10897.8 unit_label=ns/page mmap_us_avg=11.8 unmap_ms_avg=20.053 faults_avg=12800.0 majflt_avg=0.0 pre_total_avg=5735.0 pre_free_avg=43.2 pre_avail_avg=-1 pre_free_spec_avg=54.0 pre_file_avg=637.0 pre_anon_avg=3970.4 pre_swap_comp_avg=776.4 pre_comp_occ_avg=291.8 post_total_avg=5735.0 post_free_avg=39.8 post_avail_avg=-1 post_free_spec_avg=40.0 post_file_avg=627.2 post_anon_avg=3774.4 post_swap_comp_avg=1169.4 post_comp_occ_avg=504.6
DELTA file=case-ios-heavy-touch.txt vs=case-ios-light-touch.txt fill_ms_pct=+267.4% unit_pct=+267.4% unmap_ms_pct=+50.0% faults_pct=+0.0% pre_total_pct=+0.0% pre_free_pct=-98.8% pre_free_spec_pct=-98.5% pre_file_pct=+0.6% pre_anon_pct=+869.3% pre_swap_comp_pct=+4.9% pre_comp_occ_pct=+7.3% post_total_pct=+0.0% post_free_pct=-98.8% post_free_spec_pct=-98.8% post_file_pct=-0.9% post_anon_pct=+517.1% post_swap_comp_pct=+58.0% post_comp_occ_pct=+85.5%

跨平台 · iOS vs Linux(同为轻载 touch,200M):

compare-example-cross.txt 下载
COMPARE files=2 base=case-ios-light-touch.txt size_mib=200 mode=touch

[耗时表]
file                          ballast runs  fill_ms avg/min/max                  unit  mmap_us unmap_ms   faults
----------------------------------------------------------------------------------------------------------------
case-ios-light-touch.txt            0    5   37.97 /  35.87 /  38.88      2966.2 ns/page     12.8     13.4    12800
case-linux-light-touch.txt          0    5  313.70 /  47.33 / 1081.34      6127.1 ns/page      5.8     23.1    51202

[pre_* 申请前快照] (每轮申请时刻前均值, MiB; N/A=平台无此指标)
指标             case-ios-light-touch.txt  case-linux-light-touch.txt         Δ
-----------------------------------------------------------------------------
total                            5735.0                      4895.0    -14.6%
free                             3643.6                      2999.8    -17.7%
avail                               N/A                      4344.6         -
free_spec                        3645.4                         N/A         -
file                              633.0                      1333.0   +110.6%
anon                              409.6                       105.2    -74.3%
swap_comp                         740.2                        50.0    -93.2%
comp_occ                          272.0                         N/A         -

[post_* 申请驻留时快照] (fill 后驻留期均值, MiB)
指标             case-ios-light-touch.txt  case-linux-light-touch.txt         Δ
-----------------------------------------------------------------------------
total                            5735.0                      4895.0    -14.6%
free                             3441.8                      2810.8    -18.3%
avail                               N/A                      4155.6         -
free_spec                        3443.2                         N/A         -
file                              633.0                      1333.0   +110.6%
anon                              611.6                       304.0    -50.3%
swap_comp                         740.2                        50.0    -93.2%
comp_occ                          272.0                         N/A         -

[驻留开销 post−pre] (申请 SIZE 对系统状态的净影响, MiB)
指标             case-ios-light-touch.txt  case-linux-light-touch.txt
-------------------------------------------------------------------
total                              +0.0                        +0.0
free                             -201.8                      -189.0
avail                                 -                      -189.0
free_spec                        -202.2                           -
file                               +0.0                        +0.0
anon                             +202.0                      +198.8
swap_comp                          +0.0                        +0.0
comp_occ                           +0.0                           -

DELTA case-linux-light-touch.txt   vs base: fill_ms=+726.2% unit=+106.6% unmap_ms=+73.1% faults=+300.0% pre_free=-17.7%

# 机器可读行 (key=value, 与用例记录同风格):
ROW file=case-ios-light-touch.txt size_mib=200 mode=touch ballast_mib=0 runs=5 fill_ms_avg=37.968 fill_ms_min=35.873 fill_ms_max=38.881 unit_avg=2966.2 unit_label=ns/page mmap_us_avg=12.8 unmap_ms_avg=13.368 faults_avg=12800.0 majflt_avg=0.0 pre_total_avg=5735.0 pre_free_avg=3643.6 pre_avail_avg=-1 pre_free_spec_avg=3645.4 pre_file_avg=633.0 pre_anon_avg=409.6 pre_swap_comp_avg=740.2 pre_comp_occ_avg=272.0 post_total_avg=5735.0 post_free_avg=3441.8 post_avail_avg=-1 post_free_spec_avg=3443.2 post_file_avg=633.0 post_anon_avg=611.6 post_swap_comp_avg=740.2 post_comp_occ_avg=272.0
ROW file=case-linux-light-touch.txt size_mib=200 mode=touch ballast_mib=0 runs=5 fill_ms_avg=313.704 fill_ms_min=47.334 fill_ms_max=1081.342 unit_avg=6127.1 unit_label=ns/page mmap_us_avg=5.8 unmap_ms_avg=23.139 faults_avg=51202.0 majflt_avg=0.0 pre_total_avg=4895.0 pre_free_avg=2999.8 pre_avail_avg=4344.6 pre_free_spec_avg=-1 pre_file_avg=1333.0 pre_anon_avg=105.2 pre_swap_comp_avg=50.0 pre_comp_occ_avg=-1 post_total_avg=4895.0 post_free_avg=2810.8 post_avail_avg=4155.6 post_free_spec_avg=-1 post_file_avg=1333.0 post_anon_avg=304.0 post_swap_comp_avg=50.0 post_comp_occ_avg=-1
DELTA file=case-linux-light-touch.txt vs=case-ios-light-touch.txt fill_ms_pct=+726.2% unit_pct=+106.6% unmap_ms_pct=+73.1% faults_pct=+300.0% pre_total_pct=-14.6% pre_free_pct=-17.7% pre_file_pct=+110.6% pre_anon_pct=-74.3% pre_swap_comp_pct=-93.2% post_total_pct=-14.6% post_free_pct=-18.3% post_file_pct=+110.6% post_anon_pct=-50.3% post_swap_comp_pct=-93.2%

怎么读对比表

三平台真实输出(均为实测,非演示数据)

iOS — iPhone 12 Pro · iOS 14.8.1 · /bin/sh=dash · touch 64M ×2 · ballast 1200M
case-sample-ios.txt 下载
CASE ts=2026-10-10T16:46:39+0800 platform=Darwin kernel=Darwin-20.6.0 arch=iPhone13,3 size_mib=64 mode=touch runs=2 ballast_mib=1200 tool=mem_bench
PHASE name=case_start ts=2026-10-10T16:46:39+0800 pre_total=5735 pre_free=3622 pre_avail=-1 pre_free_spec=3634 pre_file=644 pre_anon=426 pre_swap_comp=721 pre_comp_occ=267
PHASE name=round_start run=1 ts=2026-10-10T16:46:39+0800 pre_total=5735 pre_free=3622 pre_avail=-1 pre_free_spec=3634 pre_file=644 pre_anon=426 pre_swap_comp=721 pre_comp_occ=267
PHASE name=ballast_start run=1 ts=2026-10-10T16:46:39+0800 ballast_mib=1200 hold_s=510 pre_total=5735 pre_free=3622 pre_avail=-1 pre_free_spec=3634 pre_file=644 pre_anon=426 pre_swap_comp=721 pre_comp_occ=267
PHASE name=ballast_filled run=1 ts=2026-10-10T16:46:42+0800 waited_s=2 marker=1 pre_total=5735 pre_free=2420 pre_avail=-1 pre_free_spec=2432 pre_file=644 pre_anon=1628 pre_swap_comp=721 pre_comp_occ=267
PHASE name=ballast_settling run=1 waited_s=32 plateau=0 free_mib=2379 swap_comp=698
PHASE name=ballast_settling run=1 waited_s=64 plateau=0 free_mib=2323 swap_comp=691
PHASE name=ballast_settling run=1 waited_s=96 plateau=0 free_mib=1659 swap_comp=677
PHASE name=ballast_settled run=1 ts=2026-10-10T16:48:44+0800 waited_s=120 plateau=0 pid=12791 pre_total=5735 pre_free=1986 pre_avail=-1 pre_free_spec=2125 pre_file=960 pre_anon=1698 pre_swap_comp=677 pre_comp_occ=256
REC run=1 size_mib=64 mode=touch ballast_mib=1200 mmap_us=12.0 fill_ms=15.099 unit=3686.3 unit_label=ns/page unmap_ms=4.792 rss_mib=66.41 faults=4096 majflt=0 pre_total=5735 pre_free=1986 pre_avail=-1 pre_free_spec=2125 pre_file=960 pre_anon=1698 pre_swap_comp=677 pre_comp_occ=256 post_total=5735 post_free=1922 post_avail=-1 post_free_spec=2061 post_file=960 post_anon=1763 post_swap_comp=677 post_comp_occ=256
PHASE name=ballast_released run=1 ts=2026-10-10T16:48:48+0800 post_total=5735 post_free=3187 post_avail=-1 post_free_spec=3327 post_file=960 post_anon=497 post_swap_comp=677 post_comp_occ=256
PHASE name=round_recovered run=1 ts=2026-10-10T16:48:50+0800 waited_s=2 free_mib=3184 target_mib=2897
PHASE name=round_start run=2 ts=2026-10-10T16:48:50+0800 pre_total=5735 pre_free=3184 pre_avail=-1 pre_free_spec=3323 pre_file=961 pre_anon=499 pre_swap_comp=677 pre_comp_occ=256
PHASE name=ballast_start run=2 ts=2026-10-10T16:48:50+0800 ballast_mib=1200 hold_s=510 pre_total=5735 pre_free=3184 pre_avail=-1 pre_free_spec=3323 pre_file=961 pre_anon=499 pre_swap_comp=677 pre_comp_occ=256
PHASE name=ballast_filled run=2 ts=2026-10-10T16:48:52+0800 waited_s=2 marker=1 pre_total=5735 pre_free=1983 pre_avail=-1 pre_free_spec=2122 pre_file=961 pre_anon=1700 pre_swap_comp=677 pre_comp_occ=256
PHASE name=ballast_settling run=2 waited_s=32 plateau=0 free_mib=1973 swap_comp=675
PHASE name=ballast_settling run=2 waited_s=64 plateau=0 free_mib=1973 swap_comp=675
PHASE name=ballast_settling run=2 waited_s=96 plateau=0 free_mib=1972 swap_comp=675
PHASE name=ballast_settled run=2 ts=2026-10-10T16:50:55+0800 waited_s=120 plateau=0 pid=13231 pre_total=5735 pre_free=1975 pre_avail=-1 pre_free_spec=2121 pre_file=968 pre_anon=1703 pre_swap_comp=675 pre_comp_occ=255
REC run=2 size_mib=64 mode=touch ballast_mib=1200 mmap_us=11.0 fill_ms=12.729 unit=3107.7 unit_label=ns/page unmap_ms=4.474 rss_mib=66.36 faults=4096 majflt=0 pre_total=5735 pre_free=1973 pre_avail=-1 pre_free_spec=2118 pre_file=968 pre_anon=1704 pre_swap_comp=675 pre_comp_occ=255 post_total=5735 post_free=1910 post_avail=-1 post_free_spec=2055 post_file=968 post_anon=1768 post_swap_comp=675 post_comp_occ=255
PHASE name=ballast_released run=2 ts=2026-10-10T16:50:58+0800 post_total=5735 post_free=3174 post_avail=-1 post_free_spec=3319 post_file=968 post_anon=502 post_swap_comp=674 post_comp_occ=255
PHASE name=round_recovered run=2 ts=2026-10-10T16:51:00+0800 waited_s=2 free_mib=3170 target_mib=2897
SUMMARY runs=2 fill_ms_avg=13.914 fill_ms_min=12.729 fill_ms_max=15.099
Linux — Ubuntu · kernel 7.0.0 · aarch64 · touch 64M ×2 · ballast 2048M
case-sample-linux.txt 下载
CASE ts=2026-10-10T16:45:40+0800 platform=Linux kernel=Linux-7.0.0-34-generic arch=aarch64 size_mib=64 mode=touch runs=2 ballast_mib=2048 tool=mem_bench
PHASE name=case_start ts=2026-10-10T16:45:40+0800 pre_total=4895 pre_free=3061 pre_avail=4406 pre_free_spec=-1 pre_file=1333 pre_anon=98 pre_swap_comp=50 pre_comp_occ=-1
PHASE name=round_start run=1 ts=2026-10-10T16:45:40+0800 pre_total=4895 pre_free=3061 pre_avail=4406 pre_free_spec=-1 pre_file=1333 pre_anon=98 pre_swap_comp=50 pre_comp_occ=-1
PHASE name=ballast_start run=1 ts=2026-10-10T16:45:40+0800 ballast_mib=2048 hold_s=510 pre_total=4895 pre_free=3061 pre_avail=4406 pre_free_spec=-1 pre_file=1333 pre_anon=98 pre_swap_comp=50 pre_comp_occ=-1
PHASE name=ballast_filled run=1 ts=2026-10-10T16:45:48+0800 waited_s=8 marker=1 pre_total=4895 pre_free=1000 pre_avail=2346 pre_free_spec=-1 pre_file=1333 pre_anon=2146 pre_swap_comp=50 pre_comp_occ=-1
PHASE name=ballast_settled run=1 ts=2026-10-10T16:46:08+0800 waited_s=20 plateau=1 pid=175983 pre_total=4895 pre_free=1010 pre_avail=2355 pre_free_spec=-1 pre_file=1333 pre_anon=2146 pre_swap_comp=50 pre_comp_occ=-1
REC run=1 size_mib=64 mode=touch ballast_mib=2048 mmap_us=9.3 fill_ms=271.162 unit=16550.4 unit_label=ns/page unmap_ms=5.180 rss_mib=65.39 faults=16386 majflt=0 pre_total=4895 pre_free=1010 pre_avail=2356 pre_free_spec=-1 pre_file=1333 pre_anon=2146 pre_swap_comp=50 pre_comp_occ=-1 post_total=4895 post_free=951 post_avail=2297 post_free_spec=-1 post_file=1333 post_anon=2210 post_swap_comp=50 post_comp_occ=-1
PHASE name=ballast_released run=1 ts=2026-10-10T16:46:12+0800 post_total=4895 post_free=3020 post_avail=4366 post_free_spec=-1 post_file=1333 post_anon=98 post_swap_comp=50 post_comp_occ=-1
PHASE name=round_recovered run=1 ts=2026-10-10T16:46:13+0800 waited_s=2 free_mib=3029 target_mib=2448
PHASE name=round_start run=2 ts=2026-10-10T16:46:13+0800 pre_total=4895 pre_free=3029 pre_avail=4374 pre_free_spec=-1 pre_file=1333 pre_anon=98 pre_swap_comp=50 pre_comp_occ=-1
PHASE name=ballast_start run=2 ts=2026-10-10T16:46:13+0800 ballast_mib=2048 hold_s=510 pre_total=4895 pre_free=3029 pre_avail=4374 pre_free_spec=-1 pre_file=1333 pre_anon=98 pre_swap_comp=50 pre_comp_occ=-1
PHASE name=ballast_filled run=2 ts=2026-10-10T16:46:17+0800 waited_s=4 marker=1 pre_total=4895 pre_free=977 pre_avail=2323 pre_free_spec=-1 pre_file=1333 pre_anon=2146 pre_swap_comp=50 pre_comp_occ=-1
PHASE name=ballast_settled run=2 ts=2026-10-10T16:46:37+0800 waited_s=20 plateau=1 pid=176017 pre_total=4895 pre_free=1004 pre_avail=2350 pre_free_spec=-1 pre_file=1333 pre_anon=2146 pre_swap_comp=50 pre_comp_occ=-1
REC run=2 size_mib=64 mode=touch ballast_mib=2048 mmap_us=3.7 fill_ms=50.484 unit=3081.3 unit_label=ns/page unmap_ms=6.090 rss_mib=65.39 faults=16386 majflt=0 pre_total=4895 pre_free=1004 pre_avail=2350 pre_free_spec=-1 pre_file=1333 pre_anon=2146 pre_swap_comp=50 pre_comp_occ=-1 post_total=4895 post_free=941 post_avail=2287 post_free_spec=-1 post_file=1333 post_anon=2210 post_swap_comp=50 post_comp_occ=-1
PHASE name=ballast_released run=2 ts=2026-10-10T16:46:41+0800 post_total=4895 post_free=3031 post_avail=4377 post_free_spec=-1 post_file=1333 post_anon=98 post_swap_comp=50 post_comp_occ=-1
PHASE name=round_recovered run=2 ts=2026-10-10T16:46:43+0800 waited_s=2 free_mib=3037 target_mib=2448
SUMMARY runs=2 fill_ms_avg=160.823 fill_ms_min=50.484 fill_ms_max=271.162
macOS — Darwin 25.4 · arm64 · touch 64M ×2 · ballast 1024M
case-sample-macos.txt 下载
CASE ts=2026-10-10T16:34:40+0800 platform=Darwin kernel=Darwin-25.4.0 arch=arm64 size_mib=64 mode=touch runs=2 ballast_mib=1024 tool=mem_bench
PHASE name=case_start ts=2026-10-10T16:34:40+0800 pre_total=8192 pre_free=63 pre_avail=-1 pre_free_spec=67 pre_file=628 pre_anon=1426 pre_swap_comp=32784 pre_comp_occ=2476
PHASE name=round_start run=1 ts=2026-10-10T16:34:40+0800 pre_total=8192 pre_free=63 pre_avail=-1 pre_free_spec=67 pre_file=628 pre_anon=1426 pre_swap_comp=32784 pre_comp_occ=2476
PHASE name=ballast_start run=1 ts=2026-10-10T16:34:40+0800 ballast_mib=1024 hold_s=510 pre_total=8192 pre_free=63 pre_avail=-1 pre_free_spec=67 pre_file=628 pre_anon=1426 pre_swap_comp=32784 pre_comp_occ=2476
PHASE name=ballast_filled run=1 ts=2026-10-10T16:34:42+0800 waited_s=2 marker=1 pre_total=8192 pre_free=103 pre_avail=-1 pre_free_spec=111 pre_file=634 pre_anon=1139 pre_swap_comp=34004 pre_comp_occ=2719
PHASE name=ballast_settling run=1 waited_s=32 plateau=0 free_mib=83 swap_comp=34119
PHASE name=ballast_settling run=1 waited_s=64 plateau=0 free_mib=78 swap_comp=33474
PHASE name=ballast_settling run=1 waited_s=96 plateau=0 free_mib=75 swap_comp=33963
PHASE name=ballast_settled run=1 ts=2026-10-10T16:36:43+0800 waited_s=120 plateau=0 pid=18196 pre_total=8192 pre_free=86 pre_avail=-1 pre_free_spec=87 pre_file=633 pre_anon=1856 pre_swap_comp=33056 pre_comp_occ=2044
REC run=1 size_mib=64 mode=touch ballast_mib=1024 mmap_us=1.0 fill_ms=5.672 unit=1384.8 unit_label=ns/page unmap_ms=3.226 rss_mib=65.30 faults=4096 majflt=0 pre_total=8192 pre_free=80 pre_avail=-1 pre_free_spec=81 pre_file=633 pre_anon=1858 pre_swap_comp=33054 pre_comp_occ=2044 post_total=8192 post_free=86 post_avail=-1 post_free_spec=87 post_file=634 post_anon=1751 post_swap_comp=33223 post_comp_occ=2148
PHASE name=ballast_released run=1 ts=2026-10-10T16:36:46+0800 post_total=8192 post_free=718 post_avail=-1 post_free_spec=719 post_file=634 post_anon=1694 post_swap_comp=32191 post_comp_occ=1568
PHASE name=round_recovered run=1 ts=2026-10-10T16:36:48+0800 waited_s=2 free_mib=683 target_mib=100
PHASE name=round_start run=2 ts=2026-10-10T16:36:48+0800 pre_total=8192 pre_free=683 pre_avail=-1 pre_free_spec=686 pre_file=635 pre_anon=1713 pre_swap_comp=32177 pre_comp_occ=1564
PHASE name=ballast_start run=2 ts=2026-10-10T16:36:48+0800 ballast_mib=1024 hold_s=510 pre_total=8192 pre_free=683 pre_avail=-1 pre_free_spec=686 pre_file=635 pre_anon=1713 pre_swap_comp=32177 pre_comp_occ=1564
PHASE name=ballast_filled run=2 ts=2026-10-10T16:36:50+0800 waited_s=2 marker=1 pre_total=8192 pre_free=93 pre_avail=-1 pre_free_spec=96 pre_file=635 pre_anon=2076 pre_swap_comp=32792 pre_comp_occ=1813
PHASE name=ballast_settling run=2 waited_s=32 plateau=0 free_mib=60 swap_comp=33541
PHASE name=ballast_settling run=2 waited_s=64 plateau=0 free_mib=73 swap_comp=33586
PHASE name=ballast_settling run=2 waited_s=96 plateau=0 free_mib=77 swap_comp=33601
PHASE name=ballast_settled run=2 ts=2026-10-10T16:38:51+0800 waited_s=120 plateau=0 pid=18561 pre_total=8192 pre_free=66 pre_avail=-1 pre_free_spec=68 pre_file=714 pre_anon=1457 pre_swap_comp=33448 pre_comp_occ=2367
REC run=2 size_mib=64 mode=touch ballast_mib=1024 mmap_us=1.0 fill_ms=6.587 unit=1608.2 unit_label=ns/page unmap_ms=1.860 rss_mib=65.30 faults=4096 majflt=0 pre_total=8192 pre_free=54 pre_avail=-1 pre_free_spec=56 pre_file=714 pre_anon=1457 pre_swap_comp=33448 pre_comp_occ=2366 post_total=8192 post_free=66 post_avail=-1 post_free_spec=67 post_file=715 post_anon=1422 post_swap_comp=33542 post_comp_occ=2412
PHASE name=ballast_released run=2 ts=2026-10-10T16:38:54+0800 post_total=8192 post_free=710 post_avail=-1 post_free_spec=773 post_file=795 post_anon=1146 post_swap_comp=32747 post_comp_occ=1966
PHASE name=round_recovered run=2 ts=2026-10-10T16:38:56+0800 waited_s=2 free_mib=645 target_mib=100
SUMMARY runs=2 fill_ms_avg=6.129 fill_ms_min=5.672 fill_ms_max=6.587

500M 规模复测 — iOS/Linux × 轻载/重载 四方对比

与 200M 标准示例同一套采集流程,仅 -s 500M(ballast 比例不变)。 版本说明:本组数据采集于 v2 协议(单次加压多轮测量,重载各轮初始条件有漂移——正好可以在 pre_* 表里观察到 run1 冷/后续热的现象);REC 行格式与现行 v3 完全一致,可直接对比。 重点看 [post_*] 与 [驻留开销]:500M 下 iOS 重载的压缩器真正参战 (memset 单轮 swap_comp +117),冷启动轮的 faults 超出恒等值 (43255 vs 32000)——回收路径缺页噪声被缺页指标直接抓到。

touch — 四方对比(iOS 轻/重 + Linux 轻/重)
compare-500m-touch.txt 下载
COMPARE files=4 base=case-ios-light-touch.txt size_mib=500 mode=touch

[耗时表]
file                          ballast runs  fill_ms avg/min/max                  unit  mmap_us unmap_ms   faults
----------------------------------------------------------------------------------------------------------------
case-ios-light-touch.txt            0    5   92.34 /  89.81 /  93.57      2885.6 ns/page     16.4     27.9    32000
case-ios-heavy-touch.txt         3600    5  123.74 /  96.09 / 208.42      3866.7 ns/page     12.6     28.0    34251
case-linux-light-touch.txt          0    5  405.94 / 229.88 / 1076.41      3171.4 ns/page      5.4     35.1   128002
case-linux-heavy-touch.txt       2400    5  493.11 / 228.97 / 1126.79      3852.4 ns/page     17.5     28.7   128002

[pre_* 申请前快照] (每轮申请时刻前均值, MiB; N/A=平台无此指标)
指标             case-ios-light-touch.txt  case-ios-heavy-touch.txt  case-linux-light-touch.txt  case-linux-heavy-touch.txt  Δ:case-ios-hea  Δ:case-linux-l  Δ:case-linux-h
-------------------------------------------------------------------------------------------------------------------------------------------------------------------------
total                            5735.0                    5735.0                      4895.0                      4895.0           +0.0%          -14.6%          -14.6%
free                             3805.0                     533.2                      3065.2                       665.2          -86.0%          -19.4%          -82.5%
avail                               N/A                       N/A                      4342.0                      1967.8               -               -               -
free_spec                        3892.4                     554.8                         N/A                         N/A          -85.7%               -               -
file                              574.0                     496.2                      1316.0                      1316.0          -13.6%         +129.3%         +129.3%
anon                              457.6                    3760.6                       100.8                      2504.2         +721.8%          -78.0%         +447.2%
swap_comp                         306.0                     422.4                        50.0                        50.0          +38.0%          -83.7%          -83.7%
comp_occ                          111.0                     189.4                         N/A                         N/A          +70.6%               -               -

[post_* 申请驻留时快照] (fill 后驻留期均值, MiB)
指标             case-ios-light-touch.txt  case-ios-heavy-touch.txt  case-linux-light-touch.txt  case-linux-heavy-touch.txt  Δ:case-ios-hea  Δ:case-linux-l  Δ:case-linux-h
-------------------------------------------------------------------------------------------------------------------------------------------------------------------------
total                            5735.0                    5735.0                      4895.0                      4895.0           +0.0%          -14.6%          -14.6%
free                             3302.8                      94.8                      2601.8                       197.0          -97.1%          -21.2%          -94.0%
avail                               N/A                       N/A                      3878.8                      1497.2               -               -               -
free_spec                        3390.6                      99.4                         N/A                         N/A          -97.1%               -               -
file                              574.0                     479.8                      1316.0                      1316.0          -16.4%         +129.3%         +129.3%
anon                              959.4                    4191.0                       592.0                      3000.4         +336.8%          -38.3%         +212.7%
swap_comp                         306.0                     450.4                        50.0                        50.0          +47.2%          -83.7%          -83.7%
comp_occ                          111.0                     208.0                         N/A                         N/A          +87.4%               -               -

[驻留开销 post−pre] (申请 SIZE 对系统状态的净影响, MiB)
指标             case-ios-light-touch.txt  case-ios-heavy-touch.txt  case-linux-light-touch.txt  case-linux-heavy-touch.txt
-------------------------------------------------------------------------------------------------------------------------
total                              +0.0                      +0.0                        +0.0                        +0.0
free                             -502.2                    -438.4                      -463.4                      -468.2
avail                                 -                         -                      -463.2                      -470.6
free_spec                        -501.8                    -455.4                           -                           -
file                               +0.0                     -16.4                        +0.0                        +0.0
anon                             +501.8                    +430.4                      +491.2                      +496.2
swap_comp                          +0.0                     +28.0                        +0.0                        +0.0
comp_occ                           +0.0                     +18.6                           -                           -

DELTA case-ios-heavy-touch.txt     vs base: fill_ms=+34.0% unit=+34.0% unmap_ms=+0.4% faults=+7.0% pre_free=-86.0%
DELTA case-linux-light-touch.txt   vs base: fill_ms=+339.6% unit=+9.9% unmap_ms=+25.5% faults=+300.0% pre_free=-19.4%
DELTA case-linux-heavy-touch.txt   vs base: fill_ms=+434.0% unit=+33.5% unmap_ms=+2.7% faults=+300.0% pre_free=-82.5%

# 机器可读行 (key=value, 与用例记录同风格):
ROW file=case-ios-light-touch.txt size_mib=500 mode=touch ballast_mib=0 runs=5 fill_ms_avg=92.341 fill_ms_min=89.812 fill_ms_max=93.567 unit_avg=2885.6 unit_label=ns/page mmap_us_avg=16.4 unmap_ms_avg=27.939 faults_avg=32000.0 majflt_avg=0.0 pre_total_avg=5735.0 pre_free_avg=3805.0 pre_avail_avg=-1 pre_free_spec_avg=3892.4 pre_file_avg=574.0 pre_anon_avg=457.6 pre_swap_comp_avg=306.0 pre_comp_occ_avg=111.0 post_total_avg=5735.0 post_free_avg=3302.8 post_avail_avg=-1 post_free_spec_avg=3390.6 post_file_avg=574.0 post_anon_avg=959.4 post_swap_comp_avg=306.0 post_comp_occ_avg=111.0
ROW file=case-ios-heavy-touch.txt size_mib=500 mode=touch ballast_mib=3600 runs=5 fill_ms_avg=123.736 fill_ms_min=96.092 fill_ms_max=208.423 unit_avg=3866.7 unit_label=ns/page mmap_us_avg=12.6 unmap_ms_avg=28.040 faults_avg=34251.0 majflt_avg=0.0 pre_total_avg=5735.0 pre_free_avg=533.2 pre_avail_avg=-1 pre_free_spec_avg=554.8 pre_file_avg=496.2 pre_anon_avg=3760.6 pre_swap_comp_avg=422.4 pre_comp_occ_avg=189.4 post_total_avg=5735.0 post_free_avg=94.8 post_avail_avg=-1 post_free_spec_avg=99.4 post_file_avg=479.8 post_anon_avg=4191.0 post_swap_comp_avg=450.4 post_comp_occ_avg=208.0
ROW file=case-linux-light-touch.txt size_mib=500 mode=touch ballast_mib=0 runs=5 fill_ms_avg=405.937 fill_ms_min=229.879 fill_ms_max=1076.411 unit_avg=3171.4 unit_label=ns/page mmap_us_avg=5.4 unmap_ms_avg=35.064 faults_avg=128002.0 majflt_avg=0.0 pre_total_avg=4895.0 pre_free_avg=3065.2 pre_avail_avg=4342.0 pre_free_spec_avg=-1 pre_file_avg=1316.0 pre_anon_avg=100.8 pre_swap_comp_avg=50.0 pre_comp_occ_avg=-1 post_total_avg=4895.0 post_free_avg=2601.8 post_avail_avg=3878.8 post_free_spec_avg=-1 post_file_avg=1316.0 post_anon_avg=592.0 post_swap_comp_avg=50.0 post_comp_occ_avg=-1
ROW file=case-linux-heavy-touch.txt size_mib=500 mode=touch ballast_mib=2400 runs=5 fill_ms_avg=493.113 fill_ms_min=228.968 fill_ms_max=1126.786 unit_avg=3852.4 unit_label=ns/page mmap_us_avg=17.5 unmap_ms_avg=28.688 faults_avg=128002.0 majflt_avg=0.0 pre_total_avg=4895.0 pre_free_avg=665.2 pre_avail_avg=1967.8 pre_free_spec_avg=-1 pre_file_avg=1316.0 pre_anon_avg=2504.2 pre_swap_comp_avg=50.0 pre_comp_occ_avg=-1 post_total_avg=4895.0 post_free_avg=197.0 post_avail_avg=1497.2 post_free_spec_avg=-1 post_file_avg=1316.0 post_anon_avg=3000.4 post_swap_comp_avg=50.0 post_comp_occ_avg=-1
DELTA file=case-ios-heavy-touch.txt vs=case-ios-light-touch.txt fill_ms_pct=+34.0% unit_pct=+34.0% unmap_ms_pct=+0.4% faults_pct=+7.0% pre_total_pct=+0.0% pre_free_pct=-86.0% pre_free_spec_pct=-85.7% pre_file_pct=-13.6% pre_anon_pct=+721.8% pre_swap_comp_pct=+38.0% pre_comp_occ_pct=+70.6% post_total_pct=+0.0% post_free_pct=-97.1% post_free_spec_pct=-97.1% post_file_pct=-16.4% post_anon_pct=+336.8% post_swap_comp_pct=+47.2% post_comp_occ_pct=+87.4%
DELTA file=case-linux-light-touch.txt vs=case-ios-light-touch.txt fill_ms_pct=+339.6% unit_pct=+9.9% unmap_ms_pct=+25.5% faults_pct=+300.0% pre_total_pct=-14.6% pre_free_pct=-19.4% pre_file_pct=+129.3% pre_anon_pct=-78.0% pre_swap_comp_pct=-83.7% post_total_pct=-14.6% post_free_pct=-21.2% post_file_pct=+129.3% post_anon_pct=-38.3% post_swap_comp_pct=-83.7%
DELTA file=case-linux-heavy-touch.txt vs=case-ios-light-touch.txt fill_ms_pct=+434.0% unit_pct=+33.5% unmap_ms_pct=+2.7% faults_pct=+300.0% pre_total_pct=-14.6% pre_free_pct=-82.5% pre_file_pct=+129.3% pre_anon_pct=+447.2% pre_swap_comp_pct=-83.7% post_total_pct=-14.6% post_free_pct=-94.0% post_file_pct=+129.3% post_anon_pct=+212.7% post_swap_comp_pct=-83.7%
memset — 四方对比
compare-500m-memset.txt 下载
COMPARE files=4 base=case-ios-light-memset.txt size_mib=500 mode=memset

[耗时表]
file                          ballast runs  fill_ms avg/min/max                  unit  mmap_us unmap_ms   faults
----------------------------------------------------------------------------------------------------------------
case-ios-light-memset.txt           0    5  113.80 / 110.55 / 115.79      4395.0 MiB/s       13.0     28.4    32000
case-ios-heavy-memset.txt        3600    5  143.33 / 115.93 / 236.68      3757.4 MiB/s       12.8     25.9    33235
case-linux-light-memset.txt         0    5  342.09 / 313.05 / 393.69      1471.3 MiB/s        7.9     34.2   128002
case-linux-heavy-memset.txt      2400    5  295.23 / 257.30 / 375.77      1726.7 MiB/s        9.3     29.3   128002

[pre_* 申请前快照] (每轮申请时刻前均值, MiB; N/A=平台无此指标)
指标             case-ios-light-memset.txt  case-ios-heavy-memset.txt  case-linux-light-memset.txt  case-linux-heavy-memset.txt  Δ:case-ios-hea  Δ:case-linux-l  Δ:case-linux-h
-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------
total                             5735.0                     5735.0                       4895.0                       4895.0           +0.0%          -14.6%          -14.6%
free                              3805.4                      509.6                       3032.6                        650.6          -86.6%          -20.3%          -82.9%
avail                                N/A                        N/A                       4309.4                       1959.6               -               -               -
free_spec                         3893.2                      565.6                          N/A                          N/A          -85.5%               -               -
file                               574.4                      572.8                       1316.0                       1316.0           -0.3%         +129.1%         +129.1%
anon                               457.4                     3547.6                        104.0                       2505.0         +675.6%          -77.3%         +447.7%
swap_comp                          306.0                      680.0                         50.0                         50.0         +122.2%          -83.7%          -83.7%
comp_occ                           111.0                      328.8                          N/A                          N/A         +196.2%               -               -

[post_* 申请驻留时快照] (fill 后驻留期均值, MiB)
指标             case-ios-light-memset.txt  case-ios-heavy-memset.txt  case-linux-light-memset.txt  case-linux-heavy-memset.txt  Δ:case-ios-hea  Δ:case-linux-l  Δ:case-linux-h
-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------
total                             5735.0                     5735.0                       4895.0                       4895.0           +0.0%          -14.6%          -14.6%
free                              3302.6                       84.4                       2541.4                        170.4          -97.4%          -23.0%          -94.8%
avail                                N/A                        N/A                       3818.4                       1478.8               -               -               -
free_spec                         3390.6                      117.2                          N/A                          N/A          -96.5%               -               -
file                               574.4                      556.2                       1316.0                       1316.0           -3.2%         +129.1%         +129.1%
anon                               960.2                     3926.4                        604.0                       3005.0         +308.9%          -37.1%         +213.0%
swap_comp                          306.0                      797.0                         50.0                         50.0         +160.5%          -83.7%          -83.7%
comp_occ                           111.0                      389.6                          N/A                          N/A         +251.0%               -               -

[驻留开销 post−pre] (申请 SIZE 对系统状态的净影响, MiB)
指标             case-ios-light-memset.txt  case-ios-heavy-memset.txt  case-linux-light-memset.txt  case-linux-heavy-memset.txt
-----------------------------------------------------------------------------------------------------------------------------
total                               +0.0                       +0.0                         +0.0                         +0.0
free                              -502.8                     -425.2                       -491.2                       -480.2
avail                                  -                          -                       -491.0                       -480.8
free_spec                         -502.6                     -448.4                            -                            -
file                                +0.0                      -16.6                         +0.0                         +0.0
anon                              +502.8                     +378.8                       +500.0                       +500.0
swap_comp                           +0.0                     +117.0                         +0.0                         +0.0
comp_occ                            +0.0                      +60.8                            -                            -

DELTA case-ios-heavy-memset.txt    vs base: fill_ms=+26.0% unit=-14.5% unmap_ms=-8.7% faults=+3.9% pre_free=-86.6%
DELTA case-linux-light-memset.txt  vs base: fill_ms=+200.6% unit=-66.5% unmap_ms=+20.6% faults=+300.0% pre_free=-20.3%
DELTA case-linux-heavy-memset.txt  vs base: fill_ms=+159.4% unit=-60.7% unmap_ms=+3.1% faults=+300.0% pre_free=-82.9%

# 机器可读行 (key=value, 与用例记录同风格):
ROW file=case-ios-light-memset.txt size_mib=500 mode=memset ballast_mib=0 runs=5 fill_ms_avg=113.801 fill_ms_min=110.548 fill_ms_max=115.789 unit_avg=4395.0 unit_label=MiB/s mmap_us_avg=13.0 unmap_ms_avg=28.387 faults_avg=32000.0 majflt_avg=0.0 pre_total_avg=5735.0 pre_free_avg=3805.4 pre_avail_avg=-1 pre_free_spec_avg=3893.2 pre_file_avg=574.4 pre_anon_avg=457.4 pre_swap_comp_avg=306.0 pre_comp_occ_avg=111.0 post_total_avg=5735.0 post_free_avg=3302.6 post_avail_avg=-1 post_free_spec_avg=3390.6 post_file_avg=574.4 post_anon_avg=960.2 post_swap_comp_avg=306.0 post_comp_occ_avg=111.0
ROW file=case-ios-heavy-memset.txt size_mib=500 mode=memset ballast_mib=3600 runs=5 fill_ms_avg=143.334 fill_ms_min=115.927 fill_ms_max=236.678 unit_avg=3757.4 unit_label=MiB/s mmap_us_avg=12.8 unmap_ms_avg=25.920 faults_avg=33235.0 majflt_avg=0.0 pre_total_avg=5735.0 pre_free_avg=509.6 pre_avail_avg=-1 pre_free_spec_avg=565.6 pre_file_avg=572.8 pre_anon_avg=3547.6 pre_swap_comp_avg=680.0 pre_comp_occ_avg=328.8 post_total_avg=5735.0 post_free_avg=84.4 post_avail_avg=-1 post_free_spec_avg=117.2 post_file_avg=556.2 post_anon_avg=3926.4 post_swap_comp_avg=797.0 post_comp_occ_avg=389.6
ROW file=case-linux-light-memset.txt size_mib=500 mode=memset ballast_mib=0 runs=5 fill_ms_avg=342.095 fill_ms_min=313.051 fill_ms_max=393.690 unit_avg=1471.3 unit_label=MiB/s mmap_us_avg=7.9 unmap_ms_avg=34.227 faults_avg=128002.0 majflt_avg=0.0 pre_total_avg=4895.0 pre_free_avg=3032.6 pre_avail_avg=4309.4 pre_free_spec_avg=-1 pre_file_avg=1316.0 pre_anon_avg=104.0 pre_swap_comp_avg=50.0 pre_comp_occ_avg=-1 post_total_avg=4895.0 post_free_avg=2541.4 post_avail_avg=3818.4 post_free_spec_avg=-1 post_file_avg=1316.0 post_anon_avg=604.0 post_swap_comp_avg=50.0 post_comp_occ_avg=-1
ROW file=case-linux-heavy-memset.txt size_mib=500 mode=memset ballast_mib=2400 runs=5 fill_ms_avg=295.229 fill_ms_min=257.298 fill_ms_max=375.771 unit_avg=1726.7 unit_label=MiB/s mmap_us_avg=9.3 unmap_ms_avg=29.280 faults_avg=128002.0 majflt_avg=0.0 pre_total_avg=4895.0 pre_free_avg=650.6 pre_avail_avg=1959.6 pre_free_spec_avg=-1 pre_file_avg=1316.0 pre_anon_avg=2505.0 pre_swap_comp_avg=50.0 pre_comp_occ_avg=-1 post_total_avg=4895.0 post_free_avg=170.4 post_avail_avg=1478.8 post_free_spec_avg=-1 post_file_avg=1316.0 post_anon_avg=3005.0 post_swap_comp_avg=50.0 post_comp_occ_avg=-1
DELTA file=case-ios-heavy-memset.txt vs=case-ios-light-memset.txt fill_ms_pct=+26.0% unit_pct=-14.5% unmap_ms_pct=-8.7% faults_pct=+3.9% pre_total_pct=+0.0% pre_free_pct=-86.6% pre_free_spec_pct=-85.5% pre_file_pct=-0.3% pre_anon_pct=+675.6% pre_swap_comp_pct=+122.2% pre_comp_occ_pct=+196.2% post_total_pct=+0.0% post_free_pct=-97.4% post_free_spec_pct=-96.5% post_file_pct=-3.2% post_anon_pct=+308.9% post_swap_comp_pct=+160.5% post_comp_occ_pct=+251.0%
DELTA file=case-linux-light-memset.txt vs=case-ios-light-memset.txt fill_ms_pct=+200.6% unit_pct=-66.5% unmap_ms_pct=+20.6% faults_pct=+300.0% pre_total_pct=-14.6% pre_free_pct=-20.3% pre_file_pct=+129.1% pre_anon_pct=-77.3% pre_swap_comp_pct=-83.7% post_total_pct=-14.6% post_free_pct=-23.0% post_file_pct=+129.1% post_anon_pct=-37.1% post_swap_comp_pct=-83.7%
DELTA file=case-linux-heavy-memset.txt vs=case-ios-light-memset.txt fill_ms_pct=+159.4% unit_pct=-60.7% unmap_ms_pct=+3.1% faults_pct=+300.0% pre_total_pct=-14.6% pre_free_pct=-82.9% pre_file_pct=+129.1% pre_anon_pct=+447.7% pre_swap_comp_pct=-83.7% post_total_pct=-14.6% post_free_pct=-94.8% post_file_pct=+129.1% post_anon_pct=+213.0% post_swap_comp_pct=-83.7%
验证矩阵:iOS (iPhone13,3, dash, 设备端 ldid 签名) ✓ · Linux (Ubuntu aarch64, gcc 15.2) ✓ · macOS (bash 3.2 /bin/sh + zsh) ✓ —— 三平台的 ballast 生命周期、双模式解析、平台差异键(-1)均逐项核对通过。

脚本源码(用例全量归档)

用例涉及的 5 个文件全部在此,按用例数据流排列: run_case.sh(采集 harness)调用 mem_bench(测量工具)与 mem_stress(重载 ballast 工具), 由 Makefile 编译后两者,采集文件交给 compare_cases.py(对比工具)。本页自包含,无需跳转。

run_case.sh采集 harness · 纯 POSIX sh · 约 200 行 下载
#!/bin/sh
# run_case.sh — 轻重载"申请耗时"标准用例 harness (iOS / macOS / Linux 通用)
#
# 用法:
#   ./run_case.sh -s <N M> -m <touch|memset> -r <runs> [-b <ballast_mib>] [-w settle_s] [-t hold_s]
# 示例:
#   ./run_case.sh -s 200M -m touch  -r 5              # 轻载: 200M 纯申请 x 5 轮
#   ./run_case.sh -s 200M -m memset -r 5              # 轻载: 200M 申请+写入 x 5 轮
#   ./run_case.sh -s 200M -m touch  -r 5 -b 3600      # 重载: 自动建 3600MiB ballast, 测完自动释放
#
# 输出: 约束格式记录行 CASE/PHASE/REC/SUMMARY (key=value 令牌, 见 protocol.html)
#   REC = 耗时 (mmap_us/fill_ms/unmap_ms) + 缺页 (faults/majflt) + rss
#       + 申请前 (pre_*) 与申请驻留时 (post_*) 的内存统计
#   post_*: 每轮 fill 后 mem_bench 保持驻留 HOLD_FILL 秒, 驻留期采样
#   重载 (-b>0): 每轮独立完整加压生命周期 (安全三件套: 限速填充/孤儿防护/稳态判定)
#       round_start → ballast_start → ballast_filled(标记) → ballast_settled
#       → REC → ballast_released → round_recovered (恢复到基准 free 80% 再进下一轮)
#       · -F 限速: 填充期间 free < 地板(页)即暂停, 让回收跟得上 (防 jetsam/panic)
#       · 孤儿防护: 发现存活 mem_stress 立即 exit 5 (防双重加压)
#       · 稳态判定: settled = free 与 swap_comp 双稳定, 不收敛则 SETTLE_MAX 封顶
#
# 依赖: 同目录 mem_bench / mem_stress; 内存源 (Darwin: vm_stat+sysctl / Linux: /proc/meminfo)
# 约束: 纯 POSIX sh (dash/ash/bash/zsh), 不依赖 awk/bc/python — iOS 精简用户态可跑

SIZE=200M; MODE=touch; RUNS=5; BALLAST=0; SETTLE=20; HOLD=0
HOLD_FILL=3    # 每轮 fill 后驻留保持秒数 (post_* 采样窗口)
SETTLE_MAX=120 # 稳态判定上限秒数 (settled=free+swap_comp 双稳定, 见循环内注释)
RECOVER=30     # 轮间恢复上限秒数 (等 free 回到基准 80%)
FILL_MAX=300   # ballast 填充等待上限秒数 (限速填充受回收速率约束)
FLOOR_PAGES=2000 # ballast 限速地板 (系统 free 页数; iOS 16K页=31MiB ≈ free_target)
DIR=$(dirname "$0")
BENCH="$DIR/mem_bench"; STRESS="$DIR/mem_stress"
BPID=""; BCPID=""
TMPB=/tmp/.run_case.bench.$$
MARKER=/tmp/.run_case.ballast.$$.filled

usage() { sed -n '3,19p' "$0"; exit 2; }
while getopts "s:m:r:b:w:t:h" opt 2>/dev/null; do
    case $opt in
        s) SIZE=$OPTARG ;; m) MODE=$OPTARG ;; r) RUNS=$OPTARG ;;
        b) BALLAST=$OPTARG ;; w) SETTLE=$OPTARG ;; t) HOLD=$OPTARG ;;
        h) usage ;; *) usage ;;
    esac
done

# ---- 参数校验 (size 仅支持整 MiB) ----
case $SIZE in ''|*[!0-9M]*) echo "ERROR code=bad_size size=$SIZE (仅整 MiB, 如 200M)"; exit 2 ;; esac
SIZE_MIB=${SIZE%M}
case $SIZE_MIB in ''|*[!0-9]*) echo "ERROR code=bad_size size=$SIZE"; exit 2 ;; esac
case $MODE in touch|memset) ;; *) echo "ERROR code=bad_mode mode=$MODE (touch|memset)"; exit 2 ;; esac
case $RUNS in ''|*[!0-9]*|0) echo "ERROR code=bad_runs runs=$RUNS"; exit 2 ;; esac
case $BALLAST in ''|*[!0-9]*) echo "ERROR code=bad_ballast ballast=$BALLAST"; exit 2 ;; esac
[ -x "$BENCH" ] || { echo "ERROR code=no_bench path=$BENCH"; exit 2; }
if [ "$BALLAST" -gt 0 ]; then [ -x "$STRESS" ] || { echo "ERROR code=no_stress path=$STRESS"; exit 2; }; fi

ts() { date +%Y-%m-%dT%H:%M:%S%z; }

# ---- 平台检测 ----
KERNEL=$(uname -s)
case $KERNEL in Darwin|Linux) ;; *) echo "ERROR code=unsupported_kernel kernel=$KERNEL"; exit 2 ;; esac
ARCH=$(uname -m)
PAGE_BYTES=0; TOTAL_MIB=-1

if [ "$KERNEL" = "Darwin" ]; then
    # vm_stat 首行固定为 "Mach Virtual Memory Statistics: (page size of NNNN bytes)"
    _hdr=$(vm_stat 2>/dev/null | { IFS= read -r _l; echo "$_l"; })
    PAGE_BYTES=0
    for _t in $_hdr; do
        case $_t in
            [0-9]*) PAGE_BYTES=$_t ;;
        esac
    done
    [ "$PAGE_BYTES" -gt 0 ] 2>/dev/null || PAGE_BYTES=16384
    _ms=$(sysctl -n hw.memsize 2>/dev/null) && TOTAL_MIB=$((_ms / 1048576))
fi

kib_pages_to_mib() { echo $(( $1 * PAGE_BYTES / 1048576 )); }

# ---- 统一内存快照 -> R_* (整 MiB, -1 = 平台无此指标) ----
snap_mem() {
    R_TOTAL=$TOTAL_MIB; R_AVAIL=-1; R_FREE_SPEC=-1; R_COMP_OCC=-1
    if [ "$KERNEL" = "Linux" ]; then
        R_FREE=-1; R_FILE=-1; R_ANON=-1; R_SWAP_COMP=-1
        [ -r /proc/meminfo ] || return 0
        _t=0 _f=0 _a=0 _c=0 _an=0 _st=0 _sf=0
        while IFS=' :' read -r k v rest; do
            case $k in
                MemTotal) _t=$v ;; MemFree) _f=$v ;; MemAvailable) _a=$v ;;
                Cached) _c=$v ;; AnonPages) _an=$v ;;
                SwapTotal) _st=$v ;; SwapFree) _sf=$v ;;
            esac
        done < /proc/meminfo
        R_TOTAL=$((_t / 1024)); R_FREE=$((_f / 1024)); R_AVAIL=$((_a / 1024))
        R_FILE=$((_c / 1024)); R_ANON=$((_an / 1024))
        R_SWAP_COMP=$(( (_st - _sf) / 1024 ))
    else
        vm_stat > /tmp/.run_case.$$ 2>/dev/null || { R_FREE=-1; R_FILE=-1; R_ANON=-1; R_SWAP_COMP=-1; return 0; }
        _pf=0 _psp=0 _pfb=0 _pan=0 _psc=0 _poc=0
        while IFS= read -r line; do
            case $line in
                "Pages free:"*)                   set -- $line; _pf=${3%.}  ;;
                "Pages speculative:"*)            set -- $line; _psp=${3%.} ;;
                "File-backed pages:"*)            set -- $line; _pfb=${3%.} ;;
                "Anonymous pages:"*)              set -- $line; _pan=${3%.} ;;
                "Pages stored in compressor:"*)   set -- $line; _psc=${5%.} ;;
                "Pages occupied by compressor:"*) set -- $line; _poc=${5%.} ;;
            esac
        done < /tmp/.run_case.$$
        rm -f /tmp/.run_case.$$
        R_FREE=$(kib_pages_to_mib $_pf)
        R_FREE_SPEC=$(kib_pages_to_mib $((_pf + _psp)))
        R_FILE=$(kib_pages_to_mib $_pfb)
        R_ANON=$(kib_pages_to_mib $_pan)
        R_SWAP_COMP=$(kib_pages_to_mib $_psc)
        R_COMP_OCC=$(kib_pages_to_mib $_poc)
    fi
}

mem_tokens() { # $1=前缀(pre/post)
    printf '%s_total=%s %s_free=%s %s_avail=%s %s_free_spec=%s %s_file=%s %s_anon=%s %s_swap_comp=%s %s_comp_occ=%s' \
        "$1" "$R_TOTAL" "$1" "$R_FREE" "$1" "$R_AVAIL" "$1" "$R_FREE_SPEC" \
        "$1" "$R_FILE" "$1" "$R_ANON" "$1" "$R_SWAP_COMP" "$1" "$R_COMP_OCC"
}

cleanup() {
    rc=$?
    trap - EXIT INT TERM
    if [ -n "$BCPID" ]; then
        kill "$BCPID" 2>/dev/null; wait "$BCPID" 2>/dev/null; BCPID=""
    fi
    if [ -n "$BPID" ]; then
        kill "$BPID" 2>/dev/null; wait "$BPID" 2>/dev/null; BPID=""
        snap_mem
        printf 'PHASE name=ballast_released ts=%s ' "$(ts)"; mem_tokens post; echo
    fi
    rm -f /tmp/.run_case.$$ "$TMPB" "$MARKER"
    exit "$rc"
}
trap cleanup EXIT INT TERM HUP   # HUP: SSH 通道断连时 sshd 会 SIGHUP 会话 — 必须清理 ballast

# 浮点(<=3位小数) -> 毫单位整数: "35.096"->35096, "12.0"->12000
to_milli() {
    _i=${1%%.*}; _f=${1#*.}
    [ "$_f" = "$1" ] && _f=""
    case $_f in
        "") _f=000 ;; ?) _f=${_f}00 ;; ??) _f=${_f}0 ;; ???) ;; *) _f=${_f%${_f#???}} ;;
    esac
    printf '%s%s' "$_i" "$_f"
}
fmt3() { printf '%d.%03d' $(($1 / 1000)) $(($1 % 1000)); }

# ---- 开始 ----
printf 'CASE ts=%s platform=%s kernel=%s arch=%s size_mib=%s mode=%s runs=%s ballast_mib=%s tool=mem_bench\n' \
    "$(ts)" "$KERNEL" "$KERNEL-$(uname -r)" "$ARCH" "$SIZE_MIB" "$MODE" "$RUNS" "$BALLAST"
snap_mem
printf 'PHASE name=case_start ts=%s ' "$(ts)"; mem_tokens pre; echo

# ---- 测量循环: 重载时每轮独立完整加压生命周期 ----
#   (round_start → ballast_start → settle → REC → ballast_released → round_recovered)
#   每轮重新建压并恢复到基准 free 的 80% 再进下一轮, 保证各轮初始压力条件一致;
#   轻载 (ballast=0) 无加压, 轮间仅 sleep 1。
_sum=0; _min=999999999; _max=0; _n=0
BASE_FREE=-1
i=1
while [ "$i" -le "$RUNS" ]; do
    if [ "$BALLAST" -gt 0 ]; then
        printf 'PHASE name=round_start run=%s ts=%s ' "$i" "$(ts)"; mem_tokens pre; echo
        [ "$BASE_FREE" -lt 0 ] && BASE_FREE=$R_FREE
        # 孤儿 ballast 防护: 发现存活 mem_stress 即中止, 避免双重加压 (jetsam/panic 风险)
        # (只认命令首字段为 */mem_stress 的进程, 排除恰好参数里含该字样的无关进程)
        _orph=""
        while read -r _p _tt _st _tm _cmd _rest; do
            case $_cmd in
                */mem_stress|mem_stress) _orph="${_orph}${_p}," ;;
            esac
        done <<EOF
$(ps ax 2>/dev/null | grep '[m]em_stress')
EOF
        if [ -n "$_orph" ]; then
            echo "PHASE name=ballast_blocked ts=$(ts) run=$i reason=existing_mem_stress pids=$_orph"
            echo "ERROR code=orphan_ballast run=$i pids=$_orph hint='清理上述进程后重跑'"
            exit 5
        fi
        # 每轮 ballast 死亡定时: HOLD=0 (默认) 自适应 = 填充预算+稳态上限+恢复上限+60s
        _dm=$HOLD
        [ "$_dm" -le 0 ] && _dm=$(( FILL_MAX + SETTLE_MAX + RECOVER + 60 ))
        rm -f "$MARKER"
        printf 'PHASE name=ballast_start run=%s ts=%s ballast_mib=%s hold_s=%s ' "$i" "$(ts)" "$BALLAST" "$_dm"; mem_tokens pre; echo
        # -F 自适应限速: 填充期间 free < 地板(页)即暂停, 防 jetsam 风暴/watchdog panic
        MEM_STRESS_MARKER="$MARKER" nohup "$STRESS" "${BALLAST}M" -P 50 -q -t "$_dm" \
            -F "$FLOOR_PAGES" >/dev/null 2>&1 &
        BPID=$!
        # 阶段一: 等填充完成 (marker 文件出现; 旧版 mem_stress 无标记则 FILL_MAX 超时后继续)
        # 心跳行每 30s 一条: 既留填充轨迹, 也让远端通道不会因长时间无输出而超时断连
        _fw=0
        while [ ! -f "$MARKER" ] && [ "$_fw" -lt "$FILL_MAX" ]; do
            sleep 2; _fw=$((_fw + 2))
            snap_mem
            if [ "$R_FREE" -lt 16 ]; then
                echo "PHASE name=ballast_abort ts=$(ts) run=$i reason=free_below_16MiB free_mib=$R_FREE"; exit 3
            fi
            kill -0 "$BPID" 2>/dev/null || { echo "PHASE name=ballast_abort ts=$(ts) run=$i reason=ballast_died"; exit 3; }
            [ $((_fw % 30)) -eq 0 ] && printf 'PHASE name=ballast_filling run=%s waited_s=%s free_mib=%s\n' "$i" "$_fw" "$R_FREE"
        done
        printf 'PHASE name=ballast_filled run=%s ts=%s waited_s=%s marker=%s ' "$i" "$(ts)" "$_fw" "$([ -f "$MARKER" ] && echo 1 || echo 0)"; mem_tokens pre; echo
        # 稳态判定: 至少 SETTLE 秒, 之后 free 与 swap_comp 均稳定(4s 窗口变化小)即 settled;
        # 上限 SETTLE_MAX 秒 (压缩器未收敛也继续, 状态如实记录在 PHASE 里)
        _waited=0; _pf=-1; _ps=-1; _plateau=0
        while :; do
            sleep 4; _waited=$((_waited + 4))
            snap_mem
            if [ "$R_FREE" -lt 16 ]; then
                echo "PHASE name=ballast_abort ts=$(ts) run=$i reason=free_below_16MiB free_mib=$R_FREE"; exit 3
            fi
            kill -0 "$BPID" 2>/dev/null || { echo "PHASE name=ballast_abort ts=$(ts) run=$i reason=ballast_died"; exit 3; }
            if [ "$_waited" -ge "$SETTLE" ]; then
                if [ "$_pf" -ge 0 ]; then
                    _df=$(( R_FREE - _pf )); [ "$_df" -lt 0 ] && _df=$((-_df))
                    if [ "$R_SWAP_COMP" -lt 0 ] || [ "$_ps" -lt 0 ]; then _ds=0
                    else _ds=$(( R_SWAP_COMP - _ps )); [ "$_ds" -lt 0 ] && _ds=$((-_ds)); fi
                    [ "$_df" -le 48 ] && [ "$_ds" -le 32 ] && _plateau=1
                fi
                [ "$_plateau" = 1 ] && break
                [ "$_waited" -ge "$SETTLE_MAX" ] && break
            fi
            [ $((_waited % 32)) -eq 0 ] && printf 'PHASE name=ballast_settling run=%s waited_s=%s plateau=%s free_mib=%s swap_comp=%s\n' \
                "$i" "$_waited" "$_plateau" "$R_FREE" "$R_SWAP_COMP"
            _pf=$R_FREE; _ps=$R_SWAP_COMP
        done
        printf 'PHASE name=ballast_settled run=%s ts=%s waited_s=%s plateau=%s pid=%s ' "$i" "$(ts)" "$_waited" "$_plateau" "$BPID"; mem_tokens pre; echo
    fi

    snap_mem
    pre_tok=$(mem_tokens pre)

    # bench 后台跑: fill(数十~数百 ms) 后保持驻留 3s; shell 在 ~1s 处采 post 快照
    "$BENCH" "$SIZE" -r 1 -m "$MODE" -H "$HOLD_FILL" >"$TMPB" 2>&1 &
    BCPID=$!
    sleep 1
    snap_mem
    post_tok=$(mem_tokens post)
    wait "$BCPID"; brc=$?
    BCPID=""
    if [ "$brc" -ne 0 ]; then
        echo "ERROR code=bench_failed run=$i rc=$brc"
        cat "$TMPB"
        exit 4
    fi
    out=$(cat "$TMPB")

    mmap_v=""; fill_v=""; unit_v=""; unit_l=""; unmap_v=""; rss_v=""; faults_v=""; majflt_v=""; found=0
    while IFS= read -r line; do
        case $line in
        "run 1/1:"*)
            found=1; _st=""
            set -- $line
            for t in "$@"; do
                case $_st in
                    mmap)  mmap_v=$t ;;
                    fill)  fill_v=$t ;;
                    unit)  unit_v=$t; _st=unit_l; continue ;;
                    unit_l) unit_l=${t%)}; _st="" ;;
                    unmap) unmap_v=$t ;;
                    rss)   rss_v=$t ;;
                esac
                case $t in
                    mmap=) _st=mmap ;;
                    ')=')  _st=fill ;;
                    'fill(memset)=') _st=fill ;;
                    '(')   _st=unit ;;
                    unmap=) _st=unmap ;;
                    rss=)  _st=rss ;;
                    faults=*)  faults_v=${t#faults=} ;;
                    majflt=*)  majflt_v=${t#majflt=} ;;
                    *) case $_st in mmap|fill|unmap|rss) _st="" ;; esac ;;
                esac
            done
            ;;
        esac
    done <<EOF
$out
EOF
    [ "$found" = 1 ] || { echo "ERROR code=parse_failed run=$i"; printf '%s\n' "$out"; exit 4; }
    [ -n "$faults_v" ] || faults_v=-1
    [ -n "$majflt_v" ] || majflt_v=-1

    printf 'REC run=%s size_mib=%s mode=%s ballast_mib=%s mmap_us=%s fill_ms=%s unit=%s unit_label=%s unmap_ms=%s rss_mib=%s faults=%s majflt=%s %s %s\n' \
        "$i" "$SIZE_MIB" "$MODE" "$BALLAST" "$mmap_v" "$fill_v" "$unit_v" "$unit_l" "$unmap_v" "$rss_v" "$faults_v" "$majflt_v" "$pre_tok" "$post_tok"

    _m=$(to_milli "$fill_v")
    _sum=$((_sum + _m)); _n=$((_n + 1))
    [ "$_m" -lt "$_min" ] && _min=$_m
    [ "$_m" -gt "$_max" ] && _max=$_m

    if [ "$BALLAST" -gt 0 ]; then
        # 释放本轮 ballast, 等系统恢复到基准 free 的 80% 再进下一轮
        kill "$BPID" 2>/dev/null; wait "$BPID" 2>/dev/null; BPID=""
        snap_mem
        printf 'PHASE name=ballast_released run=%s ts=%s ' "$i" "$(ts)"; mem_tokens post; echo
        _bf8=$(( BASE_FREE * 8 / 10 )); [ "$_bf8" -lt 100 ] && _bf8=100
        _rw=0
        while [ "$_rw" -lt "$RECOVER" ]; do
            sleep 2; _rw=$((_rw + 2))
            snap_mem
            [ "$R_FREE" -ge "$_bf8" ] && break
        done
        printf 'PHASE name=round_recovered run=%s ts=%s waited_s=%s free_mib=%s target_mib=%s\n' \
            "$i" "$(ts)" "$_rw" "$R_FREE" "$_bf8"
    else
        [ "$i" -lt "$RUNS" ] && sleep 1
    fi
    i=$((i + 1))
done

if [ "$_n" -gt 0 ]; then
    printf 'SUMMARY runs=%s fill_ms_avg=%s fill_ms_min=%s fill_ms_max=%s\n' \
        "$_n" "$(fmt3 $((_sum / _n)))" "$(fmt3 $_min)" "$(fmt3 $_max)"
fi
mem_bench.c测量工具 · mmap/fill/unmap 耗时 · 439 行 下载
/*
 * mem_bench.c — 匿名页申请耗时基准 (iOS / HarmonyOS / macOS / Linux 通用)
 *
 * 功能:
 *   按入参大小反复 mmap 一块匿名私有内存, 分别统计:
 *     - mmap   耗时 (建立映射本身, 通常只有 µs 级)
 *     - fill   耗时 (memset 整块填充 —— 真正触发缺页 + 写入的主成本;
 *                     或 touch 模式: 每页写 1 字节, 专门测首次触页缺页成本)
 *     - unmap  耗时 (munmap 释放)
 *     - faults 缺页次数 (本轮申请窗口内的总缺页 + 需 I/O 的主缺页;
 *                       Darwin 用 task_events_info, 其余 getrusage)
 *   每轮结束后做轻量读回校验 (防止填充被优化掉, 并确认内存真实可读),
 *   最后输出 avg/min/max 汇总与吞吐率。
 *   -H <sec>: fill 之后、unmap 之前保持驻留 N 秒 —— 供外部 (run_case.sh)
 *   在驻留期间采集"申请后"的内存快照 (post_*); 保持期不计入任何耗时。
 *
 * 用法:
 *   mem_bench [-r n] [-m memset|touch|none] [-H sec] [-q] <size>[K|M|G|T]
 *
 * 构建:
 *   host      : cc -O2 -o mem_bench mem_bench.c
 *   iOS       : xcrun -sdk iphoneos clang -arch arm64 -O2 \
 *               -miphoneos-version-min=12.0 -o mem_bench mem_bench.c
 *   HarmonyOS : <OHOS_NDK>/llvm/bin/clang --target=aarch64-linux-ohos \
 *               --sysroot=<OHOS_NDK>/sysroot -O2 -o mem_bench mem_bench.c
 *
 * 说明:
 *   - 纯 POSIX C, 无第三方依赖; 仅 RSS 读取按平台分支。
 *   - 单位为二进制: K=1024, M=1024^2, G=1024^3, T=1024^4。
 *   - 每轮 memset 使用不同填充值 (0x11/0x22/...), 规避任何零页/去重优化。
 *   - iOS 为 16K 页, Linux/鸿蒙通常为 4K 页; touch 模式的 ns/page 指标
 *     可直接对比两平台的单缺页成本。
 */

#if !defined(__APPLE__)
#define _POSIX_C_SOURCE 200809L
#endif

#include <errno.h>
#include <inttypes.h>
#include <limits.h>
#include <signal.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/mman.h>
#include <sys/resource.h>
#include <time.h>
#include <unistd.h>

#if defined(__APPLE__)
#include <mach/mach.h>
#endif

#ifndef MAP_ANONYMOUS
#define MAP_ANONYMOUS MAP_ANON
#endif

enum fill_mode { FILL_MEMSET, FILL_TOUCH, FILL_NONE };

static volatile sig_atomic_t g_stop = 0;
static volatile uint64_t g_sink = 0;   /* 读回校验累加器, volatile 防优化 */
static size_t g_page = 4096;

/* ---------------- 公共小工具 ---------------- */

static void on_signal(int sig)
{
    (void)sig;
    g_stop = 1;
}

static double now_sec(void)
{
    struct timespec ts;
    clock_gettime(CLOCK_MONOTONIC, &ts);
    return (double)ts.tv_sec + (double)ts.tv_nsec * 1e-9;
}

static double to_mib(unsigned long long bytes)
{
    return (double)bytes / 1048576.0;
}

/* 解析大小参数 (与 mem_stress 相同: 二进制单位, 兼容 B/iB 后缀) */
static int parse_size(const char *s, unsigned long long *out)
{
    if (s == NULL || *s == '\0' || *s == '-' || *s == '+')
        return -1;

    errno = 0;
    char *end = NULL;
    unsigned long long v = strtoull(s, &end, 10);
    if (errno != 0 || end == s)
        return -1;

    const char *u = end;
    unsigned long long mult = 1ULL;

    if (*u == 'K' || *u == 'k' || *u == 'M' || *u == 'm' ||
        *u == 'G' || *u == 'g' || *u == 'T' || *u == 't') {
        switch (*u | 0x20) {
        case 'k': mult = 1ULL << 10; break;
        case 'm': mult = 1ULL << 20; break;
        case 'g': mult = 1ULL << 30; break;
        case 't': mult = 1ULL << 40; break;
        }
        u++;
        if (*u == 'i')
            u++;
        if (*u == 'B' || *u == 'b')
            u++;
    } else if (*u == 'B' || *u == 'b') {
        u++;
    }
    if (*u != '\0')
        return -1;
    if (v == 0)
        return -1;
    if (v > ULLONG_MAX / mult)
        return -1;

    *out = v * mult;
    return 0;
}

/* 当前驻留集 (字节); 取不到返回 0 (调用方按 0 跳过显示) */
static unsigned long long get_rss_bytes(void)
{
#if defined(__APPLE__)
    struct task_vm_info vm;
    mach_msg_type_number_t count = TASK_VM_INFO_COUNT;
    if (task_info(mach_task_self(), TASK_VM_INFO,
                  (task_info_t)&vm, &count) != KERN_SUCCESS)
        return 0;
    return (unsigned long long)vm.resident_size;
#else
    FILE *f = fopen("/proc/self/statm", "r");
    if (f == NULL)
        return 0;
    unsigned long long total = 0, resident = 0;
    int n = fscanf(f, "%llu %llu", &total, &resident);
    fclose(f);
    if (n != 2)
        return 0;
    return resident * (unsigned long long)g_page;
#endif
}

/* 峰值驻留集 (字节); Darwin 的 ru_maxrss 单位是字节, Linux 系是 KiB */
static unsigned long long get_peak_rss_bytes(void)
{
    struct rusage ru;
    if (getrusage(RUSAGE_SELF, &ru) != 0)
        return 0;
#if defined(__APPLE__)
    return (unsigned long long)ru.ru_maxrss;
#else
    return (unsigned long long)ru.ru_maxrss * 1024ULL;
#endif
}

/*
 * 当前累计缺页计数 (本进程)。
 * Darwin: task_events_info 的 faults=总缺页, pageins=需 I/O 的主缺页 (最可靠);
 * 其他平台: getrusage 的 ru_minflt/ru_majflt。
 * 取不到时 *ok=0 (调用方以 -1 表示不可用)。
 */
static void get_faults_now(unsigned long long *flt, unsigned long long *majflt,
                           int *ok)
{
#if defined(__APPLE__)
    struct task_events_info te;
    mach_msg_type_number_t cnt = TASK_EVENTS_INFO_COUNT;
    if (task_info(mach_task_self(), TASK_EVENTS_INFO,
                  (task_info_t)&te, &cnt) == KERN_SUCCESS) {
        *flt = (unsigned long long)te.faults;
        *majflt = (unsigned long long)te.pageins;
        *ok = 1;
        return;
    }
#endif
    struct rusage ru;
    if (getrusage(RUSAGE_SELF, &ru) == 0) {
        *flt = (unsigned long long)ru.ru_minflt + (unsigned long long)ru.ru_majflt;
        *majflt = (unsigned long long)ru.ru_majflt;
        *ok = 1;
        return;
    }
    *ok = 0;
}

/*
 * 严格的整数解析 (拒绝垃圾输入)。注: 不用 getopt —— BSD/musl 的 getopt
 * 不重排 argv, "size 在前、选项在后"的写法会解析失败; 手写解析保证
 * 各平台行为一致。
 */
static int parse_int(const char *s, long *out)
{
    if (s == NULL || *s == '\0')
        return -1;
    errno = 0;
    char *end = NULL;
    long v = strtol(s, &end, 10);
    if (errno != 0 || end == s || *end != '\0')
        return -1;
    *out = v;
    return 0;
}

static const char *fill_mode_name(enum fill_mode m)
{
    switch (m) {
    case FILL_MEMSET: return "memset";
    case FILL_TOUCH:  return "touch ";
    case FILL_NONE:   return "none  ";
    }
    return "?";
}

static void usage(FILE *out, const char *prog)
{
    fprintf(out,
        "mem_bench - anonymous mmap/memset timing benchmark (iOS/HarmonyOS/macOS/Linux)\n"
        "\n"
        "usage: %s [-r n] [-m mode] [-H sec] [-q] <size>\n"
        "\n"
        "  <size>    bytes of anonymous memory per run\n"
        "            e.g. 268435456 | 100K | 512M | 2G (binary units, B/iB suffix ok)\n"
        "  -r n      repeat runs (default 3)\n"
        "  -m mode   fill mode after mmap (default memset):\n"
        "              memset  memset the whole region (faults + bandwidth)\n"
        "              touch   write 1 byte per page (first-touch fault cost, ns/page)\n"
        "              none    mmap only (mapping setup/teardown cost)\n"
        "  -H sec    hold mapping resident N sec between fill and unmap\n"
        "            (for external post-allocation snapshots; not timed)\n"
        "  -q        quiet: per-run lines suppressed\n"
        "\n"
        "per-run line also reports faults=<total> majflt=<io> (page faults in\n"
        "the allocation window; -1 = unavailable on this platform).\n"
        "\n"
        "examples:\n"
        "  %s 512M -r 5          time mmap + memset of 512M, 5 runs\n"
        "  %s 512M -m touch      time first-touch page faults (ns/page)\n"
        "  %s 512M -H 3          hold 3s after fill for external snapshot\n",
        prog, prog, prog, prog);
}

/* 汇总打印一行: name avg/min/max (value 数组单位为秒, 乘 scale 换显示单位) */
static void print_stats(const char *name, const double *v, int n,
                        double scale, const char *unit)
{
    double sum = 0.0, mn = v[0], mx = v[0];
    for (int i = 0; i < n; i++) {
        sum += v[i];
        if (v[i] < mn) mn = v[i];
        if (v[i] > mx) mx = v[i];
    }
    printf("%-7s: avg %10.3f %s | min %10.3f %s | max %10.3f %s\n",
           name, sum / (double)n * scale, unit,
           mn * scale, unit, mx * scale, unit);
}

int main(int argc, char **argv)
{
    int repeats = 3;
    int quiet = 0;
    long hold_s = 0;
    enum fill_mode mode = FILL_MEMSET;
    const char *size_arg = NULL;

    for (int i = 1; i < argc; i++) {
        const char *a = argv[i];
        if (strcmp(a, "-q") == 0) {
            quiet = 1;
        } else if (strcmp(a, "-h") == 0 || strcmp(a, "--help") == 0) {
            usage(stdout, argv[0]);
            return 0;
        } else if (strcmp(a, "-H") == 0) {
            long v = 0;
            if (i + 1 >= argc || parse_int(argv[i + 1], &v) != 0 ||
                v < 0 || v > 3600) {
                fprintf(stderr, "error: -H expects seconds in 0..3600\n");
                return 2;
            }
            i++;
            hold_s = v;
        } else if (strcmp(a, "-r") == 0) {
            long v = 0;
            if (i + 1 >= argc || parse_int(argv[i + 1], &v) != 0 ||
                v < 1 || v > 10000) {
                fprintf(stderr, "error: -r expects an integer in 1..10000\n");
                return 2;
            }
            i++;
            repeats = (int)v;
        } else if (strcmp(a, "-m") == 0) {
            if (i + 1 >= argc) {
                fprintf(stderr, "error: -m expects a mode (memset|touch|none)\n");
                return 2;
            }
            i++;
            if (strcmp(argv[i], "memset") == 0)      mode = FILL_MEMSET;
            else if (strcmp(argv[i], "touch") == 0)  mode = FILL_TOUCH;
            else if (strcmp(argv[i], "none") == 0)   mode = FILL_NONE;
            else {
                fprintf(stderr, "error: unknown mode '%s' (memset|touch|none)\n",
                        argv[i]);
                return 2;
            }
        } else if (a[0] == '-' && a[1] != '\0') {
            fprintf(stderr, "error: unknown option '%s'\n", a);
            usage(stderr, argv[0]);
            return 2;
        } else {
            if (size_arg != NULL) {
                fprintf(stderr, "error: multiple sizes given ('%s' and '%s')\n",
                        size_arg, a);
                return 2;
            }
            size_arg = a;
        }
    }
    if (size_arg == NULL) {
        usage(stderr, argv[0]);
        return 2;
    }

    unsigned long long req = 0;
    if (parse_size(size_arg, &req) != 0) {
        fprintf(stderr, "error: bad size '%s' (try 512M / 2G / 65536K)\n",
                size_arg);
        return 2;
    }

    long ps = sysconf(_SC_PAGESIZE);
    if (ps > 0)
        g_page = (size_t)ps;

    if (req > (unsigned long long)(SIZE_MAX - (g_page - 1))) {
        fprintf(stderr, "error: size %llu bytes exceeds this platform's address space\n",
                req);
        return 2;
    }
    size_t size = ((size_t)req + g_page - 1) & ~((size_t)g_page - 1);
    unsigned long long pages = (unsigned long long)(size / g_page);

    setvbuf(stdout, NULL, _IOLBF, 0);

    struct sigaction sa;
    memset(&sa, 0, sizeof sa);
    sa.sa_handler = on_signal;
    sigemptyset(&sa.sa_mask);
    sa.sa_flags = 0;
    sigaction(SIGINT, &sa, NULL);
    sigaction(SIGTERM, &sa, NULL);

    double *t_mmap = malloc((size_t)repeats * sizeof(double));
    double *t_fill = malloc((size_t)repeats * sizeof(double));
    double *t_unmap = malloc((size_t)repeats * sizeof(double));
    if (t_mmap == NULL || t_fill == NULL || t_unmap == NULL) {
        fprintf(stderr, "error: out of memory\n");
        return 1;
    }

    printf("target : %llu bytes (%.2f MiB), %llu page(s) x %zu B, "
           "mode=%s, runs=%d\n",
           (unsigned long long)size, to_mib(size), pages, g_page,
           fill_mode_name(mode), repeats);

    int completed = 0;
    for (int i = 0; i < repeats && !g_stop; i++) {
        /* --- 申请窗口缺页计数: 起点采样 --- */
        unsigned long long f0 = 0, f0m = 0;
        int fok0 = 0;
        get_faults_now(&f0, &f0m, &fok0);

        /* --- mmap --- */
        double t0 = now_sec();
        char *p = mmap(NULL, size, PROT_READ | PROT_WRITE,
                       MAP_PRIVATE | MAP_ANONYMOUS, -1, 0);
        double t1 = now_sec();
        if (p == MAP_FAILED) {
            fprintf(stderr, "error: mmap %llu bytes failed on run %d: %s\n",
                    (unsigned long long)size, i + 1, strerror(errno));
#if defined(__APPLE__)
            fprintf(stderr, "hint: iOS caps a single process's anonymous "
                            "reservation (~4GiB observed on 14.8); "
                            "try a smaller size.\n");
#endif
            break;
        }
        t_mmap[i] = t1 - t0;

        /* --- fill --- */
        unsigned char pat = (unsigned char)(0x11 * (unsigned)(i + 1)); /* 0x11,0x22,... */
        t0 = now_sec();
        if (mode == FILL_MEMSET) {
            memset(p, pat, size);
        } else if (mode == FILL_TOUCH) {
            for (size_t off = 0; off < size && !g_stop; off += g_page)
                p[off] = (char)pat;
        }
        t1 = now_sec();
        t_fill[i] = t1 - t0;

        /* 轻量读回校验: 每页读首字节累加, 防止填充/映射被优化掉 */
        if (mode != FILL_NONE) {
            uint64_t sum = 0;
            for (size_t off = 0; off < size; off += g_page)
                sum += (uint64_t)(unsigned char)p[off];
            g_sink = sum;
        }
        unsigned long long rss = get_rss_bytes();

        /* --- 申请窗口缺页计数: 终点采样 (fill+读回全部计入) --- */
        unsigned long long f1 = 0, f1m = 0;
        int fok1 = 0;
        get_faults_now(&f1, &f1m, &fok1);
        long long flt = -1, flt_maj = -1;
        if (fok0 && fok1) {
            flt = (long long)(f1 - f0);
            flt_maj = (long long)(f1m - f0m);
        }

        /* --- 驻留保持 (供外部采"申请后"快照; 不计入任何耗时) --- */
        if (hold_s > 0 && !g_stop) {
            struct timespec hts = { hold_s, 0 };
            nanosleep(&hts, NULL);
        }

        /* --- unmap --- */
        t0 = now_sec();
        munmap(p, size);
        t_unmap[i] = now_sec() - t0;

        completed = i + 1;
        if (!quiet) {
            printf("run %d/%d: mmap=%9.1f us | fill(%s)=%10.3f ms",
                   i + 1, repeats, t_mmap[i] * 1e6,
                   mode == FILL_MEMSET ? "memset" :
                   mode == FILL_TOUCH  ? "touch " : "none  ",
                   t_fill[i] * 1e3);
            if (mode == FILL_MEMSET && t_fill[i] > 0.0)
                printf(" (%8.1f MiB/s)", to_mib(size) / t_fill[i]);
            if (mode == FILL_TOUCH && t_fill[i] > 0.0 && pages > 0)
                printf(" (%8.1f ns/page)",
                       t_fill[i] * 1e9 / (double)pages);
            printf(" | unmap=%8.3f ms", t_unmap[i] * 1e3);
            if (rss)
                printf(" | rss=%8.2f MiB", to_mib(rss));
            printf(" | faults=%lld majflt=%lld", flt, flt_maj);
            if (hold_s > 0)
                printf(" | held=%lds", hold_s);
            printf("\n");
        }

        if (i + 1 < repeats) {
            struct timespec ts = { 0, 100 * 1000 * 1000 };  /* 100ms 间隔 */
            nanosleep(&ts, NULL);
        }
    }

    if (completed == 0) {
        fprintf(stderr, "error: no run completed\n");
        free(t_mmap); free(t_fill); free(t_unmap);
        return 1;
    }

    /* ---- 汇总 ---- */
    printf("---- summary (%d run%s, %.2f MiB, %s mode) ----\n",
           completed, completed == 1 ? "" : "s", to_mib(size),
           mode == FILL_MEMSET ? "memset" :
           mode == FILL_TOUCH  ? "touch"  : "none");
    print_stats("mmap", t_mmap, completed, 1e6, "us");
    if (mode != FILL_NONE)
        print_stats("fill", t_fill, completed, 1e3, "ms");
    print_stats("unmap", t_unmap, completed, 1e3, "ms");

    if (mode == FILL_MEMSET) {
        double tp_sum = 0.0;
        int tp_n = 0;
        for (int i = 0; i < completed; i++) {
            if (t_fill[i] > 0.0) {
                tp_sum += to_mib(size) / t_fill[i];
                tp_n++;
            }
        }
        if (tp_n > 0)
            printf("memset throughput: avg %.1f MiB/s over %d run(s)\n",
                   tp_sum / (double)tp_n, tp_n);
    }
    if (mode == FILL_TOUCH && pages > 0) {
        double fp_sum = 0.0;
        int fp_n = 0;
        for (int i = 0; i < completed; i++) {
            if (t_fill[i] > 0.0) {
                fp_sum += t_fill[i] * 1e9 / (double)pages;
                fp_n++;
            }
        }
        if (fp_n > 0)
            printf("first-touch fault: avg %.1f ns/page over %d run(s)\n",
                   fp_sum / (double)fp_n, fp_n);
    }

    printf("peak rss: %.2f MiB\n", to_mib(get_peak_rss_bytes()));
    (void)g_sink;
    free(t_mmap); free(t_fill); free(t_unmap);
    return 0;
}
mem_stress.c重载 ballast 工具 · -P 50 填充 · 497 行 下载
/*
 * mem_stress.c — 匿名页内存加压工具 (iOS / HarmonyOS / macOS / Linux 通用)
 *
 * 功能:
 *   按入参大小 mmap 一块匿名私有内存 (MAP_PRIVATE|MAP_ANONYMOUS), 并逐页写入
 *   触页 (touch) 提交物理页, 形成真实内存压力; 之后按参数保持一段时间
 *   (或直到收到 SIGINT/SIGTERM) 再释放。
 *
 *   可选 -k 周期性重触: 每个周期向所有页写入新值, 迫使被 XNU compressor /
 *   Linux zram 压缩回收的页重新解压驻留, 并把干净页重新弄脏, 维持压力。
 *
 * 用法:
 *   mem_stress [-f|-R] [-k sec] [-t sec] [-qv] <size>[K|M|G|T]
 *     -R = 随机字节填充 (不可压缩, 模拟真实负载; 默认触页/0xA5 模式会被
 *          compressor 几乎零成本吸收, 无法形成持续压力)
 *
 * 构建:
 *   host      : cc -O2 -o mem_stress mem_stress.c
 *   iOS       : xcrun -sdk iphoneos clang -arch arm64 -O2 \
 *               -miphoneos-version-min=12.0 -o mem_stress mem_stress.c
 *   HarmonyOS : <OHOS_NDK>/llvm/bin/clang --target=aarch64-linux-ohos \
 *               --sysroot=<OHOS_NDK>/sysroot -O2 -o mem_stress mem_stress.c
 *
 * 说明:
 *   - 纯 POSIX C, 无第三方依赖; 仅 RSS/footprint 读取按平台分支。
 *   - 单位为二进制: K=1024, M=1024^2, G=1024^3, T=1024^4。
 *   - iOS 上单进程匿名保留有 ~4GiB 上限 (14.8 实测), 超限 mmap 直接失败;
 *     更常见的是压力过大先被 jetsam 以 SIGKILL 终止 —— 属预期行为。
 */

#if !defined(__APPLE__)
#define _POSIX_C_SOURCE 200809L
#endif

#include <errno.h>
#include <inttypes.h>
#include <limits.h>
#include <signal.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/mman.h>
#include <sys/resource.h>
#include <time.h>
#include <unistd.h>

#if defined(__APPLE__)
#include <mach/mach.h>
#endif

#ifndef MAP_ANONYMOUS
#define MAP_ANONYMOUS MAP_ANON
#endif

static volatile sig_atomic_t g_stop = 0;
static size_t g_page = 4096;

/* ---------------- 公共小工具 ---------------- */

static void on_signal(int sig)
{
    (void)sig;
    g_stop = 1;
}

static double now_sec(void)
{
    struct timespec ts;
    clock_gettime(CLOCK_MONOTONIC, &ts);
    return (double)ts.tv_sec + (double)ts.tv_nsec * 1e-9;
}

static double to_mib(unsigned long long bytes)
{
    return (double)bytes / 1048576.0;
}

/*
 * 解析大小参数: "1024" "100K" "512M" "2G" "1T", 兼容 B/iB 后缀与大小写。
 * 二进制单位 (K=1024)。成功返回 0, 失败返回 -1。
 */
static int parse_size(const char *s, unsigned long long *out)
{
    if (s == NULL || *s == '\0' || *s == '-' || *s == '+')
        return -1;

    errno = 0;
    char *end = NULL;
    unsigned long long v = strtoull(s, &end, 10);
    if (errno != 0 || end == s)
        return -1;

    const char *u = end;
    unsigned long long mult = 1ULL;

    if (*u == 'K' || *u == 'k' || *u == 'M' || *u == 'm' ||
        *u == 'G' || *u == 'g' || *u == 'T' || *u == 't') {
        switch (*u | 0x20) {
        case 'k': mult = 1ULL << 10; break;
        case 'm': mult = 1ULL << 20; break;
        case 'g': mult = 1ULL << 30; break;
        case 't': mult = 1ULL << 40; break;
        }
        u++;
        if (*u == 'i')                 /* KiB / MiB / GiB ... */
            u++;
        if (*u == 'B' || *u == 'b')
            u++;
    } else if (*u == 'B' || *u == 'b') {
        u++;                           /* "100B" = 100 字节 */
    }
    if (*u != '\0')
        return -1;
    if (v == 0)
        return -1;
    if (v > ULLONG_MAX / mult)
        return -1;

    *out = v * mult;
    return 0;
}

/* 当前驻留集 (字节); 取不到返回 0 (调用方按 0 跳过显示) */
static unsigned long long get_rss_bytes(void)
{
#if defined(__APPLE__)
    struct task_vm_info vm;
    mach_msg_type_number_t count = TASK_VM_INFO_COUNT;
    if (task_info(mach_task_self(), TASK_VM_INFO,
                  (task_info_t)&vm, &count) != KERN_SUCCESS)
        return 0;
    return (unsigned long long)vm.resident_size;
#else
    /* Linux / OpenHarmony(musl): /proc/self/statm 第二列 = 驻留页数 */
    FILE *f = fopen("/proc/self/statm", "r");
    if (f == NULL)
        return 0;
    unsigned long long total = 0, resident = 0;
    int n = fscanf(f, "%llu %llu", &total, &resident);
    fclose(f);
    if (n != 2)
        return 0;
    return resident * (unsigned long long)g_page;
#endif
}

#if defined(__APPLE__)
/* iOS jetsam 的判定口径: phys_footprint = 驻留脏页 + 已压缩脏页 + IOKit 映射 */
static unsigned long long get_footprint_bytes(void)
{
    struct task_vm_info vm;
    mach_msg_type_number_t count = TASK_VM_INFO_COUNT;
    if (task_info(mach_task_self(), TASK_VM_INFO,
                  (task_info_t)&vm, &count) != KERN_SUCCESS)
        return 0;
    return (unsigned long long)vm.phys_footprint;
}
#endif

/* 峰值驻留集 (字节); Darwin 的 ru_maxrss 单位是字节, Linux 系是 KiB */
static unsigned long long get_peak_rss_bytes(void)
{
    struct rusage ru;
    if (getrusage(RUSAGE_SELF, &ru) != 0)
        return 0;
#if defined(__APPLE__)
    return (unsigned long long)ru.ru_maxrss;
#else
    return (unsigned long long)ru.ru_maxrss * 1024ULL;
#endif
}

/* xorshift64* 伪随机数: 填充不可压缩 ballast (随机数据 WKdm 压不动,
 * 与 0xA5 模式填充不同, 无法被 compressor 几乎零成本地吸收) */
static uint64_t g_prng = 0;

static void prng_seed(uint64_t s)
{
    g_prng = s ? s : 0x9E3779B97F4A7C15ULL;
}

static uint64_t prng_next(void)
{
    uint64_t x = g_prng;
    x ^= x << 13;
    x ^= x >> 7;
    x ^= x << 17;
    g_prng = x;
    return x * 0x2545F4914F6CDD1DULL;
}

/* ---- -F 自适应限速: 填充期间每 4MiB 检查系统 free, 低于地板则暂停等待回收 ----
 * 背景: 一次性高速填充会让 free 瞬间砸穿 vm_page_free_min, 压缩器来不及反应,
 * 触发 jetsam 风暴甚至 watchdog panic (iOS 14.8 实测事故)。细步进+自适应暂停
 * 让填充速率始终不超过回收速率, free 永不落地。 */
static unsigned long g_floor_pages = 0;   /* 0 = 不限速 */
static double g_throttle_total = 0.0;
static unsigned long g_throttle_n = 0;
#define THROTTLE_CHUNK_BYTES (4U << 20)   /* 每步进 4MiB */

static long free_pages_now(void)
{
#if defined(__APPLE__)
    struct vm_statistics64 st;
    mach_msg_type_number_t cnt = HOST_VM_INFO64_COUNT;
    if (host_statistics64(mach_host_self(), HOST_VM_INFO64,
                          (host_info_t)&st, &cnt) != KERN_SUCCESS)
        return -1;
    return (long)st.free_count;           /* 不含 speculative, 严格口径 */
#elif defined(__linux__)
    FILE *f = fopen("/proc/meminfo", "r");
    if (!f)
        return -1;
    long pages = -1;
    char key[64];
    unsigned long long v;
    while (fscanf(f, "%63s %llu", key, &v) == 2) {
        if (strcmp(key, "MemFree:") == 0) {
            pages = (long)(v * 1024ULL / g_page);
            break;
        }
    }
    fclose(f);
    return pages;
#else
    return -1;
#endif
}

static void maybe_throttle(void)
{
    if (g_floor_pages == 0)
        return;
    long fr;
    int started = 0;
    double t0 = 0.0;
    while ((fr = free_pages_now()) >= 0 && (unsigned long)fr < g_floor_pages) {
        if (!started) {
            t0 = now_sec();
            started = 1;
        }
        if (g_stop)                       /* 信号到来立即让路 */
            break;
        struct timespec ts = { 0, 50 * 1000 * 1000 };
        nanosleep(&ts, NULL);
        if (now_sec() - t0 > 120.0)       /* 单次暂停上限: 地板不可达时硬闯 */
            break;
    }
    if (started) {
        g_throttle_total += now_sec() - t0;
        g_throttle_n++;
    }
}

/* 填充完成标记: 供父脚本 (run_case.sh) 区分"填充中"与"已驻留"两个阶段 */
static void write_fill_marker(void)
{
    const char *path = getenv("MEM_STRESS_MARKER");
    if (path == NULL || *path == '\0')
        return;
    FILE *f = fopen(path, "w");
    if (f == NULL)
        return;
    fprintf(f, "filled\n");
    fclose(f);
}

/* 整块填充随机字节, 密度 pct (1..100): 每页前 pct% 为随机字节, 其余清零。
 * pct=100 即全随机 (WKdm ~1:1, compressor 无净收益); pct=50 约 2:1 ——
 * 接近真实 App 数据的压缩比, 可形成"压缩器仍有回收余量"的可持续重载。 */
static void fill_random(char *buf, size_t size, unsigned cycle, int pct)
{
    prng_seed(0x9E3779B97F4A7C15ULL * (uint64_t)(cycle + 1) ^
              (uint64_t)time(NULL) ^ (uint64_t)(uintptr_t)buf);
    if (pct >= 100) {
        size_t n64 = size / 8;
        size_t chk = THROTTLE_CHUNK_BYTES / 8, since = 0;
        uint64_t *q = (uint64_t *)(void *)buf;   /* mmap 区页对齐, 安全 */
        for (size_t i = 0; i < n64; i++) {
            if (g_floor_pages != 0 && since++ >= chk) {
                maybe_throttle();
                since = 0;
            }
            q[i] = prng_next();
        }
        for (size_t i = n64 * 8; i < size; i++)
            buf[i] = (char)prng_next();
        return;
    }
    /* 部分密度: 每页 [0, rand_bytes) 随机, 其余 0 */
    size_t rand_bytes = (size_t)((unsigned long long)g_page * (unsigned)pct / 100);
    if (rand_bytes == 0)
        rand_bytes = 1;
    size_t chk = THROTTLE_CHUNK_BYTES / g_page, since = 0;
    memset(buf, 0, size);
    for (size_t off = 0; off < size; off += g_page) {
        if (g_floor_pages != 0 && since++ >= chk) {
            maybe_throttle();
            since = 0;
        }
        size_t remain = rand_bytes;
        if (off + remain > size)
            remain = size - off;
        size_t n64 = remain / 8;
        uint64_t *q = (uint64_t *)(void *)(buf + off);
        for (size_t i = 0; i < n64; i++)
            q[i] = prng_next();
        for (size_t i = n64 * 8; i < remain; i++)
            buf[off + i] = (char)prng_next();
    }
}

/*
 * 解析秒数参数 (strtod 严格校验, 拒绝垃圾输入)。成功返回 0。
 * 注: 不用 getopt —— BSD/musl 的 getopt 不重排 argv, "size 在前、选项在后"
 * 的写法会解析失败; 手写解析保证各平台行为一致。
 */
static int parse_seconds(const char *s, double *out)
{
    if (s == NULL || *s == '\0')
        return -1;
    errno = 0;
    char *end = NULL;
    double v = strtod(s, &end);
    if (errno != 0 || end == s || *end != '\0' || v < 0.0)
        return -1;
    *out = v;
    return 0;
}

static void usage(FILE *out, const char *prog)
{
    fprintf(out,
        "mem_stress - anonymous memory pressure tool (iOS/HarmonyOS/macOS/Linux)\n"
        "\n"
        "usage: %s [-f] [-k sec] [-t sec] [-qv] <size>\n"
        "\n"
        "  <size>    bytes of anonymous memory to commit\n"
        "            e.g. 268435456 | 100K | 512M | 1500M | 2G | 1T\n"
        "            (binary units: K=1024, M=1024^2, G=1024^3; B/iB suffix ok)\n"
        "  -f        full fill: memset the whole region (default: 1 byte per page)\n"
        "  -R        fill with pseudo-random bytes: incompressible ballast that\n"
        "            the compressor cannot absorb (realistic heavy load)\n"
        "  -P n      partial random fill: first n%% of each page random, rest zero\n"
        "            (n=50 approximates real app data, ~2:1 WKdm ratio)\n"
        "  -k sec    re-touch all pages every <sec> while holding, to defeat\n"
        "            compressor/zram reclaim (0 = off, default off)\n"
        "  -t sec    hold duration after touch, then release\n"
        "            (default: hold until SIGINT/SIGTERM)\n"
        "  -F pages  adaptive throttle: while filling, check system free pages\n"
        "            every 4MiB and pause until free >= floor (protects against\n"
        "            jetsam storms / watchdog panic on big ballasts; 0 = off)\n"
        "  -q        quiet (suppress progress lines)\n"
        "  -v        verbose progress\n"
        "\n"
        "examples:\n"
        "  %s 512M -t 30              commit 512M, hold 30s, release\n"
        "  %s 1500M -t 60 -k 5        hold 60s, re-touch every 5s against compression\n"
        "  %s 2000M -R -t 900          incompressible ballast (heavy load emulation)\n",
        prog, prog, prog, prog);
}

int main(int argc, char **argv)
{
    int full_fill = 0, random_fill = 0, random_pct = 0, verbose = 0, quiet = 0;
    double keep_interval = 0.0;
    double hold_secs = -1.0;           /* -1 = 一直持有直到信号 */
    const char *size_arg = NULL;

    for (int i = 1; i < argc; i++) {
        const char *a = argv[i];
        if (strcmp(a, "-f") == 0) {
            full_fill = 1;
        } else if (strcmp(a, "-R") == 0) {
            random_fill = 1;
            random_pct = 100;
        } else if (strcmp(a, "-P") == 0) {
            if (i + 1 >= argc) {
                fprintf(stderr, "error: -P expects a percentage 1..100\n");
                return 2;
            }
            i++;
            long v = strtol(argv[i], NULL, 10);
            if (v < 1 || v > 100 || argv[i][0] == '\0' ||
                (strchr(argv[i], '-') != NULL)) {
                fprintf(stderr, "error: -P expects a percentage 1..100\n");
                return 2;
            }
            random_fill = 1;
            random_pct = (int)v;
        } else if (strcmp(a, "-F") == 0) {
            if (i + 1 >= argc) {
                fprintf(stderr, "error: -F expects a page count (e.g. 2000)\n");
                return 2;
            }
            i++;
            long v = strtol(argv[i], NULL, 10);
            if (v < 0 || argv[i][0] == '\0' || strchr(argv[i], '-') != NULL) {
                fprintf(stderr, "error: -F expects a non-negative page count\n");
                return 2;
            }
            g_floor_pages = (unsigned long)v;
        } else if (strcmp(a, "-q") == 0) {
            quiet = 1;
        } else if (strcmp(a, "-v") == 0) {
            verbose = 1;
        } else if (strcmp(a, "-h") == 0 || strcmp(a, "--help") == 0) {
            usage(stdout, argv[0]);
            return 0;
        } else if (strcmp(a, "-k") == 0 || strcmp(a, "-t") == 0) {
            double v = 0.0;
            if (i + 1 >= argc || parse_seconds(argv[i + 1], &v) != 0) {
                fprintf(stderr, "error: %s expects a non-negative number of seconds\n", a);
                return 2;
            }
            i++;
            if (a[1] == 'k')
                keep_interval = v;
            else
                hold_secs = v;
        } else if (a[0] == '-' && a[1] != '\0') {
            fprintf(stderr, "error: unknown option '%s'\n", a);
            usage(stderr, argv[0]);
            return 2;
        } else {
            if (size_arg != NULL) {
                fprintf(stderr, "error: multiple sizes given ('%s' and '%s')\n",
                        size_arg, a);
                return 2;
            }
            size_arg = a;
        }
    }
    if (size_arg == NULL) {
        usage(stderr, argv[0]);
        return 2;
    }

    unsigned long long req = 0;
    if (parse_size(size_arg, &req) != 0) {
        fprintf(stderr, "error: bad size '%s' (try 512M / 2G / 65536K)\n",
                size_arg);
        return 2;
    }

    long ps = sysconf(_SC_PAGESIZE);
    if (ps > 0)
        g_page = (size_t)ps;

    if (req > (unsigned long long)(SIZE_MAX - (g_page - 1))) {
        fprintf(stderr, "error: size %llu bytes exceeds this platform's address space\n",
                req);
        return 2;
    }
    size_t size = ((size_t)req + g_page - 1) & ~((size_t)g_page - 1);
    unsigned long long pages = (unsigned long long)(size / g_page);

    setvbuf(stdout, NULL, _IOLBF, 0);  /* 管道/ssh 下进度实时可见 */

    struct sigaction sa;
    memset(&sa, 0, sizeof sa);
    sa.sa_handler = on_signal;
    sigemptyset(&sa.sa_mask);
    sa.sa_flags = 0;                   /* 不重启: nanosleep 被信号打断立刻返回 */
    sigaction(SIGINT, &sa, NULL);
    sigaction(SIGTERM, &sa, NULL);

    double t_start = now_sec();

    /* ---- 申请匿名区 ---- */
    double t0 = now_sec();
    char *mem = mmap(NULL, size, PROT_READ | PROT_WRITE,
                     MAP_PRIVATE | MAP_ANONYMOUS, -1, 0);
    double map_s = now_sec() - t0;
    if (mem == MAP_FAILED) {
        fprintf(stderr, "error: mmap %llu bytes failed: %s\n",
                (unsigned long long)size, strerror(errno));
#if defined(__APPLE__)
        fprintf(stderr, "hint: iOS caps a single process's anonymous reservation "
                        "(~4GiB observed on 14.8); try a smaller size.\n");
#endif
        return 1;
    }

    if (!quiet)
        printf("alloc  : %llu bytes (%.2f MiB), %llu page(s) x %zu B, "
               "mmap %.3f ms\n",
               (unsigned long long)size, to_mib(size), pages, g_page,
               map_s * 1e3);

    /* ---- 触页提交物理内存 ---- */
    unsigned long long done = 0;       /* 已触页数 (page-touch 模式) */
    t0 = now_sec();
    if (random_fill) {
        fill_random(mem, size, 0, random_pct);   /* 密度见 -P/-R */
    } else if (full_fill) {
        /* 整块填充, 分 4MiB 步进 (可限速可打断) */
        for (size_t off = 0; off < size; off += THROTTLE_CHUNK_BYTES) {
            if (g_stop)
                break;
            size_t n = size - off < THROTTLE_CHUNK_BYTES ? size - off
                                                         : THROTTLE_CHUNK_BYTES;
            memset(mem + off, 0xA5, n);
            maybe_throttle();
        }
    } else {
        /* 每页首字节写一次即触发缺页提交; 默认每 10% 汇报, -v 每 1% */
        unsigned long long report_every = pages / (verbose ? 100 : 10);
        if (report_every == 0)
            report_every = 1;
        size_t chk = THROTTLE_CHUNK_BYTES / g_page, since = 0;
        for (size_t off = 0; off < size; off += g_page) {
            if (g_stop)
                break;
            if (g_floor_pages != 0 && since++ >= chk) {
                maybe_throttle();
                since = 0;
            }
            mem[off] = (char)0xA5;
            done++;
            if (!quiet && (done % report_every) == 0)
                printf("touch  : %3llu%% (%8.2f MiB) elapsed %.2f s\n",
                       done * 100ULL / pages,
                       to_mib(done * (unsigned long long)g_page),
                       now_sec() - t0);
        }
    }
    double touch_s = now_sec() - t0;
    write_fill_marker();                 /* 通知父脚本: 填充阶段结束 */

    if (g_stop && !full_fill && !random_fill) {
        printf("touch  : interrupted at %.2f MiB (%llu/%llu pages)\n",
               to_mib(done * (unsigned long long)g_page), done, pages);
    } else {
        printf("touch  : %s %llu pages (%.2f MiB) in %.3f s",
               (random_fill || full_fill) ? "filled" : "touched",
               pages, to_mib(size), touch_s);
        if (random_fill)
            printf(" [random %d%%/page]", random_pct);
        if (touch_s > 0.0)
            printf(" [%.1f MiB/s, %.1fK pages/s]",
                   to_mib(size) / touch_s,
                   (double)pages / touch_s / 1000.0);
        printf("\n");
    }
    if (g_throttle_n > 0)
        printf("throttle: %lu pause(s), %.1f s total (floor %lu pages)\n",
               g_throttle_n, g_throttle_total, g_floor_pages);

    {
        unsigned long long rss = get_rss_bytes();
#if defined(__APPLE__)
        unsigned long long fp = get_footprint_bytes();
        if (rss || fp)
            printf("rss    : resident %.2f MiB, phys_footprint %.2f MiB\n",
                   to_mib(rss), to_mib(fp));
#else
        if (rss)
            printf("rss    : resident %.2f MiB\n", to_mib(rss));
#endif
    }

    /* ---- 保持 (可选周期重触) ---- */
    if (!g_stop) {
        if (hold_secs >= 0.0) {
            if (!quiet)
                printf("hold   : %.3f s%s\n", hold_secs,
                       keep_interval > 0.0 ? ", re-touch on" : "");
        } else if (!quiet) {
            printf("hold   : until SIGINT/SIGTERM (press Ctrl-C to release)\n");
        }

        double hold0 = now_sec();
        double deadline = (hold_secs >= 0.0) ? hold0 + hold_secs : -1.0;
        double next_cycle = (keep_interval > 0.0) ? hold0 + keep_interval : -1.0;
        unsigned cycle = 0;

        while (!g_stop) {
            double now = now_sec();
            if (deadline > 0.0 && now >= deadline)
                break;
            if (next_cycle > 0.0 && now >= next_cycle) {
                cycle++;
                /* 写入与上一周期不同的数据: 强制压缩页解压回 RAM、干净页重新变脏;
                 * -R 模式每轮换随机数据, 保持不可压缩 */
                unsigned char pat = (unsigned char)(0xA0 | (cycle & 0x0F));
                if (random_fill) {
                    fill_random(mem, size, cycle, random_pct);
                } else if (full_fill) {
                    memset(mem, pat, size);
                } else {
                    for (size_t off = 0; off < size && !g_stop; off += g_page)
                        mem[off] = (char)pat;
                }
                if (!quiet) {
                    printf("cycle  : %u re-touched %.2f MiB", cycle, to_mib(size));
                    unsigned long long r2 = get_rss_bytes();
                    if (r2)
                        printf(", rss %.2f MiB", to_mib(r2));
                    printf("\n");
                }
                next_cycle += keep_interval;
                continue;
            }
            struct timespec ts = { 0, 20 * 1000 * 1000 };  /* 20ms 轮询 */
            nanosleep(&ts, NULL);
        }
    }

    /* ---- 释放 ---- */
    munmap(mem, size);
    printf("release: munmap ok, total %.3f s, peak rss %.2f MiB\n",
           now_sec() - t_start, to_mib(get_peak_rss_bytes()));
    return 0;
}
Makefile编译 · host / ios / ohos 三目标 下载
# mem_alloc_bench — 跨平台构建
#
#   make            本机 (macOS/Linux) 编译 mem_stress + mem_bench
#   make ios        交叉编译 iOS arm64 二进制 (输出到 ios/)
#   make ohos       交叉编译 HarmonyOS arm64 二进制 (输出到 ohos/, 需 OHOS NDK)
#   make clean
#
# OHOS NDK 路径按实际安装位置覆盖, 例如:
#   make ohos OHOS_NATIVE=/path/to/openharmony/native

CC      ?= cc
CFLAGS  ?= -O2 -Wall -Wextra -std=c11
BINS     = mem_stress mem_bench

# ---- iOS (Xcode 自带工具链) ----
IOS_CC     := xcrun -sdk iphoneos clang
IOS_CFLAGS := -arch arm64 -O2 -Wall -Wextra -std=c11 -miphoneos-version-min=12.0

# ---- HarmonyOS (DevEco Studio 的 OHOS NDK, musl libc) ----
OHOS_NATIVE ?= /Applications/DevEco-Studio.app/Contents/sdk/default/openharmony/native
OHOS_CC     := $(OHOS_NATIVE)/llvm/bin/clang
# 真机 arm64 用 aarch64-linux-ohos; 模拟器改 x86_64-linux-ohos
OHOS_TRIPLE ?= aarch64-linux-ohos
OHOS_CFLAGS := --target=$(OHOS_TRIPLE) --sysroot=$(OHOS_NATIVE)/sysroot \
               -O2 -Wall -Wextra -std=c11

all: $(BINS)

mem_stress: mem_stress.c
	$(CC) $(CFLAGS) -o $@ $<

mem_bench: mem_bench.c
	$(CC) $(CFLAGS) -o $@ $<

ios: ios/mem_stress ios/mem_bench

ios/%: %.c
	@mkdir -p ios
	$(IOS_CC) $(IOS_CFLAGS) -o $@ $<

ohos: ohos/mem_stress ohos/mem_bench

ohos/%: %.c
	@mkdir -p ohos
	$(OHOS_CC) $(OHOS_CFLAGS) -o $@ $<

clean:
	rm -rf $(BINS) ios ohos

.PHONY: all ios ohos clean
compare_cases.py对比工具 · Python3 标准库 · 电脑侧执行 下载
#!/usr/bin/env python3
# compare_cases.py — 标准用例数据对比 (消费 run_case.sh 生成的 case-*.txt)
#
# 用法:
#   python3 compare_cases.py case-light-touch.txt case-heavy-touch.txt [...]   # 第一个文件为基准
#
# 输出: ① 耗时表   — 每文件一行 (fill/mmap/unmap/faults 计时与缺页指标)
#       ② 状态表   — pre_* (申请前) 与 post_* (申请驻留时) 全量指标
#                    (转置: 指标为行, 文件为列, 逐项 Δ vs 基准)
#       ③ 驻留开销  — post−pre 逐项差值 (申请 200M 对系统状态的净影响)
#       ④ Δ 汇总行 — 主指标相对基准变化
#       ⑤ 机器可读 key=value 行 (ROW/DELTA, 含全部 pre_*/post_*_avg/_pct 与 faults)
# 约定: pre_*/post_* 单位 MiB; -1/N/A = 平台无此指标 (如 iOS 无 pre_avail, Linux 无 pre_free_spec)
#       旧格式 case 文件 (无 post_*/faults) 仍可解析: 对应列显示 "-", post 块自动省略
# 仅依赖 Python3 标准库; 对比在采集侧(电脑)进行, 不要求 iOS 上有 python。
import sys

METRICS = ["total", "free", "avail", "free_spec", "file", "anon",
           "swap_comp", "comp_occ"]   # pre_<m> / post_<m>

def parse(path):
    case, recs = {}, []
    for line in open(path, encoding="utf-8", errors="replace"):
        line = line.strip()
        if not line:
            continue
        toks = line.split()
        if toks[0] not in ("CASE", "PHASE", "REC", "SUMMARY", "ERROR"):
            continue
        d = dict(t.split("=", 1) for t in toks[1:] if "=" in t)
        if toks[0] == "CASE":
            case = d
        elif toks[0] == "REC":
            recs.append(d)
    return case, recs

def num(d, key):
    try:
        return float(d[key])
    except (KeyError, ValueError):
        return None

def mean(xs):
    xs = [x for x in xs if x is not None]
    return sum(xs) / len(xs) if xs else None

def avg_key(recs, key):
    """REC 令牌求均值; 全 -1 (平台无此指标) -> None"""
    vals = [num(r, key) for r in recs]
    vals = [v for v in vals if v is not None and v != -1.0]
    return mean(vals) if vals else None

def pct(new, old):
    if new is None or old is None or old == 0:
        return None
    return (new - old) / old * 100.0

def fmt_pct(v):
    return ("%+.1f%%" % v) if v is not None else "-"

def fmt_int(v):
    return ("%d" % round(v)) if v is not None else "-"

def main():
    if len(sys.argv) < 2:
        print(__doc__)
        sys.exit(2)
    stats = []
    for p in sys.argv[1:]:
        case, recs = parse(p)
        if not recs:
            print("ERROR no REC in %s" % p, file=sys.stderr)
            sys.exit(3)
        pre = {("pre_" + m): avg_key(recs, "pre_" + m)
               for m in METRICS if ("pre_" + m) in recs[0]}
        post = {("post_" + m): avg_key(recs, "post_" + m)
                for m in METRICS if ("post_" + m) in recs[0]}
        stats.append({
            "file": p,
            "size_mib": case.get("size_mib", "?"),
            "mode": case.get("mode", "?"),
            "ballast": case.get("ballast_mib", "?"),
            "runs": len(recs),
            "fill_avg": mean([num(r, "fill_ms") for r in recs]),
            "fill_min": min(num(r, "fill_ms") for r in recs),
            "fill_max": max(num(r, "fill_ms") for r in recs),
            "unit_avg": mean([num(r, "unit") for r in recs]),
            "unit_label": recs[0].get("unit_label", "?"),
            "mmap_avg": mean([num(r, "mmap_us") for r in recs]),
            "unmap_avg": mean([num(r, "unmap_ms") for r in recs]),
            "faults": avg_key(recs, "faults"),
            "majflt": avg_key(recs, "majflt"),
            "pre": pre, "post": post,
            "has_post": bool(post),
        })
    base = stats[0]
    any_post = any(s["has_post"] for s in stats)
    print("COMPARE files=%d base=%s size_mib=%s mode=%s" %
          (len(stats), base["file"], base["size_mib"], base["mode"]))
    print()

    # ① 耗时表
    hdr = "%-28s %8s %4s  %-24s %16s %8s %8s %8s" % (
        "file", "ballast", "runs", "fill_ms avg/min/max",
        "unit", "mmap_us", "unmap_ms", "faults")
    print("[耗时表]")
    print(hdr)
    print("-" * len(hdr))
    for s in stats:
        print("%-28s %8s %4d  %6.2f / %6.2f / %6.2f  %10.1f %-7s %8.1f %8.1f %8s" % (
            s["file"], s["ballast"], s["runs"],
            s["fill_avg"], s["fill_min"], s["fill_max"],
            s["unit_avg"], s["unit_label"], s["mmap_avg"], s["unmap_avg"],
            fmt_int(s["faults"])))
    print()

    # ②③ 状态表 + 驻留开销
    def state_block(title, key_prefix, delta_vs_base):
        col_w = [max(len(s["file"]), 8) for s in stats]
        if delta_vs_base and len(stats) > 1:
            dlab = ["Δ"] if len(stats) == 2 else \
                   ["Δ:%s" % s["file"][:12] for s in stats[1:]]
            d_w = [max(len(l), 8) for l in dlab]
        print(title)
        print("%-13s" % "指标" +
              "".join("%*s" % (w + 2, s["file"]) for w, s in zip(col_w, stats)) +
              ("".join("%*s" % (w + 2, l) for w, l in zip(d_w, dlab))
               if delta_vs_base and len(stats) > 1 else ""))
        print("-" * (13 + sum(w + 2 for w in col_w) +
                     (sum(w + 2 for w in d_w) if delta_vs_base and len(stats) > 1 else 0)))
        for m in METRICS:
            k = key_prefix + m
            if not any(k in s[key_prefix[:-1]] for s in stats if key_prefix[:-1] in s):
                continue
            row = "%-13s" % m
            for s, w in zip(stats, col_w):
                v = s[key_prefix[:-1]].get(k)
                row += "%*s" % (w + 2, "N/A" if v is None else "%.1f" % v)
            if delta_vs_base and len(stats) > 1:
                for s, w in zip(stats[1:], d_w):
                    row += "%*s" % (w + 2, fmt_pct(
                        pct(s[key_prefix[:-1]].get(k), base[key_prefix[:-1]].get(k))))
            print(row)
        print()

    state_block("[pre_* 申请前快照] (每轮申请时刻前均值, MiB; N/A=平台无此指标)",
                "pre_", True)
    if any_post:
        state_block("[post_* 申请驻留时快照] (fill 后驻留期均值, MiB)", "post_", True)
        # 驻留开销 post - pre
        col_w = [max(len(s["file"]), 8) for s in stats]
        print("[驻留开销 post−pre] (申请 SIZE 对系统状态的净影响, MiB)")
        print("%-13s" % "指标" +
              "".join("%*s" % (w + 2, s["file"]) for w, s in zip(col_w, stats)))
        print("-" * (13 + sum(w + 2 for w in col_w)))
        for m in METRICS:
            kp, ks = "pre_" + m, "post_" + m
            if not any(s["has_post"] for s in stats):
                continue
            row = "%-13s" % m
            for s, w in zip(stats, col_w):
                a, b = s["pre"].get(kp), s["post"].get(ks)
                row += "%*s" % (w + 2, "-" if (a is None or b is None)
                                else "%+.1f" % (b - a))
            print(row)
        print()

    # ④ Δ 汇总
    if len(stats) > 1:
        for s in stats[1:]:
            print("DELTA %-28s vs base: fill_ms=%s unit=%s unmap_ms=%s faults=%s pre_free=%s" % (
                s["file"],
                fmt_pct(pct(s["fill_avg"], base["fill_avg"])),
                fmt_pct(pct(s["unit_avg"], base["unit_avg"])),
                fmt_pct(pct(s["unmap_avg"], base["unmap_avg"])),
                fmt_pct(pct(s["faults"], base["faults"])),
                fmt_pct(pct(s["pre"].get("pre_free"), base["pre"].get("pre_free")))))
        print()

    # ⑤ 机器可读行
    print("# 机器可读行 (key=value, 与用例记录同风格):")
    for s in stats:
        row = ("ROW file=%s size_mib=%s mode=%s ballast_mib=%s runs=%d "
               "fill_ms_avg=%.3f fill_ms_min=%.3f fill_ms_max=%.3f "
               "unit_avg=%.1f unit_label=%s mmap_us_avg=%.1f unmap_ms_avg=%.3f "
               "faults_avg=%s majflt_avg=%s" % (
                   s["file"], s["size_mib"], s["mode"], s["ballast"], s["runs"],
                   s["fill_avg"], s["fill_min"], s["fill_max"],
                   s["unit_avg"], s["unit_label"], s["mmap_avg"], s["unmap_avg"],
                   "-1" if s["faults"] is None else "%.1f" % s["faults"],
                   "-1" if s["majflt"] is None else "%.1f" % s["majflt"]))
        for m in METRICS:
            v = s["pre"].get("pre_" + m)
            row += " pre_%s_avg=%s" % (m, "-1" if v is None else "%.1f" % v)
        for m in METRICS:
            if s["has_post"]:
                v = s["post"].get("post_" + m)
                row += " post_%s_avg=%s" % (m, "-1" if v is None else "%.1f" % v)
        print(row)
    for s in stats[1:]:
        row = ("DELTA file=%s vs=%s fill_ms_pct=%s unit_pct=%s unmap_ms_pct=%s faults_pct=%s" % (
            s["file"], base["file"],
            fmt_pct(pct(s["fill_avg"], base["fill_avg"])),
            fmt_pct(pct(s["unit_avg"], base["unit_avg"])),
            fmt_pct(pct(s["unmap_avg"], base["unmap_avg"])),
            fmt_pct(pct(s["faults"], base["faults"]))))
        for m in METRICS:
            p = pct(s["pre"].get("pre_" + m), base["pre"].get("pre_" + m))
            if p is not None:
                row += " pre_%s_pct=%s" % (m, fmt_pct(p))
        if s["has_post"] and base["has_post"]:
            for m in METRICS:
                p = pct(s["post"].get("post_" + m), base["post"].get("post_" + m))
                if p is not None:
                    row += " post_%s_pct=%s" % (m, fmt_pct(p))
        print(row)

if __name__ == "__main__":
    main()