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

923 lines
50 KiB
Markdown
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.
# 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` 里唯一的一处:
```cpp
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 时发现:
```make
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` 新增唯一派发点(各带可写/只读重载):
```cpp
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/`,已入库)
```text
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` 让"删除死列"从"我看过没人用"变成**可验证**:
```text
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)面板静态特征收敛为唯一登记点**
```cpp
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 差异** |