Files
openclaw-config/scripts/perf-analyze.py
T
yangxuan 5328320396 docs+perf: 定位 API 慢的真实瓶颈并移除 new-api 残留
- 新增 docs/6 openclaw-API响应性能分析与优化.md:两天 419 次调用 / 69 段交互实测,
  deepseek-flash p50 302ms 而模型耗时仅占 5.4%,94.6% 花在串行工具循环;
  new-api 慢 5 倍(p50 1463ms)已消除;含 P0/P1/P2 优化建议与未验证项声明
- 新增 scripts/perf-analyze.py:复现时间预算分析的配套脚本
- 5 个 agent 的 models.json 移除残留 new-api provider(含指向 192.168.2.74:3000
  与 100.115.195.188:3000 的条目及明文 key),主模型回退 deepseek-flash
- README 登记第 6 份文档
2026-09-16 16:23:32 +08:00

120 lines
4.4 KiB
Python
Executable File

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