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:
Martino FerrariandClaude Opus 4.6 committed 2026-10-15 16:30:48 +02:00
1 parent 074cb611bc
commit d91957a2fe
15 files changed
+374 -45

No files matched your search

+25 -3
View File
@@ -284,8 +284,19 @@ void update_measures(App& app, bool force, bool accumulate) {
const double s0 = app.model->stream_at_x0();
const plot::Range view = r0.plot->x_range(r0.axis_x);
const plot::Range swin{view.min + s0, view.max + s0};
app.measure_view_window = swin;
const bool held = app.model->display_mode() == DisplayMode::Held;
const bool moved = app.measure_window_valid && !held &&
// "It moved" is asked in **every** mode, a held event included. This test used to carry a
// `&& !held`, and a held event is precisely the mode where a *gesture* is the only thing that
// can move the window: with it, zooming a latched event forty times over left the pane
// describing the whole 2 s event (`for=[0.47..2.47]` against a `swin=[1.417..1.467]`, `passes`
// frozen), which is what "the measurements move when zooming or panning" was -- the numbers and
// the time marks belonged to a window the plot had left. The term was aimed at a *pinned*
// window recomputing every frame, and nothing is needed for that: a settled window satisfies
// `swin == measure_window` exactly and so reads `moved == false` on its own. A pause is the
// standing proof, since `display_mode()` reports `Paused` rather than `Held` for one and the
// statistics probe already requires `n` to advance by at most one across a two-second pause.
const bool moved = app.measure_window_valid &&
std::fabs(swin.min - app.measure_window.min) +
std::fabs(swin.max - app.measure_window.max) >
0.02 * std::max(1.0e-12, swin.span());
@@ -638,8 +649,19 @@ gboolean measure_trace(gpointer user) {
RowSet::Row& r0 = a->rows->row(0);
if (r0.plot == nullptr) return G_SOURCE_REMOVE;
const plot::Range view = r0.plot->x_range(r0.axis_x);
std::string line = "[measure-trace] view=[" + std::to_string(view.min) + ".." +
std::to_string(view.max) + "]";
// The window on screen *and* the window the numbers describe, both on the stream's clock, with
// the offset that converts between them: a reading that moved without the view moving is a
// change of `s0` (a new snapshot under the same mode), and only these three fields tell the two
// apart. `passes` beside them says whether a recompute answered it.
const double s0 = a->model->stream_at_x0();
std::string line = "[measure-trace] mode=" +
std::string(display_mode_name(a->model->display_mode())) + " view=[" +
std::to_string(view.min) + ".." + std::to_string(view.max) + "] s0=" +
std::to_string(s0) + " swin=[" + std::to_string(view.min + s0) + ".." +
std::to_string(view.max + s0) + "] for=[" +
std::to_string(a->measure_window.min) + ".." +
std::to_string(a->measure_window.max) + "] passes=" +
std::to_string(a->measure_passes);
for (std::size_t i = 0; i < a->measures.size() && i < a->measure_results.size(); ++i) {
const MeasureResult& r = a->measure_results[i];
line += " " + std::string(measure_type_name(a->measures[i].type)) + "=" +