派生曲线内存治理(弱峰调整等)+ 边沿统计改为统一预算状态机(含中心轴)+ 离屏测试零打扰

一、派生曲线内存治理(按上一轮报告的方案实施;绘制性能优先,同时为小内存设备留出空间)
1. PeakExtremum 字段重排(double value + uint32 index + int8 type):24 → 16 字节。
   编码器方波这类极值密集信号省约 1/3 的极值表内存,且**不改变任何数值结果**
   (value 保持 double,改成 float 会让结果与历史版本不一致):
   实测 3M 采样单次计算的工作缓冲峰值 33.23 → 26.73 MiB。
2. ComputeDerivedWindow 对「整段窗口」不再复制采样(直接引用原向量):
   8 B/采样 × 2580 万 ≈ 206 MB/条,而“预览曲线一律按整条信号计算”正是最常见情形。
3. 大窗口任务的并发上限由内存压力决定(CurveJob::hugeLimit:内存充足 2、紧张 1):
   单条整段曲线工作缓冲约 620 MB(2580 万采样),原先最多 4 条并发 → 峰值 2.5GB+;
   实测 4 并发 3M 采样 412 → 324 MiB,且外推值不再随线程数放大。
4. 内存严重不足时**直接拒绝计算**(DerivedMemoryAllows:可用内存 < 输出曲线本体 + 工作缓冲的
   最小需求)并只告警一次(新增 kMessagePlotLowMemory;宿主弹一次明确的提示框)。
   界面保持可用,绝不会因内存不足抛 bad_alloc 逸出线程导致进程终止。
5. 低内存模式“少量多次”:throttle = true 时并发降为 1、且每阶之间 Sleep(1),慢慢算完。
   内存充足时不做任何节流,尽可能快。
6. 取消标志贯通到算法内部(ComputeWeakPeakAdjust / ComputeMovingAverageIterated 新增
   cancel 参数,每阶 / 每 1M 采样检查一次)⇒ **过时任务立即退出并归还工作缓冲**;
   PauseDerivedAsync / StopCurveWorker 先 m_curveCancelAll = true 再 join。
7. AvailablePhysicalBytes() 结果按 200ms 缓存(绘制路径上不再每帧多次系统调用)。
8. 极值表按 count/8 预留容量(try/catch(bad_alloc) 保护),避免 push_back 反复扩容的 1.5x 峰值。
9. MeanFilterCurveFor / WeakPeakCurveFor(整条同步计算)现在只有测试使用;
   产品代码一律走 CurveSpanFor(窗口化 + 异步)。

二、边沿统计:统一「逐块累积 + 每 tick 预算」状态机(普通边沿计数与中心轴共用)
1. EdgeScanState + EdgeScanFor 成为唯一实现,EdgeCountsFor / EdgeCenterCountsFor 是薄包装;
   删除 EdgeCenterCountEntry / m_edgeCenterCountCache。
2. **未完成时主动请求下一帧**(RequestEdgeScanFrame → PostMessage(kMessagePlotDerivedReady)):
   修掉“鼠标不动就一直显示统计中,只有把鼠标移到面板上才更新”(根因就是没有重绘 → 预算不推进)。
3. 阈值/参数变化就地把状态重置(不新建条目,避免拖动时状态爆炸);重置当 tick 不展示部分值
   (修掉“除了最后一个信号显示了一个错误的且不更新的数字,其它变成统计中”);
   稳定后展示递增的部分值 + “…”;扫完展示精确值(统一走 EdgeCountsText)。
4. 中心轴判定所需曲线**只在后台计算**:只在 AcquireCoveringCurve 命中(键完全相同)时扫块,
   否则 ScheduleCurveJob 后本帧不推进。原先该调用没传 allowAsyncCompute(默认 false)⇒
   首帧 / 参数变化后同步算整条均值滤波曲线 ⇒ 界面停顿数秒(用户报告“一改参数整个程序卡住很久”)。
5. MeanFilterValueAt(光标读数)不再调整条同步的 MeanFilterCurveFor,改为窗口化 + 异步。
6. ScheduleCurveJob 改为幂等(m_curveRunningKeys 集合 + 队列去重):统计侧会每帧请求同一份
   整条曲线,若每次都当作新请求,正在算的那份立刻被判过时 → 请求→作废→重请求 死循环。
7. 扫描热路径加“无变换/无镜像/非中心轴”的紧凑循环:单次预算耗时 18ms → 1.3ms。
8. 实测(合成方波):6M 采样首帧 1.4ms(部分值)、单次最坏 2.1ms;
   拖动横向光标 10 次查询 1.07ms;**同一帧内 5 条信号合计 1.3ms**(预算跨信号共享);
   保持不动无需移动鼠标即收敛到与参考实现逐位一致。

三、离屏测试绝不打扰用户的前台工作
- .preflight/uitest 与 .preflight/probe 的 main 开头设置
  SetPriorityClass(GetCurrentProcess(), BELOW_NORMAL_PRIORITY_CLASS) +
  SetThreadPriority(GetCurrentThread(), THREAD_PRIORITY_BELOW_NORMAL),
  再长的测试也不会与用户的编辑/浏览抢 CPU(此前“打字不顺畅”的根因)。
- 继续保持:窗口 SW_HIDE、只注入合成消息(不动真实鼠标键盘)、每次注入后立即 ReleaseCapture
  (SetCapture 是系统级的,不释放会把用户的鼠标输入吸进测试进程)。

四、验证(全部离屏)
- .preflight/uitest 新增/强化:预算推进首帧不显示部分值 → 第二帧显示递增部分值 →
  保持不动收敛到精确值;未完成时会 PostMessage 请求下一帧(直接断言);
  拖动阈值期间 complete == false、不显示部分数字、总耗时 < 60ms;
  5 条信号同帧合计 < 120ms(实测 1.3ms);中心轴与参考实现逐位一致且无需鼠标移动即可完成。
- 合计 751/751 通过(连跑 3 次稳定);make release 零警告。

规则同步:Documents/agent_mem.md 新增 §9.23(含内存治理表与实测数据)、§11.2 新增第 32–38 条硬约束、
变更日志补一行。
This commit is contained in:
DeepSeek V4.1 Flash committed 2026-10-14 15:24:19 +08:00
1 parent d6d3fcd6c1
commit 375a15c8c2
6 files changed
+784 -429

No files matched your search

+91
View File
@@ -1119,6 +1119,79 @@ GDI+ 抗锯齿逐段扫描这些**近竖直、满幅**的边,代价爆炸(
---
### 9.23 派生曲线内存治理(弱峰调整等)+ 边沿统计改为统一预算状态机(含中心轴)(本轮)
> 背景:用户报告 ① 弱峰调整把迭代次数 1→10 时内存峰值到 4.4GB;② 边沿计数面板
> "鼠标不动就一直显示统计中,瞄到面板才更新",拖动横向光标时显示错误/不更新;
> ③ 中心轴边沿计数**没有被优化到**,一个网格里多个信号各开一项、一改参数整个程序卡住很久。
> 另:**离屏 harness 绝不允许打扰用户的前台输入**(本轮加了进程/线程优先级保护)。
#### 一、边沿统计:统一「逐块累积 + 每 tick 预算」状态机(普通 + 中心轴共用)
- `EdgeScanState`(原 `EdgeCountEntry`)现在是**唯一**的累积结构,`EdgeScanFor(...)` 是唯一实现,
`EdgeCountsFor` / `EdgeCenterCountsFor` 都只是薄包装(后者多传 `centre = true` + `points/iterations`)。
`EdgeCenterCountEntry`/`m_edgeCenterCountCache` **已删除**。
- ⚠️ **必须主动请求下一帧**:`EdgeScanFor` 未完成时调用 `RequestEdgeScanFrame()`
(`PostMessage(kMessagePlotDerivedReady)`)。缺了这一步,用户不移动鼠标就没有重绘 →
预算永远不推进 → **面板永远显示"统计中"**(用户报告②的根因;过去"鼠标移到面板上就好了"
正是因为悬停变化触发了重绘)。
- **阈值/参数变化就地把状态重置**(不新建条目,避免拖动时状态爆炸);
⚠️ 重置当 tick **不展示部分值**(`justReset`)——部分计数每帧都被重置,显示出来就是
"错误且不更新的数字"(用户报告②的另一半)。稳定后展示**递增的部分值 + `…`**,
扫完展示精确值。界面文本统一走 `EdgeCountsText(rising, falling, complete)`。
- **中心轴判定所需曲线必须来自后台**:`EdgeScanFor` 只在 `AcquireCoveringCurve` 命中
(**键完全相同**,不能像绘制那样用旧参数曲线顶替)时扫块,否则 `ScheduleCurveJob` 后本帧不推进。
⚠️ 原先 `EdgeCenterCountsFor` 调 `CurveSpanFor(...)` **没有传 `allowAsyncCompute`**(默认 false)⇒
首帧/参数变化后**同步**算整条均值滤波曲线 ⇒ 界面停顿数秒(用户报告③的根因)。
- `MeanFilterValueAt`(光标读数)也不再调整条同步的 `MeanFilterCurveFor`,改为窗口化 + 异步。
- ⚠️ `ScheduleCurveJob` 现在**幂等**(`m_curveRunningKeys` 集合 + 队列去重):
统计侧会每帧请求同一份整条曲线,若每次都当作新请求,正在算的那份立刻被判过时 →
**请求→作废→重请求 的死循环**(永远算不完)。这是本轮最容易踩的坑。
- 实测(合成方波):6M 采样首帧 **1.4ms**(部分值)、单次最坏 2.1ms;
横向光标拖动中 10 次查询 **1.07ms**;**同一帧内 5 条信号合计 1.3ms**(预算跨信号共享);
保持不动 → 收敛到与参考实现**逐位一致**。
#### 二、派生曲线内存治理(弱峰调整 / 迭代均值滤波)
| 措施 | 效果 |
| --- | --- |
| `PeakExtremum` 字段重排(`double value` + `uint32 index` + `int8 type`) | 24 → **16 字节**;密集极值信号(编码器方波)省 ≈1/3 极值表内存(**不改变数值结果**,不能把 `value` 改成 float) |
| 整段窗口**不再复制采样**(`ComputeDerivedWindow` 整段时直接引用原向量) | 每条整段曲线省 **8 B/采样**(2580 万采样 ≈ **206 MB**) |
| **大任务并发上限**(`CurveJob::hugeLimit`:内存充足 2、紧张 1) | 峰值从 ≈4×单条降到 ≤2×(实测 4 并发 3M 采样 412MB → 324MB,且外推值不再随线程数放大) |
| **内存严重不足时直接拒绝计算**(`DerivedMemoryAllows`:可用 < 输出本体所需最小空间) | 只告警**一次**(`kMessagePlotLowMemory` → 宿主 `SetStatusMessage(..., true)`),**不再崩溃** |
| **低内存节流**(`throttle`:阶与阶之间 `Sleep(1)`,并发降为 1) | "少量多次"慢慢算完,不把内存带宽/CPU 抢光 |
| **取消标志贯通到算法**(`ComputeWeakPeakAdjust` / `ComputeMovingAverageIterated` 新增 `cancel` 参数,每阶 / 每 1M 采样检查) | **过时任务立即退出并归还缓冲**(拖动滑块时旧参数的计算立刻作废) |
| `PauseDerivedAsync`/`StopCurveWorker` 先 `m_curveCancelAll = true` 再 join | 关闭预览 / 结构变化时在算任务尽快退出 |
| `AvailablePhysicalBytes()` 结果按 200ms 缓存 | 绘制路径上不再每帧多次系统调用 |
| 极值表按 `count/8` 预留容量(`try/catch(std::bad_alloc)` 保护) | 避免 push_back 反复扩容的 1.5x 峰值 |
- 复核数据(探针 `.preflight/probe`,3M 采样单次计算):密集极值信号工作缓冲峰值
**33.23 → 26.73 MiB**;4 并发 **412 → 324 MiB**。
- ⚠️ 仍然存在的一次性峰值:一条整段曲线 ≈(8 + 12 + 4)B/采样 ≈ 620 MB(2580 万采样),
这是"整段 O(n) 计算"的固有代价;现在它**有上限、可取消、可拒绝**。
- ⚠️ `MeanFilterCurveFor` / `WeakPeakCurveFor`(整条同步计算)现在**只有测试用到**,
产品代码一律走 `CurveSpanFor`(窗口化 + 异步)。
#### 三、离屏验证绝不打扰用户(用户第三次强调)
- `.preflight/uitest` 与 `.preflight/probe` 现在都在 `main` 开头设置
`SetPriorityClass(GetCurrentProcess(), BELOW_NORMAL_PRIORITY_CLASS)` +
`SetThreadPriority(GetCurrentThread(), THREAD_PRIORITY_BELOW_NORMAL)` ⇒
再长的测试也不会与用户的前台编辑抢 CPU("打字不顺畅"的根因)。
- 仍然保持:窗口 `SW_HIDE`、只注入合成消息(不移动真实鼠标/键盘)、每次注入后立即
`ReleaseCapture`(`SetCapture` 是**系统级**的,不释放会把用户的鼠标输入吸走)。
- ⚠️ 新增/修改 harness 时**必须**保留这三项(隐藏窗口 / 释放捕获 / 低于正常优先级)。
#### 四、验证
- `.preflight/uitest` 新增/强化:预算推进首帧不显示部分值 → 第二帧显示递增部分值 →
保持不动收敛到精确值;**未完成时会 PostMessage 请求下一帧**(直接断言);
拖动阈值期间 `complete == false` 且不显示部分数字且总耗时 < 60ms;
5 条信号同帧合计 < 120ms(实测 1.3ms);中心轴与参考实现逐位一致且**无需鼠标移动**即可完成。
- 合计 **751/751 通过**(连跑 3 次稳定);`make release` 零警告。
---
## 10. 变更日志(本文件维护记录)
| 日期 | 设备/模型 | 摘要 |
@@ -1152,6 +1225,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(**起源设备**) | **派生曲线内存治理 + 边沿统计统一预算状态机(含中心轴)(详见 §9.23)**。① **弱峰调整内存**:`PeakExtremum` 字段重排 24→**16 字节**(密集极值信号省 ≈1/3 极值表);`ComputeDerivedWindow` 整段窗口**不再复制采样**(省 8 B/采样 ≈ 206 MB/条);**大任务并发上限**(内存充足 2 / 紧张 1,实测 4 并发 3M 采样 412→324 MiB);**内存严重不足时直接拒绝计算并只告警一次**(`kMessagePlotLowMemory`,不再 `bad_alloc` 崩溃);**低内存节流**(阶间 `Sleep(1)`、并发 1,"少量多次");**取消标志贯通到算法**(每阶 / 每 1M 采样检查)⇒ **过时任务立即退出并归还缓冲**;`AvailablePhysicalBytes()` 按 200ms 缓存。② **边沿统计**:统一为 `EdgeScanState` + `EdgeScanFor`(普通/中心轴共用),阈值/参数变化就地把状态重置、重置当 tick 不展示部分值(修掉"错误的且不更新的数字");**未完成时主动 `PostMessage(kMessagePlotDerivedReady)` 请求下一帧**(修掉"鼠标不动就一直统计中、移上去才好");中心轴所需曲线**只在后台算**(原先 `CurveSpanFor` 未传 `allowAsyncCompute` ⇒ 同步算整条均值滤波曲线 ⇒ 界面停顿数秒),`MeanFilterValueAt` 同步整条计算的路径也已移除;⚠️ `ScheduleCurveJob` 改为**幂等**(否则统计侧每帧请求会造成"请求→作废→重请求"死循环)。实测:6M 采样首帧 1.4ms、拖动中 10 次查询 1.07ms、**同帧 5 条信号合计 1.3ms**、保持不动收敛到与参考实现逐位一致。③ **离屏 harness/探针**:`main` 开头设置 `BELOW_NORMAL_PRIORITY_CLASS` + 线程低于正常(再长的测试也不抢用户前台 CPU),保持隐藏窗口 / 只注入合成消息 / 注入后立即 `ReleaseCapture`。验证:`uitest` **751/751 通过**(连跑 3 次稳定),`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` 零警告。 |
@@ -1255,6 +1329,23 @@ GDI+ 抗锯齿逐段扫描这些**近竖直、满幅**的边,代价爆炸(
⚠️ 新增/修改 `NewTab` 调用点时别忘了带上这个形参。
31. ⚠️ **网格 GX 徽标悬停不改鼠标形状**(`QueryCursor` 在徽标上返回 `nullptr`,且**提前 return**
以免被光标线/图例判定抢走);只有拖拽中(`LANE_REORDER`)才是 `IDC_SIZENS`。
32. ⚠️ **预算推进型统计必须自己请求下一帧**:任何“分帧推进”的逻辑(边沿统计、图层重建)
在未完成时都要 `PostMessage(kMessagePlotDerivedReady)`,否则**用户不动鼠标就永远不推进**
(边沿计数面板“一直显示统计中、鼠标移上去才好”就是这个原因)。
33. ⚠️ **阈值/参数变化期间不要展示“部分值”**:部分计数每帧都被重置,显示出来就是
“错误且不更新的数字”。用 `justReset` 抑制一 tick,稳定后再展示递增的部分值(带 `…`)。
34. ⚠️ **`ScheduleCurveJob` 必须幂等**(`m_curveRunningKeys` + 队列去重):统计侧会每帧请求
同一份整条曲线;若每次都当作新请求,正在算的那份立刻被判过时 → **请求→作废→重请求 死循环**。
35. ⚠️ **大窗口派生计算不能无限并发**:单条整段曲线工作缓冲 ≈620MB(2580 万采样),
并发上限由内存压力决定(充足 2 / 紧张 1);内存**严重不足**时直接拒绝并只告警一次
(`kMessagePlotLowMemory`),绝不抛 `bad_alloc` 逸出线程。
36. ⚠️ **取消标志必须贯通到算法内部**(每阶 / 每 1M 采样检查一次),否则“过时任务立即清退”
只是口号:千万级一条曲线的工作缓冲要几百 MB,必须尽早归还。
37. ⚠️ **整段窗口不要复制采样**(`ComputeDerivedWindow`):8 B/采样 × 2580 万 ≈ 206 MB/条;
整段时直接引用原向量(结构变化前已有 `PauseDerivedAsync` 保证不被改)。
38. ⚠️ **离屏 harness / 探针必须在 `main` 开头把进程与线程优先级降到「低于正常」**
(`BELOW_NORMAL_PRIORITY_CLASS`),并保持“隐藏窗口 + 只注入合成消息 + 注入后立即 `ReleaseCapture`”。
`SetCapture` 是系统级的,不释放会把用户的真实鼠标输入吸进测试进程。
### 11.3 离屏验证方法学(本机偏好,强烈建议沿用)