5328320396
- 新增 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 份文档
120 lines
4.4 KiB
Python
Executable File
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()
|