#!/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('' % (bx, by, bw, bh, bg, color)) add('%s' % (bx + BAND_PAD_X, by + 22, BAND_TITLE_FONT, color, esc(title))) if sub: tw = text_w(title, BAND_TITLE_FONT) add('%s' % (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('' % (cx, cy, cw, CHIP_H, color)) add('%s' % (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('' % (W, total_h)) add('%s' % (PAD, 40, TITLE_FONT, esc("为什么网络「一直不稳」——七个误会与逐个拆掉(自愈架构复盘)"))) add('%s' % (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('' % (x0, ay, x1, ay, C_GRAY)) add('' % (x1, ay, C_GRAY)) add('%s' % (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('' % (ax, y0, ax, y1, C_PROB)) add('' % (ax, y1, C_PROB)) add('%s' % (PAD - 14, placed[3][5] + placed[3][6] / 2, C_PROB, PAD - 14, placed[3][5] + placed[3][6] / 2, esc("叠加累积 → 越扫越脏"))) # 底部总结盒 add('' % (PAD, sum_y, lw, SUM_H, C_PROB_BG, C_PROB)) add('%s' % (PAD + BAND_PAD_X, sum_y + 28, C_PROB, esc(SUM_LEFT[0]))) add('%s' % (PAD + BAND_PAD_X, sum_y + 52, esc(SUM_LEFT[1]))) rx = PAD + lw + COL_GAP add('' % (rx, sum_y, rw, SUM_H, C_FIX_BG, C_FIX)) add('%s' % (rx + BAND_PAD_X, sum_y + 28, C_FIX, esc(SUM_RIGHT[0]))) add('%s' % (rx + BAND_PAD_X, sum_y + 52, esc(SUM_RIGHT[1]))) add('%s' % (PAD, total_h - 16, esc(FOOT))) svg = ('\n%s\n\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()