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:
DeepSeek-V4.1-Flash committed 2026-11-04 02:02:03 +08:00
1 parent 8b9c1e3de3
commit 266ab657be
9 files changed
+725 -259

No files matched your search

+12 -69
View File
@@ -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;