MF4 补 ##SD/##SI/VLSD + 信号枚举文本 enumText + .mat 改随机读(流式进度)

1) 修复上轮记录的两个已知问题
   - MatLoader: LoadPlainObject 不再为拿 dims 调 ReadDataset(整块解码第二次),
     改用 ReadDatasetInfo;普通 v7.3 大矩阵的时间/内存不再翻倍。
   - MdfFile::TextBlock 注释与实现对齐:真的支持 ##MD(UTF-8 XML,无长度前缀)。

2) 信号枚举信息(以 TestData/NEW-MF4.mf4 为目标)
   - 新增 SignalEnumText(DataTable.hpp):values[] + texts[] + Empty()/Find(),
     命名用 enumText —— 它描述"取值代表哪个状态",不是单位。
   - DataTable::columnEnumText 与 columns 同下标;SignalData::enumText 由
     BuildSignalGroup 移动赋值;其它格式(CSV/.mat)保持为空。
   - MF4 cc_type = 7:cc_val[i](F64,数据段 +24)↔ cc_ref[4 + i](##TX/##MD 文本),
     最后一个引用是默认文本,不使用。
   - 界面只在**光标取值标注**追加 " = <枚举文本>"(等号两侧各一个空格、无尖括号),
     查表用采样原值(ApplyDrawTransform 之前);常规状态显示一个像素都不变。
   - 实测:NEW-MF4.mf4 8 信号 / 4 条带枚举(32+5+32+5),MF4-Record.MF4 另有 2 种(12/32)。

3) MF4 支持范围
   - ##SI 采样索引:只有索引 ≥2 条且全部 < 周期数才生效(逐记录有效性位图)。
     实测部分工具给每个通道挂只含常量 513/4 的 ##SI,必须忽略,否则整列变 NaN。
   - ##SD + VLSD(cn_type == 1):##SD 数据段是「UINT32 长度 + 字节」样本序列,
     记录里放 64 位偏移;AppendSignalData 递归展开 SD/DZ/DL/HL(单通道上限 512MB),
     DecodeVlsdValue 先按直接偏移、失败再按 >>1 回退,取样本第一个元素。
   - CN 的 cn_data / md_unit / md_comment 下标按链接总数反推:8 链接(MDF 4.0 风格)
     = 换算[4]/数据[5]/单位[6]/注释[7],9 链接(4.1)= [5]/[6]/[7]/[8]。
     实测 Simulink 导出的“4.10”仍写 8 链接,不能写死 Link(cn,4/5)。
   - 主通道:cn_sync_type 1/2/3(时间/角度/距离)→ 名字 t/time/timestamp/zeit/时间
     (含 t[...])→ 第一个数值通道兜底;cg_flags 第 2 位(远程主通道)跳过 sync 判定。
   - 记录长度优先 cg_data_bytes(CG 数据段 +24);旧实现把 U8(cgData) 当
     cg_record_id_len,其实是 cg_flags 低字节(总线事件位)。
   - 进度:先走 DG 链得到真实组数(不再写死 64),hasBytes = true,bytesRead 取
     MdfFile::ReadAt 累计(夹到文件大小);4MB 分块读取检查取消。
   - ##DZ:数据段前有 28 字节头(original_type/zip_type/param/original_size/zip_size),
     旧实现整段当 zlib 流是错的;只支持纯 deflate(zip_type = 0)。
   - 一个信号都没有时,错误信息附带「布局无法解析 / 缺少时间通道 / 空数据组」计数。

4) .mat:内存映射 → 随机读 + 元数据块缓存(消除 SEH 风险)
   - MatHdf5::Open 改为 CreateFileW(FILE_FLAG_SEQUENTIAL_SCAN) + ReadFile;
     删除 MappedBytes,改为 ReadAt / Cached(16 × 64KB LRU + 上次命中槽)/ Tag /
     ReadData / ReadCString。I/O 失败只会变成 false,不再触发页访问异常。
   - 数据读取:连续布局按 4MB 切片;分块布局按需 + 64KB 对齐。
     固定 4MB 预读在交错存放的分块数据集上会把读盘量放大到 2.2 倍(2874MB/1293MB),
     改后 1301MB。
   - 关键提速:dims=[N]、chunk=[32768,1] 归一化后 rank=2、每行 1 字节,逐行算坐标
     是 12.9 亿次迭代;新增「rank == 2 且 chunkDims[1] == dims[1]」快路径(整块一次
     sink)。BIG_MAT.mat(1.29GB / 5 × 16,142,471):21.5s → 7.9s,私有内存 +741MB。
   - 进度:Open 立刻上报「0 / 总大小」,之后每读一块回调 ReadProgress(bytesRead,total)
     → MatLoader 映射到 0.02–0.98 并单调夹紧(字节进度与逐流解码取较大值),收尾 1.0。
     遮罩层百分比不再一上来就 100%;取消会中止(StreamDataset 返回 false),不留 NaN 空洞。
   - 连续布局数据不足(文件被截断)现在明确失败,不再静默截短。

验证(全部离屏:无可见窗口、不注入鼠标键盘屏幕)
- 新增 .preflight/perf/loadercheck(数据层)32/32:
  NEW-MF4 8 信号 / 4 条枚举 / 枚举文本逐条核对 / 9396 与 94 行;
  MF4-Record 124 信号 / 3 轴 / 4 条枚举;data-sample.mat 4 组 8 信号 / 行数;
  BIG_MAT.mat 5 信号 × 16,142,471 + 进度(total 已知、ratio 单调、bytes 递增、
  起点 ≤0.05、终值 1.0、更新 ≥10 次)。
- 新增 .preflight/perf/enumui(真实 Application + SW_HIDE)12/12:
  只显示 gVR_WinOpenReq[0],光标落在枚举采样点上 —— 标签宽度 265px;
  临时清空 enumText 后 25px;差值 240px 与 " = WinOpenReqAtDrvr_Idle" 的
  UiMeasureTextWidth 完全一致;恢复后宽度回到 265px。
- 回归:datacheck 69/69、matcheck 21/21、matui 15/15(loading bytes 达到总大小)、
  verify 49/49。
- make release / make debug 零警告。Build/Release/TargetCheck.exe = 820,224 字节
  (上轮 785,920,+34KB)。(Target.exe 正在被用户运行的程序占用,链接步骤未覆盖它。)
This commit is contained in:
DeepSeek-V4.1-Flash committed 2026-10-20 01:56:47 +08:00
1 parent 75f0fafd41
commit 181f95cc3a
10 files changed
+1328 -291

No files matched your search

+6
View File
@@ -338,6 +338,12 @@ namespace jdi
}
signal.axisIndex = axisIndex;
// 值 → 文本枚举表(MDF4 `cc_type = 7` 等):与列同下标,缺省为空。
if (column < table.columnEnumText.size())
{
signal.enumText = std::move(table.columnEnumText[column]);
}
signal.values = std::move(table.columns[column]);
const SignalStats stats = ComputeStats(signal.values, signal.coarseMin, signal.coarseMax);
signal.coarseStride = kCoarseStride;
+323 -70
View File
@@ -82,9 +82,212 @@ namespace jdi
}
} // namespace
namespace
{
/** @brief 元数据缓存的块大小与槽数(16 × 64KB = 1MB,足够覆盖对象头/B-tree 热点)。 */
constexpr std::size_t kMetaBlockBytes = 64u * 1024u;
constexpr std::size_t kMetaBlockSlots = 16;
/** @brief 顺序预读窗口上限。 */
constexpr std::size_t kReadAheadBytes = 4u * 1024u * 1024u;
/**
* @brief 数据读取的最小粒度:按请求大小向上对齐读取,命中窗口时直接复用。
*
* ⚠️ 分块数据集的 chunk 在文件里**并非完全顺序**(多个样本流交错写入,
* 每段连续区域约 1.8MB)。实测 `BIG_MAT.mat`:固定预读 4MB 会让每个连续段
* 末尾浪费 2.2MB,总读盘量达到文件大小的 **2.2 倍**(2.87GB / 1.29GB)。
* 改为「按需 + 64KB 对齐」后,读盘量回到文件大小附近。
*/
constexpr std::size_t kReadGranularity = 64u * 1024u;
}
bool MatHdf5::ReadAt(std::uint64_t offset, void* dst, std::size_t bytes) const
{
if (m_fileHandle == nullptr || bytes == 0)
{
return bytes == 0;
}
if (offset > m_size || bytes > m_size - offset)
{
return false;
}
const HANDLE file = static_cast<HANDLE>(m_fileHandle);
LARGE_INTEGER position = {};
position.QuadPart = static_cast<LONGLONG>(offset);
if (!SetFilePointerEx(file, position, nullptr, FILE_BEGIN))
{
return false;
}
std::size_t done = 0;
while (done < bytes)
{
DWORD read = 0;
const DWORD want = static_cast<DWORD>(std::min<std::size_t>(bytes - done, 1u << 30));
if (!ReadFile(file, static_cast<std::uint8_t*>(dst) + done, want, &read, nullptr))
{
return false;
}
if (read == 0)
{
return false; // 文件被截断
}
done += read;
}
m_bytesRead += bytes;
return true;
}
const std::uint8_t* MatHdf5::Cached(std::uint64_t offset, std::size_t bytes) const
{
if (offset >= m_size || bytes > kMetaBlockBytes)
{
return nullptr;
}
if (m_cache.empty())
{
m_cache.resize(kMetaBlockSlots);
}
const std::uint64_t base = offset - (offset % kMetaBlockBytes);
// 快路径:连续读同一块(U16/U32/U64 与 B-tree 遍历几乎都是这种模式)。
{
CacheSlot& slot = m_cache[m_lastSlot];
if (slot.length > 0 && slot.base == base)
{
slot.stamp = ++m_stamp;
const std::uint64_t delta = offset - slot.base;
return (delta + bytes <= slot.length) ? (slot.bytes.data() + delta) : nullptr;
}
}
CacheSlot* victim = nullptr;
for (std::size_t index = 0; index < m_cache.size(); ++index)
{
CacheSlot& slot = m_cache[index];
if (slot.length > 0 && slot.base == base)
{
slot.stamp = ++m_stamp;
m_lastSlot = index;
const std::uint64_t delta = offset - slot.base;
return (delta + bytes <= slot.length) ? (slot.bytes.data() + delta) : nullptr;
}
if (slot.length == 0 || victim == nullptr || slot.stamp < victim->stamp)
{
victim = &slot;
}
}
if (victim == nullptr)
{
return nullptr;
}
// 未命中:整块读入(块可能超出文件末尾,按可用长度裁剪)。
const std::size_t wanted =
static_cast<std::size_t>(std::min<std::uint64_t>(kMetaBlockBytes, m_size - base));
if (bytes > wanted)
{
return nullptr;
}
victim->bytes.resize(wanted);
victim->base = base;
victim->length = static_cast<std::uint32_t>(wanted);
victim->stamp = ++m_stamp;
m_lastSlot = static_cast<std::size_t>(victim - m_cache.data());
if (!ReadAt(base, victim->bytes.data(), wanted))
{
victim->length = 0;
return nullptr;
}
return victim->bytes.data() + (offset - base);
}
bool MatHdf5::Tag(std::uint64_t offset, const char* id) const
{
const std::uint8_t* bytes = Cached(offset, 4);
if (bytes == nullptr)
{
return false;
}
return std::memcmp(bytes, id, 4) == 0;
}
void MatHdf5::ReadCString(std::uint64_t offset, std::string& out) const
{
out.clear();
std::uint64_t position = offset;
while (position < m_size && out.size() < 4096)
{
const std::uint8_t* bytes = Cached(position, 1);
if (bytes == nullptr)
{
return;
}
// 只消费本缓存块内剩余的字节,保证下一次循环拿到的新块仍然有效。
const std::uint64_t blockBase = position - (position % kMetaBlockBytes);
const std::size_t limit =
static_cast<std::size_t>(std::min<std::uint64_t>(kMetaBlockBytes - (position - blockBase),
m_size - position));
std::size_t used = 0;
while (used < limit)
{
if (bytes[used] == '\0')
{
return;
}
out.push_back(static_cast<char>(bytes[used]));
++used;
}
position += used;
}
}
void MatHdf5::DropReadAhead() const
{
m_readAheadValid = false;
m_readAheadLength = 0;
}
const std::uint8_t* MatHdf5::ReadData(std::uint64_t offset, std::size_t bytes) const
{
if (bytes > kReadAheadBytes || offset > m_size || bytes > m_size - offset)
{
return nullptr;
}
if (m_readAheadValid && offset >= m_readAheadBase &&
(offset - m_readAheadBase) + bytes <= m_readAheadLength)
{
return m_readAhead.data() + (offset - m_readAheadBase);
}
// 按需读取 + 64KB 对齐:命中当前窗口就直接复用,否则只读「这次需要的一段」。
// (固定 4MB 预读在交错存放的分块数据集上会浪费 2 倍以上的 I/O。)
const std::size_t requested = std::max<std::size_t>(bytes, kReadGranularity);
std::size_t wanted = static_cast<std::size_t>(std::min<std::uint64_t>(
m_size - offset, std::min<std::size_t>(kReadAheadBytes,
((requested + kReadGranularity - 1) / kReadGranularity) *
kReadGranularity)));
if (m_readAhead.size() < wanted)
{
m_readAhead.resize(wanted);
}
if (!ReadAt(offset, m_readAhead.data(), wanted))
{
DropReadAhead();
return nullptr;
}
m_readAheadBase = offset;
m_readAheadLength = static_cast<std::uint32_t>(wanted);
m_readAheadValid = true;
if (m_readProgress && !m_readProgress(m_bytesRead, m_size))
{
m_readCancelled = true;
DropReadAhead();
return nullptr;
}
return m_readAhead.data();
}
std::uint8_t MatHdf5::U8(std::uint64_t offset) const
{
return (offset < m_data.size()) ? m_data[static_cast<std::size_t>(offset)] : 0;
const std::uint8_t* bytes = Cached(offset, 1);
return (bytes != nullptr) ? *bytes : 0;
}
std::uint16_t MatHdf5::U16(std::uint64_t offset) const
@@ -121,12 +324,13 @@ namespace jdi
error.clear();
Close();
// ⚠️ **内存映射而不是整文件读入**:SDI 导出的 `.mat` 可能是 GB 级,
// “整文件读入 + 分块解码成字节数组 + 再拷成 double”会同时驻留三份内存
//(用户 2026-10-19 报告,并在本轮改为流式填充)。
// 映射只占虚拟地址空间:未被解析到的页不会驻留,系统也可以在内存压力下回收。
// ⚠️ **随机读,不映射**:SDI 导出的 `.mat` 可能是 GB 级,整文件读入会同时驻留
//「文件 + 未压缩字节 + double」三份内存;而内存映射在文件被截断 / 网络共享掉线 /
// 移动介质拔出时会触发 SEH(页访问异常),GCC 下的 `try/catch` 捕获不到 ⇒ 进程直接崩溃。
// 这里改用 `ReadFile` + 元数据块缓存:任何 I/O 失败都只会变成 `false`。
const HANDLE file = CreateFileW(path.c_str(), GENERIC_READ, FILE_SHARE_READ, nullptr,
OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, nullptr);
OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL | FILE_FLAG_SEQUENTIAL_SCAN,
nullptr);
if (file == INVALID_HANDLE_VALUE)
{
error = L"无法打开文件。";
@@ -139,29 +343,14 @@ namespace jdi
error = L"无法读取文件大小。";
return false;
}
const HANDLE mapping = CreateFileMappingW(file, nullptr, PAGE_READONLY, 0, 0, nullptr);
if (mapping == nullptr)
{
CloseHandle(file);
error = L"无法映射文件(请确认文件可读且未被独占)。";
return false;
}
const void* view = MapViewOfFile(mapping, FILE_MAP_READ, 0, 0, 0);
if (view == nullptr)
{
CloseHandle(mapping);
CloseHandle(file);
error = L"无法映射文件视图。";
return false;
}
m_fileHandle = file;
m_mapHandle = mapping;
m_data.pointer = static_cast<const std::uint8_t*>(view);
m_data.length = static_cast<std::size_t>(fileSize.QuadPart);
m_size = static_cast<std::uint64_t>(fileSize.QuadPart);
m_bytesRead = 0;
m_readCancelled = false;
// 字节已全部可访问(映射完成):把“已加载 = 总大小”报给遮罩层,
// 之后的时间主要在“解码样本流”阶段(由 MatLoader 逐流推进)。
if (progress && !progress(m_data.size(), m_data.size()))
// 打开阶段已知道总大小:立即上报一次,遮罩层可以显示「0 / 总大小」,
// 之后的真实进度由 `ReadAt` 累计的已读字节数驱动。
if (progress && !progress(0, m_size))
{
Close();
error.clear(); // 用户取消:不显示错误
@@ -169,17 +358,16 @@ namespace jdi
}
// 超级块:签名可能出现在 0、512、1024…(MATLAB 用 512 字节 user block)。
std::size_t signature = m_data.size();
for (std::size_t index = 0; index + 8 <= m_data.size() && index <= 4096; index += 512)
std::size_t signature = static_cast<std::size_t>(m_size);
for (std::size_t index = 0; index + 8 <= m_size && index <= 4096; index += 512)
{
if (m_data[index] == 0x89 && m_data[index + 1] == 'H' && m_data[index + 2] == 'D' &&
m_data[index + 3] == 'F')
if (Tag(index, "\x89HDF"))
{
signature = index;
break;
}
}
if (signature == m_data.size())
if (signature == m_size)
{
error = L"不是 HDF5(MATLAB 7.3)格式。";
return false;
@@ -200,7 +388,7 @@ namespace jdi
m_base = U64(signature + 12);
m_root = Resolve(U64(signature + 36));
}
if (m_root == 0 || m_root >= m_data.size())
if (m_root == 0 || m_root >= m_size)
{
error = L"HDF5 超级块损坏。";
return false;
@@ -210,31 +398,27 @@ namespace jdi
void MatHdf5::Close()
{
// 先解除视图,再关映射与文件句柄(顺序不能反)。
if (m_data.pointer != nullptr)
{
UnmapViewOfFile(m_data.pointer);
}
if (m_mapHandle != nullptr)
{
CloseHandle(static_cast<HANDLE>(m_mapHandle));
m_mapHandle = nullptr;
}
DropReadAhead();
m_cache.clear();
m_lastSlot = 0;
if (m_fileHandle != nullptr)
{
CloseHandle(static_cast<HANDLE>(m_fileHandle));
m_fileHandle = nullptr;
}
m_data.clear();
m_size = 0;
m_bytesRead = 0;
m_readCancelled = false;
m_base = 0;
m_root = 0;
m_heaps.clear();
m_readProgress = ReadProgress();
}
void MatHdf5::ReadMessages(std::uint64_t address, std::vector<Message>& messages) const
{
messages.clear();
if (address + 8 > m_data.size())
if (address + 8 > m_size)
{
return;
}
@@ -253,18 +437,21 @@ namespace jdi
std::uint64_t offset = blocks[blockIndex].first;
const std::uint64_t end =
(blocks[blockIndex].second > 0) ? (offset + blocks[blockIndex].second) : 0;
while (read < count && offset + 8 <= m_data.size() && (end == 0 || offset + 8 <= end))
while (read < count && offset + 8 <= m_size && (end == 0 || offset + 8 <= end))
{
Message message;
message.type = U16(offset);
const std::uint16_t messageSize = U16(offset + 2);
offset += 8;
if (offset + messageSize > m_data.size())
if (offset + messageSize > m_size)
{
break;
}
message.data.resize(messageSize);
if (messageSize > 0 && !ReadAt(offset, message.data.data(), messageSize))
{
break;
}
message.data.assign(m_data.begin() + static_cast<std::ptrdiff_t>(offset),
m_data.begin() + static_cast<std::ptrdiff_t>(offset + messageSize));
offset += (messageSize + 7u) & ~static_cast<std::uint16_t>(7u);
++read;
if (message.type == kMessageContinuation && message.data.size() >= 16)
@@ -286,9 +473,9 @@ namespace jdi
std::vector<std::pair<std::string, std::uint64_t>>& out) const
{
int guard = 0;
while (node != 0 && node + 24 < m_data.size() && guard++ < 100000)
while (node != 0 && node + 24 < m_size && guard++ < 100000)
{
if (std::memcmp(&m_data[static_cast<std::size_t>(node)], "TREE", 4) != 0)
if (!Tag(node, "TREE"))
{
return;
}
@@ -323,8 +510,7 @@ namespace jdi
void MatHdf5::WalkSymbolNode(std::uint64_t node, std::uint64_t heapData,
std::vector<std::pair<std::string, std::uint64_t>>& out) const
{
if (node == 0 || node + 8 > m_data.size() ||
std::memcmp(&m_data[static_cast<std::size_t>(node)], "SNOD", 4) != 0 || heapData == 0)
if (node == 0 || node + 8 > m_size || !Tag(node, "SNOD") || heapData == 0)
{
return;
}
@@ -332,19 +518,20 @@ namespace jdi
std::uint64_t offset = node + 8;
for (std::uint16_t index = 0; index < count; ++index)
{
if (offset + 40 > m_data.size())
if (offset + 40 > m_size)
{
return;
}
const std::uint64_t nameOffset = U64(offset);
const std::uint64_t objectAddress = Resolve(U64(offset + 8));
offset += 40;
if (objectAddress == 0 || heapData + nameOffset >= m_data.size())
if (objectAddress == 0 || heapData + nameOffset >= m_size)
{
continue;
}
const char* name = reinterpret_cast<const char*>(m_data.data() + heapData + nameOffset);
out.emplace_back(std::string(name), objectAddress);
std::string name;
ReadCString(heapData + nameOffset, name);
out.emplace_back(std::move(name), objectAddress);
}
}
@@ -360,8 +547,7 @@ namespace jdi
const std::uint64_t btree = Resolve(ReadLE(message.data.data(), 8));
const std::uint64_t heap = Resolve(ReadLE(message.data.data() + 8, 8));
std::uint64_t heapData = 0;
if (heap + 32 <= m_data.size() &&
std::memcmp(&m_data[static_cast<std::size_t>(heap)], "HEAP", 4) == 0)
if (heap + 32 <= m_size && Tag(heap, "HEAP"))
{
heapData = Resolve(U64(heap + 24));
bool known = false;
@@ -724,26 +910,45 @@ namespace jdi
return sink(dataset.compact.data(), dataset.compact.size(), 0);
}
// 连续布局:按 4MB 切片直接吐出(文件是映射的,这里只是指针运算,不产生额外缓冲)。
// 连续布局:按 4MB 切片读出(随机读 + 复用缓冲,不产生整文件大小的额外内存)。
if (dataset.layout == LayoutClass::Contiguous)
{
const std::uint64_t total = dataset.ElementCount();
const std::uint64_t totalBytes = total * elementSize;
const std::uint64_t available =
std::min<std::uint64_t>(dataset.dataBytes,
(dataset.dataAddress < m_data.size())
? (m_data.size() - dataset.dataAddress)
: 0);
(dataset.dataAddress < m_size) ? (m_size - dataset.dataAddress)
: 0);
const std::uint64_t bytes = std::min(totalBytes, available);
// ⚠️ 元数据声明了 N 个元素、文件里却不够:明确失败,而不是静默截短
//(静默截短会让曲线“看起来只有一半”,很难诊断)。
if (bytes < totalBytes)
{
return false;
}
std::size_t slice = kStreamSliceBytes - (kStreamSliceBytes % elementSize);
if (slice == 0)
{
slice = elementSize;
}
if (m_slice.size() < slice)
{
m_slice.resize(slice);
}
for (std::uint64_t offset = 0; offset < bytes; offset += slice)
{
const std::size_t count = static_cast<std::size_t>(std::min<std::uint64_t>(slice, bytes - offset));
if (!sink(m_data.data() + dataset.dataAddress + offset, count, offset / elementSize))
const std::size_t count =
static_cast<std::size_t>(std::min<std::uint64_t>(slice, bytes - offset));
if (!ReadAt(dataset.dataAddress + offset, m_slice.data(), count))
{
return false;
}
if (m_readProgress && !m_readProgress(m_bytesRead, m_size))
{
m_readCancelled = true;
return false;
}
if (!sink(m_slice.data(), count, offset / elementSize))
{
return false;
}
@@ -788,8 +993,7 @@ namespace jdi
{
const std::uint64_t node = stack.back();
stack.pop_back();
if (node == 0 || node + 24 > m_data.size() ||
std::memcmp(&m_data[static_cast<std::size_t>(node)], "TREE", 4) != 0)
if (node == 0 || node + 24 > m_size || !Tag(node, "TREE"))
{
continue;
}
@@ -799,7 +1003,7 @@ namespace jdi
std::uint64_t offset = node + 8 + 8 + 8;
for (std::uint16_t index = 0; index < static_cast<std::uint16_t>(entries + 1u); ++index)
{
const std::uint32_t chunkSize = (offset + 4 <= m_data.size()) ? U32(offset) : 0;
const std::uint32_t chunkSize = (offset + 4 <= m_size) ? U32(offset) : 0;
std::vector<std::uint64_t> chunkOffset(rank, 0);
for (std::size_t axis = 0; axis < rank; ++axis)
{
@@ -817,12 +1021,38 @@ namespace jdi
stack.push_back(child);
continue;
}
if (child == 0 || chunkSize == 0 || child + chunkSize > m_data.size())
if (child == 0 || chunkSize == 0 || child + chunkSize > m_size)
{
continue;
}
const std::uint8_t* source = m_data.data() + child;
// 顺序预读:分块数据通常连续存放,逐 chunk 读会退化成大量小系统调用。
const std::uint8_t* source = nullptr;
if (chunkSize <= kReadAheadBytes)
{
source = ReadData(child, chunkSize);
}
if (source == nullptr)
{
if (m_chunk.size() < chunkSize)
{
m_chunk.resize(chunkSize);
}
if (!ReadAt(child, m_chunk.data(), chunkSize))
{
continue;
}
if (m_readProgress && !m_readProgress(m_bytesRead, m_size))
{
m_readCancelled = true;
return false;
}
source = m_chunk.data();
}
if (m_readCancelled)
{
return false;
}
std::size_t sourceSize = chunkSize;
if (dataset.deflate && chunkSize != expected)
{
@@ -844,6 +1074,29 @@ namespace jdi
// 归一化后 rank=2 ⇒「每行只有 1 字节」)。若逐行回调,就是上千万次
// 小 memcpy + 上千万次 vector 分配,实测把解压吞吐拖到 ~11MB/s。
// 因此这里把**目标连续的行合并成一次回调**(典型情况整块只需 1 次)。
//
// 快路径(rank == 2 且第二维完全对齐):整块在目标里就是一段连续区域,
// 直接一次回调,省掉「每行算坐标 + 判断合并」的循环。
// 实测 BIG_MAT.mat:`#data#` 为 129,139,768 字节 × 32KB chunk,每 chunk 32768 行,
// 逐行循环共 12.9 亿次迭代(约占总加载时间的一半以上)。
if (rank == 2 && chunkDims[1] == dims[1] && chunkDims[1] > 0)
{
const std::uint64_t startRow =
std::min<std::uint64_t>(chunkOffset[0], dims[0]);
const std::uint64_t rowCount =
std::min<std::uint64_t>(chunkDims[0], dims[0] - startRow);
const std::uint64_t byteCount = rowCount * rowBytes;
if (startRow < dims[0] && byteCount <= static_cast<std::uint64_t>(sourceSize))
{
if (!sink(source, static_cast<std::size_t>(byteCount),
startRow * chunkDims[1]))
{
return false;
}
}
continue;
}
std::vector<std::size_t> coordinate(rank > 0 ? rank - 1 : 0, 0);
std::uint64_t runElement = 0; // 合并段的起始元素下标
std::size_t runSourceOffset = 0; // 合并段的源偏移
+53 -25
View File
@@ -142,6 +142,7 @@ namespace jdi
double high = 1.0; /**< 本阶段占的进度区间上界。 */
std::uint64_t read = 0; /**< 已读字节。 */
std::uint64_t total = 0; /**< 总字节。 */
mutable double lastRatio = -1.0; /**< 已上报的最大比例(保证单调不回退)。 */
/** @brief 上报一个 0-1 的阶段内进度(向宿主传递字节数与百分比)。 */
bool Report(double fraction) const
@@ -158,11 +159,21 @@ namespace jdi
{
fraction = 1.0;
}
double ratio = low + (high - low) * fraction;
// 「已读字节」与「逐流解码」是两条独立进度来源:取较大值,避免进度条回退。
if (ratio < lastRatio)
{
ratio = lastRatio;
}
else
{
lastRatio = ratio;
}
LoadProgressInfo info;
info.ratio = low + (high - low) * fraction;
info.ratio = ratio;
// 总字节已知时一并上报(遮罩层据此显示“已加载 / 总大小”)。
info.hasBytes = (total > 0);
info.bytesRead = (total > 0) ? read : 0;
info.bytesRead = (total > 0) ? std::min(read, total) : 0;
info.totalBytes = total;
return (*sink)(info);
}
@@ -503,13 +514,19 @@ namespace jdi
{
if (file.IsDataset(address))
{
// ⚠️ 只解析元信息(维度),**不要**用 `ReadDataset`:那会把整块数据再解码一遍。
// 旧实现先 `ReadNumbers` 解码一次、再 `ReadDataset` 只为拿 `dims`,
// 大矩阵的时间与内存都翻倍。
MatHdf5::Dataset dataset;
if (!file.ReadDatasetInfo(address, dataset) || dataset.dims.empty())
{
return;
}
std::vector<double> numbers;
if (!file.ReadNumbers(address, numbers) || numbers.empty())
{
return;
}
MatHdf5::Dataset dataset;
file.ReadDataset(address, dataset);
std::size_t rows = 1;
std::size_t columns = 1;
if (dataset.dims.size() >= 2)
@@ -566,22 +583,24 @@ namespace jdi
// ------------------------------------------------------------------
// 进度拆成三段:
// 0.00 – 0.02 映射文件(内存映射,几乎瞬时;遮罩层仍能看到“已加载 = 总大小”)
// 0.02 – 0.98 逐条解码样本流(每个流解压后直接填进数据模型,含解压开销)
// 0.00 – 0.02 打开文件(立刻知道总大小;遮罩层显示「0 / 总大小」)
// 0.02 – 0.98 随机读取 + 逐条解码样本流(进度按**实际已读字节**推进)
// 0.98 – 1.00 收尾
// ⚠️ 旧实现把 0–0.55 全给了“读文件字节”(因为旧实现真的整文件读入);
// 现在改成内存映射后那一段几乎没有耗时,若不调整权重,进度条会“瞬间跳到 55% 再慢慢走”。
// ⚠️ 旧实现是内存映射:字节瞬间「全部可访问」⇒ 文件读取百分比立刻 100%,
// 而真正的解析还要跑很久(用户 2026-10-20 报告“这部分不太对劲”)。
// 现在每次读盘都累计到 `MatHdf5::BytesRead()`,百分比随真实 I/O 推进。
// ------------------------------------------------------------------
ProgressReporter readReporter;
readReporter.sink = &progress;
readReporter.low = 0.0;
readReporter.high = 0.02;
ProgressReporter parseReporter;
parseReporter.sink = &progress;
parseReporter.low = 0.02;
parseReporter.high = 0.98;
const MatHdf5::OpenProgress openProgress =
[&readReporter](std::uint64_t bytesRead, std::uint64_t totalBytes) -> bool
[&parseReporter](std::uint64_t bytesRead, std::uint64_t totalBytes) -> bool
{
readReporter.read = bytesRead;
readReporter.total = totalBytes;
return readReporter.Report(totalBytes > 0 ? static_cast<double>(bytesRead) /
parseReporter.read = bytesRead;
parseReporter.total = totalBytes;
return parseReporter.Report(totalBytes > 0 ? static_cast<double>(bytesRead) /
static_cast<double>(totalBytes)
: 0.0);
};
@@ -589,12 +608,17 @@ namespace jdi
{
return false; // 用户取消时 `error` 为空
}
ProgressReporter parseReporter;
parseReporter.sink = &progress;
parseReporter.low = 0.02;
parseReporter.high = 0.98;
parseReporter.read = readReporter.total;
parseReporter.total = 0; // 字节已读完:本阶段只报百分比
// 流式解码期间的字节进度(每读一块回调一次);返回 false 即取消。
file.SetReadProgress(
[&parseReporter](std::uint64_t bytesRead, std::uint64_t totalBytes) -> bool
{
parseReporter.read = bytesRead;
parseReporter.total = totalBytes;
return parseReporter.Report(totalBytes > 0 ? static_cast<double>(bytesRead) /
static_cast<double>(totalBytes)
: 0.0);
});
if (!parseReporter.Report(0.0))
{
error.clear();
@@ -609,6 +633,10 @@ namespace jdi
{
if (!LoadSdi(file, refs, sigstream, stem, groups, error, parseReporter))
{
if (file.Cancelled())
{
error.clear(); // 用户取消:不显示错误
}
return false;
}
}
@@ -637,9 +665,9 @@ namespace jdi
{
LoadProgressInfo info;
info.ratio = 1.0;
info.hasBytes = (readReporter.total > 0);
info.bytesRead = readReporter.total;
info.totalBytes = readReporter.total;
info.hasBytes = (file.FileBytes() > 0);
info.bytesRead = file.FileBytes();
info.totalBytes = file.FileBytes();
progress(info);
}
return true;
File diff suppressed because it is too large. Load diff