test: a probe reads the resequencing path's state, not only its counters

This commit is contained in:
Martino Ferrari committed 2026-10-08 20:23:38 +02:00
1 parent ee55ac84b7
commit 4666b807fc
1 file changed
+71
+71
View File
@@ -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<App*>(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<std::mutex> 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<f32>& 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<unsigned long long>(m.stats().resequenced.load()),
static_cast<unsigned long long>(m.stats().late.load()),
static_cast<unsigned long long>(m.stats().stale.load()),
static_cast<unsigned long long>(m.stats().holes.load()),
static_cast<unsigned long long>(m.stats().hole_slots.load()),
static_cast<unsigned long long>(m.stats().backfilled.load()), open_holes, bands,
static_cast<unsigned long long>(nan_slots),
static_cast<unsigned long long>(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<guint>(std::atoi(env)), color_probe, &app);
}
if (const char* env = g_getenv("UDPSCOPE_PROBE_RESEQ_MS")) {
g_timeout_add(static_cast<guint>(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().