fix: a still frame's measurements are the frame's, not the viewport's
A measurement is a statistic of the window the pane is *about*, and on a pinned view -- a held event, a paused stream, an opened file -- that is the frame, not the part of it on screen. The pane followed the viewport instead, so a wheel on a latched event moved `average` and `top`: the reported "when measuring a still frame, moving the zoom makes measurements such as average or top change". Nothing about the data had changed; the user was only looking closer. The window is `RowSet::pinned_range()` while the pin frames the view and the viewport otherwise, and the choice is made in `update_measures()` -- the call site -- rather than inside `MeasureWindowReader`, because it is *booked* as well as read: that window is what the "it moved" test compares against `App::measure_window`, so a gesture inside the pin spends no recompute either, and a reader that chose its own would leave a pinned view recomputing every gesture frame for a number that cannot change. `pinned_range()` is now the one expression of the pin's extent, shared with the framing (`hold_pinned_window()`, `fit_x()`), and the reader is handed the window instead of reading the plot. Live is unchanged: there the window really moves and the statistic follows it, and the accumulator's rule about what resets the history is about neither. `UDPSCOPE_PROBE_MEASUREPIN_MS` is the guard, three legs of before/after a gesture on the row's own axis. On a latched event zoomed to a quarter (`view=[-1..1]` -> `[-0.25..0.25]`, `pin` unchanged): `same=4/4 for_same=1 pass_delta=0`, every value bit-identical -- `average=0.000000 top=0.957975 peak to peak=2.000000 amplitude=1.915950` both sides. The same on a pause taken from it. Since the key `for=` follows `pin=` now, `[measure-trace]` gained the field, so a held run reads `for==pin` with `swin` free to sit inside it. On a live view a zoom to an eighth reads `same=0/4 for_same=0 pass_delta=3 span_after=0.125000 follows=1` with the values moving -- the case the rule must not swallow. Before the fix the same probe reads `same=1/4 for_same=0 pass_delta=1 span_after=0.500000` on the held event (`top` `0.957975` -> `0.957966`) and `same=0/4 span_after=0.125000` on the pause (`top` `0.957944` -> `0.358614`). Live reads the same either way. Also fixed, found by the pause leg: pausing while an event is *held* froze an **empty** picture. `on_pause_toggled()` read `stream_at_x0()` after setting the pause state, and that accessor follows the display -- `paused` with no pause view published yet answers `-display_epoch()`, which on a held event is 0, so the cut searched the ring at edge-relative x, found nothing, and the frame bound an empty snapshot: every measurement `--`, every ruler gone (`ev=650001` on the probe's line now, `ev=0` before). The offset and the window are read first, which is the offset the axis is actually displaying. The live case is unaffected, which is why the pause probe, which pauses a live view, never saw it. Docs corrected where they asserted the old half-truth: the invariants now separate the accumulator's rule (unchanged) from the window a value is computed over (this change), the trigger/pinning file names the pin as load-bearing for both the framing and the numbers and records the pause ordering rule, `verification.md`/`probe-readings.md` describe the new probe and `pin=`, and the README and the manual say what a still frame measures. Checked: `cmake --build build --clean-first -j4` 0 errors and the documented three warnings; `make -C build test-ui` 13 ok; `make -C build test-help` green; an exit-dump run 0 Gtk-WARNING/Gtk-CRITICAL and `gtk warnings: 0`; `[measurepin-probe]`, `[pan-probe]` (`held=1`, `allowed=1`, `limited=1`), `[pause-probe]` (`event_size` non-zero exactly while paused, `axis` inside `frozen_x`, `skips` 0 live / 30 paused), `[stats-probe]` (`dn=1` across the pause), `[zoom-probe]` (`gap_px=0.0` at every step) and `[measure-probe]` + the dump's `measures window:` (all-types run `marks_in_window=4/4`, `pin=off` live) on the fast mock. `measure cost:` is unchanged in kind: `raw=37298 (0.0%)` of `covered=121704537`, 1.04 ms a pass -- the runs that read higher are the probe's own zoomed-to-a-slice windows, where the window is legitimately shorter than a bucket.
This commit is contained in:
1 parent
d91957a2fe
commit
0a209becb1
13 files changed
+719
-114
No files matched your search
+56
-17
@@ -264,10 +264,13 @@ void place_measure_marks(App& app) {
|
||||
app.rows->set_measure_marks(app.measure_marks_display);
|
||||
}
|
||||
|
||||
/// Recomputes every measurement over the visible window and writes the results into the pane,
|
||||
/// collecting the marks the ones asking to be shown need.
|
||||
/// Recomputes every measurement over the window it describes and writes the results into the
|
||||
/// pane, collecting the marks the ones asking to be shown need.
|
||||
///
|
||||
/// A measurement is a statistic of the window, so it is recomputed when the *window* moves --
|
||||
/// **The window is the viewport live and the pin while the view is pinned** (see below): a still
|
||||
/// frame is measured as a frame, so on a held event or a pause a pan or a zoom changes no value.
|
||||
///
|
||||
/// A measurement is a statistic of that window, so it is recomputed when the *window* moves --
|
||||
/// not on a timer. A timer decouples the two: with the window scrolling under a 5 Hz cadence
|
||||
/// the stored marks describe a range the plot has already left, which is how a min/max marker
|
||||
/// ended up outside the viewport, and how a dragged marker survived until the next tick. The
|
||||
@@ -283,19 +286,46 @@ void update_measures(App& app, bool force, bool accumulate) {
|
||||
// `[-window, 0]` that never changes while the data under it does.
|
||||
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;
|
||||
// The window a statistic describes. A **pinned** view -- a held event, a paused view, an opened
|
||||
// file -- is a *still frame*: the data behind it does not change, so a pan or a zoom only looks
|
||||
// closer and no number may move because of one. What the pane describes there is the **pin's own
|
||||
// extent**, which is the same accessor the framing itself uses (`hold_pinned_window()` clamps a
|
||||
// pinned view into exactly this, so the pin *is* what frames it), and it is decided here rather
|
||||
// than inside `MeasureWindowReader` because the decision is booked as well as read: `swin` below
|
||||
// is what the "it moved" test compares against the last result's window, and a reader that chose
|
||||
// its own window would leave a pinned view recomputing on every gesture frame for a number that
|
||||
// cannot change -- the 457-pass run this pane's cadence was fixed for.
|
||||
//
|
||||
// A live view is the other case and is unchanged: there the window genuinely moves and the
|
||||
// statistic follows it, and the accumulator's rule about what resets the history (`measure_*`
|
||||
// stats) is about neither -- a pan or a zoom still changes *which samples* answer a live
|
||||
// measurement, which is what averaging over many windows is for.
|
||||
const plot::Range pin = app.rows->pinned_range();
|
||||
const bool pinned_view = app.rows->window_pinned() && pin.span() > 0.0 &&
|
||||
std::isfinite(pin.min) && std::isfinite(pin.max);
|
||||
const plot::Range window = pinned_view ? pin : view;
|
||||
const plot::Range swin{window.min + s0, window.max + s0};
|
||||
// Both windows of the tick, on the stream's clock: what is **on screen**, and (while pinned)
|
||||
// what the pin *is*. The dump reads them instead of calling `stream_at_x0()` itself, which by
|
||||
// the time it runs answers for a mode no frame was drawn in.
|
||||
app.measure_view_window = plot::Range{view.min + s0, view.max + s0};
|
||||
app.measure_pin_valid = pinned_view;
|
||||
if (pinned_view) app.measure_pin_window = swin;
|
||||
const bool held = app.model->display_mode() == DisplayMode::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.
|
||||
// "It moved" is asked in **every** mode, a held event included -- but it is asked about the
|
||||
// window the measurement *describes* (`swin`, the viewport live and the pin when pinned), never
|
||||
// about the viewport. That distinction is the whole of a reported defect in both directions.
|
||||
// This test used to compare the viewport, with a `&& !held` on it: exempting a held event left
|
||||
// the pane describing the whole 2 s event (`for=[0.47..2.47]`) while a 50 ms slice of it was on
|
||||
// screen, and *removing* the exemption made the opposite mistake -- the numbers followed the
|
||||
// zoom into the slice, so an average or a top moved whenever the user moved the wheel on a
|
||||
// frozen event, which is the report this pins down. Neither is a staleness question: on a
|
||||
// pinned view the pin is what the pane is about, so a gesture cannot move it at all, and the
|
||||
// numbers a gesture would have disturbed are the frame's and stay put. A pinned view still
|
||||
// recomputes when it should -- a fresh latch, a re-stated pin, a new snapshot and the mode
|
||||
// change below all move `swin` -- and a settled one satisfies `swin == measure_window` exactly
|
||||
// and so reads `moved == false` on its own, which is what keeps it from recomputing every
|
||||
// frame (457 passes in one run is the shape that mistake took).
|
||||
const bool moved = app.measure_window_valid &&
|
||||
std::fabs(swin.min - app.measure_window.min) +
|
||||
std::fabs(swin.max - app.measure_window.max) >
|
||||
@@ -445,7 +475,7 @@ void update_measures(App& app, bool force, bool accumulate) {
|
||||
std::vector<MeasureBucket>& buckets = app.measure_bucket_scratch;
|
||||
for (App::MeasureWindow& w : app.measure_samples) {
|
||||
const gint64 fetch_start_us = g_get_monotonic_time();
|
||||
MeasureWindowReader reader(app, static_cast<std::size_t>(w.channel));
|
||||
MeasureWindowReader reader(app, static_cast<std::size_t>(w.channel), window);
|
||||
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);
|
||||
@@ -653,12 +683,21 @@ gboolean measure_trace(gpointer user) {
|
||||
// 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.
|
||||
//
|
||||
// `pin` is the fourth, and it is where a *pinned* view's `for` comes from: while the pin is what
|
||||
// frames the plot the numbers describe it and not `swin`, so a gesture inside the pin leaves
|
||||
// `for`, `passes` and every value exactly where they were. Both are captured on the tick, which
|
||||
// is why `for` and `pin` are readable together; `view`/`s0` are read here, as they always were.
|
||||
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(view.max + s0) + "] pin=[" +
|
||||
(a->measure_pin_valid ? std::to_string(a->measure_pin_window.min) + ".." +
|
||||
std::to_string(a->measure_pin_window.max)
|
||||
: std::string("off")) +
|
||||
"] for=[" +
|
||||
std::to_string(a->measure_window.min) + ".." +
|
||||
std::to_string(a->measure_window.max) + "] passes=" +
|
||||
std::to_string(a->measure_passes);
|
||||
|
||||
Reference in new issue
Block a user