Files
STM32F4-Base/tools/arch_diagram/gen_memory_svg.py
T
super_agent_bot 3a2f9b21fb
CI / 静态门禁 (pre-commit) (push) Failing after 43s
CI / 主机侧单测 (MSVC) (push) Failing after 45s
CI / 静态门禁 (pre-commit) (pull_request) Failing after 45s
CI / 主机侧单测 (MSVC) (pull_request) Failing after 45s
refactor(time_sync)!: 退役 TCP :8888 对时任务,timeSyncTask 转纯 SNTP 校时节拍宿主(#78)
SNTP 客户端已随 #73/PR #77 上线合入(merge 2b848c4a),用户裁定
8888 对时任务退出历史舞台(gitea:77 线程原话,跟踪 Issue #78)。

生产侧(App/sync/time_sync.c v2.0,任务入口名不变,freertos.c 零改动):
- 删除 :8888 监听、TIME 包重组、sys_clock 写入、OPER 落痕、
  net_drop_conn 队头自救整条链(TIME_SYNC_PORT 宏随退役删除)
- 任务体收敛为:就绪屏障(100ms 分片、osFlagsNoClear)→
  200ms 校时节拍 task_wd_beat → time_sntp_run(net_get_generation())
  → 分片延时;喂狗节奏(TASK_WD_TIMESYNC=5,required_mask)不变
- 时钟写入口自此 = SNTP / UI 手工 / 104 C_CS,双写竞态(后写者赢)
  随之消失(墙钟说明 §15 退役公告、§16 现势口径)

工具与测试:
- 删除 test/host/time_sync_server.py(对时源)与 e02_probe.py
  (队头阻塞探针,载体随监听退役;判据脚本 v5_log_gap_report.py
  保留供旧日志解读)
- time_sync 主机单测重写 v2.0:33 断言覆盖分片延时 / 屏障 /
  SNTP 节拍与网络代次当轮交接 / 未出网守门;stub 的
  net_socket.h 裁剪至 net_get_generation 作编译期绊线
- 覆盖率基线 --update-baseline 收口(time_sync.c 101→20 行为刻意
  缩减,全仓 48/48 套绿)

文档与图:墙钟说明 §15 改退役公告(教训保留:CH395F FIN 丢未读
缓冲)、§16 单一网络来源;产品需求 FR-TIME-2 退役注记、§6.3 重写;
架构总览 / 软件架构规格 / 网络配置 / 网络自愈 §9.3 / BSD 指南 /
看门狗说明心跳与白名单行 / 实机测试指南 / 部署环境 / 继保规划 /
UI补齐时基口径 / 页面补齐规划 SNTP 页行 / 任务规划 README 叶数与
暂缓项行(SNTP 叶已绑、UI_PAGE_MAP_COUNT=31 口径)等约 30 处现势
口径同步;arch / net_stack 两图生成器与 SVG 同笔重跑。

台账:ram_baseline --update + check_mem_numbers --write + 体积凭证
(Code 254,648→253,428,−1,220 B)。静态链复测发现 timeSyncTask
1,904 B / iec104Task 1,600 B 低于 ISR 硬线:master 2b848c4a 同判据
timeSyncTask 即 2,056 B(本 PR 只减不增,−152 B 旧任务自身帧),
双 FAIL 为 #74/#77 后既有债;抬栈属 Ask-first 未擅自执行,登记簿
新增「静态链硬线双 FAIL」待台架复采,复测快照入 docs/内存管理/data。

验证:keil-build 0 错 0 警;time_sync/sntp 主机单测绿;
ci_local 全绿;doc-links --all 0。
2026-11-04 18:07:03 +08:00

418 lines
18 KiB
Python
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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
"""Generate memory layout SVG diagrams for STM32F4-Base.
Usage:
python tools/arch_diagram/gen_memory_svg.py [--type ram|flash|taskstack|all]
Output (default --all):
docs/images/内存布局_RAM.svg
docs/images/内存布局_FLASH.svg
docs/images/任务栈预算.svg
"""
import argparse
import os
import sys
# ---------------------------------------------------------------------------
# Diagram data
# ---------------------------------------------------------------------------
# ── RAM breakdown (2026-10-22 构建, map Grand Totals; data/ram_report_20261022.md) ──
# Region 1: main SRAM RW_IRAM1 @0x20000000 (128KB = 131,072 B, DMA-reachable)
SRAM1_TOTAL = 131072
SRAM1_USED = 75264
SRAM1_FREE = SRAM1_TOTAL - SRAM1_USED
# Region 2: CCM RW_CCM @0x10000000 —— scatter 给的是 0x0000FF00 = 65,280 B(CPU only,
# DMA 不可达);另有 0x1000FF00 起 256 B 的 UNINIT_CCM 尾区(.noinit 崩溃现场用 48 B)
CCM_TOTAL = 65280
CCM_USED = 57376
CCM_FREE = CCM_TOTAL - CCM_USED
RAM1_CATEGORIES = [
("LVGL 池 16KB", 16876, "#4CAF50"),
("UI", 13546, "#FF9800"),
("lftpd", 8440, "#8BC34A"),
("FTL/BBT", 7036, "#9C27B0"),
("NET/CH395F", 5250, "#F44336"),
("CMSIS-RTOS", 1724, "#00BCD4"),
("HAL", 84, "#607D8B"),
("C stack", 1024, "#795548"),
("other", 21284, "#9E9E9E"),
("free", SRAM1_FREE, "#E0E0E0"),
]
RAM2_CATEGORIES = [
("FreeRTOS 堆", CCM_USED, "#2196F3"),
("free", CCM_FREE, "#E0E0E0"),
]
# ── Flash breakdown (1MB = 1,048,576 B; 2026-10-23 构建) ──────────────────
FLASH_TOTAL = 1048576
FLASH_CODE = 213676
FLASH_RO = 18860
FLASH_RW = 1188
FLASH_USED = FLASH_CODE + FLASH_RO + FLASH_RW # ROM 合计(含 RW 初始化表)
FLASH_IROM = FLASH_CODE + FLASH_RO # ER_IROM1 区用量 = 232,536 B(22.2%)
FLASH_FREE = FLASH_TOTAL - FLASH_USED
FLASH_CATEGORIES = [
("代码段 (Code)", FLASH_CODE, "#2196F3"),
("只读数据 (RO)", FLASH_RO, "#4CAF50"),
("初始化数据 (RW)", FLASH_RW, "#FF9800"),
("Flash 空闲", FLASH_FREE, "#E0E0E0"),
]
# Flash Code+RO 分布(= ER_IROM1 232,536 B;2026-10-23 按 .map 对象表逐对象归类,
# 口径同 data/ram_report_20261023.md;"其他"含链接器填充)
FLASH_CODE_SUB = [
("LVGL 库", 87706, "#4CAF50"),
("应用代码/UI", 57047, "#2196F3"),
("CH395F+FTP", 25916, "#F44336"),
("HAL 驱动", 17876, "#607D8B"),
("dhara / FTL", 11222, "#FF9800"),
("FreeRTOS 内核", 10762, "#00BCD4"),
("FatFS", 8468, "#9C27B0"),
("C 库 (microlib)", 5486, "#795548"),
("startup", 468, "#B0BEC5"),
("其他 (长尾)", 7585, "#E0E0E0"), # remainder(2026-10-23 的零散增量)
]
# ── Task stack budgets (2026-10-22 构建 + 同日真机 [STACK] 实测; data/stack_report_20261022.md) ──
# (name, cfg, prio, worst, peak_used, margin, margin_pct, verdict, desc)
# worst = max(静态最坏深度, 实测峰值); margin = cfg - worst
# 9 行含 testTask(仅测试构建创建、无实测 → 取静态最坏链,峰位列填同值)
TASK_STACKS = [
("defaultTask", 5120, "Normal", 3024, 2152, 2096, 40, "PASS", "FatFS 挂载/探针 + 配置引擎 + syslog + LED"),
("netTask", 3840, "Normal", 1912, 1116, 1928, 50, "PASS", "CH395F 轮询、消息处理"),
("ftpTask", 4096, "Normal", 1456, 1404, 2640, 64, "PASS", "lftpd FTP 服务(大缓冲已静态化)"),
("uiTask", 16384, "BelowNormal", 4052, 4052, 12332, 75, "PASS", "LVGL UI 主循环 + 远程显示推流"),
("testTask", 8192, "BelowNormal", 736, 736, 7456, 91, "PASS", "测试套件(产品构建不创建,取静态最坏链)"),
("timeSyncTask", 2560, "Normal", 1264, 648, 1296, 50, "PASS", "时间同步(8888 退役,SNTP 宿主)"),
("adcTask", 2560, "AboveNormal", 1320, 952, 1240, 48, "PASS", "1 kHz 节拍消费者(语义取数与有效性采集,纯取数)"),
("iec104Task", 2560, "Normal", 1456, 540, 1104, 43, "PASS", "IEC 60870-5-104 上行(默认 2404)"),
("protectTask", 2048, "Normal", 872, 828, 1176, 57, "PASS", "20 ms 保护判据 + 闭锁 + 定值校验和巡检"),
]
KERNEL_STACKS = [("idle", 512), ("timer", 1024)] # 128/256 words(未实测)
HEAP_TOTAL = 57344 # configTOTAL_HEAP_SIZE 56 KB(位于 RW_CCM;段实占 57,376 B 含元数据)
HEAP_FREE = 15800 # 2026-10-22 真机 [STACK](启动 + 网络/FTP/104 就绪后)
HEAP_MIN_EVER = 8464
TOTAL_STACKS = sum(t[1] for t in TASK_STACKS) # 47,360(含 testTask;产品构建 8 任务 = 39,168)
# ---------------------------------------------------------------------------
# SVG helpers
# ---------------------------------------------------------------------------
def _text_width(text, char_w=7.2):
"""Rough monospace text width estimate (px)."""
return len(text) * char_w
def _wrap(text, max_chars=12):
"""Wrap label to multiple <tspan> lines."""
words = text.split("\n") if "\n" in text else text.split()
lines = []
current = ""
for w in words:
test = (current + " " + w).strip() if current else w
if len(test) > max_chars and current:
lines.append(current)
current = w
else:
current = test
if current:
lines.append(current)
return lines
def svg_header(w, h):
return (
f'<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 {w} {h}" '
f'width="{w}" height="{h}" style="font-family:Consolas,\'Courier New\',monospace; '
f'font-size:12px; background:#fff">\n'
'<defs>\n'
'<style>\n'
'.title { font-size:14px; font-weight:bold; }\n'
'.label { font-size:11px; }\n'
'.legend-text { font-size:10px; }\n'
'.bar-seg { stroke:#fff; stroke-width:0.5; }\n'
'</style>\n'
'</defs>\n'
)
def svg_footer():
return '</svg>\n'
def _draw_bar_segment(x, y, w, h, fill, label_inside, label_outside="",
label_font=10, min_inside_w=40):
"""Return SVG fragment for a bar segment with label."""
parts = []
parts.append(f' <rect class="bar-seg" x="{x}" y="{y}" width="{w:.1f}" height="{h}" '
f'fill="{fill}" rx="2"/>\n')
if w > min_inside_w and label_inside:
# Center text inside bar
lines = _wrap(label_inside, max_chars=int(w / 7))
ty = y + h / 2 - (len(lines) - 1) * 6
for line in lines:
lx = x + w / 2
parts.append(f' <text x="{lx:.1f}" y="{ty:.1f}" text-anchor="middle" '
f'dominant-baseline="central" class="label" fill="#000">{line}</text>\n')
ty += 12
elif label_outside:
lx = x + w + 4
ty = y + h / 2
parts.append(f' <text x="{lx:.1f}" y="{ty:.1f}" text-anchor="start" '
f'dominant-baseline="central" class="legend-text" fill="#333">{label_outside}</text>\n')
return "".join(parts)
# ---------------------------------------------------------------------------
# Diagram generators
# ---------------------------------------------------------------------------
def gen_ram_svg():
"""内存布局_RAM.svg — 两条堆叠条:主 SRAM(DMA 可达)+ CCM(仅 CPU)。"""
W, H = 820, 360
BAR_X, BAR_H = 60, 44
SCALE = (W - 80) / SRAM1_TOTAL # 128KB 占满宽;CCM (64KB) 占半宽
svg = svg_header(W, H)
svg += f'<text x="{W/2}" y="24" text-anchor="middle" class="title">SRAM (192KB) 使用布局 — 2026-10-22 构建</text>\n'
# Region 1: 主 SRAM
y1 = 50
svg += f'<text x="{BAR_X}" y="{y1-8}" class="label" fill="#555">主 SRAM RW_IRAM1 @0x20000000(128KB,DMA 可达)</text>\n'
x = BAR_X
for label, size, color in RAM1_CATEGORIES:
w = size * SCALE
if w < 1:
continue
inside = label if w > _text_width(label) + 10 else ""
svg += _draw_bar_segment(x, y1, w, BAR_H, color, inside, "")
x += w
svg += f'<text x="{BAR_X}" y="{y1+BAR_H+18}" class="label" fill="#333">'
svg += f'已用: {SRAM1_USED}B ({SRAM1_USED/SRAM1_TOTAL*100:.1f}%)'
svg += f' 空闲: {SRAM1_FREE}B ({SRAM1_FREE/SRAM1_TOTAL*100:.1f}%)'
svg += f' 总量: 128KB</text>\n'
# Region 2: CCM
y2 = y1 + BAR_H + 50
svg += f'<text x="{BAR_X}" y="{y2-8}" class="label" fill="#555">CCM RW_CCM @0x10000000(段 65,280 B;仅 CPU 可达、DMA 不可达 → R2/R3)</text>\n'
x = BAR_X
for label, size, color in RAM2_CATEGORIES:
w = size * SCALE
if w < 1:
continue
inside = label if w > _text_width(label) + 10 else ""
svg += _draw_bar_segment(x, y2, w, BAR_H, color, inside, "")
x += w
svg += f'<text x="{BAR_X}" y="{y2+BAR_H+18}" class="label" fill="#333">'
svg += f'已用: {CCM_USED}B ({CCM_USED/CCM_TOTAL*100:.1f}%)'
svg += f' 空闲: {CCM_FREE}B ({CCM_FREE/CCM_TOTAL*100:.1f}%)'
svg += f' 总量: 63.75KB(+256 B UNINIT_CCM 尾区)——CCM 是当前全局卡点</text>\n'
# Legend(主 SRAM 两列 + CCM 两行)
ly = y2 + BAR_H + 40
lx = 60
half = (len(RAM1_CATEGORIES) + 1) // 2
for i, (label, size, color) in enumerate(RAM1_CATEGORIES):
pct = size / SRAM1_TOTAL * 100
kb = size / 1024
if i >= half:
lx2 = 420
else:
lx2 = 60
yy = 226 + (i % half) * 16
svg += f' <rect x="{lx2}" y="{yy}" width="12" height="12" fill="{color}" rx="2"/>\n'
svg += f' <text x="{lx2+16}" y="{yy+10}" class="legend-text" fill="#333">'
svg += f'主SRAM {label}: {kb:.1f}KB ({pct:.1f}%)</text>\n'
for i, (label, size, color) in enumerate(RAM2_CATEGORIES):
pct = size / CCM_TOTAL * 100
kb = size / 1024
yy = 322 + i * 16
svg += f' <rect x="{60}" y="{yy}" width="12" height="12" fill="{color}" rx="2"/>\n'
svg += f' <text x="{76}" y="{yy+10}" class="legend-text" fill="#333">'
svg += f'CCM {label}: {kb:.1f}KB ({pct:.1f}%)</text>\n'
svg += svg_footer()
return svg
def gen_flash_svg():
"""内存布局_FLASH.svg — bar + code sub-breakdown."""
W, H = 820, 240
BAR_X, BAR_Y, BAR_H = 60, 50, 40
SCALE = (W - 80) / FLASH_TOTAL
svg = svg_header(W, H)
svg += f'<text x="{W/2}" y="24" text-anchor="middle" class="title">Flash (1MB) 使用布局 — 2026-10-23 构建</text>\n'
x = BAR_X
for label, size, color in FLASH_CATEGORIES:
w = size * SCALE
if w < 1:
continue
inside = f"{label} {size/1024:.1f}KB" if w > 60 else ""
outside = "" if w > 60 else f"{label} {size/1024:.1f}KB"
svg += _draw_bar_segment(x, BAR_Y, w, BAR_H, color, inside, outside)
x += w
# Summary below bar
svg += f'<text x="{BAR_X}" y="{BAR_Y+BAR_H+18}" class="label" fill="#333">'
svg += f'已用: {FLASH_USED/1024:.1f}KB ({FLASH_USED/FLASH_TOTAL*100:.1f}%)'
svg += f' 空闲: {FLASH_FREE/1024:.1f}KB ({FLASH_FREE/FLASH_TOTAL*100:.1f}%)'
svg += f' ER_IROM1(Code+RO): {FLASH_IROM/1024:.1f}KB ({FLASH_IROM/FLASH_TOTAL*100:.1f}%)'
svg += f' 总量: 1MB (= ROM 合计口径)'
svg += f'</text>\n'
# Code sub-breakdown mini bar
Y2 = 130
svg += f'<text x="{BAR_X}" y="{Y2-6}" class="label" fill="#555">Code + RO 分布(ER_IROM1 {FLASH_IROM/1024:.1f}KB,按 .map 对象归类)</text>\n'
SCALE2 = (W - 80) / FLASH_IROM
x2 = BAR_X
for label, size, color in FLASH_CODE_SUB:
w = size * SCALE2
if w < 1:
continue
# 窄段(多数桶 < 40px)不放内嵌文字,统一列到下方图例,避免互相压字
inside = f"{size/1024:.1f}KB" if w > 40 else ""
svg += _draw_bar_segment(x2, Y2, w, 24, color, inside, "")
x2 += w
# 图例:四列三行(10 个桶)
for i, (label, size, color) in enumerate(FLASH_CODE_SUB):
col, row = i % 4, i // 4
lx = 60 + col * 195
ly = 176 + row * 16
svg += f' <rect x="{lx}" y="{ly-9}" width="10" height="10" fill="{color}" rx="2"/>\n'
svg += f' <text x="{lx+14}" y="{ly}" class="legend-text" fill="#333">{label} {size/1024:.1f}KB</text>\n'
svg += svg_footer()
return svg
def gen_taskstack_svg():
"""任务栈预算.svg — 每任务:分配栈条 + 最坏占用叠加条 + 实测余量/判定。"""
W, H = 880, 620 # 9 个任务 + 2 内核栈 + 堆汇总,行高 40 需要 ~600 高
BAR_X = 190
MAX_STACK = 16384
SCALE = (W - BAR_X - 40) / MAX_STACK
Y_START = 64
ROW_PITCH = 40
BAR_H = 16
svg = svg_header(W, H)
svg += f'<text x="{W/2}" y="22" text-anchor="middle" class="title">FreeRTOS 任务栈预算(9 任务;栈全部从 56KB FreeRTOS 堆分配,堆位于 CCM)</text>\n'
svg += f'<text x="{W/2}" y="40" text-anchor="middle" class="legend-text" fill="#777">数据: 2026-10-22 构建 + 同日真机 [STACK] 水位(data/stack_report_20261022.md);最坏 = max(静态最坏链, 实测峰值);判定线 = 余量 ≥ max(1024B ISR, 配置×30%)</text>\n'
svg += f'<text x="{BAR_X}" y="{Y_START-10}" class="legend-text" fill="#555">浅色条 = 分配栈 深色条 = 最坏占用(max(静态最坏, 实测峰值)) 下方文字 = 实测余量/判定</text>\n'
y = Y_START
for name, cfg, prio, worst, peak, margin, pct, verdict, desc in TASK_STACKS:
w = cfg * SCALE
wc = worst * SCALE
# 三档优先级配色:Normal 蓝 / BelowNormal 琥珀 / AboveNormal 绿(adcTask)
dark = {"Normal": "#2196F3", "BelowNormal": "#FF9800",
"AboveNormal": "#4CAF50"}[prio]
light = {"Normal": "#BBDEFB", "BelowNormal": "#FFE0B2",
"AboveNormal": "#C8E6C9"}[prio]
vc = "#2E7D32" if verdict == "PASS" else "#C62828"
# 任务名(左侧)
svg += f' <text x="{BAR_X-4}" y="{y+8}" text-anchor="end" dominant-baseline="central" class="label" fill="#333">{name}</text>\n'
# 分配栈条(浅色)+ 最坏占用叠加条(深色)
svg += f' <rect class="bar-seg" x="{BAR_X}" y="{y}" width="{w:.1f}" height="{BAR_H}" fill="{light}" rx="3"/>\n'
svg += f' <rect x="{BAR_X}" y="{y}" width="{wc:.1f}" height="{BAR_H}" fill="{dark}" rx="3"/>\n'
# 数据行
svg += f' <text x="{BAR_X}" y="{y+BAR_H+11}" dominant-baseline="central" class="legend-text" fill="#333">'
svg += f'配置 {cfg}B | 实测峰值 {peak}B | 最坏 {worst}B | 余量 {margin}B ({pct}%) '
svg += f'<tspan fill="{vc}" font-weight="bold">{verdict}</tspan></text>\n'
y += ROW_PITCH
# 内核任务(静态小栈,未取句柄实测)
y += 4
for name, stack in KERNEL_STACKS:
w = max(stack * SCALE, 2)
svg += f' <text x="{BAR_X-4}" y="{y+6}" text-anchor="end" dominant-baseline="central" class="label" fill="#333">{name}</text>\n'
svg += f' <rect class="bar-seg" x="{BAR_X}" y="{y}" width="{w:.1f}" height="12" fill="#90A4AE" rx="2"/>\n'
svg += f' <text x="{BAR_X + w + 4}" y="{y+6}" dominant-baseline="central" class="legend-text" fill="#333">{stack}B(未实测)</text>\n'
y += 22
# 堆汇总(实测值)
y += 8
svg += f'<text x="{BAR_X}" y="{y}" class="label" fill="#555">'
svg += f'堆总量: {HEAP_TOTAL}B(configTOTAL_HEAP_SIZE=56KB,位于 RW_CCM)'
svg += f' 任务栈合计: {TOTAL_STACKS}B(含 testTask 8192B)</text>\n'
y += 16
svg += f'<text x="{BAR_X}" y="{y}" class="label" fill="#555">'
svg += f'产品构建 8 任务栈 = 39168B + 内核栈 {512+1024}B'
svg += f' 实测: free={HEAP_FREE}B / min_ever={HEAP_MIN_EVER}B(2026-10-22)</text>\n'
y += 16
svg += f'<text x="{BAR_X}" y="{y}" class="label" fill="#555">'
svg += '判定: 9/9 PASS(testTask 无实测,取静态最坏链)</text>\n'
y += 24
# 颜色图例
svg += f' <rect x="{BAR_X}" y="{y}" width="10" height="10" fill="#2196F3" rx="2"/>\n'
svg += f' <text x="{BAR_X+14}" y="{y+8}" class="legend-text" fill="#333">Normal 优先级</text>\n'
svg += f' <rect x="{BAR_X+130}" y="{y}" width="10" height="10" fill="#FF9800" rx="2"/>\n'
svg += f' <text x="{BAR_X+144}" y="{y+8}" class="legend-text" fill="#333">BelowNormal 优先级</text>\n'
svg += f' <rect x="{BAR_X+300}" y="{y}" width="10" height="10" fill="#4CAF50" rx="2"/>\n'
svg += f' <text x="{BAR_X+314}" y="{y+8}" class="legend-text" fill="#333">AboveNormal 优先级(adcTask)</text>\n'
svg += f' <rect x="{BAR_X+520}" y="{y}" width="10" height="10" fill="#90A4AE" rx="2"/>\n'
svg += f' <text x="{BAR_X+534}" y="{y+8}" class="legend-text" fill="#333">内核栈 idle/timer</text>\n'
svg += svg_footer()
return svg
# ---------------------------------------------------------------------------
# Main
# ---------------------------------------------------------------------------
def main():
parser = argparse.ArgumentParser(description="Generate memory layout SVG diagrams")
parser.add_argument("--type", choices=["ram", "flash", "taskstack", "all"], default="all")
args = parser.parse_args()
out_dir = os.path.join(os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))),
"docs", "images")
os.makedirs(out_dir, exist_ok=True)
generators = {
"ram": ("内存布局_RAM.svg", gen_ram_svg),
"flash": ("内存布局_FLASH.svg", gen_flash_svg),
"taskstack": ("任务栈预算.svg", gen_taskstack_svg),
}
if args.type == "all":
targets = generators.values()
else:
targets = [generators[args.type]]
for filename, gen_func in targets:
path = os.path.join(out_dir, filename)
svg = gen_func()
with open(path, "w", encoding="utf-8", newline="\n") as f:
f.write(svg)
print(f"OK: {filename} ({len(svg)} bytes)")
# XML self-check
for filename, _ in targets:
path = os.path.join(out_dir, filename)
with open(path, "r", encoding="utf-8") as f:
content = f.read()
if not content.startswith("<svg "):
print(f"FAIL: {filename} does not start with <svg", file=sys.stderr)
sys.exit(1)
if "</svg>" not in content:
print(f"FAIL: {filename} missing </svg>", file=sys.stderr)
sys.exit(1)
if 'xmlns="http://www.w3.org/2000/svg"' not in content:
print(f"FAIL: {filename} missing xmlns", file=sys.stderr)
sys.exit(1)
print("All SVG self-checks passed")
if __name__ == "__main__":
main()