Files
Just-DataInspector/Documents/UI_Refactor_Progress.md
T
DeepSeek V4.1 Flash 044b87383b 体积 L3(-fno-exceptions + MemProbe + LTO)与 L1 几何产物层;产物 909,824 -> 788,992 B
## 为什么

用户 2026-10-29 要求:按既定计划完整实现改进,但
① **不得把编译产物拆成 DLL**(项目定位是单文件轻量工具,最终只能有一个 .exe);
② 异常处理改用**预检验**而不是 try/catch(-fno-exceptions 省体积,且 try/catch 本身开销大);
③ 先删除 Documents 内三份 UI 重构文档再按"计划 / 当前进度 / 最终报告"重建;
④ 顺便审计代码,删除无用/过时注释,修复名实不符;
⑤ 提交到本地仓库,期间不要占用用户的鼠标/键盘/屏幕。

## 改了什么

### 1. 体积(909,824 B -> 788,992 B,-120,832 B,-13.3%)

Makefile 新增三个可覆盖开关(默认全为 1):

- `LAYOUT_BOUNDS`  编入/剔除「显示布局边界」叠加层
- `NO_EXCEPTIONS`  追加 `-fno-exceptions`
- `LTO`            追加 `-flto`(编译 + 链接)

派生:`FUNC_SECTIONS`(LTO 下为空)、`GC_SECTIONS`(`-Wl,--gc-sections`)、
`ASM_LISTING`/`ASM_LISTING_C`(LTO 下为空,因为 lto-wrapper 会丢弃 `-Wa` 清单)。

两个实测复现的坑(已写进 Makefile 注释):

- **切换 LTO 必须 `make clean`**:LTO 与非 LTO 目标文件混链会让 GCC 15.2.0 在
  `binds_to_current_def_p (symtab.cc:2598)` 处 ICE。
- **`-flto` 与 `-ffunction-sections -fdata-sections` 同时打开也 ICE**(干净重编也必崩,
  报错落点是 `DataModel.hpp:393`,但那是 COMDAT 解析落点、不是原因)⇒
  LTO 下 `FUNC_SECTIONS` 置空,并加 `-flto-partition=one`:默认 11 个分区的 LTRANS
  稳定复现,单分区从未复现。**这是稳定性选项,不是性能选项。**

### 2. 关异常必须成对交付:预检验 + 兜底

新增 `Utilities/Inc/MemProbe.hpp` + `Utilities/Src/MemProbe.cpp`:

- `MemAvailablePhysical()`:`GlobalMemoryStatusEx` + 200ms 缓存
- `MemCanAllocate(bytes)`:预留后仍须保有 1/8 可用内存
- `MemReserveIfRoom(container, n)`:含 `capacity()` 早退与乘法溢出保护
- `MemInstallTerminateHandler()`:OOM 时弹一次带建议的模态提示后 `_Exit`(而不是静默消失)

接入:

- `Utilities/Src/DataModel.cpp`:全仓**唯一**的 `try/catch`(`extremes.reserve` 捕
  `std::bad_alloc`)改为 `MemReserveIfRoom(extremes, count / 8)`
- `Components/PlotView/Src/PlotView.cpp`:`AvailablePhysicalBytes()` 原有一份**完整重复**
  的 `GlobalMemoryStatusEx` + 缓存实现 → 转发 `MemAvailablePhysical()`
- `Components/Application/Src/Application.cpp`:`Initialize()` 开头安装 terminate handler

### 3. L1 几何产物层

新增 `Components/Shared/Inc/UiBox.hpp` + `Src/UiBox.cpp`:

- `struct UiBox { rect, clip, depth, layer }` —— 纯值类型,不继承任何东西
  (继承会和 PlotView 4 层位图缓存 / SignalPanel 行位图缓存的失效体系打架)
- `class UiBoxTable : public UiTextRectSink` —— 只累计几何 + 裁剪/层/遮挡,**不含绘制代码**

`UiComponent::CollectDebugBoxes` → **`virtual void BuildBoxes(UiBoxTable&)`**:
组件从"收集布局边界(调试用)"改为"上报几何产物",不再需要知道"调试"这个概念存在。
`UiDebugBounds.hpp/.cpp` 退化为唯一的消费者(配色 + 包角描边 + 遮挡挖洞)。

除宿主 `ApplicationCommands.cpp` 外,所有组件源码现在只包含 `UiBox.hpp`。

### 4. 审计

- `MemProbe` 里"省 40~60KB"的估算 → 改为实测 27,136 B
- `MemProbe.hpp` 的 `<new>`、`MemProbe.cpp` 的 `<cstdio>`:未使用,删除
- `UiDebugPaintBounds(g, rect, depth)`:零调用者,删除(只保留带 clip 的版本)
- `TabBar`/`Toolbar`/`PlotView`/`SignalPanel` 的 `BuildBoxes` 文档"(调试用)"→"几何 box"
- `PlotView::SetForceFullRepaint` 补写"为什么保留而不优化"

### 5. 文档

`Documents/UI_Refactor_Plan.md` / `_Progress.md` / `_Report.md` 先 `git rm` 再按
「计划 / 当前进度 / 最终报告」重建;`Documents/agent_mem.md` 追加第十三轮记录。

## 怎么验证

```
make clean
make release TARGET_FILE_NAME=JustDataInspector -j16   # 零 error / 零 warning,788,992 B
make debug   TARGET_FILE_NAME=JustDataInspectorD -j16  # 零 warning

uicheck 69/69   lbcheck 21/21  lbresid 5/5  lbzorder 4/4  lbcovr 7/7  lbpanel 10/10
rendercheck: 8 个场景哈希与改造前逐一相同 + FUNC 7/7 PASS, exit=0
  A FAED59DF284153DB  B E6443BFE4E3955F8  C 96A0AA8C402F1729  D E3D5DBD998E43720
  E DA0583CB13C26A6F  F FFB822159F604D1D  G 19F8DE8FE3A8DF18  H DBBDDE4CB2C97C92
```

体积溯源(`objdump -h`,基线 → 最终):
`.text` 735,600 → 657,168;`.rdata` 49,184 → 35,016;
`.xdata` 48,132 → 27,512;`.pdata` 35,424 → 28,524;`.rsrc` 20,656 不变。

## 没做什么(有意为之)

- **没有拆 DLL**(用户明确要求单文件)
- **没有改优化等级**(`-Oz` 保持)
- **L2(`UiDamage` 脏矩形 + 独立叠加层)评估后缓做**:受益方只有"调试开关打开时的帧率",
  而正确实现必须把叠加层旧矩形注入底层组件重绘范围,穿透 PlotView 四条快路径 + 两层缓存;
  在用户反复报过"拖拽错位 / 贴图残留"的背景下,风险与收益不成比例。理由写在
  `Documents/UI_Refactor_Plan.md` §5 与 `PlotView::SetForceFullRepaint` 的文档里。
2026-10-29 11:53:54 +08:00

9.2 KiB
Raw Blame History

Just DataInspector UI 重构 —— 当前进度

计划见 UI_Refactor_Plan.md,结论与遗留见 UI_Refactor_Report.md。 最后更新:2026-10-29(本轮:体积 L3 落地 + L1 几何产物层 + 审计)


1. 总览

层 内容 状态
L0 UiComponent 协议 + 有序组件表 + UiWidget 控件树 ✅ 已完成(前几轮)
L1 UiBox / UiBoxTable / UiComponent::BuildBoxes ✅ 本轮完成
L2 UiDamage 脏矩形 + S4 独立叠加层 ⏸ 评估后缓做(理由见 Plan §5)
L3 -fno-exceptions + MemProbe + -ffunction-sections + --gc-sections + LTO ✅ 本轮完成

2. L3 体积与异常策略

2.1 Makefile 开关

开关 默认 作用 备注
LAYOUT_BOUNDS 1 是否编入「显示布局边界」调试叠加层 make release LAYOUT_BOUNDS=0 可剔除
NO_EXCEPTIONS 1 追加 -fno-exceptions 必须配合 MemProbe 的预检验
LTO 1 追加 -flto(编译 + 链接) 切换需 make clean

派生变量:

变量 LTO=1 LTO=0
FUNC_SECTIONS 空 -ffunction-sections -fdata-sections
ASM_LISTING / ASM_LISTING_C 空 -Wa,-a,-ad,-ahlms=$(@:%.o=%.lst)
GC_SECTIONS -Wl,--gc-sections 同左

GC_SECTIONS / FUNC_SECTIONS / ASM_LISTING* 都定义在 LDFLAGS 之后。 它们是递归变量(= / +=),make 在展开 recipe 时才求值,所以顺序不影响生效; 之所以还是放在后面,是为了让"三个开关"集中在同一处,不散落在两个位置。

2.2 两个已复现的坑

坑 1:切换 LTO 必须 make clean。 LTO 与非 LTO 目标文件混链会让 GCC 15.2.0 在 binds_to_current_def_p (symtab.cc:2598) 处 ICE。

坑 2:-flto 与 -ffunction-sections -fdata-sections 同时打开也会触发同一个 ICE。 即使全部干净重编也必崩(Utilities/Inc/DataModel.hpp:393 的 struct SignalData 被指为出错点, 但那只是 COMDAT 解析的落点,不是原因)。 解决办法:LTO 下 FUNC_SECTIONS 置空(LTO 自己就做跨单元分区与去重), 并追加 -flto-partition=one: 默认多分区 LTRANS(本项目 11 个分区)稳定复现,单分区从未复现。 代价是 LTRANS 串行,全量构建慢约一倍。

结论:-flto-partition=one 不是性能选项,是稳定性选项,不要删。

2.3 实测体积

阶段 产物 体积 相对基线
基线 BaseCheck.exe 909,824 B —
-fno-exceptions + --gc-sections + MemProbe Step1.exe 882,688 B −27,136 B
再叠加 LTO JustDataInspector.exe 788,992 B −120,832 B(−13.3%)

段构成(objdump -h,最终构建):

段 基线 -fno-exc + gc-sections 最终(LTO)
.text 735,600 720,528 657,168
.rdata 49,184 45,072 35,016
.xdata 48,132 34,152 27,512
.pdata 35,424 34,524 28,524
.rsrc 20,656 20,656 20,656
.data 2,272 9,312 1,952
.bss 10,224 3,088 10,000
.idata 12,440 12,440 12,440
.tls 16 16 16
.reloc — — 1,700

体积溯源(超阈值时必须能回答这个问题):

  • .text 减少最多(−78 KB / −10.7%)—— 主要来自 LTO 的跨单元内联与重复代码合并。
  • .xdata+.pdata(异常展开表 + 函数表)减少 27.5 KB —— 来自 -fno-exceptions 与 --gc-sections 丢弃未引用的函数。
  • .rdata 减少 14 KB —— 来自"未引用字符串/常量被 gc 掉"。
  • .rsrc 完全不变(图标/清单)。

预警阈值 1MB(1,048,576 B):当前 788,992 B,余量 259,584 B(约 253 KiB)。

2.4 MemProbe(预检验 + 兜底)

函数 作用
MemAvailablePhysical() GlobalMemoryStatusEx + 200ms 缓存(绘制路径每帧可能问多次)
MemCanAllocate(bytes) 留出 1/8 可用内存作 headroom 后判断是否安全
MemReserveIfRoom(container, n) 仅供"按数据规模预留":含 capacity() 早退与乘法溢出保护
MemInstallTerminateHandler() std::set_terminate 装一次性模态提示后 _Exit

改动点:

  • Utilities/Src/DataModel.cpp:原先唯一的 try/catch(extremes.reserve(count / 8) 捕 std::bad_alloc)→ MemReserveIfRoom(extremes, count / 8)。
  • Components/PlotView/Src/PlotView.cpp:PlotView::AvailablePhysicalBytes() 原有一份完整的 GlobalMemoryStatusEx + 200ms 缓存实现(与 MemProbe 重复)→ 改为转发 MemAvailablePhysical()。
  • Components/Application/Src/Application.cpp:Initialize() 开头 MemInstallTerminateHandler();。

全仓已确认无 try/catch 残留(除 MemProbe 注释中对它的说明)。


3. L1 几何产物层

3.1 结构

UiBox.hpp          struct UiBox { rect, clip, depth, layer };   ← 纯值,无行为
                   class UiBoxTable : UiTextRectSink           ← 只累积几何 + 裁剪/层/遮挡
UiDebugBounds.hpp  UiDebugBoundsColor / UiDebugPaintBoundsClipped / UiDebugPaintBoxList
                   ↑ 唯一的消费者(调试描边),与几何解耦

UiComponent::BuildBoxes(UiBoxTable&) 默认实现:

void UiComponent::BuildBoxes(UiBoxTable& boxes) { boxes.Add(Area(), 0); }

各组件覆盖情况:

组件 上报内容
SignalPanel 表头 / 筛选框 / 列表 / 样式区 / 每个可见行 / 行内元素(复选框、色块、名称、GX 徽标、✕、折起三角、组计数)/ 滚动轨道与滑块 / 编辑框
PlotView 网格、GX 徽标、图例条目、5 类浮动面板(含逐字段)+ 面板遮挡与裁剪
Toolbar 区域、每个按钮、按钮内容(文本/图标)
TabBar 区域、每个标签条、新建按钮、标签 ✕、控件树
PopupMenu 条目(裁到 m_scrollEnabled ? m_scrollViewport : m_rect)、可视区、轨道、滑块、二级子菜单
ColorPickerPopup 各控件

3.2 与 UiDebugBounds 的边界

  • 几何(矩形 / 层级 / 层号 / 裁剪 / 遮挡)→ UiBox.hpp
  • 观感(配色 / 包角粗细 / 图元数量 / 抗锯齿开关)→ UiDebugBounds.hpp

这样 SignalPanel.cpp / PlotViewPaint.cpp / PopupMenu.cpp 等只包含 UiBox.hpp, 不再把"调试描边"这个依赖拉进自己的编译单元;只有宿主 ApplicationCommands.cpp 同时包含两者。

3.3 零像素回归的证明

L1 落地前后 JustDataInspector.exe 字节数完全相同(788,992 B), rendercheck 8 个场景哈希逐一相同 —— 因为这只是"把几何从调试绘制里搬出来", 没有动任何绘制顺序或图元。


4. 回归结果(最终构建上跑)

探针 结果
Tools/uicheck 69 passed, 0 failed
.preflight/lbcheck 21 passed, 0 failed
.preflight/lbresid 5 passed, 0 failed
.preflight/lbzorder 4 passed, 0 failed
.preflight/lbcovr 7 passed, 0 failed
.preflight/lbpanel 10 passed, 0 failed

Tools/rendercheck(逐像素哈希,必须与基线逐一相同):

A FAED59DF284153DB    B E6443BFE4E3955F8    C 96A0AA8C402F1729    D E3D5DBD998E43720
E DA0583CB13C26A6F    F FFB822159F604D1D    G 19F8DE8FE3A8DF18    H DBBDDE4CB2C97C92
FUNC 7/7 PASS, exit=0

构建:make release 零 error / 零 warning;make debug 零 warning。

⚠️ 探针 build.ps1 链接的是 Build/Release/*.o。LTO 下这些是 LTO 字节码, 因此 7 个探针脚本的链接参数里都补了 -flto -flto-partition=one。 若把 LTO 关掉,这两个参数无害(可与普通目标文件混链)。


5. 本轮审计清单

项 处理
MemProbe.hpp / .cpp 里"省 40~60KB"的估算 改为实测值 27,136 B(909,824 → 882,688)
MemProbe.hpp 的 #include <new> 未使用,删除
MemProbe.cpp 的 #include <cstdio> 未使用,删除
UiDebugPaintBounds(g, rect, depth) 零调用者(唯一调用者是它自己),删除;只保留带 clip 的版本
TabBar::BuildBoxes / Toolbar::BuildBoxes / PlotView::BuildBoxes / SignalPanel::BuildBoxes 的"(调试用)" 改为"几何 box"(层级已变,它们不再关心调试)
PlotView::SetForceFullRepaint 文档 补写"为什么保留而不优化"(L2 缓做的理由)
全仓旧名 UiDebugBoxList / UiDebugBox / CollectDebugBoxes 源码零残留(仅 Documents/agent_mem.md 历史记录保留原样)

6. 已知问题 / 未做

  1. L2 未做(见 Plan §5)。SetForceFullRepaint 仍在,布局边界打开时 PlotView 走整幅重绘。
  2. Tools/ 与 .preflight/ 下的探针脚本未被 git 跟踪(本地验证工具), 本轮给它们补的 -flto 参数不会进版本库。若 LTO 长期默认开启, 建议把探针脚本纳入版本管理,否则下次 make clean 后重建探针需要手工补参数。
  3. .lst 汇编清单在 LTO=1 时不再生成(lto-wrapper 会丢弃 -Wa 参数)。 需要看汇编时用 make release LTO=0。