.mat 改为流式解码(内存映射 + 逐 chunk 填充);MF4 支持 ##DL/##DZ/##HL;光标数据点被时间面板遮住时改三角

1) .mat(BIG_MAT.mat 1.29GB / 5 信号 x 16,142,471 采样)
   旧链路同时驻留三份内存:整文件 fread 进 vector、整个数据集解压成字节数组、再 memcpy 成
   double;外加 streams 缓存与每条信号各一份时间列拷贝。实测私有内存 +1233.9MB、117s。
   现改为:
   - MatHdf5::Open 用 CreateFileW + CreateFileMappingW + MapViewOfFile(MappedBytes 只读视图,
     解析代码零改动;虚拟地址不等于物理内存,未访问页不驻留);
   - 新增 ReadDatasetInfo(只读元信息)+ StreamDataset(ByteSink)(连续布局 4MB 切片、
     分块布局逐 chunk inflate)+ ReadDoubleStream(SDI #data# 字节流按 8 字节一组直接写 double,
     7 字节尾巴处理非 8 对齐 chunk);ReadNumbers 改为流式转换;
   - MatLoader 先收集信号清单,再按(分组,时间流)合并为一个 TableBlock:时间列按内容去重、
     数据列直接 move;进度改成 0-0.02 映射 / 0.02-0.98 逐流解码 / 1.0。
   结果:117.3s -> 12.5s,私有内存 +1233.9MB -> +740.4MB(最终列数据 738.9MB)。
   关键性能修正:SDI 的 #data# 是 dims=[N]、chunk=[N,1],归一化后每行只有 1 字节;旧的逐行回调
   (且每行分配 coordinate 向量)只有 11MB/s。把目标连续的行合并成一次回调后约 110MB/s。
   顺带把 inflate 的 Huffman 解码改成 9 位快查表,并抽到新的 Deflate.hpp/.cpp 供 MF4 复用。

2) MDF4 数据块
   Problem-MF4.mf4 的第二个数据组把记录写在 ##DL(数据列表,两个 ##DT:131040+504 字节)
   里,旧实现只认 ##DT,整组被跳过,于是 8 个信号只读出 4 个。
   新增 CollectDataSegments(递归展开 ##DL / ##HL,叶子识别 ##DT / ##DV / ##DZ)与
   ReadRecordSegments(跨段拼接、压缩段先 inflate、非压缩段仍按 4MB 分块读),
   压缩段的记录长度改用 max(cg_record_id_len*8, 各通道 bitOffset+bitCount) 推算(载荷是压缩长度)。
   实测 8 个信号:gVP_WindowLearnSt[0] 94 行、gVR_WinOpenReq[0] 9396 行,时间列单调。
   ##SD(已排序数据)仍未实现,这类数据组会被跳过。

   另按要求只做检查、不改代码:该文件中 gVP_WindowLearnSt[0] 与 gVR_WinOpenReq[0] 都存在
   值枚举文本(##CC,cc_type=7;links[4+i] 对应 cc_val[i]),清单已写入 Documents/agent_mem.md。

3) 光标数据点标记
   光标的时间读数与差值胶囊绘制在绘图区底部、且在取值标注之后绘制,靠近底部的数据点圆点
   会被完全盖住。新增 MarkerCoveredByCursorLabel(圆心落入面板或重叠面积达到半个圆)
   与 CoverHiddenMarker(改成向下的三角、锚点放到面板上沿上方),在原始信号与派生曲线胶囊
   两处调用,语义与“数据点在视界下方”一致。

验证(全部离屏:SW_HIDE 窗口 + 只注入合成消息 + BELOW_NORMAL,不占用鼠标键盘屏幕):
- 新增 .preflight/round4check 34/34;A/B:临时禁用 ##DL 支持 => 2 项如实失败;
  临时禁用 CoverHiddenMarker => 3 项如实失败(含像素断言为 0);
- 回归:.preflight/round3check 31/31、editcheck 20/20、check 137/137;
- make release / make debug 零警告;Build/Release/Target.exe 785,920 字节(< 800KB)。
This commit is contained in:
DeepSeek V4.1 Flash committed 2026-10-19 20:13:17 +08:00
1 parent be76f523f3
commit 75f0fafd41
10 files changed
+1677 -795

No files matched your search

+196 -106
View File
@@ -8,6 +8,7 @@
#include "MatH5.hpp"
#include <algorithm>
#include <cstdint>
#include <cstring>
#include <map>
#include <set>
@@ -66,21 +67,11 @@ namespace jdi
{
return false;
}
MatHdf5::Dataset dataset;
if (!file.ReadDataset(data, dataset) || !dataset.ok)
{
return false;
}
// ⚠️ SDI 的 `#data#` 是**字节流**(`uint8` 数据集,长度 = 采样数 × 8),
// 而不是 double 数据集:必须按 8 字节一组重新解释成 double(`#sampleSize#` 可印证)。
if (dataset.data.size() % sizeof(double) != 0)
{
return false;
}
const std::size_t count = dataset.data.size() / sizeof(double);
out.resize(count);
std::memcpy(out.data(), dataset.data.data(), count * sizeof(double));
return true;
// 而不是 double 数据集:必须按 8 字节一组重新解释成 double。
// `ReadDoubleStream` 会逐 chunk 解压并**直接写入** `out`:
// 不生成“整个数据集的未压缩字节”,也不做第二次拷贝(GB 级文件的关键)。
return file.ReadDoubleStream(data, out) && !out.empty();
}
/** @brief 解析 `#refs#` 里的一个元素(信号 / 子数据集)。 */
@@ -260,53 +251,19 @@ namespace jdi
stack.push_back(std::move(pending));
}
std::map<std::wstring, std::size_t> groupIndex;
std::vector<std::vector<double>> streams; // 流编号 → 样本
std::vector<bool> streamCached;
// 进度:`#sigstream#` 的总流数,用来把"解码样本流"这段映射成阶段内 0-1。
std::size_t streamTotal = 0;
for (const auto& member : file.Members(sigstream))
// ⚠️ **先收集信号清单、再解码**(用户 2026-10-19 要求:边解析边填充):
// * 旧实现把**每条**信号的时间列和时间流各拷一份(`streams` 缓存 + `AppendSignal`
// 里的整份复制)⇒ GB 级文件会出现“文件 + 解压字节 + 每条信号两份 double”叠加;
// * 现在按「分组 + 时间流」合并成一个 `TableBlock`:时间列只存一份、信号列直接
// `std::move` 进数据模型,且每条数据流解码完立刻进入最终缓冲。
struct PendingSignal
{
if (!member.first.empty() && member.first[0] != '#')
{
++streamTotal;
}
}
std::size_t streamDone = 0;
const auto streamFor = [&](int index) -> const std::vector<double>*
{
if (index < 0)
{
return nullptr;
}
if (streams.size() <= static_cast<std::size_t>(index))
{
streams.resize(static_cast<std::size_t>(index) + 1);
streamCached.resize(static_cast<std::size_t>(index) + 1, false);
}
if (!streamCached[static_cast<std::size_t>(index)])
{
const std::uint64_t streamObject =
file.Member(sigstream, std::to_string(index));
if (streamObject == 0 || !ReadStream(file, streamObject, streams[static_cast<std::size_t>(index)]))
{
streams[static_cast<std::size_t>(index)].clear();
}
streamCached[static_cast<std::size_t>(index)] = true;
// ⚠️ 必须在这里回报进度:读一个 42MB 的 `.mat` 时,"解码样本流"才是真正的
// 耗时大头(每个流都要解压 + 拷贝);不报进度的话界面会停在原地等
//(用户 2026-10-19 报告:加载 `.mat` 时已加载大小与百分比都不更新)。
++streamDone;
reporter.Report(streamTotal > 0
? static_cast<double>(streamDone) /
static_cast<double>(streamTotal)
: 1.0);
}
return streams[static_cast<std::size_t>(index)].empty()
? nullptr
: &streams[static_cast<std::size_t>(index)];
std::wstring groupName;
std::wstring signalName;
int timeStream = -1;
int dataStream = -1;
};
std::vector<PendingSignal> signals;
int unnamedSignals = 0;
while (!stack.empty())
{
@@ -335,68 +292,201 @@ namespace jdi
}
continue;
}
if (!element.isSignal)
if (!element.isSignal || element.dataStream < 0)
{
continue;
}
const std::vector<double>* time = streamFor(element.timeStream);
const std::vector<double>* values = streamFor(element.dataStream);
if (values == nullptr)
{
continue;
}
std::vector<double> timeCopy;
if (time != nullptr && time->size() == values->size())
{
timeCopy = *time;
}
else
{
// 时间流缺失/长度不符:退化为采样序号(秒),保证信号仍可显示。
timeCopy.resize(values->size());
for (std::size_t index = 0; index < values->size(); ++index)
{
timeCopy[index] = static_cast<double>(index);
}
}
std::wstring groupName;
PendingSignal signal;
for (const std::wstring& piece : pending.path)
{
if (!groupName.empty())
if (!signal.groupName.empty())
{
groupName += L"-";
signal.groupName += L"-";
}
groupName += piece;
signal.groupName += piece;
}
if (groupName.empty())
if (signal.groupName.empty())
{
groupName = stem;
signal.groupName = stem;
}
std::wstring signalName = element.name;
if (signalName.empty())
signal.signalName = element.name;
if (signal.signalName.empty())
{
signalName = L"信号" + std::to_wstring(++unnamedSignals);
signal.signalName = L"信号" + std::to_wstring(++unnamedSignals);
}
signal.timeStream = element.timeStream;
signal.dataStream = element.dataStream;
signals.push_back(std::move(signal));
}
std::size_t index = 0;
const auto existing = groupIndex.find(groupName);
// 2) 解码:每个(分组,时间流)一个时间轴块;每条数据流只解码一次并直接进列。
std::map<std::wstring, std::size_t> groupIndex;
std::map<std::pair<std::wstring, int>, std::size_t> blockIndex;
std::size_t decoded = 0;
const auto reportStream = [&]()
{
++decoded;
const double fraction = (signals.empty())
? 1.0
: static_cast<double>(decoded) /
static_cast<double>(signals.size());
return reporter.Report(std::min(1.0, fraction));
};
for (PendingSignal& signal : signals)
{
// 分组(每个分组名对应一个 `LoadedSourceGroup`)。
std::size_t groupSlot = 0;
const auto existing = groupIndex.find(signal.groupName);
if (existing == groupIndex.end())
{
LoadedSourceGroup group;
group.name = groupName;
group.source.sourceName = groupName;
group.name = signal.groupName;
group.source.sourceName = signal.groupName;
groups.push_back(std::move(group));
index = groups.size() - 1;
groupIndex.emplace(groupName, index);
groupSlot = groups.size() - 1;
groupIndex.emplace(signal.groupName, groupSlot);
}
else
{
index = existing->second;
groupSlot = existing->second;
}
LoadedSource& source = groups[groupSlot].source;
// 数据列:直接解码到最终 vector(不再经过 `Dataset.data` 的整块字节)。
std::vector<double> values;
const std::uint64_t dataObject = file.Member(sigstream, std::to_string(signal.dataStream));
if (dataObject == 0 || !ReadStream(file, dataObject, values) || values.empty())
{
if (!reportStream())
{
return false;
}
continue;
}
// 找(或建)该信号所属的时间轴块。
const std::pair<std::wstring, int> key{signal.groupName, signal.timeStream};
std::size_t blockSlot = 0;
const auto blockFound = blockIndex.find(key);
if (blockFound == blockIndex.end())
{
TableBlock block;
block.timeHeader = L"time";
std::vector<double> time;
bool haveTime = false;
if (signal.timeStream >= 0)
{
const std::uint64_t timeObject =
file.Member(sigstream, std::to_string(signal.timeStream));
haveTime = timeObject != 0 && ReadStream(file, timeObject, time) &&
!time.empty();
}
if (!haveTime)
{
// 时间流缺失:退化为采样序号(秒),保证信号仍可显示(长度在下面裁剪)。
time.resize(values.size());
for (std::size_t index = 0; index < values.size(); ++index)
{
time[index] = static_cast<double>(index);
}
}
// ⚠️ **时间轴按内容去重**:SDI 里每条 timeseries 都有自己的时间流,
// 但内容通常完全一致。若同一分组里已经有一模一样的时间列,就直接复用它
//(否则每条信号各存一份时间轴:1.29GB / 5 信号的文件会多占 ~500MB)。
std::size_t reused = SIZE_MAX;
for (std::size_t index = 0; index < source.blocks.size(); ++index)
{
const std::vector<double>& existingTime = source.blocks[index].table.columns[0];
if (existingTime.size() != time.size())
{
continue;
}
if (time.empty() ||
std::memcmp(existingTime.data(), time.data(),
time.size() * sizeof(double)) == 0)
{
reused = index;
break;
}
}
if (reused != SIZE_MAX)
{
blockIndex.emplace(key, reused);
blockSlot = reused;
}
else
{
block.table.headers.push_back(block.timeHeader);
block.table.columns.push_back(std::move(time));
source.blocks.push_back(std::move(block));
blockSlot = source.blocks.size() - 1;
blockIndex.emplace(key, blockSlot);
}
}
else
{
blockSlot = blockFound->second;
}
TableBlock& block = source.blocks[blockSlot];
// 时间列长度与信号不一致(损坏 / 异构)⇒ 单独成块,用采样序号当时间,
// 保持“每条信号都能显示”的既有语义,而不是把数据截掉或错位。
if (block.table.columns[0].size() != values.size())
{
TableBlock own;
own.timeHeader = L"time";
std::vector<double> time(values.size());
for (std::size_t index = 0; index < values.size(); ++index)
{
time[index] = static_cast<double>(index);
}
own.table.headers.push_back(own.timeHeader);
own.table.columns.push_back(std::move(time));
own.table.headers.push_back(signal.signalName);
own.table.columns.push_back(std::move(values));
source.blocks.push_back(std::move(own));
if (!reportStream())
{
return false;
}
continue;
}
block.table.headers.push_back(signal.signalName);
block.table.columns.push_back(std::move(values));
if (!reportStream())
{
return false;
}
}
// 3) 统一行数:同一块内取各列最小长度(时间流与数据流长度不一致时按最小裁剪)。
for (LoadedSourceGroup& group : groups)
{
for (TableBlock& block : group.source.blocks)
{
std::size_t rows = SIZE_MAX;
for (const std::vector<double>& column : block.table.columns)
{
rows = std::min(rows, column.size());
}
if (rows == SIZE_MAX)
{
rows = 0;
}
for (std::vector<double>& column : block.table.columns)
{
if (column.size() > rows)
{
column.resize(rows);
}
}
block.table.rowCount = rows;
}
AppendSignal(groups[index].source, signalName, std::move(timeCopy),
std::vector<double>(*values));
}
if (groups.empty())
@@ -475,17 +565,17 @@ namespace jdi
MatHdf5 file;
// ------------------------------------------------------------------
// 进度拆成三段(用户 2026-10-19 报告:加载 `.mat` 时已加载大小与百分比都不更新,
// 界面只是原地等待):
// 0.00 – 0.55 读文件字节(遮罩层显示"已加载 / 总大小")
// 0.55 – 0.98 解码样本流(每个流解码后推进,含解压与拷贝)
// 进度拆成三段:
// 0.00 – 0.02 映射文件(内存映射,几乎瞬时;遮罩层仍能看到“已加载 = 总大小”)
// 0.02 – 0.98 逐条解码样本流(每个流解压后直接填进数据模型,含解压开销)
// 0.98 – 1.00 收尾
// 读文件阶段把 `hasBytes` 置真;之后字节数已满,只推进百分比。
// ⚠️ 旧实现把 0–0.55 全给了“读文件字节”(因为旧实现真的整文件读入);
// 现在改成内存映射后那一段几乎没有耗时,若不调整权重,进度条会“瞬间跳到 55% 再慢慢走”。
// ------------------------------------------------------------------
ProgressReporter readReporter;
readReporter.sink = &progress;
readReporter.low = 0.0;
readReporter.high = 0.55;
readReporter.high = 0.02;
const MatHdf5::OpenProgress openProgress =
[&readReporter](std::uint64_t bytesRead, std::uint64_t totalBytes) -> bool
{
@@ -501,7 +591,7 @@ namespace jdi
}
ProgressReporter parseReporter;
parseReporter.sink = &progress;
parseReporter.low = 0.55;
parseReporter.low = 0.02;
parseReporter.high = 0.98;
parseReporter.read = readReporter.total;
parseReporter.total = 0; // 字节已读完:本阶段只报百分比