Files
Just-DataInspector/Documents/Code_Review_Report.md
T
DeepSeek V4.1 Flash 5c48bf922e review(follow-ups): 落地上轮建议并给出实测结论(#2/#4 回退、#3 不重写)
一、Tools 完善
* OffscreenChecks `edgestats`:改为自动挑"最有起伏"的信号。旧实现固定取
  `signals[0]`,而 MF4 的第一条信号是常量(6000)⇒ 三个阈值的断言退化成
  "0 == 0" 的空转。现在扫描前 200 条挑 `max-min` 最大者,SCATTER_TEST 上
  三个阈值分别得到 (20,19)/(20,19)/(20,19),与独立参考实现逐位相同。
* UnitTests 新增 `stdio_mingw_vs_ucrt_formats`:把 MinGW ANSI stdio 与系统
  UCRT 的 printf 在**本程序实际用到的全部格式**(`%.*f` / `%.6g` / `%g` /
  `%.4e` / `%.2f %ls` / `%llu` / `%lld` / `%02X` / `%.2f%%`)上做逐字符比对,
  13.6 万次调用 **0 处差异**(覆盖 1e-40~1e40)。它是"把 swprintf 让给 UCRT"
  这条体积优化的验收标准。
* Tools/README.md:同步新增用例与断言数量(219 项 / 18 用例)。

二、第 33 轮报告 §10 后续建议的结论(详细数字见 Progress 第 34 轮)

1. **`EdgeScanFor` 状态查找哈希化:实测后回退。**
   实现方式为 `deque` + `unordered_multimap<keyHash, EdgeScanState*>`(桶内仍
   逐字段比对,碰撞安全;新建/淘汰/定向作废/清空四处同步索引),功能与像素
   验证全绿。但 198 个状态的整轮查询只从 0.097 降到 0.073 us/query(一轮省
   ≈4.8 us),而产物体积 +5,632 B。⇒ 回退。
   同时修正报告的量级:原估"288 信号 ≈0.3ms/帧",实测 ≈20us/帧。

2. **`EdgePanelLayoutFor` 表驱动化:不做。** 实测 1.9~2.5 us/次(每帧约 14 次
   ≈30us),不是热点;第 33 轮已消除"按种类分派"的重复,剩余 957 行是几何算式
   本身,无法表驱动。本轮改为交付"让这类重写可验证"的手段:
   `-run panels -mode record/compare` 逐面板比对几何 + 像素哈希。

3. **`wcstod` 自写替身:实测 0 字节收益。** 只加
   `-D__USE_MINGW_ANSI_STDIO=0` 全量重编后产物仍是 788,480 B、`mingw_wpformat.o`
   仍被链接。原因是预编译 libstdc++ 自己引用 `__mingw_swprintf`:只要还调用
   `std::to_wstring` / `std::to_string`(全仓约 80 处,若干处实参是浮点),
   gdtoa 必然被链入。⇒ 回退,并在 Makefile 留下说明防止重复尝试。

三、文档
* `Documents/Code_Review_Plan.md` 新增 §六(第 34 轮节点与验收标准)。
* `Documents/Code_Review_Progress.md` 新增第 34 轮逐节点记录(含全部实测数字)。
* `Documents/Code_Review_Report.md` 新增 §11(结论表 + 两处量级修正 + 更新的后续建议)。
* `Documents/agent_mem.md` 新增第 34 轮规则:Tools 约定、两个构建陷阱、
  两条"别再重复做"的实验结论。

验证:
* `make release`(删 .o 全量)零 error / 零 warning,产物 **788,480 B**(与改动前一致)。
* `make -C Tools` 零 error / 零 warning。
* `UnitTests.exe` → checks=219 failed=0 cases=18。
* `OffscreenChecks.exe -all` → 5/5 检查 PASS,checks=71 failed=0。
2026-11-04 11:52:10 +08:00

20 KiB
Raw Blame History

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

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

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


0. 一句话结论

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

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


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

1.1 节表前后对照

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

1.2 逐项来源

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

Toolbar::BuildBoxes 里唯一的一处:

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

审查 Makefile 时发现:

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

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

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

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

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


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

3.1 问题(屎山核心)

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

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

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

3.2 修法

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

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

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

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

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

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

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

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

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

3.4 验证

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


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

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

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

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

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


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

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

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

6. 性能审查结论

6.1 结论

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

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

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

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

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

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

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

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

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


7. 注释与文档

7.1 删除

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

7.2 校正(agent_mem.md)

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

7.3 源码注释

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

8. 验证清单

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

9. 提交清单

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

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

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

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

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

11.1 一句话结论

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

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

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

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

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

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

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

11.4 体积现状

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

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

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