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