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)。
238 lines
9.6 KiB
C++
238 lines
9.6 KiB
C++
#ifndef __MAT_H5_HPP__
|
||
#define __MAT_H5_HPP__
|
||
|
||
#include <cstdint>
|
||
#include <functional>
|
||
#include <string>
|
||
#include <utility>
|
||
#include <vector>
|
||
|
||
namespace jdi
|
||
{
|
||
|
||
/**
|
||
* @brief 只读的 HDF5 子集解析器(用于 MATLAB 7.3 的 `.mat` 文件)。
|
||
*
|
||
* MATLAB 7.3(`-v7.3`)的 `.mat` 其实是 **HDF5** 文件:数值矩阵是数据集,
|
||
* 结构体是分组(字段 = 成员),元信息放在属性里(如 `MATLAB_class`)。
|
||
* 本项目不引入 HDF5 库,因此这里实现一个**刚好够用**的只读子集:
|
||
*
|
||
* * 超级块 v0/v1/v2/v3;
|
||
* * 对象头 v1(含续块消息),v2 仅识别并跳过;
|
||
* * 分组成员:符号表(B-tree v1 + 本地堆)与 link 消息两种方式;
|
||
* * 数据空间 v1/v2、数据类型 v1(整数 / 浮点 / 定长字符串 / 引用 / 复合 / vlen);
|
||
* * 数据布局 v3(紧凑 / 连续 / 分块),分块数据集支持 deflate 压缩(zlib)。
|
||
*
|
||
* ⚠️ 不支持:外部数据文件、共享消息表、非 deflate 过滤器、极老的 v1 布局。
|
||
* 这些在 MATLAB 保存的文件里都不出现。
|
||
*/
|
||
class MatHdf5
|
||
{
|
||
public:
|
||
/** @brief 数据类型类别(对应 HDF5 的 class 编号)。 */
|
||
enum class TypeClass : int
|
||
{
|
||
Integer = 0,
|
||
Float = 1,
|
||
Time = 2,
|
||
String = 3,
|
||
Bitfield = 4,
|
||
Opaque = 5,
|
||
Compound = 6,
|
||
Reference = 7,
|
||
Enum = 8,
|
||
VariableLength = 9,
|
||
Array = 10
|
||
};
|
||
|
||
/** @brief 数据类型(只保留读取 MATLAB 数据所需的字段)。 */
|
||
struct Type
|
||
{
|
||
TypeClass cls = TypeClass::Opaque;
|
||
int version = 1;
|
||
std::size_t size = 0; /**< 单个元素占用的字节数。 */
|
||
bool isSigned = false; /**< 整数是否有符号。 */
|
||
bool bigEndian = false; /**< 字节序(MATLAB 一律小端)。 */
|
||
bool isVariableString = false; /**< vlen 字符串。 */
|
||
std::vector<std::pair<std::string, Type>> members; /**< 复合类型成员。 */
|
||
};
|
||
|
||
/** @brief 数据布局。 */
|
||
enum class LayoutClass
|
||
{
|
||
Compact = 0,
|
||
Contiguous = 1,
|
||
Chunked = 2
|
||
};
|
||
|
||
/** @brief 一个数据集的定义与数据。 */
|
||
struct Dataset
|
||
{
|
||
std::vector<std::size_t> dims; /**< 各维长度。 */
|
||
Type type; /**< 元素类型。 */
|
||
LayoutClass layout = LayoutClass::Compact;
|
||
std::vector<std::uint8_t> data; /**< 解码后的原始字节(行优先,按 `dims`)。 */
|
||
std::uint64_t chunkAddress = 0; /**< 分块数据集的 B-tree 地址。 */
|
||
std::vector<std::size_t> chunkDims;
|
||
bool deflate = false; /**< 是否使用了 deflate(zlib)过滤器。 */
|
||
std::uint64_t dataAddress = 0; /**< 连续布局的数据起始地址(已加基地址)。 */
|
||
std::uint64_t dataBytes = 0; /**< 连续布局的数据字节数。 */
|
||
/** 紧凑布局的字节(在布局消息里,数据量很小)。 */
|
||
std::vector<std::uint8_t> compact;
|
||
bool ok = false;
|
||
|
||
/** @brief 元素总数(各维长度相乘)。 */
|
||
std::uint64_t ElementCount() const
|
||
{
|
||
std::uint64_t count = 1;
|
||
for (const std::size_t dim : dims)
|
||
{
|
||
count *= dim;
|
||
}
|
||
return dims.empty() ? 0 : count;
|
||
}
|
||
};
|
||
|
||
/** @brief 读文件进度回调:`已读字节 / 总字节`。返回 false 表示请求取消。 */
|
||
using OpenProgress = std::function<bool(std::uint64_t bytesRead, std::uint64_t totalBytes)>;
|
||
|
||
/**
|
||
* @brief 打开文件(**内存映射**,不把整个文件读进堆内存)。
|
||
*
|
||
* ⚠️ 早期版本把整个文件一次读入 `std::vector`(注释里假设“`.mat` 通常几十 MB”),
|
||
* 但 SDI 导出的录波文件可能是 **GB 级**:整文件读入 + 分块解码 + 再拷成 double 会同时
|
||
* 驻留“文件 + 未压缩字节 + double”三份内存。现改为:
|
||
* * 映射文件(只占虚拟地址空间,按页面需求驻留);
|
||
* * 分块数据集逐 chunk 解压并**直接写入**自持有的 double 列(见 `ReadDoubleStream`);
|
||
* * 进度回调在映射完成后立即报告一次(字节已全部可访问),后续解码阶段由 `MatLoader` 驱动。
|
||
*
|
||
* @param [IN] path 文件路径。
|
||
* @param [OUT] error 失败原因。
|
||
* @param [IN] progress 读取进度回调(可为空)。
|
||
* @return 成功返回 true;用户取消时也返回 false。
|
||
*/
|
||
bool Open(const std::wstring& path, std::wstring& error,
|
||
const OpenProgress& progress = OpenProgress());
|
||
void Close();
|
||
|
||
/** @brief 析构时释放映射(否则栈上对象的映射会留到进程退出)。 */
|
||
~MatHdf5() { Close(); }
|
||
|
||
/** @brief 文件字节数(映射长度)。 */
|
||
std::uint64_t FileBytes() const { return static_cast<std::uint64_t>(m_data.size()); }
|
||
|
||
/** @brief 根分组地址。 */
|
||
std::uint64_t Root() const { return m_root; }
|
||
|
||
/** @brief 该对象是否为数据集(有数据空间消息)。 */
|
||
bool IsDataset(std::uint64_t address) const;
|
||
|
||
/** @brief 分组(或结构体)的成员:`名字 → 对象头地址`。 */
|
||
std::vector<std::pair<std::string, std::uint64_t>> Members(std::uint64_t address) const;
|
||
|
||
/** @brief 按名字取成员(不存在返回 0)。 */
|
||
std::uint64_t Member(std::uint64_t address, const std::string& name) const;
|
||
|
||
/** @brief 读取数据集定义(含解码后的原始数据)。 */
|
||
bool ReadDataset(std::uint64_t address, Dataset& out) const;
|
||
|
||
/** @brief 只读取数据集定义(不解码数据;大数据用 `StreamDataset`)。 */
|
||
bool ReadDatasetInfo(std::uint64_t address, Dataset& out) const;
|
||
|
||
/**
|
||
* @brief 字节流回调:把数据集内容按块吐出(**不生成整块缓冲**)。
|
||
* @param [IN] bytes 块起始指针(仅在回调期间有效)。
|
||
* @param [IN] byteCount 块字节数。
|
||
* @param [IN] elementOffset 该块在数据集中的起始**元素**下标(行优先)。
|
||
* @return 返回 false 中止遍历。
|
||
*/
|
||
using ByteSink = std::function<bool(const std::uint8_t* bytes, std::size_t byteCount,
|
||
std::uint64_t elementOffset)>;
|
||
|
||
/**
|
||
* @brief 流式遍历数据集字节(连续布局按 4MB 切片;分块布局逐个 chunk 解压)。
|
||
* @param [IN] dataset `ReadDatasetInfo` 得到的信息。
|
||
* @param [IN] sink 字节回调。
|
||
* @return 成功返回 true。
|
||
*/
|
||
bool StreamDataset(const Dataset& dataset, const ByteSink& sink) const;
|
||
|
||
/** @brief 读取字符串属性(如 `MATLAB_class`、`MATLAB_fields`)。 */
|
||
std::string AttributeString(std::uint64_t address, const char* name) const;
|
||
|
||
/** @brief 读取 `char` 数据集为宽字符串(MATLAB 的 char 是 uint16)。 */
|
||
bool ReadText(std::uint64_t address, std::wstring& out) const;
|
||
|
||
/** @brief 读取数值数据集为 double 序列(流式转换,不生成整块字节缓冲)。 */
|
||
bool ReadNumbers(std::uint64_t address, std::vector<double>& out) const;
|
||
|
||
/**
|
||
* @brief 读取 Simulink SDI 的字节流数据集(`#data#`)为 double 序列。
|
||
*
|
||
* SDI 的 `#data#` 是 `uint8` 数据集,长度 = 采样数 × 8:按 8 字节一组重新解释为 double。
|
||
* 实现上逐 chunk 解压并直接写入输出缓冲(不产生整块字节数组,也不二次拷贝),
|
||
* 因此 GB 级文件也不会出现“文件 + 未压缩字节 + double”三份内存叠加。
|
||
*/
|
||
bool ReadDoubleStream(std::uint64_t address, std::vector<double>& out) const;
|
||
|
||
/** @brief 读取引用数据集(HDF5 对象引用 = 对象头地址,相对基地址)。 */
|
||
bool ReadReferences(std::uint64_t address, std::vector<std::uint64_t>& out) const;
|
||
|
||
private:
|
||
struct Message
|
||
{
|
||
std::uint16_t type = 0;
|
||
std::vector<std::uint8_t> data;
|
||
};
|
||
|
||
std::uint8_t U8(std::uint64_t offset) const;
|
||
std::uint16_t U16(std::uint64_t offset) const;
|
||
std::uint32_t U32(std::uint64_t offset) const;
|
||
std::uint64_t U64(std::uint64_t offset) const;
|
||
std::uint64_t Addr(std::uint64_t offset) const;
|
||
std::uint64_t Resolve(std::uint64_t address) const;
|
||
|
||
void ReadMessages(std::uint64_t address, std::vector<Message>& messages) const;
|
||
bool ParseType(const std::uint8_t* data, std::size_t size, std::size_t offset, Type& out,
|
||
std::size_t& consumed) const;
|
||
void WalkBtree(std::uint64_t node, std::uint64_t heapData,
|
||
std::vector<std::pair<std::string, std::uint64_t>>& out) const;
|
||
void WalkSymbolNode(std::uint64_t node, std::uint64_t heapData,
|
||
std::vector<std::pair<std::string, std::uint64_t>>& out) const;
|
||
|
||
/**
|
||
* @brief 内存映射的文件视图(代替“整个文件读进 `vector`”)。
|
||
*
|
||
* ⚠️ 行为与 `std::vector<std::uint8_t>` 的**只读子集**一致(`size/data/begin/end/[]`),
|
||
* 因此所有“按偏移取字节”的解析代码一行都不用改;但**虚拟地址不等于物理内存**:
|
||
* 只有真正被解析到的页才驻留,GB 级文件不会再先吃掉一整个文件大小的私有内存。
|
||
*/
|
||
struct MappedBytes
|
||
{
|
||
const std::uint8_t* pointer = nullptr;
|
||
std::size_t length = 0;
|
||
const std::uint8_t* data() const { return pointer; }
|
||
std::size_t size() const { return length; }
|
||
const std::uint8_t& operator[](std::size_t index) const { return pointer[index]; }
|
||
const std::uint8_t* begin() const { return pointer; }
|
||
const std::uint8_t* end() const { return pointer + length; }
|
||
void clear()
|
||
{
|
||
pointer = nullptr;
|
||
length = 0;
|
||
}
|
||
};
|
||
MappedBytes m_data; /**< 文件映射视图(只读;语义同字节数组)。 */
|
||
void* m_fileHandle = nullptr; /**< `HANDLE`(避免头文件引入 windows.h)。 */
|
||
void* m_mapHandle = nullptr;
|
||
std::uint64_t m_base = 0;
|
||
std::uint64_t m_root = 0;
|
||
int m_offsetSize = 8;
|
||
int m_lengthSize = 8;
|
||
mutable std::vector<std::pair<std::uint64_t, std::uint64_t>> m_heaps; /**< 堆地址 → 数据地址。 */
|
||
};
|
||
|
||
} // namespace jdi
|
||
|
||
#endif // __MAT_H5_HPP__
|