Files
STM32F4-Base/tools/arch_diagram/gen_net_pending_svg.py
T
wandering1 0edbfaf08b
CI / L0 静态门禁 (pre-commit) (push) Failing after 1m12s
CI / L0.5 主机侧单测 (MSVC) (push) Successful in 17s
fix(NET): 监听重建单一所有者收口 + 沉降窗 800ms 上限只留痕
T0 降噪 + T1 单一所有者:四处重建触发(recon/accept/eof/drop)与 pending
第五处统一走 net_listener_request(),由 net_listener_tick 单点执行;
沉降重试日志改"只留首末"(E0 口径),每次恢复少打 3 行重复行。

force open 降级为只留痕(E19 反事实 A/B 实测):芯片未回 CLOSED 时强行
open 注定吃 USE 错(TRAP-NET-09),放行与不放行的端口恢复时刻同一分布
(放行 +512 / 不放行 +505·533 / 降级后 +509 ms)——恢复由芯片何时回
CLOSED 决定。真正兜住"监听器永久失效"的是 D10 预算到期 + 对账形态④
重新登记,不是放行。新增 s_relisten_capped[] 使上限留痕只打一次,
隔离标志不清 → 下一轮继续排空残渣(构建 B 实测每 30ms 两行会刷屏)。

负向注入三轮打靶(E18,注入代码不入库):形态① 吞掉 DISCONNECT 后
4/4 救回、③ 空闲监听器静默失位两轮独立复现、⑤ 开盲区吞掉芯片自发中断
+ 收回 LISTEN 后 -> resync 命中且挂起 recv 被取消 → 三条均不可降级为
只留痕。顺带稳定复现 [ERR] relisten retry budget exhausted = 对端不配合
拆链的签名,非回归。

台架:boot PASS;ftp_abort_probe RST 10/10 + FIN 5/5;relisten_stress
:21 50/50(p50 507ms);n30_repro 30/30。构建 0 错 0 警 Code=212316;
netTask 静态最坏链 1920→1912 B;RAM 台账受管区已 --write 同步(RW +8B)。
文档:E17/E18/E19 三行 + §4.1 常态 0 命中归因 + §4.3 兜底归属改正;
登记簿 N-31 销项(结论沉淀进 §4.3 与 E19)。
2026-10-14 15:57:10 +08:00

308 lines
13 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 -*-
"""生成 docs/images/Net层阻塞模型.svg —— pending 请求模型架构图(纯标准库)。
结构:左右两栏 + 主循环横带 + recv 五路 / send 四路判定带
左 = 调用方任务(net_recv → 数据已在? → 登记挂起 / 睡任务通知;net_send 同构)
右 = netTask 立即 SPI 读出(快速路径)+ CH395F 芯片
中 = netTask 主循环(~1ms 节拍:net_poll / net_pending_tick / net_send_pending_tick / ...)
下 = recv 五路 + send 四路完成/取消判定 → xTaskNotify 唤醒调用方
目的:把"等数据 / 等发送缓冲"从执行者内的自旋改为执行者外的登记——登记后调用方在自己
上下文睡眠,netTask 零占用地(send 为有界自旋 ≤ NET_SEND_SPIN_MS)继续服务整个队列。
用法(仓库根目录,任意 python3):
python tools/arch_diagram/gen_net_pending_svg.py
输出:
docs/images/Net层阻塞模型.svg
改流程时:编辑本文件顶部的 CALLER_STEPS / LOOP_STEPS / PHASES 数据后重跑。
"""
import html
import math
import os
ROOT = os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
OUT = os.path.join(ROOT, "docs", "images", "Net层阻塞模型.svg")
# ----------------------------- 画布与样式 -----------------------------
W = 1620
H = 1074
PAD = 40
FONT = "Microsoft YaHei, Segoe UI, PingFang SC, sans-serif"
TITLE_FONT = 21
SUB_FONT = 13
PANEL_TITLE_FONT = 15
STEP_TITLE_FONT = 14
STEP_NOTE_FONT = 12
LANE_FONT = 12.5
FOOT_FONT = 12
C_CALLER, BG_CALLER = "#2563eb", "#eff6ff"
C_ACTOR, BG_ACTOR = "#7c3aed", "#f5f3ff"
C_CHIP, BG_CHIP = "#0d9488", "#f0fdfa"
C_TICK, BG_TICK = "#d97706", "#fffbeb"
C_CANCEL = "#dc2626"
C_OK = "#16a34a"
ARROW_W = 2.2
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) >= 0x2E80 else 0.58)
return w
# ----------------------------- 数据(改这里) -----------------------------
# 左栏:调用方流程(自上而下)
CALLER_STEPS = [
("net_recv(fd, buf, len, timeout)", "公开 API 与调用契约不变(签名 / 返回值 / 超时语义)"),
("数据已在?", "readable 标志由 net_poll 每轮刷新"),
("登记 pending 到 s_net_pending[fd]", "占用执行者到此结束(微秒级)· 每 fd 至多一个,不覆盖"),
("调用方在自己上下文睡任务通知", "与 LwIP 消费者线程模型同构 · 取消必通知,永不挂死"),
]
FAST_PATH = ("立即 SPI 读出",
"GET_REMV_LEN / READ → 通知调用方(结果 = 字节数)")
LOOP_STEPS = [
("net_poll()", "刷新 readable", C_CHIP),
("net_pending_tick()", "结束挂起 recv", C_TICK),
("net_send_pending_tick()", "推进挂起 send", C_TICK),
("net_listener_tick()", "重试对账", C_ACTOR),
("排空消息队列", "服务所有消费者", "#64748b"),
]
PHASES = [
("①", "数据到达", "SPI 读出,通知调用方(结果 = 字节数)", C_CHIP),
("②", "到点 deadline", "通知 NET_ERR_TIMEDOUT(对外 -2 映射保持)", C_TICK),
("③", "EOF / DISCONNECT", "按 0(EOF) 完成 + auto-relisten + 通知", C_ACTOR),
("④", "同 fd 后续操作", "先行结算该 fd 挂起,再执行新操作", C_CALLER),
("⑤", "网络重建(芯片复位)", "取消 + 必须通知(否则调用方永久挂死)", C_CANCEL),
]
WATCHDOG = "挂起超时留痕探针(编译期选项,默认关):挂起超阈值仍未结束 → 只 DBG_ERROR 留痕(不强制结束、不伪造返回值)"
FOOT = ("关键性质:挂起期间 netTask 零额外占用(recv 判据复用 net_poll 的 readable,不引入额外 SPI;"
"send 有界自旋 ≤ NET_SEND_SPIN_MS,netTask 最坏停滞由 1s 压到 2ms);公开 API 不变。")
# ----------------------------- 生成 -----------------------------
def build():
out = []
add = out.append
def poly(points, color, dash=False, head=True, width=ARROW_W):
pts = " ".join("%.1f,%.1f" % p for p in points)
d = ' stroke-dasharray="7 5"' if dash else ''
add('<polyline points="%s" fill="none" stroke="%s" stroke-width="%.1f" '
'stroke-linejoin="round"%s/>' % (pts, color, width, d))
if head:
(x1, y1), (x2, y2) = points[-2], points[-1]
ang = math.atan2(y2 - y1, x2 - x1)
hl, hw = 11.0, 5.5
bx, by = x2 - hl * math.cos(ang), y2 - hl * math.sin(ang)
px, py = -math.sin(ang) * hw, math.cos(ang) * hw
add('<path d="M %.1f %.1f L %.1f %.1f L %.1f %.1f z" fill="%s"/>'
% (x2, y2, bx + px, by + py, bx - px, by - py, color))
def line(x1, y1, x2, y2, color, dash=False, head=False, width=ARROW_W):
poly([(x1, y1), (x2, y2)], color, dash=dash, head=head, width=width)
def box(x, y, w, h, color, bg, title, note=None, tfs=STEP_TITLE_FONT):
add('<rect x="%.1f" y="%.1f" width="%.1f" height="%.1f" rx="10" fill="%s" '
'stroke="%s" stroke-opacity="0.55" stroke-width="1.6"/>'
% (x, y, w, h, bg, color))
add('<text x="%.1f" y="%.1f" font-size="%.1f" font-weight="700" fill="#0f172a">%s</text>'
% (x + 16, y + 32, tfs, esc(title)))
if note:
add('<text x="%.1f" y="%.1f" font-size="%.1f" fill="#64748b">%s</text>'
% (x + 16, y + 56, STEP_NOTE_FONT, esc(note)))
# ---- 画布 ----
add('<rect x="0" y="0" width="%d" height="%d" fill="#ffffff"/>' % (W, H))
# ---- 标题 ----
add('<text x="%d" y="44" font-size="%d" font-weight="700" fill="#0f172a">%s</text>'
% (PAD, TITLE_FONT, esc("Net 层阻塞模型 · pending 请求架构")))
add('<text x="%d" y="70" font-size="%d" fill="#64748b">%s</text>'
% (PAD, SUB_FONT,
esc("把「等数据 / 等发送缓冲」从执行者内的自旋,改为执行者外的登记 —— 调用方在自己"
"上下文睡眠,netTask 零占用地服务整个队列")))
# ---- 图例 ----
lx = PAD
for color, label in [(C_CALLER, "调用方任务"),
(C_ACTOR, "netTask(唯一执行者)"),
(C_CHIP, "CH395F 芯片"),
(C_TICK, "主循环节拍 ~1ms")]:
add('<rect x="%.1f" y="100" width="16" height="16" rx="4" fill="#ffffff" '
'stroke="%s" stroke-width="2"/>' % (lx, color))
add('<text x="%.1f" y="112" font-size="%.1f" fill="#475569">%s</text>'
% (lx + 24, LANE_FONT, esc(label)))
lx += 30 + text_w(label, LANE_FONT) + 18
# ---- 布局参数 ----
LANE_Y, LANE_H = 140, 600
LB_X, LB_W = PAD, 440
MB_X, MB_W = LB_X + LB_W + 30, W - PAD - (LB_X + LB_W + 30)
STEP_H = 74
STEP_GAP = 30
sx, sw = LB_X + 24, LB_W - 48
# 左栏背景
add('<rect x="%d" y="%d" width="%d" height="%d" rx="14" fill="#f8fafc" '
'stroke="#64748b" stroke-opacity="0.28"/>' % (LB_X, LANE_Y, LB_W, LANE_H))
add('<text x="%d" y="%d" font-size="%.1f" font-weight="700" fill="#334155">%s</text>'
% (LB_X + 18, LANE_Y + 28, PANEL_TITLE_FONT, esc("调用方任务(消费者)")))
# 左栏四个步骤
step_y = []
y = LANE_Y + 56
for i, (title, note) in enumerate(CALLER_STEPS):
color = C_ACTOR if i == 2 else C_CALLER
bg = BG_ACTOR if i == 2 else BG_CALLER
box(sx, y, sw, STEP_H, color, bg, title, note)
step_y.append(y)
if i < len(CALLER_STEPS) - 1:
line(sx + sw / 2, y + STEP_H, sx + sw / 2, y + STEP_H + STEP_GAP,
color, head=True)
y += STEP_H + STEP_GAP
# 分支标签
add('<text x="%.1f" y="%.1f" font-size="12" font-weight="700" fill="%s">%s</text>'
% (sx + sw + 12, step_y[1] + 30, C_OK, esc("是")))
add('<text x="%.1f" y="%.1f" font-size="12" font-weight="700" fill="%s">%s</text>'
% (sx + 14, step_y[1] + STEP_H + 22, C_CANCEL, esc("否,timeout > 0")))
# ---- 快速路径(中栏右上,与"数据已在?"对齐) ----
fp_x = MB_X + 30
fp_w = MB_W - 60
fp_y = step_y[1]
box(fp_x, fp_y, fp_w, STEP_H, C_CHIP, BG_CHIP, FAST_PATH[0], FAST_PATH[1])
line(sx + sw, fp_y + STEP_H / 2, fp_x - 2, fp_y + STEP_H / 2, C_OK, head=True)
# ---- 登记 → 主循环(箭头说明) ----
reg_y = step_y[2]
lw_x, lw_w = MB_X + 30, MB_W - 60
lw_h = 100
lw_y = reg_y - 12
line(sx + sw, reg_y + STEP_H / 2, lw_x - 2, reg_y + STEP_H / 2, C_ACTOR, head=True)
add('<text x="%.1f" y="%.1f" font-size="11.5" fill="#64748b" text-anchor="middle">%s</text>'
% ((sx + sw + lw_x) / 2, reg_y + 14, esc("登记即返回")))
add('<text x="%.1f" y="%.1f" font-size="11.5" fill="#64748b" text-anchor="middle">%s</text>'
% ((sx + sw + lw_x) / 2, reg_y + 28, esc("(微秒级)")))
# ---- 主循环横带 ----
add('<rect x="%d" y="%d" width="%d" height="%d" rx="12" fill="%s" '
'stroke="%s" stroke-opacity="0.5" stroke-width="1.6"/>'
% (lw_x, lw_y, lw_w, lw_h, BG_TICK, C_TICK))
add('<text x="%d" y="%d" font-size="%.1f" font-weight="700" fill="#b45309">%s</text>'
% (lw_x + 16, lw_y + 26, STEP_TITLE_FONT,
esc("netTask 主循环(~1ms 节拍)—— 挂起期间照常服务整个队列")))
n = len(LOOP_STEPS)
gap = 12
cw = (lw_w - 32 - gap * (n - 1)) / n
for i, (name, note, color) in enumerate(LOOP_STEPS):
cx = lw_x + 16 + i * (cw + gap)
cy = lw_y + 42
add('<rect x="%.1f" y="%.1f" width="%.1f" height="46" rx="8" fill="#ffffff" '
'stroke="%s" stroke-opacity="0.6"/>' % (cx, cy, cw, color))
add('<text x="%.1f" y="%.1f" font-size="11.5" font-weight="700" fill="%s">%s</text>'
% (cx + 10, cy + 20, color, esc(name)))
add('<text x="%.1f" y="%.1f" font-size="11" fill="#64748b">%s</text>'
% (cx + 10, cy + 38, esc(note)))
# ---- recv 五路 / send 四路判定带 ----
ph_y = LANE_Y + LANE_H + 26
ph_h = 264
add('<rect x="%d" y="%d" width="%d" height="%d" rx="14" fill="#f8fafc" '
'stroke="#64748b" stroke-opacity="0.28"/>'
% (PAD, ph_y, W - 2 * PAD, ph_h))
add('<text x="%d" y="%d" font-size="%.1f" font-weight="700" fill="#334155">%s</text>'
% (PAD + 18, ph_y + 28, PANEL_TITLE_FONT,
esc("挂起 recv:五路完成 / 取消判定(net_pending_tick 每轮对每个挂起 fd 按序)")))
# 主循环 → 五路
line(lw_x + lw_w / 2, lw_y + lw_h, lw_x + lw_w / 2, ph_y - 2, C_TICK, head=True)
rows_y = ph_y + 50
rh = 27
for i, (no, cond, act, color) in enumerate(PHASES):
ry = rows_y + i * rh
add('<text x="%d" y="%.1f" font-size="14" font-weight="700" fill="%s">%s</text>'
% (PAD + 26, ry + 16, color, esc(no)))
add('<text x="%d" y="%.1f" font-size="12.5" font-weight="700" fill="#0f172a">%s</text>'
% (PAD + 56, ry + 16, esc(cond)))
add('<text x="%d" y="%.1f" font-size="12.5" fill="#475569">%s</text>'
% (PAD + 230, ry + 16, esc("→ " + act)))
wd_y = rows_y + len(PHASES) * rh + 10
add('<text x="%d" y="%d" font-size="12" font-weight="700" fill="%s">%s</text>'
% (PAD + 56, wd_y + 8, C_CANCEL, esc(WATCHDOG)))
# send 四路(net_send_pending_tick,二期)
sg_y = wd_y + 30
add('<text x="%d" y="%d" font-size="12" font-weight="700" fill="%s">%s</text>'
% (PAD + 26, sg_y + 8, C_TICK,
esc("挂起 send 四路:①断连(以已发字节数结束)②推进(每轮至多一块 ≤ NET_SEND_CHUNK_MAX)"
"③写完(以总长度结束)④无进展超时(短写)—— net_send_pending_tick 每轮判定")))
# xTaskNotify 回环 → 调用方睡眠盒
nb_w, nb_h = 250, 62
nb_x = W - PAD - 30 - nb_w
nb_y = ph_y + 46
add('<rect x="%d" y="%d" width="%d" height="%d" rx="10" fill="%s" '
'stroke="%s" stroke-opacity="0.55" stroke-width="1.6"/>'
% (nb_x, nb_y, nb_w, nb_h, BG_CALLER, C_CALLER))
add('<text x="%d" y="%d" font-size="13.5" font-weight="700" fill="%s">%s</text>'
% (nb_x + 14, nb_y + 26, C_CALLER, esc("xTaskNotify")))
add('<text x="%d" y="%d" font-size="11.5" fill="#64748b">%s</text>'
% (nb_x + 14, nb_y + 46, esc("唤醒调用方(结果 = 字节数 / 负错误码)")))
# 五路 → notify(左向短箭头)
line(W - PAD - 30 - nb_w - 40, nb_y + nb_h / 2, nb_x - 2, nb_y + nb_h / 2,
C_CALLER, head=True)
# notify → 调用方睡眠盒(虚线长回环:沿判定带下缘走左外侧)
slp_y = step_y[3]
back_x = PAD + 10
bus_y = ph_y + ph_h - 12
poly([(nb_x + nb_w / 2, nb_y + nb_h),
(nb_x + nb_w / 2, bus_y),
(back_x, bus_y),
(back_x, slp_y + STEP_H / 2),
(sx - 8, slp_y + STEP_H / 2)],
C_CALLER, dash=True, head=True)
# ---- 页脚 ----
add('<text x="%d" y="%d" font-size="%d" fill="#64748b">%s</text>'
% (PAD, H - 24, FOOT_FONT, esc(FOOT)))
svg = ['<svg xmlns="http://www.w3.org/2000/svg" width="%d" height="%d" '
'viewBox="0 0 %d %d" font-family="%s">' % (W, H, W, H, FONT)]
svg.extend(out)
svg.append('</svg>')
return "\n".join(svg) + "\n"
def main():
svg = build()
os.makedirs(os.path.dirname(OUT), exist_ok=True)
with open(OUT, "w", encoding="utf-8", newline="\n") as f:
f.write(svg)
print("已生成: %s (%d bytes)" % (OUT, len(svg.encode("utf-8"))))
if __name__ == "__main__":
main()