对应 Code_Review_Report §15.6 第 1 项(方案 B:有结构,无列视图)。
结构
- EdgePanelLayout 的 16 个平行 std::vector 合并为 std::vector<PanelRow> rows,
PanelRow 带 17 个具名成员:ref / header / nameRect / nameClose / triangle / check /
stats / levelTrack / levelValue / levelReset / iterTrack / iterValue / iterReset /
infoMaxInterval / infoMinInterval / intervalField / frequencyField。
- 唯一新增入口:AppendRow(ref, header, nameRect, triangle, check)(返回新行引用)与
AppendMarkerGroupRow(ref, nameRect, nameClose)(「自由标记」分组行)。
参数行/重采样行一律 layout.rows.back().xxx = … 赋值。
- 原 int rows(可见信号数)与新容器同名 ⇒ 改名 signalCount,并注明它与 rows.size()
在 MARK 面板里不相等(只有有固定点的信号才有行)。
消掉的两类问题
1. 某分支漏 push 一列 ⇒ 该列之后所有行读到别人的矩形(静默错位)。现在一行一次成型。
2. rowRefs 字段重载(列表式 = 行信号 / MARK = 分组信号,其余 12 列必须为空)。
现在每行自带 ref,语义由“填哪些字段”表达;不变式简化为语义断言
(每行必须有名称区;MARK 行不得带 header/check/stats/滑块/重采样字段)。
顺带清理
- 合并后 rowIndex < layout.rowXxx.size() 全部变成恒真,批量删除 50+ 处
(含 15 元组长度数组、? x : gd::RectF() 三元判空、if(...){extendRow(...)} 包裹)。
- 调试框与缓存键改为逐行遍历(新增字段只需在 PanelRow 与这两处循环各加一行)。
等价性证明(rowgeom 键名刻意保持不变,故可逐键比对)
old keys = 919, new keys = 965
仅存在于旧 = 0;已有键值变化 = 20(全部是 ".count":0 -> N)
仅存在于新 = 46(全部是新增列的 [NNN],值 0.00,0.00,0.00,0.00)
⇒ 逐行逐字段的真实几何零位移;7 个面板像素哈希(含 resample=2BFC74B3D9B3239C)逐项不变。
验证
- make release:零 warning;784,896 → 782,336 B(−2,560);make debug / make -C Tools 同样零 warning
- UnitTests:checks=260 failed=0 cases=19
- OffscreenChecks -all:7 检查 / 133 断言(上轮 127)全 PASS
- panels -mode compare:7 面板像素哈希逐项不变
- rowgeom -mode compare:965+7 键 0 差异;-data MF4-Record.MF4 时 1268+7 键 0 差异
- 读点:旧 243 处 layout.rowXxx[…] → 现 95 处 layout.rows[…](84 命中判定 / 11 绘制)
过程记录:清理恒真判空时曾用整段替换把「均值滤波」双滑块段误换成「弱峰」双滑块,
编译器报 brace 不平衡,用备份回滚后重做 —— 整段替换必须先精确读出上下文。
文档:Plan §十一、Progress 第 39 轮、Report §16、agent_mem 第 39 轮、Tools/README §4.1/§4.4
756 lines
41 KiB
Markdown
756 lines
41 KiB
Markdown
# 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 结论)。
|
||
|
||
|