#!/usr/bin/env python3 """openclaw API 响应性能分析 —— 复现 docs/6 的时间预算结论。 用法: python3 ~/.openclaw/scripts/perf-analyze.py [日志文件 ...] 缺省分析 /tmp/openclaw/openclaw-<前一天>.log 与 <当天>.log。 核心指标: 模型调用耗时的「时间占比」(占比低 = 时间花在模型之外)。 """ import json, re, sys, datetime, glob, os from collections import defaultdict, Counter IDLE_GAP = 120 # 秒: 超过此空闲视为新的一段交互 def default_logs(): today = datetime.date.today() out = [] for d in (today - datetime.timedelta(days=1), today): p = "/tmp/openclaw/openclaw-%s.log" % d.isoformat() if os.path.exists(p): out.append(p) return out or sorted(glob.glob("/tmp/openclaw/openclaw-*.log")) def sec(ts): return datetime.datetime.fromisoformat(ts).timestamp() def main(): logs = sys.argv[1:] or default_logs() if not logs: sys.exit("找不到日志: /tmp/openclaw/openclaw-*.log") resps, starts, retries, delays = [], [], [], [] for L in logs: with open(L, encoding="utf-8", errors="replace") as fh: for line in fh: if '"[model-fetch]' not in line and "empty-error-retry" not in line \ and "heartbeat delayed" not in line: continue try: o = json.loads(line) except Exception: continue t, m = o.get("time", ""), o.get("message", "") if not t: continue if "[model-fetch] start" in m: p = dict(re.findall(r"(\w+)=(\S+)", m)) starts.append((t, p.get("provider"), p.get("model"))) elif "[model-fetch] response" in m: p = dict(re.findall(r"(\w+)=(\S+)", m)) try: e = int(p.get("elapsedMs", 0)) except ValueError: e = 0 resps.append((t, p.get("provider"), p.get("model"), e, p.get("status"))) if "empty-error-retry" in m: retries.append(m[:150]) if "heartbeat delayed" in m: delays.append(m[:100]) resps.sort() starts.sort() if not resps: sys.exit("日志中无 model-fetch 记录") print("日志文件: %s" % ", ".join(logs)) print("调用总数: start=%d response=%d" % (len(starts), len(resps))) print("status 分布: %s" % dict(Counter(r[4] for r in resps))) print("\n== 按 provider/model 的延迟(ms) ==") g = defaultdict(list) for _, pv, mo, e, _ in resps: g["%s/%s" % (pv, mo)].append(e) for k, v in sorted(g.items()): v.sort() print(" %-28s n=%-4d p50=%-6d p90=%-6d max=%-6d mean=%d" % (k, len(v), v[len(v) // 2], v[int(len(v) * 0.9)], v[-1], sum(v) // len(v))) print("\n== 时间预算(交互段, 空闲 > %ds 切分)==" % IDLE_GAP) sessions, cur = [], [resps[0]] for a, b in zip(resps, resps[1:]): if sec(b[0]) - sec(a[0]) > IDLE_GAP: sessions.append(cur) cur = [b] else: cur.append(b) sessions.append(cur) print(" %-10s %5s %9s %9s %7s %s" % ("开始", "调用", "模型s", "跨度s", "模型占比", "主模型")) tm = ts = 0.0 for s in sessions: if len(s) < 2: continue span = sec(s[-1][0]) - sec(s[0][0]) if span <= 0: continue mod = sum(x[3] for x in s) / 1000.0 tm += mod ts += span top, n = Counter("%s/%s" % (x[1], x[2]) for x in s).most_common(1)[0] print(" %-10s %5d %9.1f %9.1f %6.1f%% %s x%d" % (s[0][0][11:19], len(s), mod, span, 100 * mod / span, top, n)) if ts > 0: print("\n ★ 合计: 模型 %.1fs / 跨度 %.1fs = %.1f%%" % (tm, ts, 100 * tm / ts)) print(" 非模型时间(工具/IO/上下文/等待) %.1f%% <- 高即说明瓶颈不在 API" % (100 - 100 * tm / ts)) print("\n== 异常事件 ==") print(" empty-error-retry: %d 次" % len(retries)) for r, c in Counter(retries).most_common(3): print(" %dx %s" % (c, r)) print(" heartbeat delayed: %d 次" % len(delays)) for r, c in Counter(delays).most_common(3): print(" %dx %s" % (c, r)) if __name__ == "__main__": main()