31 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 像素差异。
9.6 架构:派生曲线计算与 UI 绘制分离(后台线程 + 双缓冲)
- 动机/结论:界面卡顿的根因是「重计算发生在绘制线程里」(
PlotView::Paint内同步算派生曲线)。只要某次Paint要算整段弱波增强(单条 ≈368ms),消息循环就被阻塞,滑块/光标/视界全部失去跟手。因此把预览曲线的重计算移到后台线程,绘制线程永不等计算。 - 实现(
Components/PlotView/,全部改动仅在PlotView.hpp/.cpp与宿主Application.cpp):- 后台工作线程:
m_curveWorker+ 任务队列(m_curveJobs)+condition_variable(惰性启动、析构/结构变化时StopCurveWorkerjoin)。线程只读采样、在锁外计算,算完把结果作为不可变条目(shared_ptr<const CurveEntry>)追加进m_curveEntries,再PostMessage(kMessagePlotDerivedReady)通知宿主重绘。 - 双缓冲:
CurveSpanFor命中覆盖区间的条目就直接用;未命中且窗口大(>kDerivedAsyncWindowThreshold= 65536 采样)时本帧不计算,改用该曲线最新的旧条目顶替(AcquireNewestCurve),同时排一个后台任务;窗口小(≤门槛)仍同步计算以保证预览实时跟随滑块。条目用shared_ptr共享、绘制线程取到后钉进m_curvePins保持整帧有效,后台只追加不改写 → 无数据竞争。 - 一致性与生命周期:后台发布新条目后置
m_curveSceneDirty,下一帧作废静态场景缓存以纳入新曲线;任何会改变数据层结构(增删文件/信号、插入派生信号)的操作前,宿主先调用PlotView::PauseDerivedAsync()(join 后台线程 + 清队列/缓存 + 自增代次),杜绝后台线程读取已释放/已移动的采样。InvalidateDerivedCaches()内部也会PauseDerivedAsync()。 - 精确取值路径(中心轴边沿计数
EdgeCenterCountsFor、取值胶囊需要精确值时)传allowAsyncCompute = false,走同步计算。
- 后台工作线程:
- 实测(离屏,
VERY_BIG_DATA,5 网格;单位 ms/帧):弱波增强平移(整段)2247 → 15;弱波增强拖滑块(整段)2303 → 15;弱波增强静止重绘 0.25;后台结算后的全质量帧 ≈19ms/帧(一次性)。所有交互均 ≤ ~30ms,不再有 300ms/2s 级尖峰。 - 显示效果:小窗口(同步)路径与旧实现逐像素 0 差异;大窗口(异步)路径的曲线像素与旧实现一致(差异仅出现在合成测试中一个与缓存相关的旧实现的时轴陈旧现象上,新实现按设定视图正确绘制)。后台完成后的全质量帧与同步结果相同。
- 代价:大窗口缓存条目可占用较多内存(整段弱波增强单条 ≈104MB/信号),由
kCurveSpanCacheMaxBytes(768MB)+ 条目数上限(32)兜底,最旧优先淘汰。
9.7 完善异步架构 + 预览曲线层级修正(本仓库当前状态)
在 9.6 基础上做了三项关键完善:
- 后台线程池(真·多线程):原先是单线程顺序计算,5 个网格各一条整段弱波增强要 5×368ms≈1.8s,用户会长时间看不到预览曲线(看起来"完全没画出来")。改为
m_curveWorkers线程池(EnsureCurveWorker,线程数 =clamp(硬件并发-1, 1, 4),给 UI 留一个核),多条曲线并行计算,预览曲线因此很快出现。StopCurveWorker会 join 全部线程。后台线程优先级设为THREAD_PRIORITY_BELOW_NORMAL,保证拖动时 UI/绘制线程始终优先获得 CPU(计算只在 UI 空闲时占用),避免"多线程反而抢核导致卡顿"。 - 预览曲线绘制在「本网格全部原信号之上」:原先在同一个信号循环里就地叠加派生曲线,导致网格内有多条信号时,后绘制的原信号会把先前信号的预览曲线盖住(用户明确要求"预览信号处于原信号上层不要被覆盖")。现在
PaintLane分两遍:第一遍画全部原信号(线/散点/光晕),第二遍画全部派生预览曲线。 - 快速帧(关抗锯齿)只用于「会改变画面主体」的拖拽:原先
m_fastFrame = (拖拽中),会给静态场景缓存打上「已降级」标记 → 缓存失效 → 每帧整幅重画。像拖时间光标 / 选区框选 / 拖浮动面板这些只改叠加层、主体不变的交互因此被拖慢(实测拖光标从约 1ms 升到十几毫秒)。现在只有PAN / SCROLL_LANES / LANE_REORDER / MOVE_SIGNAL / BOOST_LEVEL / BOOST_ITERATIONS才算快速帧;其余拖拽直接复用静态场景缓存(贴图 ~1ms)+ 叠加层。实测拖光标 全程命中缓存、约 1–4ms。
- 平移(尤其"一大格=2ms")的固有下限:平移会让视图每帧变化 → 静态场景缓存必然失效 → 每帧都要重画波形(这部分无法异步,因为 GDI+ 绘制必须在 UI 线程;异步只覆盖"派生预览曲线"的计算)。在 5 个网格、含 2576 万采样编码器方波的
VERY_BIG_DATA上,实测拖拽平移约 6–15ms/帧(机器空闲/繁忙分别为下限/上限),已无尖峰。若日后要进一步降低,只能减少波形绘制本身(受"不牺牲显示效果"约束)或把整帧渲染也搬到离屏线程(改动很大、风险高)。 - 预览曲线"看起来没画出来"的真相:对编码器方波这类强信号,弱波增强几乎不改动波形,所以预览曲线与原信号几乎重合(读数胶囊里两者数值只差末位),视觉上"看不见"——这是正确行为。用
mode=1导出可确认曲线确实被绘制(G1–G5 均有浅色叠加 +「(弱波增强)」读数)。小跨度渲染与旧实现逐像素 0 差异。
9.8 预览曲线严格等同「构建为新信号」+ 平移装饰层缓存(本仓库当前状态)
(一)预览曲线与「构建为新信号」逐位一致
- 原实现按「可见区间 ± 保护带」计算预览曲线(近似,边界可能略有差异)。用户明确要求"预览显示效果要和构建为新信号一致"。
- 现改为一律按整条信号计算(
PaintMeanFilterCurve/PaintWeakBoostCurve传begin=0, end=count):与BuildDerivedSignal调用的是同一个ComputeWeakWaveBoost/ComputeMovingAverageIterated、同一段values,因此逐位一致。整段窗口超过异步门槛时由后台线程计算(绘制线程不阻塞)。
(二)平移期间的「装饰层」像素缓存(m_decorDc)
- 实测(
VERY_BIG_DATA,2ms/大格)整幅重绘约 10ms,其中 图例 ≈3.5ms、Y 轴刻度 ≈1.5ms(文字 + 圆角底板),二者合计过半;而它们只随网格几何/信号外观变化,与时间轴视图无关(平移时不变)。 - 实现:每张"全质量帧"后,把图例带(编号徽标 + 底板 + 色块 + 文字)与Y 轴刻度胶囊的像素从后备缓冲区捕获进
m_decorDc;拖拽(快速帧)时跳过重绘、直接BitBlt贴回。用专用指纹SceneDecorHash()(含外观样式、不含派生曲线参数)校验,内容一旦变化即失效并重新捕获,绝不使用过期像素。仅捕获装饰矩形本身(逐胶囊 + 图例带),矩形之间的波形照常绘制,因此无错位/无鬼影。 - 效果:拖拽平移(2ms/大格)整帧 13ms → 约 5ms;0.4% 跨度 20→8ms。注意:该缓存只作用于「拖拽(快速帧)」——静止帧仍按全质量重绘装饰(保证最终画质)。
- 拖弱波增强滑块时,
SceneDecorHash不含派生参数 → 装饰层不会因滑块每帧变参数而失效(否则拖滑块要重绘图例/刻度)。
(三)后台任务去重(避免拖滑块淹没线程池)
- 拖滑块每帧改参数 → 若每帧都入队"整条曲线"任务,会灌满队列、后台线程池被淹没并与 UI 抢 CPU。现按「信号 + 曲线种类」只保留一个待算任务、以最新参数为准(旧的待算任务被移除;正在计算的那个结果由后续条目取代)。
(四)后台线程优先级
- 见 9.7 第 3 点:后台线程为
THREAD_PRIORITY_BELOW_NORMAL,UI 需要运行时永远优先获得 CPU。
(五)平移仍有固有下限(诚实说明)
- 平移必然让视图每帧变化 → 静态场景缓存失效 → 波形必须在 UI 线程重画(GDI+ 只能在 UI 线程绘制,无法异步)。装饰层缓存已把"与视图无关"的部分(图例/刻度)从每帧开销中移除,剩余开销是波形本身 + 网格线 + 时间轴,属"不牺牲显示效果"下的固有成本。
(六)离屏逐像素校验(含历史踩坑提醒)
- 工具:
.preflight/perf/perf_<ver>.exe <csv> 1 <prefix>导出确定性 PNG;.preflight/perf/compare.ps1用LockBits+Marshal.Copy逐字节比对。 - 踩坑:比对若用
[System.Drawing.Bitmap]::GetPixel且两侧位图尺寸不一致或文件缺失,会抛调用"GetPixel"时发生异常:"参数无效"。根因通常是"旧版本"的 harness 链接失败(例如它引用了新版本才有的符号)导致对照图没生成。务必:①先确认两侧 PNG 都存在且尺寸一致;②用LockBits而非逐点GetPixel(后者对上兆像素级图片极慢)。 - 实测结论(
709cbe4的"可见窗口预览" vs389b4ec的"整段预览",同 harness 同数据):全部全质量帧(均值滤波/弱波增强 × FitAll/2.5s/103ms 三个缩放)逐像素 0 差异 —— 证明"改为整段计算"未带来任何显示回归,预览与"构建为新信号"一致;仅拖拽中途帧有 0.09% 差异(快速帧关抗锯齿 + 装饰层贴回的预期差异)。
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)、计算缓冲复用;显示逐像素不变。 |
| 2026-10-11 | DeepSeek-V4.1-Flash | 架构升级:派生曲线计算与 UI 绘制分离(后台线程 + 双缓冲)。绘制线程永不等计算,弱波增强拖滑块/平移从 2303/2247ms 降到 ~15ms;结构变化前 PauseDerivedAsync 保证线程安全;小窗口同步路径与旧实现逐像素 0 差异。 |
| 2026-10-11 | DeepSeek-V4.1-Flash | 完善异步:后台改多线程池(预览曲线并行计算、更快出现);PaintLane 分两遍把预览曲线画在全部原信号之上(不再被覆盖);快速帧只用于改主体的拖拽(拖光标/框选改为命中场景缓存,约 1–4ms)。小跨度逐像素 0 差异。 |
| 2026-10-11 | DeepSeek-V4.1-Flash | 预览曲线改为按整条信号计算,与「构建为新信号」逐位一致;新增平移装饰层像素缓存(图例 + Y 轴刻度在拖拽时直接贴回,2ms/大格拖拽 13ms→约 5ms);后台任务按 (信号,种类) 去重以防拖滑块淹没线程池。 |