边沿统计改为「定向失效」:改一个信号的参数不再重算其它信号的统计
一、问题
加载 VERY_BIG_DATA.CSV 并把所有信号显示在同一个网格,同时启用「边沿计数」与
「中心轴边沿计数」(两个面板都作用于网格内全部信号)。此时改变**一个**信号的
中心轴边沿计数参数,两个浮动面板里的**所有**统计值都从头重新计算。
二、根因
参数提交路径(PlotView::EndEdit)与勾选切换路径里用的是 m_edgeScans.clear():
它把两个面板、所有信号的累积统计状态全部丢掉。于是:
- 其它信号(参数没变、键没变、块和依然有效)被迫从头重扫;
- 连普通「边沿计数」(centre == false)的状态也被一起丢掉 → 另一个面板也全部重算。
三、修法
1. 新增 PlotView::InvalidateEdgeScansFor(const SignalData* signal, bool centreOnly):
按键(文件/信号/种类/样式指纹/参数/区间)只擦除**该信号**的累积状态;
centreOnly = true 时只擦除它的「中心轴」状态。
2. EndEdit 的参数提交分支:
- 中心轴参数(EDGE_CENTER_POINTS / EDGE_CENTER_ITERATIONS)变化 →
InvalidateEdgeScansFor(该信号, true);该信号的普通边沿计数、以及其它信号完全不受影响;
- 均值滤波参数变化 → 与统计无关(中心轴用**自己的**条目列表与自己的曲线键)→ 不作废任何统计。
3. 勾选/取消勾选信号:该信号的统计(与参数相关)依然有效,只是"是否显示"变化 →
删除原来的三个 clear()(m_meanCurveCache/m_edgeScans/m_weakPeakCurveCache),
不再作废任何缓存(改为依赖 m_sceneDerived 快照做局部重画)。
四、验证(全部离屏,不占用鼠标/键盘/屏幕;harness 与探针均降到 BELOW_NORMAL 优先级)
- .preflight/uitest 新增 [AT]:3 个信号合并到同一网格,中心轴统计全部扫完后,通过**真实编辑路径**
(plot.BeginEdit(EDGE_CENTER_POINTS, 0, L"7") + plot.EndEdit(true))修改第 2 个信号的参数,
断言:
* 被改信号的中心轴状态已作废,且其条目参数确实变成 7;
* 另两个信号的 scannedPair / rising **逐位不变**(累计块和没有被丢弃);
* 被改信号的**普通**边沿计数状态 scannedPair / rising **逐位不变**(另一个面板完全未失效);
* 未被改动的信号下一次查询仍然 complete(瞬时返回,不需要重扫)。
- 合计 774/774 通过(连跑 3 次稳定);make release 零警告。
规则同步:Documents/agent_mem.md 新增 §9.24,§11.2 新增第 39 条硬约束(参数变化必须定向失效、
禁止对累积缓存整体 clear()),变更日志补一行。
This commit is contained in:
1 parent
375a15c8c2
commit
48bc534bc1
3 files changed
+78
-7
No files matched your search
@@ -1945,6 +1945,19 @@ namespace jdi
|
||||
/** @brief 边沿统计未完成时请宿主再送一帧(无需鼠标移动也能持续推进)。 */
|
||||
void RequestEdgeScanFrame() const;
|
||||
|
||||
/**
|
||||
* @brief **定向**作废边沿统计状态(参数变化时只影响被改动的那个信号)。
|
||||
*
|
||||
* ⚠️ 绝不要用 `m_edgeScans.clear()` 代替:那会把**所有**信号的累积状态都丢掉,
|
||||
* 于是两个浮动面板里的每一个统计值都会从头重新扫描
|
||||
*(用户报告:“改变一个信号的中心轴参数,其它未改变参数的统计也重新计算了”)。
|
||||
* 其它信号的键没有变化,它们的块和依然有效,必须保留。
|
||||
*
|
||||
* @param [IN] signal 参数被改动的信号(nullptr 时不做任何事)。
|
||||
* @param [IN] centreOnly true = 只作废该信号的中心轴统计(改中心轴参数时用)。
|
||||
*/
|
||||
void InvalidateEdgeScansFor(const SignalData* signal, bool centreOnly);
|
||||
|
||||
/** 多次迭代均值滤波曲线缓存项(按「信号指针 + 均值点数 + 迭代次数」标识)。 */
|
||||
struct MeanCurveEntry
|
||||
{
|
||||
|
||||
@@ -2964,6 +2964,29 @@ namespace jdi
|
||||
}
|
||||
}
|
||||
|
||||
void PlotView::InvalidateEdgeScansFor(const SignalData* signal, bool centreOnly)
|
||||
{
|
||||
// ⚠️ **定向作废**:只有被改动的那个信号(并且只限需要的那一类)要重算,
|
||||
// 其它信号的键没有变化、累计的块和依然有效 → 必须原样保留。
|
||||
// 否则(过去用 `m_edgeScans.clear()`)会连**普通边沿计数**与**其它信号的中心轴统计**
|
||||
// 一起丢掉,于是两个浮动面板里的每一个数字都要从头重新扫描:
|
||||
// 用户报告“改变一个信号的中心轴参数,其它未改变参数的统计也重新计算了”。
|
||||
if (signal == nullptr)
|
||||
{
|
||||
return;
|
||||
}
|
||||
m_edgeScans.erase(std::remove_if(m_edgeScans.begin(), m_edgeScans.end(),
|
||||
[signal, centreOnly](const EdgeScanState& state)
|
||||
{
|
||||
if (state.signal != signal)
|
||||
{
|
||||
return false; // 别的信号:原样保留
|
||||
}
|
||||
return !centreOnly || state.centre;
|
||||
}),
|
||||
m_edgeScans.end());
|
||||
}
|
||||
|
||||
void PlotView::EdgeScanFor(const DataFile& file, const SignalData& signal, const SignalStyle& style,
|
||||
bool centre, double threshold, int points, int iterations, bool hasRange,
|
||||
double rangeMin, double rangeMax, std::size_t& rising, std::size_t& falling,
|
||||
@@ -4226,9 +4249,14 @@ namespace jdi
|
||||
{
|
||||
item.iterations = value;
|
||||
}
|
||||
// 参数变化会改变曲线:清掉派生缓存即可(局部重画由 `m_sceneDerived` 快照驱动)。
|
||||
// ⚠️ **只作废被改动的那个信号**的统计状态。
|
||||
// 过去的 `m_edgeScans.clear()` 会把两个面板里**所有**信号的统计一起丢掉,
|
||||
// 于是改一个信号的参数会让其余未改参数的统计也从头重扫(用户报告的缺陷)。
|
||||
m_meanCurveCache.clear();
|
||||
m_edgeScans.clear();
|
||||
if (centerTarget)
|
||||
{
|
||||
InvalidateEdgeScansFor(ResolveSignal(m_editMeanRef).data, true);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -5270,11 +5298,9 @@ namespace jdi
|
||||
{
|
||||
ToggleLaneEdgeCenterSignal(index, ref);
|
||||
}
|
||||
m_meanCurveCache.clear();
|
||||
m_edgeScans.clear();
|
||||
m_weakPeakCurveCache.clear();
|
||||
// 勾选变化只影响该网格的内容:交给 `m_sceneDerived` 快照驱动局部重画,
|
||||
// 这里**不**整体作废场景缓存(否则每次勾选都要重绘全部网格)。
|
||||
// 勾选变化只改变该信号“是否显示”,它的统计状态(与参数相关)依然有效,
|
||||
// 其它信号更是完全无关 ⇒ **不要**清任何缓存(清掉就会让所有统计重算)。
|
||||
// 这里**不**整体作废场景缓存(交给 `m_sceneDerived` 快照驱动局部重画)。
|
||||
m_dragMode = PlotDragMode::NONE;
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -1192,6 +1192,32 @@ GDI+ 抗锯齿逐段扫描这些**近竖直、满幅**的边,代价爆炸(
|
||||
|
||||
---
|
||||
|
||||
### 9.24 边沿统计:参数变化只作废**被改动的那个信号**(定向失效)
|
||||
|
||||
> 用户报告:`VERY_BIG_DATA.CSV` 全部信号放在同一网格、同时启用「边沿计数」与「中心轴边沿计数」,
|
||||
> 改**一个**信号的中心轴参数后,**两个浮动面板的所有统计**都从头重新计算。
|
||||
|
||||
- **根因**:参数提交路径(`EndEdit`)与勾选切换路径里用的是 `m_edgeScans.clear()`——
|
||||
它把**两个面板、所有信号**的累积状态全部丢掉。用户看到的"全部重新统计"正是它。
|
||||
- **修法**:新增 **`InvalidateEdgeScansFor(const SignalData* signal, bool centreOnly)`**,
|
||||
只按键(文件/信号/样式/参数/区间)擦除**该信号**的状态:
|
||||
* 改**中心轴**参数(`EDGE_CENTER_POINTS/ITERATIONS`)→ 只作废该信号的**中心轴**状态
|
||||
(`centreOnly = true`);该信号的**普通**边沿计数不受影响,其它信号完全不受影响;
|
||||
* 改**均值滤波**参数 → 与统计无关(中心轴用的是它自己的条目列表 + 自己的曲线键)→ 不作废任何统计;
|
||||
* **勾选/取消勾选**信号 → 该信号的统计(与参数相关)依然有效 → **不清任何缓存**
|
||||
(过去的三个 `clear()` 已全部删除)。
|
||||
- ⚠️ **硬约束**:凡"参数变化"类失效一律用**定向**接口;**禁止**再对 `m_edgeScans` 整体 `clear()`
|
||||
(只有**数据层结构变化**才允许,那条路径经 `InvalidateDerivedCaches` → `PauseDerivedAsync`)。
|
||||
- 验证:`uitest` 新增 `[AT]`——3 个信号合并在同一网格,中心轴统计全部扫完后,通过**真实编辑路径**
|
||||
(`BeginEdit(EDGE_CENTER_POINTS, 0, L"7")` + `EndEdit(true)`)改**第 2 个**信号的参数,断言:
|
||||
* 被改信号的中心轴状态**已作废**;其条目参数确实变成 7;
|
||||
* 另两个信号的 `scannedPair`/`rising` **逐位不变**(累计块和没有被丢弃);
|
||||
* 被改信号的**普通**边沿计数状态 `scannedPair`/`rising` **逐位不变**(另一个面板完全未失效);
|
||||
* 未被改动的信号下一次查询**仍然 `complete`**(瞬时返回,不需要重扫)。
|
||||
- 合计 **774/774 通过**(连跑 3 次稳定);`make release` 零警告。
|
||||
|
||||
---
|
||||
|
||||
## 10. 变更日志(本文件维护记录)
|
||||
|
||||
| 日期 | 设备/模型 | 摘要 |
|
||||
@@ -1226,6 +1252,7 @@ GDI+ 抗锯齿逐段扫描这些**近竖直、满幅**的边,代价爆炸(
|
||||
| 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(**起源设备**) | **边沿统计改为“定向失效”(详见 §9.24)**:改变**一个**信号的中心轴参数时,过去用 `m_edgeScans.clear()` 把**两个浮动面板、所有信号**的累积状态全部丢掉 → 所有统计从头重算。新增 `InvalidateEdgeScansFor(const SignalData*, bool centreOnly)`,只擦除该信号(中心轴参数变化时只限它的中心轴状态);均值滤波参数变化与统计无关(不作废);勾选切换只改变“是否显示”(三个 `clear()` 全部删除,不再作废任何缓存)。验证:`uitest` 新增 `[AT]`——3 信号合并同一网格,用**真实编辑路径**改第 2 个信号的中心轴参数,断言被改信号中心轴已作废、另两个信号 `scannedPair/rising` 逐位不变、被改信号的**普通**边沿计数逐位不变、未改动信号仍 `complete` 瞬时返回;**774/774 通过**(连跑 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` 零警告。 |
|
||||
|
||||
@@ -1346,6 +1373,11 @@ GDI+ 抗锯齿逐段扫描这些**近竖直、满幅**的边,代价爆炸(
|
||||
38. ⚠️ **离屏 harness / 探针必须在 `main` 开头把进程与线程优先级降到「低于正常」**
|
||||
(`BELOW_NORMAL_PRIORITY_CLASS`),并保持“隐藏窗口 + 只注入合成消息 + 注入后立即 `ReleaseCapture`”。
|
||||
`SetCapture` 是系统级的,不释放会把用户的真实鼠标输入吸进测试进程。
|
||||
39. ⚠️ **参数变化类失效必须“定向”**:一律用 `InvalidateEdgeScansFor(signal, centreOnly)`
|
||||
之类的按键接口,**禁止**对累积缓存(`m_edgeScans` 等)整体 `clear()`——
|
||||
那会让所有信号、所有面板的统计一起重算(用户报告“改一个信号的参数,其它统计也重新计算了”)。
|
||||
只有**数据层结构变化**(增删文件/信号、重排网格)才允许整体作废(走 `InvalidateDerivedCaches`)。
|
||||
同理:**勾选/取消勾选**只改变“是否显示”,不应作废任何统计或曲线缓存。
|
||||
|
||||
### 11.3 离屏验证方法学(本机偏好,强烈建议沿用)
|
||||
|
||||
|
||||
Reference in new issue
Block a user