yes fuking shit

This commit is contained in:
Horldsence
2026-09-12 14:48:43 +08:00
parent 61fd22598a
commit 45e48d4c91
64 changed files with 11540 additions and 57 deletions
+229
View File
@@ -0,0 +1,229 @@
"""运行历史计时分解:对任意会话日志按官方计时模型独立复算并分相。
支持两种格式(自动识别):
- 参赛队客户端日志:``scripts/capture_practice.py`` / ``SimulatorClient`` 写出的
JSONL,逐行 ``attempt`` / ``accepted`` / ``step``;
- 评测机日志:``referee-data/sessions/<case>/log.jsonl``,逐行
``session_start`` / ``action`` / ``session_end``。
复算口径与 ``scripts/verify_q3_practice.py::timing_rows`` 完全一致(题面附件2 §4.1):
``T = Σ 距离/5 + 5·N_measure + N_switch + 5·N_clear⁺ + 3·N_clear⁻``,
起点位置 (0,0)、起始频道 1,``/clear`` 不计换频,移动按整数微秒累计。
用途:把"虚拟时间花在哪"变成一条可随时重跑的命令,而不是一次性的分析结论。
uv run python scripts/analyze_run_timing.py session.jsonl
uv run python scripts/analyze_run_timing.py referee-data/sessions/*/log.jsonl --json out.json
uv run python scripts/analyze_run_timing.py session.jsonl --plan seven # 站点/探点分类
"""
from __future__ import annotations
import argparse
import json
import math
from collections import Counter, defaultdict
from pathlib import Path
SPEED_MPS = 5.0 # [题面] 附件1 表1
MEASURE_S = 5 # [题面]
SWITCH_S = 1 # [题面]
CLEAR_SUCCESS_S = 5 # [题面]
CLEAR_FAIL_S = 3 # [题面]
START_POSITION = (0.0, 0.0)
START_CHANNEL = 1
def load_actions(path: Path) -> list[dict]:
"""读取一份 JSONL,归一化为已接受动作序列。"""
actions: list[dict] = []
for line in path.read_text(encoding="utf-8", errors="replace").splitlines():
line = line.strip()
if not line:
continue
try:
record = json.loads(line)
except json.JSONDecodeError:
continue
event = record.get("event")
if event == "accepted":
request, response = record.get("request", {}), record.get("response", {})
accepted = bool(response.get("accepted", True))
elif event == "action":
request, response = record.get("request", {}), record.get("response", {})
accepted = bool(response.get("accepted", record.get("status") == 200))
else:
continue
if not accepted:
continue
actions.append({
"path": record.get("path") or request.get("path") or "",
"position": request.get("position"),
"channel": request.get("channel"),
"result": response.get("measure_result") or response.get("clear_result") or "",
"recorded_s": response.get("virtual_time_s"),
"elapsed_s": record.get("elapsed_s"),
})
return actions
def recompute(actions: list[dict]) -> tuple[list[dict], dict]:
"""按官方模型逐动作复算;返回明细与汇总(微秒整数累计)。"""
position, channel, virtual_us = START_POSITION, START_CHANNEL, 0
rows: list[dict] = []
totals = Counter()
distance_total = 0.0
max_gap_us = 0
for index, action in enumerate(actions):
path = action["path"]
move_s = switches = action_s = 0.0
if path in ("/measure", "/clear") and action["position"] is not None:
target = (float(action["position"]["x"]), float(action["position"]["y"]))
distance_total += math.dist(position, target)
move_s = math.dist(position, target) / SPEED_MPS
position = target
if path == "/measure":
switches = int(channel != action["channel"])
channel = action["channel"]
action_s = MEASURE_S
else:
action_s = CLEAR_SUCCESS_S if action["result"] == "success" else CLEAR_FAIL_S
move_us = round(move_s * 1_000_000)
virtual_us += move_us + int((switches + action_s) * 1_000_000)
totals["move_us"] += move_us
totals["switch_s"] += switches
totals["action_us"] += int(action_s * 1_000_000)
totals["count"] += 1
if action["elapsed_s"]:
totals["elapsed_s"] += action["elapsed_s"]
recorded = action["recorded_s"]
gap_us = ((round(recorded * 1_000_000) - virtual_us)
if isinstance(recorded, (int, float)) else 0)
max_gap_us = max(max_gap_us, abs(gap_us))
rows.append({"index": index, "path": path, "channel": action["channel"],
"x": position[0], "y": position[1], "result": action["result"],
"move_us": move_us, "switch_s": switches, "action_s": action_s,
"recomputed_us": virtual_us, "recorded_s": recorded, "gap_us": gap_us})
summary = {
"actions": len(actions),
"virtual_s": round(virtual_us / 1_000_000, 6),
"move_s": round(totals["move_us"] / 1_000_000, 3),
"switch_s": float(totals["switch_s"]),
"action_s": round(totals["action_us"] / 1_000_000, 3),
"distance_m": round(distance_total, 1),
"real_elapsed_s": round(totals["elapsed_s"], 3),
"max_recorded_gap_s": max_gap_us / 1_000_000,
}
return rows, summary
def phase_report(rows: list[dict]) -> dict:
"""清除前后分相 + 重复复扫检测(定位"时间被重复动作吃掉"的历史问题)。"""
clear_indices = [r["index"] for r in rows if r["path"] == "/clear"]
last_clear = max(clear_indices) if clear_indices else None
measure_keys = defaultdict(int)
for row in rows:
if row["path"] == "/measure":
key = (round(row["x"] * 2), round(row["y"] * 2), row["channel"])
measure_keys[key] += 1
repeated = sum(count - 1 for count in measure_keys.values() if count > 1)
report = {
"measures": sum(1 for r in rows if r["path"] == "/measure"),
"clears": len(clear_indices),
"failed_clears": sum(1 for r in rows
if r["path"] == "/clear" and r["result"] != "success"),
"distinct_measure_stops": len(measure_keys),
"repeated_measure_actions": repeated,
"repeat_ratio": round(repeated / max(1, len(measure_keys)), 3),
}
if last_clear is not None:
tail = [r for r in rows if r["index"] > last_clear]
report["last_clear_virtual_s"] = round(rows[last_clear]["recomputed_us"] / 1_000_000, 3)
report["after_last_clear_virtual_s"] = round(
sum(r["move_us"] + (r["switch_s"] + r["action_s"]) * 1_000_000 for r in tail)
/ 1_000_000, 3)
report["after_last_clear_measures"] = sum(1 for r in tail if r["path"] == "/measure")
return report
def classify_by_plan(rows: list[dict], points, tol_m: float = 2.0) -> dict:
"""按给定站点集把 /measure 分成"站点扫频"与"专用探点",并给出各自移动耗时。"""
buckets: dict[str, Counter] = defaultdict(Counter)
for row in rows:
if row["path"] != "/measure":
continue
nearest = min((math.dist((row["x"], row["y"]), p) for p in points), default=math.inf)
kind = "station" if nearest <= tol_m else "probe"
buckets[kind]["n"] += 1
buckets[kind]["move_us"] += row["move_us"]
return {kind: {"measures": value["n"],
"move_s": round(value["move_us"] / 1_000_000, 3)}
for kind, value in buckets.items()}
def _plan_points(name: str):
if name == "none":
return None
if name == "seven":
from mcm26b.planner.seven import seven_points
return seven_points()
if name == "49":
from mcm26b.coverage import coverage_points
return coverage_points()
raise ValueError(f"未知 --plan: {name}")
def main() -> int:
parser = argparse.ArgumentParser(description=__doc__.splitlines()[0])
parser.add_argument("logs", nargs="+", type=Path, help="会话 JSONL(客户端或评测机格式)")
parser.add_argument("--plan", choices=("none", "seven", "49"), default="none",
help="站点集,用于把 /measure 分成站点扫频与专用探点")
parser.add_argument("--json", type=Path, default=None, help="汇总输出路径")
parser.add_argument("--top", type=int, default=6, help="逐频道表格显示前 N 个热点频道")
args = parser.parse_args()
points = _plan_points(args.plan)
payload = {}
for path in args.logs:
actions = load_actions(path)
if not actions:
print(f"[跳过] {path}:没有已接受动作")
continue
rows, summary = recompute(actions)
report = phase_report(rows)
total = summary["virtual_s"] or 1.0
print(f"\n===== {path} =====")
print(f"虚拟总时间 {summary['virtual_s']:.3f} s "
f"移动 {summary['move_s']:.1f} s ({100 * summary['move_s'] / total:.1f}%) "
f"检测/清除动作 {summary['action_s']:.1f} s ({100 * summary['action_s'] / total:.1f}%) "
f"换频 {summary['switch_s']:.0f} s ({100 * summary['switch_s'] / total:.1f}%)")
print(f"移动距离 {summary['distance_m']:.1f} m 请求 {summary['actions']} 次 "
f"现实耗时 {summary['real_elapsed_s']:.3f} s "
f"与服务器记录最大偏差 {summary['max_recorded_gap_s']:.6f} s")
print(f"检测 {report['measures']} 清除 {report['clears']}(失败 {report['failed_clears']}) "
f"不同测点 {report['distinct_measure_stops']} "
f"重复检测 {report['repeated_measure_actions']}"
f"(重复率 {report['repeat_ratio']})")
if "last_clear_virtual_s" in report:
print(f"末次清除于 {report['last_clear_virtual_s']:.1f} s;其后又花 "
f"{report['after_last_clear_virtual_s']:.1f} s 做 "
f"{report['after_last_clear_measures']} 次检测")
per_channel = Counter(r["channel"] for r in rows if r["path"] == "/measure")
hottest = per_channel.most_common(args.top)
print("检测次数最多的频道:" + ",".join(f"ch{c}:{n}" for c, n in hottest))
if points is not None:
print("测点分类:" + json.dumps(classify_by_plan(rows, points),
ensure_ascii=False))
payload[str(path)] = {"summary": summary, "phases": report,
"per_channel_measures": dict(sorted(per_channel.items()))}
if args.json is not None:
args.json.parent.mkdir(parents=True, exist_ok=True)
args.json.write_text(json.dumps(payload, ensure_ascii=False, indent=2) + "\n",
encoding="utf-8")
print(f"\n汇总已写入 {args.json}")
return 0
if __name__ == "__main__":
raise SystemExit(main())