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 @@ -4,7 +4,7 @@ description: >
统计 FastDeploy Go Router 日志中的三层 cache 命中率指标,生成可视化报告。
三层指标:Prefix Hit Ratio(KV Cache 内容复用度)、Session Hit Rate(请求级路由粘性)、
Per-Worker Cache Stats(各 prefill worker 的缓存利用排名)。支持全量统计、tail 快速查看、
持续监控模式、指定时间段统计(--start/--end)。
指定时间段统计(--start/--end)。

当用户提到以下内容时触发此 skill:统计/查看 cache 命中率、查看 cache-aware 调度效果、
查看缓存预热情况、统计 hitRatio、查看 prefix 命中率、session hit rate。
Expand Down Expand Up @@ -37,9 +37,8 @@ IMPORTANT: 执行前阅读 references/log_formats.md 了解日志格式和解析
### 2. 分析模式
必须使用 **AskUserQuestion 的离散选项**(不要只发纯文本编号,避免客户端偶发不显示第 4 项):
- 选项 1: `全量统计(默认)` — 扫描完整日志
- 选项 2: `快速查看尾部` — 只看最近的数据(可指定行数如 2000 或时间如 30m)
- 选项 3: `持续监控` — 全量分析后提示监控命令
- 选项 4: `指定时间段` — 分析特定时间范围(如 `--start "16:00" --end "17:00"`)
- 选项 2: `快速查看尾部` — 只看最近的数据(可指定 `2000/2k` 行,或 `30m/2h/1d` 时间窗口)
- 选项 3: `指定时间段` — 分析特定时间范围(如 `--start "16:00" --end "17:00"`)

若用户选择“指定时间段”,直接让用户填写:
- 从 `xxx` 开始,到 `xxx` 结束(`start/end` 可只填一个);
Expand All @@ -66,10 +65,10 @@ python3 .claude/skills/stat-cache-hitrate/scripts/stat_cache_hitrate.py <日志
# 快速查看尾部数据
python3 .claude/skills/stat-cache-hitrate/scripts/stat_cache_hitrate.py <日志文件> --tail # 默认最后 2000 行
python3 .claude/skills/stat-cache-hitrate/scripts/stat_cache_hitrate.py <日志文件> --tail 5000 # 指定行数
python3 .claude/skills/stat-cache-hitrate/scripts/stat_cache_hitrate.py <日志文件> --tail 30m # 指定时间

# 持续监控
python3 .claude/skills/stat-cache-hitrate/scripts/stat_cache_hitrate.py <日志文件> --watch
python3 .claude/skills/stat-cache-hitrate/scripts/stat_cache_hitrate.py <日志文件> --tail 2k # 行数缩写
python3 .claude/skills/stat-cache-hitrate/scripts/stat_cache_hitrate.py <日志文件> --tail 30m # 分钟窗口
python3 .claude/skills/stat-cache-hitrate/scripts/stat_cache_hitrate.py <日志文件> --tail 2h # 小时窗口
python3 .claude/skills/stat-cache-hitrate/scripts/stat_cache_hitrate.py <日志文件> --tail 1d # 天窗口

# 指定时间段(--start 和 --end 可单独或同时使用)
python3 .claude/skills/stat-cache-hitrate/scripts/stat_cache_hitrate.py <日志文件> --start "16:00:00" --end "17:00:00"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -198,14 +198,4 @@ output = f"{bar} {percentage}% (N={count})"
+---+---+---+---+---→
-5m -4m -3m -2m -1m

💡 持续跟踪: /loop 30s /analyze-cache-hitrate --tail
```

## --watch 持续监控模板

`--watch` 模式先输出完整报告(同终端概览报告模板),末尾额外提示:

```
💡 全量分析完成。持续跟踪后续变化:
/loop 30s /analyze-cache-hitrate --tail
```
Original file line number Diff line number Diff line change
Expand Up @@ -8,11 +8,12 @@
3. Per-Worker Stats — 各 worker 缓存利用排名

用法:
python3 stat_cache_hitrate.py <log_file> [--tail N|Nm] [--watch] [--output DIR]
python3 stat_cache_hitrate.py <log_file> [--tail N|2k|30m|2h|1d] [--output DIR]
"""

import argparse
import json
import math
import os
import re
import subprocess
Expand Down Expand Up @@ -170,7 +171,7 @@ def count_lines(filepath):

def read_lines(filepath, tail=None):
"""读取日志文件,支持 tail 模式。"""
if tail:
if tail is not None:
if isinstance(tail, str) and tail.endswith("m"):
# 按时间 tail:读取全部行,过滤最近 N 分钟
minutes = int(tail[:-1])
Expand Down Expand Up @@ -282,7 +283,7 @@ def extract_data(filepath, tail=None):
strategy_recs = grep_and_parse(filepath, STRATEGY_PATTERN, "parse-cache-strategy", tail)
stats_recs = grep_and_parse(filepath, STATS_PATTERN, "parse-stats", tail)
inference_count = grep_count(filepath, r"\] \[POST\] /v1/chat/completions |\] \[POST\] /v1/completions ", tail)
line_count = int(tail) if tail and not (isinstance(tail, str) and tail.endswith("m")) else total
line_count = int(tail) if tail is not None and not (isinstance(tail, str) and tail.endswith("m")) else total
return strategy_recs, stats_recs, inference_count, line_count


Expand Down Expand Up @@ -984,8 +985,12 @@ def parse_args():
epilog=__doc__,
)
parser.add_argument("log_file", help="日志文件路径")
parser.add_argument("--tail", nargs="?", const="2000", help="只分析尾部数据(行数如 2000,或时间如 30m)")
parser.add_argument("--watch", action="store_true", help="全量分析后提示持续监控命令")
parser.add_argument(
"--tail",
nargs="?",
const="2000",
help="只分析尾部数据(支持 2000/2k 行,或 30m/2h/1d 时间窗口)",
)
parser.add_argument(
"--output", default=None, help="详细报告输出目录(默认:skill_output/stat-cache-hitrate/<timestamp>/)"
)
Expand All @@ -996,6 +1001,45 @@ def parse_args():
return parser.parse_args()


def parse_tail_arg(tail_str):
"""解析 --tail 参数,返回 int(行数) 或 '<minutes>m'(时间窗口)。"""
if tail_str is None:
return None

s = str(tail_str).strip().lower()
if not s:
raise ValueError("--tail 不能为空")

# 行数: 2000
if re.fullmatch(r"\d+", s):
value = int(s)
if value <= 0:
raise ValueError("--tail 行数必须 > 0")
return value

# 行数缩写: 2k => 2000
m = re.fullmatch(r"(\d+)k", s)
if m:
value = int(m.group(1)) * 1000
if value <= 0:
raise ValueError("--tail 行数必须 > 0")
return value

# 时间窗口: 30m/2h/1d(最终统一成分钟)
m = re.fullmatch(r"(\d+)(m|h|d)", s)
if m:
num = int(m.group(1))
unit = m.group(2)
if num <= 0:
raise ValueError("--tail 时间窗口必须 > 0")
factor = {"m": 1, "h": 60, "d": 1440}[unit]
minutes = num * factor
minutes = max(1, math.ceil(minutes))
return f"{minutes}m"

raise ValueError("不支持的 --tail 格式:请使用 2000/2k 或 30m/2h/1d")


def main():
args = parse_args()

Expand All @@ -1009,6 +1053,12 @@ def main():
print("Error: --tail 与 --start/--end 不能同时使用,请选择其一", file=sys.stderr)
sys.exit(1)

try:
tail = parse_tail_arg(args.tail)
except ValueError as e:
print(f"Error: {e}", file=sys.stderr)
sys.exit(1)

# 时间范围预过滤(--start 和 --end 可单独或同时指定)
import atexit

Expand All @@ -1023,7 +1073,7 @@ def main():
print(f'时间范围过滤: {start_ts or "..."} ~ {end_ts or "..."}', file=sys.stderr)

# Phase 2: 提取 + 解析
strategy_recs, stats_recs, inference_count, line_count = extract_data(log_file, args.tail)
strategy_recs, stats_recs, inference_count, line_count = extract_data(log_file, tail)

if not strategy_recs and not stats_recs:
print(
Expand All @@ -1039,7 +1089,7 @@ def main():
diagnosis = cross_diagnose(prefix_hr, session_hr)

# Phase 4: 输出
if args.tail:
if tail is not None:
print(format_tail_report(args.log_file, line_count, prefix_hr, session_hr, scheduling))
else:
time_span = compute_time_span(strategy_recs, stats_recs)
Expand Down Expand Up @@ -1094,9 +1144,5 @@ def main():
print(f" - Session 明细: {session_abs}")
print(f" URI: {session_uri}")

if args.watch:
print("\n\U0001f4a1 持续跟踪: /loop 30s /stat-cache-hitrate --tail")


if __name__ == "__main__":
main()
Loading