## 为什么
用户 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` 的文档里。
7.2 KiB
Just DataInspector UI 重构 —— 最终报告
计划见
UI_Refactor_Plan.md,过程与实测数据见UI_Refactor_Progress.md。 日期:2026-10-29
1. 一句话结论
在不拆 DLL、不改优化等级、不改变任何绘制像素的前提下, 把界面从"宿主点名各组件 + 各算一遍几何"改成"组件协议 + 几何产物层", 同时把产物从 909,824 B 降到 788,992 B(−120,832 B,−13.3%), 并用 6 组、共 116 条断言 + 8 个逐像素哈希证明零回归。
2. 收益
2.1 结构
| 之前 | 之后 |
|---|---|
宿主 if 链逐个点名 4 个大组件,同一件事要写 5 遍 |
一张有序组件表,新增组件不改宿主 |
几何分散在 Paint* / HitTest / CollectDebugBoxes 三处,三份实现 |
BuildBoxes() 一处产出,绘制/命中/调试同源 |
| "调试"概念渗透进每个组件 | 组件只吐几何;"怎么画"收敛在 UiDebugBounds |
单个 .cpp 数千行 |
按职责分片(PlotView 5 片、SignalPanel 3 片…) |
2.2 体积
| 项 | 数值 |
|---|---|
| 基线 | 909,824 B |
| 最终 | 788,992 B |
| 减少 | 120,832 B(−13.3%) |
| 相对 1MB 预警阈值的余量 | 259,584 B(约 253 KiB) |
构成变化的解释见 UI_Refactor_Progress.md §2.3(.text −78 KB 来自 LTO,
.xdata+.pdata −27.5 KB 来自关异常 + gc,.rdata −14 KB 来自 gc 掉未引用常量)。
2.3 健壮性
-fno-exceptions之后没有"静默消失":OOM 走MemInstallTerminateHandler(), 弹一次带建议的模态提示再干净退出。- 唯一一处
try/catch改成预检验(MemReserveIfRoom),语义等价且不依赖异常。 PlotView里重复实现的"可用物理内存查询"被去重到MemProbe(单一实现、单一缓存)。
3. 关键决策与依据
3.1 拒绝"拆 DLL 换体积"
用户的定位是单文件轻量工具。拆 DLL 只把字节从 .exe 挪到旁边的 .dll,
总交付物更大、部署更脆(版本错配、路径缺失),且 -static 换来的"拷一个文件就能跑"
这一核心体验会丢掉。因此体积优化的边界是"编译期与链接期手段",不含产物拆分。
3.2 关异常 → 必须预检验,而不是"少写 try/catch"
-fno-exceptions 的真实代价不是"少了个 try",而是分配失败从"可恢复"变成"进程终止"。
所以必须成对交付:
- 所有"按数据规模预留"的路径先问内存(
MemReserveIfRoom); - 遗漏路径有兜底提示(terminate handler)。
只做第 1 步是不够的 —— 事后新增的代码很容易忘记检查。
3.3 LTO 用 -flto-partition=one 换稳定性
-flto 单独用会在 COMDAT 解析处 ICE(binds_to_current_def_p, symtab.cc:2598),
根因是多分区 LTRANS;单分区稳定(实测 --gc-sections 开/关都得到 791,552 B,
说明这一档体积收益来自 LTO 本身而不是分区数)。
代价是 LTRANS 串行 —— 这是用构建时间换"构建能过",值得。
同时确认:-flto 与 -ffunction-sections -fdata-sections 不能同时开(同一个 ICE),
LTO 下把 FUNC_SECTIONS 置空。
3.4 L1 而不是"给所有元素加基类"
PlotView 有 4 层自持位图缓存、SignalPanel 有行位图缓存,它们要求"像素由组件自己负责"。
把几何塞进继承体系会和缓存失效体系打架(历史上"后台渲染覆盖用户平移""命中错位"
就是这类冲突)。UiBox 是纯值类型,因此可以被绘制、命中、将来的脏矩形共用,
却不需要虚函数与生命周期管理。
3.5 L2 缓做
见 UI_Refactor_Plan.md §5。一句话:受益方只有调试开关打开时的帧率,
而正确实现要穿透 PlotView 全部四条快路径与两层缓存。
在"用户反复报告过拖拽错位/贴图残留"的背景下,这笔风险不值得为一个调试特性去承担。
4. 验证方法与结果
4.1 为什么"逐像素哈希"是唯一可信的验收
"看起来一样"不可作为判据:这类重构最容易出的问题是1px 偏移与边界条件
(例如某个元素在某个 DPI 下多画了一条线)。Tools/rendercheck 对 8 个典型场景
(空白 / 单网格 / 多网格 / 分组 / 面板展开 / 菜单 / 拾色器 / 样式区)渲染到内存位图后
取 FNV 哈希,与基线逐一比对,因此任何像素级差异都会暴露。
结果:8 个哈希与改造前逐一相同,且 JustDataInspector.exe 在 L1 前后字节数完全相同。
4.2 断言矩阵
| 探针 | 断言 | 覆盖 |
|---|---|---|
uicheck |
69 | 协议行为、命中、几何同源、浮层表面 |
lbcheck |
21 | 边界收集 + 关闭后逐像素还原 |
lbresid |
5 | 同一状态连续两帧逐像素相同(残留判据) |
lbzorder |
4 | 浮层压住本体 |
lbcovr |
7 | 覆盖完整性(时间轴/滚动条/胶囊/文本框/缓存行) |
lbpanel |
10 | 面板遮挡、面板内容裁剪、滚动不闪烁、菜单条目裁剪 |
| 合计 | 116 | — |
4.3 方法论教训(已写入 agent_mem.md)
- 体积结论必须区分稳态与瞬时;改编译开关后必须
make clean(混链会 ICE)。 - 判断"残留"不能用"拖拽帧 vs 松开后一帧"(预热进度不同会产生几十万像素合法差异), 正确判据是同一状态连续两帧逐像素相同。
--gc-sections已经承担了内联去重(UiPointInRect等自有符号重复组数为 0), 不必为了体积手工把 inline 实现搬进.cpp。- 探针判"盒子越界"必须按绘制层过滤,否则会把更上层浮层的合法越界算成错误。
5. 风险与缓解
| 风险 | 缓解 |
|---|---|
| LTO 构建偶发 ICE(编译器缺陷,非代码缺陷) | -flto-partition=one;LTO=0 可一键回退(体积回到 882,688 B,仍远低于阈值) |
-fno-exceptions 下遗漏一处分配检查 |
terminate handler 兜底 + 全仓已无 try/catch + MemReserveIfRoom 是唯一预留入口 |
| LTO 构建时间翻倍 | 日常开发可用 make release LTO=0;提交前用 LTO=1 校验体积 |
探针脚本未纳入版本库,重建时缺 -flto |
已写入 Progress §6 待办;脚本本身只是本地验证工具 |
| L2 未做,布局边界打开时帧率下降 | 该开关是调试特性,默认关闭;文档已记录取舍 |
6. 遗留
- L2
UiDamage+ S4 叠加层:设计已定型(kMaxRects=12、超限退化为包围盒、 整数像素外扩 1px、只增不减;PlotView::Paint(g, UiDamage&)契约;6 条路径各自damage.Add), 但未实施。UiBoxTable的几何可以直接喂给它,不需要重新收集。 - 探针脚本入版本库(
Tools/*/build.ps1、.preflight/*/build.ps1)。 .lst汇编清单在 LTO 下不可用:需要时用make release LTO=0,这一点已在 Makefile 注释中写明。
7. 交付确认
git status --short # 干净
make clean; make release TARGET_FILE_NAME=JustDataInspector -j16
# → 零 error / 零 warning,788,992 B
唯一产物:Build/Release/JustDataInspector.exe(含图标、清单,-static 链 CRT)。
无任何新增 DLL 依赖。