From 79c84edea8cd8f629efa364c2ba3494b23c02a23 Mon Sep 17 00:00:00 2001 From: "DeepSeek V4.1 Flash" <> Date: Sat, 10 Oct 2026 09:32:59 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BF=AE=E5=A4=8D=E5=BC=80=E5=90=AF=E8=BE=B9?= =?UTF-8?q?=E6=B2=BF=E8=AE=A1=E6=95=B0=E5=90=8E=E6=8B=96=E5=8A=A8=E6=97=B6?= =?UTF-8?q?=E9=97=B4=E5=85=89=E6=A0=87=E5=8D=A1=E9=A1=BF=EF=BC=88=E5=A2=9E?= =?UTF-8?q?=E9=87=8F=E7=BB=9F=E8=AE=A1=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 症状:VERY_BIG_DATA.CSV 全选信号后拖动时间光标,未开启实验性功能时约 13ms/帧很顺畅, 一旦对某个网格开启「边沿计数」或「边沿计数(中心轴)」,拖动就变卡(实测约 24ms / 34ms 每帧)。 根因:两个边沿统计函数的缓存键里包含时间区间(rangeMin/rangeMax)。拖动时间光标时区间 每帧都在变,于是每帧全部缓存失效,对网格内每条可见信号重扫整段区间(亿级采样即数十毫秒); 同时缓存是无上限的 vector,每个新区间都会追加一条,拖一会儿就积累数百条,查找也退化为线性扫描。 修复:缓存键改为「数据分组 + 信号 + 判定基准(横向光标幅值 / 平滑半径)+ 是否限定区间」, 时间区间不再进键;缓存项改为记录「上一次统计的采样下标区间 [begin, end) 与其计数」: - 区间完全一致 → 直接命中; - 与上一帧共享一个端点(拖动光标时必然如此)→ 只扫描两端差集并增量加减; - 两端都变 / 首次统计 / 时间轴非单调 → 整段重算。 另加缓存条目上限(48 条,超出淘汰最早一条),避免长时间拖动导致缓存无界增长。 新增的滑动区间基元 AccumulateEdgeCrossings(pairFirst, pairLast) 统计采样对 k ∈ [pairFirst, pairLast](每对为 (k-1, k)),起点对的连续性由其前一个采样决定;这样把区间切成互不相交的 若干段分别累加(或从总量中减去)与一次性扫描整段完全等价,是增量加减的正确性依据。 验证(VERY_BIG_DATA、5 信号、双光标、网格 1 开启对应功能、反复拖动时间光标): - 中心轴边沿计数 33.8ms → 13.3ms/帧;普通边沿计数 24.5ms → 11.3ms/帧; - 用临时探针把增量结果与「整段重算」逐位比对(两个功能都覆盖),零 MISMATCH; - 面板数值随拖动实时更新,单光标(无区间)走全信号统计并被缓存,横向光标与面板交互不变。 --- Components/PlotView/Inc/PlotView.hpp | 46 +-- Components/PlotView/Src/PlotView.cpp | 404 ++++++++++++++++++--------- 2 files changed, 297 insertions(+), 153 deletions(-) diff --git a/Components/PlotView/Inc/PlotView.hpp b/Components/PlotView/Inc/PlotView.hpp index f09d23a..1e74b84 100644 --- a/Components/PlotView/Inc/PlotView.hpp +++ b/Components/PlotView/Inc/PlotView.hpp @@ -1235,17 +1235,26 @@ namespace jdi std::vector m_cursorLabelRects; /**< 本次绘制已占用的取值标注矩形(用于避让重叠)。 */ std::vector m_lanes; /**< 当前网格绘制信息集合。 */ - /** 边沿统计结果缓存项(按「文件 + 信号 + 横向光标幅值 + 时间区间」标识,任一变化即重算)。 */ + /** + * @brief 边沿统计缓存项。 + * + * 计数只由「采样下标区间 [begin, end) 与判定基准(横向光标幅值)」决定, + * 因此**时间区间不进缓存键**:拖动时间光标时区间每帧都在变,若按时间区间做键就会 + * 每帧全量重扫整段区间(亿级采样下正是拖拽卡顿的主因,实测单帧数十毫秒)。 + * 改为记住「上一帧统计过的下标区间」:新区间与旧区间共享一个端点时(拖动时必然如此) + * 只扫描两端差集并增量加减,单帧代价与光标位移成正比、与区间长度无关。 + */ struct EdgeCountEntry { - const DataFile* file = nullptr; - const SignalData* signal = nullptr; - double edgeValue = 0.0; - bool hasRange = false; - double rangeMin = 0.0; - double rangeMax = 0.0; - std::size_t rising = 0; - std::size_t falling = 0; + const DataFile* file = nullptr; /**< 所属数据分组。 */ + const SignalData* signal = nullptr; /**< 目标信号。 */ + 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; /**< 该区间内的下降沿数量。 */ }; mutable std::vector m_edgeCountCache; /**< 边沿统计缓存(纯缓存,逻辑上不影响对象状态)。 */ @@ -1259,17 +1268,18 @@ namespace jdi /** 中心平均值曲线缓存:**deque** 保证 push_back 后已有元素引用仍然有效。 */ mutable std::deque m_centerCurveCache; - /** 中心轴边沿统计缓存项。 */ + /** 中心轴边沿统计缓存项(增量统计机制与 `EdgeCountEntry` 完全一致)。 */ struct EdgeCenterCountEntry { - const DataFile* file = nullptr; - const SignalData* signal = nullptr; - int smooth = 0; - bool hasRange = false; - double rangeMin = 0.0; - double rangeMax = 0.0; - std::size_t rising = 0; - std::size_t falling = 0; + const DataFile* file = nullptr; /**< 所属数据分组。 */ + const SignalData* signal = nullptr; /**< 目标信号。 */ + int smooth = 0; /**< 中心平均值曲线的平滑半径 N。 */ + 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; /**< 该区间内的下降沿数量。 */ }; mutable std::vector m_edgeCenterCountCache; /**< 中心轴边沿统计缓存。 */ diff --git a/Components/PlotView/Src/PlotView.cpp b/Components/PlotView/Src/PlotView.cpp index 515f417..600e7ba 100644 --- a/Components/PlotView/Src/PlotView.cpp +++ b/Components/PlotView/Src/PlotView.cpp @@ -57,6 +57,110 @@ namespace jdi const float kEdgePanelEditFieldGap = 8.0f; /**< 编辑行说明文字与数值框之间的间隔。 */ const float kEdgePanelButtonHeight = 30.0f; /**< 均值滤波面板底部按钮行高度。 */ const wchar_t* const kMeanFilterBuildLabel = L"构建为新信号"; /**< 均值滤波面板底部按钮文案。 */ + const std::size_t kEdgeCountCacheLimit = 48; /**< 边沿统计缓存条目上限(超出时淘汰最早的一条)。 */ + + /** + * @brief 统计相邻采样对 (k-1, k) 中穿越判定基准的边沿数量。 + * + * `deltaAt(k)` 返回第 k 个采样相对判定基准的偏差;返回非有限值(NaN)表示该采样 + * 不参与判定,并**打断连续性**(与前一点之间不计边沿)——这正好覆盖 + * 「采样缺失」与「采样落在统计时间区间之外」两种情况。 + * + * 统计的是采样对 `k ∈ [pairFirst, pairLast]`(闭区间),每对的「前一点」为 `k-1`, + * 因此起点对的连续性由 `deltaAt(pairFirst - 1)` 决定(它无效时从下一个有效点重新起算)。 + * + * 该函数是增量统计的基本单位:只要把区间切成互不相交的若干段分别累加(或从总量中减去), + * 结果与一次性扫描整段完全一致,因此拖动光标时可以只扫描两端差集。 + * + * @param [IN] pairFirst 起始采样对的下标 k(含),必须 ≥ 1。 + * @param [IN] pairLast 结束采样对的下标 k(含);小于起点时不做任何事。 + * @param [IN] deltaAt 偏差取值仿函数。 + * @param [IN,OUT] rising 累加的上升沿数量。 + * @param [IN,OUT] falling 累加的下降沿数量。 + */ + template + void AccumulateEdgeCrossings(std::size_t pairFirst, std::size_t pairLast, DeltaAt deltaAt, + std::size_t& rising, std::size_t& falling) + { + if (pairFirst == 0 || pairFirst > pairLast) + { + return; + } + double previousDelta = deltaAt(pairFirst - 1); + bool hasPrevious = std::isfinite(previousDelta); + for (std::size_t index = pairFirst; index <= pairLast; ++index) + { + const double current = deltaAt(index); + if (!std::isfinite(current)) + { + hasPrevious = false; + continue; + } + if (hasPrevious) + { + if (previousDelta < 0.0 && current >= 0.0) + { + ++rising; + } + else if (previousDelta > 0.0 && current <= 0.0) + { + ++falling; + } + } + previousDelta = current; + hasPrevious = true; + } + } + + /** + * @brief 计算统计区间对应的采样下标区间 [begin, end)。 + * + * 时间轴单调时用二分定位,并在区间左侧多取一个采样点, + * 使跨越区间边界的边沿也能被计入。 + * + * @param [IN] file 所属数据分组。 + * @param [IN] count 采样点数量。 + * @param [IN] hasRange 是否限定时间区间。 + * @param [IN] rangeMin 区间左端点时间。 + * @param [IN] rangeMax 区间右端点时间。 + * @param [OUT] begin 输出区间起点(含)。 + * @param [OUT] end 输出区间终点(不含)。 + * @return 计数是否完全由该下标区间决定(时间轴单调时为真)。 + */ + bool EdgeIndexRange(const DataFile& file, std::size_t count, bool hasRange, double rangeMin, + double rangeMax, std::size_t& begin, std::size_t& end) + { + begin = 0; + end = count; + if (!hasRange) + { + return true; + } + if (!file.timeMonotonic) + { + // 时间轴非单调时无法用下标区间表达统计范围(需逐点按时间过滤)。 + return false; + } + + const std::vector& times = file.time; + const std::size_t timeCount = std::min(times.size(), count); + begin = static_cast( + std::lower_bound(times.begin(), times.begin() + static_cast(timeCount), + rangeMin) - + times.begin()); + end = static_cast( + std::upper_bound(times.begin(), times.begin() + static_cast(timeCount), + rangeMax) - + times.begin()); + if (begin > 0) + { + --begin; + } + end = std::min(end, timeCount); + begin = std::min(begin, end); + return true; + } + const std::uint64_t kHintHoldMs = 1300; /**< 提示浮层完全显示的停留时长(毫秒)。 */ const std::uint64_t kHintFadeMs = 500; /**< 提示浮层的淡出时长(毫秒)。 */ @@ -1576,91 +1680,108 @@ namespace jdi rising = 0; falling = 0; - for (const EdgeCenterCountEntry& entry : m_edgeCenterCountCache) - { - if (entry.file == &file && entry.signal == &signal && entry.smooth == smooth && - entry.hasRange == hasRange && entry.rangeMin == rangeMin && entry.rangeMax == rangeMax) - { - rising = entry.rising; - falling = entry.falling; - return; - } - } - const std::vector& curve = CenterCurveFor(signal, smooth); const std::size_t count = std::min(curve.size(), signal.values.size()); - std::size_t begin = 0; - std::size_t end = count; - if (hasRange && file.timeMonotonic) - { - const std::vector& times = file.time; - const std::size_t timeCount = std::min(times.size(), count); - begin = static_cast( - std::lower_bound(times.begin(), times.begin() + static_cast(timeCount), - rangeMin) - - times.begin()); - end = static_cast( - std::upper_bound(times.begin(), times.begin() + static_cast(timeCount), - rangeMax) - - times.begin()); - if (begin > 0) - { - --begin; - } - end = std::min(end, timeCount); - } - begin = std::min(begin, end); + std::size_t end = 0; + const bool indexed = EdgeIndexRange(file, count, hasRange, rangeMin, rangeMax, begin, end); - // 边沿判定:比较信号本身与其中心平均值曲线。 - // 信号由下向上越过中心曲线 → 上升沿;由上向下穿过 → 下降沿。 - bool hasPrevious = false; - double previousDelta = 0.0; - for (std::size_t index = begin; index < end; ++index) + // 判定基准:信号与中心平均值曲线之差(区间之外的采样返回 NaN)。 + const auto deltaAt = [&signal, &file, &curve, count, hasRange, rangeMin, + rangeMax](std::size_t index) -> double { + if (index >= count) + { + return std::numeric_limits::quiet_NaN(); + } const double value = signal.values[index]; const float center = curve[index]; if (!std::isfinite(value) || !std::isfinite(center)) { - hasPrevious = false; - continue; + return std::numeric_limits::quiet_NaN(); } if (hasRange && !file.timeMonotonic && index < file.time.size()) { const double time = file.time[index]; if (time < rangeMin || time > rangeMax) { - hasPrevious = false; - continue; + return std::numeric_limits::quiet_NaN(); } } + return value - static_cast(center); + }; - const double delta = value - static_cast(center); - if (hasPrevious) + // 按「文件 + 信号 + 平滑半径 + 是否限定区间」定位缓存项(**时间区间不进缓存键**)。 + EdgeCenterCountEntry* entry = nullptr; + for (EdgeCenterCountEntry& candidate : m_edgeCenterCountCache) + { + if (candidate.file == &file && candidate.signal == &signal && + candidate.hasRange == hasRange && candidate.smooth == smooth) { - if (previousDelta < 0.0 && delta >= 0.0) - { - ++rising; - } - else if (previousDelta > 0.0 && delta <= 0.0) - { - ++falling; - } + entry = &candidate; + break; } - previousDelta = delta; - hasPrevious = true; + } + if (entry == nullptr) + { + if (m_edgeCenterCountCache.size() >= kEdgeCountCacheLimit) + { + m_edgeCenterCountCache.erase(m_edgeCenterCountCache.begin()); + } + EdgeCenterCountEntry created; + created.file = &file; + created.signal = &signal; + created.smooth = smooth; + created.hasRange = hasRange; + m_edgeCenterCountCache.push_back(created); + entry = &m_edgeCenterCountCache.back(); } - EdgeCenterCountEntry entry; - entry.file = &file; - entry.signal = &signal; - entry.smooth = smooth; - entry.hasRange = hasRange; - entry.rangeMin = rangeMin; - entry.rangeMax = rangeMax; - entry.rising = rising; - entry.falling = falling; - m_edgeCenterCountCache.push_back(entry); + // 区间完全一致 → 直接命中。 + if (indexed && entry->reusable && entry->begin == begin && entry->end == end) + { + rising = entry->rising; + falling = entry->falling; + return; + } + + // 与上一帧共享一个端点(拖动光标时必然如此)→ 只扫描两端差集并增量加减。 + 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 std::size_t low = std::min(oldEnd, end); + const std::size_t high = std::max(oldEnd, end); + AccumulateEdgeCrossings(std::max(1, low), high - 1, deltaAt, deltaRising, + deltaFalling); + rising = (end > oldEnd) ? (entry->rising + deltaRising) : (entry->rising - deltaRising); + falling = (end > oldEnd) ? (entry->falling + deltaFalling) : (entry->falling - deltaFalling); + } + else if (shareEnd && !shareBegin) + { + const std::size_t low = std::min(oldBegin, begin); + const std::size_t high = std::max(oldBegin, begin); + AccumulateEdgeCrossings(low + 1, high, deltaAt, 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, deltaAt, rising, falling); + } + + entry->reusable = indexed; + entry->begin = begin; + entry->end = end; + entry->rising = rising; + entry->falling = falling; } void PlotView::EdgeCountsFor(const DataFile& file, const SignalData& signal, double edgeValue, @@ -1670,96 +1791,109 @@ namespace jdi rising = 0; falling = 0; - // 缓存键必须包含横向光标幅值与时间区间:拖动光标或拖动时间光标都会改变统计结果。 - for (const EdgeCountEntry& entry : m_edgeCountCache) - { - if (entry.file == &file && entry.signal == &signal && entry.hasRange == hasRange && - entry.edgeValue == edgeValue && entry.rangeMin == rangeMin && entry.rangeMax == rangeMax) - { - rising = entry.rising; - falling = entry.falling; - return; - } - } - - // 采样范围:时间单调时二分定位,否则按区间过滤(与波形绘制的处理一致)。 + const std::vector& values = signal.values; std::size_t begin = 0; - std::size_t end = signal.values.size(); - if (hasRange && file.timeMonotonic) - { - const std::vector& times = file.time; - const std::size_t count = std::min(times.size(), signal.values.size()); - begin = static_cast( - std::lower_bound(times.begin(), times.begin() + static_cast(count), - rangeMin) - - times.begin()); - end = static_cast( - std::upper_bound(times.begin(), times.begin() + static_cast(count), - rangeMax) - - times.begin()); - if (begin > 0) - { - // 区间左侧补一个采样点:跨越区间边界的边沿也能被计入。 - --begin; - } - end = std::min(end, count); - } - else if (hasRange) - { - end = std::min(end, signal.values.size()); - } - begin = std::min(begin, end); + std::size_t end = 0; + const bool indexed = EdgeIndexRange(file, values.size(), hasRange, rangeMin, rangeMax, begin, end); - // 边沿判定:以横向光标的幅值 edgeValue 为阈值。 - // 信号从下往上穿越 → 上升沿;从上往下穿越 → 下降沿。 - bool hasPrevious = false; - double previous = 0.0; - for (std::size_t index = begin; index < end; ++index) + // 判定基准:采样值相对阈值的偏差。区间之外的采样返回 NaN(等价于原实现 + // 「区间外采样不参与判定且打断连续」的语义)。 + const auto deltaAt = [&values, &file, hasRange, rangeMin, rangeMax](std::size_t index) -> double { - const double value = signal.values[index]; + const double value = values[index]; if (!std::isfinite(value)) { - // NaN 视为断点:不参与边沿判定,也不与前一点相连。 - hasPrevious = false; - continue; + return std::numeric_limits::quiet_NaN(); } if (hasRange && !file.timeMonotonic && index < file.time.size()) { const double time = file.time[index]; if (time < rangeMin || time > rangeMax) { - hasPrevious = false; - continue; + return std::numeric_limits::quiet_NaN(); } } + return value; + }; - if (hasPrevious) + // 按「文件 + 信号 + 阈值 + 是否限定区间」定位缓存项(**时间区间不进缓存键**)。 + EdgeCountEntry* entry = nullptr; + for (EdgeCountEntry& candidate : m_edgeCountCache) + { + if (candidate.file == &file && candidate.signal == &signal && + candidate.hasRange == hasRange && candidate.edgeValue == edgeValue) { - const double before = previous - edgeValue; - const double after = value - edgeValue; - if (before < 0.0 && after >= 0.0) - { - ++rising; - } - else if (before > 0.0 && after <= 0.0) - { - ++falling; - } + entry = &candidate; + break; } - previous = value; - hasPrevious = true; + } + if (entry == nullptr) + { + if (m_edgeCountCache.size() >= kEdgeCountCacheLimit) + { + m_edgeCountCache.erase(m_edgeCountCache.begin()); + } + EdgeCountEntry created; + created.file = &file; + created.signal = &signal; + created.edgeValue = edgeValue; + created.hasRange = hasRange; + m_edgeCountCache.push_back(created); + entry = &m_edgeCountCache.back(); } - EdgeCountEntry entry; - entry.file = &file; - entry.signal = &signal; - entry.edgeValue = edgeValue; - entry.hasRange = hasRange; - entry.rangeMin = rangeMin; - entry.rangeMax = rangeMax; - entry.rising = rising; - entry.falling = falling; - m_edgeCountCache.push_back(entry); + // 阈值参与判定,因此仿函数要在累加时一并带入。 + const auto crossingAt = [&deltaAt, edgeValue](std::size_t index) -> double + { return deltaAt(index) - edgeValue; }; + + // 区间完全一致 → 直接命中。 + if (indexed && entry->reusable && entry->begin == begin && entry->end == end) + { + rising = entry->rising; + falling = entry->falling; + return; + } + + // 与上一帧共享一个端点(拖动光标时必然如此)→ 只扫描两端差集并增量加减。 + 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) + { + // 右端移动:变动的是采样对 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(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); + } + else if (shareEnd && !shareBegin) + { + // 左端移动:变动的是采样对 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; } float PlotView::EdgePanelWidth(int laneIndex, EdgePanelKind kind) const