新标签页分组默认折起 + 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
@@ -149,6 +149,42 @@ namespace jdi
|
||||
const wchar_t* const kWeakPeakTitle = L"弱峰调整"; /**< 弱峰调整面板标题。 */
|
||||
const wchar_t* const kBuildSignalLabel = L"构建为新信号"; /**< 逐条目面板底部按钮文案(均值滤波 / 弱峰调整共用)。 */
|
||||
const std::size_t kEdgeCountCacheLimit = 48; /**< 边沿统计缓存条目上限(超出时淘汰最早的一条)。 */
|
||||
/**
|
||||
* @brief 「边沿计数」累积统计的分块大小(采样点数)。
|
||||
*
|
||||
* 每块各存上升/下降沿数:查询某个时间区间时,内部整块用存储值相加、只有两端边界块
|
||||
* 需要就地精确扫描(≤ 2 块),因此拖动时间光标的单帧代价与区间长度无关。
|
||||
* 取小一点 ⇒ 边界块扫描更便宜;取大一点 ⇒ 块数组更省内存。
|
||||
*/
|
||||
const std::size_t kEdgeBlockSamples = 8192;
|
||||
/**
|
||||
* @brief 每个 tick(约一帧)允许「边沿计数」扫描的采样对总数。
|
||||
*
|
||||
* 千万级信号整段扫描一次 ≈115ms(用户报告:5 条信号同一网格时单帧 ≈575ms,
|
||||
* 界面完全失去响应)。改成预算推进后,单帧最多只花 ≈5ms 在统计上,
|
||||
* 剩下的部分在后续帧里接着扫(未扫完时界面显示上一个完整结果 + “…”)。
|
||||
*/
|
||||
const std::size_t kEdgeScanPairsPerTick = 1200000;
|
||||
/**
|
||||
* @brief 「边沿计数」结果的显示文本。
|
||||
* @param [IN] rising 上升沿数量。 @param [IN] falling 下降沿数量。
|
||||
* @param [IN] complete 是否为该阈值的完整精确值。
|
||||
* @return 面板/宽度测量共用的文本(未算完时带省略号,完全未开始时显示“统计中…”)。
|
||||
*/
|
||||
std::wstring EdgeCountsText(std::size_t rising, std::size_t falling, bool complete)
|
||||
{
|
||||
if (!complete && rising == 0 && falling == 0)
|
||||
{
|
||||
return L"统计中…";
|
||||
}
|
||||
std::wstring text = L"上升沿 " + std::to_wstring(rising) + L" 下降沿 " +
|
||||
std::to_wstring(falling);
|
||||
if (!complete)
|
||||
{
|
||||
text += L" …";
|
||||
}
|
||||
return text;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief 判断坐标是否命中「弱峰调整」的某条滑块轨道。
|
||||
@@ -2896,27 +2932,31 @@ namespace jdi
|
||||
|
||||
void PlotView::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
|
||||
{
|
||||
rising = 0;
|
||||
falling = 0;
|
||||
complete = false;
|
||||
|
||||
const std::vector<double>& values = signal.values;
|
||||
// ⚠️ 阈值(横向光标幅值)是**屏幕上的值**,因此判定必须基于**画出来的曲线**:
|
||||
// 先按下标映射(水平镜像)取源采样,再套用「调整」变换(系数 → 绝对值 → 垂直镜像 →
|
||||
// Y 偏移)。只比较原始采样值时,“系数 50”这类信号的原始值域与阈值完全不相交,
|
||||
// 表现就是“明明是很规整的方波,统计出来却全是 0”(用户报告)。
|
||||
// Y 偏移)。只比较原始采样值时,"系数 50" 这类信号的原始值域与阈值完全不相交,
|
||||
// 表现就是"明明是很规整的方波,统计出来却全是 0"(用户报告)。
|
||||
const std::size_t count = values.size();
|
||||
if (count < 2)
|
||||
{
|
||||
complete = true;
|
||||
return;
|
||||
}
|
||||
const bool mirrored = style.mirrorHorizontal;
|
||||
const bool transform = (style.gain != 1.0f || style.absolute || style.mirrorVertical ||
|
||||
style.offsetY != 0.0f);
|
||||
const std::uint64_t styleKey = EdgeStyleKey(style);
|
||||
std::size_t begin = 0;
|
||||
std::size_t end = 0;
|
||||
const bool indexed = EdgeIndexRange(file, count, hasRange, rangeMin, rangeMax, begin, end);
|
||||
// 时间单调 ⇒ 统计区间可以用**采样对下标区间**表达(区间不进键,拖动时间光标无需重扫)。
|
||||
const bool monotonic = file.timeMonotonic;
|
||||
|
||||
// 判定基准:**绘制值**相对阈值的偏差。区间之外的采样返回 NaN(等价于原实现
|
||||
// 「区间外采样不参与判定且打断连续」的语义)。
|
||||
// 判定基准:**绘制值**相对阈值的偏差(区间之外 / 缺失值返回 NaN = 不参与且打断连续)。
|
||||
const auto deltaAt = [&values, &file, &style, mirrored, transform, count, hasRange, rangeMin,
|
||||
rangeMax](std::size_t index) -> double
|
||||
{
|
||||
@@ -2946,86 +2986,234 @@ namespace jdi
|
||||
return std::isfinite(drawn) ? drawn : std::numeric_limits<double>::quiet_NaN();
|
||||
};
|
||||
|
||||
// 按「文件 + 信号 + 样式指纹 + 阈值 + 是否限定区间」定位缓存项(**时间区间不进缓存键**)。
|
||||
EdgeCountEntry* entry = nullptr;
|
||||
for (EdgeCountEntry& candidate : m_edgeCountCache)
|
||||
// 请求的「采样对」区间 [pairFirst, pairLast](pair k = 采样 k-1 → k,k ≥ 1)。
|
||||
// ⚠️ 只有**两个端点都落在区间内**的采样对才计入(非单调时无法用下标表达,由 deltaAt 过滤)。
|
||||
std::size_t pairFirst = 1;
|
||||
std::size_t pairLast = count - 1;
|
||||
if (hasRange && monotonic)
|
||||
{
|
||||
if (candidate.file == &file && candidate.signal == &signal &&
|
||||
candidate.styleKey == styleKey && candidate.hasRange == hasRange &&
|
||||
candidate.edgeValue == edgeValue)
|
||||
std::size_t indexBegin = 0;
|
||||
std::size_t indexEnd = 0;
|
||||
EdgeIndexRange(file, count, hasRange, rangeMin, rangeMax, indexBegin, indexEnd);
|
||||
pairFirst = (indexBegin + 1 < count) ? (indexBegin + 1) : count;
|
||||
pairLast = (indexEnd > 0) ? (indexEnd - 1) : 0;
|
||||
if (pairLast < pairFirst)
|
||||
{
|
||||
entry = &candidate;
|
||||
break;
|
||||
complete = true;
|
||||
return;
|
||||
}
|
||||
}
|
||||
if (entry == nullptr)
|
||||
|
||||
// ---- 定位 / 推进累积条目 -------------------------------------------------
|
||||
// ⚠️ 拖动横向光标(`EDGE_MOVE`)时阈值每帧都变,推进等于纯白做功:此时不推进,
|
||||
// 直接把上一个完整结果当作临时值(界面用 "…" 标出"还没算完")。
|
||||
const bool thresholdDragging = (m_dragMode == PlotDragMode::EDGE_MOVE);
|
||||
const auto sameSignal = [&file, &signal, styleKey](const EdgeCountEntry& candidate)
|
||||
{ return candidate.file == &file && candidate.signal == &signal && candidate.styleKey == styleKey; };
|
||||
const auto sameKey = [&](const EdgeCountEntry& candidate)
|
||||
{
|
||||
if (m_edgeCountCache.size() >= kEdgeCountCacheLimit)
|
||||
if (!sameSignal(candidate) || candidate.threshold != edgeValue)
|
||||
{
|
||||
m_edgeCountCache.erase(m_edgeCountCache.begin());
|
||||
return false;
|
||||
}
|
||||
EdgeCountEntry created;
|
||||
created.file = &file;
|
||||
created.signal = &signal;
|
||||
created.styleKey = styleKey;
|
||||
created.edgeValue = edgeValue;
|
||||
created.hasRange = hasRange;
|
||||
m_edgeCountCache.push_back(created);
|
||||
entry = &m_edgeCountCache.back();
|
||||
}
|
||||
// 非单调时间轴:区间参与判定,必须一并比较(否则会混用别的区间结果)。
|
||||
return monotonic || (candidate.hasRange == hasRange && candidate.rangeMin == rangeMin &&
|
||||
candidate.rangeMax == rangeMax);
|
||||
};
|
||||
|
||||
// 阈值参与判定,因此仿函数要在累加时一并带入。
|
||||
const auto crossingAt = [&deltaAt, edgeValue](std::size_t index) -> double
|
||||
{ return deltaAt(index) - edgeValue; };
|
||||
|
||||
// 区间完全一致 → 直接命中。
|
||||
if (indexed && entry->reusable && entry->begin == begin && entry->end == end)
|
||||
// 用**下标**而不是指针记录候选(`push_back` 可能重新分配,指针会失效)。
|
||||
int entryIndex = -1;
|
||||
int newestCompleteIndex = -1;
|
||||
for (std::size_t index = 0; index < m_edgeCountCache.size(); ++index)
|
||||
{
|
||||
rising = entry->rising;
|
||||
falling = entry->falling;
|
||||
return;
|
||||
const EdgeCountEntry& candidate = m_edgeCountCache[index];
|
||||
if (!sameSignal(candidate))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
if (entryIndex < 0 && sameKey(candidate))
|
||||
{
|
||||
entryIndex = static_cast<int>(index);
|
||||
}
|
||||
if (candidate.complete)
|
||||
{
|
||||
newestCompleteIndex = static_cast<int>(index); // 淘汰从 front 开始 ⇒ 越靠后越新
|
||||
}
|
||||
}
|
||||
EdgeCountEntry* entry = (entryIndex >= 0) ? &m_edgeCountCache[static_cast<std::size_t>(entryIndex)] : nullptr;
|
||||
|
||||
if (!thresholdDragging && (entry == nullptr || !entry->complete))
|
||||
{
|
||||
if (entry == nullptr)
|
||||
{
|
||||
if (m_edgeCountCache.size() >= kEdgeCountCacheLimit)
|
||||
{
|
||||
m_edgeCountCache.erase(m_edgeCountCache.begin());
|
||||
}
|
||||
EdgeCountEntry created;
|
||||
created.file = &file;
|
||||
created.signal = &signal;
|
||||
created.styleKey = styleKey;
|
||||
created.threshold = edgeValue;
|
||||
created.hasRange = hasRange && !monotonic;
|
||||
created.rangeMin = rangeMin;
|
||||
created.rangeMax = rangeMax;
|
||||
const std::size_t blocks = (count - 1) / kEdgeBlockSamples + 2;
|
||||
created.blockRising.assign(blocks, 0);
|
||||
created.blockFalling.assign(blocks, 0);
|
||||
m_edgeCountCache.push_back(std::move(created));
|
||||
entry = &m_edgeCountCache.back();
|
||||
newestCompleteIndex = -1; // 可能已被重新分配/淘汰,不再使用它
|
||||
}
|
||||
|
||||
// 扫描预算按 tick 共享:同一帧里本函数会被调用多次(排版 + 绘制 + 多条信号),
|
||||
// 若每次都各扫一份,单帧总开销就会翻好几倍(实测整段 25.8M 採样 ≈115ms)。
|
||||
const std::uint64_t tick = GetTickCount64();
|
||||
if (tick != m_edgeScanTick)
|
||||
{
|
||||
m_edgeScanTick = tick;
|
||||
m_edgeScanBudget = kEdgeScanPairsPerTick;
|
||||
}
|
||||
if (m_edgeScanBudget > 0 && entry->scannedPair < count)
|
||||
{
|
||||
std::size_t scanned = 0;
|
||||
std::size_t block = (entry->scannedPair - 1) / kEdgeBlockSamples;
|
||||
std::size_t pair = entry->scannedPair;
|
||||
const bool fastPath = monotonic && !mirrored && !transform;
|
||||
while (pair < count && scanned < m_edgeScanBudget)
|
||||
{
|
||||
const std::size_t blockFirst = block * kEdgeBlockSamples + 1;
|
||||
const std::size_t blockLast = std::min(count - 1, (block + 1) * kEdgeBlockSamples);
|
||||
const std::size_t from = std::max(pair, blockFirst);
|
||||
const std::size_t to = std::min(blockLast, from + (m_edgeScanBudget - scanned) - 1);
|
||||
if (to < from)
|
||||
{
|
||||
break;
|
||||
}
|
||||
std::size_t localRising = 0;
|
||||
std::size_t localFalling = 0;
|
||||
if (fastPath)
|
||||
{
|
||||
// 紧凑循环:无变换 / 无镜像 / 时间单调 ⇒ 直接比较阈值,NaN 天然不计数。
|
||||
// (NaN 与任何数比较都为 false,因此不需要额外的 isfinite 分支。)
|
||||
double previous = values[from - 1] - edgeValue;
|
||||
for (std::size_t index = from; index <= to; ++index)
|
||||
{
|
||||
const double current = values[index] - edgeValue;
|
||||
if (previous < 0.0 && current >= 0.0)
|
||||
{
|
||||
++localRising;
|
||||
}
|
||||
else if (previous > 0.0 && current <= 0.0)
|
||||
{
|
||||
++localFalling;
|
||||
}
|
||||
previous = current;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
double previous = deltaAt(from - 1) - edgeValue;
|
||||
for (std::size_t index = from; index <= to; ++index)
|
||||
{
|
||||
const double current = deltaAt(index) - edgeValue;
|
||||
if (std::isfinite(previous) && std::isfinite(current))
|
||||
{
|
||||
if (previous < 0.0 && current >= 0.0)
|
||||
{
|
||||
++localRising;
|
||||
}
|
||||
else if (previous > 0.0 && current <= 0.0)
|
||||
{
|
||||
++localFalling;
|
||||
}
|
||||
}
|
||||
previous = current;
|
||||
}
|
||||
}
|
||||
if (block < entry->blockRising.size())
|
||||
{
|
||||
entry->blockRising[block] += localRising;
|
||||
entry->blockFalling[block] += localFalling;
|
||||
}
|
||||
entry->rising += localRising;
|
||||
entry->falling += localFalling;
|
||||
scanned += (to - from + 1);
|
||||
pair = to + 1;
|
||||
if (to == blockLast)
|
||||
{
|
||||
++block;
|
||||
}
|
||||
}
|
||||
entry->scannedPair = pair;
|
||||
entry->complete = (pair >= count);
|
||||
m_edgeScanBudget -= std::min(m_edgeScanBudget, scanned);
|
||||
}
|
||||
}
|
||||
|
||||
// 与上一帧共享一个端点(拖动光标时必然如此)→ 只扫描两端差集并增量加减。
|
||||
const bool shareBegin = indexed && entry->reusable && entry->begin == begin;
|
||||
const bool shareEnd = indexed && entry->reusable && entry->end == end;
|
||||
const std::size_t oldBegin = entry->begin;
|
||||
const std::size_t oldEnd = entry->end;
|
||||
|
||||
std::size_t deltaRising = 0;
|
||||
std::size_t deltaFalling = 0;
|
||||
if (shareBegin && !shareEnd)
|
||||
// ---- 汇总请求区间 -------------------------------------------------------
|
||||
// 优先用「键完全匹配」的条目(可能仍在推进);没有就用最新的完整条目顶替,
|
||||
// 这样拖动阈值期间数字保持稳定、算完后自动切换到精确值。
|
||||
const EdgeCountEntry* source = (entry != nullptr)
|
||||
? entry
|
||||
: ((newestCompleteIndex >= 0)
|
||||
? &m_edgeCountCache[static_cast<std::size_t>(newestCompleteIndex)]
|
||||
: nullptr);
|
||||
if (source == nullptr)
|
||||
{
|
||||
// 右端移动:变动的是采样对 k ∈ [min(oldEnd, end), max(oldEnd, end) - 1]。
|
||||
const std::size_t low = std::min(oldEnd, end);
|
||||
const std::size_t high = std::max(oldEnd, end);
|
||||
AccumulateEdgeCrossings(std::max<std::size_t>(1, low), high - 1, crossingAt, deltaRising,
|
||||
deltaFalling);
|
||||
rising = (end > oldEnd) ? (entry->rising + deltaRising) : (entry->rising - deltaRising);
|
||||
falling = (end > oldEnd) ? (entry->falling + deltaFalling) : (entry->falling - deltaFalling);
|
||||
return; // 尚无任何结果:界面显示"统计中…"
|
||||
}
|
||||
else if (shareEnd && !shareBegin)
|
||||
const auto sumRange = [&](const EdgeCountEntry& from, std::size_t first, std::size_t last,
|
||||
std::size_t& outRising, std::size_t& outFalling) -> bool
|
||||
{
|
||||
// 左端移动:变动的是采样对 k ∈ [min(oldBegin, begin) + 1, max(oldBegin, begin)]。
|
||||
const std::size_t low = std::min(oldBegin, begin);
|
||||
const std::size_t high = std::max(oldBegin, begin);
|
||||
AccumulateEdgeCrossings(low + 1, high, crossingAt, deltaRising, deltaFalling);
|
||||
rising = (begin < oldBegin) ? (entry->rising + deltaRising) : (entry->rising - deltaRising);
|
||||
falling = (begin < oldBegin) ? (entry->falling + deltaFalling) : (entry->falling - deltaFalling);
|
||||
}
|
||||
else
|
||||
{
|
||||
// 两端都变了(或首次统计):整段重算。
|
||||
const std::size_t pairFirst = (end > begin) ? (begin + 1) : 1;
|
||||
const std::size_t pairLast = (end > begin) ? (end - 1) : 0;
|
||||
AccumulateEdgeCrossings(pairFirst, pairLast, crossingAt, rising, falling);
|
||||
}
|
||||
|
||||
entry->reusable = indexed;
|
||||
entry->begin = begin;
|
||||
entry->end = end;
|
||||
entry->rising = rising;
|
||||
entry->falling = falling;
|
||||
outRising = 0;
|
||||
outFalling = 0;
|
||||
if (last < first)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
const std::size_t limit = (from.scannedPair > 0) ? std::min(last, from.scannedPair - 1) : 0;
|
||||
if (limit >= first)
|
||||
{
|
||||
std::size_t block = (first - 1) / kEdgeBlockSamples;
|
||||
const std::size_t lastBlock = (limit - 1) / kEdgeBlockSamples;
|
||||
for (; block <= lastBlock; ++block)
|
||||
{
|
||||
const std::size_t blockFirst = block * kEdgeBlockSamples + 1;
|
||||
const std::size_t blockLast = (block + 1) * kEdgeBlockSamples;
|
||||
const std::size_t low = std::max(first, blockFirst);
|
||||
const std::size_t high = std::min(limit, blockLast);
|
||||
const bool wholeBlock = (low == blockFirst) && (high == blockLast);
|
||||
if (wholeBlock && from.scannedPair > blockLast && block < from.blockRising.size())
|
||||
{
|
||||
outRising += from.blockRising[block];
|
||||
outFalling += from.blockFalling[block];
|
||||
continue;
|
||||
}
|
||||
// 边界块 / 尚未扫完的块:就地精确扫描这一小段(≤ 1 个块)。
|
||||
double previous = deltaAt(low - 1) - edgeValue;
|
||||
for (std::size_t index = low; index <= high; ++index)
|
||||
{
|
||||
const double current = deltaAt(index) - edgeValue;
|
||||
if (std::isfinite(previous) && std::isfinite(current))
|
||||
{
|
||||
if (previous < 0.0 && current >= 0.0)
|
||||
{
|
||||
++outRising;
|
||||
}
|
||||
else if (previous > 0.0 && current <= 0.0)
|
||||
{
|
||||
++outFalling;
|
||||
}
|
||||
}
|
||||
previous = current;
|
||||
}
|
||||
}
|
||||
}
|
||||
return (last < from.scannedPair);
|
||||
};
|
||||
const bool covered = sumRange(*source, pairFirst, pairLast, rising, falling);
|
||||
complete = covered && sameKey(*source);
|
||||
}
|
||||
|
||||
float PlotView::EdgePanelWidth(int laneIndex, EdgePanelKind kind) const
|
||||
@@ -3070,6 +3258,7 @@ namespace jdi
|
||||
}
|
||||
std::size_t rising = 0;
|
||||
std::size_t falling = 0;
|
||||
bool complete = true;
|
||||
if (meanFilter || weakPeak)
|
||||
{
|
||||
continue;
|
||||
@@ -3088,12 +3277,12 @@ namespace jdi
|
||||
else
|
||||
{
|
||||
EdgeCountsFor(*resolved.file, *resolved.data, *resolved.style, lane.edgeValue,
|
||||
EdgeRangeActive(), EdgeRangeMin(), EdgeRangeMax(), rising, falling);
|
||||
EdgeRangeActive(), EdgeRangeMin(), EdgeRangeMax(), rising, falling, complete);
|
||||
}
|
||||
// 宽度按实际要显示的文本测量(含未算完时的“统计中…”/省略号),
|
||||
// 否则推进过程中数字变长会被截断(统计数字必须完整显示)。
|
||||
const float countWidth =
|
||||
UiMeasureTextWidth(m_fonts->monoSmall,
|
||||
L"上升沿 " + std::to_wstring(rising) + L" 下降沿 " +
|
||||
std::to_wstring(falling));
|
||||
UiMeasureTextWidth(m_fonts->monoSmall, EdgeCountsText(rising, falling, complete));
|
||||
width = std::max(width, kEdgePanelPad * 2.0f * m_scale + std::max(nameWidth, countWidth));
|
||||
}
|
||||
|
||||
@@ -4233,12 +4422,12 @@ namespace jdi
|
||||
}
|
||||
}
|
||||
|
||||
// 图例中的「GX」编号徽标:悬停时返回**手型** —— 与左侧信号列表里同一个 GX 徽标的
|
||||
// 悬停效果完全一致(用户明确要求"参考信号选区的信号名后方的 GX 标签");
|
||||
// 真正按下拖拽时再切为上下箭头(见 LANE_REORDER 分支)。
|
||||
// 图例中的「GX」编号徽标:**悬停时不改变鼠标形状**(用户明确要求:只在按下左键
|
||||
// 拖拽时才显示上下箭头,见 `LANE_REORDER` 分支)。这里保持“不改变”的语义并
|
||||
// **提前返回**(不再往下落到光标线/图例判定),与原先的优先级完全一致。
|
||||
if (LaneBadgeAtPoint(x, y) >= 0)
|
||||
{
|
||||
return LoadCursorW(nullptr, IDC_HAND);
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
// 光标时间读数标签与双光标差值标签:**悬停(未按下)时不改变鼠标形状**。
|
||||
@@ -11665,10 +11854,11 @@ namespace jdi
|
||||
|
||||
std::size_t rising = 0;
|
||||
std::size_t falling = 0;
|
||||
bool complete = true;
|
||||
EdgeCountsFor(*resolved.file, *resolved.data, *resolved.style, tabLane.edgeValue,
|
||||
EdgeRangeActive(), EdgeRangeMin(), EdgeRangeMax(), rising, falling);
|
||||
const std::wstring counts = L"上升沿 " + std::to_wstring(rising) + L" 下降沿 " +
|
||||
std::to_wstring(falling);
|
||||
EdgeRangeActive(), EdgeRangeMin(), EdgeRangeMax(), rising, falling,
|
||||
complete);
|
||||
const std::wstring counts = EdgeCountsText(rising, falling, complete);
|
||||
UiText(g, counts, m_fonts->monoSmall,
|
||||
gd::RectF(panel.X + pad, rowY + rowHeight, textWidth, rowHeight),
|
||||
m_theme->textMuted, TextAlign::LEFT);
|
||||
|
||||
Reference in new issue
Block a user