Files
Just-DataInspector/Documents/Code_Review_Report.md
T
DeepSeek V4.1 Flash 0c26da07d2 refactor(panels): 面板静态特征收敛为唯一登记表 + 标记面板绘制分段(§17.6 第 1/2 项)
- 新增 pvdetail::PanelKindTraits + PanelTraitsOf(唯一 switch):标题 / 排版类别 /
  滚动字段 / 面板状态 / 功能开关同表登记;新增面板只改「枚举 + 一行」,
  -Wswitch 仍在唯一登记点生效。PanelTitle 改为「前缀 + rangeSuffix 标志」,
  不再依赖 switch(标题降级为数据,不再需要函数指针)。
- PaintPanelMarkerGroups(约 340 行)拆成 6 个具名函数,主函数只保留绘制顺序
  (普通分组栏 -> 条目 -> 表头 -> 吸顶栏 -> 滚动条 -> 透明度行);纯搬移、0 字节变化。
- 顺手修正该函数体遗留的多余缩进。

验证:release/debug/tools 零 warning;UnitTests 260/19;OffscreenChecks -all 7/133;
panels -mode compare 7 面板像素哈希逐项不变;rowgeom compare 965+7 / 1268+7 键 0 差异。
产物 781,824 -> 782,336 B(+512,全部来自登记表)。
2026-11-04 17:22:06 +08:00

50 KiB
Raw Blame History

Code Review 报告(2026-11-04,分支 Code_Review)

计划:Documents/Code_Review_Plan.md | 逐节点记录:Documents/Code_Review_Progress.md

起点 764e5be(dev,已删除过时的 UI_Refactor_* 三份文档),终点 f139440。


0. 一句话结论

体积从 890,880 B 降到 794,112 B(−96,768 B,−10.9%),同时 ① 修掉一个"Debug 构建从来不是 Debug"的真实缺陷; ② 把"新增实验性功能面板必须手改 20+ 处"的屎山点收敛成唯一派发点 + 编译期强制检查; ③ 把 2,165 行的 ApplicationCommands.cpp 拆成 4 个职责分片; ④ 校正了项目记忆里已经失真的目录结构与编译设置。

性能方面:本轮没有发现新的、可复现的卡顿热点(上一轮公式映射的 4 处卡死已根治); 审到 3 个"可以再快一点"的点,但都因为缺少可自证的验证手段而只记录、不动手(见 §6)。


1. 体积:890,880 → 794,112 B

1.1 节表前后对照

节 改前 改后 差
.text(代码) 753,136 703,616 −49,520
.rdata(只读数据) 36,936 26,456 −10,480
.pdata(SEH 展开) 30,420 12,684 −17,736
.xdata(SEH 展开) 29,496 10,156 −19,340
.data 1,680 2,704 +1,024
.idata(导入表) 12,856 12,692 −164
.rsrc(图标/清单/版本) 20,656 20,656 0
文件总计 890,880 794,112 −96,768(−10.9%)

1.2 逐项来源

(1)dynamic_cast 是一个 +75 KB 的"体积炸弹"——提交 5de3763

Toolbar::BuildBoxes 里唯一的一处:

const UiButtonWidget* raw = dynamic_cast<const UiButtonWidget*>(button.get());

而 Button::widget 本来就是 UiButtonWidget* —— 这个转换毫无作用。

用最小复现程序量化(g++ -O2 -fno-exceptions -static -s):

最小程序 体积
纯虚函数 + 虚析构(无 dynamic_cast) 39,936 B
多一个 dynamic_cast 115,200 B
差 +75,264 B

原因:dynamic_cast 需要 RTTI 元数据,并牵出 libstdc++ 的 __dynamic_cast → __cxa_bad_cast → verbose terminate handler → cp-demangle (Release map 里 cp-demangle.o 占 .text 49,600 + .rdata 6,720)。

改为直接遍历 m_buttons(与 m_root->Children() 同源同序),并开启 -fno-rtti。

(2)只关 RTTI 不够:demangler 仍被 vterminate.o 拉入——同一提交

关掉 RTTI 后 cp-demangle.o 依然在链接里(实测只省 2 KB)。查 map 找到真凶:

libstdc++.a(eh_terminate.o) (__gnu_cxx::__verbose_terminate_handler())
libstdc++.a(vterminate.o) (__cxa_demangle)
libstdc++.a(cp-demangle.o) ...

std::terminate()(eh_terminate.o)引用 __gnu_cxx::__verbose_terminate_handler(), 该符号由归档成员 vterminate.o 提供,而它调用 __cxa_demangle。

修法:在 Utilities/Src/MemProbe.cpp 里自行定义该函数 (std::terminate 的未定义引用被我们满足 ⇒ vterminate.o 不再被拉入 ⇒ demangler 消失)。

⚠️ 必须定义在全局 __gnu_cxx 命名空间,不能嵌在 jdi 里。 ⚠️ 行为等效:应用启动后本来就会 std::set_terminate 换成自己的处理函数, 这里的默认实现只在"启动早期崩溃"时生效,同样给出内存不足提示后退出。

(3)Release 关闭异步展开表——提交 94fc1c4

RELEASE_ONLY_OPT = -fno-asynchronous-unwind-tables,只加在 Release 的两条编译规则上。

-fno-exceptions 下绝大多数函数不再需要 .pdata/.xdata:−36,352 B。

代价(已评估并接受):Release 崩溃时 WER 拿不到可靠调用栈。 本程序不打印栈回溯,功能不受影响;Debug 构建保留完整展开表。 (-fno-unwind-tables 全关会链接失败,历史上踩过,不采用。)

(4).rsrc 20,656 B 无优化空间

图标 17,404 B(已是 PNG 承载的多尺寸帧)+ manifest 1,955 B。 再压只能减图标帧或降尺寸,会影响观感,不动。

1.3 未采纳的体积方案(实测否决)

方案 实测 结论
-fno-asynchronous-unwind-tables(Debug 也开) — 否决:Debug 需要展开表才能调试
-fno-unwind-tables(全关) 链接失败 否决(历史已踩)
替换 wcstod 以去掉 strtodg/gdtoa/gethex/hexnan ≈20 KB 暂缓:公式/输入框的数值语义必须与现状逐位一致,收益不足以承担风险
图标继续瘦身 — 否决:观感优先

2. 构建系统:修掉一个"Debug 从来不是 Debug"的缺陷——提交 94fc1c4

审查 Makefile 时发现:

DEBUG_OPT = -O0          # 定义了
...
# Debug 目标文件编译规则
$(CXX_COMPLIER) -c $(CXXFLAGS) $(RELEASE_OPT) ...   # ← 用的是 -Oz!

DEBUG_OPT 从未被任何规则引用,且全流程没有 -g。 所谓 "Debug 构建" 实际上只是"不 strip 的 Release",根本没有调试信息、也不是 -O0。

现在 Debug 规则改用 $(DEBUG_OPT) -g,链接也加 -g。

构建 改前 改后 说明
Release 890,880 794,112 体积优化
Debug 1,801,617 5,994,805 变大是正确的:真实 -O0 -g

⚠️ 这意味着"Debug 构建更慢、更大"从此名副其实;如果以后仍在用 Build/Debug 做 日常运行,请改用 Release。


3. 架构:面板分派收敛为唯一派发点——提交 7a3fefb

3.1 问题(屎山核心)

Lane 里 7 种实验性功能(边沿计数 / 中心轴 / 均值滤波 / 弱峰调整 / 自由标记 / 重采样 / 选区截取)各持有一份 LanePanelState 与一个滚动偏移,共 14 个字段。 旧实现在每一处"按种类取用"的地方各写一遍嵌套三元式或 switch:

位置 旧写法
EdgePanelLayoutFor 的 enabled / open / collapsed / storedX / storedY 5 条 7 层嵌套三元式
标题栏拖拽 7 分支 switch(漏 MARK/RESAMPLE 各一次 ⇒ 面板拖不动)
✕ 关闭 7 分支 switch + default(漏 RESAMPLE ⇒ 关掉的是别的面板)
滚轮翻页 三元式链 + switch(漏 CLIP ⇒ 永远滚不到底)
滚动条拖动 switch + default
折叠状态读写 2 个 switch
首现错开序数 局部 kOrder[] + switch

新增一种面板要改 20+ 处,漏一处就是运行时 BUG —— 历史上 MARK、RESAMPLE、 CLIP 三种面板各自都真实踩过。

3.2 修法

PlotViewInternal.hpp 新增唯一派发点(各带可写/只读重载):

LanePanelEnabled(lane, kind);      // 功能是否启用
LanePanelStateOf(lane, kind);      // 展开/折起/位置
LanePanelScrollOf(lane, kind);     // 内容滚动偏移

调用点全部改为访问器:布局里 5 条嵌套三元式 → 3 行;标题栏拖拽 7 分支 → 1 次写入; 滚轮/滚动条/✕/折叠全部改为穷举。

3.3 ⭐ 关键收益:编译期保护

这些 switch 穷举 7 种且一律不写 default: ⇒ -Wswitch(包含在 -Wall 里) 在新增 EdgePanelKind 而漏登记时直接编译报警。

已用最小程序验证该机制确实生效:

warning: enumeration value 'C' not handled in switch [-Wswitch]

⇒ 今后新增实验性功能面板,忘记登记会编译失败,而不是运行时静默出错。

(对比:旧代码用 case COUNT: default: 兜底,-Wswitch 永远不会报。)

3.4 验证

release + debug 零 warning;产物体积不变(793,088 B) —— 这正好说明本次是 纯粹的等价结构收敛,没有引入额外代码。


4. 架构:拆分 2,165 行的 ApplicationCommands.cpp——提交 8437100

原文件混杂六类职责。按职责纯搬移(零逻辑改动、函数体一字未改):

文件 行数 职责
ApplicationTabs.cpp 355 标签页新建/关闭/切换/重排、视图同步、窗口标题、标签栏回调、标签页右键菜单
ApplicationDerive.cpp 786 派生烘焙、重采样、选区截取(新建 / 就地)、克隆(含带调整)
ApplicationMenus.cpp 409 绘图区右键菜单、菜单数值回写、主题切换、设置菜单、菜单结果派发
ApplicationCommands.cpp 642 分组/信号增删、移动与重排、文件对话框、工具栏动作、键盘鼠标分发、布局边界绘制

Makefile 按目录通配收集源文件,无需登记。

体积 793,088 → 794,112 B(+1,024,LTO 内联边界变化,< 0.13%)。


5. 审查中"没有发现问题"的项(同样重要)

这些是主动查了但结论是干净的,避免后续接手者重复怀疑:

检查项 方法 结论
死代码(未使用的静态函数) -Wall 零警告(GCC 会对未使用的 static 函数报警) 无
死字段(只声明不使用) 对 PlotView / SignalPanel / Application / TabBar / Toolbar / DataSet 的全部 m_* 成员做全仓引用计数 233+ 个成员无一只声明不使用
TODO / FIXME / 临时方案 全仓 grep 无(源码里的"未完成"全是"边沿统计进行中"的业务语义)
布局文本测量是否重复开销 读 UiMeasureTextWidth 实现 已记忆化(键 = 字体 + 文本),不是热点
页面上是否残留指向已删文件的注释 grep UI_Refactor_* / refactor_report / structcheck / .preflight 仅 1 处(PlotView.hpp 的 FormulaEnvelopeEntry 注释),已删

6. 性能审查结论

6.1 结论

没有发现新的、可复现的卡顿热点。 上一轮(第 32 轮)的四处卡死同源于 "公式信号幅值范围/包络各扫一遍整条信号",已通过 DataSet::FormulaTransformFor (一次扫描 + 缓存,命中 0.0015 ms)根治。

逐一复核了下列曾出过问题的路径,均仍走快路径:

路径 现状
BuildLaneVisuals 的幅值范围 指纹缓存(m_laneRangeKey),命中时 O(1)
场景/装饰/面板/图层像素缓存 键齐全,未发现遗漏项
边沿统计 按帧预算 + 每状态配额推进,暂停条件齐备
图层预建 EnsureLaneLayersPrefetched(不再整批重建)
信号面板行集合自愈 RowsSignature 每帧 O(组数),组数远小于信号数

6.2 记录但未实施的三处"还能再快一点"

位置 量级 为什么不改
EdgeScanFor 的累积状态查找是线性扫描 m_edgeScans(上限 2×信号数+余量)。288 信号时每帧约 16 万次指针比较 ~0.3 ms/帧 改哈希索引需要重做"淘汰最旧已完成状态"的逻辑;收益与风险不匹配,且该路径已被预算化
EdgePanelLayoutFor(957 行分支)整体改为"每面板一行元数据表" 可读性 + 少量代码体积 没有离屏像素对比工具(.preflight 已删,崩溃会弹模态框),纯逻辑重写无法自证"像素完全一致"
SceneContentHash 里 6 段逐面板混入循环可合并 可读性 合并会改变哈希值;虽然"多加字段安全、少加字段危险",但在无法逐像素验证的前提下不值得冒险

6.3 关于"性能验证手段"的重要说明

本轮没有做任何 GUI 探针,原因(也是给后续接手的约束):

  • 用户要求不占用其鼠标/键盘/屏幕;
  • .preflight 测试框架已被用户删除,且已明确"不要重建";
  • -fno-exceptions + MemProbe::OnTerminate() 会在探针崩溃时弹模态"内存不足"框, 反复运行就是反复弹窗(第 26 轮的真实教训)。

因此本轮的体积结论全部来自可复现的静态测量(objdump -h 节表 + Makefile 变量 + 非 LTO 目标文件尺寸),架构结论来自编译期检查(-Wswitch)与等价性论证 (体积逐字节不变)。


7. 注释与文档

7.1 删除

  • Documents/UI_Refactor_Plan.md、UI_Refactor_Progress.md、UI_Refactor_Report.md (描述 2026-10-28 那一阶段,已被后续多轮实现推翻,与当前代码不一致)。

7.2 校正(agent_mem.md)

位置 原内容(错) 现内容
§2 Release 用 -Os -Oz + LTO + -s
§2 未提体积开关 补充 NO_EXCEPTIONS / NO_RTTI / LTO / RELEASE_ONLY_OPT,并警告"改 Makefile 必须删 .o 全量重编"(本轮实测:只改 Makefile 不会触发重编,会误以为"优化无效")
§2 链接库少 msimg32 已补
§3 目录里写着早已不存在的 Utilities/CsvTable/、Utilities/DataModel/、Utilities/PaintUtil/、Utilities/PopupMenu/… 按现状重写(Components/Shared 承载通用控件与 PopupMenu/ColorPickerPopup;Utilities 为数据与格式层),并写明依赖方向
末尾 — 新增「第 33 轮」完整记录

7.3 源码注释

  • PlotView.hpp:删除指向已删除代码的 FormulaEnvelopeEntry 注释块。
  • 新增/改写的注释一律说明为什么(尤其是 -fno-rtti、__verbose_terminate_handler 覆盖、-fno-asynchronous-unwind-tables 的代价),避免后续被"好心"改回去。

8. 验证清单

项 结果
make release(删 *.o/*.d 全量) 零 error、零 warning
make debug(真实 -O0 -g,全量) 零 error、零 warning
Release 产物 794,112 B(< 1 MB 红线)
cp-demangle.o 是否仍占 .text 已无 .text 段分配
__gnu_cxx::__verbose_terminate_handler 定义在本产物内(0x140057ed1),非库导入
导入 DLL 列表 comdlg32 / GDI32 / gdiplus / KERNEL32 / MSIMG32 / SHELL32 / USER32 + UCRT,无新增依赖
工作区状态 干净;4 个节点提交 + 1 个文档提交 + 1 个 dev 提交

9. 提交清单

提交 分支 内容
764e5be dev 删除过时的 UI_Refactor_* 三份文档
86ff029 Code_Review 新增 Code_Review_Plan.md / Code_Review_Progress.md
5de3763 Code_Review 移除多余 dynamic_cast + 关闭 RTTI + 覆盖 terminate handler(−61,440 B)
94fc1c4 Code_Review Release 关闭异步展开表(−36,352 B)+ 修正 Debug 构建参数
7a3fefb Code_Review 面板分派收敛为唯一派发点(编译期保护)
8437100 Code_Review 拆分 ApplicationCommands.cpp → 4 个职责分片
f139440 Code_Review 清理过时注释 + 校正 agent_mem.md §2/§3

10. 后续建议(按性价比排序)

  1. 重建离屏验证手段(不弹窗口、不占用用户输入)。 没有它,"重构面板几何 / 缓存键"这类工作无法自证正确性 —— 这正是本轮 §6.2 三项被搁置的唯一原因。建议做纯控制台 + SW_HIDE + 逐像素哈希比对,并且 绝不调用 Application::Initialize()(会 LoadCommandLineFiles → 失败 → 模态框)。
  2. EdgeScanFor 状态查找哈希化(0.3 ms/帧 @288 信号)。
  3. EdgePanelLayoutFor 表驱动化(需第 1 项就绪)。
  4. 若要继续压体积:wcstod 自写替身(≈20 KB,需逐位一致性验证)、 图标帧裁剪(观感优先,不建议)。

11. 第 34 轮(2026-11-04):§10 建议的落地结果

计划见 Code_Review_Plan.md §六,逐节点记录见 Code_Review_Progress.md 第 34 轮。

11.1 一句话结论

§10 里唯一"必须做"的第一条(重建离屏验证手段)已完成并入库(Tools/), 它让其余三条建议第一次变得可测量、可自证;而用这套手段一测, 第 2、4 条建议的实际收益远小于报告当初的估计,第 3 条根本不是性能项 —— 三条都以"实测数据 + 回退"收尾,而不是靠猜测去改 957 行几何或 80 处字符串转换。

§10 建议 结果 关键数字
1. 重建离屏验证手段 ✅ 完成并入库 单元测试 219 条断言 / 18 用例;离屏 71 条断言 / 5 个检查;全程 SW_HIDE + 合成消息
2. EdgeScanFor 状态查找哈希化 ⛔ 实测后回退 查询 0.097 → 0.073 us/次(198 状态,整轮省 ≈4.8 μs);体积 +5,632 B ⇒ 不划算
3. EdgePanelLayoutFor 表驱动化 ⏸ 不重写(交付验证手段) 实测 1.9~2.5 μs/次,每帧约 30 μs ⇒ 不是热点;剩余重复是几何算式本身
4. wcstod 自写替身(≈20 KB) ⛔ 实测 0 字节 只切 stdio 实现体积不变;真收益需替换 80 处 std::to_*string(含浮点)

11.2 验证基础设施(Tools/,已入库)

Tools/UnitTests/       纯数据层单元测试(不创建窗口)
Tools/OffscreenChecks/ 隐藏窗口离屏验证(抓帧 / 哈希 / 面板基线 / 参考统计 / 性能)
Tools/Makefile         统一构建入口:make / make <Tool> / make clean / make help
Tools/README.md        目录结构、作用、测试项、验收标准、A/B 用法、扩展步骤、已知限制
  • 工具链接本体 Build/Release/*.o(排除 Main.o),因此必须先构建本体; 产物在 Tools/<Tool>/Build/,不入库;截图 / 基线 / 报告一律 Temp/tools/(已忽略)。
  • smoke 检查里有一条硬判据:同一状态连续两帧逐像素相同(实测差异 0 像素), 这是所有像素 A/B 可信的前提。
  • edgestats 用独立的朴素逐点实现当金标准,比对真实数据的上升/下降沿计数 (20/19、20/19、20/19 三个阈值全等)——它同时是 EdgeScanFor 任何重构的回归网。

11.3 对原报告两处量级的修正

位置 原表述 实测(本轮)
§6.2 第 1 行 EdgeScanFor 线性扫描「~0.3 ms/帧 @288 信号」 ≈20 μs/帧 @198 状态(差一个数量级;线性扫描的常数很小)
§1.3 第 3 行 替换 wcstod「≈20 KB」 0 B(只要还用 std::to_*string,MinGW printf 引擎 + gdtoa 必然被链入)

这两条修正同样是"结论":它们避免后续接手者按错误量级去做无收益的重写。

11.4 体积现状

项 数值
第 33 轮末 794,112 B(本机实测 788,480 B,工具链差异)
第 34 轮末 788,480 B(T4 / T6 均回退,产品代码零改动)
红线 1 MB 预警(Makefile 注释),距红线 ≈260 KB

11.5 后续建议(替代本报告 §10)

  1. 保持 Tools/ 常绿:任何改动按 Tools/README.md §5 的"最小集"跑针对性检查; 改数据层必跑 UnitTests,改绘图/面板必跑 -run smoke -run panels(必要时 -mode compare)。
  2. 性能类改动一律先量后改:用 -run perf 取同一会话的前后数字; 绝对耗时受机器状态影响,比较时用同一次运行内的相对值。
  3. 如果将来真要压体积(优先级从高到低):
    • 替换 std::to_wstring / std::to_string(≈80 处)→ 可整条去掉 gdtoa(≈33 KB), 验收标准已就绪(stdio_mingw_vs_ucrt_formats);注意其中若干处实参是浮点。
    • 图标帧继续瘦身(观感优先,不建议)。
  4. 如果将来要重写 EdgePanelLayoutFor:先 -run panels -mode record, 重写后 -mode compare;只要有一处像素/几何差异就说明改坏了。
  5. 不要重复这两次实验:EdgeScanFor 哈希化(+5.6 KB / −5 μs 不划算)、 仅切 __USE_MINGW_ANSI_STDIO(0 B)。数字见 §11.1 与 Code_Review_Progress.md 第 34 轮。

12. 第 35 轮(2026-11-04):浮动面板架构评估与收敛 + 整数格式化

计划见 Code_Review_Plan.md §七,逐节点记录见 Code_Review_Progress.md 第 35 轮。

12.1 一句话结论

「每个面板骨架一致、但每次改布局就出错」的机制不是"缺一个公共基类", 而是同一套"按种类分支"被抄了 6 份(分类布尔量 ×3、滚动几何 ×3、滚动条绘制 ×3、 滚动设置 ×7、按种类转发 ×2、标题 ×2)。本轮把这些抄本收敛成"唯一来源 + 编译期穷举检查", 并补了两条离屏护栏(panelscroll、平行数组长度一致), 用 786,944 B(−1,024)的产物体积换来"新增面板只改 3 个地方"。

12.2 评估:这些面板/菜单算不算"公共组件"?

对象 现状 结论
右键菜单 / 二级菜单 Components/Shared/PopupMenu + Layout/Paint 分片 已经是公共组件(5 类菜单共用一份实现)
拾色器浮层 Components/Shared/ColorPickerPopup 已经是公共组件
实验性功能浮动面板 全部在 PlotView 内,骨架一份、按种类分支多份 骨架公共,分支需收敛(本轮已做)

为什么不把面板做成 UiWidget 树节点(像 PopupMenu 那样):

  1. 面板像素要进 m_panelDc 图层缓存(PanelGlobalKey ⊕ PanelSlotKey), 与 UiWidget 的 dirty/capture 体系叠加会互相失效(SignalPanel 行位图缓存 与控件树就是刻意分开的);
  2. 面板几何必须由 EdgePanelLayoutFor 唯一给出(绘制 / 命中 / 光标 / 调试盒 / 缓存键五处共用),控件树会拆掉这条"几何同源";
  3. 面板要往 m_panelDc BitBlt 抓像素、AddOccluder 挖洞、按 m_panelOrder 逐层设文本层号 —— 全是 PlotView 内部协议。

⇒ 结论:公共化方向是"派发点收敛",不是"搬进控件树"。本轮照此执行。

12.3 做了什么(提交 abcf3e6、34365b3、本提交)

项 内容 效果
面板特征层 pvdetail::PanelStyle + PanelStyleOf(kind)、PanelScrollMetricsFor(...)、PlotView::PanelTitle(kind);宽度 / 布局 / 绘制三处调用点全部改读 分类 3 份 → 1 份;滚动几何 3 份 → 1 份
潜伏错误修复 EdgePanelWidth 的标题链漏了 MARK/CLIP(按"边沿计数"量宽) 标题与宽度从此同源
滚动收敛 SetLanePanelScroll 唯一实现 + 7 个一行包装;滚轮/拖滚动条的 2 条 switch 链改直接调用 7 份 → 1 份,读写同源
滚动条绘制 paintPanelScrollBar(layout),3 处复用 3 份 → 1 份
整数格式化 Utf8Util::FormatInteger* 替代 76 处 std::to_wstring + 2 处 std::to_string −1,024 B
审查清理 删 FreeMarkerValue 未使用形参与 (void)lane;;clipReady 3 处判定收敛为 1 处 减少分歧点

12.4 ⚠️ 一条建议的最终结论:gdtoa(≈33 KB)去不掉

  • 只替换整数转换:−1,024 B。
  • 叠加 -D__USE_MINGW_ANSI_STDIO=0(swprintf 让给 UCRT):0 字节。
  • 根因定位到行:libstdc++ 的 .../include/c++/x86_64-w64-mingw32/bits/os_defines.h:48-51 无条件 #undef __USE_MINGW_ANSI_STDIO + #define … 1("libstdc++ depends on POSIX-definitions of those functions")。g++ -E -dM 实测最终值恒为 1 ⇒ 任何 C++ 翻译单元的 swprintf / strtod 都是 MinGW 版本,命令行无法覆盖。
  • ⇒ 要省这 33 KB 只能自己重写「浮点 → 十进制(正确舍入)」与浮点解析的特殊分支 (等价于重写 gdtoa)。判定:不做,已在 Makefile 留下结论与证据,避免重复尝试。

12.5 新增的护栏(本轮最重要的产出之一)

  1. panelscroll 检查:7 种面板逐一"滚到底 / 滚回顶",验证夹取、滑块端点、内容区不动、 不可滚时无滚动条。用 MF4-Record.MF4(566 信号并入同一网格)实测 6/7 可滚 (自由标记无固定点,不可滚是正确行为)。
  2. 平行数组长度不变式(panels + panelscroll 各一份):EdgePanelLayout 里 13 个按行平行的 vector 必须与行数一致;4 个「重采样」专属数组允许为空(非空则必须对齐)。 这条检查成本 O(1),却直接守住"某列读到别人矩形"这一整类错位缺陷。
  3. 像素基线在真实事故中证明了价值:本轮把 hasButton 映射到 buildButton 后, 「选区截取」底部按钮行高归零 ⇒ 面板高度 124 → 87 px,panels -mode compare 当场报 MISMATCH(另外 4 个面板的哈希也变了 —— 因为面板互相重叠,短掉的那块露出下层)。 如果没有这条基线,这个回归只会在用户那里以"截取面板底部怪怪的"形式出现。

12.6 体积与验证

项 结果
Release 产物 786,944 B(第 34 轮末 788,480 → −1,536;其中 −1,024 来自整数格式化,−512 来自面板收敛)
make release / make -C Tools 零 error / 零 warning(全量重建)
UnitTests checks=260 failed=0 cases=19
OffscreenChecks -all 6 个检查全 PASS,checks=108 failed=0
panels -mode compare 7 个面板像素哈希 + 几何逐项不变

12.7 后续建议(按性价比)

  1. EdgePanelLayout 的平行数组合并为 std::vector<PanelRow>(314 处引用)。 这是"改布局就出错"的最后一块机制性隐患:每个分支必须按同一顺序 push 恰好 13 项。 本轮先补了长度一致检查作为护栏;真正的合并仍需一次专门的重构窗口。
  2. PaintEdgeCursor 拆分(约 1600 行):把三段内容绘制(表格 / 列表 / 自由标记) 机械提取为 3 个成员函数,判据 = panels -mode compare 全绿。
  3. EdgeScanFor 状态查找:维持线性扫描(第 34 轮实测 198 状态 0.097 µs/次, 哈希化需 +5,632 B,不划算)。

13. 第 36 轮(2026-11-04):§12.7 两项建议的落地结果

计划 Code_Review_Plan.md §八,逐节点 Code_Review_Progress.md 第 36 轮。

13.1 一句话结论

§12.7 的两项都做完了,并且都用像素基线证明是纯结构改动: PaintEdgeCursor 从约 1900 行降到约 900 行(内容绘制拆到 PlotViewPanelPaint.cpp 1016 行), 平行数组的"漏推一列"隐患用 AppendRow() 唯一入口消除。 产物 786,432 → 785,408 B(−1,024),7 个面板的像素哈希与几何逐项不变。

§12.7 建议 结果
1. EdgePanelLayout 平行数组隐患 ✅ 用 AppendRow() 消除真实隐患;完整结构合并按其风险(悬垂视图)暂不做,理由见 §13.3
2. 拆分 PaintEdgeCursor ✅ 完成,新建 PlotViewPanelPaint.cpp

13.2 拆分后的文件职责

文件 行数 职责
PlotViewPaint.cpp 5575 面板骨架(边框 / 标题栏 / 折起三角 / GX 徽标 / ✕)+ 底部按钮 + 其余全部绘制
PlotViewPanelPaint.cpp 1016 面板内容:PaintPanelCountTableRows / PaintPanelListRows / PaintPanelMarkerGroups

⚠️ 骨架与内容的划分依据:对所有面板都是同一份的留在骨架; 按排版类别不同的进内容函数。新增一种面板 = PanelStyleOf 加一支 + 在对应内容函数里加一支(不会碰到骨架)。

13.3 为什么没做"完整合并成 std::vector<PanelRow>"

技术上可行:PanelColumn<T> 持有 std::vector<PanelRow>* + 成员指针, 读点只需 layout.rowXxx → layout.rowXxx()(纯正则,314 处可信)。

但:EdgePanelLayout 必须自定义拷贝构造 / 拷贝赋值来重绑视图, 而它是 EdgePanelLayoutFor() 的返回值、又被存进 std::vector<EdgePanelLayout> —— 拷贝在热路径上。视图一旦在拷贝后忘记重绑,行为是悬垂指针: 可能读出垃圾矩形、也可能"看起来正常"直到某次重排才崩。

对比两个方案的失败可见性:

  • 现在的平行数组 + AppendRow():漏一列 ⇒ 长度不变式当场报错;
  • 列视图:忘了重绑 ⇒ 没有检查能抓到。

因此本轮只做"消除真实隐患"(AppendRow),完整合并推迟到 "逐行几何基线"(把每行 18 个矩形都写进基线文件、逐项比对)就绪之后。

13.4 本轮"机械搬移"踩到的三个真问题(值得记住)

  1. 局部 lambda 无法跨函数复用:paintCounts 只服务列表段 ⇒ 下沉为该函数的局部 lambda; paintPanelScrollBar 服务三段 ⇒ 提升为 pvdetail::PaintPanelScrollBar 内联函数 (参数显式化,不再隐式捕获 m_theme)。
  2. 提取块依赖外层局部变量:「标记透明度」滑块用 panel / pad ⇒ 改为从 layout.panel 派生。 这类隐式依赖正是"整个函数都看得见"的害处,提取时必须逐个查。
  3. const 正确性:tabLanes 在 marker 段里是被写的(回写各列实测宽度)⇒ 参数不能是 const std::vector<Lane>&。提取时"看起来很只读"的参数要核对写入点。

13.5 验证与现状

项 结果
make release(全量) 零 error / 零 warning;785,408 B(距 1 MB 预警线 ≈260 KB)
UnitTests checks=260 failed=0 cases=19
OffscreenChecks -all 6/6 PASS,checks=101 failed=0
panels -mode compare 7 个面板像素哈希 + 几何逐项不变
panelscroll(566 行) 18 个行数组全部等于 566

13.6 后续建议(更新)

  1. 逐行几何基线:把每行 18 个矩形写进 Temp/tools/rows-baseline.txt,逐项比对。 这是"完整结构合并"与"重写布局算式"的前置护栏,成本低(导出 + 字符串比对)。
  2. rowPoints / rowIterations 已无绘制方使用(旧式输入框排版已删除): 可在 AppendRow 里保留但标注"仅为兼容保留",或在下一轮与文档一起清掉。
  3. PaintPanelListRows 仍约 470 行:可再按"条目栏 / 参数行 / 重采样行"拆三段, 但当前已不在"改一次就出错"的路径上,优先级低于第 1 项。

十四、第 37 轮:逐行几何基线(§13.6 第 1 项)

14.1 结论

§13.6 建议 结果
1. 逐行几何基线 ✅ 新增 OffscreenChecks -run rowgeom(record / compare),并做了负向自检证明它能抓回归
2. rowPoints / rowIterations 清理 ⏸ 未做(本轮与该建议无耦合,留给下一轮)
3. PaintPanelListRows 再拆 ⏸ 未做(纯可读性,优先级低)

-all 从 6 个检查 / 101 条断言 → 7 个检查 / 127 条断言,全绿。

14.2 基线的两层设计(为什么不是"一行一个文件")

层 场景 记录 键数 用途
细节 6 信号 1 网格、7 面板全开、参数行展开、3 个固定点、折起/展开各一份 每个矩形的每个字段 983(MF4 上 1304) 定位:直接看出哪一行哪一列偏了几像素
规模 全部信号 1 网格(MF4 = 566 行) 每面板一个 FNV-1a 摘要 + 行数 7 覆盖:任何一行变动都改哈希,且不产生几十万行基线

两层互补:哈希层保证"改动一定被看见",明细层保证"看见了能定位"。

14.3 这次护栏暴露的两个结构事实

  1. rowRefs 是重载的字段。 「列表式」面板里 rowRefs[i] = 第 i 行的信号; 「自由标记」面板里 rowRefs[i] = 第 i 个信号分组,而它真正的条目行在 markerRows, 且只填 rowNameRects / rowNameCloses,另外 14 列必须为空。 ⇒ 第 36 轮定的"18 列全等"不变式对 MARK 是错的,本轮改为按 PanelStyle 分流判定。 同一个"字段重载"也让导出器必须逐列按自身长度遍历(否则越界崩溃 —— 这正是本轮第一次 0xC0000005 的来源)。
  2. SetMode() 不同步 m_lanes。ExecuteToolbarAction(MERGE_LANES/SPLIT_LANES) 之后 TabConfig 的网格配置已是新的,但 PlotView::m_lanes 要等下一次布局/绘制才刷新 —— 任何"合并后立刻读 LaneCount()"的代码都必须先 SynchronizeView(true)。

14.4 一个被误判成缺陷的既有行为

场景 B 一开始 mark 面板是空行:Session::ShowSignals() 先 SetAllVisible(false), 而"全部不可见 ⇒ 丢弃网格"是 RebuildLanes 的既有设计(与工具栏「清空」一致), 网格级状态(固定点 / 面板开关 / 滚动位置)随之清空。⇒ 不是新缺陷; 检查改为在该场景重新播固定点,顺带把规模覆盖补全(rows=2 markerRows=3)。

14.5 下一步(按价值排序)

  1. rowPoints / rowIterations 清理(确认已无绘制方使用,仅 BuildBoxes / 命中键残留): 这是当前唯一"写了没人读"的列,删掉能减少每次 AppendRow 的 2 次 push 与 2 个 vector。 判据:rowgeom -mode compare(细节基线会逐项证明几何没变)+ panels -mode compare。
  2. 完整结构合并(std::vector<PanelRow> + 列视图):前置护栏(本轮)已就绪, 但 §13.3 的失败可见性问题仍未解决 —— 列视图拷贝后忘记重绑是静默悬垂。 若要做,必须同时给 EdgePanelLayout 加"视图自检"(例如保存 rows 容量并与视图长度比对), 否则不建议动。
  3. PaintPanelListRows 再拆三段(纯可读性)。

十五、第 38 轮:§14.5 第 1 / 第 3 项

15.1 结论

§14.5 建议 结果
1. rowPoints / rowIterations 清理 ✅ 删除(EdgePanelLayout 16 列),产物 −512 B,基线证明等价
2. 完整结构合并(vector<PanelRow> + 列视图) ⏸ 未做,闸门仍关闭(理由见 §15.3)
3. PaintPanelListRows 再拆 ◑ 重采样行已提取(132 行),参数行未提取(理由见 §15.4)

15.2 "删掉一列"的等价性怎么证明

rowgeom 让"删除死列"从"我看过没人用"变成可验证:

old lines = 983, new lines = 919
only in OLD = 64, only in NEW = 0
only-in-OLD lines NOT belonging to rowPoints/rowIterations = 0
  • 唯一差异就是被删两列的 64 条键;其余 919 条逐字节相同;
  • 因为 rowgeom 导出的是每个矩形的每个字段,"这两列恒为空"这一前提被直接证伪/证实, 不依赖人工 grep 的完整性。

这给出一条可复用的方法:先有全字段基线,再删字段;否则"删掉的东西没人用"永远只是断言。

15.3 为什么 §14.5 第 2 项仍然不做

报告本人的判据是"若要做,必须同时给 EdgePanelLayout 加视图自检,否则不建议动"。 本轮把这一步的门槛量化了:

方案 改动面 失败可见性
A:std::vector<PanelRow> + 列视图(PanelColumn<T>) 242 处读点(其中 127 处在 PlotViewInteraction.cpp 的命中判定里)+ 自定义拷贝/赋值 + 视图自检 拷贝后忘重绑 = 静默悬垂,只能靠新增自检兜住
B:std::vector<PanelRow> + 具名成员访问(无视图) 同样是 242 处,但所有访问变成 rows[i].check,结构上不可能悬垂 编译期全查;无新风险

结论:真要做就做 B(无视图),A 的"零改动读点"是幻觉 —— 省下的正则改写换来一个 必须靠新自检才能发现的隐患。B 的代价是 242 处机械替换(含 30 余处 range-for 与 .size()), 适合单独一轮、以 rowgeom + panels 双基线为判据,而不是塞在本轮末尾。

另外现在"漏推一列"已被 AppendRow + 长度不变式双重挡住, B 的剩余收益主要是"单一分配 + 少 15 个 vector 头"(EdgePanelLayout 每实例省约 360 B 头部,566 个网格时约 200 KB,但那是布局对象、每帧临时构造,实际峰值影响很小)。 因此 B 的优先级应低于"面板结构再简化"的实际痛点,属于整洁性收益而非正确性收益。

15.4 §14.5 第 3 项的取舍

段落 状态
重采样行(132 行:两行只读统计 + 两行联动输入框) ✅ 提取为 PaintPanelResampleRows,零逻辑改动
通用参数行(中心轴统计 + 两行滑块) ⏸ 提取需 9 个参数(layout/rowIndex/laneIndex/ref/kind/resolved/item/peakItem/tabLane),签名比函数体更难读
条目栏 保留(三种排版的公共前缀)

15.5 验证

项 结果
make release 零 warning;784,896 B(−512)
UnitTests 260 / 19 PASS
OffscreenChecks -all 7 / 127 PASS
panels -mode compare 7 面板像素哈希逐项不变
rowgeom -mode compare 919 + 7 键,0 差异

15.6 下一步(更新)

  1. 方案 B(vector<PanelRow> 无视图)单独一轮:判据 = 编译通过 + rowgeom/panels 双基线归零; 预期收益是"字段重载(rowRefs 双语义)"这类问题从结构上消失 —— 这比省字节更重要。
  2. 若要在 B 之上再做语义化:把 rowRefs 的双语义拆成 rows[].ref + markerGroups[], 让"MARK 面板 12 列为空"的注释消失。
  3. 面板几何算式重写(§13.6 遗留):基线已就绪,但需先完成 1 或 2。

十六、第 39 轮:平行数组合并为 std::vector<PanelRow>(§15.6 第 1 项,方案 B)

16.1 结论

§15.6 建议 结果
1. 方案 B(vector<PanelRow> 无视图) ✅ 完成:16 个平行数组 → std::vector<PanelRow> rows(17 个具名成员)
2. 在 B 之上拆分 rowRefs 双语义 ✅ 顺带完成:rows[i].ref 取代重载的 rowRefs,MARK 分组行改走 AppendMarkerGroupRow
3. 面板几何算式重写 ⏸ 未做(本轮只做结构合并,算式未动)

16.2 结构对比

旧(16 个平行数组) 新(std::vector<PanelRow>)
新增一列 改 AppendRow + 14 处布局分支 + 绘制 + 缓存键 + 调试框 + 检查的 15 元组 只在 PanelRow 加一个成员
漏推一列 该列之后所有行读到别人的矩形(静默) 不可能(一行一次成型)
rowRefs 语义 列表式 = 行信号;MARK = 分组信号(同一字段两种含义) rows[i].ref 一种含义;MARK 的分组行只填 nameRect/nameClose
长度不变式 15 个 size() 组成的判据,且 MARK 要单独分流 恒真 ⇒ 改为语义断言(每行都要有名称区;MARK 行不得带参数行字段)
EdgePanelLayout 每实例 16 个 vector 头(16×24 B)+ 各自独立分配 1 个 vector 头

16.3 为什么这次敢做(前一轮不敢)

上一轮的顾虑是"列视图拷贝后忘重绑 = 静默悬垂"。方案 B 没有视图: 所有访问都是 rows[i].check,PanelRow 是值类型、随 rows 一起拷贝,结构上不可能悬垂。 代价是 243 处读点必须机械改写(不省),收益是"漏推一列"与"字段重载"两类问题从根上消失。

16.4 等价性证明(本轮方法论的核心)

rowgeom 的键名刻意保持 <panel>.<旧列名>[NNN],于是重构前后可以直接逐键比对:

old keys = 919, new keys = 965
仅存在于旧 = 0                     ← 没有任何键消失
已有键的值变化 = 20                ← 全部是 ".count" 键:0 -> N(结构变化)
仅存在于新 = 46                    ← 全部是新增列的 [NNN],值 0.00,0.00,0.00,0.00

再叠加 7 个面板的像素哈希逐项不变 ⇒ 渲染与几何都没有任何位移。 这比"编译通过 + 肉眼看一眼"强得多:它把"等价重构"变成了可证伪的断言。

16.5 过程中唯一的一次事故

清理恒真判空时用"整段替换"把「均值滤波」双滑块段落误替换成「弱峰」双滑块, 编译器立刻报 brace 不平衡;用备份回滚后重做。 结论:整段替换必须先精确读出上下文,oldString 不能凭记忆拼。

16.6 验证

项 结果
make release 零 warning;784,896 → 782,336 B(−2,560)
make debug / make -C Tools 零 warning
UnitTests 260 / 19 PASS
OffscreenChecks -all 7 / 133 PASS(上轮 127)
panels -mode compare 7 面板像素哈希逐项不变
rowgeom -mode compare 965 + 7 键 / 1268 + 7 键(MF4),0 差异

16.7 下一步(按价值排序)

  1. 面板几何算式重写(§13.6 遗留):结构合并与逐行基线都已就绪,现在"改算式"是安全的; 建议先做 EdgePanelWidth(逐信号量宽)与 EdgePanelLayoutFor 的行高累加, 判据 = rowgeom -mode compare + panels -mode compare 双基线归零。
  2. PaintPanelListRows 的通用参数行提取(上轮因需 9 个参数而搁置): 现在 row 是一个整体、extendRow 已成组,可把"参数行"收成一个函数, 参数表从 9 个降到 (g, layout, row, kind, state) 5 个。
  3. 若还要压体积:EdgePanelLayout 已无冗余列,剩余空间主要在字符串/浮点格式化(见 §11 的 gdtoa 结论)。

十七、第 40 轮:几何算式重写 + 参数行提取(§16.7 第 1 / 第 2 项)

17.1 结论

§16.7 建议 结果
1. 面板几何算式重写(EdgePanelWidth + 行高累加) ✅ 两处都收敛成单一来源(PanelColumnMetrics / PanelRowPlan),双基线零差异
2. PaintPanelListRows 通用参数行提取 ✅ PaintPanelParamRows(5 参数),函数由约 300 行降到约 120 行
3. 体积 ⏸ 按用户指示暂不投入(当前没有便宜的压缩空间,等后续功能更新产生冗余时再做)

产物顺带 −512 B(782,336 → 781,824 B),零 warning。

17.2 "重构"与"重写"的区别(本轮的核心方法论)

前几轮做的是重构(结构变、行为不变),判据是"双基线零差异"。 本轮做的是算式重写(把两份同源算式合成一份),风险更高:两份算式本来可能已经不一致, 合并后必然有一边要改数 —— 那就不是"行为不变"了。

所以先做了一次等价性判别:

量 宽度算式里的算法 列几何里的算法 是否等价
valueColumn max(mono"100000", mono"-400%") max(mono"100000", mono"1000") ✅ 都等于 mono"100000"
labelColumn max(均值标签, 弱峰标签) max(均值标签, 弱峰标签) ✅ 同式
resampleLabel 单独算(不并入上面) 单独算 ✅ 同式,且不能并入
nameColumn 逐信号取 max(跳过无 axis 的) 同上(另一处循环) ✅ 同式

⇒ 只有"可以证明等价"的量才合并。resampleLabel 明确不并入 sliderLabel: 「重采样」标签("最大数据间隔")比均值/弱峰的长,并进去会让后两个面板整体变宽 —— 那是像素变化, 不属于等价重构。

17.3 双遍历合一:PanelRowPlan

改前 EdgePanelLayoutFor 里对同一批信号遍历两次(先算总高、再出行几何), 两处的判定条件(可见 / 勾选 / 收起 / 统计行 / 参数行数)必须逐字一致。 这种"两趟必须同步修改"的结构就是"改一次布局就出错"的另一半机制(前半是平行数组,已在 §16 解决)。

改后一个 PanelRowPlan{ref, checked, collapsed, hasStats, sliderRows} 数组同时喂给 高度算式与几何生成,判定只写一次。

17.4 过程中的两次锚点误伤(值得记)

  1. 锚点 // 两行滑块 在 EdgePanelWidthFor 内也匹配到 ⇒ 误删宽度函数尾部;
  2. 锚点 float rowY = layout.content.Y - scrollMetrics.scroll; 在表格分支先出现 ⇒ 误删「边沿计数」的行发射。

两次都是编译器立刻报错(brace 不平衡 / 未声明标识符),从备份回滚后重做。 规则:整段替换的锚点必须唯一;同名语句出现多次时先定位所属区块(例如"第二个 else if (listStyle)")再取偏移。

17.5 验证

项 结果
make release / make debug / make -C Tools 零 warning
产物 781,824 B(−512)
UnitTests 260 / 19 PASS
OffscreenChecks -all 7 检查 / 133 断言 PASS
panels -mode compare 7 面板像素哈希逐项不变
rowgeom -mode compare 965+7 键 / 1268+7 键(MF4),0 差异

17.6 下一步(按价值排序)

  1. 面板"三处穷举 switch"再收敛:现在新增面板仍要改 PanelStyleOf / PanelTitle / LanePanelScrollOf 三处;可以考虑把它们合成一张 constexpr 表 ({kind, title, style, scrollPtr}),新增面板只加一行数据 —— 但 PanelTitle 需要 EdgeRangeSuffix()(运行时字符串),需先解决"标题是函数"的问题。
  2. PaintPanelMarkerGroups(约 260 行)按"分组栏 / 表头 / 条目吸顶"再拆, 与 §17.3 同理:先给"吸顶推挤"单独一个可测函数。
  3. 体积:等有新的冗余再说(用户已明确)。

十八、第 41 轮(2026-11-04,收尾):面板登记表 + 标记面板分段,以及"能否结束审查"的判定

18.1 结论

§17.6 建议 结果
1. 三处穷举 switch 合成 constexpr 表 ✅ 完成,而且实际收敛的是五处(PanelStyleOf / PanelTitle / LanePanelScrollOf / LanePanelStateOf / LanePanelEnabled)
2. PaintPanelMarkerGroups 再拆 ✅ 拆成 6 个具名函数,主函数只剩"绘制顺序"
3. 体积 ⏸ 按用户指示不做(+512 B 是本轮唯一代价,来自登记表)

18.2 本轮做了什么

(1)面板静态特征收敛为唯一登记点

inline constexpr PanelKindTraits PanelTraitsOf(EdgePanelKind kind);  // 唯一 switch

PanelKindTraits{style, title, rangeSuffix, scroll, panel, enabled} —— 新增一种面板只改两处:EdgePanelKind 加枚举值 + 表里加一行。 五个消费者的签名一字未改,调用点一处未动。

PanelTitle 从"返回 std::wstring 的 switch"变成"读表 + 可选追加 EdgeRangeSuffix()"。 这解决了 §17.6 留下的那个具体障碍:"标题是函数、依赖运行时字符串,所以没法进表"—— 答案是把标题降级为数据(前缀 + 标志),而不是把函数塞进表里。

(2)标记面板绘制分段

函数 职责
MeasureMarkerColumnWidths 六列实测宽度 → Lane::freeMarkerColumnWidths
PaintMarkerGroupBar 分组栏(三角 / 名称 / N 标记点 / ✕)
PaintMarkerColumnSeparators 列分隔竖线(条目与表头共用)
PaintMarkerRow 固定点条目(六列 + 分隔线 + ✕)
PaintMarkerTableHeader 展开分组的表头
PaintMarkerOpacityRow 「标记透明度」滑块行
PaintPanelMarkerGroups 只负责顺序:分组栏 → 条目 → 表头 → 吸顶栏 → 滚动条 → 透明度行

判据不是"看起来更清楚",而是 panels 的 7 个面板像素哈希 + rowgeom 的 965/1268 条几何键逐项不变;产物字节数在 P40 前后完全相同(782,336 B), 说明这一段是真正的零成本搬移。

18.3 现在可以结束这次代码审查了吗?——可以

这不是"我觉得差不多了",而是逐条对账的结果:

审查项 状态 证据
体积(890,880 → 782,336 B,−12.2%) 完成 objdump -h 节表;-fno-rtti + terminate handler 覆盖 + 关异步展开表;红线 1 MB,余量 ≈260 KB
Debug 构建"名不副实" 已修 DEBUG_OPT -g 真正生效(Debug 6.16 MB 属预期)
面板分派"改一次就出错" 已消除 唯一登记表 + -Wswitch 编译期保护
平行数组错位 / 字段重载 已消除 std::vector<PanelRow>(结构上不可能错位)
几何算式两处/双遍历 已收敛 PanelColumnMetrics + PanelRowPlan
巨型函数(1900 / 340 行) 已拆 PlotViewPanelPaint.cpp 三个内容函数 + 本轮六个标记面板函数
过时文档/注释 已清理 UI_Refactor_* 已删;agent_mem.md §2/§3 已校正
死代码 / 死字段 / TODO 无 -Wall 零警告 + 全成员引用计数 + 全仓 grep
性能热点 无新发现 第 32 轮四处卡死已根治;三处"还能再快一点"均已量化并判定不做

明确"不做"的三项(都有数字支撑,写在这里防止后续重复尝试):

  1. EdgeScanFor 状态查找哈希化:198 状态 0.097 → 0.073 µs/次,但体积 +5,632 B。
  2. EdgePanelLayoutFor 表驱动化:实测 1.9~2.5 µs/次(每帧约 30 µs),不是热点。
  3. gdtoa(≈33 KB):MinGW 下只要还用 std::to_*string(含浮点)就必然链入, 根因在 libstdc++ 的 os_defines.h 无条件 #define __USE_MINGW_ANSI_STDIO 1。

剩下的都属"收益 < 风险/成本":PaintPanelListRows 还可再拆(纯可读性)、 体积只在出现新冗余时再压、面板几何算式已经是单一来源。

18.4 合并前的建议动作(给用户)

  1. 远端机器务必全量重编:本分支动过 Makefile(NO_RTTI / RELEASE_ONLY_OPT / Debug -O0 -g),而只改 Makefile 不会触发重编 ⇒ 先 make clean,再 make release。 否则会误以为"体积优化无效"或"Debug 还是 -Oz"。
  2. 合并前跑一遍最小集(见 Tools/README.md §5): make release → make -C Tools → UnitTests → OffscreenChecks -all → -run panels -mode compare(注意:面板/几何基线是本机生成的,跨机器不可比, 远端首次应 -mode record 重建或直接跳过 compare)。
  3. 分支顺序:Code_Review → dev → main。本分支只含代码 / 文档 / Tools/, 没有构建产物、没有 Temp/、没有测试数据。

18.5 本轮验证

项 结果
make release / make debug / make -C Tools 零 error / 零 warning
Release 产物 782,336 B(+512,全部来自 P39 登记表;P40 为 0)
UnitTests checks=260 failed=0 cases=19
OffscreenChecks -all 7 检查 / 133 断言,全 PASS
panels -mode compare 7 个面板像素哈希逐项不变
rowgeom -mode compare 965 + 7 键 / 1268 + 7 键(MF4),0 差异