Files
drouter/_dev/t-perf.py
T
liuzhuohua 64df3cf331 feat: Drouter 1.0.0 —— Debian 13 软路由管理系统
一套跑在 Debian 13 (trixie) 上的软路由管理系统,提供 43 个功能页面的
Web 管理面板:WAN 接入、PPPoE 多拨、DHCP/DNS、IPv6、防火墙、QoS 限速、
访问控制、DDNS、文件共享、Docker 管理等。

核心设计原则:不重新实现任何协议
  PPPoE 交给 ppp,DHCP 交给 dnsmasq,IPv6 RA 交给 radvd,NAT 交给 nftables。
  Web 面板只做三件事:渲染原生配置文件 → 语法预检 → 原子写入并 reload。
  面板删掉后,系统仍是一台配置完好的 Debian 路由器。

安全默认值
  · 安装后不接管网络:不改默认路由、不开 IP 转发、不启动 DHCP 服务
  · 所有改网络的动作都需要用户在面板上亲手点「应用」
  · 避开 5900 端口,不触碰 VNC / X 会话
  · 配置写入前先做语法预检,失败则回滚

架构
  · backend/  Python 3 标准库,零第三方依赖(9 个模块)
  · web/      原生 JS/CSS,无构建步骤
  · 常驻 helper 通过 Unix socket 提供 75 个动作,socket 不可用时回退 subprocess
  · 鉴权走 X-Token 请求头

打包
  · packaging/build-deb.sh            → drouter_1.0.0_all.deb(含 6 个 systemd 单元)
  · scripts/make-offline-deps.sh      → 236 个依赖 .deb + 本地仓库索引
  · packaging/build-offline-bundle.sh → 自包含离线包,目标机零外网可装
  · packaging/build-docker.sh         → 容器镜像(体验用)

测试
  40 套单元测试 + 3 个前端检查脚本(菜单映射 / 离线渲染 / 手机适配),
  全部挂在 devtools/sync.sh 的预检链里。

许可:MIT
2026-09-30 19:26:24 +08:00

185 lines
8.6 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.
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""性能回归(#85):钉死本轮修掉的性能问题,防止改回去。
这个测试专门盯三类「看起来没事、实际一直在浪费」的问题:
① 页面切换后定时器泄漏(切走还在轮询 → 后端被空打);
② 高频接口里的 subprocess 滥用(每次切页面都要 fork 好几次);
③ 死代码分支(写了 quick 参数却两个分支一样)。
放这里而不是散在各模块测试里,是因为它们都是「跨模块的性能约定」,
改任何一个视图或读接口都可能破坏,需要一处集中守住。
"""
import io
import os
import re
import sys
ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
HELPER = os.path.join(ROOT, 'backend', 'drouter-helper.py')
APP = os.path.join(ROOT, 'web', 'app.js')
PASS = FAIL = 0
def chk(name, cond, extra=''):
global PASS, FAIL
if cond:
PASS += 1
print('[OK] %s %s' % (name, extra))
else:
FAIL += 1
print('[FAIL] %s %s' % (name, extra))
HELPER_SRC = io.open(HELPER, encoding='utf-8').read()
APP_SRC = io.open(APP, encoding='utf-8').read()
HELPER_LINES = HELPER_SRC.split('\n')
APP_LINES = APP_SRC.split('\n')
def hseg(name):
"""取 helper 里某个顶层函数的函数体(按行切片)。"""
try:
i = next(k for k, l in enumerate(HELPER_LINES) if l.startswith('def %s(' % name))
except StopIteration:
return ''
j = next((k for k in range(i + 1, len(HELPER_LINES))
if HELPER_LINES[k].startswith(('def ', 'class ', '@'))), len(HELPER_LINES))
return '\n'.join(HELPER_LINES[i:j])
def strip_comment_lines(text):
"""去掉整行注释与行尾注释,只留可执行代码。
为什么需要:源码里保留「原来是 sh(['cat', '/proc/loadavg'])」这类**说明性
注释**是好事(后人知道为什么改),但如果断言直接对原始文本做 substring
匹配,就会被自己的注释骗过 —— 本轮 t-perf 已经踩过一次。
这里做一个够用的近似:逐行截断到第一个未出现在字符串字面量里的 '#'。
"""
out = []
for line in text.split('\n'):
q = None
cut = len(line)
for k, c in enumerate(line):
if q:
if c == '\\':
continue
if c == q:
q = None
elif c in ('"', "'"):
q = c
elif c == '#':
cut = k
break
out.append(line[:cut])
return '\n'.join(out)
def main():
print('--- ① 页面切换不得泄漏定时器(前端)---')
chk('定义了 stopPageTimers', 'function stopPageTimers(' in APP_SRC)
chk('go() 首件事就是停表',
re.search(r'function go\(k\) \{\s*\n\s*(//[^\n]*\n\s*)*stopPageTimers\(\);', APP_SRC) is not None)
# 所有模块级 interval 定时器变量都必须出现在 stopPageTimers 里
for v in ('DASH_TIMER', 'WANLOG_TIMER', 'FWL_TIMER', 'DK_TIMER'):
seg = APP_SRC[APP_SRC.index('function stopPageTimers('):]
seg = seg[:seg.index('\nfunction go(')]
chk('stopPageTimers 里清了 %s' % v, v in seg)
chk('stopPageTimers 调用了 pubip 的停止函数', 'pubipProbeStopPoll' in
APP_SRC[APP_SRC.index('function stopPageTimers('):APP_SRC.index('\nfunction go(')])
# 闭包内定时器通过全局钩子暴露
chk('声明了 stopLeaseTimer 钩子', re.search(r'let stopLeaseTimer\s*=', APP_SRC) is not None)
chk('声明了 stopV6Timer 钩子', re.search(r'let stopV6Timer\s*=', APP_SRC) is not None)
chk('viewDhcp 挂上了 stopLeaseTimer', 'stopLeaseTimer = () =>' in APP_SRC)
chk('viewV6Test 挂上了 stopV6Timer', 'stopV6Timer = () =>' in APP_SRC)
print('\n--- ② 轮询函数必须先判 DOM 再发请求 ---')
# liveWanLog:不能出现「先 api(...) 再 if (!box) return」
seg = APP_SRC[APP_SRC.index('async function liveWanLog('):]
seg = seg[:seg.index('\n}') + 2]
i_dom = seg.find("$('#pp-live')")
i_api = seg.find('api(')
chk('liveWanLog 判 DOM 在发请求之前', 0 <= i_dom < i_api,
'dom@%d api@%d' % (i_dom, i_api))
# loadLeases 同理
seg2 = APP_SRC[APP_SRC.index('const loadLeases = ()'):]
seg2 = seg2[:seg2.index('stopLeaseTimer =')]
chk('loadLeases 先判 #dh-leases 再请求',
seg2.find("if (!$('#dh-leases'))") < seg2.find('api('))
print('\n--- ③ 导航代次:旧请求不得覆盖新页面 ---')
chk('定义了 NAV_GEN', re.search(r'let NAV_GEN\s*=\s*0', APP_SRC) is not None)
chk('go() 里递增代次', re.search(r'function go\(k\) \{.*?NAV_GEN\+\+', APP_SRC, re.S) is not None)
chk('有 navStale 判断函数', 'function navStale(' in APP_SRC)
chk('渲染链路校验代次', 'if (navStale(gen)) return;' in APP_SRC)
print('\n--- ④ 高频接口不得滥用 subprocess ---')
seg_si = hseg('read_sysinfo')
# 断言只看「可执行代码」,先剥掉注释——否则解释性注释里写一句
# 「原来是 sh(['cat', '/proc/loadavg'])」就会把测试自己骗过(本轮已踩)。
code_si = strip_comment_lines(seg_si)
chk('read_sysinfo 不用 sh([\'hostname\'])', "sh(['hostname'])" not in code_si)
chk("read_sysinfo 不用 sh(['cat', '/proc/loadavg'])",
'loadavg' not in code_si or "sh(['cat'" not in code_si)
chk('read_sysinfo 用 socket.gethostname()', 'socket.gethostname()' in seg_si)
chk('read_sysinfo 用 os.uname()', 'os.uname()' in seg_si)
chk('read_sysinfo 改用 _read_disks()', '_read_disks()' in seg_si)
chk('read_sysinfo 不再直接调 df', "sh(['df'" not in code_si)
print('\n--- ⑤ _read_disks 实现正确性 ---')
seg_d = hseg('_read_disks')
chk('_read_disks 读 /proc/mounts', "'/proc/mounts'" in seg_d)
chk('_read_disks 用 statvfs', 'os.statvfs(' in seg_d)
chk('_read_disks 过滤 tmpfs/devtmpfs',
"'tmpfs'" in seg_d and "'devtmpfs'" in seg_d)
chk('_read_disks 输出与 df 同结构(含 mount/pct)',
"x['mount']" in seg_d and "'pct'" in seg_d)
chk('_read_disks 根分区排最前', "x['mount'] != '/'" in seg_d)
chk('_read_disks 注册在 read_sysinfo 之前',
HELPER_SRC.index('def _read_disks(') < HELPER_SRC.index('def read_sysinfo('))
print('\n--- ⑥ 死代码分支(quick 参数)---')
seg_m = strip_comment_lines(hseg('read_metrics'))
# 注意:不能只判 'if not quick else',因为合法写法 `x if quick else y` 也存在。
# 真正的「死代码」是**条件与两侧完全颠倒成同一结果**,即 `A if not quick else A`。
chk('read_metrics 不再出现「两个分支一样」的写法',
re.search(r'(\w[\w\.\(\)\[\]]*)\s+if\s+not\s+quick\s+else\s+\1\b', seg_m) is None)
# quick 仍应被读取(保留参数兼容),但不得造成同义分支
chk('quick 参数仍被读取(向后兼容)', "quick = bool(" in seg_m)
print('\n--- ⑦ 批量取服务状态(不再逐条 fork systemctl)---')
chk('定义了 _svc_states 批量函数', 'def _svc_states(' in HELPER_SRC)
seg_ss = hseg('_svc_states')
chk('_svc_states 用一次 systemctl show', "'show'" in seg_ss and "'--value'" in seg_ss)
chk('_svc_states 同时请求两个属性',
"'-p'" in seg_ss and 'ActiveState' in seg_ss and 'UnitFileState' in seg_ss)
chk('_svc_states 按属性个数分段切分',
'len(props)' in seg_ss)
seg_rs = strip_comment_lines(hseg('read_services'))
chk('read_services 不再循环调 systemctl is-active',
"'is-active'" not in seg_rs and "'is-enabled'" not in seg_rs)
chk('read_services 改用 _svc_states', '_svc_states(' in seg_rs)
# read_share / read_logd / read_docker 三处同样的逐条 fork 也要收口
for fn in ('read_share', 'read_logd', 'read_docker'):
seg = strip_comment_lines(hseg(fn))
chk('%s 不再逐条 systemctl is-active/is-enabled' % fn,
"'is-active'" not in seg and "'is-enabled'" not in seg
and '_svc_states(' in seg)
# which / command -v 也不该在循环里反复 fork
chk('read_ulog_conf 用 shutil.which(零 fork)',
'shutil.which(' in strip_comment_lines(hseg('read_ulog_conf')))
chk('_dep_installed 用 shutil.which',
'shutil.which(' in strip_comment_lines(hseg('_dep_installed')))
seg_dpi = strip_comment_lines(hseg('_dpi_installed'))
chk('_dpi_installed 不再循环 fork bash -lc',
"bash', '-lc'" not in seg_dpi)
print('\n' + '=' * 58)
print('通过 %d / 失败 %d' % (PASS, FAIL))
return 1 if FAIL else 0
if __name__ == '__main__':
sys.exit(main())