mirror of
https://gitee.com/xmz7891/ykxmz.git
synced 2026-10-18 14:50:45 +00:00
实测单个请求有 2~4% 的偶发失败(4G 丢包打在一条隧道连接上, 或者那条连接正在重连)。原逻辑是一失败就 online = false, 于是界面上会闪一下已断开 —— 用户点按钮时正好撞上那一闪, 看起来就像又连不上了。 而链路其实 1.5~15 秒就自己恢复(客户端本来就有退避重试)。 改成连续失败 3 次才判离线(手表至少 4.5 秒、电脑至少 6 秒): * 单次抖动完全不影响界面 * 真的断了仍然如实显示 这不是掩盖问题,而是把是否可用的判断从一次采样改成连续采样 —— 单点采样在有丢包的链路上噪声太大。 两边同步改: android .../RelayTransport.java failStreak + OFFLINE_AFTER windows/lanremote/relay.py _fail_streak + OFFLINE_AFTER 顺带记录 PC 端基准(真实用户视角,之前都是在设备上量的, 那等于把设备 4G 算了两遍): 公网可用率 98% 耗时 中位 911 P90 1889 P99 23868 ms 长轮询 5/6 耗时 60.8~62.1 秒 对比设备端(中位 1344 / P90 2563)—— 用户实际体验比设备端量出来的好。
181 lines
6.5 KiB
Python
181 lines
6.5 KiB
Python
# -*- coding: utf-8 -*-
|
||
r'''从这台 Windows 机器测中转 —— 这才是"真实用户"的视角。
|
||
|
||
== 为什么要换测量端 ==
|
||
|
||
之前所有测量都是在设备自己上跑的 curl。那等于:
|
||
设备 4G --> Cloudflare 边缘 (第一段)
|
||
边缘 --> 隧道 --> 设备 4G 源站 (第二段)
|
||
**两段都压在同一个 4G 链路上**,量出来的数字是双重打击。
|
||
|
||
真实用户(手机/电脑在 WiFi、宽带、5G 上):
|
||
用户 --> Cloudflare 边缘 (第一段,好网络)
|
||
边缘 --> 隧道 --> 设备 4G 源站 (第二段,只有这段是 4G)
|
||
|
||
所以从这台机器量出来的,才是用户实际会遇到的延迟和可用率。
|
||
|
||
用法: python _build/bench_from_pc.py [标签]
|
||
'''
|
||
from __future__ import annotations
|
||
|
||
import json
|
||
import statistics
|
||
import sys
|
||
import time
|
||
import urllib.error
|
||
import urllib.request
|
||
from pathlib import Path
|
||
|
||
BASE = "https://relay.xxmxxs.top"
|
||
UA = "LanRemote/1.8.2"
|
||
OUT_DIR = Path(r"C:\Users\shu\Desktop\deepseekagent\LanRemote\_build")
|
||
|
||
|
||
def req(path: str, method: str = "GET", data: bytes | None = None,
|
||
headers: dict | None = None, timeout: float = 150.0):
|
||
"""发一个请求,返回 (状态码, 字节数, 秒数, 响应头)。"""
|
||
hdrs = {"User-Agent": UA}
|
||
hdrs.update(headers or {})
|
||
r = urllib.request.Request(BASE + path, data=data, headers=hdrs,
|
||
method=method)
|
||
t0 = time.perf_counter()
|
||
try:
|
||
with urllib.request.urlopen(r, timeout=timeout) as resp:
|
||
body = resp.read()
|
||
return resp.status, len(body), time.perf_counter() - t0, resp.headers
|
||
except urllib.error.HTTPError as exc:
|
||
try:
|
||
exc.read()
|
||
except Exception: # noqa: BLE001
|
||
pass
|
||
return exc.code, 0, time.perf_counter() - t0, exc.headers
|
||
except Exception: # noqa: BLE001
|
||
return 0, 0, time.perf_counter() - t0, {}
|
||
|
||
|
||
def pct(values: list, q: float) -> int:
|
||
if not values:
|
||
return 0
|
||
s = sorted(values)
|
||
idx = min(int(len(s) * q), len(s) - 1)
|
||
return int(s[idx])
|
||
|
||
|
||
def main() -> int:
|
||
label = sys.argv[1] if len(sys.argv) > 1 else "pc"
|
||
lines: list[str] = []
|
||
|
||
def say(text: str = "") -> None:
|
||
print(text)
|
||
lines.append(text)
|
||
|
||
say("=" * 62)
|
||
say(" 从 PC 量中转 · %s · %s" % (label, time.strftime("%Y-%m-%d %H:%M:%S")))
|
||
say("=" * 62)
|
||
|
||
# ---------------- A. 公网可用率 ----------------
|
||
say()
|
||
say("── A. 公网可用率(100 次 /v1/health)──")
|
||
ok, times, fails = 0, [], 0
|
||
for i in range(100):
|
||
code, _n, secs, _h = req("/v1/health", timeout=25)
|
||
if code == 200:
|
||
ok += 1
|
||
times.append(int(secs * 1000))
|
||
else:
|
||
fails += 1
|
||
time.sleep(0.3)
|
||
say(" 可用率 %d/100 = %d%% 失败 %d 次" % (ok, ok, fails))
|
||
if times:
|
||
say(" 耗时 最小 %d 中位 %d P90 %d P99 %d 最大 %d ms"
|
||
% (min(times), int(statistics.median(times)), pct(times, 0.9),
|
||
pct(times, 0.99), max(times)))
|
||
|
||
# ---------------- E. 静态资源 ----------------
|
||
say()
|
||
say("── E. 静态资源(各 10 次)──")
|
||
for path in ("/", "/shot/pc-main.png", "/favicon.ico"):
|
||
o, ts = 0, []
|
||
for _ in range(10):
|
||
code, _n, secs, _h = req(path, timeout=25)
|
||
if code == 200:
|
||
o += 1
|
||
ts.append(int(secs * 1000))
|
||
time.sleep(0.2)
|
||
med = int(statistics.median(ts)) if ts else 0
|
||
say(" %-22s %2d/10 中位 %d 最大 %d ms"
|
||
% (path, o, med, max(ts) if ts else 0))
|
||
|
||
# ---------------- C. 下载 ----------------
|
||
say()
|
||
say("── C. 三个成品下载(完整下载)──")
|
||
dl_results = {}
|
||
for name in ("ykxmz-setup-1.8.2.exe", "ykxmz-server-1.8.2.zip",
|
||
"ykxmz-watch-1.8.2.apk"):
|
||
code, size, secs, hdrs = req("/download/" + name, timeout=300)
|
||
kbps = (size / secs / 1024) if secs > 0 else 0
|
||
cf = (hdrs.get("cf-cache-status") or "?") if hdrs else "?"
|
||
say(" %-30s HTTP %-4s %9d 字节 %6.1fs %7.0f KB/s cf=%s"
|
||
% (name, code, size, secs, kbps, cf))
|
||
if code == 200 and size > 0:
|
||
dl_results[name] = kbps
|
||
|
||
# ---------------- B. 长轮询 ----------------
|
||
say()
|
||
say("── B. 长轮询(6 次真实 poll,每次挂 60 秒)──")
|
||
stamp = int(time.time())
|
||
dev = "pcbench-%s-%d" % (label, stamp)
|
||
code6 = "%06d" % (stamp % 1000000)
|
||
payload = json.dumps({
|
||
"deviceId": dev, "code": code6, "name": "pcbench",
|
||
"version": "1.8.2",
|
||
}, ensure_ascii=False).encode("utf-8")
|
||
status, _n, _s, _h = req("/v1/agent/register", method="POST",
|
||
data=payload, timeout=30,
|
||
headers={"Content-Type": "application/json"})
|
||
token = ""
|
||
if status == 200:
|
||
# 重新取一次拿 body
|
||
r = urllib.request.Request(
|
||
BASE + "/v1/agent/register", data=payload,
|
||
headers={"User-Agent": UA, "Content-Type": "application/json"},
|
||
method="POST")
|
||
try:
|
||
with urllib.request.urlopen(r, timeout=30) as resp:
|
||
token = json.loads(resp.read().decode("utf-8")).get("token", "")
|
||
except Exception: # noqa: BLE001
|
||
pass
|
||
say(" 探针注册: %s" % ("成功" if token else "失败"))
|
||
|
||
if token:
|
||
okb, btimes, bfails = 0, [], []
|
||
for i in range(6):
|
||
code, _n, secs, _h = req(
|
||
"/v1/agent/poll", method="POST", data=b"",
|
||
headers={"X-Token": token}, timeout=150)
|
||
ms = int(secs * 1000)
|
||
if code == 200:
|
||
okb += 1
|
||
btimes.append(ms)
|
||
say(" %d: 200 %6dms" % (i + 1, ms))
|
||
else:
|
||
bfails.append("%d:%s" % (i + 1, code))
|
||
say(" %d: %-4s **失败**" % (i + 1, code))
|
||
say(" 长轮询成功 %d/6 = %d%% 失败: %s"
|
||
% (okb, okb * 100 // 6, " ".join(bfails) or "无"))
|
||
if btimes:
|
||
say(" 耗时 最小 %d 中位 %d P90 %d 最大 %d ms(理想 ≈ 61000)"
|
||
% (min(btimes), int(statistics.median(btimes)),
|
||
pct(btimes, 0.9), max(btimes)))
|
||
|
||
say()
|
||
say("=" * 62)
|
||
out = OUT_DIR / ("bench_pc_%s.txt" % label)
|
||
out.write_text("\n".join(lines), encoding="utf-8")
|
||
say(" 存档: %s" % out)
|
||
return 0
|
||
|
||
|
||
if __name__ == "__main__":
|
||
sys.exit(main())
|