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
+2
-2
@@ -745,7 +745,7 @@ void RowSet::hold_pinned_window() {
|
||||
// and a following group would re-derive the window from a live edge on the next layout --
|
||||
// ignoring the pin altogether.
|
||||
figure_->set_x_link_follow(x_group_, plot::XFollow::Off);
|
||||
const plot::Range pin{-pinned_pre_, pinned_post_};
|
||||
const plot::Range pin = pinned_range();
|
||||
// The *request*: what the user's gesture left on the row's own axis. A member's `set_x_range`
|
||||
// -- what the controller writes -- is not adopted into the group while the group is not
|
||||
// following, so reading the group here only ever saw the previous clamp's answer and the wheel
|
||||
@@ -857,7 +857,7 @@ void RowSet::fit_x() {
|
||||
// *members* have to be written as well as the group, because `hold_pinned_window()` prefers a
|
||||
// member that has moved since our own last statement -- that is how it tells a gesture from its
|
||||
// own echo -- so writing the group alone would be reverted to the zoomed-in member next frame.
|
||||
const plot::Range pin{-pinned_pre_, pinned_post_};
|
||||
const plot::Range pin = pinned_range();
|
||||
if (!(pin.span() > 0.0) || !std::isfinite(pin.min) || !std::isfinite(pin.max)) return;
|
||||
for (int i = 0; i < rows_; ++i) {
|
||||
Row& r = row(i);
|
||||
|
||||
@@ -385,6 +385,12 @@ class RowSet {
|
||||
/// Whether a window is pinned: a held event, a filling capture or a paused view. The condition
|
||||
/// `fit_x()` branches on, which is why the fit probe reports it.
|
||||
bool window_pinned() const noexcept { return pinned_; }
|
||||
|
||||
/// The pin's own extent, in the x the rows' axes read -- what a pinned view is a view *of*.
|
||||
/// The one expression of it, shared by the framing (`hold_pinned_window()`, `fit_x()`) and by
|
||||
/// the measurement pane, which measures *this* while the view is pinned so that a gesture
|
||||
/// changes no number (see `update_measures()`); meaningless when `window_pinned()` is false.
|
||||
plot::Range pinned_range() const noexcept { return {-pinned_pre_, pinned_post_}; }
|
||||
/// True when the pin already *is* this window. A caller that re-states a pin every frame -- the file
|
||||
/// view does, from the ring's own extent -- uses this to update the pin's numbers with
|
||||
/// `set_data_pin()` instead of writing the group's range again with `pin_event_window()`, which would
|
||||
|
||||
+65
-31
@@ -1136,22 +1136,38 @@ void on_window_step(GtkButton* button, gpointer user) {
|
||||
void on_pause_toggled(GtkToggleButton* button, gpointer user) {
|
||||
App* app = static_cast<App*>(user);
|
||||
const bool paused = gtk_toggle_button_get_active(button);
|
||||
// Freeze the *view*: snapshot exactly the window on screen, and pin the axes there. The
|
||||
// acquisition thread keeps writing the live ring behind it, so resuming shows the data that
|
||||
// arrived while paused instead of a hole.
|
||||
//
|
||||
// The window and its offset are read **before** the pause is set, and that order is the whole
|
||||
// of what makes a pause taken while an event is *held* work. `stream_at_x0()` follows the
|
||||
// *display*: the moment `paused` is true with no pause view published yet, it answers
|
||||
// `-display_epoch()`, which on a held event is 0 -- so the cut searched the ring at
|
||||
// edge-relative x, found no samples, and froze an *empty* picture (`ev=0` on the probe's own
|
||||
// line, every measurement `--`). What it has to hand the snapshot is the offset the axis is
|
||||
// reading at the instant of the click, which is exactly what it says below.
|
||||
plot::Range view{0.0, 0.0};
|
||||
double s0 = 0.0;
|
||||
bool have_view = false;
|
||||
if (RowSet::Row& r0 = app->rows->row(0); r0.plot != nullptr) {
|
||||
view = r0.plot->x_range(r0.axis_x);
|
||||
have_view = true;
|
||||
}
|
||||
if (have_view) {
|
||||
// The axis is in display x and the ring is on the stream's clock, so the window has to be
|
||||
// put back on that clock to be cut -- `stream_at_x0()` is the one accessor that is right in
|
||||
// every mode, including a pause taken while an event is *held*, where the axis is
|
||||
// edge-relative and the old `- display_epoch()` form searched the ring with event
|
||||
// coordinates. The same offset is handed to the snapshot as its `x_offset`, so its stamps
|
||||
// come back rebased onto the x the axis is already showing and the frozen picture does not
|
||||
// move at the instant of the pause.
|
||||
s0 = app->model->stream_at_x0();
|
||||
}
|
||||
app->acq->set_paused(paused);
|
||||
app->model->set_paused(paused);
|
||||
if (paused) {
|
||||
// Freeze the *view*: snapshot exactly the window on screen, and pin the axes there. The
|
||||
// acquisition thread keeps writing the live ring behind it, so resuming shows the data that
|
||||
// arrived while paused instead of a hole.
|
||||
if (RowSet::Row& r0 = app->rows->row(0); r0.plot != nullptr) {
|
||||
const plot::Range view = r0.plot->x_range(r0.axis_x);
|
||||
// The axis is in display x and the ring is on the stream's clock, so the window has to be
|
||||
// put back on that clock to be cut -- `stream_at_x0()` is the one accessor that is right in
|
||||
// every mode, including a pause taken while an event is *held*, where the axis is
|
||||
// edge-relative and the old `- display_epoch()` form searched the ring with event
|
||||
// coordinates. The same offset is handed to the snapshot as its `x_offset`, so its stamps
|
||||
// come back rebased onto the x the axis is already showing and the frozen picture does not
|
||||
// move at the instant of the pause.
|
||||
const double s0 = app->model->stream_at_x0();
|
||||
if (have_view) {
|
||||
app->model->publish_pause_view(view.min + s0, view.max + s0, s0);
|
||||
app->pause_started_us = g_get_monotonic_time();
|
||||
// Pin on what the snapshot *got*: the cut is clamped to the ring, so it can be narrower than
|
||||
@@ -5815,6 +5831,9 @@ int run_ui(int argc, char** argv, const Config& cfg, ScopeModel& model, Acquisit
|
||||
if (const char* env = g_getenv("UDPSCOPE_PROBE_MEASURE_MS")) {
|
||||
g_timeout_add(static_cast<guint>(std::atoi(env)), measure_probe, &app);
|
||||
}
|
||||
if (const char* env = g_getenv("UDPSCOPE_PROBE_MEASUREPIN_MS")) {
|
||||
g_timeout_add(static_cast<guint>(std::atoi(env)), measurepin_probe, &app);
|
||||
}
|
||||
if (const char* env = g_getenv("UDPSCOPE_PROBE_EVENTZOOM_MS")) {
|
||||
g_timeout_add(static_cast<guint>(std::atoi(env)), eventzoom_probe, &app);
|
||||
}
|
||||
@@ -6474,24 +6493,34 @@ int run_ui(int argc, char** argv, const Config& cfg, ScopeModel& model, Acquisit
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(model.mutex());
|
||||
// `results_for` is the window the numbers describe, `win` the one on screen now: the
|
||||
// drift between them is the assertion that a measurement is a statistic of the *current*
|
||||
// window -- a timer-decoupled recomputation is what used to leave a marker outside it.
|
||||
// Both on the **stream's** clock, which is the space `measure_window` and the time marks
|
||||
// are stored in: a live axis is a fixed `[-window, 0]`, so comparing display x would read
|
||||
// no drift whatever the data did (see `App::measure_window`).
|
||||
// drift between them is the assertion that a measurement is a statistic of the window it
|
||||
// *should* describe -- a timer-decoupled recomputation is what used to leave a marker
|
||||
// outside it. Both on the **stream's** clock, which is the space `measure_window` and
|
||||
// the time marks are stored in: a live axis is a fixed `[-window, 0]`, so comparing display x
|
||||
// would read no drift whatever the data did (see `App::measure_window`).
|
||||
//
|
||||
// `win` comes from the last **tick** (`App::measure_view_window`) and is not read back here:
|
||||
// this runs from the destroy handler, where the exit probe has already switched the trigger
|
||||
// on, and `stream_at_x0()` then answers for a mode no frame was drawn in.
|
||||
// Which window that should be depends on the view, so `drift` is measured against the
|
||||
// **target**: `win` live, and the **pin** (`pin=`, the same stream seconds) while the view
|
||||
// is pinned. On a pinned view the pane is about the frame, and a zoom inside it is allowed
|
||||
// to leave the screen showing a slice of that frame -- reading that as drift would report
|
||||
// the intended behaviour as the defect it was already reported as once.
|
||||
//
|
||||
// `win` and `pin` come from the last **tick** (`App::measure_view_window`,
|
||||
// `App::measure_pin_window`) and are not read back here: this runs from the destroy
|
||||
// handler, where the exit probe has already switched the trigger on, and `stream_at_x0()`
|
||||
// then answers for a mode no frame was drawn in.
|
||||
const plot::Range win = app.measure_view_window;
|
||||
const double span = app.measure_window.span();
|
||||
const plot::Range target = app.measure_pin_valid ? app.measure_pin_window : win;
|
||||
const double span = target.span();
|
||||
int in_window = 0;
|
||||
int marks = 0;
|
||||
// Against `win`, the window **on screen** -- not against `measure_window`, which is the
|
||||
// window the mark was computed over and so contains it by construction. Read that way the
|
||||
// count was a tautology: it printed `2/2` while the pane's numbers described a window 11 s
|
||||
// and a forty-fold zoom away from the one displayed, which is how a held event that never
|
||||
// recomputed under a gesture stayed invisible to every run of this check.
|
||||
// Against `win`, the window **on screen** -- not against `measure_window`/`target`, which
|
||||
// contain every mark by construction: read that way the count was a tautology, and it
|
||||
// printed `2/2` while the pane's numbers described a window 11 s and a forty-fold zoom
|
||||
// away from the one displayed. A *time* mark may legitimately sit outside `win` on a
|
||||
// pinned view that has been zoomed into the pin -- the mark is a position in the window the
|
||||
// result describes, and that window is the frame, not the slice -- so the strict `K/K`
|
||||
// reading belongs to a live view and to a pinned view left at its pin.
|
||||
for (const RowSet::MeasureMark& mk : app.measure_marks) {
|
||||
if (mk.level) continue;
|
||||
++marks;
|
||||
@@ -6510,11 +6539,16 @@ int run_ui(int argc, char** argv, const Config& cfg, ScopeModel& model, Acquisit
|
||||
if (r.plot->cursor(r.measure_v[mi]).selectable) ++selectable;
|
||||
}
|
||||
}
|
||||
std::printf("[udpscope] measures window: win=[%.6f..%.6f] results_for=[%.6f..%.6f] "
|
||||
const std::string pin_text =
|
||||
app.measure_pin_valid ? std::to_string(app.measure_pin_window.min) + ".." +
|
||||
std::to_string(app.measure_pin_window.max)
|
||||
: std::string("off");
|
||||
std::printf("[udpscope] measures window: win=[%.6f..%.6f] pin=[%s] results_for=[%.6f..%.6f] "
|
||||
"drift=%.4f%% marks_in_window=%d/%d marks_selectable=%d\n",
|
||||
win.min, win.max, app.measure_window.min, app.measure_window.max,
|
||||
span > 0.0 ? 100.0 * (std::fabs(win.min - app.measure_window.min) +
|
||||
std::fabs(win.max - app.measure_window.max)) /
|
||||
win.min, win.max, pin_text.c_str(), app.measure_window.min,
|
||||
app.measure_window.max,
|
||||
span > 0.0 ? 100.0 * (std::fabs(target.min - app.measure_window.min) +
|
||||
std::fabs(target.max - app.measure_window.max)) /
|
||||
span
|
||||
: 0.0,
|
||||
in_window, marks, selectable);
|
||||
|
||||
+29
-5
@@ -484,6 +484,11 @@ struct App {
|
||||
/// `[-window, 0]` whatever arrives, so "the window moved" is invisible in display coordinates
|
||||
/// and the pane would compute once and then never again -- the readout frozen on the samples
|
||||
/// that happened to be on screen when the pane opened.
|
||||
///
|
||||
/// **Which** window that is depends on the view: the viewport while the view is live, and the
|
||||
/// **pin** (`RowSet::pinned_range()`, captured in `measure_pin_window` below) while it is
|
||||
/// pinned. A still frame is measured as a frame, so a gesture inside the pin moves neither this
|
||||
/// nor any number.
|
||||
plot::Range measure_window{};
|
||||
bool measure_window_valid = false;
|
||||
bool measure_was_held = false;
|
||||
@@ -493,7 +498,16 @@ struct App {
|
||||
/// destroy handler, after the exit probe has switched the trigger on, and `stream_at_x0()` by
|
||||
/// then answers for a mode no frame was ever drawn in -- which read as a 45000 % drift on a run
|
||||
/// whose every frame was exact.
|
||||
///
|
||||
/// While the view is pinned this and `measure_window` are *allowed* to differ: the pane
|
||||
/// describes the pin, and a gesture may have narrowed what is on screen to a slice of it.
|
||||
plot::Range measure_view_window{};
|
||||
/// The pin's own extent in the same stream seconds, and whether the view was pinned when the
|
||||
/// tick read it. This is the window a pinned view's results describe, so the dump measures its
|
||||
/// drift against it -- otherwise a zoomed held event would read as a pane describing a window
|
||||
/// the plot has left, which is precisely what it must do there.
|
||||
plot::Range measure_pin_window{};
|
||||
bool measure_pin_valid = false;
|
||||
/// What the two walks over one channel's visible window produced, shared by every measurement
|
||||
/// on that signal: the window's statistics, and one accumulator per *distinct level* any of
|
||||
/// them asked for. Several measurements on one signal must cost two walks, not two each -- and
|
||||
@@ -1076,16 +1090,22 @@ struct ColorPick {
|
||||
};
|
||||
class MeasureWindowReader {
|
||||
public:
|
||||
MeasureWindowReader(App& app, std::size_t channel) {
|
||||
/// `window` is the x range to read, **in the coordinates the axes read** -- the viewport for a
|
||||
/// live view, the pin for a pinned one -- and it is handed in rather than read off the plot
|
||||
/// here, because *which* window a measurement describes is a decision (`update_measures()`)
|
||||
/// that also has to be booked: the pane compares the window it last measured against this one
|
||||
/// to decide whether a recompute is owed, and a reader that chose its own would leave the two
|
||||
/// disagreeing -- a pinned view recomputing on every gesture frame for a number that cannot
|
||||
/// change.
|
||||
MeasureWindowReader(App& app, std::size_t channel, plot::Range window) {
|
||||
const RowSet::Row& r0 = app.rows->row(0);
|
||||
if (r0.plot == nullptr) return;
|
||||
const plot::Range view = r0.plot->x_range(r0.axis_x);
|
||||
if ((snap_ = app.model->display_snapshot()) != nullptr) {
|
||||
if (channel >= snap_->values.size()) return;
|
||||
const std::vector<f64>& st = snap_->stamps;
|
||||
if (snap_->values[channel].size() != st.size()) return;
|
||||
const auto lo = std::lower_bound(st.begin(), st.end(), view.min);
|
||||
const auto hi = std::upper_bound(st.begin(), st.end(), view.max);
|
||||
const auto lo = std::lower_bound(st.begin(), st.end(), window.min);
|
||||
const auto hi = std::upper_bound(st.begin(), st.end(), window.max);
|
||||
if (hi <= lo) return;
|
||||
off_ = static_cast<std::size_t>(lo - st.begin());
|
||||
count_ = static_cast<std::size_t>(hi - lo);
|
||||
@@ -1101,7 +1121,7 @@ class MeasureWindowReader {
|
||||
epoch_ = app.model->display_epoch();
|
||||
u64 first = 0;
|
||||
u64 last = 0;
|
||||
app.model->live_window_locked(view.min - epoch_, view.max - epoch_, first, last);
|
||||
app.model->live_window_locked(window.min - epoch_, window.max - epoch_, first, last);
|
||||
if (last <= first) return;
|
||||
store_ = &app.model->live();
|
||||
channel_ = channel;
|
||||
@@ -1710,6 +1730,10 @@ gboolean mathwrap_probe(gpointer user);
|
||||
gboolean mathwrap_probe_wait(gpointer user);
|
||||
gboolean mathwrap_probe_report(gpointer user);
|
||||
gboolean measure_probe(gpointer user);
|
||||
gboolean measurepin_probe(gpointer user);
|
||||
gboolean measurepin_held_wait(gpointer user);
|
||||
gboolean measurepin_pause_wait(gpointer user);
|
||||
gboolean measurepin_live_wait(gpointer user);
|
||||
gboolean measure_trace(gpointer user);
|
||||
gboolean stats_probe_report(gpointer user);
|
||||
gboolean stats_probe(gpointer user);
|
||||
|
||||
+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);
|
||||
|
||||
@@ -1326,6 +1326,345 @@ gboolean measure_probe(gpointer user) {
|
||||
return G_SOURCE_REMOVE;
|
||||
}
|
||||
|
||||
/// `UDPSCOPE_PROBE_MEASUREPIN_MS`: on a **still frame** the pane describes the frame and not the
|
||||
/// viewport, so a pan or a zoom changes no number -- and on a live view the numbers still follow
|
||||
/// their window.
|
||||
///
|
||||
/// Three legs, each one reading the pane's own results before and after a **gesture**: the range
|
||||
/// is written on the row's own axis, which is what a drag, a wheel or a box zoom writes and what
|
||||
/// `Figure::layout()` adopts. Nothing here takes the window-control path, which *unpins* on the
|
||||
/// way and would be testing something else.
|
||||
///
|
||||
/// * `held`: arms the trigger in single mode, polls for a latched event, reads, zooms to a
|
||||
/// quarter of the event, reads again. **`same=4/4`, `for_same=1` and `pass_delta=0` are the
|
||||
/// assertion**: the frame's samples did not change, so no number may, and the pane is not
|
||||
/// even allowed to spend a recompute on the gesture.
|
||||
/// * `paused`: presses the pause button and does the same, with `for=` at the *snapshot's* own
|
||||
/// extent.
|
||||
/// * `live`: resumes and zooms to a sixteenth of the window. The results must move with it --
|
||||
/// `follows=1`, `for=`'s span narrowed to the axis' -- and the values move with them, which
|
||||
/// is the case the still-frame rule must not swallow.
|
||||
struct MeasurePinProbe {
|
||||
App* app = nullptr;
|
||||
/// Which leg is running: 0 held, 1 paused, 2 live. It only names the `before` line, since each
|
||||
/// step is scheduled from the last.
|
||||
int stage = 0;
|
||||
std::vector<double> before;
|
||||
std::vector<bool> before_valid;
|
||||
plot::Range for_before{};
|
||||
double span_before = 0.0;
|
||||
/// `App::measure_passes` is an `int`, and the delta below is a difference of two of them.
|
||||
int passes_before = 0;
|
||||
/// The settle guard's own state: the pass count the last poll saw, and how many polls in a row
|
||||
/// have been quiet.
|
||||
int passes_seen = -1;
|
||||
int settled = 0;
|
||||
int polls = 0;
|
||||
/// The last state the wait line was printed for: mode, pinned and at-pin in one number.
|
||||
int last_state = -1;
|
||||
};
|
||||
|
||||
/// Whether two windows are the same window **by the app's own criterion** -- the 2 %-of-span
|
||||
/// rule `update_measures()` uses to decide that a recompute is not owed. Equality is not that
|
||||
/// criterion and would never hold: the pin is recomputed from the snapshot's stamps every tick
|
||||
/// while the pane keeps the window its last pass used, and the two differ in the last decimals
|
||||
/// without a recompute being owed for it.
|
||||
bool measurepin_same_window(const plot::Range& a, const plot::Range& b) {
|
||||
const double span = std::max(1.0e-12, std::max(a.span(), b.span()));
|
||||
return std::fabs(a.min - b.min) + std::fabs(a.max - b.max) <= 0.02 * span;
|
||||
}
|
||||
|
||||
/// Whether the pane's results describe the pin.
|
||||
bool measurepin_at_pin(App& a) {
|
||||
return a.measure_pin_valid && measurepin_same_window(a.measure_window, a.measure_pin_window);
|
||||
}
|
||||
|
||||
/// The pane's values, its window and its pass count, as the pane itself holds them.
|
||||
void measurepin_read(App& a, std::vector<double>& values, std::vector<bool>& valid,
|
||||
plot::Range& window, int& passes) {
|
||||
values.clear();
|
||||
valid.clear();
|
||||
for (const MeasureResult& r : a.measure_results) {
|
||||
values.push_back(r.primary);
|
||||
valid.push_back(r.valid);
|
||||
}
|
||||
window = a.measure_window;
|
||||
passes = a.measure_passes;
|
||||
}
|
||||
|
||||
/// One leg's line: the state, the window, every value the pane is showing, and -- the frame the
|
||||
/// measurement is *about* -- the bound event series' own sample count and extent. A pinned view
|
||||
/// with no event bound, or one bound to an empty snapshot, is the defect that reads as `--` in
|
||||
/// every column, and nothing else in the line would say which of the two windows it was.
|
||||
void measurepin_line(App& a, const char* leg, const std::vector<double>& values,
|
||||
const std::vector<bool>& valid) {
|
||||
const RowSet::Row& r0 = a.rows->row(0);
|
||||
const plot::Range view =
|
||||
r0.plot != nullptr ? r0.plot->x_range(r0.axis_x) : plot::Range{0.0, 0.0};
|
||||
// The pin only means something while it is what frames the plot: a *stale* pin (one that a
|
||||
// previous mode left behind) printed as a window would read as the answer.
|
||||
const bool pinned = a.rows->window_pinned();
|
||||
const plot::Range pin = a.rows->pinned_range();
|
||||
const EventSeries* ev = r0.slots[0].bound_event();
|
||||
const plot::Range ev_x = ev != nullptr ? ev->x_bounds() : plot::Range{0.0, 0.0};
|
||||
std::fprintf(stderr,
|
||||
"[measurepin-probe] %-13s pinned=%d mode=%-7s view=[%.6f..%.6f] pin=[%s] "
|
||||
"for=[%.6f..%.6f] passes=%d ev=%zu evx=[%.6f..%.6f]",
|
||||
leg, pinned ? 1 : 0, display_mode_name(a.model->display_mode()),
|
||||
static_cast<double>(view.min), static_cast<double>(view.max),
|
||||
pinned ? (std::to_string(pin.min) + ".." + std::to_string(pin.max)).c_str() : "off",
|
||||
a.measure_window.min, a.measure_window.max, a.measure_passes,
|
||||
ev != nullptr ? ev->size() : 0, ev_x.min, ev_x.max);
|
||||
for (std::size_t i = 0; i < a.measures.size() && i < values.size(); ++i) {
|
||||
std::fprintf(stderr, " %s=", measure_type_name(a.measures[i].type));
|
||||
if (valid[i]) {
|
||||
std::fprintf(stderr, "%.6f", values[i]);
|
||||
} else {
|
||||
std::fprintf(stderr, "--");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// A gesture zoom: `keep` of the span on screen now, about its centre, written where the pointer
|
||||
/// puts it. The next frame's `hold_pinned_window()` adopts it -- a zoom *in* is allowed, which is
|
||||
/// what makes this the case that used to move the numbers.
|
||||
void measurepin_zoom(App& a, double keep) {
|
||||
RowSet::Row& r0 = a.rows->row(0);
|
||||
if (r0.plot == nullptr) return;
|
||||
const plot::Range now = r0.plot->x_range(r0.axis_x);
|
||||
const double centre = 0.5 * (now.min + now.max);
|
||||
const double span = now.span() * keep;
|
||||
r0.plot->set_x_range(r0.axis_x, plot::Range{centre - 0.5 * span, centre + 0.5 * span});
|
||||
}
|
||||
|
||||
/// Reads the leg's second half and compares it with the first. `same` counts the values that are
|
||||
/// bit-identical, which is what "the measurement did not change" means for a statistic over the
|
||||
/// same samples; `for_same` says the window is the same, and `pass_delta` that no recompute was
|
||||
/// spent.
|
||||
void measurepin_compare(MeasurePinProbe& p, const char* leg) {
|
||||
App& a = *p.app;
|
||||
std::vector<double> after;
|
||||
std::vector<bool> after_valid;
|
||||
plot::Range window{};
|
||||
int passes = 0;
|
||||
measurepin_read(a, after, after_valid, window, passes);
|
||||
int same = 0;
|
||||
for (std::size_t i = 0; i < after.size() && i < p.before.size(); ++i) {
|
||||
if (after_valid[i] && p.before_valid[i] && after[i] == p.before[i]) ++same;
|
||||
}
|
||||
measurepin_line(a, leg, after, after_valid);
|
||||
const bool for_same = measurepin_same_window(window, p.for_before);
|
||||
const double swin_span = window.span();
|
||||
std::fprintf(stderr,
|
||||
" same=%d/%zu for_same=%d pass_delta=%d"
|
||||
" span_before=%.6f span_after=%.6f follows=%d\n",
|
||||
same, after.size(), for_same ? 1 : 0, passes - p.passes_before,
|
||||
p.span_before, swin_span,
|
||||
// Live, the results *are* the viewport: the pass that follows a gesture must be
|
||||
// over the narrowed window, which is where "the numbers follow the window" is a
|
||||
// reading rather than a claim. Pinned, `for` must not have moved at all.
|
||||
(for_same == false && swin_span > 0.0 && swin_span <= 0.75 * p.span_before) ? 1 : 0);
|
||||
std::fflush(stderr);
|
||||
}
|
||||
|
||||
/// Starts a leg: what the pane says now, and the gesture that must not move it.
|
||||
void measurepin_begin(MeasurePinProbe& p, double keep) {
|
||||
App& a = *p.app;
|
||||
p.before_valid.clear();
|
||||
measurepin_read(a, p.before, p.before_valid, p.for_before, p.passes_before);
|
||||
p.span_before = p.for_before.span();
|
||||
measurepin_line(a, p.stage == 0 ? "held/before" : (p.stage == 1 ? "paused/before" : "live/before"),
|
||||
p.before, p.before_valid);
|
||||
std::fprintf(stderr, "\n");
|
||||
std::fflush(stderr);
|
||||
measurepin_zoom(a, keep);
|
||||
}
|
||||
|
||||
/// The wait budget every leg shares: every step is scheduled from the last, so a step that is
|
||||
/// never reached has to *say* so -- a probe that quietly waits until the window closes reads as a
|
||||
/// passing run with no lines, which is the worst of both.
|
||||
bool measurepin_polls(MeasurePinProbe& p, const char* what, App& a) {
|
||||
if (++p.polls <= 600) return true;
|
||||
std::fprintf(stderr, "[measurepin-probe] gave up waiting for %s after %d polls (mode=%s pinned=%d)\n",
|
||||
what, p.polls, display_mode_name(a.model->display_mode()),
|
||||
a.rows->window_pinned() ? 1 : 0);
|
||||
std::fflush(stderr);
|
||||
return false;
|
||||
}
|
||||
|
||||
/// The pause leg: presses the button, waits for the frozen frame to settle, then reads and zooms
|
||||
/// it like the held leg.
|
||||
gboolean measurepin_pause_wait(gpointer user);
|
||||
/// The live leg: resumes, waits for the follow to re-frame, then reads and zooms.
|
||||
gboolean measurepin_live_wait(gpointer user);
|
||||
|
||||
/// Whether a **pinned** pane has settled on the pin: the results describe it, and no pass has
|
||||
/// landed since the last poll. Both halves are needed, and the second is what makes the reading
|
||||
/// below about the gesture: a pass landing between the two reads moves a value for a reason that
|
||||
/// is not the zoom.
|
||||
bool measurepin_pinned_settled(MeasurePinProbe& p) {
|
||||
App& a = *p.app;
|
||||
if (!measurepin_at_pin(a) || a.measure_passes != p.passes_seen) {
|
||||
p.passes_seen = a.measure_passes;
|
||||
p.settled = 0;
|
||||
return false;
|
||||
}
|
||||
// Three polls of quiet: one is the pass, the next is the frame it landed in being re-clamped.
|
||||
return ++p.settled >= 3;
|
||||
}
|
||||
|
||||
gboolean measurepin_pause_wait(gpointer user) {
|
||||
MeasurePinProbe& p = *static_cast<MeasurePinProbe*>(user);
|
||||
App& a = *p.app;
|
||||
if (a.window == nullptr || !measurepin_polls(p, "the pause to settle", a)) {
|
||||
delete &p;
|
||||
return G_SOURCE_REMOVE;
|
||||
}
|
||||
if (a.model->display_mode() != DisplayMode::Paused ||
|
||||
!measurepin_pinned_settled(p)) {
|
||||
return G_SOURCE_CONTINUE;
|
||||
}
|
||||
p.stage = 1;
|
||||
measurepin_begin(p, 0.25);
|
||||
g_timeout_add(400, +[](gpointer user) {
|
||||
MeasurePinProbe& probe = *static_cast<MeasurePinProbe*>(user);
|
||||
measurepin_compare(probe, "paused/after");
|
||||
// The live leg: the display has to *leave* the event, and a resumed view does not -- a
|
||||
// `Held` model holds its snapshot with the trigger on, so resuming only un-pins it. Switching
|
||||
// the trigger off is the user's own way back to the stream, and it is what makes the last leg
|
||||
// a live view rather than the same event under another name.
|
||||
App& a = *probe.app;
|
||||
if (a.pause_button != nullptr) {
|
||||
gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(a.pause_button), FALSE);
|
||||
}
|
||||
if (a.trigger_toggle != nullptr) {
|
||||
gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(a.trigger_toggle), FALSE);
|
||||
}
|
||||
probe.settled = 0;
|
||||
probe.polls = 0;
|
||||
g_timeout_add(50, measurepin_live_wait, &probe);
|
||||
return G_SOURCE_REMOVE;
|
||||
}, &p);
|
||||
return G_SOURCE_REMOVE;
|
||||
}
|
||||
|
||||
gboolean measurepin_live_wait(gpointer user) {
|
||||
MeasurePinProbe& p = *static_cast<MeasurePinProbe*>(user);
|
||||
App& a = *p.app;
|
||||
if (a.window == nullptr || !measurepin_polls(p, "the stream to come back", a)) {
|
||||
delete &p;
|
||||
return G_SOURCE_REMOVE;
|
||||
}
|
||||
// A resumed view is live again, and there "settled" is not a state: the window scrolls and a
|
||||
// pass lands every `kMeasureRefreshUs`. What is waited for here is the *resume* itself -- the
|
||||
// follow re-armed and the pin gone -- and then a few frames for the results to be over the new
|
||||
// window rather than the frozen one.
|
||||
const bool live = a.model->display_mode() == DisplayMode::Live && !a.rows->window_pinned();
|
||||
if (!live || ++p.settled < 8) return G_SOURCE_CONTINUE;
|
||||
p.stage = 2;
|
||||
measurepin_begin(p, 0.0625);
|
||||
g_timeout_add(400, +[](gpointer user) {
|
||||
MeasurePinProbe& probe = *static_cast<MeasurePinProbe*>(user);
|
||||
measurepin_compare(probe, "live/after");
|
||||
delete &probe;
|
||||
return G_SOURCE_REMOVE;
|
||||
}, &p);
|
||||
return G_SOURCE_REMOVE;
|
||||
}
|
||||
|
||||
/// Polls for the held event the first leg needs: a latch happens when the stream says so, and a
|
||||
/// sweep on a timer has nothing to zoom into. Then for the *settled* pin, which is the state the
|
||||
/// gesture is read against.
|
||||
gboolean measurepin_held_wait(gpointer user) {
|
||||
MeasurePinProbe& p = *static_cast<MeasurePinProbe*>(user);
|
||||
App& a = *p.app;
|
||||
if (a.window == nullptr) {
|
||||
delete &p;
|
||||
return G_SOURCE_REMOVE;
|
||||
}
|
||||
if (!measurepin_polls(p, "a latched event to settle on its pin", a)) {
|
||||
// A wait that never ends is a *reading* about the state, not a hung probe: say what it was
|
||||
// waiting for rather than printing nothing at all.
|
||||
delete &p;
|
||||
return G_SOURCE_REMOVE;
|
||||
}
|
||||
// One line per *change* of the state being waited on, which is a handful in a normal run and the
|
||||
// whole reading in a stuck one: `mode=held at_pin=0` for ever is a pane whose results never land
|
||||
// on the pin, and it is invisible in the leg lines because there are none.
|
||||
const int state = static_cast<int>(a.model->display_mode()) * 4 +
|
||||
(a.rows->window_pinned() ? 2 : 0) + (measurepin_at_pin(a) ? 1 : 0);
|
||||
if (state != p.last_state) {
|
||||
p.last_state = state;
|
||||
std::fprintf(stderr,
|
||||
"[measurepin-probe] wait mode=%s pinned=%d at_pin=%d for=[%.6f..%.6f] "
|
||||
"pin=[%.6f..%.6f] passes=%d\n",
|
||||
display_mode_name(a.model->display_mode()), a.rows->window_pinned() ? 1 : 0,
|
||||
measurepin_at_pin(a) ? 1 : 0, a.measure_window.min, a.measure_window.max,
|
||||
a.measure_pin_window.min, a.measure_pin_window.max, a.measure_passes);
|
||||
std::fflush(stderr);
|
||||
}
|
||||
if (a.model->display_mode() != DisplayMode::Held || !measurepin_pinned_settled(p)) {
|
||||
if (a.model->display_mode() != DisplayMode::Held) p.settled = 0;
|
||||
return G_SOURCE_CONTINUE;
|
||||
}
|
||||
p.stage = 0;
|
||||
measurepin_begin(p, 0.25);
|
||||
g_timeout_add(400, +[](gpointer user) {
|
||||
MeasurePinProbe& probe = *static_cast<MeasurePinProbe*>(user);
|
||||
measurepin_compare(probe, "held/after");
|
||||
App& a = *probe.app;
|
||||
if (a.pause_button != nullptr) {
|
||||
gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(a.pause_button), TRUE);
|
||||
}
|
||||
probe.settled = 0;
|
||||
probe.polls = 0;
|
||||
g_timeout_add(50, measurepin_pause_wait, &probe);
|
||||
return G_SOURCE_REMOVE;
|
||||
}, &p);
|
||||
return G_SOURCE_REMOVE;
|
||||
}
|
||||
|
||||
gboolean measurepin_probe(gpointer user) {
|
||||
App* a = static_cast<App*>(user);
|
||||
if (a->window == nullptr) return G_SOURCE_REMOVE;
|
||||
// The same four types `measure_probe` adds, on a *channel* -- the first signal of a stream is
|
||||
// its clock, which is not acquired, so naming it would measure nothing at all and every
|
||||
// comparison below would be a comparison of two `--`.
|
||||
{
|
||||
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;
|
||||
}
|
||||
for (const MeasureType t : {MeasureType::Average, MeasureType::Top, MeasureType::PeakToPeak,
|
||||
MeasureType::Amplitude}) {
|
||||
MeasureOptions options;
|
||||
options.show_on_plot = measure_can_show_on_plot(t);
|
||||
a->measures.push_back({name, t, options});
|
||||
}
|
||||
rebuild_measures_pane(*a);
|
||||
// Single trigger mode, so one event stays on screen for the whole gesture.
|
||||
if (a->trigger_mode_drop != nullptr) {
|
||||
gtk_drop_down_set_selected(GTK_DROP_DOWN(a->trigger_mode_drop), 1);
|
||||
}
|
||||
if (a->trigger_toggle != nullptr) {
|
||||
gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(a->trigger_toggle), TRUE);
|
||||
}
|
||||
std::fprintf(stderr, "[measurepin-probe] %zu measurements on '%s', trigger single\n",
|
||||
a->measures.size(), name.c_str());
|
||||
std::fflush(stderr);
|
||||
auto* probe = new MeasurePinProbe;
|
||||
probe->app = a;
|
||||
g_timeout_add(50, measurepin_held_wait, probe);
|
||||
return G_SOURCE_REMOVE;
|
||||
}
|
||||
|
||||
gboolean stats_probe_report(gpointer user) {
|
||||
App* a = static_cast<App*>(user);
|
||||
if (a->window == nullptr) return G_SOURCE_REMOVE;
|
||||
|
||||
Reference in new issue
Block a user