From 37dbd7886b11f526ad0289e379d05f04000dfcc3 Mon Sep 17 00:00:00 2001 From: mouxin <494624263qq@gmail.com> Date: Sun, 12 Apr 2026 21:22:25 +0800 Subject: [PATCH] Improve stat-cache-hitrate UX and running metric normalization --- .../skills/stat-cache-hitrate/SKILL.md | 5 +- .../scripts/stat_cache_hitrate.py | 71 ++++++++++++------- .../scripts/window_utils.py | 8 ++- 3 files changed, 51 insertions(+), 33 deletions(-) diff --git a/fastdeploy/golang_router/.claude/skills/stat-cache-hitrate/SKILL.md b/fastdeploy/golang_router/.claude/skills/stat-cache-hitrate/SKILL.md index f9c5156ca69..e7925127dec 100644 --- a/fastdeploy/golang_router/.claude/skills/stat-cache-hitrate/SKILL.md +++ b/fastdeploy/golang_router/.claude/skills/stat-cache-hitrate/SKILL.md @@ -23,10 +23,9 @@ IMPORTANT: 执行前阅读 references/log_formats.md 了解日志格式和解析 运行脚本前,Claude 必须先向用户确认以下参数: ### 1. 日志文件路径 -使用 AskUserQuestion 工具向用户询问日志文件路径。提供两个常用快捷选项 + Other 自定义输入(支持绝对路径和相对路径): +使用 AskUserQuestion 工具向用户询问日志文件路径。提供两个常用快捷选项(客户端会自动提供 Other 自定义输入): - 选项 1: `logs/router.log`(默认) - 选项 2: `fd-router.log`(golang_router 根目录常用文件名) -- 选项 3: Other(用户直接输入任意路径,例如 `logs/fd-router.log`、`/home/user/logs/router.log`) **重要规则**: - 如果用户已经在消息中明确指定了日志路径,直接使用该路径,跳过询问步骤 @@ -95,7 +94,7 @@ python3 .claude/skills/stat-cache-hitrate/scripts/stat_cache_hitrate.py <日志 - 主报告 `cache_hitrate_report_*.md` — Per-Worker 统计 + Fallback 明细 - `details/per_window_data.md` — 每5s窗口明细(连续空窗口自动合并为 3 行:起始/合并说明/结束) -- `details/session_hit_details.md` — 每个 session 的命中明细(`session / req_count / first_hit / avg_hit(excl_first) / max_hit / min_hit / all_hits`),并附带 `prefill_urls`、prefill URL 切换前后 request_id(或 req_id/trace_id)以及命中率突降 request_id +- `details/session_hit_details.md` — 每个 session 的命中明细(TSV 单行格式,便于横向滚动查看),包含 `session / req_count / first_hit / avg_hit(excl_first) / max_hit / min_hit / all_hits / prefill_urls / switch_req_pairs / sharp_drop_request_ids` ### 交叉诊断矩阵 diff --git a/fastdeploy/golang_router/.claude/skills/stat-cache-hitrate/scripts/stat_cache_hitrate.py b/fastdeploy/golang_router/.claude/skills/stat-cache-hitrate/scripts/stat_cache_hitrate.py index 7adc0b97d02..1476e61d724 100644 --- a/fastdeploy/golang_router/.claude/skills/stat-cache-hitrate/scripts/stat_cache_hitrate.py +++ b/fastdeploy/golang_router/.claude/skills/stat-cache-hitrate/scripts/stat_cache_hitrate.py @@ -48,6 +48,28 @@ def _build_path_links(path): file_uri = "file://" + quote(abs_path, safe="/:-._~") return abs_path, file_uri + +def _format_half_running(total_running): + """将 stats.total_running 归一化为 prefill 口径(decode+prefill 合计 / 2)。""" + normalized = total_running / 2 + if float(normalized).is_integer(): + return str(int(normalized)) + return f"{normalized:.1f}" + + +def _render_scrollable_tsv(data, columns): + """渲染单行 TSV 文本,适合在 Markdown 查看器里横向滚动。""" + if not data: + return "```tsv\n(no data)\n```" + + def _escape(v): + return str(v).replace("\t", " ").replace("\n", "\\n") + + lines = ["\t".join(columns)] + for row in data: + lines.append("\t".join(_escape(row.get(col, "")) for col in columns)) + return "```tsv\n" + "\n".join(lines) + "\n```" + # ════════════════════════════════════════════════════════════════ # Phase 1: 日志读取 # ════════════════════════════════════════════════════════════════ @@ -461,8 +483,8 @@ def format_full_report(filepath, line_count, prefix_hr, session_hr, per_worker, parts.append( render_table( window_rows[:10], - columns=["Time", "Prefix HR", "Session HR", "Scoring", "Fallback", "Total Running"], - right_align={"Scoring", "Fallback", "Total Running"}, + columns=["Time", "Prefix HR", "Session HR", "Scoring", "Fallback", "Total Running (prefill≈stats/2)"], + right_align={"Scoring", "Fallback", "Total Running (prefill≈stats/2)"}, ) ) @@ -542,7 +564,7 @@ def build_per_window_rows(strategies, stats_recs): else: session_hr = "-" - running = str(d["running"]) if d["has_running"] else "-" + running = _format_half_running(d["running"]) if d["has_running"] else "-" rows.append( { "Time": short_ts, @@ -550,7 +572,7 @@ def build_per_window_rows(strategies, stats_recs): "Session HR": session_hr, "Scoring": str(d["scoring"]), "Fallback": str(d["fallback"]), - "Total Running": running, + "Total Running (prefill≈stats/2)": running, } ) return rows @@ -686,8 +708,8 @@ def save_detailed_report( detail_parts.append( render_table( window_rows_merged, - columns=["Time", "Prefix HR", "Session HR", "Scoring", "Fallback", "Total Running"], - right_align={"Scoring", "Fallback", "Total Running"}, + columns=["Time", "Prefix HR", "Session HR", "Scoring", "Fallback", "Total Running (prefill≈stats/2)"], + right_align={"Scoring", "Fallback", "Total Running (prefill≈stats/2)"}, ) ) detail_parts.append("") @@ -720,27 +742,22 @@ def save_detailed_report( f' (N={session_summary["non_first_total"]})' ) session_parts.append("") - session_parts.append("## 明细表") - session_parts.append( - render_table( - session_rows, - columns=[ - "session", - "req_count", - "first_hit", - "avg_hit(excl_first)", - "max_hit", - "min_hit", - "all_hits", - "prefill_urls", - "switch_req_pairs", - "sharp_drop_request_ids", - "sticky", - "unique_workers", - ], - right_align={"req_count", "first_hit", "avg_hit(excl_first)", "max_hit", "min_hit", "unique_workers"}, - ) - ) + session_columns = [ + "session", + "req_count", + "first_hit", + "avg_hit(excl_first)", + "max_hit", + "min_hit", + "all_hits", + "prefill_urls", + "switch_req_pairs", + "sharp_drop_request_ids", + "sticky", + "unique_workers", + ] + session_parts.append("## 明细(单行 TSV,可横向滚动)") + session_parts.append(_render_scrollable_tsv(session_rows, session_columns)) session_parts.append("") session_path = os.path.join(details_dir, "session_hit_details.md") diff --git a/fastdeploy/golang_router/.claude/skills/stat-cache-hitrate/scripts/window_utils.py b/fastdeploy/golang_router/.claude/skills/stat-cache-hitrate/scripts/window_utils.py index 4ff6aa666d5..4e09710f6f9 100644 --- a/fastdeploy/golang_router/.claude/skills/stat-cache-hitrate/scripts/window_utils.py +++ b/fastdeploy/golang_router/.claude/skills/stat-cache-hitrate/scripts/window_utils.py @@ -3,6 +3,8 @@ 窗口明细压缩工具:合并连续空窗口,降低 per_window_data.md 噪声。 """ +RUNNING_COL = "Total Running (prefill≈stats/2)" + def _is_blank_window_row(row): """判断是否为空窗口(无 Prefix/Session 明细值)。""" @@ -52,7 +54,7 @@ def merge_blank_window_rows(rows, min_merge_len=5): "Session HR": "-", "Scoring": "0", "Fallback": "0", - "Total Running": rows[i].get("Total Running", "-"), + RUNNING_COL: rows[i].get(RUNNING_COL, "-"), } ) merged.append( @@ -62,7 +64,7 @@ def merge_blank_window_rows(rows, min_merge_len=5): "Session HR": f"merged {seg_len} windows", "Scoring": "0", "Fallback": "0", - "Total Running": "-", + RUNNING_COL: "-", } ) merged.append( @@ -72,7 +74,7 @@ def merge_blank_window_rows(rows, min_merge_len=5): "Session HR": "-", "Scoring": "0", "Fallback": "0", - "Total Running": rows[j - 1].get("Total Running", "-"), + RUNNING_COL: rows[j - 1].get(RUNNING_COL, "-"), } ) i = j