refactor(compare-alert): 抽 _last_segment、StuckPoint 加 dwell_ms 字段

末段扫描+时长算法抽成 _last_segment 供三处复用;StuckPoint 新增
dwell_ms(默认 None、承载末帧所在屏停留),T5 行为不变。

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
guke
2026-08-07 17:41:32 +08:00
parent 0b9016ae5e
commit dafa0445e3
+27 -11
View File
@@ -47,6 +47,7 @@ class StuckPoint:
frames: int # 末段连续困住的帧数(上限 max_tail)
stuck_ms: int | None = None # 末段连续卡住的时长(ms);无 timestamp 时 None
detected_page: str | None = None # 末帧所在页面(pricebot detected_page 原值);无则 None
dwell_ms: int | None = None # 末帧所在屏停留时长(ms);末帧路径(failed/兜底)用,无 ts → None
def label(self) -> str:
p = PLATFORM_LABELS.get(self.platform, self.platform)
@@ -90,10 +91,15 @@ def _read_head(path: Path, nbytes: int = 4096) -> tuple[str | None, str | None,
return (ps.group(1) if ps else None, pg.group(1) if pg else None, ts.group(1) if ts else None)
def _platform_stuck(
platform: str, step_files: list[Path], threshold: int, max_tail: int
) -> StuckPoint | None:
"""末帧往前数连续同 (pipeline_step, detected_page) 的帧数 ≥threshold → 卡死。"""
def _last_segment(
step_files: list[Path], max_tail: int
) -> tuple[str, str | None, int, int | None] | None:
"""末帧往前数连续同 (pipeline_step, detected_page) 的一段。
返回 (pipeline_step, detected_page, count, dwell_ms);末帧 pipeline_step 抠不出 → None。
dwell_ms = 段末帧ts 段首帧ts(ms);两端 ts 不全可解析、或负(帧钟回退) → None。
这是「末段停留」的唯一算法,卡死判据(≥threshold)与末帧停留(无门槛)都复用它。
"""
tail = step_files[-max_tail:]
heads = [_read_head(p) for p in tail] # [(ps, pg, ts), ...]
last_ps, last_pg, _ = heads[-1]
@@ -105,15 +111,25 @@ def _platform_stuck(
seg_ts.append(ts)
else:
break
count = len(seg_ts)
t_last, t_first = _parse_ts(seg_ts[0]), _parse_ts(seg_ts[-1])
dwell_ms = round((t_last - t_first).total_seconds() * 1000) if t_last and t_first else None
if dwell_ms is not None and dwell_ms < 0:
dwell_ms = None # 时钟不单调(帧 timestamp 回退)→ 降级为不显示时长
return last_ps, last_pg, len(seg_ts), dwell_ms
def _platform_stuck(
platform: str, step_files: list[Path], threshold: int, max_tail: int
) -> StuckPoint | None:
"""末帧往前数连续同 (pipeline_step, detected_page) 的帧数 ≥threshold → 卡死。"""
seg = _last_segment(step_files, max_tail)
if seg is None:
return None
ps, pg, count, dwell_ms = seg
if count < threshold:
return None
# seg_ts[0]=末帧, seg_ts[-1]=段首帧;两端都能解析才算时长
t_last, t_first = _parse_ts(seg_ts[0]), _parse_ts(seg_ts[-1])
stuck_ms = round((t_last - t_first).total_seconds() * 1000) if t_last and t_first else None
if stuck_ms is not None and stuck_ms < 0:
stuck_ms = None # 时钟不单调(帧 timestamp 回退)→ 降级为不显示时长
return StuckPoint(platform, last_ps, count, stuck_ms, detected_page=last_pg)
# 卡死:frames=末段帧数、stuck_ms=末段时长(同段自洽);dwell_ms 字段留默认 None(T5 用 stuck_ms)
return StuckPoint(platform, ps, count, dwell_ms, detected_page=pg)
def read_stuck_points(trace_dir: Path, *, threshold: int, max_tail: int) -> StuckResult: