派生曲线计算与 UI 绘制分离(后台线程 + 双缓冲),彻底消除交互卡顿
根因:重计算发生在绘制线程内(Paint 同步算派生预览曲线),一次整段弱波增强约 368ms 会阻塞消息循环,滑块/光标/视界失去跟手。 方案:新增后台计算线程 + 双缓冲。大窗口预览曲线不在绘制线程计算,本帧用最新旧曲线顶替并排后台任务;小窗口仍同步以保证预览实时;后台算完通过自定义消息通知宿主重绘(双缓冲切换)。条目用 shared_ptr 共享、绘制线程钉住整帧有效,后台只追加不改写,无数据竞争。任何改变数据层结构的操作前先 PauseDerivedAsync(join 后台线程),杜绝读取已释放/已移动采样。 实测 VERY_BIG_DATA(5 网格):弱波增强平移 2247ms→15ms,拖滑块 2303ms→15ms,静止重绘→0.25ms,交互均 ≤约 30ms,无 300ms/2s 级尖峰。小窗口同步路径与旧实现逐像素 0 差异。
This commit is contained in:
1 parent
aca84df0ac
commit
99247622c1
4 files changed
+520
-167
No files matched your search
@@ -80,34 +80,20 @@ namespace jdi
|
||||
* 上限避免在完全缩小时把保护带放大到整条信号(那就退化成重算整条曲线)。
|
||||
*/
|
||||
const std::size_t kDerivedCurveGuardMax = 65536;
|
||||
/** 区间窗口缓存条目上限(超出时淘汰最早的条目;它只是加速手段)。 */
|
||||
/** 派生曲线缓存条目上限(超出时按「最旧优先」淘汰;它只是加速与内存控制手段)。 */
|
||||
const std::size_t kCurveSpanCacheLimit = 32;
|
||||
/**
|
||||
* @brief 进入「区间窗口缓存」的单条窗口采样数上限。
|
||||
*
|
||||
* **必须足够大**,使「完全放大到整条信号」的窗口也能进缓存:否则每一帧(含静止时
|
||||
* 的鼠标悬停重绘)都要重算整条曲线 —— 弱波增强在 2576 万采样上单条 ≈368ms,
|
||||
* 5 条网格就是 ≈1.8s/帧,交互完全无法进行。
|
||||
*
|
||||
* 窗口一旦计算完成,同一参数下(平移/缩放/悬停)都能直接复用;
|
||||
* 内存总量由 `kCurveSpanCacheMaxBytes` 兜底,因此这里不再按采样数排除大窗口。
|
||||
*/
|
||||
const std::size_t kCurveSpanCacheMaxSamples = 67108864;
|
||||
/** 区间窗口缓存允许占用的总字节数(约 768MB):超出时按「最旧优先」淘汰。
|
||||
* 取足够大是为了让「完全放大时 5 个网格各缓存一条整段曲线」可以同时驻留,
|
||||
* 否则缓存互相淘汰会导致每帧重算其中一条(弱波增强 ≈368ms 的卡顿尖峰)。 */
|
||||
/** 派生曲线缓存允许占用的总字节数(约 768MB):超出时按「最旧优先」淘汰。
|
||||
* 取足够大是为了让「完全放大时 5 个网格各缓存一条整段曲线」可以同时驻留。 */
|
||||
const std::size_t kCurveSpanCacheMaxBytes = 768ull * 1024ull * 1024ull;
|
||||
/**
|
||||
* @brief 交互期间允许**同步重算**的派生曲线窗口采样数上限。
|
||||
* @brief 派生(预览)曲线走「后台异步计算」的窗口采样数门槛。
|
||||
*
|
||||
* 拖动滑块 / 移动视界 / 缩放等交互每帧都会请求派生(预览)曲线。窗口不大时同步重算
|
||||
* 足够快(几毫秒),预览曲线能实时跟随控件;一旦窗口超过本上限(例如大时间跨度或
|
||||
* 整条信号),同步重算会阻塞交互(弱波增强整条信号单条 ≈368ms)。
|
||||
*
|
||||
* 此时**不在交互过程中重算**,而是复用上一次算好的曲线(位置由当前视图映射,仍准确),
|
||||
* 等交互结束后再按最终参数精确重算 —— 控件(滑块/光标/视界)的反馈因此始终跟手。
|
||||
* 拖动滑块 / 移动视界 / 缩放每帧都会请求预览曲线。窗口不超过本门槛时同步重算足够快
|
||||
* (弱波增强 ≈0.9ms/6.5 万采样),预览曲线能**实时跟随控件**;一旦窗口超过门槛
|
||||
* (大时间跨度、整条信号),同步重算会阻塞交互(弱波增强整条信号单条 ≈368ms),
|
||||
* 此时改为交后台线程计算、本帧先用旧曲线顶替(双缓冲),保证控件反馈始终跟手。
|
||||
*/
|
||||
const std::size_t kDerivedInteractiveComputeBudget = 131072;
|
||||
const std::size_t kDerivedAsyncWindowThreshold = 65536;
|
||||
const wchar_t* const kWeakBoostLevelLabel = L"弱波增强等级"; /**< 等级滑块行左侧的说明文字。 */
|
||||
const wchar_t* const kWeakBoostIterationsLabel = L"迭代次数 N"; /**< 迭代次数滑块行左侧的说明文字。 */
|
||||
const wchar_t* const kWeakBoostTitle = L"弱波增强"; /**< 弱波增强面板标题。 */
|
||||
@@ -300,6 +286,47 @@ namespace jdi
|
||||
/** 实验性功能浮动面板的种类数(遍历与命中测试统一用它,避免新增功能时漏改)。 */
|
||||
const int kEdgePanelKindCount = static_cast<int>(sizeof(kEdgePanelKinds) / sizeof(kEdgePanelKinds[0]));
|
||||
|
||||
/**
|
||||
* @brief 计算一条派生曲线在给定采样窗口内的取值(同步路径与后台线程共用)。
|
||||
*
|
||||
* 只拷贝所需的窗口,再调用与「构建为新信号」完全相同的自由函数,因此
|
||||
* 预览曲线与构建出的采样值逐位一致。本函数**只读**入参、不接触任何成员状态,
|
||||
* 因此可以安全地在后台线程调用。
|
||||
*
|
||||
* @param [IN] signal 目标信号(数据只读)。
|
||||
* @param [IN] windowBegin 窗口起点(采样下标)。
|
||||
* @param [IN] windowEnd 窗口终点(不含)。
|
||||
* @param [IN] kind `DerivedCurveKind` 的整型值。
|
||||
* @param [IN] points 均值点数 X。
|
||||
* @param [IN] iterations 迭代次数 N。
|
||||
* @param [IN] levelPercent 弱波增强等级(0–100)。
|
||||
* @return 曲线数值(长度 = 有效窗口长度)。
|
||||
*/
|
||||
std::vector<float> ComputeDerivedWindow(const SignalData& signal, std::size_t windowBegin,
|
||||
std::size_t windowEnd, int kind, int points,
|
||||
int iterations, int levelPercent)
|
||||
{
|
||||
std::vector<float> out;
|
||||
const std::vector<double>& values = signal.values;
|
||||
const std::size_t hi = std::min(windowEnd, values.size());
|
||||
const std::size_t lo = std::min(windowBegin, hi);
|
||||
if (hi <= lo)
|
||||
{
|
||||
return out;
|
||||
}
|
||||
const std::vector<double> window(values.begin() + static_cast<std::ptrdiff_t>(lo),
|
||||
values.begin() + static_cast<std::ptrdiff_t>(hi));
|
||||
if (kind == static_cast<int>(DerivedCurveKind::WEAK_BOOST))
|
||||
{
|
||||
ComputeWeakWaveBoost(window, static_cast<double>(levelPercent) / 100.0, iterations, out);
|
||||
}
|
||||
else
|
||||
{
|
||||
ComputeMovingAverageIterated(window, points, iterations, out);
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief 包络带的单个像素列(已映射到屏幕坐标)。
|
||||
*/
|
||||
@@ -642,6 +669,8 @@ namespace jdi
|
||||
|
||||
PlotView::~PlotView()
|
||||
{
|
||||
// 先停掉后台计算线程:它可能正在读取采样数据,必须在释放其它资源前回收。
|
||||
StopCurveWorker();
|
||||
ReleaseSceneCache();
|
||||
ReleasePanelCache();
|
||||
}
|
||||
@@ -1254,13 +1283,14 @@ namespace jdi
|
||||
|
||||
void PlotView::InvalidateDerivedCaches()
|
||||
{
|
||||
// 后台线程可能在读取采样数据:先暂停并作废异步结果,再清同步缓存。
|
||||
PauseDerivedAsync();
|
||||
m_polylineCache.clear();
|
||||
// 数据层结构变化后边沿统计与均值滤波曲线的派生结果都会失效。
|
||||
m_edgeCountCache.clear();
|
||||
m_edgeCenterCountCache.clear();
|
||||
m_meanCurveCache.clear();
|
||||
m_weakBoostCurveCache.clear();
|
||||
m_curveSpanCache.clear();
|
||||
// 信号下标可能已经整体位移:清掉按引用缓存的悬停/拖拽目标,避免指向错误的信号。
|
||||
m_hoverSignal = SignalRef();
|
||||
m_hoverLegend = SignalRef();
|
||||
@@ -1269,6 +1299,18 @@ namespace jdi
|
||||
InvalidateSceneCache();
|
||||
}
|
||||
|
||||
void PlotView::PauseDerivedAsync()
|
||||
{
|
||||
// 等待在算任务结束,然后清空队列与缓存,并自增代次使任何迟到结果作废。
|
||||
StopCurveWorker();
|
||||
std::lock_guard<std::mutex> lock(m_curveMutex);
|
||||
++m_curveGeneration;
|
||||
m_curveJobs.clear();
|
||||
m_curveEntries.clear();
|
||||
m_curvePins.clear();
|
||||
m_curveSceneDirty.store(false, std::memory_order_relaxed);
|
||||
}
|
||||
|
||||
bool PlotView::FitTimeToLane(int laneIndex)
|
||||
{
|
||||
if (m_dataSet == nullptr || m_tab == nullptr)
|
||||
@@ -2124,7 +2166,7 @@ namespace jdi
|
||||
int points, int iterations, int levelPercent,
|
||||
std::size_t begin, std::size_t end,
|
||||
std::size_t& spanBegin,
|
||||
bool allowInteractiveDefer) const
|
||||
bool allowAsyncCompute) const
|
||||
{
|
||||
spanBegin = 0;
|
||||
const std::size_t count = signal.values.size();
|
||||
@@ -2149,135 +2191,284 @@ namespace jdi
|
||||
const std::size_t alignedBegin = (begin / grid) * grid;
|
||||
const std::size_t alignedEnd = std::min(count, ((end + grid - 1) / grid) * grid);
|
||||
|
||||
for (const CurveSpanEntry& entry : m_curveSpanCache)
|
||||
CurveKey key;
|
||||
key.signal = &signal;
|
||||
key.kind = kindValue;
|
||||
key.points = safePoints;
|
||||
key.iterations = safeIterations;
|
||||
key.levelPercent = safeLevel;
|
||||
|
||||
// 1) 已有覆盖请求区间的最新条目 → 直接复用(钉住,防止本帧被回收)。
|
||||
if (const CurveEntry* hit = AcquireCoveringCurve(key, begin, end, spanBegin))
|
||||
{
|
||||
if (entry.signal == &signal && entry.kind == kindValue && entry.points == safePoints &&
|
||||
entry.iterations == safeIterations && entry.levelPercent == safeLevel &&
|
||||
entry.begin <= begin && entry.end >= end)
|
||||
{
|
||||
spanBegin = entry.begin;
|
||||
return &entry.values;
|
||||
}
|
||||
return &hit->values;
|
||||
}
|
||||
|
||||
// ⚠️ 交互期间的「延迟重算」:拖动滑块 / 移动视界 / 缩放每帧都会请求预览曲线,
|
||||
// 若窗口很大(例如大时间跨度、整条信号),同步重算会阻塞交互数毫秒到数百毫秒
|
||||
// (弱波增强整条信号单条 ≈368ms)。此时**不重算**,改复用上一次算好的曲线:
|
||||
// 曲线下标映射到时间仍是当前视图的映射,因此位置依旧准确,只是参数/覆盖范围可能是
|
||||
// 上一帧的;等交互结束后(`m_dragMode == NONE`)再按最终参数精确重算。
|
||||
// 这样滑块 / 光标 / 视界移动的反馈始终跟手,不会被绘制计算拖住。
|
||||
if (allowInteractiveDefer && m_dragMode != PlotDragMode::NONE &&
|
||||
(end - begin) > kDerivedInteractiveComputeBudget)
|
||||
// 2) 未就绪且窗口很大:预览曲线交后台计算,本帧先用最新旧曲线顶替(双缓冲)。
|
||||
// 这样绘制线程永远不做重计算,拖动滑块/光标/视界/缩放的反馈因此始终跟手。
|
||||
if (allowAsyncCompute && (end - begin) > kDerivedAsyncWindowThreshold)
|
||||
{
|
||||
const CurveSpanEntry* covering = nullptr;
|
||||
const CurveSpanEntry* newest = nullptr;
|
||||
for (auto it = m_curveSpanCache.rbegin(); it != m_curveSpanCache.rend(); ++it)
|
||||
ScheduleCurveJob(key, alignedBegin, alignedEnd, count);
|
||||
if (const CurveEntry* stale = AcquireNewestCurve(key, spanBegin))
|
||||
{
|
||||
if (it->signal != &signal || it->kind != kindValue)
|
||||
return &stale->values;
|
||||
}
|
||||
return nullptr; // 尚无任何可显示曲线:本帧跳过该预览曲线(不阻塞绘制)
|
||||
}
|
||||
|
||||
// 3) 窗口较小或需要精确取值 → 同步计算并发布(代价可接受,且保证取值精确)。
|
||||
std::size_t computedBegin = 0;
|
||||
std::size_t computedEnd = 0;
|
||||
DerivedGuardWindow(kind, alignedBegin, alignedEnd, safePoints, safeIterations, count,
|
||||
computedBegin, computedEnd);
|
||||
if (computedEnd <= computedBegin)
|
||||
{
|
||||
return nullptr;
|
||||
}
|
||||
std::vector<float> values = ComputeDerivedWindow(signal, computedBegin, computedEnd, kindValue,
|
||||
safePoints, safeIterations, safeLevel);
|
||||
const CurveEntry* published =
|
||||
PublishCurve(key, computedBegin, computedEnd, std::move(values), spanBegin);
|
||||
return (published != nullptr) ? &published->values : nullptr;
|
||||
}
|
||||
|
||||
void PlotView::DerivedGuardWindow(DerivedCurveKind kind, std::size_t alignedBegin,
|
||||
std::size_t alignedEnd, int points, int iterations,
|
||||
std::size_t count, std::size_t& begin, std::size_t& end) const
|
||||
{
|
||||
// 保护带取窗口长度的 1/8(夹在 4096~65536):弱波增强的修正沿相邻极值向内传播,
|
||||
// 1/8 个可见宽度足以覆盖真实信号的极值间距。
|
||||
// 均值滤波必须额外**覆盖滤波半径**:N 阶共向外传播 X*N 个采样,否则窗口边界取值
|
||||
// 会与「整条曲线」不同(边界上可用采样数更少)。
|
||||
std::size_t guard =
|
||||
std::clamp((alignedEnd - alignedBegin) / 8, std::size_t(4096), kDerivedCurveGuardMax);
|
||||
if (kind == DerivedCurveKind::MEAN_FILTER)
|
||||
{
|
||||
const std::size_t reach =
|
||||
static_cast<std::size_t>(points + 1) * static_cast<std::size_t>(iterations) + 64u;
|
||||
guard = std::clamp(std::max(guard, reach), std::size_t(4096), kDerivedCurveGuardMax);
|
||||
}
|
||||
begin = (alignedBegin > guard) ? (alignedBegin - guard) : 0;
|
||||
end = std::min(count, alignedEnd + guard);
|
||||
}
|
||||
|
||||
const PlotView::CurveEntry* PlotView::AcquireCoveringCurve(const CurveKey& key, std::size_t begin,
|
||||
std::size_t end,
|
||||
std::size_t& spanBegin) const
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(m_curveMutex);
|
||||
const CurveEntryPtr* best = nullptr;
|
||||
for (const CurveEntryPtr& entry : m_curveEntries)
|
||||
{
|
||||
if (entry->key == key && entry->begin <= begin && entry->end >= end)
|
||||
{
|
||||
if (best == nullptr || entry->serial > (*best)->serial)
|
||||
{
|
||||
best = &entry;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (best == nullptr)
|
||||
{
|
||||
return nullptr;
|
||||
}
|
||||
m_curvePins.push_back(*best); // 钉住:保证本帧内指针有效
|
||||
spanBegin = (*best)->begin;
|
||||
return best->get();
|
||||
}
|
||||
|
||||
const PlotView::CurveEntry* PlotView::AcquireNewestCurve(const CurveKey& key,
|
||||
std::size_t& spanBegin) const
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(m_curveMutex);
|
||||
const CurveEntryPtr* newest = nullptr;
|
||||
for (const CurveEntryPtr& entry : m_curveEntries)
|
||||
{
|
||||
if (entry->key == key)
|
||||
{
|
||||
if (newest == nullptr || entry->serial > (*newest)->serial)
|
||||
{
|
||||
newest = &entry;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (newest == nullptr)
|
||||
{
|
||||
return nullptr;
|
||||
}
|
||||
m_curvePins.push_back(*newest);
|
||||
spanBegin = (*newest)->begin;
|
||||
return newest->get();
|
||||
}
|
||||
|
||||
const PlotView::CurveEntry* PlotView::PublishCurve(const CurveKey& key, std::size_t begin,
|
||||
std::size_t end, std::vector<float>&& values,
|
||||
std::size_t& spanBegin) const
|
||||
{
|
||||
std::shared_ptr<CurveEntry> entry = std::make_shared<CurveEntry>();
|
||||
entry->key = key;
|
||||
entry->begin = begin;
|
||||
entry->end = end;
|
||||
entry->values = std::move(values);
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(m_curveMutex);
|
||||
entry->serial = ++m_curveSerial;
|
||||
m_curveEntries.push_back(entry);
|
||||
}
|
||||
m_curvePins.push_back(entry);
|
||||
spanBegin = begin;
|
||||
return entry.get();
|
||||
}
|
||||
|
||||
void PlotView::ScheduleCurveJob(const CurveKey& key, std::size_t alignedBegin,
|
||||
std::size_t alignedEnd, std::size_t count) const
|
||||
{
|
||||
std::size_t windowBegin = 0;
|
||||
std::size_t windowEnd = 0;
|
||||
DerivedGuardWindow(static_cast<DerivedCurveKind>(key.kind), alignedBegin, alignedEnd, key.points,
|
||||
key.iterations, count, windowBegin, windowEnd);
|
||||
if (windowEnd <= windowBegin)
|
||||
{
|
||||
return;
|
||||
}
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(m_curveMutex);
|
||||
for (const CurveJob& job : m_curveJobs)
|
||||
{
|
||||
if (job.key == key)
|
||||
{
|
||||
return; // 该曲线已有在算任务,避免任务堆积
|
||||
}
|
||||
}
|
||||
CurveJob job;
|
||||
job.key = key;
|
||||
job.windowBegin = windowBegin;
|
||||
job.windowEnd = windowEnd;
|
||||
job.generation = m_curveGeneration;
|
||||
m_curveJobs.push_back(job);
|
||||
}
|
||||
EnsureCurveWorker();
|
||||
m_curveCv.notify_one();
|
||||
}
|
||||
|
||||
void PlotView::EnsureCurveWorker() const
|
||||
{
|
||||
if (m_curveWorker.joinable())
|
||||
{
|
||||
return;
|
||||
}
|
||||
m_curveWorkerStop = false;
|
||||
m_curveWorker = std::thread([this]() { CurveWorkerLoop(); });
|
||||
}
|
||||
|
||||
void PlotView::StopCurveWorker() const
|
||||
{
|
||||
if (!m_curveWorker.joinable())
|
||||
{
|
||||
return;
|
||||
}
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(m_curveMutex);
|
||||
m_curveWorkerStop = true;
|
||||
m_curveJobs.clear();
|
||||
}
|
||||
m_curveCv.notify_all();
|
||||
m_curveWorker.join();
|
||||
}
|
||||
|
||||
void PlotView::CurveWorkerLoop() const
|
||||
{
|
||||
for (;;)
|
||||
{
|
||||
CurveJob job;
|
||||
{
|
||||
std::unique_lock<std::mutex> lock(m_curveMutex);
|
||||
m_curveCv.wait(lock, [this]() { return m_curveWorkerStop || !m_curveJobs.empty(); });
|
||||
if (m_curveJobs.empty())
|
||||
{
|
||||
return; // 仅在 stop 且队列为空时到达
|
||||
}
|
||||
job = m_curveJobs.front();
|
||||
m_curveJobs.pop_front();
|
||||
if (job.generation != m_curveGeneration)
|
||||
{
|
||||
continue; // 代次已变化:丢弃过期任务
|
||||
}
|
||||
}
|
||||
|
||||
// 计算在锁外进行:读取的采样在「结构未变」期间只读、且地址稳定
|
||||
//(结构变化前宿主会先 `PauseDerivedAsync`)。
|
||||
std::vector<float> values =
|
||||
ComputeDerivedWindow(*job.key.signal, job.windowBegin, job.windowEnd, job.key.kind,
|
||||
job.key.points, job.key.iterations, job.key.levelPercent);
|
||||
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(m_curveMutex);
|
||||
if (job.generation != m_curveGeneration)
|
||||
{
|
||||
continue; // 计算期间发生了结构变化:丢弃,避免发布过期结果
|
||||
}
|
||||
std::shared_ptr<CurveEntry> entry = std::make_shared<CurveEntry>();
|
||||
entry->key = job.key;
|
||||
entry->begin = job.windowBegin;
|
||||
entry->end = job.windowEnd;
|
||||
entry->serial = ++m_curveSerial;
|
||||
entry->values = std::move(values);
|
||||
m_curveEntries.push_back(std::move(entry));
|
||||
}
|
||||
|
||||
// 通知宿主重绘:下一帧会取到刚发布的曲线(双缓冲切换)。
|
||||
m_curveSceneDirty.store(true, std::memory_order_relaxed);
|
||||
const HWND host = m_host;
|
||||
if (host != nullptr)
|
||||
{
|
||||
PostMessageW(host, kMessagePlotDerivedReady, 0, 0);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void PlotView::PruneCurveEntriesLocked() const
|
||||
{
|
||||
// 1) 去掉被「同键、更新、且完全覆盖」的条目取代的旧条目。
|
||||
std::deque<CurveEntryPtr> kept;
|
||||
for (const CurveEntryPtr& entry : m_curveEntries)
|
||||
{
|
||||
bool superseded = false;
|
||||
for (const CurveEntryPtr& other : m_curveEntries)
|
||||
{
|
||||
if (other.get() == entry.get())
|
||||
{
|
||||
continue;
|
||||
}
|
||||
if (newest == nullptr)
|
||||
if (other->key == entry->key && other->serial > entry->serial &&
|
||||
other->begin <= entry->begin && other->end >= entry->end)
|
||||
{
|
||||
newest = &*it;
|
||||
}
|
||||
if (it->begin <= begin && it->end >= end)
|
||||
{
|
||||
covering = &*it;
|
||||
superseded = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
const CurveSpanEntry* fallback = (covering != nullptr) ? covering : newest;
|
||||
if (fallback != nullptr)
|
||||
if (!superseded)
|
||||
{
|
||||
spanBegin = fallback->begin;
|
||||
return &fallback->values;
|
||||
kept.push_back(entry);
|
||||
}
|
||||
// 完全没有可复用的曲线:本帧跳过该预览曲线(不阻塞交互)。
|
||||
return nullptr;
|
||||
}
|
||||
m_curveEntries.swap(kept);
|
||||
|
||||
// 未命中:向外扩一个保护带后再计算,保证可见区间内的取值与"整条曲线"一致。
|
||||
// 保护带取窗口长度的 1/8(夹在 4096~65536):弱波增强的修正沿相邻极值向内传播
|
||||
// (每迭代一次传一个极值),1/8 个可见宽度足以覆盖真实信号的极值间距。
|
||||
//
|
||||
// 均值滤波的保护带必须**覆盖滤波半径**:第 k 阶的输出在 i 处依赖第 k-1 阶的
|
||||
// [i-X, i+X] 取样,N 阶总共向外传播 X*N 个采样。否则窗口边界处的取值会与
|
||||
// 「整条曲线」不同(边界上的可用采样数更少)。
|
||||
std::size_t guard = std::clamp((alignedEnd - alignedBegin) / 8, std::size_t(4096),
|
||||
kDerivedCurveGuardMax);
|
||||
if (kind == DerivedCurveKind::MEAN_FILTER)
|
||||
{
|
||||
const std::size_t reach = static_cast<std::size_t>(safePoints + 1) *
|
||||
static_cast<std::size_t>(safeIterations) + 64u;
|
||||
guard = std::clamp(std::max(guard, reach), std::size_t(4096), kDerivedCurveGuardMax);
|
||||
}
|
||||
const std::size_t computedBegin = (alignedBegin > guard) ? (alignedBegin - guard) : 0;
|
||||
const std::size_t computedEnd = std::min(count, alignedEnd + guard);
|
||||
|
||||
// 只把这个窗口拷进自由函数(它们按整段向量工作)。
|
||||
const std::vector<double>& values = signal.values;
|
||||
const std::vector<double> window(values.begin() + static_cast<std::ptrdiff_t>(computedBegin),
|
||||
values.begin() + static_cast<std::ptrdiff_t>(computedEnd));
|
||||
|
||||
// ⚠️ 超大窗口(例如「中心轴边沿计数」的整条信号统计)不进缓存:
|
||||
// 进缓存迟早会被淘汰,淘汰后下一帧就要重算整条曲线 —— 实测单次 ≈1s 的卡顿尖峰,
|
||||
// 而且多个信号同时驻留会占掉数百 MB。改为写入一次性缓冲。
|
||||
if ((computedEnd - computedBegin) > kCurveSpanCacheMaxSamples)
|
||||
{
|
||||
m_curveSpanScratch.clear();
|
||||
if (kind == DerivedCurveKind::WEAK_BOOST)
|
||||
{
|
||||
ComputeWeakWaveBoost(window, static_cast<double>(safeLevel) / 100.0, safeIterations,
|
||||
m_curveSpanScratch);
|
||||
}
|
||||
else
|
||||
{
|
||||
ComputeMovingAverageIterated(window, safePoints, safeIterations, m_curveSpanScratch);
|
||||
}
|
||||
spanBegin = computedBegin;
|
||||
return &m_curveSpanScratch;
|
||||
}
|
||||
|
||||
// 缓存窗口:条目数与总字节数双重约束,避免大窗口把内存吃满。
|
||||
// 淘汰策略为「最旧优先」(`deque` 头部即最早写入的)。
|
||||
const auto cacheBytes = [this]() -> std::size_t
|
||||
// 2) 条目数与总字节数双重约束(最旧优先淘汰)。
|
||||
const auto bytes = [this]() -> std::size_t
|
||||
{
|
||||
std::size_t total = 0;
|
||||
for (const CurveSpanEntry& cached : m_curveSpanCache)
|
||||
for (const CurveEntryPtr& entry : m_curveEntries)
|
||||
{
|
||||
total += cached.values.size() * sizeof(float);
|
||||
total += entry->values.size() * sizeof(float);
|
||||
}
|
||||
return total;
|
||||
};
|
||||
while (!m_curveSpanCache.empty() &&
|
||||
(m_curveSpanCache.size() >= kCurveSpanCacheLimit ||
|
||||
cacheBytes() + (computedEnd - computedBegin) * sizeof(float) >
|
||||
kCurveSpanCacheMaxBytes))
|
||||
while (!m_curveEntries.empty() &&
|
||||
(m_curveEntries.size() > kCurveSpanCacheLimit || bytes() > kCurveSpanCacheMaxBytes))
|
||||
{
|
||||
m_curveSpanCache.pop_front();
|
||||
m_curveEntries.pop_front();
|
||||
}
|
||||
m_curveSpanCache.push_back(CurveSpanEntry());
|
||||
CurveSpanEntry& entry = m_curveSpanCache.back();
|
||||
entry.signal = &signal;
|
||||
entry.kind = kindValue;
|
||||
entry.points = safePoints;
|
||||
entry.iterations = safeIterations;
|
||||
entry.levelPercent = safeLevel;
|
||||
entry.begin = computedBegin;
|
||||
entry.end = computedEnd;
|
||||
|
||||
if (kind == DerivedCurveKind::WEAK_BOOST)
|
||||
{
|
||||
ComputeWeakWaveBoost(window, static_cast<double>(safeLevel) / 100.0, safeIterations,
|
||||
entry.values);
|
||||
}
|
||||
else
|
||||
{
|
||||
ComputeMovingAverageIterated(window, safePoints, safeIterations, entry.values);
|
||||
}
|
||||
spanBegin = entry.begin;
|
||||
return &m_curveSpanCache.back().values;
|
||||
}
|
||||
|
||||
const std::vector<float>& PlotView::MeanFilterCurveFor(const SignalData& signal, int points,
|
||||
@@ -5250,6 +5441,19 @@ namespace jdi
|
||||
|
||||
BuildLaneVisuals();
|
||||
|
||||
// 双缓冲:清掉上一帧钉住的曲线条目(本帧会按需重新钉住),并清理已被更新条目覆盖 /
|
||||
// 超出内存预算的旧条目。此刻没有任何条目被钉住,回收是安全的。
|
||||
m_curvePins.clear();
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(m_curveMutex);
|
||||
PruneCurveEntriesLocked();
|
||||
}
|
||||
// 后台算好了新曲线:静态场景缓存里仍是旧曲线(或还没有曲线),作废一次以纳入最新结果。
|
||||
if (m_curveSceneDirty.exchange(false, std::memory_order_relaxed))
|
||||
{
|
||||
InvalidateSceneCache();
|
||||
}
|
||||
|
||||
if (m_lanes.empty())
|
||||
{
|
||||
InvalidateSceneCache();
|
||||
|
||||
Reference in new issue
Block a user