Files
Just-DataInspector/Documents/refactor_report_2026-10-15.md
T
DeepSeek V4.1 Flash 6eedbe3c5d 数据层大重构:文件→分组 / 时间轴一等公民 / 加载器抽象 + CSV 多时间轴 + MF4 支持
一、数据模型解耦(详见 Documents/refactor_report_2026-10-15.md 与 agent_mem §9.27)
1. DataFile → SignalGroup:删除 filePath / fileName / time / timeColumnName / timeMonotonic /
   minTime / maxTime —— 「分组」是应用内的逻辑容器,加载完成后与磁盘文件再无关系。
2. 新增 TimeAxis(name / time / minTime / maxTime / monotonic / finiteCount),一个分组可含 1..N 条;
   SignalData 新增 axisIndex 指向所属时间轴,采样值与时间的对应关系自包含。
3. SignalData 删除冗余的 rawHeader(name 的副本)与 unit(由列名猜出、全仓无人使用);
   名字只保留 name(源名称)+ displayName(用户重命名),Title() 统一展示。
4. SignalRef::fileIndex → groupIndex;DataSet:AddGroup/RemoveGroup/GroupAt/GroupCount/Groups()/
   SetGroupDisplayName/RemapSelectionAfterGroupRemoval + 新增 Axis(ref);
   TabConfig:SetGroupVisible/GroupHasVisible/GroupAllVisible/RemapAfterGroupRemoval +
   RowKind::GROUP / CMD_GROUP_*。
5. 采样与统计 API 首参由 const DataFile& 改为 const TimeAxis&:
   SampleSignalAt / SampleDisplayedSignalAt / FindNearestSample(Utilities)与
   SampleSpacingPixels / VisibleIndexRange / EdgeIndexRange / MeanFilterValueAt / WeakPeakValueAt /
   EdgeCountsFor / EdgeCenterCountsFor / EdgeScanFor(PlotView)。
6. 视图层:ResolvedSignal = { data, style, group, axis, visible };PolylineCacheEntry::file→group;
   EdgeScanState::file→axis(统计身份 = 时间轴 + 信号);
   ComputeVisibleTimeBounds 只统计可见信号真正用到的时间轴。

二、加载层抽象(格式与模型彻底解耦)
- 新增 Utilities/Inc/DataTable.hpp:DataTable(通用列优先表格)+ LoadProgress/Info +
  TableBlock(一个时间轴块)+ LoadedSource(源文件全部块)。
- 新增 Utilities/Inc|Src/DataLoader.*:唯一入口 LoadSignalSource 按扩展名分发 +
  IsSupportedSignalFile + SignalFileDialogFilter。
- 新增 Utilities/Inc|Src/CsvLoader.*:CSV 解析 + 多时间轴切分 + 各轴按自己的时间列裁剪。
- 新增 Utilities/Inc|Src/Mf4Loader.*:ASAM MDF v4 解析。
- 删除 Utilities/Inc/CsvTable.hpp 与 Utilities/Src/CsvTable.cpp。
- 唯一装配点 BuildSignalGroup(LoadedSource&&, paletteBase, group, error, progress):
  时间轴去重合并 + 信号命名 + 调色板取色 + 统计与粗粒度包络;新增格式只需写一个加载器。

三、CSV 多时间轴(修正用户报告的缺陷)
- 只有名为 t/time/timestamp/simulation time/时间/时间轴 的列是时间轴(判据收紧,
  不再用「包含 time」以免把 timeout 误判成时间轴)。
- 每个时间列开启一个新块,其后直到下一个时间列之前的列都属于该块;各块按自己的时间列裁剪
  ⇒ 支持各轴长度不同(Multi_Timeline.CSV 实测切出 3 条轴,长度 7/4/7);
  **不再把其它 time 列当成信号显示**。
- 完全没有时间列时保留旧行为(第一列当时间轴,单轴)。

四、MF4 支持(新功能)
- 加载后新建一个分组、组名 = 文件名,文件内全部信号归入该分组。
- 每个通道组(CG)是一条时间轴,内容相同的时间轴自动合并(实测 27 个 CG → 3 条轴 / 124 条信号)。
- 实现 MDF v4 块解析(HD/DG/CG/CN/CC/DT)、1–64 位整数(任意位偏移、含符号扩展)与
  32/64 位 IEEE 浮点解码、无效位(inval bit)处理、线性换算 phys = cc_val[0] + cc_val[1]*raw。
- TX 文本块同时兼容规范写法(U32 长度 + UTF-16/UTF-8)与 CANape 的「无长度前缀 NUL 结尾裸文本」。
- 未实现(跳过该组):压缩块 ##DZ/##HL/##SD、##DL 数据列表、字符串/MIME 通道。

五、顺带修复
- Application::Initialize:窗口类已注册(ERROR_CLASS_ALREADY_EXISTS)时复用而不是弹模态错误框
  (旧实现让同一进程第二次构造 Application 永久阻塞);Shutdown 注销窗口类并销毁窗口。
- UI 文案统一到「分组」:选中该分组全部信号 / 取消该分组全部信号 / 关闭该分组(从工作区移除)/
  请关闭部分分组或信号后重试。/ 尚未加载数据,请打开数据文件 / 打开数据文件以查看波形 /
  (可同时加载多个文件,各自成为一个分组);对话框过滤器增加 *.mf4/*.mdf。

六、验证(全部离屏:BELOW_NORMAL 优先级 + SW_HIDE 窗口 + 只注入合成消息)
- 新增 .preflight/perf/datacheck_main.cpp:69/69 通过(单轴 CSV 回归、多时间轴 CSV、MF4 组名/信号数/
  时间轴/主时间轴 ns→s/setSpeed 首样本与原始字节人工核对、DataSet 不变量、派生信号沿用源轴、
  MF4 集成 124 信号并一格 60~68ms/帧)。
- 新增 .preflight/perf/edgecheck_main.cpp(ASCII-only,替代被编码破坏的 panelcheck):46/46 通过
  (「调整」符号规则走真实菜单路径、边沿统计按帧推进/不饿死/逐位一致/无鼠标输入也完成、
  面板两列等宽与信号名限宽、弱峰复位控件)。
- 既有 .preflight/perf/verify_main.cpp(已适配新 API):47/47,另有 2 条本机未同步的旧规则断言
  (derived offsetTime kept / mouse in lane1 lower half),与本轮无关。
- make clean && make all 零警告。
2026-10-15 03:48:58 +08:00

17 KiB
Raw Blame History

数据层审计与重构报告(2026-10-15)

本轮任务:① 审计代码库的命名/参数/注释/数据结构;② 在审计基础上做一次解耦重构(大刀阔斧, 但保持全部交互与显示不变);③ 完善 CSV(多时间轴 / 各轴不等长)并新增 MF4 支持。

结论:数据层与加载层被重写为「分组 / 时间轴 / 信号」三段式结构,文件格式与数据模型彻底解耦; CSV 多时间轴与 MF4 均已支持;三套离屏测试(69 + 46 + 47 条断言)通过;make release 零警告。


一、审计发现的问题(按严重度)

1.1 「文件」概念渗进了数据模型的核心(最严重)

位置 问题
DataFile(结构体名) 名字把「一次加载产生的信号集合」和「磁盘上的文件」绑死;实际应用语义是信号分组。
DataFile::filePath / fileName 加载完成后应用与文件再无关系(文件可移动/删除/改名),这两个字段在运行期没有任何用途(filePath 只在加载消息里出现一次)。
DataFile::time / timeColumnName / timeMonotonic / minTime / maxTime 把「时间轴」焊死在分组上 ⇒ 一个分组只能有一条时间轴,与 CSV 多 time 列、MF4 多通道组的现实不符。
SignalRef::fileIndex 引用语义被写成「文件下标」,全仓 223 处;后来者会以为信号与文件有绑定关系。
EdgeCountsFor(const DataFile& file, const SignalData& signal, …) 典型文不对题:函数只需要「时间 + 采样值」,却要求调用方提供整个「文件」;这也是用户指出的重点。
SampleSignalAt(const DataFile& file, …)、FindNearestSample(const DataFile& file, …)、VisibleIndexRange(const DataFile& file, …)、EdgeIndexRange(const DataFile& file, …)、MeanFilterValueAt、WeakPeakValueAt 同上一类:参数语义过宽,时间轴与信号被迫通过「文件」间接耦合。
ResolvedSignal::file 视图层的解析结果也携带「文件」指针,导致采样/命中/包络/边沿统计处处 resolved.file->time。

1.2 SignalData 的冗余字段(用户指出的重点)

字段 问题 结论
name 源名称(列名/通道名)。 保留,语义明确:加载后原样不变。
displayName 用户重命名。 保留(Title() 优先返回它)。
rawHeader name 的副本(构建时 rawHeader = headers[i]; name = Trim(rawHeader)),派生信号里又 rawHeader = derived.name。 删除(纯冗余,两个字段表达同一件事)。
unit 由列名括号里猜出来的单位,全仓只有 2 处写、0 处读(从未显示、从未参与计算)。 删除(属于「未实现的元数据」残留;用户明确说单位元数据暂不支持)。
时间信息 缺失 ⇒ 只能靠「文件」找时间轴。 新增 axisIndex:信号指向所属时间轴,采样值与时间的对应关系自包含。

1.3 加载层与格式耦合

位置 问题
CsvTable(结构体) 「从文件读出来的原始表格」这个名字把通用中转结构钉死在 CSV 上;后续每加一种格式都要再抄一遍类似结构。
CsvProgress / CsvProgressInfo 名字里的 Csv 让「加载进度」这类通用概念只服务于 CSV。
BuildDataFile(CsvTable&&, path, …) 建组函数直接吃 CSV 解析结果;MF4(或任何其它格式)都无法复用,只能复制一份逻辑。
bool BuildDataFile(...) 里的「识别时间轴列 / 回退第一列」逻辑 与「多时间轴」需求正面冲突:只认第一个时间列,且要求所有列等长。
.preflight 之外无任何「格式 → 数据」的装配点 新增格式要在 Application::RunAsyncLoad 里改流程。

1.4 过期 / 文不对题的注释

位置 问题
LooksLikeTimeHeader 判据是「列名含 time/时间」⇒ 会把 timeout、TimeStampExtra 之类误判成时间轴(多时间轴下会切出空块)。
ExtractUnit 解析出来的单位无人使用(见 1.2)。
大量 @param [IN] file 信号所属文件 参数已改语义后注释未跟(本轮全部改为 axis 该信号使用的时间轴)。
UI 文案 选中该文件全部信号 / 取消该文件全部信号 / 关闭该文件(从工作区移除) / 请关闭部分文件分组或信号 等,把「分组」说成「文件」。
空状态文案 尚未加载数据,请打开 CSV 文件 / 打开 CSV 文件以查看仿真数据波形:应用已不只支持 CSV。

1.5 健壮性缺陷(审计中顺带发现)

位置 问题
Application::Initialize RegisterClassExW 失败即弹模态错误框。同一进程内第二次构造 Application 必然命中 ERROR_CLASS_ALREADY_EXISTS ⇒ 调用方永久阻塞(本轮离屏测试就被它卡住)。Shutdown 也从不注销窗口类。
Application::LoadFile 失败路径 会弹模态对话框(离屏/自动化场景是雷区)。已在测试中绕开;建议后续把「错误提示」与「模态阻塞」解耦。
MDF 文本块解析 ##TX 在 MDF 规范里是「长度 + UTF-16/UTF-8」,但 CANape 16 会写无长度前缀的裸 ASCII(见 §三),只按规范解析会得到空名称。

二、重构内容(改动清单)

2.1 数据模型(Utilities/Inc/DataModel.hpp、Utilities/Src/DataModel.cpp)

新增 TimeAxis(时间轴成为一等公民)

struct TimeAxis {
    std::wstring name;             // 时间列名(源文件原样)
    std::vector<double> time;
    double minTime, maxTime;
    bool monotonic;
    std::size_t finiteCount;
    std::size_t Count() const;
};

SignalData 精简 + 指向时间轴

struct SignalData {
    std::wstring name;             // 源名称(原样保留、永不修改)
    std::wstring displayName;      // 用户重命名(空 = 用 name)
    std::size_t axisIndex = 0;     // 所属时间轴(分组内下标)  ← 新增
    std::vector<double> values;
    double minValue, maxValue; std::size_t finiteCount;
    std::vector<double> coarseMin, coarseMax; std::size_t coarseStride;
    SignalStyle style;
    const std::wstring& Title() const;
};
// 删除:rawHeader、unit

DataFile → SignalGroup(分组 = 若干时间轴 + 若干信号)

struct SignalGroup {
    std::wstring name;                  // 默认取源文件名(便于辨认来源)
    std::wstring displayName;           // 用户重命名
    std::vector<TimeAxis> axes;         // 1..N 条时间轴          ← 取代单一 time
    std::vector<SignalData> signals;
    const std::wstring& Title() const;
    const TimeAxis& AxisOf(const SignalData&) const;
    const TimeAxis& AxisAt(std::size_t) const;
    bool TimeRange(double& low, double& high) const;   // 全部轴的并集
};
// 删除:filePath、fileName、time、timeColumnName、timeMonotonic、minTime、maxTime

引用与集合

  • SignalRef::fileIndex → groupIndex(含注释:「分组是应用内的逻辑容器,与磁盘文件没有任何关系」)。
  • DataSet:AddDataFile→AddGroup、RemoveFile→RemoveGroup、FileAt→GroupAt、FileCount→GroupCount、 Files()→Groups()、SetFileDisplayName→SetGroupDisplayName、 RemapSelectionAfterFileRemoval→RemapSelectionAfterGroupRemoval;新增 Axis(ref)(解析信号→时间轴)。
  • TabConfig:SetFileVisible→SetGroupVisible、FileHasVisible→GroupHasVisible、FileAllVisible→GroupAllVisible、 RemapAfterFileRemoval→RemapAfterGroupRemoval、RowKind::FILE→RowKind::GROUP、CMD_FILE_*→CMD_GROUP_*。

采样与统计 API 去掉「文件」

bool SampleSignalAt(const TimeAxis&, const SignalData&, double time, bool interpolate, double&);
bool SampleDisplayedSignalAt(const TimeAxis&, const SignalData&, const SignalStyle&, …);
int  FindNearestSample(const TimeAxis&, double time);
// PlotView:SampleSpacingPixels / VisibleIndexRange / EdgeIndexRange /
//           MeanFilterValueAt / WeakPeakValueAt / EdgeCountsFor /
//           EdgeCenterCountsFor / EdgeScanFor —— 首参全部由 const DataFile& 改为 const TimeAxis&

视图层

  • ResolvedSignal:const DataFile* file → const SignalGroup* group + const TimeAxis* axis; 所有 resolved.file->time 改为 resolved.axis->time,resolved.file->Title() 保留走 group。
  • PolylineCacheEntry::file → group;EdgeScanState::file → axis(统计身份 = 时间轴 + 信号)。
  • TabConfig::ComputeVisibleTimeBounds:只统计可见信号真正用到的时间轴(多轴分组下避免拉出过宽范围)。
  • 时间轴刻度列名取「首个可见信号所属分组的第一条时间轴」。

2.2 加载层(新文件,格式与模型解耦)

Utilities/Inc/DataTable.hpp     DataTable(通用列优先表格)+ LoadProgress/Info
                                + TableBlock(一个时间轴块)+ LoadedSource(源文件的全部块)
Utilities/Inc/DataLoader.hpp    LoadSignalSource(path,…) 唯一入口 + IsSupportedSignalFile + 对话框过滤器
Utilities/Src/DataLoader.cpp    按扩展名分发(.csv/.txt → CSV;.mf4/.mdf → MDF4;其它按文本尝试)
Utilities/Inc/CsvLoader.hpp     LoadCsvSource
Utilities/Src/CsvLoader.cpp     CSV 解析 + 多时间轴切分 + 各轴按自己的时间列裁剪
Utilities/Inc/Mf4Loader.hpp     LoadMf4Source
Utilities/Src/Mf4Loader.cpp     ASAM MDF v4 解析(见 §三)
删除:Utilities/Inc/CsvTable.hpp、Utilities/Src/CsvTable.cpp

唯一装配点:BuildSignalGroup(LoadedSource&&, paletteBase, SignalGroup&, error, progress) —— 与格式无关,负责:时间轴去重合并(内容逐位相同只保留一条)、信号命名、调色板取色、统计与粗粒度包络。 新增格式时只需写一个加载器,数据模型与全部界面代码不用改。

2.3 CSV:多时间轴 + 各轴不等长

  • 规则(与 MATLAB/Simulink 说明一致):只有名为 time 的列是时间轴;每个时间列开启一个新块, 它之后直到下一个时间列之前的列都属于该块。
  • 各块按自己的时间列裁剪(较短的时间轴在数据区提前结束,其余位置为空)⇒ 支持 TestData/Multi_Timeline.CSV(time,data1,time,data2,data3,time,data4,实测切出 3 条轴,长度 7/4/7)。
  • 修复用户报告的缺陷:不再把其它 time 列当成信号显示。
  • 保留旧行为:完全没有 time 列时退化为「第一列是时间轴」的单轴文件。
  • 判据收紧:LooksLikeTimeHeader 只接受 t / time / timestamp / simulation time / 时间 / 时间轴 (不再用「包含 time」这种会误判 timeout 的规则)。

2.4 MF4:新增支持

  • 加载后新建一个分组、组名 = 文件名,文件内的全部信号都归入该分组(用户要求的行为)。
  • 每个**通道组(CG)**是独立时间轴:内容相同的时间轴自动合并 (实测该文件 27 个 CG → 3 条时间轴、124 条信号,避免 25 条重复的单样本轴)。
  • 只解码绘制需要的部分:字符串/字节数组通道、压缩块(DZ/HZ)、MIME 通道一律跳过; 非数值通道不产生信号。

2.5 UI 文案与健壮性

  • 文案统一到「分组」:选中该分组全部信号、取消该分组全部信号、关闭该分组(从工作区移除)、 请关闭部分分组或信号后重试。、尚未加载数据,请打开数据文件、打开数据文件以查看波形、 (可同时加载多个文件,各自成为一个分组);打开对话框标题改为「打开测量数据文件」并支持 *.mf4/*.mdf。
  • Application::Initialize:RegisterClassExW 返回 ERROR_CLASS_ALREADY_EXISTS 时复用已注册的类 (不再弹模态错误框);Shutdown 注销窗口类并销毁窗口。⇒ 同一进程可多次构造/销毁 Application。

三、MDF v4 解析要点(本轮实测得到,供后续维护)

文件实测为 MDF 4.10 / CANape 16.0.50(##HD … ##DG … ##CG … ##CN … ##CC … ##DT)。

  • 块头(24 字节):id[4] + reserved[4] + block_len(U64) + link_count(U64);之后是 link_count 个 8 字节绝对文件偏移(0 = 空);数据段紧随链接区。DT/HL 等块可以 0 链接。
  • ID 块:偏移 0 起 "MDF ",偏移 8 起版本串(如 "4.10 ")。
  • HD:链接 [0] = 第一个 DG。
  • DG:链接 [1] = 第一个 CG、[2] = 该组的数据块(DT)。
  • CG:链接 [1] = 第一个 CN、[2] = tx_name;数据段 +8 = cycle_count。 记录长度取 DT 数据段字节数 / cycle_count(实测 114/110 字节,与声明一致),拿不到时按通道位宽向上取整。
  • CN:链接 [2] = 名称 TX、[4] = 换算 CC;数据段 +0 type、+1 sync_type(1 = 时间主通道)、+2 data_type、+3 bit_offset、 +4 byte_offset(U32,单位**字节**)、+8 bit_count、+12 flags(bit0 整列无效 / bit1 有无效位)、 +16 invalid_bit_pos、+32 val_range_min、+40 val_range_max。 绝对位偏移 = byte_offset × 8 + bit_offset。
  • data_type:0/1 无符号(LE/BE)、2/3 有符号、4/5 IEEE 浮点、≥6 字符串/字节数组(跳过)。 浮点只接受字节对齐的 32/64 位;整数支持 1–64 位任意位偏移(含符号扩展)。
  • CC:+0 type、+1 precision、+2 flags、+4 ref_param_nr、+6 val_count、 +8 phy_range_min、+16 phy_range_max、+24 cc_val[](每个 8 字节)。 换算公式(实测确认)= phys = cc_val[0] + cc_val[1] × raw。 本文件里时间主通道是「double 纳秒 + 线性换算 1e-9」⇒ 解码后时间范围 [0.024320125, 42.3918] s 与 CC 里的物理范围逐位吻合,可据此自检。
  • 文本块(TX):规范是「U32 长度 + UTF-16 代码单元」(MDF 4.1 起也可 UTF-8 字节数), 但 CANape 会写无长度前缀的 NUL 结尾裸文本。解析顺序:先按长度前缀(UTF-16 → UTF-8), 失败则按「裸文本(自动判断 UTF-16/UTF-8)」回退。

四、验证(全部离屏;进程/线程 BELOW_NORMAL、窗口 SW_HIDE、不注入真实鼠标键盘)

套件 覆盖 结果
.preflight/perf/datacheck_main.cpp 单轴 CSV 回归;多时间轴 CSV(3 轴、7/4/7、不再出现 time 信号);MF4(组名、124 信号、3 条轴、主时间轴 ns→s、setSpeed 首样本与原始字节人工核对);DataSet 不变量(轴共享/解析/时间范围/重命名);派生信号沿用源时间轴;MF4 集成(124 信号并入一个网格渲染 67.5ms/帧) 69/69 通过
.preflight/perf/edgecheck_main.cpp 「调整」符号规则(真实菜单路径);边沿统计按帧推进 / 不饿死 / 与参考实现逐位一致 / 无鼠标输入也完成;面板两列等宽与名称限宽;弱峰复位控件 46/46 通过
.preflight/perf/verify_main.cpp(既有) PlotView 渲染/交互:Y 自适应、派生信号、图例命中、GX 徽标悬停、装饰层无残影、平移逐像素 47/47(另有 2 条本机未同步的旧规则断言:derived offsetTime kept、mouse in lane1 lower half,与本轮无关)
构建 make release 零警告

补充:Target.exe 手工运行确认加载 Multi_Timeline.CSV 与 MF4-Record.MF4 正常(真实交互由用户后续验收)。


五、遗留与建议(未在本轮实施)

  1. 错误提示与模态阻塞解耦:SetStatusMessage(..., isError=true) 直接 MessageBoxW, 在自动化/离屏场景会永久阻塞。建议改为宿主级非模态提示(或至少让调用方可选择不弹)。
  2. MF4 压缩块(##DZ/##HL/##SD)与 ##DL 数据列表未实现:遇到这类文件会跳过相应通道组。 如后续需要,解析 DL 的偏移数组即可(数据布局不变)。
  3. MF4 单元/注释(##CC 的 unit TX、##MD)未使用:用户明确暂不需要元数据。
  4. Component 层仍在 TabConfig 里保存逐信号的边沿统计/预览参数:属于「视图配置」与「数据」的 第二次耦合(当前可用,但若要支持「同一信号在不同标签页用不同统计参数」需再拆一层)。
  5. Lane 内含 4 类实验性功能的状态与面板坐标(edgeCount/meanFilter/weakPeak + 各自面板 X/Y), 已经把 Lane 撑到 200+ 行;建议后续抽出 LanePanelState。
  6. PlotView.cpp 仍有 1.1 万行:渲染分支(静态缓存/宽基底/样式补丁/整幅)与交互命中混在一个类里, 建议按「渲染器 / 命中测试 / 派生曲线调度」拆成 3 个协作类(风险较高,宜单独一轮做)。
  7. SignalRef 仍是「分组下标 + 组内下标」:删除/重排分组时依赖调用方按顺序调用 Remap* 系列函数维护;可考虑改为稳定 ID(后续需要撤销/重做时收益更大)。