4c74cfa825
批次二文档半(道 gate-docs 执行)。先纠三个数的口径账,别再拿任何旧数当准:**234 处是准数** —— 卡片写 52(Lead 用了门禁 Checker 的引用集合,它**刻意排除属主文档内部的定义位**)、道里先报 265(裸正则把 `SDL2-2.32.10` 里的 `L2-2` 当成功能号,误报 10 处)。最终边界口径 = 带左边界 + 跳过反引号代码段。 · 19 只文件 HEAD 共 234 处 → 现 40 处,**40 处 100% 落在变更记录行**(冻结历史,按 Q3 一条没动), 非变更记录残留 **0**;改写 163 行 / 16 只文档。站点最多:规划文档 110→37、相量层单测说明 25→1、 采集与数据处理短板评测 21→0、App/algo/README 26→0。 (`App/algo/README.md` 由上一笔代码提交带上车,路径归属已在该笔正文注明,内容是本批改写) · **定义表首列由编号改成语义标识**(编号规范 §3.3「表首格就是短语」的完全形态):首列原为编号的表 共 **7 张**(不是 5 张),删掉与之重复的「语义名」列;表头标签按域词区分——三张 L3 保护**元件**表 保留 `| 元件 |`,四张 L1/L2 表用 `| 功能名 |`(把「元件」统一洗成功能名会丢掉领域信息)。 七张表逐张核:表头竖线数 == 分隔行 == 首条数据行 == 列数+1;与 HEAD 逐块对拍**净新增不一致块 0**, 并且正是这个对拍抓到第一版「三张表数据行已删列、表头没删」的破表。 · 7 处删号留下的残词/重复解释括号人工收口(如「L1-5(通道标定)」→「通道标定(一次系数 + 零漂)」), 复扫待收口站点 0;措辞逐字取自映射表,无第二种说法。 · tools/arch_diagram 三只生成器 4 处(3 处 docstring + 1 处图形文案)与产物同批:重跑后**只有位图那张 变 2 个 text 节点的换行位置**,画布 1580x740 与总行数不变,另两张逐字节未变;改前先干跑对照证明三张 svg 原本与生成器一致(不在 stale 基线里)——`check_generated_fresh --only docs-svg` PASS。 本笔另含 **F-* 半批的文档面**(同一轮 `git add docs tools/arch_diagram AGENTS.md` 一并收进 ,非误伤):BSD 函数调用关系里 9 处反引号紧邻指针、属主文档 §7 的停发句与导读行(**12 条定义行原样保留、不删登记族**)、 以及 `gen_net_instability_svg.py` 与它的产物 `网络不稳根因总览.svg`(「唯一读者红线」那条的框宽自适应 526 → 551,右缘 1411 未越界,画布 1560x1468 不变)——该半批的**代码注释面**在下一笔。 ⚠ 因 `docs/任务规划/登记簿.md` 与 `docs/任务规划/UI补齐.md` 正被另一张卡(UI 叶数复核)改动,本笔**不含 那两只文件**,批次二的 §9 存量收尾与它们同批提交。
261 lines
12 KiB
Python
261 lines
12 KiB
Python
#!/usr/bin/env python3
|
||
# -*- coding: utf-8 -*-
|
||
"""生成 docs/images/网络不稳根因总览.svg —— 自愈架构复盘配图(纯标准库)。
|
||
|
||
布局:左列 = 七个误会(问题环,玫红),右列 = 对应拆法(绿),行间水平箭头;
|
||
左列环间竖向细箭头 = "叠加累积";底部两个总结盒。
|
||
为什么不用 Mermaid:双列对齐 + 水平映射箭头需要精确布局。
|
||
|
||
用法(仓库根目录,任意 python3):
|
||
python tools/arch_diagram/gen_net_instability_svg.py
|
||
输出:
|
||
docs/images/网络不稳根因总览.svg
|
||
改内容时:编辑本文件顶部的 RINGS / SUMMARY 数据后重跑即可。
|
||
"""
|
||
import html
|
||
import os
|
||
|
||
ROOT = os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
||
OUT = os.path.join(ROOT, "docs", "images", "网络不稳根因总览.svg")
|
||
|
||
# ----------------------------- 画布与尺寸参数 -----------------------------
|
||
W = 1560
|
||
PAD = 26
|
||
TITLE_FONT = 20
|
||
SUB_FONT = 13
|
||
BAND_TITLE_FONT = 15
|
||
BAND_SUB_FONT = 11.5
|
||
CHIP_FONT = 13
|
||
|
||
CHIP_H = 34 # 芯片盒高
|
||
CHIP_GAP = 10 # 芯片水平间距
|
||
CHIP_ROW_GAP = 8 # 芯片行间距
|
||
CHIP_PAD = 14 # 芯片内左右留白
|
||
BAND_PAD_X = 16 # 框内左右留白
|
||
BAND_TITLE_H = 32 # 框内标题区高
|
||
BAND_PAD_BOTTOM = 12
|
||
COL_GAP = 128 # 左右列间距(放映射箭头)
|
||
ROW_V_GAP = 40 # 行间距(放"叠加"竖向箭头)
|
||
TITLE_H = 78 # 顶部标题区高
|
||
SUM_H = 78 # 底部总结盒高
|
||
|
||
FONT = "Microsoft YaHei, Segoe UI, PingFang SC, sans-serif"
|
||
C_PROB = "#e11d48" # 问题列(玫红)
|
||
C_PROB_BG = "#fff1f2"
|
||
C_FIX = "#16a34a" # 拆法列(绿)
|
||
C_FIX_BG = "#f0fdf4"
|
||
C_GRAY = "#94a3b8"
|
||
|
||
# ----------------------------- 数据(改这里) -----------------------------
|
||
# (问题标题, 问题副标, [问题芯片], 拆法标题, [拆法芯片])
|
||
RINGS = [
|
||
("① 门铃听不见", "TRAP-NET-17:事件未收到(E14 复核:非芯片丢,是被读清消费)",
|
||
["症状:UI 复连永久拒连、FTP 第 29 轮自己死掉,全程不报一个错",
|
||
"机制:快速复连时门铃事件被读清消费,netTask 只靠门铃干活 → 状态静默滞留"],
|
||
"100ms 巡逻对账",
|
||
["单一 reconciler:判据读驱动状态缓存(E2 背书 18.1µs),分级去抖",
|
||
"门铃听不见不要紧,每 100ms 亲自数一遍人头"]),
|
||
("② 门铃总闸看一眼就清零", "TRAP-NET-20 B3",
|
||
["症状:别的 Socket 的 CONNECT/DISCONNECT 从此永久失踪",
|
||
"机制:全局中断寄存器整体读即清,open/relisten 丢弃式读一遍=拔全楼门铃线"],
|
||
"总闸唯一读者",
|
||
["唯一读者红线:全局寄存器唯一读者=net_poll;open/relisten 只清本 Socket SINT",
|
||
"查自己门铃时,不许拔别人家的线"]),
|
||
("③ 柜台和货梯是同一台", "单连接模型",
|
||
["症状:会话 EOF 之后端口「连接被拒」",
|
||
"机制:监听 fd == 数据 fd,应用在 EOF 时 net_close = 把柜台连电梯一起拆"],
|
||
"送客走正门",
|
||
["铁律:EOF 不得 close 监听 fd;新 API net_drop_conn(拆会话保柜台)",
|
||
"net_drop_conn = 优雅 FIN + 本地拆除 + relisten,幂等"]),
|
||
("④ 三个临时工都在修电梯", "多所有者 relisten",
|
||
["症状:重建风暴、慢消费者的 EOF 被吞、新客户端数据被旧会话吸收",
|
||
"机制:accept/recv/pending 三处都重建监听,谁先看到断连谁动手互相踩脚"],
|
||
"修电梯的只剩一个人",
|
||
["重建收敛到唯一所有者(net_listener + reconciler),其余位置只置事实不动手",
|
||
"不变量 II:唯一周期重建决策者(消费者即时重建三处保留,台架实测)"]),
|
||
("⑤ 传达员被按在椅子上", "队头阻塞",
|
||
["症状:FTP banner 迟到 25~30 s(台架实测),netTask 频繁冻结半秒",
|
||
"机制:30 s 阻塞收 + 500 ms 盲等忙等,而 netTask 是全楼唯一传达员"],
|
||
"传达员永不离开传达室",
|
||
["忙等统一为 net_sock_reset_sync 有界排渣(600 ms 硬上限),阻塞消息化",
|
||
"recv 语义主流化:net_recv 默认无限阻塞 + net_recv_nb + 60 s tripwire 日志"]),
|
||
("⑥ 僵尸客人赖着不走", "E13:网卡闪断",
|
||
["症状:设备网络「莫名死了」、新连接被拒——在和幽灵做生意",
|
||
"机制:PC 网卡闪断,内核悄悄撕掉 socket 不发 FIN,芯片侧仍 ESTABLISHED 占死柜台"],
|
||
"查岗处理幽灵",
|
||
["keepalive 60/5/3(E11 实测 ≈30 s 判死)+ 芯片 TIMEOUT → QUARANTINE → 重建",
|
||
"自愈 30~90 s,不需要冷启动(E13 台架收口:用户判断正确)"]),
|
||
("⑦ 只会礼貌送客不会轰人", "close 无 RST(手册定案)",
|
||
["症状:FIN_WAIT_2 长尾,监听恢复受限于对端 close(台架定案)",
|
||
"机制:芯片 close 只发 FIN;对端内核存活但应用挂死 = 回 ACK 却不起身"],
|
||
"知道他不轰人,就别指望他轰人",
|
||
["定案写入 TRAP-NET-07:兜底靠查岗(keepalive/TIMEOUT),不靠 RST",
|
||
"net_drop_conn 本地拆除 + reconciler 形态③④ 认领僵尸态"]),
|
||
("⑧ 垃圾不清越扫越脏", "残渣风暴态",
|
||
["症状:同一份固件故障率 2%~54% 摆动(E8 实测),看起来像「玄学」",
|
||
"机制:迟到 DISCONNECT 锁存 / 半死 Socket / 粘滞中断位,被带进下一条连接"],
|
||
"开张前把垃圾扫干净",
|
||
["sock_reset 标准排渣:读空 RX → CLOSE → 排空事件 → 确认落地(byte0=00H)",
|
||
"M3 矛盾态门 + M5 drain-until-quiescent(风暴态免疫)"]),
|
||
]
|
||
|
||
SUM_LEFT = ["七环叠加 =「玄学不稳」",
|
||
"随机 · 无错误日志 · 时好时坏——每个环节单看都「正常工作」"]
|
||
SUM_RIGHT = ["逐环拆掉 = 稳(现状机制已全部落地)",
|
||
"不依赖门铃 · 唯一主人 · 永不冻结 · 100ms 对账 · 开张前排渣——台架回归全绿"]
|
||
|
||
FOOT = ("证据链:docs/问题记录/CH395F_Trap_Records.md(TRAP-NET-07/-09/-12/-17/-18/-20) · "
|
||
"架构现状 docs/网络/网络自愈架构设计.md §2~§4 · 实证锚点同文档 §5.2(E0~E19)")
|
||
|
||
|
||
# ----------------------------- 工具函数 -----------------------------
|
||
def esc(s):
|
||
return html.escape(s, quote=True)
|
||
|
||
|
||
def text_w(s, fs):
|
||
"""粗略估算文本像素宽:CJK/全角/圈码/箭头 ≈ 1.0em,其余 ≈ 0.58em(宁大勿小)。"""
|
||
w = 0.0
|
||
for ch in s:
|
||
w += fs * (1.0 if ord(ch) >= 0x2460 else 0.58)
|
||
return w
|
||
|
||
|
||
def chip_w(label):
|
||
return text_w(label, CHIP_FONT) + 2 * CHIP_PAD
|
||
|
||
|
||
def wrap_chips(chips, avail):
|
||
rows, cur, cur_w = [], [], 0.0
|
||
for c in chips:
|
||
cw = chip_w(c)
|
||
need = cw if not cur else cur_w + CHIP_GAP + cw
|
||
if cur and need > avail:
|
||
rows.append(cur)
|
||
cur, cur_w = [c], cw
|
||
else:
|
||
cur.append(c)
|
||
cur_w = need
|
||
if cur:
|
||
rows.append(cur)
|
||
return rows
|
||
|
||
|
||
def band_h(rows):
|
||
return BAND_TITLE_H + len(rows) * CHIP_H + (len(rows) - 1) * CHIP_ROW_GAP + BAND_PAD_BOTTOM
|
||
|
||
|
||
# ----------------------------- 生成 -----------------------------
|
||
def draw_band(out, title, sub, chips, bx, by, bw, bh, color, bg):
|
||
add = out.append
|
||
add('<rect x="%d" y="%.1f" width="%d" height="%.1f" rx="10" fill="%s" stroke="%s" '
|
||
'stroke-opacity="0.45"/>' % (bx, by, bw, bh, bg, color))
|
||
add('<text x="%d" y="%.1f" font-size="%d" font-weight="700" fill="%s">%s</text>'
|
||
% (bx + BAND_PAD_X, by + 22, BAND_TITLE_FONT, color, esc(title)))
|
||
if sub:
|
||
tw = text_w(title, BAND_TITLE_FONT)
|
||
add('<text x="%.0f" y="%.1f" font-size="%s" fill="#64748b">%s</text>'
|
||
% (bx + BAND_PAD_X + tw + 10, by + 22, BAND_SUB_FONT, esc(sub)))
|
||
cy = by + BAND_TITLE_H
|
||
for row in chips:
|
||
cx = bx + BAND_PAD_X
|
||
for c in row:
|
||
cw = chip_w(c)
|
||
add('<rect x="%d" y="%.1f" width="%d" height="%d" rx="7" fill="#ffffff" '
|
||
'stroke="%s" stroke-opacity="0.55"/>' % (cx, cy, cw, CHIP_H, color))
|
||
add('<text x="%d" y="%.1f" font-size="%d" fill="#1e293b" text-anchor="middle">%s</text>'
|
||
% (cx + cw / 2, cy + CHIP_H / 2 + CHIP_FONT * 0.35, CHIP_FONT, esc(c)))
|
||
cx += cw + CHIP_GAP
|
||
cy += CHIP_H + CHIP_ROW_GAP
|
||
|
||
|
||
def build():
|
||
out = []
|
||
add = out.append
|
||
|
||
lw = (W - 2 * PAD - COL_GAP) // 2
|
||
rw = W - 2 * PAD - COL_GAP - lw
|
||
avail_l = lw - 2 * BAND_PAD_X
|
||
avail_r = rw - 2 * BAND_PAD_X
|
||
|
||
# 预排:每行高度 = 左右两框较高者
|
||
placed = []
|
||
y = TITLE_H
|
||
for (pt, ps, pc, ft, fc) in RINGS:
|
||
lrows = wrap_chips(pc, avail_l)
|
||
rrows = wrap_chips(fc, avail_r)
|
||
h = max(band_h(lrows), band_h(rrows))
|
||
placed.append((pt, ps, lrows, ft, rrows, y, h))
|
||
y += h + ROW_V_GAP
|
||
sum_y = y - ROW_V_GAP + 26
|
||
total_h = sum_y + SUM_H + 46
|
||
|
||
add('<rect x="0" y="0" width="%d" height="%d" fill="#ffffff"/>' % (W, total_h))
|
||
add('<text x="%d" y="%d" font-size="%d" font-weight="700" fill="#0f172a">%s</text>'
|
||
% (PAD, 40, TITLE_FONT, esc("为什么网络「一直不稳」——七个误会与逐个拆掉(自愈架构复盘)")))
|
||
add('<text x="%d" y="%d" font-size="%d" fill="#64748b">%s</text>'
|
||
% (PAD, 62, SUB_FONT,
|
||
esc("左列 = 根因(比喻 + 机制);右列 = 现状机制的对应拆法;环与环叠加才构成「玄学」")))
|
||
|
||
for (pt, ps, lrows, ft, rrows, by, h) in placed:
|
||
draw_band(out, pt, ps, lrows, PAD, by, lw, h, C_PROB, C_PROB_BG)
|
||
draw_band(out, ft, "", rrows, PAD + lw + COL_GAP, by, rw, h, C_FIX, C_FIX_BG)
|
||
# 水平映射箭头(问题 → 拆法)
|
||
ay = by + h / 2
|
||
x0 = PAD + lw + 6
|
||
x1 = PAD + lw + COL_GAP - 10
|
||
add('<line x1="%d" y1="%.1f" x2="%d" y2="%.1f" stroke="%s" stroke-width="2.5"/>'
|
||
% (x0, ay, x1, ay, C_GRAY))
|
||
add('<path d="M %d %.1f l -9 -6 v 12 z" fill="%s"/>' % (x1, ay, C_GRAY))
|
||
add('<text x="%d" y="%.1f" font-size="11" fill="#64748b" text-anchor="middle">%s</text>'
|
||
% (PAD + lw + COL_GAP / 2, ay - 8, esc("拆掉")))
|
||
|
||
# 左列环间竖向箭头(叠加累积)
|
||
for i in range(len(placed) - 1):
|
||
y0 = placed[i][5] + placed[i][6] + 5
|
||
y1 = placed[i + 1][5] - 7
|
||
ax = PAD + 60
|
||
add('<line x1="%d" y1="%.1f" x2="%d" y2="%.1f" stroke="%s" stroke-width="1.6" '
|
||
'stroke-dasharray="4 3" stroke-opacity="0.7"/>' % (ax, y0, ax, y1, C_PROB))
|
||
add('<path d="M %d %.1f l -4.5 -7 h 9 z" fill="%s" fill-opacity="0.7"/>' % (ax, y1, C_PROB))
|
||
add('<text x="%d" y="%.1f" font-size="11" fill="%s" transform="rotate(-90 %d %.1f)">%s</text>'
|
||
% (PAD - 14, placed[3][5] + placed[3][6] / 2, C_PROB, PAD - 14,
|
||
placed[3][5] + placed[3][6] / 2, esc("叠加累积 → 越扫越脏")))
|
||
|
||
# 底部总结盒
|
||
add('<rect x="%d" y="%d" width="%d" height="%d" rx="10" fill="%s" stroke="%s" '
|
||
'stroke-opacity="0.5"/>' % (PAD, sum_y, lw, SUM_H, C_PROB_BG, C_PROB))
|
||
add('<text x="%d" y="%d" font-size="15" font-weight="700" fill="%s">%s</text>'
|
||
% (PAD + BAND_PAD_X, sum_y + 28, C_PROB, esc(SUM_LEFT[0])))
|
||
add('<text x="%d" y="%d" font-size="12" fill="#334155">%s</text>'
|
||
% (PAD + BAND_PAD_X, sum_y + 52, esc(SUM_LEFT[1])))
|
||
rx = PAD + lw + COL_GAP
|
||
add('<rect x="%d" y="%d" width="%d" height="%d" rx="10" fill="%s" stroke="%s" '
|
||
'stroke-opacity="0.5"/>' % (rx, sum_y, rw, SUM_H, C_FIX_BG, C_FIX))
|
||
add('<text x="%d" y="%d" font-size="15" font-weight="700" fill="%s">%s</text>'
|
||
% (rx + BAND_PAD_X, sum_y + 28, C_FIX, esc(SUM_RIGHT[0])))
|
||
add('<text x="%d" y="%d" font-size="12" fill="#334155">%s</text>'
|
||
% (rx + BAND_PAD_X, sum_y + 52, esc(SUM_RIGHT[1])))
|
||
|
||
add('<text x="%d" y="%d" font-size="12" fill="#94a3b8">%s</text>'
|
||
% (PAD, total_h - 16, esc(FOOT)))
|
||
|
||
svg = ('<svg xmlns="http://www.w3.org/2000/svg" width="%d" height="%d" '
|
||
'viewBox="0 0 %d %d" font-family="%s">\n%s\n</svg>\n'
|
||
% (W, total_h, W, total_h, FONT, "\n".join(out)))
|
||
return svg, total_h
|
||
|
||
|
||
def main():
|
||
svg, h = build()
|
||
os.makedirs(os.path.dirname(OUT), exist_ok=True)
|
||
with open(OUT, "w", encoding="utf-8", newline="\n") as f:
|
||
f.write(svg)
|
||
import xml.etree.ElementTree as ET
|
||
ET.fromstring(svg)
|
||
print("OK %d bytes, canvas %dx%d -> %s" % (len(svg.encode("utf-8")), W, h, OUT))
|
||
|
||
|
||
if __name__ == "__main__":
|
||
main()
|