Files
Just-DataInspector/Documents/agent_mem.md
T
DeepSeek V4.1 Flash 99247622c1 派生曲线计算与 UI 绘制分离(后台线程 + 双缓冲),彻底消除交互卡顿
根因:重计算发生在绘制线程内(Paint 同步算派生预览曲线),一次整段弱波增强约 368ms 会阻塞消息循环,滑块/光标/视界失去跟手。

方案:新增后台计算线程 + 双缓冲。大窗口预览曲线不在绘制线程计算,本帧用最新旧曲线顶替并排后台任务;小窗口仍同步以保证预览实时;后台算完通过自定义消息通知宿主重绘(双缓冲切换)。条目用 shared_ptr 共享、绘制线程钉住整帧有效,后台只追加不改写,无数据竞争。任何改变数据层结构的操作前先 PauseDerivedAsync(join 后台线程),杜绝读取已释放/已移动采样。

实测 VERY_BIG_DATA(5 网格):弱波增强平移 2247ms→15ms,拖滑块 2303ms→15ms,静止重绘→0.25ms,交互均 ≤约 30ms,无 300ms/2s 级尖峰。小窗口同步路径与旧实现逐像素 0 差异。
2026-10-11 12:04:53 +08:00

241 lines
24 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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 工作流要求
1. 接手前先**探索工作区/提交历史/代码**建立认知(本文件应作为第一参考)。
2. 完成需求后**编译验证**(`make release` 至少成功),必要时用 `TestData/` 数据功能验证。
3. **验证与探索时尽量不占用用户的鼠标/屏幕/键盘**(后台构建为主)。
4. 本地仓库提交完成后,**不要在未经指示的情况下继续探索或优化**,等待用户下一步指令。
5. **每次完成任务后更新本文件**(需求、修复、约定变更)。
---
## 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` → `paintDerivedCapsule` lambda):
- 绘制时间光标处的**派生曲线取值胶囊**时,把请求窗口取成「**可见区间 ∪ 光标采样下标**」的并集;
- 当光标不在当前视界内(平移视界会把光标甩到视界外)时,这个并集会变成 `[光标下标, 视界右端]` 或 `[视界左端, 光标下标]` —— 上千万采样的**巨大窗口**;
- 该窗口超过 `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 帧的秒级耗时,正常)。
- **测量工具**:`.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`):
1. 派生(预览)曲线按「可见采样窗口 ± 保护带」计算,**代价正比于窗口内的采样数**。实测:弱波增强整段(2576 万采样)单条 **≈368ms**,均值滤波整段单条 ≈57ms。5 个网格就是 ≈1.8s。
2. 旧代码有一条「**超过 4M 采样的窗口不进缓存**」的规则(为省内存),于是**大跨度下每帧都重算**整段曲线 —— 包括静止时的鼠标悬停重绘。这就是"某些缩放等级特别卡"的原因:
- 完全放大:窗口 = 整条信号(千万级)→ 每帧重算 → 秒级卡顿;
- 很小的时间跨度:窗口只有几千采样 → 计算只要微秒级 → **毫不卡顿**;
- 中间跨度:窗口在十几万~几百万采样之间 → 单帧几十毫秒 → 卡,且保护带(≤65536)相对窗口太短,平移很容易越出缓存覆盖 → 频繁重算。
3. 拖动「弱波增强」滑块每帧都改等级 → 缓存键变化 → 每帧都命中"未缓存"→ 每帧重算(叠加上述放大效应)。
- **修复**(三管齐下,**显示效果逐像素不变**):
1. **大窗口也进缓存**:`kCurveSpanCacheMaxSamples` 提高到 64M(相当于不再按采样数排除),并新增**总字节预算** `kCurveSpanCacheMaxBytes`(≈768MB,最旧优先淘汰)兜底内存。这样同一参数下的平移/缩放/悬停都能复用,静止重绘降到 **0.25ms**。
2. **交互期间「延迟重算」**(`CurveSpanFor` 新增 `allowInteractiveDefer`):拖动滑块/移动视界/缩放时,若所需窗口超过 `kDerivedInteractiveComputeBudget`(131072 采样),**本帧不重算**,改复用上一次算好的曲线(曲线下标→时间的映射仍是当前视图,因此位置准确),交互结束后再按最终参数精确重算。窗口不大时(常见工作缩放)照常同步重算,预览曲线**实时跟随控件**。
- 用途区分:**预览绘制**(`PaintMeanFilterCurve`/`PaintWeakBoostCurve`/光标取值胶囊)传 `allowInteractiveDefer = true`;**需要精确取值的统计**(中心轴边沿计数 `EdgeCenterCountsFor`、`MeanFilterValueAt`/`WeakBoostValueAt`)传 `false`(默认)。
3. **计算缓冲复用**:`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`(惰性启动、析构/结构变化时 `StopCurveWorker` join)。线程只读采样、在锁外计算,算完把结果作为**不可变条目**(`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)兜底,最旧优先淘汰。
---
## 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 差异。 |