## 性能:瓶颈在传输,不在代码
先量后改。helper 冷启动 0.44s(编译 22173 行)看着像瓶颈,但
drouter-helpd 是常驻守护,请求走 Unix socket,正常路径根本不 fork;
实测各 API 已是毫秒级。app.js 语法编译 18ms、顶层执行 18ms
(393 个顶层函数 / 13401 行)—— JS 执行不是瓶颈。
真瓶颈是 app.js 690,482 字节每次重传。**决定不拆包**:JS 执行只 18ms,
拆包引入首屏 waterfall 的风险大于收益。改走三条:
1. 缓存策略按「有没有指纹」区分(不是按扩展名)
- 带 ?v=<mtime> → public, max-age=31536000, immutable
- 没指纹 → 仍 no-cache(否则部署后用户拿到旧文件)
- index.html → 永远 no-store(每次都要重新注入)
2. 编码协商 br > gzip > 原文,**逐级降级**
3. 压缩结果按 (path, mtime, size, enc) 缓存
真机实测:首次 690,482 → 200,137 字节,第二次打开 0 字节(304)。
## 逐级降级不是可选项
目标机**没装 python3-brotli**(只有 C 库 libbrotli1)。最初写的
_brotli_cached 在模块缺失时 `return data, None`,而 None 是 encoding,
调用方理解为「压不动,原样输出」—— 于是 583KB 裸传,**比优化前更慢**。
改成 (None, None) 明确表示「这条路不通」,由 _encode_body 继续试 gzip。
同时修:Accept-Encoding 探测必须认 q=0(br;q=0 是「明确不要」,
子串匹配会正好发给解不开的客户端 → 满屏乱码);编码必须进缓存键
(共用键会互相覆盖);brotli 固定 q5(q11 只省 10KB 却要 1.5-3s)。
## 宽 except 把 bug 装成了特性
兜底只接 ImportError 这类**真·预期**的缺失,不接 TypeError/ValueError
(「str 传给要 bytes 的接口」也是 TypeError,接了等于没接)。
早先的 `except Exception` 吞掉了一个 NameError(漏注入 _comp_lock),
表现是压缩功能完好但从不生效,页面照常能开、只看响应头的判据完全发现不了。
## 版本号:建立单一真源 packaging/VERSION
原来版本号在 4 个构建脚本各写死一个默认值 + openapi 里硬编码。
漏改一处就产出「文件名 1.0.9、control 里 1.0.8」的包 —— apt 按
control 认版本,dpkg -l 永远显示旧版且升不上去。
index.html 的 ETag 并入版本号:它发出去的是注入后的内容,
ETag 却按原文件算,只改 VERSION 文件时 ETag 不变,客户端换到 304
继续用旧副本,页面版本号永远停在旧版。
## logo 回概述
用 <a> 而不是 <div onclick>:Tab 能聚焦、Enter 能触发、读屏会念「链接」。
已在 dash 页时不重复 go()(go 会 stopPageTimers + 重拉数据)。
CSS 显式去 text-decoration/color(UA 样式表不声明仍生效),
:focus-visible 焦点环保留(纯键盘用户唯一的路标)。
## 顺带修掉一个已发布版本的真 bug
_vpn_env 里 os.path.join('/lib/modules', ...) 被留成了 '/nonexistent'
(反向验证脚本被 SIGTERM 打断,finally 没执行,注入留在源码里),
于是 mod_file 永远为空、**need_module 整个状态从四态里消失** ——
这正是 v1.0.8 release notes 声称修好的那个 bug(PVE 的 KVM 虚机内核
明明有 wireguard.ko.xz 却报 unsupported,前端配一句写死的
「通常说明跑在精简容器里」把排查方向带偏)。
新增 _dev/t-worktree-clean.py 扫注入哨兵,放在 sync.sh 所有判据之前 ——
基线被污染时,后面每一条红都不可信。
## 判据
新增 t-static-perf.py(42 项静态)、t-encoding-runtime.py(26 项**真跑**,
覆盖 9 种 Accept-Encoding 组合、brotli 缺失降级、两种编码不互相覆盖、
压不动直出、内部错误不被吞)、t-worktree-clean.py。
三个都做了反向验证,注入后确实变红。
反向验证本身也踩了坑:判据里 len(body) 在 body 为 None 时先崩 ——
退出码非 0 但没有 FAIL 行,看着像「跑了」,实际没人知道它验出了什么。
取长度一律过 _sz() 包一层。
全量回归 54 个判据 0 失败;真机验收 22 项全通过;
前端三查 47 视图 0 异常、26 项移动端适配全过。
152 lines
8.6 KiB
Bash
152 lines
8.6 KiB
Bash
#!/bin/bash
|
||
# 真机验收:静态资源传输层(1.0.9)
|
||
#
|
||
# 验的是「浏览器实际收到什么」,不是源码里写了什么:
|
||
# ① 带指纹的 app.js → Cache-Control: immutable 长缓存
|
||
# ② 第二次请求 If-None-Match → 304,且**零字节响应体**
|
||
# ③ 无指纹的 /app.js → 仍是 no-cache
|
||
# ④ Accept-Encoding: br 但目标机没装 brotli → **必须退到 gzip**
|
||
# (这是本轮最大的风险点,退化就是 583KB 裸传)
|
||
# ⑤ 页脚版本号已注入,不再是 __APP_VERSION__ 字面量
|
||
# ⑥ index.html 的 ETag 里含版本号
|
||
# ⑦ logo 是 <a id="brand-home">,页脚有版本号元素
|
||
set -u
|
||
BASE="http://127.0.0.1:8080"
|
||
WEB="http://127.0.0.1:8080"
|
||
fail=0
|
||
ck() { if [ "$2" = "1" ]; then echo " ✓ $1"; else echo " ✗ $1"; fail=$((fail+1)); fi; }
|
||
|
||
echo "=== 1. 探测面板是否在跑 ==="
|
||
# ⚠️ **不要在这里试登录**。密码猜错有锁定计数(实测错误返回
|
||
# 「还可尝试 3 次」),验收脚本反复试会把用户的账号锁死 ——
|
||
# 而且脚本里硬编码密码这个做法本身就不该保留。
|
||
# 本脚本要验的东西全是公开静态资源 + 免鉴权的 /api/openapi,不需要登录。
|
||
CODE=$(curl -sk -m 5 -o /dev/null -w '%{http_code}' "$BASE/index.html")
|
||
echo " GET /index.html → $CODE"
|
||
[ "$CODE" = "200" ] || { echo "面板没起来($CODE),后续无意义"; exit 1; }
|
||
|
||
echo
|
||
echo "=== 2. 取 app.js 的指纹(从 index.html 里解析)==="
|
||
IDX=$(curl -sk -m 5 "$BASE/index.html")
|
||
# ⚠️ 不能用 sed 抽 /app.js?v=... —— 问号是 sed 的选项分隔符,
|
||
# 会报「s 的未知选项」,然后指纹解析静默失败、退回测无指纹 URL,
|
||
# 于是「immutable 长缓存」那条必然变红 —— 假红比不测更坏。
|
||
APPV=$(printf '%s' "$IDX" | grep -o '/app\.js?v=[0-9]*' | head -1)
|
||
[ -z "$APPV" ] && APPV="/app.js"
|
||
echo " 指纹 URL = $APPV"
|
||
|
||
echo
|
||
echo "=== 3. 带指纹请求 → 应为 immutable 长缓存 ==="
|
||
H1=$(curl -sk -m 10 -D - -o /dev/null "$BASE$APPV")
|
||
CC=$(printf '%s' "$H1" | sed -n 's/^[Cc]ache-[Cc]ontrol:[[:space:]]*//p' | tr -d '\r')
|
||
echo " Cache-Control: $CC"
|
||
[ -z "$CC" ] && CC=$(printf '%s' "$H1" | grep -i '^cache-control:' | sed 's/^[^:]*:[[:space:]]*//' | tr -d '\r')
|
||
printf '%s' "$CC" | grep -q 'immutable' && ck "带指纹 → immutable 长缓存" 1 || ck "带指纹 → immutable 长缓存(实际: $CC)" 0
|
||
printf '%s' "$CC" | grep -q 'max-age=31536000' && ck "max-age 是一年" 1 || ck "max-age 是一年" 0
|
||
|
||
ETAG=$(printf '%s' "$H1" | grep -i '^etag:' | sed 's/^[^:]*:[[:space:]]*//' | tr -d '\r')
|
||
[ -n "$ETAG" ] && ck "有 ETag($ETAG)" 1 || ck "有 ETag" 0
|
||
|
||
echo
|
||
echo "=== 4. 传输量(br / gzip / 无压缩 各一次)==="
|
||
SZ_BR=$(curl -sk -m 20 -H 'Accept-Encoding: br, gzip' -o /dev/null -w '%{size_download}' "$BASE$APPV")
|
||
ENC_BR=$(curl -sk -m 20 -D - -o /dev/null -H 'Accept-Encoding: br, gzip' "$BASE$APPV" | grep -i '^content-encoding:' | sed 's/^[^:]*:[[:space:]]*//' | tr -d '\r')
|
||
SZ_GZ=$(curl -sk -m 20 -H 'Accept-Encoding: gzip' -o /dev/null -w '%{size_download}' "$BASE$APPV")
|
||
ENC_GZ=$(curl -sk -m 20 -D - -o /dev/null -H 'Accept-Encoding: gzip' "$BASE$APPV" | grep -i '^content-encoding:' | sed 's/^[^:]*:[[:space:]]*//' | tr -d '\r')
|
||
SZ_RAW=$(curl -sk -m 20 -H 'Accept-Encoding:' -o /dev/null -w '%{size_download}' "$BASE$APPV")
|
||
SRC=$(stat -c %s /opt/drouter/web/app.js)
|
||
echo " 源文件 $SRC 字节"
|
||
echo " Accept-Encoding: br,gzip → $SZ_BR 字节(Content-Encoding: ${ENC_BR:-无})"
|
||
echo " Accept-Encoding: gzip → $SZ_GZ 字节(Content-Encoding: ${ENC_GZ:-无})"
|
||
echo " 不带编码 → $SZ_RAW 字节"
|
||
|
||
# 关键:目标机没装 python3-brotli,br 必须降级到 gzip,
|
||
# 绝不能「要 br 就发原文」—— 那样等于 583KB 裸传,比优化前更慢。
|
||
if [ -n "$ENC_BR" ]; then
|
||
ck "带 br 时有编码(实际 ${ENC_BR})" 1
|
||
if [ "$ENC_BR" = "gzip" ]; then
|
||
ck "br 不可用时已降级到 gzip(本机无 python3-brotli,属预期)" 1
|
||
elif [ "$ENC_BR" = "br" ]; then
|
||
echo " (本机装了 brotli,用的是 br)"
|
||
fi
|
||
else
|
||
SZ_NOGZ=$(curl -sk -m 20 -H 'Accept-Encoding: br' -o /dev/null -w '%{size_download}' "$BASE$APPV")
|
||
echo " ⚠ br 无编码且不支持 gzip → $SZ_NOGZ 字节裸传"
|
||
ck "br 不可用且无 gzip 兜底时会裸传(应为 0,本机应装 gzip 支持)" 0
|
||
fi
|
||
[ "$SZ_GZ" -lt "$SRC" ] && ck "gzip 确实压小了($SZ_GZ < $SRC)" 1 || ck "gzip 确实压小了" 0
|
||
[ "$SZ_RAW" = "$SRC" ] && ck "不带编码时原样输出($SZ_RAW = 源文件大小)" 1 || ck "不带编码时原样输出" 0
|
||
|
||
echo
|
||
echo "=== 5. 304 协商(第二次打开应当零传输)==="
|
||
# 304 响应没有 body,curl **不会创建** -o 指定的文件 → stat 会报
|
||
# 「没有那个文件或目录」,进而让 [: -lt ] 拿到空值。
|
||
# 用 %{size_download} 让 curl 自己报字节数,不依赖文件是否存在。
|
||
rm -f /tmp/_304body
|
||
read -r CODE BODY <<<"$(curl -sk -m 10 -o /tmp/_304body -w '%{http_code} %{size_download}' \
|
||
-H "If-None-Match: $ETAG" "$BASE$APPV")"
|
||
BODY=${BODY:-0}
|
||
echo " 状态码 $CODE,响应体 $BODY 字节"
|
||
[ "$CODE" = "304" ] && ck "If-None-Match 命中 → 304" 1 || ck "If-None-Match 命中 → 304(实际 $CODE)" 0
|
||
[ "$BODY" -lt 100 ] && ck "304 不回传响应体" 1 || ck "304 不回传响应体(实际 $BODY 字节)" 0
|
||
rm -f /tmp/_304body
|
||
|
||
echo
|
||
echo "=== 6. 无指纹请求 → 必须仍是 no-cache ==="
|
||
CC2=$(curl -sk -m 10 -D - -o /dev/null "$BASE/app.js" | grep -i '^cache-control:' | sed 's/^[^:]*:[[:space:]]*//' | tr -d '\r')
|
||
echo " Cache-Control: $CC2"
|
||
printf '%s' "$CC2" | grep -q 'no-cache' && ck "无指纹 → no-cache(否则部署后拿到旧文件)" 1 || ck "无指纹 → no-cache(实际: $CC2)" 0
|
||
printf '%s' "$CC2" | grep -q 'immutable' && ck "⚠ 无指纹却给了 immutable(会导致用户卡在旧版本)" 0 || ck "无指纹没有误给 immutable" 1
|
||
|
||
echo
|
||
echo "=== 7. Vary 头(代理正确性)==="
|
||
V=$(curl -sk -m 10 -D - -o /dev/null -H 'Accept-Encoding: gzip' "$BASE$APPV" | grep -ci '^vary:.*Accept-Encoding')
|
||
[ "$V" -ge 1 ] && ck "压缩响应带 Vary: Accept-Encoding" 1 || ck "压缩响应带 Vary: Accept-Encoding" 0
|
||
|
||
echo
|
||
echo "=== 8. 版本号注入 ==="
|
||
if printf '%s' "$IDX" | grep -q '__APP_VERSION__'; then
|
||
ck "页脚版本号已注入(不应残留占位符)" 0
|
||
else
|
||
ck "页脚版本号已注入(无占位符残留)" 1
|
||
fi
|
||
VER=$(printf '%s' "$IDX" | sed -n 's/.*id="app-ver"[^>]*>\(v[0-9][^<]*\)<.*/\1/p' | head -1)
|
||
[ -z "$VER" ] && VER=$(printf '%s' "$IDX" | grep -o 'v[0-9]\+\.[0-9]\+\(\.[0-9]\+\)\?' | tail -1)
|
||
echo " 页面显示版本号 = ${VER:-(未找到)}"
|
||
[ -n "$VER" ] && ck "页脚有版本号" 1 || ck "页脚有版本号" 0
|
||
VFILE=$(cat /opt/drouter/VERSION 2>/dev/null | tr -d ' \t\r\n')
|
||
echo " /opt/drouter/VERSION = ${VFILE:-(不存在)}"
|
||
[ -n "$VFILE" ] && ck "VERSION 文件已部署" 1 || ck "VERSION 文件已部署" 0
|
||
if [ -n "$VER" ] && [ -n "$VFILE" ]; then
|
||
[ "v$VFILE" = "$VER" ] && ck "页面版本号与 VERSION 文件一致" 1 || ck "页面版本号与 VERSION 文件一致(页面 $VER vs 文件 v$VFILE)" 0
|
||
fi
|
||
|
||
echo
|
||
echo "=== 9. index.html 的 ETag 含版本号 ==="
|
||
IE=$(curl -sk -m 10 -D - -o /dev/null "$BASE/index.html" | grep -i '^etag:' | sed 's/^[^:]*:[[:space:]]*//' | tr -d '\r')
|
||
echo " index.html ETag = $IE"
|
||
if [ -n "$VFILE" ]; then
|
||
printf '%s' "$IE" | grep -q "$VFILE" && ck "ETag 里含版本号(改版本即换 ETag)" 1 || ck "ETag 里含版本号" 0
|
||
fi
|
||
|
||
echo
|
||
echo "=== 10. 两个 UI 元素 ==="
|
||
printf '%s' "$IDX" | grep -q 'id="brand-home"' && ck "左上角 logo 是可点击元素" 1 || ck "左上角 logo 是可点击元素" 0
|
||
printf '%s' "$IDX" | grep -q '<a class="brand' && ck "logo 用 <a>(键盘/读屏可用)" 1 || ck "logo 用 <a>" 0
|
||
printf '%s' "$IDX" | grep -q 'title="返回系统概览"' && ck "logo 有可访问名称" 1 || ck "logo 有可访问名称" 0
|
||
printf '%s' "$IDX" | grep -q 'id="app-ver"' && ck "页脚有版本号容器" 1 || ck "页脚有版本号容器" 0
|
||
|
||
echo
|
||
echo "=== 11. openapi 的 version 与页面一致(/api/openapi 免鉴权)==="
|
||
API_VER=$(curl -sk -m 5 "$BASE/api/openapi" | grep -o '"version"[[:space:]]*:[[:space:]]*"[^"]*"' | head -1 | sed 's/.*"\([^"]*\)"$/\1/')
|
||
echo " openapi version = ${API_VER:-(未取到)}"
|
||
[ -n "$API_VER" ] && ck "openapi 可免鉴权访问且能取到 version" 1 || ck "openapi version 取不到" 0
|
||
if [ -n "$VFILE" ] && [ -n "$API_VER" ]; then
|
||
[ "$API_VER" = "$VFILE" ] && ck "openapi version 与 VERSION 文件一致(都是 $VFILE)" 1 \
|
||
|| ck "openapi version 与 VERSION 一致($API_VER vs $VFILE)" 0
|
||
fi
|
||
|
||
echo
|
||
if [ "$fail" -eq 0 ]; then echo "=== 全部通过 ==="; else echo "=== $fail 项失败 ==="; fi
|
||
exit $fail
|