# -*- coding: utf-8 -*- """M11b-4:端到端时延基准(adjudication → write → COMMIT → broadcast → consumer)。 链路分段(对应任务验收「P99 ≤ 1s」):: t0 裁决触发(run_chain 进入,模拟裁决服务决定下发事件) t1 write_event_with_state 返回(内部已完成事件+状态单事务 COMMIT,M11b-2) t2 post-commit hook 广播发出完成(M11b-3 install_broadcast_hook 包装后的函数) t3 消费端回调收到广播消息(感知) e2e_latency = t3 - t0 本模块 **只组合调用** M11b-2/M11b-3 已交付的公开 API,不修改其核心逻辑: - 写入:``world_sync.write_event_with_state``(若宿主已 ``load_world_sync()`` 则 该引用已被 ``install_broadcast_hook`` 包成 COMMIT 后自动广播版本); - 广播:``subscribe_broadcast`` / ``drain_broadcast`` / ``broadcast_stats``。 消费端感知判定:优先「广播回调到达」(真实链路);若广播丢失,则由 :mod:`world_sync.pbl_runtime_polling` 的 3s 轮询兜底补漏到达(此时 e2e 必然 >1s,报告会把它归入 ``fallback_recovered`` 并单独统计,供瓶颈分析)。 """ import time from typing import Any, Callable, Dict, List, Optional from .pbl_runtime_broadcast import ( broadcast_stats, drain_broadcast, get_broadcaster, subscribe_broadcast, unsubscribe_broadcast, ) from .pbl_runtime_errors import TxConfigError from .pbl_runtime_polling import FALLBACK_SLO_MS, get_fallback from .pbl_runtime_sql import percentile #: 端到端时延 SLO(毫秒)——任务规定 P99 ≤ 1s E2E_LATENCY_SLO_MS = 1000.0 __all__ = [ "E2E_LATENCY_SLO_MS", "FALLBACK_SLO_MS", "measure_once", "run_chain", "summarize_latency", "latency_report", ] def _now_ms() -> float: return time.monotonic() * 1000.0 def measure_once(write_fn: Callable[..., Any], event_data: Dict[str, Any], state_update: Optional[Dict[str, Any]] = None, wait_ms: float = 1000.0, poll_fallback: bool = False) -> Dict[str, Any]: """跑一条事件的端到端测量。 :param write_fn: 写入函数(应为已安装 post-commit hook 的 ``write_event_with_state``;纯写入端时延也能测,广播段记 None)。 :param poll_fallback: True 时广播丢失不判 fail,改由轮询兜底补漏计时。 """ scene_id = str(event_data.get("scene_id") or event_data.get("world_id") or "") fb = get_fallback() holder: Dict[str, Any] = {} t_done = {"v": None} def _on_msg(msg: Any) -> None: if holder.get("t_recv_ms") is None: holder["t_recv_ms"] = _now_ms() holder["msg"] = msg # 广播正常路径:推进兜底水位线(消费端标准接线姿势) try: fb.notify_broadcast(msg) except Exception: pass holder["t_recv_ms"] = None sub = subscribe_broadcast(_on_msg) try: t0 = _now_ms() try: result = write_fn(event_data, state_update or {}) finally: t1 = _now_ms() # post-commit hook 在 write_fn 内部同步/异步发出;drain 确保已派发 try: drain_broadcast(timeout=0.5) except Exception: pass t2 = _now_ms() # 等消费端感知(广播回调) deadline = _now_ms() + wait_ms while holder.get("t_recv_ms") is None and _now_ms() < deadline: time.sleep(0.001) t3 = holder.get("t_recv_ms") via = "broadcast" if t3 is None and poll_fallback: # 广播丢失 → 3s 轮询兜底补漏(真实节奏:interval=3s) eid = None if isinstance(result, dict): eid = result.get("event_id") or result.get("id") w = fb.wait_event(scene_id, str(eid or ""), timeout=FALLBACK_SLO_MS / 1000.0) via = "polling_fallback" t3 = _now_ms() if w.get("found") else None e2e = (t3 - t0) if t3 is not None else None return { "scene_id": scene_id, "event_id": (result.get("event_id") if isinstance(result, dict) else None), "t_write_ms": round(t1 - t0, 3), "t_broadcast_emit_ms": round(t2 - t1, 3), "t_consume_ms": round(t3 - t1, 3) if t3 is not None else None, "e2e_ms": round(e2e, 3) if e2e is not None else None, "via": via, "perceived": t3 is not None, } finally: unsubscribe_broadcast(sub) def run_chain(write_fn: Callable[..., Any], events: List[Dict[str, Any]], state_builder: Optional[Callable[[Dict[str, Any]], Dict[str, Any]]] = None, wait_ms: float = 1000.0, poll_fallback: bool = False) -> List[Dict[str, Any]]: """批量端到端测量(裁决触发→…→消费端感知),返回逐条样本。""" samples: List[Dict[str, Any]] = [] for ev in events: st = state_builder(ev) if state_builder else {} samples.append(measure_once(write_fn, ev, st, wait_ms=wait_ms, poll_fallback=poll_fallback)) return samples def summarize_latency(samples: List[Dict[str, Any]], slo_ms: float = E2E_LATENCY_SLO_MS) -> Dict[str, Any]: """汇总:P50/P90/P99/max + SLO 判定 + 分段瓶颈定位。""" e2e = [s["e2e_ms"] for s in samples if s.get("e2e_ms") is not None] write_seg = [s["t_write_ms"] for s in samples if s.get("t_write_ms") is not None] emit_seg = [s["t_broadcast_emit_ms"] for s in samples if s.get("t_broadcast_emit_ms") is not None] consume_seg = [s["t_consume_ms"] for s in samples if s.get("t_consume_ms") is not None] p99 = round(percentile(e2e, 99), 3) if e2e else None via_broadcast = sum(1 for s in samples if s.get("via") == "broadcast") via_polling = sum(1 for s in samples if s.get("via") == "polling_fallback") lost = sum(1 for s in samples if not s.get("perceived")) # 瓶颈定位:P99 分段最大者 seg_p99 = { "write_commit": round(percentile(write_seg, 99), 3) if write_seg else 0.0, "broadcast_emit": round(percentile(emit_seg, 99), 3) if emit_seg else 0.0, "consume_wait": round(percentile(consume_seg, 99), 3) if consume_seg else 0.0, } bottleneck = max(seg_p99.items(), key=lambda kv: kv[1])[0] if seg_p99 else None return { "n": len(samples), "perceived": len(samples) - lost, "lost": lost, "via_broadcast": via_broadcast, "via_polling_fallback": via_polling, "p50_ms": round(percentile(e2e, 50), 3) if e2e else None, "p90_ms": round(percentile(e2e, 90), 3) if e2e else None, "p99_ms": p99, "max_ms": round(max(e2e), 3) if e2e else None, "segment_p99_ms": seg_p99, "bottleneck_segment": bottleneck, "slo_ms": slo_ms, "slo_passed": (p99 is not None and p99 <= slo_ms), "broadcast_stats": broadcast_stats(), } def latency_report(write_fn: Callable[..., Any], events: List[Dict[str, Any]], state_builder: Optional[Callable[[Dict[str, Any]], Dict[str, Any]]] = None, slo_ms: float = E2E_LATENCY_SLO_MS, wait_ms: float = 1000.0, poll_fallback: bool = False) -> Dict[str, Any]: """一键基准:跑链路 + 汇总 + 结论(供集成测试 / scripts/m11b4_e2e_bench.py 复用)。""" if write_fn is None or not callable(write_fn): raise TxConfigError("latency_report: write_fn must be a callable " "(world_sync.write_event_with_state)") samples = run_chain(write_fn, events, state_builder=state_builder, wait_ms=wait_ms, poll_fallback=poll_fallback) summary = summarize_latency(samples, slo_ms=slo_ms) summary["samples"] = samples summary["verdict"] = ("PASS" if summary["slo_passed"] else "FAIL: P99 %.1fms > SLO %.0fms, bottleneck=%s" % (summary["p99_ms"] or -1, slo_ms, summary["bottleneck_segment"])) return summary