fix(perf): 公式映射导致确认/适应视图/拖拽排序/组复选框四处卡死根治
四次卡顿是**同一个根因**:公式信号(不保序)的幅值范围与包络带各写了一份 「逐点扫描整条信号」,且都没有缓存。2580 万采样时实测 1589ms + 1481ms, 而幅值范围指纹包含公式文本 / 信号顺序 / 可见性 / lane.autoRange ⇒ 确认编辑、适应视图、拖拽排序、组复选框每一个都会让它失效、每次都重付 3.1 秒。 修法: - 新增 DataSet::FormulaTransformCache / FormulaTransformFor:一次扫描同时算出 绘制值域与逐分组包络,缓存到数据层(std::deque,指针稳定)。 - TabConfig::ComputeLaneRange 的公式分支改为查该缓存(保留非法公式的逐点回退)。 - PlotView::AcquireFormulaEnvelope 改为直接复用数据层同一条目, 删除 PlotView 自有的 FormulaEnvelopeEntry / m_formulaEnvelopes 与两个重复常量。 - FormulaEval 新增 FormulaProgram(编译一次、批量求值):always_inline、 常量折叠(2/1.25 => 1.6)、幂次改写(x^2 => x*x、x^3、x^0.5 => sqrt)、 按分组两级循环(去掉每点一次整数除法)。 - EvaluateFormula 复用 thread_local FormulaProgram(按内容缓存),两条路径语义一致。 实测(2580 万采样,纯控制台探针):确认公式 3070ms => 379ms;改 x^2 1.9s => 0.19s; 适应视图 / 拖拽排序 / 组复选框 1589ms => 0.0015ms(命中缓存)。 回归:缓存命中/失效/值域、14 组公式语义与逐位一致性、非法/空公式回退全部 PASS; release/debug 零警告;Release TargetCheck.exe 890,880 B(< 1MB)。 未做:跨组移动共享时间轴仍深拷贝 197MB(55-66ms,值语义决定,风险大于收益); 首次 379ms 仍同步阻塞(后台化的复杂度暂不划算)。
This commit is contained in:
1 parent
8b9c1e3de3
commit
266ab657be
9 files changed
+725
-259
No files matched your search
@@ -1842,44 +1842,25 @@ namespace jdi
|
||||
bool& bandMode, bool& bandSpiky) const;
|
||||
|
||||
/**
|
||||
* @brief 「公式映射」信号的逐分组包络缓存条目(见 `AcquireFormulaEnvelope`)。
|
||||
* @brief 取得「公式映射」信号的逐分组包络(**数据层共享缓存**)。
|
||||
*
|
||||
* 键 = 信号 + 采样数组 + 采样数 + 公式文本 + 影响数值的样式字段。
|
||||
* `minimums` / `maximums` 存的是**已经套用完整绘制变换**(公式 → 系数 → 绝对值 →
|
||||
* 镜像 → Y 偏移)之后、每 `stride` 个采样一个分组的极值(无有效值的分组为 NaN)。
|
||||
*/
|
||||
struct FormulaEnvelopeEntry
|
||||
{
|
||||
const void* signal = nullptr; /**< `resolved.data`(区分不同信号)。 */
|
||||
const void* values = nullptr; /**< `values.data()`(区分重新加载的数据)。 */
|
||||
std::size_t count = 0; /**< 参与统计的采样数。 */
|
||||
std::size_t stride = 0; /**< 每个分组覆盖的采样数。 */
|
||||
std::wstring formula; /**< 公式文本(空 = 不适用)。 */
|
||||
float gain = 1.0f;
|
||||
bool absolute = false;
|
||||
bool mirrorVertical = false;
|
||||
float offsetY = 0.0f;
|
||||
std::vector<double> minimums; /**< 每组的绘制值下限。 */
|
||||
std::vector<double> maximums; /**< 每组的绘制值上限。 */
|
||||
bool valid = false; /**< 是否已成功建立。 */
|
||||
};
|
||||
|
||||
/**
|
||||
* @brief 取得(必要时建立)「公式映射」信号的逐分组包络。
|
||||
* ⚠️⚠️ 缓存实体是 `DataSet::FormulaTransformCache`(见 `DataSet::FormulaTransformFor`):
|
||||
* 它与幅值自适应(`TabConfig::ComputeLaneRange`)**共用同一次全量扫描**。
|
||||
* `SignalData::coarseMin/coarseMax` 只存原始采样的分组极值,只有在数值变换
|
||||
* **保序**时才能直接缩放使用(见 `DrawTransformOrderPreserving`);公式
|
||||
* (如 `f(t)^2`、`1/f(t)`)不保序,必须用变换后的分组极值。
|
||||
*
|
||||
* ⚠️⚠️ **为什么必须单独建一层**:`SignalData::coarseMin/coarseMax` 只存**原始采样**的
|
||||
* 分组极值,只有在数值变换**保序**时才能直接缩放使用(见 `DrawTransformOrderPreserving`)。
|
||||
* 公式(如 `f(t)^2`、`1/f(t)`)不保序,旧实现在这种情况会退化成**逐点扫描整条信号**:
|
||||
* `VERY_BIG_DATA` 单条 2580 万采样时,一次折线构建 ≈ 850ms(缩放一帧就卡一次)。
|
||||
* 这里把“变换后的分组极值”算**一次**缓存起来,之后每帧/每次缩放只按分组聚合
|
||||
* (几十微秒),且极值仍是逐点统计出来的 ⇒ **不会漏掉尖峰**。
|
||||
* 旧实现自查自建了一份包络缓存,并与幅值自适应各自全扫一遍 ⇒ 2580 万采样时
|
||||
* 「确认公式 / 适应视图 / 拖拽排序 / 勾选组」全部卡 1.6 s + 1.5 s
|
||||
*(用户 2026-11-04 报告)。现在两条路径共享一次扫描的结果。
|
||||
*
|
||||
* 只在公式信号上使用;无公式时返回 nullptr(继续走既有的 `coarseMin/Max` 快路径)。
|
||||
* 只在公式信号上使用;无公式 / 公式非法时返回 nullptr(继续走 `coarseMin/Max` 快路径)。
|
||||
*
|
||||
* @param [IN] resolved 已解析的信号视图。
|
||||
* @return 缓存条目指针(内容有效);不适用或无法建立时返回 nullptr。
|
||||
*/
|
||||
const FormulaEnvelopeEntry* AcquireFormulaEnvelope(const ResolvedSignal& resolved) const;
|
||||
const DataSet::FormulaTransformCache* AcquireFormulaEnvelope(
|
||||
const ResolvedSignal& resolved) const;
|
||||
|
||||
|
||||
/**
|
||||
@@ -3639,7 +3620,13 @@ namespace jdi
|
||||
* 只在信号带公式时建立;条目键含公式文本与影响数值的样式字段,
|
||||
* 公式 / 系数 / 镜像等变化时自然失效。用 `deque` 保证追加时已有引用不失效。
|
||||
*/
|
||||
mutable std::deque<FormulaEnvelopeEntry> m_formulaEnvelopes;
|
||||
/**
|
||||
* @brief 「公式映射」信号的绘制值缓存(**唯一实现在数据层**)。
|
||||
*
|
||||
* 旧实现是 `mutable std::deque<FormulaEnvelopeEntry> m_formulaEnvelopes;` —— 与
|
||||
* `TabConfig::ComputeLaneRange` 各扫一遍整条信号。现已合并进
|
||||
* `DataSet::m_formulaTransforms`(见 `DataSet::FormulaTransformFor`)。
|
||||
*/
|
||||
/**
|
||||
* @brief 折线顶点缓存的**容量上限**(条目数)。
|
||||
*
|
||||
|
||||
@@ -622,16 +622,10 @@ namespace jdi
|
||||
*/
|
||||
inline const wchar_t* const kClipNeedDoubleCursorHint = L"启用双光标模式以进行选区截取";
|
||||
|
||||
// ---- 「公式映射」的分组包络缓存(见 `PlotView::AcquireFormulaEnvelope`)----
|
||||
/**
|
||||
* @brief 每个信号的包络分组数上限(分组宽度按采样数反算)。
|
||||
*
|
||||
* 65536 组 × 2(min/max)× 4B ≈ 512KB/信号;对 2580 万采样 ⇒ 每组 ~394 个采样,
|
||||
* 而屏幕上一个像素列覆盖上万采样 ⇒ 分组粒度造成的包络误差远小于 1 像素。
|
||||
*/
|
||||
inline const std::size_t kFormulaEnvelopeBuckets = 65536;
|
||||
/** @brief 包络缓存最多保存的信号数(超出即整表清空,避免无界增长)。 */
|
||||
inline const std::size_t kFormulaEnvelopeCacheLimit = 16;
|
||||
// ---- 「公式映射」的分组包络(缓存实体在数据层:`DataSet::FormulaTransformFor`)----
|
||||
// ⚠️ 分组数(65536)与缓存上限(8 条)现由 `Utilities/Src/DataModel.cpp` 里的
|
||||
// `kFormulaTransformBuckets` / `kFormulaTransformCacheLimit` 控制;这里不再重复定义,
|
||||
// 避免“绘制侧与幅值自适应各扫一遍”(旧实现的两个常量 + 两份缓存就是卡顿根源)。
|
||||
|
||||
|
||||
// ---- 「均值滤波」面板的两行参数滑块(用户 2026-10-30 要求)----
|
||||
|
||||
@@ -210,82 +210,25 @@ namespace jdi
|
||||
return &m_polylineCache.back();
|
||||
}
|
||||
|
||||
const PlotView::FormulaEnvelopeEntry* PlotView::AcquireFormulaEnvelope(
|
||||
const DataSet::FormulaTransformCache* PlotView::AcquireFormulaEnvelope(
|
||||
const ResolvedSignal& resolved) const
|
||||
{
|
||||
if (resolved.data == nullptr || resolved.style == nullptr || !resolved.style->HasFormula())
|
||||
if (resolved.data == nullptr || resolved.style == nullptr || m_dataSet == nullptr ||
|
||||
!resolved.style->HasFormula())
|
||||
{
|
||||
return nullptr;
|
||||
}
|
||||
const std::vector<double>& values = resolved.data->values;
|
||||
const std::size_t count = values.size();
|
||||
if (count == 0)
|
||||
// ⚠️⚠️ 缓存**只有一份**,在数据层(`DataSet::FormulaTransformFor`):
|
||||
// 它与 `TabConfig::ComputeLaneRange` 共用同一次全量扫描的结果。
|
||||
// 旧实现在这里又建了一份包络缓存、并且与幅值自适应各自全扫一遍
|
||||
// ⇒ 2580 万采样时「确认公式」要卡 1.6 s + 1.5 s(用户 2026-11-04 报告)。
|
||||
const DataSet::FormulaTransformCache* transform =
|
||||
m_dataSet->FormulaTransformFor(*resolved.data, *resolved.style);
|
||||
if (transform == nullptr || !transform->ready || transform->stride == 0)
|
||||
{
|
||||
return nullptr;
|
||||
}
|
||||
const SignalStyle& style = *resolved.style;
|
||||
const void* signalKey = static_cast<const void*>(resolved.data);
|
||||
const void* valuesKey = static_cast<const void*>(values.data());
|
||||
|
||||
// 命中缓存:公式与全部影响数值的样式字段都一致时可直接复用。
|
||||
for (const FormulaEnvelopeEntry& entry : m_formulaEnvelopes)
|
||||
{
|
||||
if (entry.valid && entry.signal == signalKey && entry.values == valuesKey &&
|
||||
entry.count == count && entry.formula == style.formula && entry.gain == style.gain &&
|
||||
entry.absolute == style.absolute && entry.mirrorVertical == style.mirrorVertical &&
|
||||
entry.offsetY == style.offsetY)
|
||||
{
|
||||
return &entry;
|
||||
}
|
||||
}
|
||||
|
||||
// 未命中:建立新条目(容量上限很小:只有“带公式”的信号才会进来)。
|
||||
if (m_formulaEnvelopes.size() >= kFormulaEnvelopeCacheLimit)
|
||||
{
|
||||
m_formulaEnvelopes.clear();
|
||||
}
|
||||
m_formulaEnvelopes.emplace_back();
|
||||
FormulaEnvelopeEntry& entry = m_formulaEnvelopes.back();
|
||||
entry.signal = signalKey;
|
||||
entry.values = valuesKey;
|
||||
entry.count = count;
|
||||
entry.formula = style.formula;
|
||||
entry.gain = style.gain;
|
||||
entry.absolute = style.absolute;
|
||||
entry.mirrorVertical = style.mirrorVertical;
|
||||
entry.offsetY = style.offsetY;
|
||||
// 分组宽度:让分组数落在 `kFormulaEnvelopeBuckets` 量级(内存 = 2 × 分组数 × 4B)。
|
||||
entry.stride = std::max<std::size_t>(
|
||||
1, (count + kFormulaEnvelopeBuckets - 1) / kFormulaEnvelopeBuckets);
|
||||
const std::size_t buckets = (count + entry.stride - 1) / entry.stride;
|
||||
entry.minimums.assign(buckets, std::numeric_limits<double>::quiet_NaN());
|
||||
entry.maximums.assign(buckets, std::numeric_limits<double>::quiet_NaN());
|
||||
|
||||
// ⚠️ **一次**全量扫描:逐点套用完整绘制变换后按分组取极值。
|
||||
// 之后每帧 / 每次缩放都只按分组聚合,代价与采样数无关。
|
||||
for (std::size_t index = 0; index < count; ++index)
|
||||
{
|
||||
const double mapped = ApplyDrawTransform(style, values[index]);
|
||||
if (!std::isfinite(mapped))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
const std::size_t bucket = index / entry.stride;
|
||||
const double value = mapped;
|
||||
double& low = entry.minimums[bucket];
|
||||
double& high = entry.maximums[bucket];
|
||||
if (!(low <= high))
|
||||
{
|
||||
// 该分组第一次写入(初值是 NaN)。
|
||||
low = value;
|
||||
high = value;
|
||||
continue;
|
||||
}
|
||||
low = std::min(low, value);
|
||||
high = std::max(high, value);
|
||||
}
|
||||
entry.valid = true;
|
||||
return &entry;
|
||||
return transform;
|
||||
}
|
||||
|
||||
void PlotView::BuildPolyline(const ResolvedSignal& resolved, const LaneVisual& lane,
|
||||
@@ -454,7 +397,7 @@ namespace jdi
|
||||
// 缩放 / 平移每重建一帧就卡一次(用户 2026-11-03 报告)。
|
||||
// 这里改用**变换后的分组包络**(按需算一次并缓存):极值仍是逐点统计的
|
||||
// ⇒ 不会漏尖峰,而每帧只按分组聚合。
|
||||
if (const FormulaEnvelopeEntry* envelope = AcquireFormulaEnvelope(resolved))
|
||||
if (const DataSet::FormulaTransformCache* envelope = AcquireFormulaEnvelope(resolved))
|
||||
{
|
||||
coarseMinPtr = &envelope->minimums;
|
||||
coarseMaxPtr = &envelope->maximums;
|
||||
|
||||
Reference in new issue
Block a user