diff --git a/src/acquisition.cpp b/src/acquisition.cpp index 0b3bbb3..754e01c 100644 --- a/src/acquisition.cpp +++ b/src/acquisition.cpp @@ -875,8 +875,8 @@ void Acquisition::backfill_from(const udps_frame_t* frame, const Partition& part // and resequencing are one design. A stamp could not answer it -- the packet's own stamps // are a claim about a timeline the ring has moved past -- but the counter indexes the // reservation table directly. - const Hole* hole = nullptr; - for (const Hole& h : model_.holes()) { + Hole* hole = nullptr; + for (Hole& h : model_.holes()) { if (frame->counter >= h.counter_first && frame->counter < h.counter_first + h.packets) { hole = &h; break; @@ -928,6 +928,12 @@ void Acquisition::backfill_from(const udps_frame_t* frame, const Partition& part } } } + // This packet's share of the hole has been delivered, whatever the ring still retains of it. + // Marked on *arrival* and not on `filled`: a decimation coarse enough that no whole slot lies + // inside one packet repairs no slot at all, and keying off `filled` would leave such a hole + // permanently unrepairable -- a solid band over data that is all there, which is the exact + // defect this task closes. + ++hole->filled_packets; model_.stats().backfilled.fetch_add(filled, std::memory_order_relaxed); } diff --git a/src/model.hpp b/src/model.hpp index 48cb2a6..818b449 100644 --- a/src/model.hpp +++ b/src/model.hpp @@ -183,6 +183,22 @@ struct Hole { u64 tick_begin = 0; ///< absolute base tick of the hole's first raw sample std::uint32_t counter_first = 0; ///< counter of the first missing packet std::uint32_t packets = 0; ///< packets the hole is worth + /// How many of those packets have since arrived late and been written in. The band's two tints + /// are this field against `packets`: a hole still missing data is shaded solidly and a fully + /// repaired one faintly, so a reader can tell "there is no data here" from "there was none when + /// this window was first drawn". + /// + /// Counted in *packets* and not in slots, because a packet is the unit the reservation was made + /// in: the hole is worth exactly `packets` of them, each late arrival delivers exactly one, and + /// the client refuses a duplicate -- so this count is exact. A slot count would not be: the + /// channels have different strides, so "how many slots did this hole want" has one answer per + /// channel and the repaired test would have to pick one. + std::uint32_t filled_packets = 0; + + /// Every packet this hole was reserved for has arrived. A hole worth no packets is never + /// repaired -- there is nothing for a late packet to deliver -- which keeps a degenerate + /// reservation shaded rather than silently downgraded. + bool repaired() const noexcept { return packets > 0 && filled_packets >= packets; } }; /// Live connection and counter state, readable from the UI thread. @@ -342,6 +358,10 @@ class ScopeModel { static constexpr std::size_t kMaxHoles = 64; void note_hole(const Hole& h); const std::vector& holes() const noexcept { return holes_; } + /// The producer's view of the same table. `Acquisition::backfill_from()` marks a hole's packet + /// filled as it repairs it, and that mark is what the band's fainter tint reads. Caller holds + /// `mutex()` -- the same requirement `note_hole()` above states, and the producer already does. + std::vector& holes() noexcept { return holes_; } /// Drops holes whose time-ring slots the ring has already discarded. void forget_holes_before(u64 slot); diff --git a/src/plot_host.cpp b/src/plot_host.cpp index 34a9b4c..daf5cfa 100644 --- a/src/plot_host.cpp +++ b/src/plot_host.cpp @@ -377,7 +377,9 @@ void RowSet::set_invalid_spans(ScopeModel& model, DisplayMode mode) { // `Hole::x_begin`/`x_end` are stream seconds; the plots' x is `stream_t + epoch`. const f64 epoch = model.display_epoch(); for (const Hole& h : model.holes()) { - spans_.emplace_back(h.x_begin + epoch, h.x_end + epoch); + // `repaired()` and not a second list: the band keeps reporting a repaired hole, in the + // fainter tint, until its slots age out of the ring and `forget_holes_before()` drops it. + spans_.push_back(plot::InvalidSpan{h.x_begin + epoch, h.x_end + epoch, h.repaired()}); } } for (int i = 0; i < rows_; ++i) { diff --git a/src/plot_host.hpp b/src/plot_host.hpp index c5199a7..6db97b9 100644 --- a/src/plot_host.hpp +++ b/src/plot_host.hpp @@ -12,7 +12,6 @@ #include #include #include -#include #include namespace udpscope { @@ -502,7 +501,7 @@ class RowSet { /// coloured without anything having to seed them. std::map signal_colors_; /// Reused across frames by `set_invalid_spans()`: see there. - std::vector> spans_; + std::vector spans_; CursorMode cursor_mode_ = CursorMode::Vertical; bool cursors_on_ = false; bool relative_x_ = false; diff --git a/src/ui.cpp b/src/ui.cpp index 8471b1a..45eba9e 100644 --- a/src/ui.cpp +++ b/src/ui.cpp @@ -10502,7 +10502,10 @@ gboolean color_probe(gpointer user) { /// `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. +/// trigger never latched inside a reserved span. The third asserts the band reached the plot, and +/// splits it by state: a hole whose late packets have all arrived keeps its band in the fainter +/// tint, so `bands_repaired` is how a reading tells a window that is *missing* data from one that +/// merely *was*. gboolean reseq_probe(gpointer user) { App* a = static_cast(user); if (a->window == nullptr) return G_SOURCE_REMOVE; @@ -10543,22 +10546,29 @@ gboolean reseq_probe(gpointer user) { } } // 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. + // row carries the same spans, so one row is the whole answer. Split by state because the two + // mean different things: `bands` is what the window is reporting and `bands_repaired` how much + // of that report is historical, so `bands == bands_repaired` with `nan_slots=0` is a healthy + // reading and not the contradiction it would be if the band had one state. std::size_t bands = 0; + std::size_t bands_repaired = 0; if (a->rows->row_count() > 0 && a->rows->row(0).plot != nullptr) { - bands = a->rows->row(0).plot->invalid_spans().size(); + for (const plot::InvalidSpan& span : a->rows->row(0).plot->invalid_spans()) { + ++bands; + if (span.repaired) ++bands_repaired; + } } 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", + "backfilled=%llu open_holes=%zu bands=%zu bands_repaired=%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), + bands_repaired, static_cast(nan_slots), static_cast(scanned), trigger_in_hole); std::fflush(stderr); return G_SOURCE_REMOVE;