diff --git a/src/ui.cpp b/src/ui.cpp index ef0a8ba..8471b1a 100644 --- a/src/ui.cpp +++ b/src/ui.cpp @@ -10496,6 +10496,74 @@ gboolean color_probe(gpointer user) { return G_SOURCE_REMOVE; } +/// `UDPSCOPE_PROBE_RESEQ_MS`: one reading of the resequencing path, mid-run. +/// +/// The counters are in the exit dump too; what is only here is `nan_slots`, `trigger_in_hole` and +/// `bands`. The first is the state of the *data* rather than of a counter: a reservation writes +/// placeholders and a backfill repairs them, and the two counters agreeing proves only that the +/// same number of slots was written and read, not that the right ones were. The second asserts the +/// trigger never latched inside a reserved span. The third asserts the band reached the plot. +gboolean reseq_probe(gpointer user) { + App* a = static_cast(user); + if (a->window == nullptr) return G_SOURCE_REMOVE; + ScopeModel& m = *a->model; + std::uint64_t nan_slots = 0; + std::uint64_t scanned = 0; + std::size_t open_holes = 0; + int trigger_in_hole = 0; + { + // The hole table and the live store are the producer's, so both are read under the one mutex + // -- the same lock the frame's bind takes (`src/ui.cpp:6263`). + std::lock_guard lock(m.mutex()); + // The first non-math channel's retained window. One channel and not all of them: every + // non-math channel is reserved and repaired by the same loop over the same ticks, so a second + // channel would restate the first's answer at the cost of another pass over the ring. + for (std::size_t i = 0; i < m.channel_count(); ++i) { + if (m.channels()[i].math) continue; + const plot::data::RingBuffer& v = m.live().values(i); + // `operator[]` is window-relative: index 0 is the slot at `base()`. + scanned = v.produced() - v.base(); + for (u64 k = 0; k < scanned; ++k) { + if (!std::isfinite(v[k])) ++nan_slots; + } + break; + } + open_holes = m.holes().size(); + // A reserved span's samples are never fed to the trigger engine, so a latch stamped inside one + // would mean they had been. `Hole::x_begin`/`x_end` and `trigger_time()` are both in stream + // seconds, so they compare directly with no epoch in between. + if (m.trigger_time_valid().load()) { + const f64 t = m.trigger_time().load(); + for (const Hole& h : m.holes()) { + if (t >= h.x_begin && t <= h.x_end) { + trigger_in_hole = 1; + break; + } + } + } + } + // The shading as the plot actually holds it, read off row 0 after the frame published it. Every + // row carries the same spans, so one row is the whole answer. + std::size_t bands = 0; + if (a->rows->row_count() > 0 && a->rows->row(0).plot != nullptr) { + bands = a->rows->row(0).plot->invalid_spans().size(); + } + std::fprintf(stderr, + "[reseq-probe] reseq=%llu late=%llu stale=%llu holes=%llu hole_slots=%llu " + "backfilled=%llu open_holes=%zu bands=%zu nan_slots=%llu scanned=%llu " + "trigger_in_hole=%d\n", + static_cast(m.stats().resequenced.load()), + static_cast(m.stats().late.load()), + static_cast(m.stats().stale.load()), + static_cast(m.stats().holes.load()), + static_cast(m.stats().hole_slots.load()), + static_cast(m.stats().backfilled.load()), open_holes, bands, + static_cast(nan_slots), + static_cast(scanned), trigger_in_hole); + std::fflush(stderr); + return G_SOURCE_REMOVE; +} + /// Fires a representative accel from each group and reports the state it changed, then /// checks the GtkEditable refusal. It goes through run_app_action() for the actions and /// through the real handler for the refusal, because the refusal lives in the handler. @@ -10717,6 +10785,9 @@ int run_ui(int argc, char** argv, const Config& cfg, ScopeModel& model, Acquisit if (const char* env = g_getenv("UDPSCOPE_PROBE_COLOR_MS")) { g_timeout_add(static_cast(std::atoi(env)), color_probe, &app); } + if (const char* env = g_getenv("UDPSCOPE_PROBE_RESEQ_MS")) { + g_timeout_add(static_cast(std::atoi(env)), reseq_probe, &app); + } // Our own options were consumed by parse_args(); GTK would reject them, so only // the program name is handed to g_application_run().