fix: a recompute that renumbered its ring, and readouts frozen under a zoom
Three defects, all of them a window describing data that is not there. A math edit refilled the derived ring with `clear()`, which renumbers it from 0, leaving `produced()` short by the inputs' retained base. Since `live_window_locked()` clamps against every channel's `produced()`, that one lagging channel answered `n = 0` for the live view, every pause view and every trigger capture until a whole ring of fresh data had gone by -- the reported freeze, invisible until the ring first wraps, hence its intermittence. It now restarts with `clear_to(base)`, and `ChannelBuffer::republish_from(base)` puts the published window back: `acquire()` is incremental, so a rewrite in place published nothing and the pyramid went on describing the samples the edit replaced. The trigger's channel is an index, re-resolved by name after a renumbering -- but only in `rebuild_enabled_locked()`, so a math add or delete went through `set_math_channels()` with no restore, the index was clamped to -1, and `process()` returned at once while the panel still named the channel. The restore moves into `rebuild_channels()`, which *is* the renumbering, so no caller can forget it. And `update_measures()` suppressed its "the window moved" test while an event was held -- the one mode where a gesture is the only thing that can move it. Zooming a latched event left the pane describing the whole captured event. A pinned window needs no exemption: it lands on the range it already measured and fails the 2 % test by itself. The checks that missed all three: `UDPSCOPE_PROBE_MATHWRAP_MS` edits a graph whose ring has already wrapped, which `math_probe` structurally cannot reach; `[measure-trace]` now prints the mode, the window on screen and the window the numbers describe; and the dump's `marks_in_window` counts against the window on screen instead of against the one the marks were computed over, where it was a tautology. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
1 parent
074cb611bc
commit
d91957a2fe
15 files changed
+374
-45
No files matched your search
@@ -1170,6 +1170,122 @@ gboolean math_probe(gpointer user) {
|
||||
return G_SOURCE_REMOVE;
|
||||
}
|
||||
|
||||
// The one state `math_probe` structurally cannot reach: an edit of a graph whose ring has already
|
||||
// *wrapped*. Every edit that probe makes follows a structural change, which replans the buffer and
|
||||
// restarts every ring at absolute 0 -- and at `base == 0` the recompute's two renumbering bugs are
|
||||
// both identities. Run it with a small `--buffer-mb` and give it time to saturate; `base=0` in its
|
||||
// second line means the run was too short or the buffer too large to have read anything.
|
||||
int g_mathwrap_index = -1;
|
||||
double g_mathwrap_y_before = 0.0;
|
||||
int g_mathwrap_polls = 0;
|
||||
|
||||
gboolean mathwrap_probe_report(gpointer user) {
|
||||
App* a = static_cast<App*>(user);
|
||||
if (a->window == nullptr || g_mathwrap_index < 0) return G_SOURCE_REMOVE;
|
||||
u64 produced = 0;
|
||||
u64 slots = 0;
|
||||
plot::Range y{0.0, 0.0};
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(a->model->mutex());
|
||||
const std::size_t idx = static_cast<std::size_t>(g_mathwrap_index);
|
||||
if (idx >= a->model->channel_count()) return G_SOURCE_REMOVE;
|
||||
produced = a->model->live().values(idx).produced();
|
||||
slots = a->model->live().time().produced();
|
||||
y = a->model->live().channel(idx).y_bounds();
|
||||
}
|
||||
// Two readings, one per half of the recompute. `produced` against `slots` is the ring's absolute
|
||||
// count: short by `base` is a channel the live window clamps everything else down to, which is the
|
||||
// reported freeze. `y` is the *pyramid's*, so it reads the published window rather than the samples,
|
||||
// and the edit **shrinks** the gain (2 -> 0.5) for that reading to exist at all: a pyramid that was
|
||||
// not republished still describes the replaced +-2 samples, and a window holding both would quote
|
||||
// the larger -- so a growing gain reads correct either way.
|
||||
std::fprintf(stderr,
|
||||
"[mathwrap-probe] after edit: produced=%llu slots=%llu (want equal) y=[%.4f,%.4f] "
|
||||
"(want +-0.5, was +-%.4f)\n",
|
||||
static_cast<unsigned long long>(produced), static_cast<unsigned long long>(slots),
|
||||
y.min, y.max, g_mathwrap_y_before);
|
||||
std::fflush(stderr);
|
||||
return G_SOURCE_REMOVE;
|
||||
}
|
||||
|
||||
gboolean mathwrap_probe_wait(gpointer user) {
|
||||
App* a = static_cast<App*>(user);
|
||||
if (a->window == nullptr || a->math.empty()) return G_SOURCE_REMOVE;
|
||||
u64 base = 0;
|
||||
u64 slots = 0;
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(a->model->mutex());
|
||||
base = a->model->live().time().base();
|
||||
slots = a->model->live().time().produced();
|
||||
const std::size_t idx = static_cast<std::size_t>(g_mathwrap_index);
|
||||
if (g_mathwrap_index >= 0 && idx < a->model->channel_count()) {
|
||||
g_mathwrap_y_before = a->model->live().channel(idx).y_bounds().max;
|
||||
}
|
||||
}
|
||||
// Polled rather than timed: the wrap is a function of the rate and of `--buffer-mb`, and a fixed
|
||||
// delay that fell short would print a passing line having tested nothing.
|
||||
if (base == 0 && ++g_mathwrap_polls < 200) return G_SOURCE_CONTINUE;
|
||||
std::fprintf(stderr, "[mathwrap-probe] wrapped: base=%llu slots=%llu (want base > 0)\n",
|
||||
static_cast<unsigned long long>(base), static_cast<unsigned long long>(slots));
|
||||
std::fflush(stderr);
|
||||
// The edit itself: the *same* channel list with one node's gain changed, which is the in-place
|
||||
// path -- `set_math_channels()` replans only when the names or the enables moved.
|
||||
for (MathNode& n : a->math.front().nodes) {
|
||||
if (n.op == MathOp::Gain) n.gain = 0.5;
|
||||
}
|
||||
apply_math_channels(*a);
|
||||
// After a tick, because the pyramid half of the reading is published by one.
|
||||
g_timeout_add(300, mathwrap_probe_report, a);
|
||||
return G_SOURCE_REMOVE;
|
||||
}
|
||||
|
||||
gboolean mathwrap_probe(gpointer user) {
|
||||
App* a = static_cast<App*>(user);
|
||||
if (a->window == nullptr) return G_SOURCE_REMOVE;
|
||||
// Its own signal and its own graph: a probe must not depend on another's side effects, and this one
|
||||
// needs a source whose extremes are known (a unit sine doubled reads +-2) for the pyramid reading.
|
||||
{
|
||||
const std::vector<SignalInfo>& signals = a->model->signals();
|
||||
for (std::size_t i = 0; i < signals.size(); ++i) {
|
||||
if (signals[i].name == "sine") a->model->set_signal_enabled(i, true);
|
||||
}
|
||||
}
|
||||
std::string name;
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(a->model->mutex());
|
||||
if (a->model->channels().empty()) return G_SOURCE_REMOVE;
|
||||
name = a->model->channels().front().name;
|
||||
}
|
||||
MathChannel gain;
|
||||
gain.name = "math wrap";
|
||||
gain.enabled = true;
|
||||
// Field by field: a partial brace-initialiser would add a fourth build warning, and the baseline
|
||||
// is exactly three.
|
||||
MathNode src;
|
||||
src.op = MathOp::Source;
|
||||
src.source = name;
|
||||
MathNode op;
|
||||
op.op = MathOp::Gain;
|
||||
op.in_a = 0;
|
||||
op.gain = 2.0;
|
||||
gain.nodes = {src, op};
|
||||
gain.output = 1;
|
||||
a->math.assign(1, gain);
|
||||
apply_math_channels(*a);
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(a->model->mutex());
|
||||
for (std::size_t c = 0; c < a->model->channels().size(); ++c) {
|
||||
if (a->model->channels()[c].name == gain.name) g_mathwrap_index = static_cast<int>(c);
|
||||
}
|
||||
}
|
||||
std::fprintf(stderr, "[mathwrap-probe] installed 'math wrap' on '%s': channel=%d\n", name.c_str(),
|
||||
g_mathwrap_index);
|
||||
std::fflush(stderr);
|
||||
if (g_mathwrap_index < 0) return G_SOURCE_REMOVE;
|
||||
g_timeout_add(100, mathwrap_probe_wait, a);
|
||||
return G_SOURCE_REMOVE;
|
||||
}
|
||||
|
||||
gboolean measure_probe(gpointer user) {
|
||||
App* a = static_cast<App*>(user);
|
||||
if (a->window == nullptr) return G_SOURCE_REMOVE;
|
||||
@@ -2768,6 +2884,12 @@ gboolean measures_probe_step(gpointer user) {
|
||||
rebuild_measures_pane(*a);
|
||||
std::fprintf(stderr, "[measures-probe] added %zu measurements on '%s'\n", a->measures.size(),
|
||||
signal.c_str());
|
||||
// This is the only probe set that asks for every type, and so the only one that places a
|
||||
// *time* mark: `pulse duration`, `period` and `frequency` mark the crossings they were read
|
||||
// from, and those are the marks `marks_in_window` counts. Without the trace the run reports
|
||||
// the count with nothing to attribute it to -- which is how a window that lagged the screen
|
||||
// read `0/6` with no way to tell a stale recompute from a mark placed correctly.
|
||||
g_timeout_add(250, measure_trace, a);
|
||||
measures_probe_next(*a, 1, 400);
|
||||
break;
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user