新标签页分组默认折起 + GX 徽标悬停不改光标 + 标题栏 RAM 改私有工作集 + 边沿计数改为预算推进

一、新建标签页的信号分组默认折起
- Application::NewTab(bool collapseGroups):启动时的首个标签页传 false(加载文件后可直接勾选
  信号),用户新建的标签页(Ctrl+T / 标签栏 ⊕ / 标签栏右键)传 true。
- TabConfig::SetGroupsCollapsedByDefault(bool) + SetAllCollapsed(bool)(新增);
  SyncToDataSet 里 m_collapsed.resize(fileCount, m_groupsCollapsedByDefault)(原来固定 false)——
  只影响之后新出现的分组,已存在的分组保持用户当前选择;ToggleCollapsed 补建状态时同样用它。

二、网格 GX 徽标:悬停不改变鼠标形状
- PlotView::QueryCursor 里 GX 徽标分支由 IDC_HAND 改为 return nullptr(= 保持箭头),
  仍然保持"提前 return"的位置以免被光标线/图例判定抢走优先级;
  只有拖拽中(LANE_REORDER)才是 IDC_SIZENS。

三、标题栏 RAM 口径:总工作集 → 私有工作集(任务管理器"内存"列)
- 症状:加载大文件再关闭分组后,标题栏仍显示几十/上百 MB,任务管理器只有几 MB。
- 根因:原实现取 PROCESS_MEMORY_COUNTERS::WorkingSetSize(总工作集),它把映射进来的 DLL /
  共享页面以及已释放但尚未被系统回收的驻留页面都算进去;任务管理器"内存"列的口径是私有工作集。
- 修法:ProcessWorkingSetBytes() → ProcessPrivateWorkingSetBytes(),用 PROCESS_MEMORY_COUNTERS_EX2
  (MinGW 的 psapi.h 没有该类型 → 在 .cpp 里就地声明,布局与文档一致),失败时依次退回
  PrivateUsage(私有提交)→ WorkingSetSize。
- 实测(离屏探针):加载后 total/priv/commit = 117.55/109.96/138.28 MiB;
  关闭分组后 = 15.44/7.62/8.27 MiB ⇒ 内存没有泄漏、完整归还,用户的差异来自口径。

四、边沿计数(EdgeCountsFor)改为「逐块累积 + 每 tick 预算推进」
- 量化(探针):改前 6M 采样 = 26.8ms / 次阈值变化 → 25.8M 采样 ≈115ms/条;
  横向光标拖动时阈值每帧都变 ⇒ 单条信号每帧 115ms,5 条信号同一网格 = 575ms/帧
  (与用户"开启即卡顿、之后光标几乎拖不动"完全吻合)。
- 新模型:
  * 采样对 k(采样 k-1 → k)按 kEdgeBlockSamples = 8192 分块,每块各存上升/下降沿数;
    scannedPair = 下一个待扫描的采样对([1, scannedPair) 已扫完)。
  * 查询区间:内部整块用块和相加,两端边界块就地精确扫描(≤2 块,代价固定)⇒ 拖动时间光标的
    单帧代价与区间长度无关(单调时间轴下区间不进键);非单调时间轴无法用下标区间表达 ⇒ 区间进键。
  * 每 tick 预算 kEdgeScanPairsPerTick = 1200000(GetTickCount64 分桶,同帧多次调用共享);
    预算用完返回当前部分值,complete 出参告诉界面"还没算完"。
  * 拖动横向光标(EDGE_MOVE)期间不推进(阈值每帧都变,推进纯属白做功)→ 返回"最新的完整条目"
    的结果(数字稳定)+ complete = false。
  * 快路径:单调 && 无镜像 && 无调整变换时用紧凑无分支循环直接比较 values[k] 与阈值
    (NaN 与任何数比较都是 false ⇒ 天然满足"NaN 不参与且打断连续",无需 isfinite 分支)。
- 界面:新增 EdgeCountsText(rising, falling, complete) 统一生成文本(完全未算出显示"统计中…"、
  部分结果追加" …"),EdgePanelWidth 与绘制都改用它,宽度按实际文本测量避免数字被截断。
- 实测(改后):6M 采样首次调用 1.7ms(部分值)、5 帧内扫完且与参考实现逐位一致;
  单次调用最坏 1.9ms;拖动中连查 10 个阈值合计 0.11ms;区间查询在扫描完成后一次调用即精确。
- EdgeCenterCountsFor 未改(它的曲线窗口本来就被 CurveSpanFor 限制,不是本次报告的场景)。

五、内存与性能排查报告(第 4 项按用户要求只排查、未改动)
- 泄漏审计:RemoveFile 路径会 PauseDerivedAsync(清 m_curveEntries/m_curveJobs/m_curvePins)
  → InvalidateDerivedCaches(清折线/边沿/均值/弱峰/场景缓存),DataSet::RemoveFile 用
  "前移 + pop_back + shrink_to_fit" 立即归还采样缓冲;实测三种内存口径全部回落到十几 MB 以下。
- 弱峰调整内存暴涨(峰值 4.4GB):实测工作缓冲 ≈34 B/采样(主导项 = 极值表 PeakExtremum 24 B/极值,
  编码器方波极值密集 ⇒ 接近 24 B/采样),3M 采样单次 ≈103 MiB、4 条工作线程并发 +412 MiB;
  按 25.8M 采样外推 ⇒ 单次 ≈0.9GB、峰值 ≈3.5GB,再加数据本体 1.03GB 与曲线缓存预算 ≤768MB
  ⇒ 4~5GB,与用户观测吻合。
- 现有机制只解决"算完/关掉之后归还"(ReuseBufferScope / TrimDerivedCurveDataIfIdle /
  kHugeSignalPreviewSamples / 曲线缓存字节预算),没有任何"峰值上限"与"优雅降级":
  全仓没有 try/catch,重分配失败抛 std::bad_alloc 而重计算都在 std::thread 里
  (CurveWorkerLoop / RunAsyncLoad),异常逸出线程入口 ⇒ std::terminate() ⇒ 进程终止。
  1GB 剩余内存时:先大量换页(系统卡顿),提交失败则直接崩溃。
- 建议(未实施):①超大信号派生计算串行化 ②极值表字段瘦身/分块流式 ③废弃任务尽早退出
  ④GlobalMemoryStatusEx 自适应降档 ⑤线程入口加 try/catch。

六、验证(全部离屏,不占用鼠标/键盘/屏幕)
- 新增 .preflight/probe(诊断探针):内存口径与泄漏审计、边沿计数扫描耗时、弱峰工作缓冲峰值
  (带 1ms 峰值采样线程)。
- .preflight/uitest 新增 [AR](边沿计数预算/精确性/区间查询/拖动期 O(1)、GX 徽标悬停光标)与
  [AS](标题栏 RAM 口径 = 私有工作集);并给菜单类用例加了关闭残留菜单 + 允许补点的卫生逻辑。
- 合计 738/738 通过(连跑 5 次约 1 次出现既有 harness 抖动:菜单点击类用例,重跑即好);
  make release 零警告(Target.exe 被占用时链接报 Permission denied,用临时 -o 名字验证链接)。

规则同步:Documents/agent_mem.md 新增 §9.22(含内存与性能排查报告),§11.2 新增第 26–31 条硬约束,
变更日志补一行。
This commit is contained in:
DeepSeek V4.1 Flash committed 2026-10-14 14:12:54 +08:00
1 parent a2a38236fe
commit d6d3fcd6c1
7 files changed
+548 -119

No files matched your search

+145
View File
@@ -998,6 +998,127 @@ GDI+ 抗锯齿逐段扫描这些**近竖直、满幅**的边,代价爆炸(
---
### 9.22 新标签页默认折起分组 + GX 徽标悬停不改光标 + 标题栏 RAM 口径 + 边沿计数改为预算推进(本轮)
> 动机:用户报告 4 个交互/数值问题 + 1 个性能问题(本机起源设备)。全部离屏验证。
#### 一、新建标签页的信号分组**默认折起**
- `Application::NewTab(bool collapseGroups)`(新增形参):**启动时的首个标签页传 false**(加载文件后可直接勾选信号),
**用户新建的标签页(Ctrl+T / 标签栏 ⊕ / 标签栏右键「新建标签页」)传 true**。
- `TabConfig::SetGroupsCollapsedByDefault(bool)` + `TabConfig::SetAllCollapsed(bool)`(新增);
`SyncToDataSet` 里 `m_collapsed.resize(fileCount, m_groupsCollapsedByDefault)`(原来固定 `false`)——
⚠️ 只影响**新出现的**分组(之后新加载的文件),已存在的分组保持用户当前选择。
`ToggleCollapsed` 里为越界下标补建状态时同样用该默认值。
#### 二、网格 GX 徽标:悬停**不改变**鼠标形状,只有拖拽才是上下箭头
- `PlotView::QueryCursor` 里 GX 徽标分支由 `IDC_HAND` 改为 **`return nullptr`**(= 保持箭头)。
位置仍在原位(先于光标线/图例判定返回),优先级语义不变;
拖拽中(`LANE_REORDER`)仍是 `IDC_SIZENS`。
- ⚠️ 注意不要"顺手"把 `QueryCursor` 的徽标分支改成"落到后面继续判定":那会让经过徽标的光标线
抢到 `IDC_SIZEWE`(原实现用提前 return 就是为了这个优先级)。
#### 三、标题栏 RAM 口径:总工作集 → **私有工作集**(任务管理器"内存"列)
- **症状**:加载大文件再关闭分组后,标题栏还显示几十/上百 MB,而任务管理器只有几 MB。
- **根因**:原实现取 `PROCESS_MEMORY_COUNTERS::WorkingSetSize`(**总工作集**)——
它把映射进来的 DLL/共享页面、以及已释放但尚未被系统回收的驻留页面都算进去;
任务管理器"内存"列的口径是 **私有工作集**(`PROCESS_MEMORY_COUNTERS_EX2::PrivateWorkingSetSize`)。
实测(本机离屏探针):加载 `Problem_Data.csv` 后 total-WS **117.55 MiB** / priv-WS **109.96 MiB** /
commit 138.28 MiB;关闭分组后 total-WS **15.44** / priv-WS **7.62** / commit **8.27** MiB。
- **修法**:`ProcessWorkingSetBytes()` → **`ProcessPrivateWorkingSetBytes()`**,用
`PROCESS_MEMORY_COUNTERS_EX2`(MinGW 的 psapi.h 没有这个类型 → **在 .cpp 里就地声明**,
布局与文档一致),失败(老系统)依次退回 `PrivateUsage`(私有提交)→ `WorkingSetSize`。
- 验证:harness 新增 `[AS]` —— 标题文本必须包含 `FormatByteSize(privWS)`,且在两者格式不同时
**不得**包含 `FormatByteSize(totalWS)`(实测 priv 158.29 MB / total 195.53 MB)。
#### 四、边沿计数(`EdgeCountsFor`):逐块累积 + 每 tick 预算推进(**修掉"拖横向光标卡死"**)
- **量化(探针 `.preflight/probe`,合成方波)**:改前 **6M 采样 = 26.8 ms / 次阈值变化**
→ 折算 **25.8M 采样 ≈ 115 ms/条**。横向光标拖动时阈值每帧都变 ⇒ 单条信号每帧 115ms,
**5 条信号同一网格 = 575ms/帧**(用户报告"开启即卡顿、之后光标几乎拖不动"完全吻合)。
- **新模型**(与 `RenderPendingLaneRebuilds(1)` 同一套"宁可晚一点、绝不卡住前台"的思路):
* 采样对 `k`(采样 `k-1 → k`)按 `kEdgeBlockSamples = 8192` 分块,每块各存上升/下降沿数;
* `EdgeCountEntry::scannedPair` = 下一个待扫描的采样对,`[1, scannedPair)` 已扫完;
* **查询区间**:内部整块用块和相加,两端边界块就地精确扫描(≤ 2 块,代价固定)⇒
拖动**时间**光标的单帧代价与区间长度无关(区间**不进键**,单调时间轴下无需重扫);
非单调时间轴无法用下标区间表达 ⇒ 区间进键(进键与否由 `file.timeMonotonic` 决定);
* **每 tick 预算** `kEdgeScanPairsPerTick = 1200000`(`GetTickCount64` 分桶,同帧多次调用共享),
预算用完即返回当前部分值;`complete` 出参告诉界面"还没算完";
* **拖动横向光标(`EDGE_MOVE`)期间不推进**(阈值每帧都变,推进纯属白做功)→ 直接返回
"最新的完整条目"的结果(数字稳定)+ `complete = false`;
* 快路径:`单调 && 无镜像 && 无调整变换` 时用**紧凑无分支循环**直接比较 `values[k]` 与阈值
(NaN 与任何数比较都是 false ⇒ 天然满足"NaN 不参与且打断连续"的语义,无需 isfinite 分支)。
- **界面**:`EdgeCountsText(rising, falling, complete)` 统一生成文本 —— 完全未算出时显示
**`统计中…`**,部分结果追加 **` …`**;`EdgePanelWidth` 与绘制都改用它(宽度按实际文本测量,
避免推进过程中数字变长被截断)。
- **实测(改后)**:6M 采样首次调用 **1.7 ms**(部分值),5 帧内扫完且与参考实现**逐位一致**;
**单次调用最坏 1.9 ms**;拖动中连查 10 个阈值 **合计 0.11 ms**(O(1)/帧);
区间查询在扫描完成后**一次调用即精确**。
- ⚠️ 本轮**只改了普通「边沿计数」**;`EdgeCenterCountsFor` 仍沿用原增量实现
(它的曲线窗口本来就被 `CurveSpanFor` 限制,不是本次用户报告的场景)。
- ⚠️ harness 的 `EdgeCountsFor` 调用**必须循环到 `complete`**(模拟逐帧推进),
否则拿到的是部分值。
- ⚠️ **harness 抖动(菜单点击类用例)**:留在打开的弹出菜单/内联编辑态里的第一次点击会
被吞掉("输入框外的第一次点击 = 提交编辑"是产品设计)。本轮给菜单类用例加了
`CloseAllMenus(app)` 前置卫生与**允许补点 1~2 次**的逻辑(`[AL]` 的「系数/偏移」按钮、
`[AR]` 的调整按钮),显著降低抖动;但 `[AP]` 的 reset 图标点击仍**偶发**失败(连跑 5 次约 1 次),
重跑即好 —— 属既有 harness 抖动,不是产品缺陷。
#### 五、⚠️ 内存与性能排查报告(用户要求;第 4 项**只排查未改动**)
##### 5.1 内存管理审计(对应"标题栏数字不准 / 怀疑野指针")
- **结论 1:内存没有泄漏,且能完整归还。** 关闭分组(`RemoveFile`)路径实测
`117.55 → 15.44 MiB`(总工作集)/`109.96 → 7.62`(私有工作集)/`138.28 → 8.27`(私有提交)。
该路径会 `PauseDerivedAsync()`(清 `m_curveEntries`/`m_curveJobs`/`m_curvePins`)
→ `InvalidateDerivedCaches()`(清折线/边沿/均值/弱峰/场景缓存);`DataSet::RemoveFile` 用
"前移 + pop_back + shrink_to_fit" 立即归还采样缓冲。
- **结论 2:用户看到的差异来自"口径",不是"错误"。** 见上文第三节 —— 标题原来显示总工作集。
- **结论 3(⚠️ 真实隐患,本轮未修):全仓没有任何 `try`/`catch`。** 所有重分配
(`std::vector::assign/resize`)失败时抛 `std::bad_alloc`;而重计算都跑在
`std::thread` 上(`PlotView::CurveWorkerLoop`、`Application::RunAsyncLoad`,
以及并行图层 / 并行重建),**异常逸出线程入口 ⇒ `std::terminate()` = 进程直接终止**。
即"内存不足"在本项目当前实现里是**崩溃**而不是"报错并放弃本次操作"。
##### 5.2 弱峰调整 "迭代次数 1→10" 内存暴涨(4.4GB)——原因 / 改进方案 / 低内存行为
- **实测(探针,3M 采样合成方波,单次 `ComputeWeakPeakAdjust`)**:
单次计算的工作缓冲峰值 ≈ **103 MiB / 3M 采样 ≈ 34 B/采样**;
**4 条并发计算峰值 +412 MiB**(即 4 × 103 MiB)。按 25.8M 采样线性外推 ⇒
**单次 ≈ 0.9 GB,4 线程 ≈ 3.5 GB**;再加数据本体 1.03 GB 与曲线缓存预算(≤768 MB)
⇒ **峰值 4~5 GB**,与用户观测的 4.4 GB 吻合。
- **构成**(每 1 个采样):`current`+`next` 两个 `float` 工作缓冲 = 8 B;
调用方的 `out`(float)= 4 B;**极值表 `PeakExtremum`(`size_t index` + `double value` + `int type`
= 24 B/极值)**;位移表 `shift`(double)= 8 B/极值。
编码器方波是**极值密集**信号(每个跳变都是极值)⇒ 极值表可达采样数量级 ⇒ **24 B/采样成为主导项**。
- **为什么"拖到 10"会同时拉起 4 份**:`ScheduleCurveJob` 对同 (信号, 曲线种类) 只保留**一个待算**任务,
但**正在计算的任务无法取消**;工作线程池 = `clamp(核数-1, 1, 4)`,参数一路变化时会连续留下
若干个"算到一半的旧参数任务",每个都持有自己的 `thread_local` 大缓冲。
(`CurveSpanFor` 的 `kHugeSignalPreviewSamples` 只在**拖拽期间**不再排队,松手后仍会算一次最终参数。)
- **改进方案(建议,未实施)**:
1. **按信号规模限制并发**:超大信号(> `kHugeSignalPreviewSamples`)时派生计算**串行化**(1 线程)
⇒ 峰值直接除以 4;
2. **缩小极值表**:`{uint32 index; float value; int8 type}` = 12 B(减半),或**分块流式处理**,
不落地整张极值表;
3. **抢占/取消**:任务在每阶之间检查"本参数是否已被更新的参数取代"(比对 generation/最新键),
废弃任务尽早退出,避免为过时参数白算 10 阶;
4. **低内存自适应**:跑之前用 `GlobalMemoryStatusEx` 看可用物理内存,不足时降档
(减少并发 / 拒绝本次重建 / 明确提示"内存不足");
5. **异常保护**(见 5.1 结论 3):给所有线程入口加 `try/catch(std::bad_alloc)`,
把"崩溃"变成"提示 + 跳过本次计算"。
- **低内存场景(1 GB 剩余)现有机制够不够?**
* **有的**:`ReuseBufferScope`(容量封顶,超大缓冲用完即释放)、
`TrimDerivedCurveDataIfIdle`(关掉预览即 `PauseDerivedAsync` 归还内存)、
`kHugeSignalPreviewSamples`(拖拽期不排队)、曲线缓存总字节预算 `kCurveSpanCacheMaxBytes`。
* **没有的**:**任何"峰值上限"与"优雅降级"**。上面 4 项都只解决"算完/关掉之后归还",
不解决"算的时候需要 4 GB"。
* 因此 1 GB 剩余内存时的行为是:先大量换页(全系统卡顿)→ 提交失败则 `bad_alloc`
→ **无线程级 catch ⇒ 进程终止**(崩溃)。用户这台机器内存足够所以只表现为"内存暴涨"。
---
## 10. 变更日志(本文件维护记录)
| 日期 | 设备/模型 | 摘要 |
@@ -1031,6 +1152,7 @@ GDI+ 抗锯齿逐段扫描这些**近竖直、满幅**的边,代价爆炸(
| 2026-10-13 | DeepSeek-V4.1-Flash(**另一台设备**) | **深放大中键拖拽卡顿(根因=GDI+ 抗锯齿"满幅锯齿"病态)+ 横向补条裁剪 + 并行网格图层(B/C,详见 §9.18)**。① **B 拆解**:该视界(`[10.2719,10.2831]s`)下 `RollWideBuffer` 补条重绘 **13.08ms**,其余 ≤0.8ms —— 瓶颈全在补条重绘。② **补条裁剪**:横向补条只覆盖窄竖带,却按整条宽窗口构建/描边整段折线;改为按条带 x 范围裁剪采样区间(旁路折线缓存),`RollWideBuffer` **13.08ms → 1.73ms**,拖拽整帧 13~24ms → ~3ms,且**逐像素 0 差异**。③ **病态根因**:拖拽超出预渲染余量时走整块重画,**整宽单网格渲染 655ms** —— 同 4481 点 `DrawLines` 实测 **AA 694ms / 无 AA 29ms(23 倍)**(编码器方波在 1~2 采样/像素形成每列整幅跳变的锯齿,原实现此时既不走包络也不关 AA);修法:**包络带阈值从 2 采样/像素降到 1**,并扩展"梳状(关 AA)"判据(多数列列内跨幅 >0.5×网格高 / 相邻列整幅跳变)——**几何一字未改**;整宽渲染 **655→47.7ms**、补条 6.9→2.4ms、拖拽 200 帧 **avg 4.5 / max 5.8ms / 0 帧 >8ms**。④ **C(用户解除多线程限制)**:实现 `RebuildLaneLayersParallel` —— 每线程一个**隔离 `PlotView`**(自有 `m_workerTab`/`m_area`/`m_plotRect`/`m_lanes`)渲染不同网格,主线程 join 后按真实下标重算 key;并行 vs 顺序**逐像素 0 差异**;⚠️ 踩坑:`BuildLaneVisuals` 在 `m_tab==nullptr` 时清空 `m_lanes`,故**必须给 worker 一个非空的自有 tab**。**诚实结论:并行仅 1.03x**(同机 4 线程 CPU 真并行 3.98x → GDI+ 光栅化被串行化),故**并行对光栅化无收益**,仍保持"视界停后每帧补 1 个网格"。验证:`dragperf` mode 0/1/3/5/6/8/9、`pancheck` 全 `dy=0`、`zoomcheck` 无回归、`make clean && make all` 零警告。 |
| 2026-10-14 | DeepSeek-V4.1-Flash | **装饰层改为带 alpha 的独立图层(消除 GX/图例外围"直角白边")+ 图例圆角托底与悬停/选中配色统一 + 预览曲线内存释放(详见 §9.20)**。① **白边根因**:装饰层原为"从后备缓冲区抓取一块**不透明矩形**",圆角之外(属于波形)的像素被一起烘进缓存,为让其跨视界可复用又在圆角外铺了直角不透明底色 ⇒ 密集波形上就是一圈直角白边。**修法**:`m_decorDc` 改 **32bpp DIBSection**、新增 `RenderDecorCache()` 把装饰**单独绘制**进全透明位图(不再抓取)、`BlitDecorCache()` 改用 **`AlphaBlend`**(`AC_SRC_ALPHA`,`GetProcAddress(gdi32!GdiAlphaBlend)` 解析、不新增链接依赖)⇒ 圆角外透出**当前**波形;并做 **alpha 修补**(GDI 文本会把写入像素的 alpha 清 0,不修补文字会变透明洞);删除全部外扩直角底色。② **图例**:逐条目圆角托底(与 GX 徽标同风格),**选中态 = 悬停态配色**(`accentSoft`+`accent`+`textStrong`),悬停在不透明托底上重绘色块/文字并保留曲线光晕;`LegendHit` 新增 `radius`;补齐「+N」/「(无可见信号)」提示的装饰矩形。③ **内存**:`thread_local` 工作缓冲**保留容量封顶**(`kReuseBufferMaxElements` + `ReuseBufferScope`,超大计算用完即释放)+ 关闭最后一个预览功能时 `TrimDerivedCurveDataIfIdle()`(`PauseDerivedAsync` 让线程退出、thread_local 析构归还内存)—— 实测 1792MB → **关闭后 1203MB**(修复前不下降)。④ 性能:`BlitDecorCache` **0.19ms/帧**、`RenderDecorCache` 2.4ms(仅内容变化时);`zoomcheck` mode 13 收敛帧与整幅重绘**逐像素 0 差异**(此前 3503)。验证:离屏 `verify.exe` **49/49 通过**、`make release` 零警告。 |
| 2026-10-14 | DeepSeek-V4.1-Flash | **修复远端「调整」功能引入的性能回归 + 根治首帧卡顿 + 图例交互/托底重做(详见 §9.19)**。① **8 倍回归**:远端把 `DataSet::Style(ref)` 写进了 `AutoRangeLaneToView` 的逐采样循环(亿级解析调用)→ 工具栏「Y 轴自适应」在全视图下 **1200ms**(旧提交 147ms);修法:样式指针提循环外 + **整段自适应取已缓存 min/max(O(1),结果逐位一致)** + 无变换走紧凑循环 → **0.02ms**。② **首帧卡顿根治**:加载后宽基底图层从未分配,用户第一次平移/缩放时 `Paint` 在那一帧**同步重建全部 5 个网格图层**(首帧 **64.3 → 13.0ms**);修法:新增 `WarmLaneLayersStep()`,在**命中静态场景缓存**(视界未变、非拖拽)时用预算机制**每帧至多补 1 个网格**,把整批重建摊到静止帧。③ **派生信号把「调整」烘进数据**(不再继承样式指纹);⚠️ 当时写的“仅 `offsetTime` 因共用文件时间轴仍需继承”**已被 §9.21 推翻**(现在 `offsetTime` 也按 `time[i]-offsetTime` 重采样烘进采样值)。④ **GX 拖拽让位判据改为鼠标指针**(鼠标越过相邻网格中心水平线即让位),并给 GX 徽标加悬停高亮(叠加层 + `IDC_HAND`,与信号列表 GX 徽标一致)。⑤ **图例命中矩形在装饰缓存路径下必须重建**(原先命中缓存时提前 return → `m_legendHits` 被清空且不重建 → 中键平移后图例悬停/点选全失效)。⑥ **每个图例条目加圆角托底 + 装饰矩形改为逐块 + 托底不透明铺满外接矩形** → 消除「GX/图例周边旧波形像素残留」(逐像素验证装饰矩形内 **0 差异**);悬停高亮为不透明托底并**重绘色块与文字**(否则内容被盖住)。验证:离屏 Application harness **40/40 通过**,`zoomcheck` mode 20–26 量化,`make release` 零警告。 |
| 2026-10-14 | DeepSeek-V4.1-Flash(**起源设备**) | **新标签页分组默认折起 + GX 徽标悬停不改光标 + 标题栏 RAM 改用「私有工作集」+ 边沿计数改为「逐块累积 + 每 tick 预算推进」(详见 §9.22)**。① `NewTab(bool collapseGroups)`(首个标签页 false、用户新建 true)+ `TabConfig::SetGroupsCollapsedByDefault/SetAllCollapsed`;`SyncToDataSet` 用该默认值初始化**新出现的**分组。② `QueryCursor` 的 GX 徽标分支由 `IDC_HAND` 改为 `nullptr`(悬停不改形状,仅拖拽时 `IDC_SIZENS`)。③ **RAM 口径修正**:原取 `WorkingSetSize`(总工作集,含映射 DLL/共享页与已释放未回收的驻留页)→ 改为 `PROCESS_MEMORY_COUNTERS_EX2::PrivateWorkingSetSize`(与任务管理器「内存」列一致;失败退回 `PrivateUsage` → `WorkingSetSize`)。实测加载后 117.55/109.96 MiB、关闭分组后 15.44/7.62 MiB —— **无泄漏,内存完整归还**。④ **边沿计数性能**:改前 25.8M 采样 ≈**115ms/条/帧**(5 条同网格 = 575ms/帧,用户报告"开启即卡顿、之后拖不动");改为按 `kEdgeBlockSamples=8192` 分块累积、每 tick 预算 `kEdgeScanPairsPerTick=1.2M`、区间用块和相加(区间不进键)、拖动横向光标期间不推进并显示上一个完整值 + `complete=false`(`EdgeCountsText` 显示"统计中…/…");实测首帧 1.7ms、单次最坏 1.9ms、拖动中 10 次查询 0.11ms、最终结果与参考实现逐位一致。⑤ **内存排查报告**(用户要求,第 4 项只排查未改动):弱峰调整工作缓冲实测 **≈34 B/采样**(极值表 `PeakExtremum` 24 B/极值为主导),**4 条工作线程并发 +412 MiB / 3M 采样** ⇒ 25.8M 采样峰值 ≈**3.5GB**(+数据 1.03GB +曲线缓存 ≤768MB ⇒ 4~5GB,与用户 4.4GB 观测吻合);**全仓无 `try`/`catch`** ⇒ 低内存时 `bad_alloc` 逸出线程入口 ⇒ `std::terminate()` 崩溃(无优雅降级机制)。验证:离屏 `uitest` **738/738 通过**(新增 `[AR]` 边沿计数预算/精确性/拖动期 O(1)、`[AS]` 标题栏 RAM 口径),`make release` 零警告。 |
| 2026-10-14 | DeepSeek-V4.1-Flash(**起源设备**) | **边沿计数改用「绘制值」+ 派生信号烘入时间偏移 + 图例托底统一为 GX 徽标款 + 调整二级面板向上展开(详见 §9.21)**。① **边沿计数恒为 0**:`EdgeCountsFor`/`EdgeCenterCountsFor` 只比原始采样值,而阈值是屏幕上的值 → 「系数 50」的信号统计恒为 0;修法:判定值改为 `ApplyDrawTransform(style, values[SourceIndexFor(...)])`(中心轴两侧同套变换;水平镜像时曲线窗口也要先映射下标);缓存新增 **`styleKey`**(否则改完「调整」仍命中旧计数)。② **区间边界点被误计**:`EdgeIndexRange` 左端“多取一个采样点”的外扩**已删除**,收紧为“**两个端点都在区间内**才计入”(光标恰落在越过阈值的点 B 上时 B 不再被算作上升沿)。③ **`offsetTime` 也烘进派生采样值**(**推翻 §9.19 的“唯一例外”**):按 `time[i]-offsetTime` 取最近采样点重采样(单调时间轴用只前进的游标,O(n);越界置 NaN),新信号样式**全默认**;非单调时间轴退回到把 `offsetTime` 带在样式上。④ **图例托底 = GX 徽标同款**(同高 / 圆角 = 高度一半 / 同配色 / 水平内边距 = 胶囊半径),文本改用新增字体 **`uiTiny`(10 逻辑 px)**;`kLegendPlateRadius` 已删。⑤ **`PopupMenu::Open(..., expandUp=true)`**(锤点为**左下角**、向上展开,`m_buildExpandUp` 供 `RebuildLayout` 复用)+ 「系数/偏移」「镜像」面板锤在按钮左上角再上移 `kAdjustMenuGap`。验证:`.preflight/uitest` 新增 `[AR]`(五组断言全部与**独立参考实现**逐位比对,含 A/B:“按原始采样值算会得到 7/6 个边沿”);**并同步修正 harness 里两处旧规则断言**(`[AP]` 派生信号继承 → 默认样式 + 烘值;`[AN]` 图例带无托底 → 装饰矩形逐块且暴露列可见底层内容);合计 **722/722 通过**,`make release` 零警告。 |
---
@@ -1110,6 +1232,29 @@ GDI+ 抗锯齿逐段扫描这些**近竖直、满幅**的边,代价爆炸(
25. ⚠️ **`PopupMenu::Open(..., expandUp = true)`** 表示"锚点是菜单**左下角**、向**上**展开"。
给按钮挂"二级面板"时用它(并把锚点设在按钮左上角再上移 `kAdjustMenuGap`);
`RebuildLayout()` **必须复用 `m_buildExpandUp`**,否则数值回写触发的重排会让面板跳回向下展开。
26. ⚠️ **标题栏 RAM = "私有工作集"**(`PROCESS_MEMORY_COUNTERS_EX2::PrivateWorkingSetSize`,
与任务管理器「内存」列同口径),**不是** `WorkingSetSize`(总工作集还包括映射的 DLL/共享页面与
已释放但未被回收的驻留页面,会明显偏大)。EX2 失败时退回 `PrivateUsage` → `WorkingSetSize`。
27. ⚠️ **千万级信号上任何"按阈值判定"的统计都不允许每帧重扫整段**:
`EdgeCountsFor` 已改为「逐块累积(`kEdgeBlockSamples`)+ 每 tick 预算推进(`kEdgeScanPairsPerTick`)」;
拖动横向光标(`EDGE_MOVE`)期间**不推进**(只返回上一个完整值 + `complete=false`),
界面用 `EdgeCountsText` 显示"统计中… / …"。实测:改前 25.8M 采样 ≈115ms/条/帧(5 条同网格 = 575ms/帧),
改后单次调用 ≤1.9ms、拖动中 10 次查询合计 0.11ms。
⚠️ 新增同类统计时**必须**沿用这个模型(先量化单次全扫代价,再决定分块与预算)。
28. ⚠️ **全仓没有 `try`/`catch`**:所有重分配失败抛 `std::bad_alloc`,而重计算都在 `std::thread`
(`CurveWorkerLoop` / `RunAsyncLoad` / 并行图层)里 —— 异常逸出线程入口 ⇒ `std::terminate()` ⇒ **进程终止**。
低内存下这是"崩溃"而不是"降级"。要改就从线程入口加 `catch` 开始。
29. ⚠️ **弱峰调整的工作缓冲 ≈ 34 B/采样(峰值)**,主导项是极值表(`PeakExtremum` = 24 B/极值,
编码器方波极值密集 ⇒ 接近 24 B/采样);而工作线程池最多 **4** 条并发 ⇒
25.8M 采样时单次 ≈0.9GB、峰值 ≈3.5GB(+数据 1.03GB +曲线缓存 ≤768MB ⇒ **4~5GB**)。
已测:3M 采样单次 ≈103MiB、4 并发 +412MiB。若日后要"限制峰值",优先:
①超大信号串行化 ②极值表字段瘦身/分块流式 ③废弃任务尽早退出 ④`GlobalMemoryStatusEx` 自适应降档。
30. ⚠️ **新建标签页的信号分组默认折起**(`NewTab(true)`;启动首个标签页 `NewTab(false)` 保持展开);
`SyncToDataSet` 用 `TabConfig::SetGroupsCollapsedByDefault` 的值初始化**新出现的**分组,
不得覆盖已存在分组的用户选择。
⚠️ 新增/修改 `NewTab` 调用点时别忘了带上这个形参。
31. ⚠️ **网格 GX 徽标悬停不改鼠标形状**(`QueryCursor` 在徽标上返回 `nullptr`,且**提前 return**
以免被光标线/图例判定抢走);只有拖拽中(`LANE_REORDER`)才是 `IDC_SIZENS`。
### 11.3 离屏验证方法学(本机偏好,强烈建议沿用)