背景:`AppendRow()`(提交 85aaed5)已经成为新增面板行的唯一入口, 18 个平行数组由它一次补齐。因此旧的宽松判据("13 个必须等长 + 4 个重采样专属列允许为空")不再需要 —— 任何一列短于行数, 就是"有人绕过 AppendRow 手写 push"的直接证据。 改动: * `panels` 检查:改为断言 18 个数组长度**全部**等于 `rowRefs.size()`, 失败时打印全部 18 列的长度便于定位。 * `panelscroll` 检查:同样收紧(大文件 566 行时才会暴露的问题)。 * `Tools/README.md`:同步不变式描述。 文档: * `Documents/Code_Review_Plan.md` 新增 §八(第 36 轮节点与硬约束)。 * `Documents/Code_Review_Progress.md` 新增第 36 轮(两次重构 + 三个搬移陷阱)。 * `Documents/Code_Review_Report.md` 新增 §13(拆分职责表、为何不做完整合并、 失败可见性对比、后续建议)。 * `Documents/agent_mem.md` 新增第 36 轮规则。 验证: * `make release` 全量零 error / 零 warning,产物 **785,408 B**。 * `UnitTests` → checks=260 failed=0;`OffscreenChecks -all` → 6/6 PASS,checks=101 failed=0。 * `panels -mode compare` → 7 个面板像素哈希与几何逐项不变。 * `panelscroll`(MF4 566 行)→ 18 个行数组全部 = 566。
30 KiB
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. 后续建议(按性价比排序)
- 重建离屏验证手段(不弹窗口、不占用用户输入)。
没有它,"重构面板几何 / 缓存键"这类工作无法自证正确性 —— 这正是本轮 §6.2
三项被搁置的唯一原因。建议做纯控制台 +
SW_HIDE+ 逐像素哈希比对,并且 绝不调用Application::Initialize()(会LoadCommandLineFiles→ 失败 → 模态框)。 EdgeScanFor状态查找哈希化(0.3 ms/帧 @288 信号)。EdgePanelLayoutFor表驱动化(需第 1 项就绪)。- 若要继续压体积:
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)
- 保持
Tools/常绿:任何改动按Tools/README.md§5 的"最小集"跑针对性检查; 改数据层必跑UnitTests,改绘图/面板必跑-run smoke -run panels(必要时-mode compare)。 - 性能类改动一律先量后改:用
-run perf取同一会话的前后数字; 绝对耗时受机器状态影响,比较时用同一次运行内的相对值。 - 如果将来真要压体积(优先级从高到低):
- 替换
std::to_wstring/std::to_string(≈80 处)→ 可整条去掉 gdtoa(≈33 KB), 验收标准已就绪(stdio_mingw_vs_ucrt_formats);注意其中若干处实参是浮点。 - 图标帧继续瘦身(观感优先,不建议)。
- 替换
- 如果将来要重写
EdgePanelLayoutFor:先-run panels -mode record, 重写后-mode compare;只要有一处像素/几何差异就说明改坏了。 - 不要重复这两次实验:
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 那样):
- 面板像素要进
m_panelDc图层缓存(PanelGlobalKey ⊕ PanelSlotKey), 与UiWidget的 dirty/capture 体系叠加会互相失效(SignalPanel行位图缓存 与控件树就是刻意分开的); - 面板几何必须由
EdgePanelLayoutFor唯一给出(绘制 / 命中 / 光标 / 调试盒 / 缓存键五处共用),控件树会拆掉这条"几何同源"; - 面板要往
m_panelDcBitBlt抓像素、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 新增的护栏(本轮最重要的产出之一)
panelscroll检查:7 种面板逐一"滚到底 / 滚回顶",验证夹取、滑块端点、内容区不动、 不可滚时无滚动条。用MF4-Record.MF4(566 信号并入同一网格)实测 6/7 可滚 (自由标记无固定点,不可滚是正确行为)。- 平行数组长度不变式(
panels+panelscroll各一份):EdgePanelLayout里 13 个按行平行的vector必须与行数一致;4 个「重采样」专属数组允许为空(非空则必须对齐)。 这条检查成本 O(1),却直接守住"某列读到别人矩形"这一整类错位缺陷。 - 像素基线在真实事故中证明了价值:本轮把
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 后续建议(按性价比)
EdgePanelLayout的平行数组合并为std::vector<PanelRow>(314 处引用)。 这是"改布局就出错"的最后一块机制性隐患:每个分支必须按同一顺序 push 恰好 13 项。 本轮先补了长度一致检查作为护栏;真正的合并仍需一次专门的重构窗口。PaintEdgeCursor拆分(约 1600 行):把三段内容绘制(表格 / 列表 / 自由标记) 机械提取为 3 个成员函数,判据 =panels -mode compare全绿。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 本轮"机械搬移"踩到的三个真问题(值得记住)
- 局部 lambda 无法跨函数复用:
paintCounts只服务列表段 ⇒ 下沉为该函数的局部 lambda;paintPanelScrollBar服务三段 ⇒ 提升为pvdetail::PaintPanelScrollBar内联函数 (参数显式化,不再隐式捕获m_theme)。 - 提取块依赖外层局部变量:「标记透明度」滑块用
panel/pad⇒ 改为从layout.panel派生。 这类隐式依赖正是"整个函数都看得见"的害处,提取时必须逐个查。 - 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 后续建议(更新)
- 逐行几何基线:把每行 18 个矩形写进
Temp/tools/rows-baseline.txt,逐项比对。 这是"完整结构合并"与"重写布局算式"的前置护栏,成本低(导出 + 字符串比对)。 rowPoints/rowIterations已无绘制方使用(旧式输入框排版已删除): 可在AppendRow里保留但标注"仅为兼容保留",或在下一轮与文档一起清掉。PaintPanelListRows仍约 470 行:可再按"条目栏 / 参数行 / 重采样行"拆三段, 但当前已不在"改一次就出错"的路径上,优先级低于第 1 项。