feat: a repaired hole keeps a fainter band, so a report is not a false accusation

This commit is contained in:
Martino Ferrari committed 2026-10-08 23:00:22 +02:00
1 parent 4666b807fc
commit 3859d2ca79
5 files changed
+48 -11

No files matched your search

+8 -2
View File
@@ -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 // 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 // are a claim about a timeline the ring has moved past -- but the counter indexes the
// reservation table directly. // reservation table directly.
const Hole* hole = nullptr; Hole* hole = nullptr;
for (const Hole& h : model_.holes()) { for (Hole& h : model_.holes()) {
if (frame->counter >= h.counter_first && frame->counter < h.counter_first + h.packets) { if (frame->counter >= h.counter_first && frame->counter < h.counter_first + h.packets) {
hole = &h; hole = &h;
break; 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); model_.stats().backfilled.fetch_add(filled, std::memory_order_relaxed);
} }
+20
View File
@@ -183,6 +183,22 @@ struct Hole {
u64 tick_begin = 0; ///< absolute base tick of the hole's first raw sample 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 counter_first = 0; ///< counter of the first missing packet
std::uint32_t packets = 0; ///< packets the hole is worth 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. /// Live connection and counter state, readable from the UI thread.
@@ -342,6 +358,10 @@ class ScopeModel {
static constexpr std::size_t kMaxHoles = 64; static constexpr std::size_t kMaxHoles = 64;
void note_hole(const Hole& h); void note_hole(const Hole& h);
const std::vector<Hole>& holes() const noexcept { return holes_; } const std::vector<Hole>& 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<Hole>& holes() noexcept { return holes_; }
/// Drops holes whose time-ring slots the ring has already discarded. /// Drops holes whose time-ring slots the ring has already discarded.
void forget_holes_before(u64 slot); void forget_holes_before(u64 slot);
+3 -1
View File
@@ -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`. // `Hole::x_begin`/`x_end` are stream seconds; the plots' x is `stream_t + epoch`.
const f64 epoch = model.display_epoch(); const f64 epoch = model.display_epoch();
for (const Hole& h : model.holes()) { 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) { for (int i = 0; i < rows_; ++i) {
+1 -2
View File
@@ -12,7 +12,6 @@
#include <map> #include <map>
#include <memory> #include <memory>
#include <string> #include <string>
#include <utility>
#include <vector> #include <vector>
namespace udpscope { namespace udpscope {
@@ -502,7 +501,7 @@ class RowSet {
/// coloured without anything having to seed them. /// coloured without anything having to seed them.
std::map<std::string, plot::u32> signal_colors_; std::map<std::string, plot::u32> signal_colors_;
/// Reused across frames by `set_invalid_spans()`: see there. /// Reused across frames by `set_invalid_spans()`: see there.
std::vector<std::pair<f64, f64>> spans_; std::vector<plot::InvalidSpan> spans_;
CursorMode cursor_mode_ = CursorMode::Vertical; CursorMode cursor_mode_ = CursorMode::Vertical;
bool cursors_on_ = false; bool cursors_on_ = false;
bool relative_x_ = false; bool relative_x_ = false;
+16 -6
View File
@@ -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 /// `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 /// 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 /// 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) { gboolean reseq_probe(gpointer user) {
App* a = static_cast<App*>(user); App* a = static_cast<App*>(user);
if (a->window == nullptr) return G_SOURCE_REMOVE; 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 // 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 = 0;
std::size_t bands_repaired = 0;
if (a->rows->row_count() > 0 && a->rows->row(0).plot != nullptr) { 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, std::fprintf(stderr,
"[reseq-probe] reseq=%llu late=%llu stale=%llu holes=%llu hole_slots=%llu " "[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 " "backfilled=%llu open_holes=%zu bands=%zu bands_repaired=%zu nan_slots=%llu "
"trigger_in_hole=%d\n", "scanned=%llu trigger_in_hole=%d\n",
static_cast<unsigned long long>(m.stats().resequenced.load()), 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().late.load()),
static_cast<unsigned long long>(m.stats().stale.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().holes.load()),
static_cast<unsigned long long>(m.stats().hole_slots.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>(m.stats().backfilled.load()), open_holes, bands,
static_cast<unsigned long long>(nan_slots), bands_repaired, static_cast<unsigned long long>(nan_slots),
static_cast<unsigned long long>(scanned), trigger_in_hole); static_cast<unsigned long long>(scanned), trigger_in_hole);
std::fflush(stderr); std::fflush(stderr);
return G_SOURCE_REMOVE; return G_SOURCE_REMOVE;