Files
drouter/_dev/accept-static-perf.sh
liuzhuohua ed5eb4428b 1.0.9 静态资源传输层优化 + 页脚版本号 + logo 回概述
## 性能:瓶颈在传输,不在代码

先量后改。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 项移动端适配全过。
2026-10-04 10:27:02 +08:00

152 lines
8.6 KiB
Bash
Raw Permalink 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.
#!/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