From dafa0445e3e2952e6c76b92563a2cf6c0efef31d Mon Sep 17 00:00:00 2001 From: guke Date: Fri, 7 Aug 2026 17:41:32 +0800 Subject: [PATCH] =?UTF-8?q?refactor(compare-alert):=20=E6=8A=BD=20=5Flast?= =?UTF-8?q?=5Fsegment=E3=80=81StuckPoint=20=E5=8A=A0=20dwell=5Fms=20?= =?UTF-8?q?=E5=AD=97=E6=AE=B5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 末段扫描+时长算法抽成 _last_segment 供三处复用;StuckPoint 新增 dwell_ms(默认 None、承载末帧所在屏停留),T5 行为不变。 Co-Authored-By: Claude Opus 4.8 (1M context) --- app/services/trace_stuck.py | 38 ++++++++++++++++++++++++++----------- 1 file changed, 27 insertions(+), 11 deletions(-) diff --git a/app/services/trace_stuck.py b/app/services/trace_stuck.py index 7142cf4..e8a3c08 100644 --- a/app/services/trace_stuck.py +++ b/app/services/trace_stuck.py @@ -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: