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 @@ -125,7 +125,7 @@ def complete_time_arg(time_str, log_file, is_end=False):
if m:
mo, d = m.group(1).zfill(2), m.group(2).zfill(2)
ts = _get_log_boundary_ts(log_file, "first")
year = ts[:4] if ts else "2026"
year = ts[:4] if ts else str(datetime.now().year)
if m.group(3): # 有时间部分
h, mi = m.group(3).zfill(2), m.group(4)
s = (m.group(5) or "00").zfill(2)
Expand All @@ -139,7 +139,7 @@ def complete_time_arg(time_str, log_file, is_end=False):
h, mi = m.group(1).zfill(2), m.group(2)
s = (m.group(3) or "00").zfill(2)
ts = _get_log_boundary_ts(log_file, "last")
date_part = ts[:10] if ts else "2026/01/01"
date_part = ts[:10] if ts else f"{datetime.now().year}/01/01"
return f"{date_part} {h}:{mi}:{s}"

# Fallback: 原样返回
Expand Down Expand Up @@ -204,9 +204,10 @@ def extract_tags(line):
# Cache-Aware 策略行解析(类别 A)
# ════════════════════════════════════════════════════════════════

URL_RE = r"(?:https?://)?[A-Za-z0-9.-]+(?::\d+)?"
STRATEGY_RE = re.compile(r"final strategy:\s*(\w+)")
SELECTED_RE = re.compile(r"selected=(http://\S+?)(?:,|\s|$)")
REASON_RE = re.compile(r"reason:\s*(.+?)(?:,\s*loads=|$)")
SELECTED_RE = re.compile(rf"selected=({URL_RE})(?:,|\s|$)")
REASON_RE = re.compile(r"reason:\s*(.+?)(?:,\s*loads=|\.?\s*ts_ms=|$)")


def parse_cache_strategy_line(line):
Expand Down Expand Up @@ -271,7 +272,7 @@ def parse_cache_strategy_line(line):
# ════════════════════════════════════════════════════════════════

TOTAL_RUNNING_RE = re.compile(r"total_running=(\d+)")
WORKER_RUNNING_RE = re.compile(r"(http://[^:]+:\d+): running=(\d+)")
WORKER_RUNNING_RE = re.compile(rf"({URL_RE}): running=(\d+)")
CACHE_HR_RE = re.compile(r"cache_hit_rate=([\d.]+)%\s*\(hits=(\d+)/total=(\d+)\)")


Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
import argparse
import json
import os
import re
import subprocess
import sys
from collections import defaultdict
Expand All @@ -32,6 +33,10 @@
)
from stats import compute_statistics, count_by, time_bucket


def _strip_scheme(url):
return re.sub(r"^https?://", "", url)

# ════════════════════════════════════════════════════════════════
# Phase 1: 日志读取
# ════════════════════════════════════════════════════════════════
Expand Down Expand Up @@ -235,7 +240,7 @@ def compute_per_worker_stats(strategies):
avg_hr = round(sum(data["hit_ratios"]) / len(data["hit_ratios"]), 1) if data["hit_ratios"] else 0
result.append(
{
"Worker": worker.replace("http://", ""),
"Worker": _strip_scheme(worker),
"Selected": data["selected_count"],
"Select%": f"{round(data['selected_count'] / total_scoring * 100, 1)}%",
"AvgHitRatio": f"{avg_hr}%",
Expand Down Expand Up @@ -339,7 +344,7 @@ def _quartile_trend(trend, value_field):
return f"Q1={quartiles[0]}% \u2192 Q2={quartiles[1]}% \u2192 Q3={quartiles[2]}% \u2192 Q4={quartiles[3]}% {arrow}"


def format_full_report(filepath, line_count, prefix_hr, session_hr, per_worker, scheduling, diagnosis, time_span=None):
def format_full_report(filepath, line_count, prefix_hr, session_hr, per_worker, scheduling, diagnosis, time_span=None, window_rows=None):
"""格式化完整终端报告。"""
parts = []

Expand All @@ -361,6 +366,7 @@ def format_full_report(filepath, line_count, prefix_hr, session_hr, per_worker,
dist_data = [
{"label": d["range"] + "%", "value": d["pct"], "count": d["count"]} for d in prefix_hr["distribution"]
]
parts.append(" Unicode 柱状图(Prefix HR 分布):")
parts.append(render_bar(dist_data, show_count=True))

parts.append(f' 冷启动率: {prefix_hr["cold_start_rate"]}%')
Expand All @@ -375,6 +381,7 @@ def format_full_report(filepath, line_count, prefix_hr, session_hr, per_worker,
{"bucket": t["bucket"], "value": t.get("selected_hitRatio_mean", 0)} for t in prefix_hr["trend"]
]
parts.append("")
parts.append(" ASCII 折线图(Prefix HR 趋势):")
parts.append(render_sparkline(sparkline_data, title="Prefix HR Trend", y_label="%", y_range=(0, 100)))
else:
parts.append(" (无 cache_aware_scoring 数据)")
Expand All @@ -391,6 +398,7 @@ def format_full_report(filepath, line_count, prefix_hr, session_hr, per_worker,

if session_hr["trend"]:
parts.append("")
parts.append(" ASCII 折线图(Session HR 趋势):")
parts.append(render_sparkline(session_hr["trend"], title="Session HR Trend", y_label="%", y_range=(0, 100)))
parts.append("")

Expand Down Expand Up @@ -428,6 +436,18 @@ def format_full_report(filepath, line_count, prefix_hr, session_hr, per_worker,
parts.append(f' {diagnosis["icon"]} {diagnosis["summary"]}')
parts.append(f' {diagnosis["detail"]}')

# 6. 每窗口明细预览
if window_rows:
parts.append("")
parts.append("### 6. 每5s窗口明细预览(前10行)")
parts.append(
render_table(
window_rows[:10],
columns=["Time", "Prefix HR", "Session HR", "Scoring", "Fallback", "Total Running"],
right_align={"Scoring", "Fallback", "Total Running"},
)
)

return "\n".join(parts)


Expand Down Expand Up @@ -458,7 +478,77 @@ def format_tail_report(filepath, line_count, prefix_hr, session_hr, scheduling):
return "\n".join(parts)


def save_detailed_report(filepath, strategies, stats_recs, prefix_hr, session_hr, per_worker, scheduling, output_dir):
def build_per_window_rows(strategies, stats_recs):
"""构建每窗口明细行,用于终端预览和 details 导出。"""
time_data = defaultdict(
lambda: {
"prefix_vals": [],
"hits": 0,
"total": 0,
"scoring": 0,
"fallback": 0,
"running": 0,
"has_running": False,
}
)
for r in strategies:
ts = r.get("ts", "")
if r.get("strategy") == "cache_aware_scoring":
time_data[ts]["scoring"] += 1
time_data[ts]["prefix_vals"].append(r.get("selected_hitRatio", 0))
else:
time_data[ts]["fallback"] += 1

for r in stats_recs:
ts = r.get("ts", "")
time_data[ts]["hits"] += r.get("hits", 0)
time_data[ts]["total"] += r.get("total", 0)
if "total_running" in r:
time_data[ts]["running"] += r.get("total_running", 0)
time_data[ts]["has_running"] = True

rows = []
for ts in sorted(time_data.keys()):
d = time_data[ts]
short_ts = ts.split(" ")[-1] if " " in ts else ts
if d["prefix_vals"]:
prefix_mean = round(sum(d["prefix_vals"]) / len(d["prefix_vals"]), 1)
prefix_hr = f"{prefix_mean}%"
else:
prefix_hr = "-"

if d["total"] > 0:
session_val = round(d["hits"] / d["total"] * 100, 1)
session_hr = f'{session_val}% ({d["hits"]}/{d["total"]})'
else:
session_hr = "-"

running = str(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,
}
)
return rows


def save_detailed_report(
filepath,
strategies,
stats_recs,
prefix_hr,
session_hr,
per_worker,
scheduling,
diagnosis,
output_dir,
time_span=None,
):
"""导出详细数据 Markdown 文件。

主报告包含 Per-Worker 统计和 Fallback 明细。
Expand All @@ -471,10 +561,63 @@ def save_detailed_report(filepath, strategies, stats_recs, prefix_hr, session_hr
parts.append("# Cache Hit Rate Detailed Report")
parts.append(f'**Generated**: {datetime.now().strftime("%Y-%m-%d %H:%M:%S")}')
parts.append(f"**Source**: {filepath}")
if time_span:
parts.append(f"**Span**: {time_span}")
parts.append("")

# 1) 主指标摘要(与终端一致,避免“只在终端可见”)
parts.append("## 1. Key Metrics Summary")
parts.append("")
parts.append("### Prefix Hit Ratio")
if prefix_hr["stats"]:
parts.append(f'- 累计平均: **{prefix_hr["mean"]}%** (N={prefix_hr["count"]})')
parts.append(f'- 冷启动率: **{prefix_hr["cold_start_rate"]}%**')
trend_str = _quartile_trend(prefix_hr["trend"], "selected_hitRatio_mean")
if trend_str:
parts.append(f"- 趋势: {trend_str}")
dist_data = [{"label": d["range"] + "%", "value": d["pct"], "count": d["count"]} for d in prefix_hr["distribution"]]
parts.append("")
parts.append("```text")
parts.append("Unicode 柱状图(Prefix HR 分布)")
parts.append(render_bar(dist_data, show_count=True))
if prefix_hr["trend"]:
sparkline_data = [{"bucket": t["bucket"], "value": t.get("selected_hitRatio_mean", 0)} for t in prefix_hr["trend"]]
parts.append("")
parts.append("ASCII 折线图(Prefix HR 趋势)")
parts.append(render_sparkline(sparkline_data, title="Prefix HR Trend", y_label="%", y_range=(0, 100)))
parts.append("```")
else:
parts.append("- (无 cache_aware_scoring 数据)")
parts.append("")

parts.append("### Session Hit Rate")
parts.append(f'- 累计: **{session_hr["rate"]}%** (hits={session_hr["hits"]}/total={session_hr["total"]})')
parts.append(f'- 覆盖率: **{session_hr["coverage"]}%**')
trend_str = _quartile_trend(session_hr["trend"], "value")
if trend_str:
parts.append(f"- 趋势: {trend_str}")
if session_hr["trend"]:
parts.append("")
parts.append("```text")
parts.append("ASCII 折线图(Session HR 趋势)")
parts.append(render_sparkline(session_hr["trend"], title="Session HR Trend", y_label="%", y_range=(0, 100)))
parts.append("```")
parts.append("")

parts.append("### Scheduling Strategy")
parts.append(
f'- cache_aware_scoring: **{scheduling["scoring_count"]} ({scheduling["scoring_pct"]}%)**'
f' | fallback: **{scheduling["fallback_count"]}**'
)
parts.append(
f'- 非最优命中选择: **{scheduling["suboptimal_pct"]}%**'
f' ({scheduling.get("suboptimal_count", 0)} 次, 负载均衡优先于命中率)'
)
parts.append(f'- Diagnosis: {diagnosis["icon"]} {diagnosis["summary"]};{diagnosis["detail"]}')
parts.append("")

# Per-Worker 完整统计
parts.append("## 1. Per-Worker 完整统计")
# 2) Per-Worker 完整统计
parts.append("## 2. Per-Worker 完整统计")
parts.append("")
if per_worker:
parts.append(
Expand All @@ -486,49 +629,34 @@ def save_detailed_report(filepath, strategies, stats_recs, prefix_hr, session_hr
)
parts.append("")

# Fallback 明细
# 3) Fallback 明细
if scheduling["fallback_reasons"]:
parts.append("## 2. Fallback 明细")
parts.append("## 3. Fallback 明细")
for reason in scheduling["fallback_reasons"]:
parts.append(f'- **{reason["value"]}**: {reason["count"]} 次 ({reason["pct"]}%)')
parts.append("")

# 每窗口明细 → 拆分到 details/
time_data = defaultdict(lambda: {"prefix_hr": "-", "session_hr": "-", "scoring": 0, "fallback": 0, "running": "-"})
for r in strategies:
ts = r.get("ts", "")
if r.get("strategy") == "cache_aware_scoring":
time_data[ts]["scoring"] += 1
else:
time_data[ts]["fallback"] += 1

for r in stats_recs:
ts = r.get("ts", "")
h = r.get("hits", 0)
t = r.get("total", 0)
time_data[ts]["session_hr"] = f"{round(h / t * 100, 1)}% ({h}/{t})" if t else "0%"
time_data[ts]["running"] = str(r.get("total_running", "-"))
window_rows = build_per_window_rows(strategies, stats_recs)

if time_data:
if window_rows:
# 主报告中添加引用
parts.append(
f"> 每窗口明细数据 ({len(time_data)} 条): [details/per_window_data.md](details/per_window_data.md)"
f"> 每5s窗口明细数据 ({len(window_rows)} 条): [details/per_window_data.md](details/per_window_data.md)"
)
parts.append("")

# 写入 details 子目录
details_dir = os.path.join(output_dir, "details")
os.makedirs(details_dir, exist_ok=True)
detail_parts = ["# 每窗口明细数据", ""]
detail_parts.append("| Time | Prefix HR | Session HR | Scoring | Fallback | Total Running |")
detail_parts.append("|------|-----------|------------|---------|----------|---------------|")
for ts in sorted(time_data.keys()):
d = time_data[ts]
short_ts = ts.split(" ")[-1] if " " in ts else ts
detail_parts.append(
f'| {short_ts} | {d["prefix_hr"]} | {d["session_hr"]} '
f'| {d["scoring"]} | {d["fallback"]} | {d["running"]} |'
detail_parts = ["# 每5s窗口明细数据", ""]
detail_parts.append(
render_table(
window_rows,
columns=["Time", "Prefix HR", "Session HR", "Scoring", "Fallback", "Total Running"],
right_align={"Scoring", "Fallback", "Total Running"},
)
)
detail_parts.append("")

detail_path = os.path.join(details_dir, "per_window_data.md")
Expand Down Expand Up @@ -564,8 +692,8 @@ def compute_time_span(strategies, stats_recs):
duration = t_max - t_min
hours = int(duration.total_seconds() // 3600)
minutes = int((duration.total_seconds() % 3600) // 60)
start = t_min.strftime("%H:%M:%S")
end = t_max.strftime("%H:%M:%S")
start = t_min.strftime("%Y-%m-%d %H:%M:%S")
end = t_max.strftime("%Y-%m-%d %H:%M:%S")
if hours > 0:
return f"{start} ~ {end} ({hours}h{minutes}m)"
return f"{start} ~ {end} ({minutes}m)"
Expand Down Expand Up @@ -642,9 +770,18 @@ def main():
print(format_tail_report(args.log_file, line_count, prefix_hr, session_hr, scheduling))
else:
time_span = compute_time_span(strategy_recs, stats_recs)
window_rows = build_per_window_rows(strategy_recs, stats_recs)
print(
format_full_report(
args.log_file, line_count, prefix_hr, session_hr, per_worker, scheduling, diagnosis, time_span
args.log_file,
line_count,
prefix_hr,
session_hr,
per_worker,
scheduling,
diagnosis,
time_span,
window_rows=window_rows,
)
)

Expand All @@ -657,7 +794,16 @@ def main():
run_timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
output_dir = os.path.join(golang_router_root, "skill_output", "stat-cache-hitrate", run_timestamp)
report_path = save_detailed_report(
args.log_file, strategy_recs, stats_recs, prefix_hr, session_hr, per_worker, scheduling, output_dir
args.log_file,
strategy_recs,
stats_recs,
prefix_hr,
session_hr,
per_worker,
scheduling,
diagnosis,
output_dir,
time_span=time_span,
)
print(f"\n\U0001f4c4 详细数据见: {report_path}")

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,10 @@
TOKENIZER_WARN_RE = re.compile(r"tokenizer failed, fallback to char tokens")


def _strip_scheme(url):
return re.sub(r"^https?://", "", url)


def classify_fallback(record, tokenizer_degraded_ts=None):
"""对 process_tokens 策略行分类 fallback 原因。

Expand Down Expand Up @@ -210,9 +214,9 @@ def _analyze_suboptimal(records, hr_weight, lb_weight):
suboptimal.append(
{
"ts": r.get("ts", ""),
"selected": selected.replace("http://", ""),
"selected": _strip_scheme(selected),
"selected_hr": sel_hr,
"best_hr_worker": best_by_hr.replace("http://", ""),
"best_hr_worker": _strip_scheme(best_by_hr),
"best_hr": max_hr,
"reason": reason,
}
Expand Down
Loading
Loading