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

203 lines
9.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_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&)` 默认实现:
```cpp
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`。