perf: the measurements pane keys its shared walks by channel and HF corner
The pane shared its two walks per *channel*, but the HF corner is a per-measurement
setting: two measurements on one signal at different corners read two different signals
and cannot share a walk. `App::MeasureWindow` therefore carries `channel` and `fc`, the
container is a vector, and the grouping loop matches on both -- keeping its dedupe of the
level accumulators by `(pct, noise)`, since a type that does not read the band must not
split an accumulator over it.
Each window carries two filters, one per walk: they are separate walks, so separate state.
A filtered window also stops using the LOD pyramid, which one `if` does -- leaving
`buckets` empty makes `bulk_from == bulk_to == n`, so the raw walk covers the window and
`lo_search`/`hi_search` stay 0, which makes the pinning pass a no-op. Nothing downstream
needs to know.
The readout loop looks its window up by index rather than by channel, which is what the
`(channel, fc)` key needs.
The documented measurement baseline is unchanged, and it is the assertion:
[measure-trace] average=0.000000 top=0.636620 peak to peak=2.000000 amplitude=1.000000
[udpscope] measure cost: ... covered=75378013 buckets=1177164 raw=159674 (0.2%)
`top` is 2/pi to the digit -- the one reading a band leaking into the sums would move --
and `raw` at 0.2 % of `covered` says the fast path is still being used on an unfiltered
window.
The measure probe now enables `sine` itself, as the noise probe does: `enable_all_probe`
ends by switching a signal off, and with `sine` gone the probe measured `square`, where
`top` is 1.0 and the baseline above is unreachable.
This commit is contained in:
1 parent
2730707372
commit
380ddb9a77
3 files changed
+79
-17
No files matched your search
+57
-15
@@ -261,6 +261,13 @@ void update_measures(App& app, bool force, bool accumulate) {
|
||||
app.measure_stamp_valid = true;
|
||||
const gint64 pass_start_us = g_get_monotonic_time();
|
||||
app.measure_samples.clear();
|
||||
// Before any `MeasureWindowReader` exists: the reader holds `model->mutex()` for a live window, and asking
|
||||
// the model for anything while one is alive would deadlock.
|
||||
double slot_dt = 0.0;
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(app.model->mutex());
|
||||
slot_dt = app.model->partition().slot_dt();
|
||||
}
|
||||
|
||||
app.measure_results.assign(app.measures.size(), MeasureResult{});
|
||||
app.measure_last_valid.resize(app.measures.size());
|
||||
@@ -284,18 +291,42 @@ void update_measures(App& app, bool force, bool accumulate) {
|
||||
// trace costs the same two walks as a single one. The common case is exactly one level: every
|
||||
// crossing type reads the mid level and `Top`/`Bottom` default to it.
|
||||
std::vector<int> chan(app.measures.size(), -1);
|
||||
std::vector<int> want_window(app.measures.size(), -1);
|
||||
std::vector<int> want_level(app.measures.size(), -1);
|
||||
for (std::size_t i = 0; i < app.measures.size(); ++i) {
|
||||
const Measure& m = app.measures[i];
|
||||
const int channel = measure_channel(app, m.signal);
|
||||
chan[i] = channel;
|
||||
if (channel < 0) continue; // unresolvable this pass: a rebuild has emptied the channel list
|
||||
App::MeasureWindow& w = app.measure_samples[channel];
|
||||
const double fc = m.options.hf_fc > 0.0 ? m.options.hf_fc : 0.0;
|
||||
std::size_t wi = 0;
|
||||
while (wi < app.measure_samples.size() &&
|
||||
(app.measure_samples[wi].channel != channel || app.measure_samples[wi].fc != fc)) {
|
||||
++wi;
|
||||
}
|
||||
if (wi == app.measure_samples.size()) {
|
||||
App::MeasureWindow fresh;
|
||||
fresh.channel = channel;
|
||||
fresh.fc = fc;
|
||||
measure_filter_set(fresh.scan_filter, fc, slot_dt);
|
||||
measure_filter_set(fresh.acc_filter, fc, slot_dt);
|
||||
app.measure_samples.push_back(fresh);
|
||||
}
|
||||
want_window[i] = static_cast<int>(wi);
|
||||
App::MeasureWindow& w = app.measure_samples[wi];
|
||||
double pct = 0.0;
|
||||
if (!measure_level_pct(m.type, m.options, pct)) continue;
|
||||
// A type that does not read the band must not split the accumulator over it: its sums split at the bare
|
||||
// level, so a `Top` is the same walk whatever the band is. Grouping those at the default is what keeps
|
||||
// the common pane at exactly one level.
|
||||
const double noise =
|
||||
measure_uses_noise(m.type) ? m.options.noise_pct : MeasureOptions{}.noise_pct;
|
||||
std::size_t k = 0;
|
||||
while (k < w.pct.size() && w.pct[k] != pct) ++k;
|
||||
if (k == w.pct.size()) w.pct.push_back(pct);
|
||||
while (k < w.pct.size() && (w.pct[k] != pct || w.noise[k] != noise)) ++k;
|
||||
if (k == w.pct.size()) {
|
||||
w.pct.push_back(pct);
|
||||
w.noise.push_back(noise);
|
||||
}
|
||||
want_level[i] = static_cast<int>(k);
|
||||
}
|
||||
|
||||
@@ -313,10 +344,9 @@ void update_measures(App& app, bool force, bool accumulate) {
|
||||
// degrades back to the O(window) cost and nothing else changes.
|
||||
constexpr std::size_t kChunk = 8192;
|
||||
std::vector<MeasureBucket>& buckets = app.measure_bucket_scratch;
|
||||
for (auto& entry : app.measure_samples) {
|
||||
App::MeasureWindow& w = entry.second;
|
||||
for (App::MeasureWindow& w : app.measure_samples) {
|
||||
const gint64 fetch_start_us = g_get_monotonic_time();
|
||||
MeasureWindowReader reader(app, static_cast<std::size_t>(entry.first));
|
||||
MeasureWindowReader reader(app, static_cast<std::size_t>(w.channel));
|
||||
const std::size_t n = reader.size();
|
||||
const std::function<double(std::size_t)> time_of = [&reader](std::size_t i) {
|
||||
return reader.time_of(i);
|
||||
@@ -325,7 +355,12 @@ void update_measures(App& app, bool force, bool accumulate) {
|
||||
u64 kb = 0;
|
||||
u64 ke = 0;
|
||||
buckets.clear();
|
||||
if (reader.bucket_range(kb, ke)) {
|
||||
// A filtered window cannot use the pyramid: a bucket aggregates *unfiltered* samples, so its extremes
|
||||
// and its sum describe a different signal. Leaving `buckets` empty is the whole disable -- `bulk_from ==
|
||||
// bulk_to == n` makes the raw walk cover the window, and `lo_search`/`hi_search` stay 0, which makes
|
||||
// `pin()` below a no-op. One `if` turns off both fast paths, and nothing downstream needs to know.
|
||||
const bool filtered = w.fc > 0.0;
|
||||
if (!filtered && reader.bucket_range(kb, ke)) {
|
||||
buckets.reserve(static_cast<std::size_t>(ke - kb));
|
||||
for (u64 k = kb; k < ke; ++k) buckets.push_back(reader.bucket(k));
|
||||
}
|
||||
@@ -342,7 +377,7 @@ void update_measures(App& app, bool force, bool accumulate) {
|
||||
}
|
||||
};
|
||||
|
||||
const auto scan = [&](const MeasureChunk& c) { measure_scan(c, w.stats); };
|
||||
const auto scan = [&](const MeasureChunk& c) { measure_scan(c, w.stats, w.scan_filter); };
|
||||
raw(0, bulk_from, scan);
|
||||
if (!buckets.empty()) measure_scan_buckets(buckets.data(), buckets.size(), w.stats);
|
||||
raw(bulk_to, n, scan);
|
||||
@@ -351,8 +386,11 @@ void update_measures(App& app, bool force, bool accumulate) {
|
||||
if (w.stats.valid) {
|
||||
const auto pin = [&](std::size_t& index, std::size_t& search, bool want_hi) {
|
||||
if (search == 0) return;
|
||||
// Only the bucket path leaves `search` non-zero, and a filtered window has no buckets, so this is
|
||||
// reached pass-through only: a fresh filter here is the *same* filter.
|
||||
MeasureFilter unfiltered;
|
||||
MeasureStats one;
|
||||
measure_scan(MeasureChunk{reader.values(index, search), search, index}, one);
|
||||
measure_scan(MeasureChunk{reader.values(index, search), search, index}, one, unfiltered);
|
||||
if (one.valid) index = want_hi ? one.hi_i : one.lo_i;
|
||||
raw_read += search;
|
||||
search = 0;
|
||||
@@ -372,10 +410,10 @@ void update_measures(App& app, bool force, bool accumulate) {
|
||||
// The level *and* its hysteresis band, in the engine: how wide the band is is a property of
|
||||
// how an edge is counted, not of the pane, and two expressions for it is how the walk and
|
||||
// the bucket fast path end up disagreeing about where a crossing was.
|
||||
measure_set_level(w.level[k], w.stats, w.pct[k]);
|
||||
measure_set_level(w.level[k], w.stats, w.pct[k], 0.01 * w.noise[k]);
|
||||
}
|
||||
const auto accumulate = [&](const MeasureChunk& c) {
|
||||
measure_accumulate(c, time_of, w.level.data(), w.level.size());
|
||||
measure_accumulate(c, time_of, w.level.data(), w.level.size(), w.acc_filter);
|
||||
};
|
||||
raw(0, bulk_from, accumulate);
|
||||
for (const MeasureBucket& b : buckets) {
|
||||
@@ -388,7 +426,8 @@ void update_measures(App& app, bool force, bool accumulate) {
|
||||
values = reader.values(b.offset, b.span);
|
||||
raw_read += b.span;
|
||||
}
|
||||
measure_accumulate(MeasureChunk{values, b.span, b.offset}, time_of, &level, 1);
|
||||
measure_accumulate(MeasureChunk{values, b.span, b.offset}, time_of, &level, 1,
|
||||
w.acc_filter);
|
||||
}
|
||||
}
|
||||
raw(bulk_to, n, accumulate);
|
||||
@@ -402,8 +441,11 @@ void update_measures(App& app, bool force, bool accumulate) {
|
||||
for (std::size_t i = 0; i < app.measures.size(); ++i) {
|
||||
const Measure& m = app.measures[i];
|
||||
GtkWidget* label = app.measure_labels[i];
|
||||
const auto it = chan[i] < 0 ? app.measure_samples.end() : app.measure_samples.find(chan[i]);
|
||||
if (it == app.measure_samples.end() || !it->second.ok) {
|
||||
const App::MeasureWindow* wp =
|
||||
want_window[i] >= 0 && static_cast<std::size_t>(want_window[i]) < app.measure_samples.size()
|
||||
? &app.measure_samples[static_cast<std::size_t>(want_window[i])]
|
||||
: nullptr;
|
||||
if (wp == nullptr || !wp->ok) {
|
||||
// Its signal is momentarily unresolvable, or the window holds nothing -- so there is nothing
|
||||
// to measure *this* pass. The marks still fall back (below); `continue`ing out of the marks
|
||||
// was the other half of the rulers blinking off.
|
||||
@@ -411,7 +453,7 @@ void update_measures(App& app, bool force, bool accumulate) {
|
||||
if (label != nullptr) gtk_label_set_text(GTK_LABEL(label), "--");
|
||||
continue;
|
||||
}
|
||||
const App::MeasureWindow& w = it->second;
|
||||
const App::MeasureWindow& w = *wp;
|
||||
const MeasureLevel* level =
|
||||
want_level[i] >= 0 && static_cast<std::size_t>(want_level[i]) < w.level.size()
|
||||
? &w.level[static_cast<std::size_t>(want_level[i])]
|
||||
|
||||
Reference in new issue
Block a user