# 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 依赖。**