From fc81e661d0f63f0b2150472eeb9c59875f62b84a Mon Sep 17 00:00:00 2001 From: Martino Ferrari Date: Thu, 8 Oct 2026 18:58:06 +0200 Subject: [PATCH] feat: the status bar and the exit dump report resequencing, holes and stale packets --- src/acquisition.cpp | 3 +++ src/model.hpp | 15 +++++++++++++++ src/status_text.cpp | 15 +++++++++++++++ src/status_text.hpp | 10 ++++++++++ src/ui.cpp | 18 ++++++++++++++++-- 5 files changed, 59 insertions(+), 2 deletions(-) diff --git a/src/acquisition.cpp b/src/acquisition.cpp index b2d41d6..06d8532 100644 --- a/src/acquisition.cpp +++ b/src/acquisition.cpp @@ -91,6 +91,9 @@ void Acquisition::run() { out.packets.store(s.packets_received, std::memory_order_relaxed); out.frames.store(s.frames_delivered, std::memory_order_relaxed); out.gaps.store(s.counter_gaps, std::memory_order_relaxed); + out.resequenced.store(s.late_resequenced, std::memory_order_relaxed); + out.late.store(s.late_delivered, std::memory_order_relaxed); + out.stale.store(s.stale_packets, std::memory_order_relaxed); out.reconnects.store(s.reconnects, std::memory_order_relaxed); out.connected.store(udps_client_is_connected(client_) != 0, std::memory_order_relaxed); } diff --git a/src/model.hpp b/src/model.hpp index dc38347..48cb2a6 100644 --- a/src/model.hpp +++ b/src/model.hpp @@ -195,6 +195,21 @@ struct StreamStats { std::atomic have_config{false}; std::atomic holes{0}; ///< reserved invalidated spans std::atomic hole_slots{0}; ///< stored slots those spans cover + /// Out-of-order packets put back in order inside the reorder window: no hole was ever + /// seen for these. + std::atomic resequenced{0}; + /// Out-of-order packets delivered behind the sequence, past the window. + std::atomic late{0}; + /// Packets the client still refused outright (a duplicate, or any late packet while + /// resequencing is disabled). Counted since the beginning and displayed nowhere until + /// now, which is why an inversion had to be inferred from `packets` and `frames` + /// diverging. + std::atomic stale{0}; + /// Stored slots a late packet repaired. Declared here rather than with the backfill that + /// writes it, because the display segment below is built once and reads it from the start: + /// a reading of 0 beside a non-zero `holes` is exactly the honest answer until the + /// backfill lands. + std::atomic backfilled{0}; }; /// What the plot is showing, which is one decision made once per frame: the bind, the diff --git a/src/status_text.cpp b/src/status_text.cpp index 1b2bc5b..b78c911 100644 --- a/src/status_text.cpp +++ b/src/status_text.cpp @@ -79,6 +79,21 @@ std::string format_detail(const StatusSnapshot& s) { s.plot1_traces, s.plot1_samples, s.buffer_mb, s.span_s, static_cast(s.time_stride)); std::string out = buf; + // Nothing about resequencing is said at all while all six read zero, which is a stream that + // never lost ordering: the field would otherwise be six zeros on every clean run, and this + // string is read by eye out of the exit dump. + if (s.resequenced || s.late || s.stale || s.holes || s.hole_slots || s.backfilled) { + char note[160]; + std::snprintf(note, sizeof(note), + " | reseq %llu late %llu stale %llu holes %llu (%llu slots, %llu filled)", + static_cast(s.resequenced), + static_cast(s.late), + static_cast(s.stale), + static_cast(s.holes), + static_cast(s.hole_slots), + static_cast(s.backfilled)); + out += note; + } if (s.paused) out += " | paused for " + format_elapsed(s.paused_for_s); if (s.trigger_on) { out += s.have_event ? " | last event " + format_elapsed(s.since_event_s) + " ago" diff --git a/src/status_text.hpp b/src/status_text.hpp index 04544f5..48c2378 100644 --- a/src/status_text.hpp +++ b/src/status_text.hpp @@ -56,6 +56,16 @@ struct StatusSnapshot { /// the plot. bool paused = false; double paused_for_s = 0.0; + + /// The resequencing counters, gathered with `packets`/`frames`/`gaps` so the detail tier and + /// the dump quote one consistent set. All six zero means the stream never lost ordering, and + /// the detail tier then says nothing about it at all. + std::uint64_t resequenced = 0; + std::uint64_t late = 0; + std::uint64_t stale = 0; + std::uint64_t holes = 0; + std::uint64_t hole_slots = 0; + std::uint64_t backfilled = 0; }; /// The bar: "live · 10 kS/s · 2 s window", plus " · event 4.2 s ago" in trigger mode, " · for diff --git a/src/ui.cpp b/src/ui.cpp index 03bcb6c..95ce578 100644 --- a/src/ui.cpp +++ b/src/ui.cpp @@ -6898,6 +6898,12 @@ gboolean tick(gpointer user) { snap.packets = st.packets.load(); snap.frames = st.frames.load(); snap.gaps = st.gaps.load(); + snap.resequenced = st.resequenced.load(); + snap.late = st.late.load(); + snap.stale = st.stale.load(); + snap.holes = st.holes.load(); + snap.hole_slots = st.hole_slots.load(); + snap.backfilled = st.backfilled.load(); snap.signals = signal_count; snap.channels = channel_count; snap.plot1_traces = bound; @@ -10718,12 +10724,20 @@ int run_ui(int argc, char** argv, const Config& cfg, ScopeModel& model, Acquisit if (exit_after_ms > 0.0) { const Partition& part = model.partition(); std::printf("[udpscope] frames=%llu packets=%llu gaps=%llu channels=%zu span=%.3fs " - "stride=%llu\n", + "stride=%llu" + " reseq=%llu late=%llu stale=%llu holes=%llu hole_slots=%llu backfilled=%llu" + "\n", static_cast(model.stats().frames.load()), static_cast(model.stats().packets.load()), static_cast(model.stats().gaps.load()), model.channel_count(), part.span_s, - static_cast(part.time_stride)); + static_cast(part.time_stride), + static_cast(model.stats().resequenced.load()), + static_cast(model.stats().late.load()), + static_cast(model.stats().stale.load()), + static_cast(model.stats().holes.load()), + static_cast(model.stats().hole_slots.load()), + static_cast(model.stats().backfilled.load())); for (std::size_t i = 0; i < model.channel_count(); ++i) { std::printf("[udpscope] channel %zu '%s' stored=%llu published=%llu\n", i, model.channels()[i].name.c_str(),