Files
ykxmz/_build/bench_from_pc.py
T
xmz7891 25e7780272 单次请求失败不再立刻判离线,改成连续 3 次
实测单个请求有 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)—— 用户实际体验比设备端量出来的好。
2026-10-07 16:16:50 +08:00

181 lines
6.5 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 -*-
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())