Files
Just-DataInspector/Tools/OffscreenChecks/Src/Checks.cpp
T
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

874 lines
30 KiB
C++
Raw 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.
/**
* @file Checks.cpp
* @brief 离屏检查的具体实现(隐藏窗口 + 合成消息,绝不占用真实鼠标/键盘/屏幕)。
*
* 检查清单:
* * `smoke` —— 能建隐藏窗口、能加载数据、能画出波形(像素级)。
* * `frames` —— **连续帧抓取**:沿绘图区移动鼠标,逐帧抓 PNG + 哈希 + 耗时;
* 并验证"同一状态连续两帧逐像素相同"(渲染确定性)。
* * `panels` —— 7 种实验性功能面板全部展开后的**几何 + 像素基线**,
* 支持 `-mode record` / `-mode compare` 做重构前后 A/B。
* * `edgestats` —— 边沿统计与**独立参考实现**逐位比对(覆盖阈值变化、缓存稳定性)。
* * `perf` —— 关键路径耗时:整帧、面板几何、边沿统计查询。
*
* ⚠️ 所有 `Expect()` 都是"记录并继续",单个检查跑完仍然给出完整报告。
*/
#include <chrono>
#include <cmath>
#include <cstdio>
#include <cstring>
#include <limits>
#include <string>
#include <vector>
#include "CheckRunner.hpp"
namespace jdi
{
namespace tools
{
namespace
{
/** @brief 各检查共用的临时输出目录(相对项目根,已被 .gitignore 忽略)。 */
const char* const kDefaultOutDir = "../Temp/tools";
/** @brief 拼路径。 */
std::string Join(const std::string& directory, const std::string& name)
{
if (directory.empty())
{
return name;
}
const char last = directory.back();
if (last == '/' || last == '\\')
{
return directory + name;
}
return directory + "/" + name;
}
/** @brief 把 ASCII/本地代码页路径转宽字符串。 */
std::wstring Widen(const std::string& text)
{
const int length = MultiByteToWideChar(CP_ACP, 0, text.c_str(), -1, nullptr, 0);
if (length <= 1)
{
return std::wstring();
}
std::wstring wide(static_cast<std::size_t>(length - 1), L'\0');
MultiByteToWideChar(CP_ACP, 0, text.c_str(), -1, &wide[0], length);
return wide;
}
/** @brief 在路径中取目录部分。 */
std::string DirectoryOf(const std::string& path)
{
const std::size_t slash = path.find_last_of("/\\");
return (slash == std::string::npos) ? std::string() : path.substr(0, slash);
}
/** @brief 扫描信号的有限采样值范围(不依赖统计字段,便于构造探针阈值)。 */
void FiniteRange(const std::vector<double>& values, double& low, double& high)
{
low = std::numeric_limits<double>::infinity();
high = -std::numeric_limits<double>::infinity();
for (const double value : values)
{
if (!std::isfinite(value))
{
continue;
}
low = std::min(low, value);
high = std::max(high, value);
}
if (!std::isfinite(low) || !std::isfinite(high))
{
low = 0.0;
high = 1.0;
}
}
/**
* @brief 边沿统计的**独立参考实现**(与 `PlotView::EdgeScanFor` 的契约一致)。
*
* 契约(源码注释与实测共同确认):
* * 判定量 `delta = 绘制值 - 阈值`;
* * 采样对 `k-1 → k`:`previous < 0 && current >= 0` 记上升沿,
* `previous > 0 && current <= 0` 记下降沿;
* * 任一端为 NaN(缺失值 / 区间外)则该采样对不计入,并**打断连续性**。
*
* ⚠️ 这里刻意用最朴素的逐点写法:参考实现宁慢勿巧,才有资格当"金标准"。
*/
void ReferenceEdgeCounts(const std::vector<double>& values, const SignalStyle& style,
const double threshold, std::size_t& rising, std::size_t& falling)
{
rising = 0;
falling = 0;
double previous = std::numeric_limits<double>::quiet_NaN();
for (std::size_t index = 0; index < values.size(); ++index)
{
const double raw = values[index];
double current = std::numeric_limits<double>::quiet_NaN();
if (std::isfinite(raw))
{
const double drawn = ApplyDrawTransform(style, raw);
if (std::isfinite(drawn))
{
current = drawn - threshold;
}
}
if (std::isfinite(previous) && std::isfinite(current))
{
if (previous < 0.0 && current >= 0.0)
{
++rising;
}
else if (previous > 0.0 && current <= 0.0)
{
++falling;
}
}
previous = current;
}
}
/** @brief 查询边沿统计直到完成(每次未完成就再出一帧以重置每帧预算)。 */
bool CollectEdgeCounts(Session& session, const TimeAxis& axis, const SignalData& signal,
const SignalStyle& style, const double threshold, std::size_t& rising,
std::size_t& falling, int& framesUsed)
{
rising = 0;
falling = 0;
framesUsed = 0;
bool complete = false;
const int maxFrames = 4000;
while (!complete && framesUsed < maxFrames)
{
session.App().m_plot.EdgeCountsFor(axis, signal, style, threshold, false, 0.0, 0.0, rising,
falling, complete);
if (complete)
{
break;
}
session.RenderFrame();
++framesUsed;
}
return complete;
}
/** @brief 简单 key=value 基线文件的读取。 */
std::vector<std::pair<std::string, std::string>> ParseBaseline(const std::string& text)
{
std::vector<std::pair<std::string, std::string>> entries;
std::size_t cursor = 0;
while (cursor < text.size())
{
std::size_t end = text.find('\n', cursor);
if (end == std::string::npos)
{
end = text.size();
}
std::string line = text.substr(cursor, end - cursor);
cursor = end + 1;
while (!line.empty() && (line.back() == '\r' || line.back() == ' '))
{
line.pop_back();
}
if (line.empty() || line[0] == '#')
{
continue;
}
const std::size_t equals = line.find('=');
if (equals == std::string::npos)
{
continue;
}
entries.emplace_back(line.substr(0, equals), line.substr(equals + 1));
}
return entries;
}
/** @brief 面板种类名(打印 / 基线文件键名)。 */
const char* PanelKindName(const EdgePanelKind kind)
{
switch (kind)
{
case EdgePanelKind::COUNT:
return "count";
case EdgePanelKind::CENTER:
return "center";
case EdgePanelKind::MEAN:
return "mean";
case EdgePanelKind::PEAK:
return "peak";
case EdgePanelKind::MARK:
return "mark";
case EdgePanelKind::RESAMPLE:
return "resample";
case EdgePanelKind::CLIP:
return "clip";
}
return "unknown";
}
/** @brief 确保某个实验性功能已开启(`Toggle*` 是开关语义)。 */
void EnsureLaneFeature(PlotView& plot, const int laneIndex, const EdgePanelKind kind)
{
switch (kind)
{
case EdgePanelKind::COUNT:
if (!plot.LaneEdgeCount(laneIndex))
{
plot.ToggleLaneEdgeCount(laneIndex);
}
break;
case EdgePanelKind::CENTER:
if (!plot.LaneEdgeCenterCount(laneIndex))
{
plot.ToggleLaneEdgeCenterCount(laneIndex);
}
break;
case EdgePanelKind::MEAN:
if (!plot.LaneMeanFilter(laneIndex))
{
plot.ToggleLaneMeanFilter(laneIndex);
}
break;
case EdgePanelKind::PEAK:
if (!plot.LaneWeakPeak(laneIndex))
{
plot.ToggleLaneWeakPeak(laneIndex);
}
break;
case EdgePanelKind::MARK:
if (!plot.LaneFreeMarker(laneIndex))
{
plot.ToggleLaneFreeMarker(laneIndex);
}
break;
case EdgePanelKind::RESAMPLE:
if (!plot.LaneResample(laneIndex))
{
plot.ToggleLaneResample(laneIndex);
}
break;
case EdgePanelKind::CLIP:
if (!plot.LaneClip(laneIndex))
{
plot.ToggleLaneClip(laneIndex);
}
break;
}
}
// ---- 检查实现 -------------------------------------------------------
int CheckSmoke(Session& session, const CheckOptions& options)
{
int clientWidth = 0;
int clientHeight = 0;
session.ClientSize(clientWidth, clientHeight);
EmitLine("[smoke] client=%dx%d lanes=%d\n", clientWidth, clientHeight, session.LaneCount());
session.ShowFirstSignals(3);
Expect(session.RenderFrame(), "render frame");
Expect(session.LaneCount() > 0, "at least one lane after showing signals");
const Frame& frame = session.LastFrame();
Expect(frame.Valid(), "captured frame is valid");
const PixelRect plot = ToPixelRect(session.PlotArea());
const int ink = CountInk(frame, plot);
EmitLine("[smoke] plot rect=(%d,%d,%dx%d) ink=%d render=%.2fms\n", plot.x, plot.y, plot.width,
plot.height, ink, session.LastRenderMs());
Expect(plot.width > 100 && plot.height > 100, "plot area is reasonable");
Expect(ink > 1000, "plot area has drawn content");
if (options.savePng)
{
const std::string path = Join(options.outDir, "smoke.png");
Expect(SavePng(frame, path), "save smoke.png");
EmitLine("[smoke] saved %s\n", path.c_str());
}
// 同一状态再渲染一帧:必须逐像素相同(渲染确定性;也验证缓存没有"半帧"状态)
const Frame first = session.LastFrame();
Expect(session.RenderFrame(), "render frame again");
const PixelRect whole{0, 0, session.LastFrame().width, session.LastFrame().height};
const int drift = CountDiff(first, session.LastFrame(), whole, 0);
EmitLine("[smoke] determinism: differing pixels between two identical frames = %d\n", drift);
Expect(drift == 0, "two identical frames are pixel-identical");
return CheckFailed();
}
int CheckFrames(Session& session, const CheckOptions& options)
{
session.ShowFirstSignals(2);
Expect(session.RenderFrame(), "initial render");
const PixelRect plot = ToPixelRect(session.PlotArea());
Expect(plot.width > 100, "plot area is wide enough");
// 抓帧同时扫过**信号列表**:行悬停会改变行底色,因此每帧像素必然不同
//(比“在空白绘图区里移动鼠标”更能证明“帧序列真的拍到了状态变化”)。
const gd::RectF panelArea = session.App().m_panel.Area();
const int panelX = static_cast<int>(panelArea.X + panelArea.Width * 0.5f);
const int panelTop = static_cast<int>(panelArea.Y) + 60;
const int frames = std::max(1, options.frames);
int identicalPairs = 0;
std::uint64_t previousHash = 0;
double totalMs = 0.0;
double worstMs = 0.0;
for (int index = 0; index < frames; ++index)
{
// 沿信号列表纵向移动“鼠标”(合成消息,不动真实光标)
const int y = panelTop + index * 34;
session.MouseMove(panelX, y);
Expect(session.RenderFrame(), "render frame while moving");
const Frame& frame = session.LastFrame();
const std::uint64_t hash = HashRegion(frame, plot);
char name[64] = {};
std::snprintf(name, sizeof(name), "frame-%03d.png", index);
if (options.savePng)
{
if (!SavePng(frame, Join(options.outDir, name)))
{
Expect(false, "save frame png");
}
}
EmitLine("[frames] #%02d mouse=(%d,%d) plot-hash=%016llX render=%.2fms file=%s\n", index, panelX,
y, static_cast<unsigned long long>(hash), session.LastRenderMs(), name);
totalMs += session.LastRenderMs();
worstMs = std::max(worstMs, session.LastRenderMs());
if (index > 0 && hash == previousHash)
{
++identicalPairs;
EmitLine("[frames] WARNING: frame %d pixel-identical to previous\n", index);
}
previousHash = hash;
}
EmitLine("[frames] average=%.2fms worst=%.2fms identical_pairs=%d\n",
totalMs / static_cast<double>(frames), worstMs, identicalPairs);
Expect(totalMs / static_cast<double>(frames) < 500.0, "average frame time below 500ms");
// 至少要有一次可见变化(某些 y 落在筛选框/空白带,重复是正常的)
Expect(identicalPairs < frames - 1, "frame sequence captures at least one visual change");
return CheckFailed();
}
int CheckPanels(Session& session, const CheckOptions& options)
{
// 单信号单网格:7 个面板都落在同一个网格上,便于逐面板取几何
session.ShowFirstSignals(1);
Expect(session.LaneCount() == 1, "exactly one lane");
Application& app = session.App();
PlotView& plot = app.m_plot;
// 「选区截取」要求双光标,先切到双光标模式
if (app.ActiveTab().CursorMode() != PlotCursorMode::DOUBLE)
{
const int guard = 4;
for (int step = 0; step < guard && app.ActiveTab().CursorMode() != PlotCursorMode::DOUBLE; ++step)
{
plot.CycleCursorMode();
}
}
static const EdgePanelKind kinds[] = {EdgePanelKind::COUNT, EdgePanelKind::CENTER,
EdgePanelKind::MEAN, EdgePanelKind::PEAK,
EdgePanelKind::MARK, EdgePanelKind::RESAMPLE,
EdgePanelKind::CLIP};
for (const EdgePanelKind kind : kinds)
{
EnsureLaneFeature(plot, 0, kind);
}
Expect(session.RenderFrame(), "render with all panels open");
const PixelRect plotRect = ToPixelRect(session.PlotArea());
std::string content;
for (const EdgePanelKind kind : kinds)
{
const PlotView::EdgePanelLayout layout = plot.EdgePanelLayoutFor(0, kind);
const PixelRect rect = ToPixelRect(layout.panel);
const Lane& lane = app.ActiveTab().Lanes()[0];
const bool enabled = LanePanelEnabled(lane, kind);
const bool open = LanePanelStateOf(lane, kind).open;
EmitLine("[panels] %-8s enabled=%d open=%d rect=(%d,%d,%dx%d) hash=%016llX\n",
PanelKindName(kind), enabled ? 1 : 0, open ? 1 : 0, rect.x, rect.y, rect.width,
rect.height, static_cast<unsigned long long>(HashRegion(session.LastFrame(), rect)));
Expect(enabled, std::string("feature enabled: ") + PanelKindName(kind));
Expect(rect.width > 0 && rect.height > 0,
std::string("panel has size: ") + PanelKindName(kind));
// 不变式:面板必须被绘图区夹住(这是历史上"面板超出绘图区"缺陷的护栏)
const bool inside = rect.x >= plotRect.x - 1 && rect.y >= plotRect.y - 1 &&
rect.x + rect.width <= plotRect.x + plotRect.width + 1 &&
rect.y + rect.height <= plotRect.y + plotRect.height + 1;
Expect(inside, std::string("panel inside plot area: ") + PanelKindName(kind));
char buffer[160] = {};
std::snprintf(buffer, sizeof(buffer), "%s=%016llX,%d,%d,%d,%d\n", PanelKindName(kind),
static_cast<unsigned long long>(HashRegion(session.LastFrame(), rect)), rect.x,
rect.y, rect.width, rect.height);
content += buffer;
}
const std::string baselinePath = Join(options.outDir, "panels-baseline.txt");
if (options.mode == "record")
{
Expect(WriteTextFile(baselinePath, content), "write panels baseline");
EmitLine("[panels] baseline recorded: %s\n", baselinePath.c_str());
}
else if (options.mode == "compare")
{
const std::string baseline = ReadTextFile(baselinePath);
Expect(!baseline.empty(), "panels baseline exists (run -mode record first)");
const std::vector<std::pair<std::string, std::string>> entries = ParseBaseline(baseline);
const std::vector<std::pair<std::string, std::string>> current = ParseBaseline(content);
ExpectEqual(static_cast<long long>(current.size()), static_cast<long long>(entries.size()),
"panel count matches baseline");
for (const auto& expected : entries)
{
bool found = false;
for (const auto& actual : current)
{
if (actual.first != expected.first)
{
continue;
}
found = true;
if (actual.second != expected.second)
{
EmitLine("[panels] MISMATCH %s: baseline=%s current=%s\n", expected.first.c_str(),
expected.second.c_str(), actual.second.c_str());
Expect(false, std::string("panel pixel hash unchanged: ") + expected.first);
}
break;
}
if (!found)
{
Expect(false, std::string("panel present in baseline: ") + expected.first);
}
}
EmitLine("[panels] compare done against %s\n", baselinePath.c_str());
}
else
{
// verify:没有基线也要守住"几何不变式",并把当前哈希打出来便于人工比对
EmitLine("[panels] (hint) use -mode record / -mode compare for pixel A/B\n");
}
if (options.savePng)
{
Expect(SavePng(session.LastFrame(), Join(options.outDir, "panels.png")),
"save panels.png");
}
return CheckFailed();
}
int CheckEdgeStats(Session& session, const CheckOptions& options)
{
session.ShowFirstSignals(1);
Expect(session.LaneCount() >= 1, "one lane");
const Application& app = session.App();
SignalRef ref{0, 0};
const DataSet& dataSet = app.m_dataSet;
const SignalGroup* group = dataSet.GroupAt(0);
Expect(group != nullptr && !group->signals.empty(), "group has signals");
if (group == nullptr || group->signals.empty())
{
return CheckFailed();
}
// 挑一条"最有起伏"的信号:常量信号(只有 1 个不同取值)上任何阈值都得不到边沿,
// 断言会退化成"0 == 0"的空转(MF4 的第一条信号就是常量)。
const SignalData* chosen = nullptr;
double chosenSpan = -1.0;
for (const SignalData& candidate : group->signals)
{
double low = 0.0;
double high = 1.0;
FiniteRange(candidate.values, low, high);
const double span = high - low;
if (candidate.values.size() >= 4 && span > chosenSpan)
{
chosen = &candidate;
chosenSpan = span;
}
}
Expect(chosen != nullptr && chosenSpan > 0.0, "found a non-constant signal");
if (chosen == nullptr || !(chosenSpan > 0.0))
{
return CheckFailed();
}
const SignalData& signal = *chosen;
const TimeAxis& axis = group->AxisOf(signal);
const SignalStyle& style = signal.style;
double low = 0.0;
double high = 1.0;
FiniteRange(signal.values, low, high);
EmitLine("[edgestats] signal=%d samples min=%.6g max=%.6g\n",
static_cast<int>(signal.values.size()), low, high);
const double thresholds[] = {low + (high - low) * 0.25, (low + high) * 0.5,
low + (high - low) * 0.75};
for (const double threshold : thresholds)
{
std::size_t rising = 0;
std::size_t falling = 0;
int framesUsed = 0;
const bool complete = CollectEdgeCounts(session, axis, signal, style, threshold, rising,
falling, framesUsed);
Expect(complete, "edge scan completes");
std::size_t expectedRising = 0;
std::size_t expectedFalling = 0;
ReferenceEdgeCounts(signal.values, style, threshold, expectedRising, expectedFalling);
EmitLine("[edgestats] threshold=%.6g frames=%d got=(%llu,%llu) expected=(%llu,%llu)\n",
threshold, framesUsed, static_cast<unsigned long long>(rising),
static_cast<unsigned long long>(falling),
static_cast<unsigned long long>(expectedRising),
static_cast<unsigned long long>(expectedFalling));
ExpectEqual(static_cast<long long>(rising), static_cast<long long>(expectedRising),
"rising edge count matches reference");
ExpectEqual(static_cast<long long>(falling), static_cast<long long>(expectedFalling),
"falling edge count matches reference");
// 缓存稳定性:同参数再查一次必须给出同一份完整结果
std::size_t againRising = 0;
std::size_t againFalling = 0;
bool againComplete = false;
app.m_plot.EdgeCountsFor(axis, signal, style, threshold, false, 0.0, 0.0, againRising,
againFalling, againComplete);
Expect(againComplete, "repeat query completes immediately");
ExpectEqual(static_cast<long long>(againRising), static_cast<long long>(rising),
"repeat query rising stable");
ExpectEqual(static_cast<long long>(againFalling), static_cast<long long>(falling),
"repeat query falling stable");
}
// 中心轴统计:只验证"能完成且结果稳定"(独立参考实现需要复刻窗口语义,
// 收益不大;普通计数已覆盖 `EdgeScanFor` 的共享状态机)
{
std::size_t rising = 0;
std::size_t falling = 0;
bool complete = false;
int guard = 0;
while (!complete && guard < 4000)
{
app.m_plot.EdgeCenterCountsFor(axis, signal, style, 5, 1, false, 0.0, 0.0, rising,
falling, complete);
if (complete)
{
break;
}
session.RenderFrame();
++guard;
}
Expect(complete, "centre edge scan completes");
EmitLine("[edgestats] centre (points=5,iterations=1) rising=%llu falling=%llu frames=%d\n",
static_cast<unsigned long long>(rising),
static_cast<unsigned long long>(falling), guard);
(void)ref;
}
return CheckFailed();
}
int CheckPerf(Session& session, const CheckOptions& options)
{
session.ShowFirstSignals(8);
Expect(session.RenderFrame(), "warm-up render");
// 1) 整帧渲染耗时(取 7 帧中位数,第一帧可能是预热)
std::vector<double> renderTimes;
for (int iteration = 0; iteration < 7; ++iteration)
{
if (!session.RenderFrame())
{
Expect(false, "render frame");
break;
}
renderTimes.push_back(session.LastRenderMs());
}
std::sort(renderTimes.begin(), renderTimes.end());
const double medianRender =
renderTimes.empty() ? 0.0 : renderTimes[renderTimes.size() / 2];
EmitLine("[perf] render median=%.2fms lanes=%d\n", medianRender, session.LaneCount());
Expect(medianRender < 500.0, "full frame below 500ms");
// 2) 面板几何:`EdgePanelLayoutFor` 在鼠标移动的高频路径上,必须是"微秒级"
Application& app = session.App();
PlotView& plot = app.m_plot;
static const EdgePanelKind kinds[] = {EdgePanelKind::COUNT, EdgePanelKind::CENTER,
EdgePanelKind::MEAN, EdgePanelKind::PEAK,
EdgePanelKind::MARK, EdgePanelKind::RESAMPLE,
EdgePanelKind::CLIP};
for (const EdgePanelKind kind : kinds)
{
EnsureLaneFeature(plot, 0, kind);
}
session.RenderFrame();
const int iterations = 2000;
double totalMicroseconds = 0.0;
LARGE_INTEGER frequency = {};
LARGE_INTEGER start = {};
LARGE_INTEGER stop = {};
QueryPerformanceFrequency(&frequency);
QueryPerformanceCounter(&start);
float sink = 0.0f;
for (int iteration = 0; iteration < iterations; ++iteration)
{
for (const EdgePanelKind kind : kinds)
{
const PlotView::EdgePanelLayout layout = plot.EdgePanelLayoutFor(0, kind);
sink += layout.panel.Width + layout.title.Height;
}
}
QueryPerformanceCounter(&stop);
totalMicroseconds = static_cast<double>(stop.QuadPart - start.QuadPart) * 1000000.0 /
static_cast<double>(frequency.QuadPart);
const double perCall =
totalMicroseconds / static_cast<double>(iterations * static_cast<int>(sizeof(kinds) / sizeof(kinds[0])));
EmitLine("[perf] EdgePanelLayoutFor: %.3f us/call (x%d calls) sink=%.1f\n", perCall,
iterations * static_cast<int>(sizeof(kinds) / sizeof(kinds[0])), sink);
Expect(perCall < 200.0, "panel layout below 200us per call");
// 3) 边沿统计查询:**多信号**时的"状态查找"成本
// (第 33 轮报告建议把 `m_edgeScans` 的线性扫描改哈希索引,改的就是这条路径)
const DataSet& dataSet = app.m_dataSet;
struct QueryTarget
{
const SignalData* signal = nullptr;
const TimeAxis* axis = nullptr;
double threshold = 0.0;
};
std::vector<QueryTarget> targets;
for (int groupIndex = 0; groupIndex < static_cast<int>(dataSet.GroupCount()); ++groupIndex)
{
const SignalGroup* group = dataSet.GroupAt(groupIndex);
if (group == nullptr)
{
continue;
}
for (const SignalData& signal : group->signals)
{
if (signal.values.size() < 2)
{
continue;
}
double low = 0.0;
double high = 1.0;
FiniteRange(signal.values, low, high);
QueryTarget target;
target.signal = &signal;
target.axis = &group->AxisOf(signal);
target.threshold = (low + high) * 0.5;
targets.push_back(target);
if (targets.size() >= 200)
{
break;
}
}
if (targets.size() >= 200)
{
break;
}
}
if (!targets.empty())
{
// 预热:把所有累积状态都建起来并扫完(否则测到的是"扫描成本"而不是"查找成本")
for (int guard = 0; guard < 400; ++guard)
{
bool allComplete = true;
for (const QueryTarget& target : targets)
{
std::size_t r = 0;
std::size_t f = 0;
bool complete = false;
plot.EdgeCountsFor(*target.axis, *target.signal, target.signal->style,
target.threshold, false, 0.0, 0.0, r, f, complete);
if (!complete)
{
allComplete = false;
}
}
if (allComplete)
{
break;
}
session.RenderFrame();
}
EmitLine("[perf] edge states=%d targets=%d\n", static_cast<int>(plot.m_edgeScans.size()),
static_cast<int>(targets.size()));
const int rounds = 20;
QueryPerformanceCounter(&start);
for (int round = 0; round < rounds; ++round)
{
for (const QueryTarget& target : targets)
{
std::size_t r = 0;
std::size_t f = 0;
bool complete = false;
plot.EdgeCountsFor(*target.axis, *target.signal, target.signal->style,
target.threshold, false, 0.0, 0.0, r, f, complete);
sink += static_cast<float>(r + f);
}
}
QueryPerformanceCounter(&stop);
const double total = static_cast<double>(stop.QuadPart - start.QuadPart) * 1000000.0 /
static_cast<double>(frequency.QuadPart);
const double perQuery = total / (static_cast<double>(rounds) *
static_cast<double>(targets.size()));
EmitLine("[perf] EdgeCountsFor sweep: %.3f us/query (%d targets x %d rounds) sink=%.1f\n",
perQuery, static_cast<int>(targets.size()), rounds, sink);
Expect(perQuery < 20.0, "edge count query below 20us with 200 states");
}
if (options.savePng)
{
Expect(SavePng(session.LastFrame(), Join(options.outDir, "perf.png")), "save perf.png");
}
return CheckFailed();
}
} // namespace
const std::vector<CheckDescriptor>& AvailableChecks()
{
static const std::vector<CheckDescriptor> checks = {
{"smoke", "offscreen init + load + render + determinism + save png", CheckSmoke},
{"frames", "capture consecutive frames while moving mouse (png + hash + timing)", CheckFrames},
{"panels", "all 7 experiment panels: geometry invariants + pixel baseline A/B", CheckPanels},
{"edgestats", "edge counts vs independent reference implementation", CheckEdgeStats},
{"perf", "render / panel layout / edge query timings", CheckPerf},
};
return checks;
}
const CheckDescriptor* FindCheck(const std::string& name)
{
for (const CheckDescriptor& descriptor : AvailableChecks())
{
if (name == descriptor.name)
{
return &descriptor;
}
}
return nullptr;
}
int& CheckTotal()
{
static int total = 0;
return total;
}
int& CheckFailed()
{
static int failed = 0;
return failed;
}
void Expect(const bool ok, const std::string& what)
{
++CheckTotal();
if (!ok)
{
++CheckFailed();
EmitLine(" [FAIL] %s\n", what.c_str());
}
}
void ExpectEqual(const long long actual, const long long expected, const std::string& what)
{
++CheckTotal();
if (actual != expected)
{
++CheckFailed();
EmitLine(" [FAIL] %s: actual=%lld expected=%lld\n", what.c_str(), actual, expected);
}
}
void ExpectNear(const double actual, const double expected, const double tolerance,
const std::string& what)
{
const double delta = (actual > expected) ? (actual - expected) : (expected - actual);
++CheckTotal();
if (!(delta <= tolerance))
{
++CheckFailed();
EmitLine(" [FAIL] %s: actual=%.17g expected=%.17g\n", what.c_str(), actual, expected);
}
}
std::string ReadTextFile(const std::string& path)
{
const std::wstring wide = Widen(path);
if (wide.empty())
{
return std::string();
}
HANDLE handle = CreateFileW(wide.c_str(), GENERIC_READ, FILE_SHARE_READ, nullptr, OPEN_EXISTING,
FILE_ATTRIBUTE_NORMAL, nullptr);
if (handle == INVALID_HANDLE_VALUE)
{
return std::string();
}
LARGE_INTEGER size = {};
if (!GetFileSizeEx(handle, &size) || size.QuadPart <= 0 || size.QuadPart > (1ll << 26))
{
CloseHandle(handle);
return std::string();
}
std::string content(static_cast<std::size_t>(size.QuadPart), '\0');
DWORD read = 0;
const BOOL ok = ReadFile(handle, &content[0], static_cast<DWORD>(content.size()), &read, nullptr);
CloseHandle(handle);
if (!ok)
{
return std::string();
}
content.resize(read);
return content;
}
bool WriteTextFile(const std::string& path, const std::string& content)
{
EnsureDirectory(DirectoryOf(path));
const std::wstring wide = Widen(path);
if (wide.empty())
{
return false;
}
HANDLE handle = CreateFileW(wide.c_str(), GENERIC_WRITE, 0, nullptr, CREATE_ALWAYS,
FILE_ATTRIBUTE_NORMAL, nullptr);
if (handle == INVALID_HANDLE_VALUE)
{
return false;
}
DWORD written = 0;
const BOOL ok = WriteFile(handle, content.data(), static_cast<DWORD>(content.size()), &written,
nullptr);
CloseHandle(handle);
return ok != FALSE && written == content.size();
}
} // namespace tools
} // namespace jdi