新标签页分组默认折起 + 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:
1 parent
a2a38236fe
commit
d6d3fcd6c1
7 files changed
+548
-119
No files matched your search
@@ -1601,9 +1601,14 @@ namespace jdi
|
||||
*
|
||||
* ⚠️ 因此判定值必须是**画出来的值**:先按 `style` 做水平镜像映射,再套用
|
||||
* `ApplyDrawTransform`(系数 → 绝对值 → 垂直镜像 → Y 偏移)。
|
||||
* 只比较原始采样值会导致「应用过调整的信号数不到任何边沿」
|
||||
*(横向光标读的是屏幕上的值,与原始值域完全不同)。
|
||||
* ⚠️ 时间区间**之外**的采样一律不参与计数:只有两个端点都在区间内的采样对才计入。
|
||||
* 只比较原始采样值会导致「应用过调整的信号数不到任何边沿」。
|
||||
* ⚠️ 时间区间**之外**的采样一律不参与计数(只有两个端点都在区间内的采样对才计入)。
|
||||
*
|
||||
* ⚠️ **本函数绝不长时间阻塞 UI 线程**(千万级信号整段扫描实测 ≈115ms/条,
|
||||
* 横向光标每一帧都会换阈值 → 拖动时每帧都要扫,5 条信号就是 ≈575ms/帧):
|
||||
* 扫描按「逐块累积」进行,每个 tick(约一帧)只消耗 `kEdgeScanPairsPerTick`
|
||||
* 个采样对的预算,未扫完时 `complete` 返回 false(界面显示上一个完整结果 + “…”)。
|
||||
* 拖动横向光标(`EDGE_MOVE`)期间阈值每帧都变,推进等于白做功 → 不推进。
|
||||
*
|
||||
* @param [IN] file 信号所属文件。
|
||||
* @param [IN] signal 目标信号。
|
||||
@@ -1614,10 +1619,11 @@ namespace jdi
|
||||
* @param [IN] rangeMax 区间右端点时间。
|
||||
* @param [OUT] rising 上升沿数量(从下往上穿越)。
|
||||
* @param [OUT] falling 下降沿数量(从上往下穿越)。
|
||||
* @param [OUT] complete 结果是否为该阈值的**完整**精确值(false = 仍在推进,当前是临时值)。
|
||||
*/
|
||||
void EdgeCountsFor(const DataFile& file, const SignalData& signal, const SignalStyle& style,
|
||||
double edgeValue, bool hasRange, double rangeMin, double rangeMax,
|
||||
std::size_t& rising, std::size_t& falling) const;
|
||||
std::size_t& rising, std::size_t& falling, bool& complete) const;
|
||||
|
||||
/**
|
||||
* @brief 计算实验性功能面板所需的宽度(排版与绘制共用,保证文字完整不被省略)。
|
||||
@@ -1819,28 +1825,37 @@ namespace jdi
|
||||
std::vector<LaneVisual> m_lanes; /**< 当前网格绘制信息集合。 */
|
||||
|
||||
/**
|
||||
* @brief 边沿统计缓存项。
|
||||
* @brief 「边沿计数」的累积统计条目。
|
||||
*
|
||||
* 计数只由「采样下标区间 [begin, end) 与判定基准(横向光标幅值)」决定,
|
||||
* 因此**时间区间不进缓存键**:拖动时间光标时区间每帧都在变,若按时间区间做键就会
|
||||
* 每帧全量重扫整段区间(亿级采样下正是拖拽卡顿的主因,实测单帧数十毫秒)。
|
||||
* 改为记住「上一帧统计过的下标区间」:新区间与旧区间共享一个端点时(拖动时必然如此)
|
||||
* 只扫描两端差集并增量加减,单帧代价与光标位移成正比、与区间长度无关。
|
||||
* 千万级信号整段扫描一次就要 ≈115ms,而横向光标拖动时**每一帧**都会换阈值
|
||||
*(5 条信号同一网格实测 ≈575ms/帧,界面直接失去响应)。
|
||||
* 因此统计改为「逐块累积 + 每 tick 有限预算推进」(与 `RenderPendingLaneRebuilds(1)`
|
||||
* 同一套「宁可晚一点、绝不卡住前台」的思路):
|
||||
* * 采样对 `k`(采样 `k-1 → k`)按 `kEdgeBlockSamples` 分块,每块各存上升/下降沿数;
|
||||
* * `scannedPair` = 下一个待扫描的采样对,[1, scannedPair) 已扫完;
|
||||
* * 查询某个区间时,内部整块用存储值相加、两端边界块精确扫描(≤ 2 块,代价固定);
|
||||
* * 时间区间**不进键**(单调时间轴下区间只是查询区间,拖动时间光标无需重扫),
|
||||
* 但非单调时间轴无法用下标区间表达统计范围,此时区间必须进键(否则会混用结果)。
|
||||
*/
|
||||
struct EdgeCountEntry
|
||||
{
|
||||
const DataFile* file = nullptr; /**< 所属数据分组。 */
|
||||
const SignalData* signal = nullptr; /**< 目标信号。 */
|
||||
std::uint64_t styleKey = 0; /**< 「调整」变换 + 水平镜像的指纹(变了必须整段重算)。 */
|
||||
double edgeValue = 0.0; /**< 判定阈值(横向光标幅值)。 */
|
||||
bool hasRange = false; /**< 是否限定在时间区间内统计。 */
|
||||
bool reusable = false; /**< 计数是否完全由 [begin, end) 决定。 */
|
||||
std::size_t begin = 0; /**< 已统计的采样区间起点(含)。 */
|
||||
std::size_t end = 0; /**< 已统计的采样区间终点(不含)。 */
|
||||
std::size_t rising = 0; /**< 该区间内的上升沿数量。 */
|
||||
std::size_t falling = 0; /**< 该区间内的下降沿数量。 */
|
||||
std::uint64_t styleKey = 0; /**< 「调整」变换 + 水平镜像的指纹。 */
|
||||
double threshold = 0.0; /**< 判定阈值(横向光标幅值)。 */
|
||||
bool hasRange = false; /**< 是否限定在时间区间内(仅非单调时间轴时进键)。 */
|
||||
double rangeMin = 0.0; /**< 统计区间左端(仅非单调时间轴时进键)。 */
|
||||
double rangeMax = 0.0; /**< 统计区间右端(仅非单调时间轴时进键)。 */
|
||||
std::vector<std::size_t> blockRising; /**< 每块的上升沿数量(已扫到的部分)。 */
|
||||
std::vector<std::size_t> blockFalling; /**< 每块的下降沿数量(已扫到的部分)。 */
|
||||
std::size_t scannedPair = 1; /**< 下一个待扫描的采样对下标([1, scannedPair) 已扫完)。 */
|
||||
std::size_t rising = 0; /**< 已扫部分的上升沿总数。 */
|
||||
std::size_t falling = 0; /**< 已扫部分的下降沿总数。 */
|
||||
bool complete = false; /**< 整条信号是否已扫完。 */
|
||||
};
|
||||
mutable std::vector<EdgeCountEntry> m_edgeCountCache; /**< 边沿统计缓存(纯缓存,逻辑上不影响对象状态)。 */
|
||||
mutable std::vector<EdgeCountEntry> m_edgeCountCache; /**< 边沿统计累积缓存(纯缓存,逻辑上不影响对象状态)。 */
|
||||
mutable std::size_t m_edgeScanBudget = 0; /**< 本 tick 剩余可扫描的采样对数量(限制单帧总开销)。 */
|
||||
mutable std::uint64_t m_edgeScanTick = 0; /**< 最近一次重置扫描预算的 tick。 */
|
||||
|
||||
/** 多次迭代均值滤波曲线缓存项(按「信号指针 + 均值点数 + 迭代次数」标识)。 */
|
||||
struct MeanCurveEntry
|
||||
|
||||
Reference in new issue
Block a user