#!/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()
