v1.0.10 两件大事:
1. 界面中英双语(英文界面无中文残留)
- 新增 web/i18n.js(约 4200 条词条),零依赖零构建
- 后端下发中文数据走 bt4() 查表翻译;独立模块(upstream/netdetail/
realtime/update)纳入翻译并注册切语言重绘钩子
- 后端「状态/聚合」接口只回稳定枚举+原始数值,不下发中文文案
- 顺带修一个真实 XSS:顶层 const 不挂 window,独立脚本 esc 静默退化成零转义
2. 修掉「文字竖着排/错位」(多分辨率反复暴露)
- 单元格 min-content 只有 1 字宽 → word-break:keep-all + 表头 nowrap
- 依赖卡片正文被标签/按钮挤成 27px → .dep-body{flex:1 1 200px} + flex-wrap
- 裸 flex:1(下限为0)给正文 → 一律改 flex:1 1 <px>
- .dep-item 横向容器直接塞块级子项 → 退回块级
- 关键陷阱:auto-fill 布局屏越宽列数越多单卡越窄,窄屏反而测不出来,
真机审计推进到「逐视图点按钮 + 1920/2560 宽视口 + 中英双语」
- 守卫 t-layout.py 扩到 35 条,12 处反向验证全红
3. 修真缺陷:_link_since() 重启检测被误改成 if False:(注释还写着「已修」),
uptime 归零后相减得负数 → 恢复比对 boot_id
附带:新增 25 个检查器(t-*.py)、排版审计工具链(shot-*/ocr-*/live-*)、
版本号兜底值同步到 1.0.10、.gitignore 收纳 i18n/排版排查的一次性脚本。
198 lines
9.5 KiB
Python
198 lines
9.5 KiB
Python
#!/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)
|
||
|
||
# ---- ⑧ 高频接口里的「阻塞 sleep / 逐包 fork」(2026-10-09 真机实测)----
|
||
# 真机实测 read:docker 960ms、read:sysinfo 252ms,两者都在「每次切页面」
|
||
# 的路径上,且都是纯浪费:878ms 是逐个 fork apt-cache(每次都要加载
|
||
# 整个包索引),250ms 是 _read_cpu_usage() 里硬 sleep 的采样间隔。
|
||
seg_si2 = strip_comment_lines(hseg('read_sysinfo'))
|
||
chk('read_sysinfo 走 _cpu_pct_cached(不直接 _read_cpu_usage)',
|
||
'_read_cpu_usage(' not in seg_si2 and '_cpu_pct_cached(' in seg_si2)
|
||
seg_pa = strip_comment_lines(hseg('_pkgs_available'))
|
||
chk('_pkgs_available 一次 apt-cache 查全部(不逐个 fork)',
|
||
"'--no-all-versions'] + names" in seg_pa)
|
||
chk('_pkgs_available 用 apt 索引指纹缓存(不是盲 TTL)',
|
||
'_apt_lists_stamp()' in seg_pa and '_PKG_AVAIL_CACHE' in seg_pa)
|
||
|
||
print('\n' + '=' * 58)
|
||
print('通过 %d / 失败 %d' % (PASS, FAIL))
|
||
return 1 if FAIL else 0
|
||
|
||
|
||
if __name__ == '__main__':
|
||
sys.exit(main())
|