Files
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

158 lines
7.2 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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",而是**分配失败从"可恢复"变成"进程终止"**。
所以必须成对交付:
1. 所有"按数据规模预留"的路径先问内存(`MemReserveIfRoom`);
2. 遗漏路径有兜底提示(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. 遗留
1. **L2 `UiDamage` + S4 叠加层**:设计已定型(`kMaxRects=12`、超限退化为包围盒、
整数像素外扩 1px、只增不减;`PlotView::Paint(g, UiDamage&)` 契约;6 条路径各自 `damage.Add`),
但未实施。`UiBoxTable` 的几何可以直接喂给它,不需要重新收集。
2. **探针脚本入版本库**(`Tools/*/build.ps1`、`.preflight/*/build.ps1`)。
3. **`.lst` 汇编清单在 LTO 下不可用**:需要时用 `make release LTO=0`,这一点已在 Makefile 注释中写明。
---
## 7. 交付确认
```powershell
git status --short # 干净
make clean; make release TARGET_FILE_NAME=JustDataInspector -j16
# → 零 error / 零 warning,788,992 B
```
唯一产物:`Build/Release/JustDataInspector.exe`(含图标、清单,`-static` 链 CRT)。
**无任何新增 DLL 依赖。**