Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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`)

**重要规则**:
- 如果用户已经在消息中明确指定了日志路径,直接使用该路径,跳过询问步骤
Expand Down Expand Up @@ -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`

### 交叉诊断矩阵

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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: 日志读取
# ════════════════════════════════════════════════════════════════
Expand Down Expand Up @@ -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)"},
)
)

Expand Down Expand Up @@ -542,15 +564,15 @@ 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,
"Prefix HR": prefix_hr,
"Session HR": session_hr,
"Scoring": str(d["scoring"]),
"Fallback": str(d["fallback"]),
"Total Running": running,
"Total Running (prefill≈stats/2)": running,
}
)
return rows
Expand Down Expand Up @@ -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("")
Expand Down Expand Up @@ -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")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@
窗口明细压缩工具:合并连续空窗口,降低 per_window_data.md 噪声。
"""

RUNNING_COL = "Total Running (prefill≈stats/2)"


def _is_blank_window_row(row):
"""判断是否为空窗口(无 Prefix/Session 明细值)。"""
Expand Down Expand Up @@ -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(
Expand All @@ -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(
Expand All @@ -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
Expand Down
Loading