2026-09-19 13:07:15 +08:00

146 lines
4.4 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# -*- coding: utf-8 -*-
"""latency —— 时延打点与 SLA 判定M11b
承诺m11b_config裁决 ≤200ms、广播 ≤300ms、端到端 ≤1000ms、轮询兜底 3000ms。
本模块只做纯内存统计(可离线断言),不依赖 DB真实数值由 selftest 连库实测填入报告。
"""
import time
from collections import deque
from .m11b_config import (POLL_FALLBACK_MS, SLA_ADJUDICATE_MS, SLA_BROADCAST_MS,
SLA_END_TO_END_MS)
STAGES = ("adjudicate", "broadcast", "end_to_end")
def percentile(values, p):
if not values:
return None
s = sorted(values)
idx = min(len(s) - 1, int(round((p / 100.0) * (len(s) - 1))))
return round(s[idx], 3)
class Timer(object):
"""with Timer() as t: ... ; t.ms —— 毫秒级计时。"""
def __init__(self):
self._t0 = None
self.ms = None
def __enter__(self):
self._t0 = time.perf_counter()
return self
def __exit__(self, *a):
self.ms = round((time.perf_counter() - self._t0) * 1000.0, 3)
return False
class LatencyTracker(object):
"""各阶段时延样本环形记录 + SLA 越界计数。"""
def __init__(self, maxlen=500):
self._samples = dict((s, deque(maxlen=maxlen)) for s in STAGES)
self._breach = dict((s, 0) for s in STAGES)
@staticmethod
def sla_for(stage):
return {"adjudicate": SLA_ADJUDICATE_MS,
"broadcast": SLA_BROADCAST_MS,
"end_to_end": SLA_END_TO_END_MS}[stage]
def record(self, stage, ms):
if stage not in self._samples:
raise KeyError("unknown stage %r" % stage)
ms = round(float(ms), 3)
self._samples[stage].append(ms)
breach = ms > self.sla_for(stage)
if breach:
self._breach[stage] += 1
return breach
def summary(self):
out = {}
for s in STAGES:
vals = list(self._samples[s])
n = len(vals)
out[s] = {
"sla_ms": self.sla_for(s),
"samples": n,
"avg_ms": round(sum(vals) / n, 3) if n else None,
"p95_ms": percentile(vals, 95),
"p99_ms": percentile(vals, 99),
"max_ms": round(max(vals), 3) if n else None,
"breaches": self._breach[s],
}
out["poll_fallback_ms"] = POLL_FALLBACK_MS
return out
def all_within_sla(self):
return all(self._breach[s] == 0 for s in STAGES)
_TRACKER = None
def get_tracker():
global _TRACKER
if _TRACKER is None:
_TRACKER = LatencyTracker()
return _TRACKER
def record(stage, ms):
return get_tracker().record(stage, ms)
def summary():
return get_tracker().summary()
def self_check():
msgs, ok = [], True
t = LatencyTracker()
if t.record("adjudicate", 120.0) is not False:
ok = False
msgs.append("latency FAIL120ms 不应判越界SLA 200ms")
else:
msgs.append("latency PASS裁决 120ms ≤ 200ms 未越界")
if t.record("adjudicate", 450.0) is not True:
ok = False
msgs.append("latency FAIL450ms 应判越界")
else:
msgs.append("latency PASS裁决 450ms > 200ms 记越界")
t.record("broadcast", 40.0)
t.record("end_to_end", 620.0)
s = t.summary()
if s["adjudicate"]["breaches"] != 1 or s["adjudicate"]["samples"] != 2:
ok = False
msgs.append("latency FAILsummary %r" % s["adjudicate"])
else:
msgs.append("latency PASSsummary avg=%s p95=%s breaches=%d"
% (s["adjudicate"]["avg_ms"], s["adjudicate"]["p95_ms"],
s["adjudicate"]["breaches"]))
if s["poll_fallback_ms"] != 3000:
ok = False
msgs.append("latency FAIL轮询兜底 %s 应为 3000" % s["poll_fallback_ms"])
else:
msgs.append("latency PASS轮询兜底 3000ms 已配置")
if percentile([1, 2, 3, 4, 5, 6, 7, 8, 9, 10], 95) is None:
ok = False
msgs.append("latency FAILpercentile 空值")
else:
msgs.append("latency PASSp95=%s" % percentile(range(1, 11), 95))
with Timer() as tm:
time.sleep(0.005)
if not (1.0 <= tm.ms < 2000):
ok = False
msgs.append("latency FAILTimer 计时 %s" % tm.ms)
else:
msgs.append("latency PASSTimer 实测 %.1fms" % tm.ms)
if ok:
msgs.append("latency.self_check PASS")
return ok, msgs