深度修复实验性功能预览曲线导致的交互卡顿(平移/拖滑块/缩放/改样式不跟手)

根因:派生预览曲线按可见采样窗口计算,代价正比于窗口采样数(弱波增强整段 2576 万采样单条约 368ms);旧策略对超过 4M 采样的窗口不缓存,导致大跨度下每帧(含悬停重绘)重算整段曲线;拖滑块每帧改参数即每帧重算。实测弱波增强整段拖滑块单帧约 2303ms,2.5s 跨度约 89ms。

修复:1) 大窗口也进缓存并按总字节预算淘汰(静止重绘 2247ms-0.25ms);2) 交互期间对超出预算的大窗口延迟重算、复用旧曲线(拖滑块 2303ms-18ms),小窗口仍实时重算;3) 计算缓冲改 thread_local 复用容量。仅改何时计算与是否缓存,几何与取值算法不变,逐像素比对 0 差异。

缩放等级差异:整段放大时窗口为整条信号(千万级)每帧重算故最卡;极小跨度窗口仅几千采样故不卡;中间跨度几十毫秒亦卡。
This commit is contained in:
DeepSeek V4.1 Flash committed 2026-10-11 11:32:58 +08:00
1 parent 2f7a6bc2d2
commit aca84df0ac
4 files changed
+138 -19

No files matched your search

+83 -11
View File
@@ -80,16 +80,34 @@ namespace jdi
* 上限避免在完全缩小时把保护带放大到整条信号(那就退化成重算整条曲线)。
*/
const std::size_t kDerivedCurveGuardMax = 65536;
/** 区间窗口缓存条目上限(超出时整体清空;它只是加速手段)。 */
/** 区间窗口缓存条目上限(超出时淘汰最早的条目;它只是加速手段)。 */
const std::size_t kCurveSpanCacheLimit = 32;
/**
* @brief 进入「区间窗口缓存」的单条窗口采样数上限。
*
* 超过它的窗口(例如「中心轴边沿计数」在未限定光标区间时的整条信号)
* 改写进 `m_curveSpanScratch`:缓存的窗口一旦被淘汰就得重算整条曲线
* (亿级采样实测 ≈1s 的卡顿尖峰),而且几条曲线同时驻留就会吃掉数百 MB。
* **必须足够大**,使「完全放大到整条信号」的窗口也能进缓存:否则每一帧(含静止时
* 的鼠标悬停重绘)都要重算整条曲线 —— 弱波增强在 2576 万采样上单条 ≈368ms,
* 5 条网格就是 ≈1.8s/帧,交互完全无法进行。
*
* 窗口一旦计算完成,同一参数下(平移/缩放/悬停)都能直接复用;
* 内存总量由 `kCurveSpanCacheMaxBytes` 兜底,因此这里不再按采样数排除大窗口。
*/
const std::size_t kCurveSpanCacheMaxSamples = 4194304;
const std::size_t kCurveSpanCacheMaxSamples = 67108864;
/** 区间窗口缓存允许占用的总字节数(约 768MB):超出时按「最旧优先」淘汰。
* 取足够大是为了让「完全放大时 5 个网格各缓存一条整段曲线」可以同时驻留,
* 否则缓存互相淘汰会导致每帧重算其中一条(弱波增强 ≈368ms 的卡顿尖峰)。 */
const std::size_t kCurveSpanCacheMaxBytes = 768ull * 1024ull * 1024ull;
/**
* @brief 交互期间允许**同步重算**的派生曲线窗口采样数上限。
*
* 拖动滑块 / 移动视界 / 缩放等交互每帧都会请求派生(预览)曲线。窗口不大时同步重算
* 足够快(几毫秒),预览曲线能实时跟随控件;一旦窗口超过本上限(例如大时间跨度或
* 整条信号),同步重算会阻塞交互(弱波增强整条信号单条 ≈368ms)。
*
* 此时**不在交互过程中重算**,而是复用上一次算好的曲线(位置由当前视图映射,仍准确),
* 等交互结束后再按最终参数精确重算 —— 控件(滑块/光标/视界)的反馈因此始终跟手。
*/
const std::size_t kDerivedInteractiveComputeBudget = 131072;
const wchar_t* const kWeakBoostLevelLabel = L"弱波增强等级"; /**< 等级滑块行左侧的说明文字。 */
const wchar_t* const kWeakBoostIterationsLabel = L"迭代次数 N"; /**< 迭代次数滑块行左侧的说明文字。 */
const wchar_t* const kWeakBoostTitle = L"弱波增强"; /**< 弱波增强面板标题。 */
@@ -2105,7 +2123,8 @@ namespace jdi
const std::vector<float>* PlotView::CurveSpanFor(DerivedCurveKind kind, const SignalData& signal,
int points, int iterations, int levelPercent,
std::size_t begin, std::size_t end,
std::size_t& spanBegin) const
std::size_t& spanBegin,
bool allowInteractiveDefer) const
{
spanBegin = 0;
const std::size_t count = signal.values.size();
@@ -2141,6 +2160,43 @@ namespace jdi
}
}
// ⚠️ 交互期间的「延迟重算」:拖动滑块 / 移动视界 / 缩放每帧都会请求预览曲线,
// 若窗口很大(例如大时间跨度、整条信号),同步重算会阻塞交互数毫秒到数百毫秒
// (弱波增强整条信号单条 ≈368ms)。此时**不重算**,改复用上一次算好的曲线:
// 曲线下标映射到时间仍是当前视图的映射,因此位置依旧准确,只是参数/覆盖范围可能是
// 上一帧的;等交互结束后(`m_dragMode == NONE`)再按最终参数精确重算。
// 这样滑块 / 光标 / 视界移动的反馈始终跟手,不会被绘制计算拖住。
if (allowInteractiveDefer && m_dragMode != PlotDragMode::NONE &&
(end - begin) > kDerivedInteractiveComputeBudget)
{
const CurveSpanEntry* covering = nullptr;
const CurveSpanEntry* newest = nullptr;
for (auto it = m_curveSpanCache.rbegin(); it != m_curveSpanCache.rend(); ++it)
{
if (it->signal != &signal || it->kind != kindValue)
{
continue;
}
if (newest == nullptr)
{
newest = &*it;
}
if (it->begin <= begin && it->end >= end)
{
covering = &*it;
break;
}
}
const CurveSpanEntry* fallback = (covering != nullptr) ? covering : newest;
if (fallback != nullptr)
{
spanBegin = fallback->begin;
return &fallback->values;
}
// 完全没有可复用的曲线:本帧跳过该预览曲线(不阻塞交互)。
return nullptr;
}
// 未命中:向外扩一个保护带后再计算,保证可见区间内的取值与"整条曲线"一致。
// 保护带取窗口长度的 1/8(夹在 4096~65536):弱波增强的修正沿相邻极值向内传播
// (每迭代一次传一个极值),1/8 个可见宽度足以覆盖真实信号的极值间距。
@@ -2183,9 +2239,23 @@ namespace jdi
return &m_curveSpanScratch;
}
if (m_curveSpanCache.size() >= kCurveSpanCacheLimit)
// 缓存窗口:条目数与总字节数双重约束,避免大窗口把内存吃满。
// 淘汰策略为「最旧优先」(`deque` 头部即最早写入的)。
const auto cacheBytes = [this]() -> std::size_t
{
m_curveSpanCache.clear();
std::size_t total = 0;
for (const CurveSpanEntry& cached : m_curveSpanCache)
{
total += cached.values.size() * sizeof(float);
}
return total;
};
while (!m_curveSpanCache.empty() &&
(m_curveSpanCache.size() >= kCurveSpanCacheLimit ||
cacheBytes() + (computedEnd - computedBegin) * sizeof(float) >
kCurveSpanCacheMaxBytes))
{
m_curveSpanCache.pop_front();
}
m_curveSpanCache.push_back(CurveSpanEntry());
CurveSpanEntry& entry = m_curveSpanCache.back();
@@ -7395,7 +7465,7 @@ namespace jdi
std::size_t spanBegin = 0;
const std::vector<float>* curvePtr =
CurveSpanFor(kind, *resolved.data, points, iterations, levelPercent, begin,
end, spanBegin);
end, spanBegin, true);
if (curvePtr == nullptr)
{
return;
@@ -8161,7 +8231,8 @@ namespace jdi
}
std::size_t spanBegin = 0;
const std::vector<float>* curve = CurveSpanFor(DerivedCurveKind::MEAN_FILTER, *resolved.data,
points, iterations, 0, begin, end, spanBegin);
points, iterations, 0, begin, end, spanBegin,
true);
if (curve == nullptr)
{
return;
@@ -8186,7 +8257,8 @@ namespace jdi
}
std::size_t spanBegin = 0;
const std::vector<float>* curve = CurveSpanFor(DerivedCurveKind::WEAK_BOOST, *resolved.data, 0,
iterations, levelPercent, begin, end, spanBegin);
iterations, levelPercent, begin, end, spanBegin,
true);
if (curve == nullptr)
{
return;