#!/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('' % (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('' % (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('' % (x, y, w, h, bg, color)) add('%s' % (x + 16, y + 32, tfs, esc(title))) if note: add('%s' % (x + 16, y + 56, STEP_NOTE_FONT, esc(note))) # ---- 画布 ---- add('' % (W, H)) # ---- 标题 ---- add('%s' % (PAD, TITLE_FONT, esc("Net 层阻塞模型 · pending 请求架构"))) add('%s' % (PAD, SUB_FONT, esc("把「等数据 / 等发送缓冲」从执行者内的自旋,改为执行者外的登记 —— 调用方在自己" "上下文睡眠,netTask 零占用地服务整个队列"))) # ---- 图例 ---- lx = PAD for color, label in [(C_CALLER, "调用方任务"), (C_ACTOR, "netTask(唯一执行者)"), (C_CHIP, "CH395F 芯片"), (C_TICK, "主循环节拍 ~1ms")]: add('' % (lx, color)) add('%s' % (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('' % (LB_X, LANE_Y, LB_W, LANE_H)) add('%s' % (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('%s' % (sx + sw + 12, step_y[1] + 30, C_OK, esc("是"))) add('%s' % (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('%s' % ((sx + sw + lw_x) / 2, reg_y + 14, esc("登记即返回"))) add('%s' % ((sx + sw + lw_x) / 2, reg_y + 28, esc("(微秒级)"))) # ---- 主循环横带 ---- add('' % (lw_x, lw_y, lw_w, lw_h, BG_TICK, C_TICK)) add('%s' % (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('' % (cx, cy, cw, color)) add('%s' % (cx + 10, cy + 20, color, esc(name))) add('%s' % (cx + 10, cy + 38, esc(note))) # ---- recv 五路 / send 四路判定带 ---- ph_y = LANE_Y + LANE_H + 26 ph_h = 264 add('' % (PAD, ph_y, W - 2 * PAD, ph_h)) add('%s' % (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('%s' % (PAD + 26, ry + 16, color, esc(no))) add('%s' % (PAD + 56, ry + 16, esc(cond))) add('%s' % (PAD + 230, ry + 16, esc("→ " + act))) wd_y = rows_y + len(PHASES) * rh + 10 add('%s' % (PAD + 56, wd_y + 8, C_CANCEL, esc(WATCHDOG))) # send 四路(net_send_pending_tick,二期) sg_y = wd_y + 30 add('%s' % (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('' % (nb_x, nb_y, nb_w, nb_h, BG_CALLER, C_CALLER)) add('%s' % (nb_x + 14, nb_y + 26, C_CALLER, esc("xTaskNotify"))) add('%s' % (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('%s' % (PAD, H - 24, FOOT_FONT, esc(FOOT))) svg = ['' % (W, H, W, H, FONT)] svg.extend(out) svg.append('') 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()