根因:派生预览曲线按可见采样窗口计算,代价正比于窗口采样数(弱波增强整段 2576 万采样单条约 368ms);旧策略对超过 4M 采样的窗口不缓存,导致大跨度下每帧(含悬停重绘)重算整段曲线;拖滑块每帧改参数即每帧重算。实测弱波增强整段拖滑块单帧约 2303ms,2.5s 跨度约 89ms。 修复:1) 大窗口也进缓存并按总字节预算淘汰(静止重绘 2247ms-0.25ms);2) 交互期间对超出预算的大窗口延迟重算、复用旧曲线(拖滑块 2303ms-18ms),小窗口仍实时重算;3) 计算缓冲改 thread_local 复用容量。仅改何时计算与是否缓存,几何与取值算法不变,逐像素比对 0 差异。 缩放等级差异:整段放大时窗口为整条信号(千万级)每帧重算故最卡;极小跨度窗口仅几千采样故不卡;中间跨度几十毫秒亦卡。
20 KiB
20 KiB
Just DataInspector — Agent 记忆与项目须知
本文件是本仓库的权威上下文备忘录,供任何接手的 AI Agent 在动手前阅读。 每次完成任务后必须同步更新本文件(补充新需求、修复记录、约定变更等)。 保持内容精确、明细、简练;多台设备协作时,可将各自的记忆合并到本文件。
1. 项目目标
- 使用 C++17 + Win32 API(GDI/GDI+) 从零构建的桌面数据查看器,项目名 Just DataInspector。
- 定位:替代 MATLAB/Simulink 的
Simulink.sdi.view仿真数据检查器,用于更方便地查看带时间轴的 CSV 仿真数据。 - 目标数据特征(参考 MathWorks 导入 CSV 的说明):CSV 中存在一个或多个时间轴列,其余列表示各时间点上的信号值。
- 产物为 GUI 程序(非 CUI)。
2. 构建与工具链
- 工具链:MinGW-w64(
x86_64-win32-seh-rev0, MinGW-Builds 16.1.0),可直接用make。 - 构建入口:根目录
Makefile(C / C++ / windres 混合构建)。 - 常用命令:
make release→Build/Release/Target.exe(-Os,strip:-s)。make debug→Build/Debug/Target.exe(-O0 -g)。make→ 同时构建 Release 与 Debug。make clean→ 删除Build/。
- 关键编译设置:
-std=c++17;-finput-charset=UTF-8 -fexec-charset=GBK -fwide-exec-charset=UTF-16LE;-static;-mwindows(GUI);链接gdi32 gdiplus comdlg32 d3d11 dwmapi d3dcompiler。 - 新增组件/工具目录后,必须在
Makefile的CXX_INCLUDES_PATHS与CXX_SOURCES_PATHS中登记(源文件按目录通配自动收集)。 - 构建产物统一在
Build/,已由.gitignore忽略;同时忽略*.o/*.d/*.lst/*.map/*.exe与TestData/。
3. 目录结构与约定
Components/ 功能组件块(每块 Inc/ 放 .hpp,Src/ 放 .cpp)
Application/ 应用主框架:窗口、布局、消息分发、标签页、绘制合成
Main/ 程序入口(WinMain → Application::Run)
TabBar/ 顶部多标签栏
Toolbar/ 顶部工具栏(导入文件 / 适应视图 / 时间轴自适应 / Y 轴自适应 / 选区放大 / 光标 / 合并·拆分·清空网格)
SignalPanel/ 左侧信号面板:按文件分组的信号树、可见性勾选、样式编辑
PlotView/ 波形绘图区:网格、时间轴、光标、缩放、拖拽、实验性功能面板
Utilities/ 工具与辅助模块(Inc/ + Src/)
Application/ ... (见上)
CsvTable/ CSV 流式解析(列优先)
DataModel/ 数据层:DataSet / DataFile / SignalData / 显示配置
PaintUtil/ GDI+ 绘制与文本/控件辅助(含 UiPointInRect、UiMeasureTextWidth 等)
PopupMenu/ 主题化自绘弹出菜单(含二级子菜单、内联编辑)
ColorPickerPopup/ 浮动拾色器
TextEdit/ 内联文本编辑模型
Theme/ 主题色板(Solarised Light)与波形默认配色
Utf8Util/ 编码转换(UTF-8 ↔ UTF-16 等)
Resources/ 图标、清单(.manifest)、版本信息(.rc)、ResourceIds.h
TestData/ 本地测试 CSV(**不纳入版本控制**)
Documents/ 项目文档(含本文件)
- 命名硬性约束:源文件/头文件不得含空格,只能字母与下划线组合。
- 头文件后缀:C 用
.h,C++ 用.hpp;C 源.c,C++ 源.cpp。 - 防重复包含:每个头文件必须
#ifndef __XXX_XX_HPP__ / #define __XXX_XX_HPP__ / ... / #endif;宏名 = 文件名的双边双下划线 + 全大写。 - 宏定义与枚举常量必须全大写 + 下划线。
- 所有对外函数/方法声明用 Doxygen 风格注释:
@brief、@param [IN/OUT]、@return(中文说明)。 - 代码风格统一,强调解耦与清晰的分层。
4. 架构要点
- 数据层共享、视图配置分标签页:全局
DataSet(已加载文件、信号、时间轴)被所有标签页共享;每个标签页仅持有各自的TabConfig(信号可见性、配色、分组、网格等),互不影响。 Application是总编排者:创建窗口、Relayout布局、HandleMessage分发消息、合成绘制、把菜单/拾色器结果派发回组件。- 自定义绘制的浮动控件(
PopupMenu、ColorPickerPopup)不创建独立窗口,作为宿主绘制的最上层内容直接画进后备缓冲区,保证外观与主界面统一(不出现系统默认菜单样式)。 - 消息优先级:菜单打开 > 拾色器打开 > 常规组件交互(见
Application::HandleMessage)。 - DPI 感知:
ApplyDpi重建字体与布局;浮动控件通过SetScale缩放。
数据模型(Utilities/Inc/DataModel.hpp)
SignalData:单条信号元数据 + 采样值。DataFile:一个已加载文件及其自有时间轴(timeColumnName/time/minTime/maxTime/timeMonotonic)。DataSet:文件集合与全局时间范围查询(MinTime/MaxTime、最近采样点查找等)。DrawMode:LINE(插值折线) /SCATTER(散点) /LINE_SCATTER(两者)。LaneMode:PER_SIGNAL/MERGED/CUSTOM(网格划分策略)。PlotCursorMode:NONE/SINGLE/DOUBLE(时间光标三态)。- 各网格自身状态(Y 轴范围、自适应开关、实验性功能开关等)随信号继承。
CSV 解析(Utilities/Src/DataModel.cpp)
- 列优先存储(每条信号一整块连续内存):避免百万行级按行存储的堆分配放大,并支持整列移动进
SignalData。 - 流式分块读取,可携带真实字节进度(
CsvProgressInfo),支持取消。 - 时间列判定
LooksLikeTimeHeader:小写匹配t / time / timestamp / simulation time,或包含time/时间;未命中时回退第 0 列。 - 缺失值以
NaN表示;要求时间列有效数值 ≥ 2,否则报错。 - 分隔符自动探测(默认
,)。
5. 功能需求清单(来自需求交互,持续补充)
5.1 基础功能(首轮构建目标,已实现)
- 波形网格(多网格);时间轴。
- 光标(单/双光标,含差值读数)。
- 缩放与自适应(适应视图、Y 轴自适应、选区放大)。
- 导入并加载符合要求(带时间轴列)的 CSV,解析出信号名。
- 信号样式:修改颜色、粗细、散点/插值线段显示方式(
DrawMode)。 - 信号可绘制到同一网格或不同网格。
5.2 后续扩展(已在代码中实现,供接手参考)
- 多标签页(新建/关闭/重命名/拖拽排序)。
- 工具栏:合并网格 / 拆分网格 / 清空网格;时间轴自适应。
- 信号面板:按文件分组、勾选可见性、仅显示/全部显示/隐藏全部、移除文件。
- 绘图区拖拽:平移、选区放大(按首个明显位移方向决定横/纵轴)、网格滚动、光标拖拽、把信号曲线拖到其它网格、网格图例 GX 徽标上下重排。
- 图例:悬停高亮、内联重命名、信号删除按钮。
5.3 「实验性功能」二级菜单(网格右键菜单内,横线上方)
包含且仅包含以下可勾选项(每网格独立状态):
- 边沿计数
- 边沿计数(中心轴)
- 均值滤波
- 弱波增强
这些功能开关会联动绘图区的浮动参数面板;部分支持「构建为新信号」。
注意:新增/改名实验性功能时,需同步
PlotView.hpp的PLOT_CMD_LANE_*、Application.cpp的菜单构建与派发、PlotView/DataModel的状态位与面板绘制。
6. UI / 交互设计语言
- 参考 VS Code 与 WinUI 的设计语言;配色为 VS Code 的 Solarised Light 方案(
Theme::SolarizedLight(),强调色accent)。 - 全部界面与交互用 Win32 API 自绘,不用系统默认控件样式。
- 文本对齐、菜单内边距、圆角、阴影等在各浮动控件内保持统一风格。
7. 交付与 Git 规范
- 工作区由本地 git 管辖。
- 仅在功能完善(可验证)后才提交;提交信息用简洁中文描述修改内容。
- 提交时仅对本工作区生效(
git config --local),不得影响全局设置:user.name= 当前模型全称。user.email=<>(留空尖括号)。
TestData/、Build/等不提交(见.gitignore)。
8. Agent 工作流要求
- 接手前先探索工作区/提交历史/代码建立认知(本文件应作为第一参考)。
- 完成需求后编译验证(
make release至少成功),必要时用TestData/数据功能验证。 - 验证与探索时尽量不占用用户的鼠标/屏幕/键盘(后台构建为主)。
- 本地仓库提交完成后,不要在未经指示的情况下继续探索或优化,等待用户下一步指令。
- 每次完成任务后更新本文件(需求、修复、约定变更)。
9. 已知问题与修复记录
9.1 网格右键菜单「实验性功能」二级菜单瞬间关闭(已修复)
- 症状:在网格右键菜单里把鼠标从「实验性功能」移向右侧二级菜单的途中,只要指针短暂离开父项交互区域,二级菜单立即关闭,用户来不及完成选择。旧设备 CPU 较慢、鼠标移动事件稀疏,指针可能"跳"进二级菜单而未暴露该问题;CPU 更快、事件更密集时必然复现。
- 根因(
Utilities/Src/PopupMenu.cpp的PopupMenu::OnMouseMove):- 子菜单展开后,只要指针不在子菜单矩形内且未命中任何可点击条目,就无条件
CloseSubmenu()。 - 主菜单条目矩形的右缘为
menu.right - kPadH,而子菜单左缘为menu.right - kSubOverlap,两者之间存在约 3px 的内边距"死区"(位于主菜单矩形内但落在任何条目之外)。鼠标事件密集时指针必然落入该窄带,从而触发误关闭。
- 子菜单展开后,只要指针不在子菜单矩形内且未命中任何可点击条目,就无条件
- 修复:子菜单展开期间,若指针仍位于「主菜单 ∪ 子菜单」矩形范围内、且尚未落到其它可点击条目上(例如掠过内边距窄带、分隔线、禁用项),则保持子菜单展开并维持父项高亮;只有明确移动到其它条目或完全离开上述范围时才收起。
9.2 打开网格右键菜单后,悬停光晕与「可拖拽」提示气泡不消失(已修复)
- 症状:鼠标停在信号曲线上(出现光晕高亮 + 「XXX · 按住拖拽可移入其它网格」提示气泡)时右键弹出网格菜单,高亮与气泡残留在原地,不随菜单打开而消失。
- 根因:菜单打开后鼠标移动被菜单独占(
Application::HandleMessage在m_menu.IsOpen()时只把事件交给菜单),绘图区再也收不到WM_MOUSEMOVE;而打开菜单时没有清空绘图区的悬停状态(m_hoverSignal/m_hoverLegend),因此PaintHoverHighlight与PaintSignalInteraction继续把高亮与气泡画出来。 - 修复:
Application在绘图区右键弹出网格菜单前调用m_plot.OnMouseLeave(),清空悬停状态;PlotView::OnMouseLeave()补上清空m_hoverLegend(原先只清m_hoverLane/m_hoverSignal等,图例半高亮会残留)。
9.3 性能:启用带预览曲线的实验性功能后,平移/拖动光标异常卡顿(已修复)
- 症状(以
VERY_BIG_DATA.CSV为例,加载并显示全部信号后):时间轴选区放大到某些区间时,左键/中键平移网格视界、或启用「均值滤波 / 中心轴边沿计数」后再平移、甚至只是左右拖动时间光标看数值,都会异常卡顿(实测单帧 300ms+)。 - 排查方法:在
.preflight/perf/(gitignored)用离屏窗口(不占用鼠标/键盘/屏幕)直接驱动PlotView,用QueryPerformanceCounter测Paint(),并对CurveSpanFor/ComputeMovingAverageIterated/PaintDerivedCurve分段埋点计时。 - 根因(
Components/PlotView/Src/PlotView.cpp的PaintCursor→paintDerivedCapsulelambda):- 绘制时间光标处的派生曲线取值胶囊时,把请求窗口取成「可见区间 ∪ 光标采样下标」的并集;
- 当光标不在当前视界内(平移视界会把光标甩到视界外)时,这个并集会变成
[光标下标, 视界右端]或[视界左端, 光标下标]—— 上千万采样的巨大窗口; - 该窗口超过
kCurveSpanCacheMaxSamples(4M) → 走一次性缓冲、不进缓存 → 每一帧都重算整段多次迭代滑动平均(实测 5 条曲线合计 300ms+)。 - 这条路径同样被平移、拖滑块、拖动光标、改色等所有「每帧重绘」的交互触发,因此它们一起变卡。
- 修复:
paintDerivedCapsule只需要光标所在采样点(以及双光标另一端)的派生取值,因此把请求窗口收窄为「需要的那几个下标」([sampleIndex, sampleIndex+1],双光标时并入另一端)。均值滤波半径造成的取值依赖由CurveSpanFor自身的保护带(guard = clamp(max(窗口/8, (X+1)*N+64), 4096, 65536))负责覆盖 —— 对一切reach ≤ 65536的参数,取值与「整条曲线」逐位一致,显示效果零变化。 - 效果(离屏实测,
VERY_BIG_DATA,5 网格):- 均值滤波平移(2% 跨度):326ms → 约 16–23ms;
- 均值滤波平移(0.4%/0.08%/0.016%):约 305ms → 4–6ms;
- 中心轴边沿计数平移(2%):约 300ms → 约 20ms;
- 光标左右移动(静态场景):命中场景缓存,仅叠加层重绘,约 9ms。
- 不影响显示的说明:本次未改动任何几何/绘制算法(没有引入曾导致"棘状细节被并成实心块"的 ±1 列扩展),只收窄了「为了取时间光标处派生取值而请求的曲线窗口」,因此画面与实际取值完全不变。
9.4 性能经验与已知残留热点(供后续参考)
- 本仓库的性能黄金法则:交互期间(
m_dragMode != NONE)走「快速帧」(关闭波形/派生曲线抗锯齿),松手后补一帧全质量;静止画面走「静态场景缓存」(BitBlt 贴图 ~1ms)。任何只在鼠标移动/拖动参数时改变的内容都必须画在贴图之后(叠加层),绝不能进静态缓存,也不能每帧触发整条曲线/整段采样的重算。 - 已知残留热点(本次未动,均为"非拖拽的整幅重绘",实际拖拽时因快速帧而很快):
- 原始波形的「逐像素列包络带」用 GDI+
FillPath填充;在某些跨度下包络带不被判为「梳状」,抗锯齿填充仍约 20–27ms/帧(VERY_BIG_DATA的全量重绘)。这是 GDI+ 抗锯齿路径的固有代价;如需进一步提速,只能降低列分辨率或对该形态关闭抗锯齿 —— 两者都会改变显示效果,需用户确认后再动。 - 「构建为新信号」等一次性操作会真正计算整条派生曲线(约 1 帧的秒级耗时,正常)。
- 原始波形的「逐像素列包络带」用 GDI+
- 测量工具:
.preflight/perf/perf.exe <csv> [mode](离屏,不弹窗)。mode=0跑各交互场景耗时;mode=1导出确定性渲染 PNG 以便逐像素比对(用于验证"显示效果不变")。该目录已由.gitignore忽略,不会入库。
9.5 性能:启用带预览曲线的实验性功能后,交互严重卡顿(核心修复,含"为什么只有某些缩放等级卡"的解答)
-
症状(以
VERY_BIG_DATA.CSV,5 信号全部显示为例):- 「弱波增强」滑块拖动时在任何缩放等级下都卡顿;越缩得小(可见时间跨度越大)越卡 —— 时间跨度到 2.5s 时单帧 ≈89ms,完全放大(整段 128.8s)时单帧 ≈2.3 秒。
- 网格「选区放大」到某些跨度后,平移视界 / 改样式也卡。
-
根因(
Components/PlotView/Src/PlotView.cpp,CurveSpanFor+ 缓存策略 +Utilities/Src/DataModel.cpp的ComputeWeakWaveBoost):- 派生(预览)曲线按「可见采样窗口 ± 保护带」计算,代价正比于窗口内的采样数。实测:弱波增强整段(2576 万采样)单条 ≈368ms,均值滤波整段单条 ≈57ms。5 个网格就是 ≈1.8s。
- 旧代码有一条「超过 4M 采样的窗口不进缓存」的规则(为省内存),于是大跨度下每帧都重算整段曲线 —— 包括静止时的鼠标悬停重绘。这就是"某些缩放等级特别卡"的原因:
- 完全放大:窗口 = 整条信号(千万级)→ 每帧重算 → 秒级卡顿;
- 很小的时间跨度:窗口只有几千采样 → 计算只要微秒级 → 毫不卡顿;
- 中间跨度:窗口在十几万~几百万采样之间 → 单帧几十毫秒 → 卡,且保护带(≤65536)相对窗口太短,平移很容易越出缓存覆盖 → 频繁重算。
- 拖动「弱波增强」滑块每帧都改等级 → 缓存键变化 → 每帧都命中"未缓存"→ 每帧重算(叠加上述放大效应)。
-
修复(三管齐下,显示效果逐像素不变):
- 大窗口也进缓存:
kCurveSpanCacheMaxSamples提高到 64M(相当于不再按采样数排除),并新增总字节预算kCurveSpanCacheMaxBytes(≈768MB,最旧优先淘汰)兜底内存。这样同一参数下的平移/缩放/悬停都能复用,静止重绘降到 0.25ms。 - 交互期间「延迟重算」(
CurveSpanFor新增allowInteractiveDefer):拖动滑块/移动视界/缩放时,若所需窗口超过kDerivedInteractiveComputeBudget(131072 采样),本帧不重算,改复用上一次算好的曲线(曲线下标→时间的映射仍是当前视图,因此位置准确),交互结束后再按最终参数精确重算。窗口不大时(常见工作缩放)照常同步重算,预览曲线实时跟随控件。- 用途区分:预览绘制(
PaintMeanFilterCurve/PaintWeakBoostCurve/光标取值胶囊)传allowInteractiveDefer = true;需要精确取值的统计(中心轴边沿计数EdgeCenterCountsFor、MeanFilterValueAt/WeakBoostValueAt)传false(默认)。
- 用途区分:预览绘制(
- 计算缓冲复用:
ComputeWeakWaveBoost/ComputeMovingAverageIterated/ApplyWeakBoostStage的工作缓冲改为thread_local复用容量,避免每次重算都分配并零初始化上千万元素(多阶弱波增强 991ms→577ms)。
- 大窗口也进缓存:
-
实测(离屏,
VERY_BIG_DATA,5 网格;单位 ms/帧):场景 修复前 修复后 弱波增强 滑块拖动(整段) ≈2303 ≈18 弱波增强 滑块拖动(2.5s 跨度) ≈89 ≈15 弱波增强 滑块拖动(103ms / 20ms 跨度) ≈14 / ≈12 ≈15 / ≈11 弱波增强 静止重绘(整段) ≈2247 ≈0.25 均值滤波 平移(2.5s 跨度) ≈27 ≈23(大窗口非拖拽时仍按需重算;拖拽时延迟) 普通平移 / 光标移动 / 改色 3–12 不变(1–12) -
已知残留(不牺牲显示时的固有成本):把可见时间跨度拉到接近整段且开启弱波增强时,首次计算某参数/窗口的预览曲线仍需要一次 ≈1.8s(5 条整段曲线,各 ≈368ms)—— 交互期间不会触发(已延迟),但交互结束后的那一帧会补上。这是"整段弱波增强 O(n) 计算"的固有代价;若要进一步消除需把该计算移到后台线程(尚未做,改动较大)。
-
为什么这样改不牺牲显示效果:本次只改「何时计算 / 是否缓存」,不改任何几何或取值算法;
mode=1的逐像素比对(均值滤波 + 弱波增强,两个缩放级别,含光标取值胶囊)结果为 0 像素差异。
10. 变更日志(本文件维护记录)
| 日期 | 设备/模型 | 摘要 |
|---|---|---|
| 2026-10-11 | DeepSeek-V4.1-Flash | 新建本文件;记录项目要点与规范;修复「实验性功能」二级菜单瞬间关闭问题。 |
| 2026-10-11 | DeepSeek-V4.1-Flash | 修复右键网格菜单后悬停光晕/提示气泡残留;修复「启用带预览曲线的实验性功能后平移/拖光标 300ms+ 卡顿」(paintDerivedCapsule 巨窗每帧重算,收窄请求窗口,显示效果零变化)。 |
| 2026-10-11 | DeepSeek-V4.1-Flash | 深度修复「实验性功能预览曲线导致的交互卡顿」:大窗口改入缓存(静止重绘 2247ms→0.25ms)、交互期间延迟重算(弱波增强拖滑块 2303ms→18ms)、计算缓冲复用;显示逐像素不变。 |