fix: a lost packet reserves the samples it owed instead of vanishing from the timeline

This commit is contained in:
Martino Ferrari committed 2026-10-08 18:18:12 +02:00
1 parent 85ba835d61
commit 5915fc8421
4 files changed
+183 -9

No files matched your search

+30
View File
@@ -167,6 +167,24 @@ class BufferStore {
std::vector<std::shared_ptr<ChannelBuffer>> channels_;
};
/// One reserved, invalidated span: samples the counter says exist and the stream has not
/// delivered. Its stored slots hold NaN, so a reader needs no special case -- the table is
/// what lets a *late* packet find the slots it belongs to, and what the band draws.
struct Hole {
f64 x_begin = 0.0; ///< stream seconds of the hole's first slot
f64 x_end = 0.0; ///< stream seconds of the hole's last slot
/// Absolute **time-ring** slot of the first reserved slot, and the count of them. The
/// time ring is the one clock every channel shares, so this is the only slot address
/// that is unambiguous: per-channel value rings run at `time_stride * stride[i]`, which
/// differs between channels. A consumer that needs a *channel's* slots derives them from
/// `tick_begin` and that channel's stride rather than reusing these.
u64 slot_begin = 0;
u64 slot_count = 0;
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
};
/// Live connection and counter state, readable from the UI thread.
struct StreamStats {
std::atomic<std::uint64_t> packets{0};
@@ -175,6 +193,8 @@ struct StreamStats {
std::atomic<std::uint64_t> reconnects{0};
std::atomic<bool> connected{false};
std::atomic<bool> have_config{false};
std::atomic<std::uint64_t> holes{0}; ///< reserved invalidated spans
std::atomic<std::uint64_t> hole_slots{0}; ///< stored slots those spans cover
};
/// What the plot is showing, which is one decision made once per frame: the bind, the
@@ -301,6 +321,15 @@ class ScopeModel {
/// is armed.
BufferStore& live();
/// Records a reserved hole. Caller holds `mutex()` (the producer already does). The table
/// is capped at `kMaxHoles`; past that the oldest is dropped, because an unbounded list of
/// gaps is a leak and the oldest gap is the one furthest from being backfilled.
static constexpr std::size_t kMaxHoles = 64;
void note_hole(const Hole& h);
const std::vector<Hole>& holes() const noexcept { return holes_; }
/// Drops holes whose time-ring slots the ring has already discarded.
void forget_holes_before(u64 slot);
/// Builds the captured event from the slice of the live buffer covering
/// `[edge - pre_s, edge + post_s]` in stream seconds, and publishes it atomically.
/// The stamps are re-based so the edge sits at x = 0. The plot binds to this
@@ -544,6 +573,7 @@ class ScopeModel {
Partition part_;
std::unique_ptr<BufferStore> store_;
std::vector<Hole> holes_;
TriggerConfig trigger_{};
StreamStats stats_{};
f64 epoch_ = 0.0;