Files
Just-DataInspector/Tools
DeepSeek V4.1 Flash 5c48bf922e review(follow-ups): 落地上轮建议并给出实测结论(#2/#4 回退、#3 不重写)
一、Tools 完善
* OffscreenChecks `edgestats`:改为自动挑"最有起伏"的信号。旧实现固定取
  `signals[0]`,而 MF4 的第一条信号是常量(6000)⇒ 三个阈值的断言退化成
  "0 == 0" 的空转。现在扫描前 200 条挑 `max-min` 最大者,SCATTER_TEST 上
  三个阈值分别得到 (20,19)/(20,19)/(20,19),与独立参考实现逐位相同。
* UnitTests 新增 `stdio_mingw_vs_ucrt_formats`:把 MinGW ANSI stdio 与系统
  UCRT 的 printf 在**本程序实际用到的全部格式**(`%.*f` / `%.6g` / `%g` /
  `%.4e` / `%.2f %ls` / `%llu` / `%lld` / `%02X` / `%.2f%%`)上做逐字符比对,
  13.6 万次调用 **0 处差异**(覆盖 1e-40~1e40)。它是"把 swprintf 让给 UCRT"
  这条体积优化的验收标准。
* Tools/README.md:同步新增用例与断言数量(219 项 / 18 用例)。

二、第 33 轮报告 §10 后续建议的结论(详细数字见 Progress 第 34 轮)

1. **`EdgeScanFor` 状态查找哈希化:实测后回退。**
   实现方式为 `deque` + `unordered_multimap<keyHash, EdgeScanState*>`(桶内仍
   逐字段比对,碰撞安全;新建/淘汰/定向作废/清空四处同步索引),功能与像素
   验证全绿。但 198 个状态的整轮查询只从 0.097 降到 0.073 us/query(一轮省
   ≈4.8 us),而产物体积 +5,632 B。⇒ 回退。
   同时修正报告的量级:原估"288 信号 ≈0.3ms/帧",实测 ≈20us/帧。

2. **`EdgePanelLayoutFor` 表驱动化:不做。** 实测 1.9~2.5 us/次(每帧约 14 次
   ≈30us),不是热点;第 33 轮已消除"按种类分派"的重复,剩余 957 行是几何算式
   本身,无法表驱动。本轮改为交付"让这类重写可验证"的手段:
   `-run panels -mode record/compare` 逐面板比对几何 + 像素哈希。

3. **`wcstod` 自写替身:实测 0 字节收益。** 只加
   `-D__USE_MINGW_ANSI_STDIO=0` 全量重编后产物仍是 788,480 B、`mingw_wpformat.o`
   仍被链接。原因是预编译 libstdc++ 自己引用 `__mingw_swprintf`:只要还调用
   `std::to_wstring` / `std::to_string`(全仓约 80 处,若干处实参是浮点),
   gdtoa 必然被链入。⇒ 回退,并在 Makefile 留下说明防止重复尝试。

三、文档
* `Documents/Code_Review_Plan.md` 新增 §六(第 34 轮节点与验收标准)。
* `Documents/Code_Review_Progress.md` 新增第 34 轮逐节点记录(含全部实测数字)。
* `Documents/Code_Review_Report.md` 新增 §11(结论表 + 两处量级修正 + 更新的后续建议)。
* `Documents/agent_mem.md` 新增第 34 轮规则:Tools 约定、两个构建陷阱、
  两条"别再重复做"的实验结论。

验证:
* `make release`(删 .o 全量)零 error / 零 warning,产物 **788,480 B**(与改动前一致)。
* `make -C Tools` 零 error / 零 warning。
* `UnitTests.exe` → checks=219 failed=0 cases=18。
* `OffscreenChecks.exe -all` → 5/5 检查 PASS,checks=71 failed=0。
2026-11-04 11:52:10 +08:00
..

Tools/ —— 单元测试与离屏验证工具

本目录是 验证基础设施,不是产品代码: 根目录 Makefile 只构建应用本体(Build/Release、Build/Debug), 不会编译这里的任何文件;这里的一切由 Tools/Makefile 独立构建。

设计目标:让"改完能不能证明没改坏"变成一条命令的事, 而不是靠肉眼截图 + 反复手点鼠标。


1. 目录结构

Tools/
├─ Makefile                  # 统一构建入口(构建 / 清理 / 帮助)
├─ README.md                 # 本文件(改动工具后必须同步更新)
├─ UnitTests/                # 工具一:纯数据层单元测试(不创建窗口)
│  ├─ Inc/
│  │  └─ UnitTestFramework.hpp
│  ├─ Src/
│  │  ├─ Main.cpp            # 测试执行器:-list / -only / -report / -verbose
│  │  ├─ Utf8UtilTests.cpp   # 数值解析与格式化语义
│  │  ├─ FormulaEvalTests.cpp# 公式语法 / 语义 / 批量求值一致性
│  │  └─ DataModelTests.cpp  # 变换顺序、滑动平均、公式值域缓存
│  └─ Build/                 # 构建产物(.gitignore,不入库)
└─ OffscreenChecks/          # 工具二:离屏验证(隐藏窗口 + 抓帧)
   ├─ Inc/
   │  ├─ OffscreenSupport.hpp# 白盒支撑:抓帧 / 哈希 / 隐藏窗口会话
   │  └─ CheckRunner.hpp     # 检查注册表 + 断言 / 输出接口
   ├─ Src/
   │  ├─ Main.cpp            # 命令行入口
   │  ├─ Session.cpp         # 隐藏窗口、加载、逐帧渲染(合成鼠标消息)
   │  ├─ Capture.cpp         # 后备缓冲抓帧、FNV 哈希、PNG 落盘
   │  └─ Checks.cpp          # 5 个检查的具体实现
   └─ Build/                 # 构建产物(.gitignore,不入库)

约定

项 约定
头文件 只放 <Tool>/Inc
实现 只放 <Tool>/Src(该目录下每个 .cpp 都会被编译)
产物 <Tool>/Build,与 Inc / Src 同级
无 Debug/Release 之分 工具一律 -O2 -g:要跑得快、要能上调试器
临时产物 PNG / 基线 / 报告一律写 Temp/tools/(已 gitignore),不许写进 Tools/

2. 构建

# 前置:应用本体必须先构建(工具链接的是本体的 .o,排除入口 Main.o)
make release -j8

# 构建全部工具
make -C Tools

# 只构建某一个
make -C Tools UnitTests
make -C Tools OffscreenChecks

# 清理(只删两个工具自己的 Build/,不动本体)
make -C Tools clean

# 看帮助
make -C Tools help

产物:

Tools/UnitTests/Build/UnitTests.exe
Tools/OffscreenChecks/Build/OffscreenChecks.exe

⚠️ 必须与本体保持同一套 ABI 开关:-fno-exceptions、-fno-rtti、 链接时 -flto -flto-partition=one。工具源码自身不加 -flto (否则 GCC 15.2.0 在 choose_baseaddr 处 ICE)。

⚠️ 头文件依赖用 -MMD -MP 跟踪:改头文件、增删源文件都会自动重编重链。 若出现"离奇的 undefined reference",先 make -C Tools clean。


3. 工具一:UnitTests(纯逻辑,最快、最常用)

不创建窗口、不初始化 GUI,因此可以无限次在后台运行。

Tools\UnitTests\Build\UnitTests.exe              # 全部
... -only formula                                # 只跑名字含 formula 的用例
... -list                                        # 列出用例
... -report ..\Temp\tools\ut.txt                 # 指定报告路径

输出约定(便于脚本 / Agent 解析):

[RUN ] formula_semantics
  [FAIL] <表达式>  (file:line)
SUMMARY checks=<n> failed=<m> cases=<k> result=<PASS|FAIL>

3.1 测试项

用例 覆盖内容 为什么重要
parse_fixed_corpus_matches_strtod 常见字面量(含 1e22、-0、1e-22)与 strtod 逐位相同 数值解析的基准线
parse_acceptance_semantics 空/空白/垃圾/尾随字符/十六进制浮点/inf/nan 的接受性 与改造前的宽松语义一致
parse_random_corpus_bit_exact 6 万条随机十进制串;≤15 位有效数字要求逐位相同,≤19 位允许 ≤1 ULP 「自写 strtod 替身」类优化的唯一验收标准
format_decimal_never_scientific FormatDecimal* 永不输出科学计数法;FormatValue = %.6g 显示层硬约束
format_byte_size_and_frequency 单位后缀存在且无指数形式 单位格式化回归
text_trim_and_utf8_conversion Trim / ToLowerAscii / Utf8ToWide / 大小写无关包含 文本基础
ticks_nice_step_and_build_ticks NiceStep 落在 1/2/2.5/5×10ⁿ;BuildTicks 端点 标尺可读性
formula_syntax_acceptance 合法 / 非法写法(单独 f、2e、()、*2 …) 公式输入框的报错边界
formula_semantics 2^3^2=512、-2^2=-4、2^-1、e/pi、恒等 语法语义
formula_identity_helpers f(t) ≡ 空串;HasFormula();NormalizeFormulaText 第 32 轮「恒等公式卡死」的护栏
formula_program_matches_pointwise 批量求值 vs 逐点求值 逐位相同(8 条公式 × 4000 点) 常量折叠 / 幂次改写不许改数值
formula_invalid_falls_back_safely 非法公式原样返回采样值 写错公式不得让波形消失
transform_apply_order 公式→系数→绝对值→垂直镜像→Y 偏移;NaN 保持;水平镜像只改下标 变换顺序
transform_value_range_and_order_preserving 区间映射(含跨 0 取绝对值)与保序判定 Y 轴自适应
filter_moving_average_equivalence 1 阶 ≡ 单次、2 阶 ≡ 链式、常数信号不变 均值滤波多阶
dataset_group_access AddGroup / GroupAt / Style(ref) 读写 数据层入口
dataset_formula_transform_cache 值域 + 逐分组极值 vs 逐点参考逐位相同;命中同指针;样式变化失效 第 32 轮核心缓存的正确性
stdio_mingw_vs_ucrt_formats MinGW ANSI stdio 与系统 UCRT 的 printf 在本程序用到的全部格式上逐字符比对(13.6 万次调用) 评估“把 swprintf 交给 UCRT”是否可行(当前 0 差异);将来真做迁移就是验收标准

3.2 验收标准

  • 零失败(result=PASS)才算通过;
  • 断言数量只增不减(当前 219 项 / 18 个用例):删断言必须说明理由;
  • 任何"为了让它过"而放宽的断言,必须在代码注释里写清依据。

4. 工具二:OffscreenChecks(真实渲染,隐藏窗口)

不显示窗口、不移动真实鼠标、不抢焦点:窗口 SW_HIDE 创建, "鼠标"通过 Application::HandleMouseMove() 注入合成消息, 像素从后备缓冲区读取(不是窗口 DC)。

Tools\OffscreenChecks\Build\OffscreenChecks.exe -list
... -all                                   # 依次跑全部检查
... -run smoke                             # 单个检查
... -run panels -mode record                # 记录面板像素基线
... -run panels -mode compare               # 与基线比对(重构前后 A/B)
... -run frames -frames 12                  # 连续抓 12 帧
... -data "..\TestData\MF4-Record.MF4" -run perf
... -out ..\Temp\tools\run1                 # 换输出目录(避免覆盖基线)
... -no-png                                 # 不落盘 PNG(只算哈希/耗时)

数据文件默认按顺序探测 TestData/ 里的 SCATTER_TEST.csv → [1].CSV → BIG_DATA.csv → Problem_Data.CSV; TestData/ 不入库,缺失时请用 -data 指定。

4.1 检查项

检查 做了什么 断言 / 判据
smoke 建隐藏窗口 → 加载 → 显示 3 条信号 → 渲染 → 抓帧 → 落盘 smoke.png 客户区/绘图区尺寸合理、绘图区墨迹 > 1000 像素、同一状态连续两帧逐像素相同(渲染确定性)
frames 沿信号列表逐行移动"鼠标",每帧抓图 + 区域哈希 + 耗时 平均帧 < 500ms、帧序列至少出现一次可见变化;输出 frame-XXX.png 供事后分析
panels 打开全部 7 个实验性面板(含双光标下的「选区截取」) 每个面板:功能已启用、有尺寸、必须被绘图区夹住;-mode record 写 panels-baseline.txt,-mode compare 逐面板比对像素哈希 + 几何
edgestats 3 个阈值下的边沿计数(普通计数)+ 中心轴计数 与 Checks.cpp 内的独立参考实现(朴素逐点)逐位相等;同参数重复查询结果稳定
perf 整帧中位数、EdgePanelLayoutFor 单次耗时、198 个统计状态的整轮查询耗时 整帧 < 500ms、面板几何 < 200µs/次、统计查询 < 20µs/次;数字用于前后对比

4.2 像素基线的正确用法(A/B)

# 1) 改动前:记录基线(同一台机器、同一 DPI、同一数据文件)
OffscreenChecks.exe -run panels -mode record -out ..\Temp\tools\panel-before

# 2) 改代码 + 重新构建本体与工具

# 3) 改动后:用同一输出目录比对
OffscreenChecks.exe -run panels -mode compare -out ..\Temp\tools\panel-before

⚠️ 像素哈希是"本机、本 DPI、本数据文件"的基线,跨机器/跨缩放不可比。 它适合"我改了一处,其它像素有没有动"的本地 A/B,不适合入库当黄金图。

4.3 输出(全部在 Temp/tools/,已 gitignore)

文件 内容
offscreen-report.txt 完整控制台输出(含 SUMMARY 行)
UnitTests-report.txt 单元测试报告
smoke.png / panels.png / perf.png 关键状态截图
frame-000.png … 连续帧(问题分析用)
panels-baseline.txt 面板像素基线(-mode record 生成)

5. 怎么用它(按改动类型选最小集)

你改了什么 建议跑
任何数据层 / 解析 / 公式 / 变换 UnitTests.exe
绘图、面板、布局、缓存键 -run smoke -run panels(+ 需要时 -mode compare)
边沿统计 / 中心轴 / 扫描预算 -run edgestats -run perf
性能相关 -run perf(前后各跑一次,比 us/query 与 median ms)
完全不确定影响面 -run -all(约几秒到几十秒)+ UnitTests.exe

用户明确要求:除非要求"全测一遍",否则只跑针对性检查。


6. 新增一个检查 / 一个工具

6.1 新增离屏检查

  1. 在 OffscreenChecks/Src/Checks.cpp 里写 int CheckXxx(Session&, const CheckOptions&), 用 Expect() / ExpectEqual() 记录断言,末尾 return CheckFailed();
  2. 在 AvailableChecks() 的列表里追加一行 { "xxx", "one-line summary", CheckXxx }
  3. 在 README 的 §4.1 表格里补一行(必须)

6.2 新增工具

  1. 建 Tools/<NewTool>/{Inc,Src}
  2. 在 Tools/Makefile 的 TOOLS 里追加 <NewTool>(目录名 = 目标名)
  3. 在本 README 的 §1 / §2 / §5 里补说明

7. 已知限制与坑

  • 工具是白盒:OffscreenSupport.hpp 用 #define private public 放宽访问级别, 为了读后备缓冲区 / 直接调面板几何。产品接口不因测试而放宽。
  • 必须先构建本体:工具链接 ../Build/Release/*.o,本体没构建会给出明确警告。
  • 像素基线不可跨机复现:DPI、字体、主题不同则哈希不同。
  • frames 里个别帧可能重复(鼠标落在筛选框/空白带),因此判据是"至少一次可见变化"。
  • 窗口一律 SW_HIDE:不要为了"看看效果"改成显示窗口,那会占用用户屏幕。 需要看图请直接打开 Temp/tools/*.png。
  • 进程退出用 std::_Exit():Application / PlotView 是单例, 若走正常析构会在 GdiplusShutdown 之后访问 GDI+ 对象(历史退出码 0xC0000005)。
  • 报告/截图不要提交:全部落在 Temp/(已忽略)。Tools/*/Build/ 也已忽略, 其它协作者本地 make -C Tools 即可重建。