Code Review :代码审查 + 产物体积 −12.2%(890,880 → 782,336 B)+ 面板结构收敛 + 离屏验证工具入库 #3

Merged
LuChiChick merged 22 commits from Code_Review into dev 2026-11-04 09:54:20 +00:00
3 changed files with 0 additions and 512 deletions
Showing only changes of commit 764e5bee71 - Show all commits

No files matched your search

-153
View File
@@ -1,153 +0,0 @@
# Just DataInspector UI 重构 —— 计划
> 本文档只描述**要做什么、为什么这么做、怎么验收**。
> 已完成情况见 `UI_Refactor_Progress.md`,结论与遗留见 `UI_Refactor_Report.md`。
> 最后更新:2026-10-29
---
## 1. 目标与非目标
### 1.1 目标
| # | 目标 | 判据 |
| --- | --- | --- |
| G1 | 收敛"宿主点名每个组件"的散乱结构 | 宿主只遍历一张组件表,不再 `if (component == plot)` 式分支 |
| G2 | 几何只有**一处**来源 | "画出来的形状"与"能点到的区域"由同一批函数给出,不再各算一遍 |
| G3 | 源码按**职责**分片,单片可读 | 单个 `.cpp` 不超过约 1200 行;文件名与内容一致 |
| G4 | 减小产物体积,且**仍然只有一个 `.exe`** | 体积比基线小,无新增依赖 DLL |
| G5 | 关闭 C++ 异常后**不牺牲健壮性** | 所有大块分配前有预检验;OOM 有可见提示 |
| G6 | 重构**不得改变任何绘制像素** | 8 个场景的逐像素哈希与基线逐一相同 |
### 1.2 非目标(明确不做)
* **不把编译产物拆成 DLL**:本项目的定位是"单文件、拷过去就能跑的轻量工具"。
任何以拆 DLL 换体积的方案一律否决。
* **不改优化等级**(`-Oz` 保持不变)。
* **不把"波形"与"网格线"拆成两层位图**:像素级平移不变量会被破坏。
* **不为体积牺牲可维护性**(例如放弃分片、放弃几何同源)。
---
## 2. 硬约束
1. **单一可执行文件**:不得引入额外的 `.dll` / 运行时依赖。
2. **体积预警阈值 = 1MB(1,048,576 B)**。这不是硬上限:超过仍可发布,
但必须主动提醒用户,并说明"超在哪一段(`.text` / `.rdata` / `.xdata`+`.pdata` / `.rsrc`)、
由哪个改动贡献"。旧的 900KB / 800KB 红线**已作废**。
3. **异常处理用预检验,不用 try/catch**:`-fno-exceptions` 下 `try/catch` 不会被编译,
因此所有"按数据规模预留大块内存"的地方必须先问 `MemReserveIfRoom()`。
4. **像素级零回归**:验收以 `Tools/rendercheck` 的 8 个场景哈希为准。
5. **中文注释按 UTF-8 无 BOM 保存**;构建参数用 `-finput-charset=UTF-8 -fexec-charset=GBK`。
---
## 3. 分层架构(L0–L3)
```
L3 体积与异常策略 -fno-exceptions / MemProbe / LTO / gc-sections
▲
L2 重绘范围控制 UiDamage(脏矩形) ← 本轮评估后**缓做**,见 §5
▲
L1 几何产物层 UiBox / UiBoxTable / UiComponent::BuildBoxes
▲
L0 组件协议 UiComponent + 组件表 + UiWidget 控件树
```
### L0 组件协议(前几轮已完成)
* `UiComponent`:`SetScale / Layout / Area / Paint / HitTest / OnMouseLeave / IsEditing / QueryCursor`。
* 宿主持有一张**有序组件表**,逐个遍历;新增组件不必改宿主的 5 处消息分支。
* `UiWidget` 与 `UiComponent` 是**兄弟**而非父子:前者管"一块区域内部的布局/命中/绘制",
后者管"自己这一整块地盘:区域、缓存、局部重绘策略"。
`Toolbar` / `TabBar` 同时是两者。
### L1 几何产物层(本轮完成)
* 新文件 `Components/Shared/Inc/UiBox.hpp` + `Src/UiBox.cpp`。
* `struct UiBox { rect, clip, depth, layer }`:**纯值类型**,不继承任何东西。
* `class UiBoxTable`:几何收集器,只维护"矩形 / 层级 / 绘制层 / 裁剪 / 遮挡"这些**几何事实**,
**不含任何绘制代码**。
* `UiComponent::BuildBoxes(UiBoxTable&)`:组件上报自己的几何产物(默认只登记 `Area()`)。
* `UiDebugBounds` 退化为**消费者**:`UiDebugPaintBoxList(g, table)` 把几何画成彩色包角线。
⇒ 组件不再需要知道"调试"这个概念存在。
### L2 重绘范围控制(缓做 —— 见 §5)
* 设计意图:`UiDamage`(最多 12 个矩形的脏区集合,超限退化为包围盒)替代
"全窗重绘 / 绘图区重绘"的二值分支;`PlotView::Paint(g, UiDamage&)` 逐路径累加脏区。
* 收益:去掉 `SetForceFullRepaint`(布局边界开关打开时的整幅重绘)。
### L3 体积与异常策略(本轮完成)
见 `UI_Refactor_Progress.md` 的实测表。
---
## 4. 阶段与验收标准
| 阶段 | 内容 | 验收 |
| --- | --- | --- |
| P1–P6 | 文件按职责拆分、`UiComponent` 协议、组件表、布局边界功能 | 编译 + `uicheck` |
| P7 | 布局边界"几何同源"(`RowRect` 前缀和、文本墨迹矩形、浮层层级/遮挡/裁剪) | `lbcovr` / `lbzorder` / `lbresid` / `lbpanel` |
| P8 | 体积:`-fno-exceptions` + `MemProbe` + `-ffunction-sections` + `--gc-sections` | 体积下降且 8 哈希不变 |
| P9 | 体积:LTO(含 `-flto-partition=one` 稳定性约束) | 同上,且可重复构建 |
| P10 | L1 几何产物层(`UiBox` / `UiBoxTable` / `BuildBoxes`) | 同上,且源码零残留旧名 |
| P11 | 代码审计(过时注释 / 名实不符 / 死代码) | 零 warning + 全量回归 |
| P12 | 文档重建 + 本地提交 | `git status` 干净 |
**统一验收命令**
```powershell
make clean
make release TARGET_FILE_NAME=JustDataInspector -j16 # 期望零 error / 零 warning
make debug TARGET_FILE_NAME=JustDataInspectorD -j16 # 期望零 warning
objdump -h Build\Release\JustDataInspector.exe # 段构成,用于体积溯源
Tools\uicheck\uicheck.exe # 69/69
.preflight\lbcheck\lbcheck.exe # 21/21
.preflight\lbresid\lbresid.exe # 5/5
.preflight\lbzorder\lbzorder.exe # 4/4
.preflight\lbcovr\lbcovr.exe # 7/7
.preflight\lbpanel\lbpanel.exe # 10/10
Tools\rendercheck\rendercheck.exe # 8 哈希不变 + FUNC 7/7
```
---
## 5. 明确缓做的部分及理由
### L2 `UiDamage` + 独立叠加层(S4)
**为什么缓做**
1. 受益方只有"**调试开关打开时**的帧率"。`SetForceFullRepaint` 只在
「显示布局边界」开启时被置位(`Application::Render` 里 `m_plot.SetForceFullRepaint(m_showLayoutBounds)`),
普通用户路径上**本来就是快路径**。
2. 正确实现无法只改 `PlotView` 内部:宿主用的是**持久后备缓冲区**,
上一帧叠加层画上去的边框像素必须在下一帧被"底层内容重画"覆盖掉。
若改成"独立叠加位图 + 只混合脏区",就必须把叠加层的旧矩形**注入**到底层组件的重绘范围里 ——
这要穿透 `PlotView` 的全部四条快路径(静态场景贴图 / 宽基底贴图 / 样式补丁 / 网格层重建)
与两层缓存。
3. 风险与收益不成比例:用户已在 2026-10-28 反复报告过"拖拽错位 / 贴图残留"一类事故,
而这类事故正是从"某条快路径漏了某块像素"开始的。
**保留了什么**:`SetForceFullRepaint` 及其四处守卫、`UiBoxTable` 的几何产物。
将来若真要做,只需把 `UiBoxTable` 的 `rect` 直接喂给 `UiDamage`,不需要重新收集几何。
---
## 6. 交付物清单
```
Components/Shared/Inc/UiBox.hpp 新增(几何产物层)
Components/Shared/Src/UiBox.cpp 新增
Components/Shared/Inc/UiDebugBounds.hpp 精简为"只画"(消费者)
Components/Shared/Src/UiDebugBounds.cpp 精简为"只画"
Components/Shared/Inc/UiComponent.hpp CollectDebugBoxes → BuildBoxes
Utilities/Inc/MemProbe.hpp 新增(分配预检验 + terminate 兜底)
Utilities/Src/MemProbe.cpp 新增
Makefile LAYOUT_BOUNDS / NO_EXCEPTIONS / LTO 三个开关
Documents/UI_Refactor_{Plan,Progress,Report}.md 重建
```
-202
View File
@@ -1,202 +0,0 @@
# 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`。
-157
View File
@@ -1,157 +0,0 @@
# 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 依赖。**