## 为什么
用户 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.7 KiB
7.7 KiB
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. 硬约束
- 单一可执行文件:不得引入额外的
.dll/ 运行时依赖。 - 体积预警阈值 = 1MB(1,048,576 B)。这不是硬上限:超过仍可发布,
但必须主动提醒用户,并说明"超在哪一段(
.text/.rdata/.xdata+.pdata/.rsrc)、 由哪个改动贡献"。旧的 900KB / 800KB 红线已作废。 - 异常处理用预检验,不用 try/catch:
-fno-exceptions下try/catch不会被编译, 因此所有"按数据规模预留大块内存"的地方必须先问MemReserveIfRoom()。 - 像素级零回归:验收以
Tools/rendercheck的 8 个场景哈希为准。 - 中文注释按 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 干净 |
统一验收命令
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)
为什么缓做
- 受益方只有"调试开关打开时的帧率"。
SetForceFullRepaint只在 「显示布局边界」开启时被置位(Application::Render里m_plot.SetForceFullRepaint(m_showLayoutBounds)), 普通用户路径上本来就是快路径。 - 正确实现无法只改
PlotView内部:宿主用的是持久后备缓冲区, 上一帧叠加层画上去的边框像素必须在下一帧被"底层内容重画"覆盖掉。 若改成"独立叠加位图 + 只混合脏区",就必须把叠加层的旧矩形注入到底层组件的重绘范围里 —— 这要穿透PlotView的全部四条快路径(静态场景贴图 / 宽基底贴图 / 样式补丁 / 网格层重建) 与两层缓存。 - 风险与收益不成比例:用户已在 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 重建