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

102 lines
3.2 KiB
C++
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.
// 内存预检验与"分配失败"兜底(配合 `-fno-exceptions` 构建)。
//
// 背景:本项目关闭了 C++ 异常(`.xdata`+`.pdata` 占产物体积约 9%,关闭后**实测省 27,136 B**:
// 909,824 → 882,688;再叠加 LTO 共减 120,832 B)。关闭后 `std::bad_alloc` 不再可捕获,
// 任何分配失败都会直接 terminate。因此:
// 1. 凡"按数据规模预留大块内存"的地方改用 `MemReserveIfRoom()`(预检验);
// 2. 万一仍有遗漏,由 `MemInstallTerminateHandler()` 给出可见提示而不是静默消失。
#include "MemProbe.hpp"
#include <algorithm>
#include <atomic>
#include <cstdlib>
#include <exception>
namespace jdi
{
namespace
{
/** @brief 可用内存查询的缓存有效期(毫秒)。 */
constexpr std::uint64_t kMemCacheMs = 200;
/**
* @brief 预留后必须保留的可用内存比例(1/8)。
*
* 不把可用内存吃光:Windows 在物理内存接近耗尽时会大量换页,
* 表现为"整个程序卡死"而不是"分配失败",对用户更糟。
*/
constexpr std::size_t kMemHeadroomRatio = 8;
std::atomic<std::uint64_t> g_memCachedAt{0};
std::atomic<std::size_t> g_memCachedBytes{0};
/**
* @brief terminate 处理:给出可见提示后退出。
*
* ⚠️ 这里**不能**用 MessageBoxW 之外的任何复杂机制:此刻堆可能已不可用,
* 连 `std::wstring` 都不该构造(字面量是静态数据,安全)。
*/
void OnTerminate()
{
MessageBoxW(nullptr,
L"内存不足:本次操作需要的内存超过系统可用量。\n\n"
L"建议:减少同时显示的网格数或信号数,或关闭占用内存较大的程序后重试。",
L"Just DataInspector", MB_ICONERROR | MB_OK | MB_SETFOREGROUND);
std::_Exit(EXIT_FAILURE);
}
} // namespace
std::size_t MemAvailablePhysical()
{
const std::uint64_t now = GetTickCount64();
const std::uint64_t last = g_memCachedAt.load(std::memory_order_relaxed);
if (last != 0 && (now - last) < kMemCacheMs)
{
const std::size_t cached = g_memCachedBytes.load(std::memory_order_relaxed);
if (cached != 0)
{
return cached;
}
}
MEMORYSTATUSEX status = {};
status.dwLength = sizeof(status);
std::size_t available = std::numeric_limits<std::size_t>::max();
if (GlobalMemoryStatusEx(&status) != FALSE)
{
available = static_cast<std::size_t>(status.ullAvailPhys);
}
g_memCachedBytes.store(available, std::memory_order_relaxed);
g_memCachedAt.store(now, std::memory_order_relaxed);
return available;
}
bool MemCanAllocate(std::size_t bytes)
{
if (bytes == 0)
{
return true;
}
const std::size_t available = MemAvailablePhysical();
if (available == std::numeric_limits<std::size_t>::max())
{
return true; // 查询失败:按"内存充足"处理(宁可尝试也不要误拒)
}
const std::size_t headroom = available / kMemHeadroomRatio;
return bytes <= (available - std::min(available, headroom));
}
void MemInstallTerminateHandler()
{
static bool installed = false;
if (installed)
{
return;
}
installed = true;
std::set_terminate(&OnTerminate);
}
} // namespace jdi