新标签页分组默认折起 + 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
@@ -51,15 +51,50 @@ namespace jdi
|
||||
const UINT kRamTimerPeriodMs = 1000; /**< 标题栏内存占用的刷新间隔(1 秒)。 */
|
||||
|
||||
/**
|
||||
* @brief 查询本进程当前占用的物理内存(工作集)。
|
||||
* @brief 查询本进程当前占用的内存(**私有工作集**,与任务管理器"内存"列同口径)。
|
||||
*
|
||||
* 与任务管理器里显示的“内存”一致:进程已驻留在物理内存中的字节数。
|
||||
* 使用 `K32GetProcessMemoryInfo`(kernel32 导出)以免为 `psapi` 增加一条链接依赖。
|
||||
* ⚠️ 原先取的是 `PROCESS_MEMORY_COUNTERS::WorkingSetSize`(**总工作集**),它把映射进来的
|
||||
* DLL / 共享页面以及已经释放但尚未被系统回收的驻留页面都算进去 —— 加载一个大文件再
|
||||
* 关闭分组后,实际占用的私有内存已经回到几 MB,标题栏却还显示几十 MB,
|
||||
* 与任务管理器(资源管理器里看到的"内存")对不上(用户报告)。
|
||||
* `PROCESS_MEMORY_COUNTERS_EX2::PrivateWorkingSetSize` 正是任务管理器"内存"列的口径:
|
||||
* 只统计**属于本进程的私有、且驻留在物理内存中**的页面。
|
||||
*
|
||||
* @return 工作集字节数;查询失败返回 0。
|
||||
* 结构体在这里就地声明(MinGW 的 psapi.h 未提供 `PROCESS_MEMORY_COUNTERS_EX2`),
|
||||
* 布局与 Win32 文档一致;EX2 查询失败(老系统)时退回 `PrivateUsage`(私有提交),
|
||||
* 再不行退回 `WorkingSetSize`,保证标题栏永远有值。
|
||||
*
|
||||
* @return 内存占用字节数;查询失败返回 0。
|
||||
*/
|
||||
unsigned long long ProcessWorkingSetBytes()
|
||||
unsigned long long ProcessPrivateWorkingSetBytes()
|
||||
{
|
||||
struct MemoryCountersEx2
|
||||
{
|
||||
DWORD cb;
|
||||
DWORD PageFaultCount;
|
||||
SIZE_T PeakWorkingSetSize;
|
||||
SIZE_T WorkingSetSize;
|
||||
SIZE_T QuotaPeakPagedPoolUsage;
|
||||
SIZE_T QuotaPagedPoolUsage;
|
||||
SIZE_T QuotaPeakNonPagedPoolUsage;
|
||||
SIZE_T QuotaNonPagedPoolUsage;
|
||||
SIZE_T PagefileUsage;
|
||||
SIZE_T PeakPagefileUsage;
|
||||
SIZE_T PrivateUsage;
|
||||
SIZE_T PrivateWorkingSetSize;
|
||||
SIZE_T SharedCommitUsage;
|
||||
};
|
||||
|
||||
MemoryCountersEx2 ex2 = {};
|
||||
ex2.cb = sizeof(ex2);
|
||||
if (K32GetProcessMemoryInfo(GetCurrentProcess(),
|
||||
reinterpret_cast<PROCESS_MEMORY_COUNTERS*>(&ex2),
|
||||
sizeof(ex2)) != FALSE &&
|
||||
ex2.PrivateWorkingSetSize > 0)
|
||||
{
|
||||
return static_cast<unsigned long long>(ex2.PrivateWorkingSetSize);
|
||||
}
|
||||
|
||||
PROCESS_MEMORY_COUNTERS_EX counters = {};
|
||||
counters.cb = sizeof(counters);
|
||||
if (K32GetProcessMemoryInfo(GetCurrentProcess(),
|
||||
@@ -68,6 +103,10 @@ namespace jdi
|
||||
{
|
||||
return 0ull;
|
||||
}
|
||||
if (counters.PrivateUsage > 0)
|
||||
{
|
||||
return static_cast<unsigned long long>(counters.PrivateUsage);
|
||||
}
|
||||
return static_cast<unsigned long long>(counters.WorkingSetSize);
|
||||
}
|
||||
|
||||
@@ -374,8 +413,8 @@ namespace jdi
|
||||
SendMessageW(m_hwnd, WM_SETICON, ICON_SMALL, reinterpret_cast<LPARAM>(smallIcon));
|
||||
}
|
||||
|
||||
// 创建首个标签页。
|
||||
NewTab();
|
||||
// 创建首个标签页(分组保持展开:加载文件后直接就能勾选信号)。
|
||||
NewTab(false);
|
||||
|
||||
Relayout();
|
||||
// 标题栏内存占用按固定间隔刷新(不使用每帧刷新,避免频繁改动窗口标题)。
|
||||
@@ -443,11 +482,18 @@ namespace jdi
|
||||
return m_tabs[static_cast<std::size_t>(m_activeTab)];
|
||||
}
|
||||
|
||||
void Application::NewTab()
|
||||
void Application::NewTab(bool collapseGroups)
|
||||
{
|
||||
TabConfig tab;
|
||||
tab.SetName(NextTabName());
|
||||
// 用户新建的标签页:信号分组**默认折起**(用户明确要求:列表默认紧凑;
|
||||
// 之后在该标签页里新加载的分组也沿用这个默认值)。
|
||||
tab.SetGroupsCollapsedByDefault(collapseGroups);
|
||||
tab.SyncToDataSet(m_dataSet);
|
||||
if (collapseGroups)
|
||||
{
|
||||
tab.SetAllCollapsed(true);
|
||||
}
|
||||
|
||||
m_tabs.push_back(std::move(tab));
|
||||
SetActiveTab(static_cast<int>(m_tabs.size()) - 1);
|
||||
@@ -899,10 +945,11 @@ namespace jdi
|
||||
{
|
||||
// 标题栏 = 应用名称 + 分隔竖线 + 内存占用(单位在 KB/MB/GB 间自适应,保留两位小数)。
|
||||
// 除内存占用外不再附带标签页、文件数量等无关信息。
|
||||
// ⚠️ 内存口径 = **私有工作集**(与任务管理器"内存"列一致),不是总工作集。
|
||||
std::wstring title = kWindowTitle;
|
||||
title += kTitleSeparator;
|
||||
title += kRamTitlePrefix;
|
||||
title += FormatByteSize(ProcessWorkingSetBytes());
|
||||
title += FormatByteSize(ProcessPrivateWorkingSetBytes());
|
||||
// 数值未变化时不调用 SetWindowTextW,避免无谓地重绘标题栏。
|
||||
if (title == m_windowTitleText)
|
||||
{
|
||||
@@ -1282,7 +1329,7 @@ namespace jdi
|
||||
}
|
||||
if (control && key == L'T')
|
||||
{
|
||||
NewTab();
|
||||
NewTab(true);
|
||||
return true;
|
||||
}
|
||||
if (control && key == L'W')
|
||||
@@ -1412,7 +1459,7 @@ namespace jdi
|
||||
switch (result.type)
|
||||
{
|
||||
case TabBarResult::Type::ADD:
|
||||
NewTab();
|
||||
NewTab(true);
|
||||
break;
|
||||
case TabBarResult::Type::SELECT:
|
||||
if (result.index != m_activeTab)
|
||||
@@ -2050,7 +2097,7 @@ namespace jdi
|
||||
const int index = m_tabMenuIndex;
|
||||
if (result.id == kMenuTabNew)
|
||||
{
|
||||
NewTab();
|
||||
NewTab(true);
|
||||
}
|
||||
else if (result.id == kMenuTabClose)
|
||||
{
|
||||
|
||||
Reference in new issue
Block a user