Files
Just-DataInspector/Components/Application/Src/ApplicationLoading.cpp
T
DeepSeek V4.1 Flash 34365b3c1c perf(size): 用自写整数格式化替换全部 std::to_wstring / std::to_string
背景:Code Review 报告 §10.3 / §11.5 建议「替换 std::to_wstring / std::to_string,
可去掉 gdtoa 链(≈33 KB)」。本轮把它做完,并给出这条建议的**真实边界**。

一、新增 `Utf8Util::FormatInteger*`(手写十进制展开,不经 printf)
* `FormatInteger(long long)` / `FormatIntegerUnsigned(unsigned long long)` /
  `FormatIntegerNarrow(long long)`(后者供 MatLoader 拼 HDF5 成员路径)。
* 语义与 `std::to_wstring` / `std::to_string` 完全一致:`0` → `"0"`、负数带 `-`、
  `LLONG_MIN` 用模 2^64 语义取绝对值(不会溢出)。
* 这样做的直接理由:预编译 libstdc++ 的 `to_string` 内部转调 `__mingw_swprintf`,
  工程里只要留一处,MinGW 的 printf 引擎(`mingw_wpformat.o`)+ gdtoa 就必然被链入。

二、全仓替换(76 处 `to_wstring` + 2 处 `to_string`,20 个文件)
* `MatLoader.cpp` 的 `TrimNumber` 整数分支也改为 `FormatInteger`(它曾是产物里
  **最后一个**引用 MinGW printf 的调用点)。
* 需要的地方补 `#include "Utf8Util.hpp"`(仅 `MatLoader.cpp` 一个新 include,其余由
  `PlotViewInternal` / `SignalPanelInternal` / `ApplicationInternal` / `DataModelInternal`
  已经包含)。

三、⚠️ 实测结论:gdtoa **去不掉**(把这件事彻底查清并写进 Makefile 注释)
* 只替换 `to_wstring`/`to_string`:**−1,024 B**(787,968 → 786,944)。
* 再叠加 `-D__USE_MINGW_ANSI_STDIO=0`(把 `swprintf` 让给系统 UCRT):**0 字节**。
* 根因(定位到文件与行):libstdc++ 的
  `lib/gcc/.../include/c++/x86_64-w64-mingw32/bits/os_defines.h:48-51` **无条件**执行
  `#undef __USE_MINGW_ANSI_STDIO` + `#define __USE_MINGW_ANSI_STDIO 1`
  (注释:libstdc++ depends on POSIX-definitions of those functions)。
  `g++ -E -dM` 实测最终值恒为 1 ⇒ **任何 C++ TU 的 `swprintf` / `strtod` 都是 MinGW 版本**,
  命令行无法覆盖。
* 要真省这 33 KB 只能自己实现「浮点 → 十进制(正确舍入)」与「浮点解析的
  十六进制/inf/nan 分支」,等价于重写 gdtoa。**判定:不做**(风险 >> 收益)。
* 该开关已回退,Makefile 里保留一段说明,避免后续接手者重复这两次实验。

四、验证
* 新增单元测试 `integer_replacement_matches_std`:12 个有符号样本(含 LLONG_MIN/MAX)
  + 5 个无符号样本(含 ULLONG_MAX),与 `std::to_wstring` **逐字符对照**,
  并校验窄/宽版本内容一致。
* `UnitTests`:`checks=260 failed=0 cases=19`(新增 41 条断言)。
* `OffscreenChecks -all`:6 个检查全 PASS,`checks=87 failed=0`。
* `panels -mode compare`:7 个面板像素哈希与几何**逐项不变**
  (整数文本从 `to_wstring` 换到手写实现后,渲染结果零差异 —— 这是最强的语义证据)。
* release 全量零 error / 零 warning;产物 787,968 → **786,944 B**。
2026-11-04 14:31:02 +08:00

292 lines
9.8 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.
// Application 实现分片 —— 本文件只包含 Application 的**成员函数定义**(纯代码搬移,行为与拆分前完全一致)。
//
// 共用的常量与自由函数在 ApplicationInternal.hpp(命名空间 `appdetail`,全部 inline)。
// 分片按“职责”划分:核心(状态/布局/几何)、渲染、交互与编辑,见各文件名。
#include "ApplicationInternal.hpp"
namespace jdi
{
bool Application::LoadFile(const std::wstring& path)
{
if (path.empty())
{
return false;
}
// 大文件在工作线程解析并构建,主线程播放加载过渡动画,界面不再整块卡住。
std::vector<SignalGroup> loaded;
std::wstring error;
if (!LoadFileAsync(path, loaded, error))
{
if (!error.empty())
{
SetStatusMessage(L"解析失败:" + error, true);
}
InvalidateRect(m_hwnd, nullptr, FALSE);
return false;
}
if (loaded.empty())
{
SetStatusMessage(L"解析失败:数据为空。", true);
InvalidateRect(m_hwnd, nullptr, FALSE);
return false;
}
// 插入新分组会移动 SignalGroup/SignalData 的存储位置,必须先暂停后台派生曲线计算,
// 否则后台线程可能正在读取即将被移动的采样数据。
m_plot.PauseDerivedAsync();
int lastGroupIndex = -1;
for (SignalGroup& group : loaded)
{
int groupIndex = -1;
// 此处只做一次指针/缓冲移动,不拷贝任何采样数据,因此不会造成可见卡顿。
if (!m_dataSet.AddGroup(std::move(group), groupIndex))
{
continue;
}
lastGroupIndex = groupIndex;
}
if (lastGroupIndex < 0)
{
SetStatusMessage(L"解析失败:数据为空。", true);
InvalidateRect(m_hwnd, nullptr, FALSE);
return false;
}
// 新分组的信号在全部标签页中默认均为未勾选状态,由用户自行决定查看内容。
SyncTabsToDataSet();
UpdateWindowTitle();
const SignalGroup* loadedGroup = m_dataSet.GroupAt(lastGroupIndex);
if (loadedGroup != nullptr)
{
// 行数取各时间轴的**最大值**(多时间轴文件的各轴长度可能不同)。
std::size_t rowCount = 0;
bool anyNonMonotonic = false;
std::wstring timeNames;
for (const TimeAxis& axis : loadedGroup->axes)
{
rowCount = std::max(rowCount, axis.Count());
anyNonMonotonic = anyNonMonotonic || !axis.monotonic;
if (!timeNames.empty())
{
timeNames += L"、";
}
timeNames += axis.name;
}
std::wstring message =
L"已加载 组" + FormatInteger(lastGroupIndex + 1) + L"|" + loadedGroup->name +
L"(" + FormatCount(static_cast<unsigned long long>(rowCount)) + L" 行采样," +
FormatInteger(loadedGroup->signals.size()) + L" 条信号," +
FormatInteger(loadedGroup->axes.size()) + L" 条时间轴:" + timeNames + L")";
if (anyNonMonotonic)
{
message += L" 提示:时间列非单调递增,光标取值使用最近邻策略。";
}
SetStatusMessage(message, false);
}
return true;
}
bool Application::LoadFileAsync(const std::wstring& path, std::vector<SignalGroup>& groups,
std::wstring& error)
{
error.clear();
AsyncLoadState state;
const std::size_t paletteBase = m_dataSet.SignalCount();
m_loading = true;
m_loadingProgress = 0.0f;
m_loadingBytesRead = 0;
m_loadingTotalBytes = 0;
// ⚠️ 告知绘图区“正在加载”:暂停边沿统计推进与统计面板绘制。
// 本函数的主循环会不断 `UpdateWindow` 触发整帧绘制,若面板继续扫统计 + 重绘上百行,
// 加载进度会被拖得很慢(用户报告“统计没算完就无法开始加载文件”)。
m_plot.SetLoadingBusy(true);
const std::size_t slashPos = path.find_last_of(L"\\/");
m_loadingText = (slashPos == std::wstring::npos) ? path : path.substr(slashPos + 1);
std::thread worker(RunAsyncLoad, &state, path, paletteBase);
// 主线程循环:抽送消息 + 刷新动画,直到后台完成或用户取消。
while (!state.done.load(std::memory_order_acquire))
{
MSG message = {};
while (PeekMessageW(&message, nullptr, 0, 0, PM_REMOVE))
{
if (message.message == WM_QUIT)
{
// 退出请求:让后台停下并重新投递 WM_QUIT,由主循环正常收尾。
state.cancelled.store(true, std::memory_order_relaxed);
PostQuitMessage(static_cast<int>(message.wParam));
break;
}
// 加载期间屏蔽鼠标与其它按键,避免重入加载或误改数据;
// 保留 Esc 作为取消入口。
if (message.message >= WM_MOUSEFIRST && message.message <= WM_MOUSELAST)
{
continue;
}
if (message.message == WM_KEYDOWN && message.wParam == VK_ESCAPE)
{
state.cancelled.store(true, std::memory_order_relaxed);
continue;
}
if (message.message >= WM_KEYFIRST && message.message <= WM_KEYLAST)
{
continue;
}
TranslateMessage(&message);
DispatchMessageW(&message);
}
m_loadingProgress = state.progress.load(std::memory_order_relaxed);
m_loadingBytesRead = state.bytesRead.load(std::memory_order_relaxed);
m_loadingTotalBytes = state.totalBytes.load(std::memory_order_relaxed);
InvalidateRect(m_hwnd, nullptr, FALSE);
UpdateWindow(m_hwnd);
Sleep(15);
}
m_loadingProgress = 1.0f;
m_loadingBytesRead = m_loadingTotalBytes;
worker.join();
m_loading = false;
m_plot.SetLoadingBusy(false);
if (state.cancelled.load(std::memory_order_relaxed))
{
// 用户主动取消:不弹错误框(与 Esc 取消缩放选区的静默语义一致)。
InvalidateRect(m_hwnd, nullptr, TRUE);
return false;
}
if (!state.ok)
{
error = state.error;
return false;
}
groups = std::move(state.groups);
return true;
}
void Application::PaintLoadingOverlay(gd::Graphics& g, int width, int height)
{
if (!m_loading)
{
return;
}
// 半透明遮罩压暗整个界面,形成明确的"正在加载"状态。
UiFillRect(g, gd::RectF(0.0f, 0.0f, static_cast<float>(width), static_cast<float>(height)),
0x66000000u);
const float cardWidth = std::min(420.0f * m_scale, static_cast<float>(width) - 40.0f * m_scale);
const float cardHeight = 132.0f * m_scale;
const gd::RectF card((static_cast<float>(width) - cardWidth) * 0.5f,
(static_cast<float>(height) - cardHeight) * 0.5f, cardWidth, cardHeight);
UiFillRoundRect(g, card, 8.0f * m_scale, m_theme.cardBg);
UiDrawRoundRect(g, card, 8.0f * m_scale, m_theme.borderStrong, 1.0f);
// 旋转弧线:用时间驱动的起始角度形成持续的加载动画。
const float spinnerRadius = 11.0f * m_scale;
const gd::RectF spinner(card.X + 26.0f * m_scale, card.Y + 30.0f * m_scale - spinnerRadius,
spinnerRadius * 2.0f, spinnerRadius * 2.0f);
m_loadingSpin += 12.0f;
if (m_loadingSpin >= 360.0f)
{
m_loadingSpin -= 360.0f;
}
gd::Pen trackPen(ToGdiColor(m_theme.border), 3.0f * m_scale);
gd::Pen spinPen(ToGdiColor(m_theme.accent), 3.0f * m_scale);
g.DrawArc(&trackPen, spinner, 0.0f, 359.0f);
g.DrawArc(&spinPen, spinner, m_loadingSpin, 110.0f);
const float textLeft = card.X + 52.0f * m_scale;
const float textWidth = cardWidth - 52.0f * m_scale - 20.0f * m_scale;
UiText(g, L"正在加载数据…", m_fonts.uiBold,
gd::RectF(textLeft, card.Y + 18.0f * m_scale, textWidth, 22.0f * m_scale), m_theme.text,
TextAlign::LEFT);
// 超长文件名用省略号截断,避免撑破卡片。
const std::wstring nameText =
UiEllipsize(m_loadingText, m_fonts.uiSmall, textWidth);
UiText(g, nameText, m_fonts.uiSmall,
gd::RectF(textLeft, card.Y + 42.0f * m_scale, textWidth, 20.0f * m_scale),
m_theme.textMuted, TextAlign::LEFT);
// 进度条(复用样式区滑块以外的简单圆角矩形,填充宽度严格按比例)。
const gd::RectF track(card.X + 26.0f * m_scale, card.Y + 76.0f * m_scale,
cardWidth - 52.0f * m_scale, 8.0f * m_scale);
UiFillRoundRect(g, track, track.Height * 0.5f, m_theme.pressed);
float ratio = m_loadingProgress;
ratio = std::max(0.0f, std::min(ratio, 1.0f));
const float fillWidth = track.Width * ratio;
if (fillWidth > 1.0f)
{
UiFillRoundRect(g, gd::RectF(track.X, track.Y, fillWidth, track.Height), track.Height * 0.5f,
m_theme.accent);
}
wchar_t buffer[64];
swprintf(buffer, sizeof(buffer) / sizeof(buffer[0]), L"%.2f%%", ratio * 100.0);
// 已读取 / 总大小:两者都只显示数字与自适应单位(GB / MB / KB),均保留两位小数;
// 百分比同样是两位小数。总大小未知(如列重组阶段)时只显示百分比。
{
std::wstring statusText = buffer;
if (m_loadingTotalBytes > 0)
{
statusText = FormatByteSize(m_loadingBytesRead) + L" / " +
FormatByteSize(m_loadingTotalBytes) + L" " + buffer;
}
UiText(g, statusText, m_fonts.monoSmall,
gd::RectF(track.X, card.Y + 92.0f * m_scale, track.Width, 20.0f * m_scale),
m_theme.text, TextAlign::LEFT);
}
UiText(g, L"按 Esc 取消", m_fonts.uiSmall,
gd::RectF(track.X, card.Y + 92.0f * m_scale, track.Width, 20.0f * m_scale),
m_theme.textMuted, TextAlign::RIGHT);
}
void Application::LoadCommandLineFiles()
{
int argumentCount = 0;
LPWSTR* arguments = CommandLineToArgvW(GetCommandLineW(), &argumentCount);
if (arguments == nullptr)
{
return;
}
int loaded = 0;
for (int index = 1; index < argumentCount; ++index)
{
const std::wstring argument = arguments[index];
if (argument.empty() || argument[0] == L'-' || argument[0] == L'/')
{
continue;
}
if (LoadFile(argument))
{
++loaded;
}
}
LocalFree(arguments);
if (loaded > 0)
{
SetStatusMessage(L"已加载 " + FormatInteger(loaded) +
L" 个文件,请在左侧勾选需要查看的信号。",
false);
}
}
} // namespace jdi