Files
Just-DataInspector/Documents/Code_Review_Report.md
T
DeepSeek V4.1 Flash 353c553721 test(tools): rowgeom 逐行几何基线 + 长度不变式按类别判定
新增 `OffscreenChecks -run rowgeom`(§13.6 第 1 项),把 7 个面板的每个矩形的
每个字段导出为文本基线并逐项比对,作为"完整结构合并 / 重写布局算式"的前置护栏。

两层覆盖
- 细节层:6 信号 1 网格、7 面板全开、参数行展开、3 个真实固定点、折起+展开各一份,
  983 条键(-data MF4-Record.MF4 时 1304 条),失败时直接给出字段名+行号+新旧值;
- 规模层:全部信号合并成 1 个网格(MF4 = 566 行),每面板一个 FNV-1a 摘要 + 行数,
  任何一行变动都会改哈希,同时不产生几十万行基线。

负向自检(证明护栏不是空转)
- 篡改基线副本 `mark.markerRowCount=3 → 2` ⇒ `CHANGED … mismatches=1` + FAIL。

顺带修正第 36 轮定得过严的不变式
- `rowRefs` 是重载字段:MARK 面板用它表示"信号分组",真正的条目行在 markerRows,
  且只填 rowNameRects / rowNameCloses,其余 14 列必须为空;
  ⇒ `panels` / `panelscroll` 的"18 列全等"改为按 `PanelStyle::markerGroups` 分流判定,
  失败时打印 15 个计数字段便于定位。
- 导出器/不变式一律**逐列按自身长度**遍历(按 rowRefs.size() 索引其它列会越界,
  这是本轮第一次 0xC0000005 的根因)。

测试用例里的两处修正
- `MERGE_LANES` 之后补 `SynchronizeView(true)`:`SetMode` 只重建 TabConfig 的网格配置,
  `PlotView::m_lanes` 要等下一次布局才刷新;
- 场景 B 重新播固定点:`ShowSignals()` 先 `SetAllVisible(false)`,而"全部不可见 ⇒ 丢弃网格"
  是 RebuildLanes 的既有设计,网格级状态随之清空(不是缺陷),因此不能跨场景依赖。

验证
- make release:零 warning,785,408 B 不变(本轮只动工具/检查与文档)
- make -C Tools clean && make -C Tools:零 warning
- UnitTests:checks=260 failed=0 cases=19
- OffscreenChecks -all:7 检查 / 127 断言全 PASS(原 6 / 101)
- rowgeom -mode compare:默认数据 983+7 键、MF4 1304+7 键,0 差异
- panels -mode compare:7 个面板像素哈希 + 几何逐项不变

文档:Code_Review_Plan §九、Progress 第 37 轮、Report §14、agent_mem 第 37 轮、Tools/README §4.1/4.3/4.4
2026-11-04 15:49:25 +08:00

613 lines
33 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` 再拆三段(纯可读性)。