.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:
1 parent
be76f523f3
commit
75f0fafd41
10 files changed
+1677
-795
No files matched your search
+85
-7
@@ -75,17 +75,37 @@ namespace jdi
|
||||
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 打开文件(一次性读入内存;`.mat` 通常几十 MB,可接受)。
|
||||
* @brief 打开文件(**内存映射**,不把整个文件读进堆内存)。
|
||||
*
|
||||
* ⚠️ 早期版本把整个文件一次读入 `std::vector`(注释里假设“`.mat` 通常几十 MB”),
|
||||
* 但 SDI 导出的录波文件可能是 **GB 级**:整文件读入 + 分块解码 + 再拷成 double 会同时
|
||||
* 驻留“文件 + 未压缩字节 + double”三份内存。现改为:
|
||||
* * 映射文件(只占虚拟地址空间,按页面需求驻留);
|
||||
* * 分块数据集逐 chunk 解压并**直接写入**自持有的 double 列(见 `ReadDoubleStream`);
|
||||
* * 进度回调在映射完成后立即报告一次(字节已全部可访问),后续解码阶段由 `MatLoader` 驱动。
|
||||
*
|
||||
* ⚠️ 按块读取并回调 `progress`:加载遮罩层的"已加载大小 / 总大小 / 百分比"
|
||||
* 全靠它驱动。省略回调时行为不变。
|
||||
* @param [IN] path 文件路径。
|
||||
* @param [OUT] error 失败原因。
|
||||
* @param [IN] progress 读取进度回调(可为空)。
|
||||
@@ -95,6 +115,12 @@ namespace jdi
|
||||
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; }
|
||||
|
||||
@@ -110,15 +136,45 @@ namespace jdi
|
||||
/** @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 序列。 */
|
||||
/** @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;
|
||||
|
||||
@@ -139,14 +195,36 @@ namespace jdi
|
||||
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;
|
||||
bool DecodeChunked(const Dataset& dataset, std::size_t elementSize,
|
||||
std::vector<std::uint8_t>& out) 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;
|
||||
|
||||
std::vector<std::uint8_t> m_data;
|
||||
/**
|
||||
* @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;
|
||||
|
||||
Reference in new issue
Block a user