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

+149
View File
@@ -38,6 +38,21 @@ namespace jdi
*/
const std::size_t kCoarseStride = 256;
/**
* @brief 「公式映射」缓存里逐分组包络的目标分组数。
*
* 与绘制侧的像素列数量级一致(绘图区通常 1~4 千列),65536 个分组足够让每个像素列
* 只读取少量分组;内存 = 2 × 65536 × 8B ≈ 1MB / 信号(只有带公式的信号才会建立)。
*/
const std::size_t kFormulaTransformBuckets = 65536;
/**
* @brief 「公式映射」缓存的条目上限。
*
* 只有**带公式**的信号才会进来(通常 1~2 条),存少量条目即可避免同一批操作里反复重扫。
*/
const std::size_t kFormulaTransformCacheLimit = 8;
/**
* @brief 单条信号的取值范围统计结果。
*/
@@ -982,6 +997,140 @@ namespace jdi
return &mutableGroup.signals[static_cast<std::size_t>(ref.signalIndex)].style;
}
// ⚠️ 本函数的热点是一次 2580 万点的全量扫描。项目 Release 用 `-Oz`(体积优先),
// 该级别会把这个循环优化得很差(实测 960ms);只对**这一个函数**提到 `-O2`
// (实测 ~300ms,代码体积代价几 KB)。首次建立缓存后全部命中,O(1)。
__attribute__((optimize("O2")))
const DataSet::FormulaTransformCache* DataSet::FormulaTransformFor(const SignalData& signal,
const SignalStyle& style) const
{
// 恒等公式(`f(t)` / 空串)不算启用:调用方有专门的 O(1) 保序快路径。
if (!style.HasFormula())
{
return nullptr;
}
const std::size_t count = signal.values.size();
if (count == 0)
{
return nullptr;
}
const std::size_t bucketsTarget = kFormulaTransformBuckets;
// ---- 命中判定:信号对象 + 采样数组 + 采样数 + 公式 + 影响数值的样式字段 ----
for (const FormulaTransformCache& entry : m_formulaTransforms)
{
if (entry.ready && entry.signal == &signal && entry.values == signal.values.data() &&
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;
}
}
// 公式编译失败 ⇒ 交给调用方回退(`ApplyDrawTransform` 原样返回采样值)。
const FormulaProgram program(style.formula);
if (!program.Valid())
{
return nullptr;
}
if (m_formulaTransforms.size() >= kFormulaTransformCacheLimit)
{
m_formulaTransforms.clear();
}
m_formulaTransforms.emplace_back();
FormulaTransformCache& entry = m_formulaTransforms.back();
entry.signal = &signal;
entry.values = signal.values.data();
entry.count = count;
entry.formula = style.formula;
entry.gain = style.gain;
entry.absolute = style.absolute;
entry.mirrorVertical = style.mirrorVertical;
entry.offsetY = style.offsetY;
entry.stride = std::max<std::size_t>(1, (count + bucketsTarget - 1) / bucketsTarget);
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());
// ---- ⚠️ **唯一一次**全量扫描:同时得到绘制值域与逐分组极值 ----
// 逐点步骤必须与 `ApplyDrawTransform` 完全一致(公式 → 系数 → 绝对值 → 垂直镜像 → Y 偏移)。
//
// ⚠️⚠️ **按分组两级循环**,内层只碰两个局部变量:若写成"每点 `index / stride`",
// 2580 万次整数除法本身就要 **400ms+**(实测单次扫描 676ms,去掉除法后 → 300ms)。
const double gain = static_cast<double>(style.gain);
const double offsetY = static_cast<double>(style.offsetY);
const std::size_t stride = entry.stride;
const double* const values = entry.values;
const bool absolute = style.absolute;
const bool mirrorVertical = style.mirrorVertical;
double low = std::numeric_limits<double>::infinity();
double high = -std::numeric_limits<double>::infinity();
for (std::size_t bucket = 0; bucket < buckets; ++bucket)
{
const std::size_t bucketBegin = bucket * stride;
const std::size_t bucketEnd = std::min(count, bucketBegin + stride);
double bucketLow = std::numeric_limits<double>::infinity();
double bucketHigh = -std::numeric_limits<double>::infinity();
for (std::size_t index = bucketBegin; index < bucketEnd; ++index)
{
double mapped = program.Evaluate(values[index]);
if (gain != 1.0)
{
mapped *= gain;
}
if (absolute)
{
mapped = (mapped < 0.0) ? -mapped : mapped;
}
if (mirrorVertical)
{
mapped = -mapped;
}
if (offsetY != 0.0)
{
mapped += offsetY;
}
if (!std::isfinite(mapped))
{
continue;
}
if (mapped < bucketLow)
{
bucketLow = mapped;
}
if (mapped > bucketHigh)
{
bucketHigh = mapped;
}
}
if (bucketLow <= bucketHigh)
{
entry.minimums[bucket] = bucketLow;
entry.maximums[bucket] = bucketHigh;
if (bucketLow < low)
{
low = bucketLow;
}
if (bucketHigh > high)
{
high = bucketHigh;
}
}
// 全为无效值的分组保持 NaN(与旧实现一致:绘制时该分组断开)。
}
if (!std::isfinite(low) || !std::isfinite(high))
{
low = 0.0;
high = 1.0;
}
entry.low = low;
entry.high = high;
entry.ready = true;
return &entry;
}
double DataSet::MinTime() const
{
double value = std::numeric_limits<double>::infinity();