// SPDX-License-Identifier: MIT // SPDX-FileCopyrightText: 2026 Martino Ferrari #pragma once #include #include #include #include "condition.hpp" #include "model.hpp" namespace udpscope { /// A scan of the whole retained buffer for every sample satisfying an `EventCondition`. /// /// It runs an `EventDetector` -- the *same* evaluator the live trigger runs -- over the ring /// in stream order, so a hit is by construction an instant the trigger would have fired on. /// That is the claim the probe's armed-vs-scanned cross-check checks, and it is the reason /// the detector was separated from the engine at all. /// /// Everything here is in **ring-stamp space**, which is stream seconds relative to the /// model's epoch: the clock `TriggerEngine::process()` is fed, the clock /// `ScopeModel::live_window_locked()` searches, and the clock `publish_pause_view()` cuts in. /// No conversion to display x happens in this class. /// /// It runs on the GTK thread in chunks rather than on a thread of its own: the samples live /// behind `ScopeModel::mutex()`, which the producer holds for each packet, and a scan that /// took the lock once would stall acquisition for the whole buffer. One chunk per call holds /// it for `kChunk` samples and no longer. class BufferSearch { public: /// The hit list's ceiling. A condition matching a large share of the samples -- an edge /// trigger on a noisy channel -- would otherwise fill memory with a list nobody can /// navigate; the scan stops and says `Capped`, which is information rather than a hang. static constexpr std::size_t kHitsMax = 10000; /// Samples per `pump()`, and therefore the only latency knob: the producer waits on the /// lock for exactly one chunk's scan. static constexpr u64 kChunk = 8192; enum class State { Idle, ///< nothing has been searched. Running, ///< `pump()` has more to do. Done, ///< the whole retained buffer was scanned. Truncated, ///< the producer overwrote samples before they were reached. Capped, ///< `kHitsMax` was reached. Cancelled, ///< the user stopped it, or the channel went away. }; /// Begins a scan of the channel for the condition. Discards any previous result. void start(const EventCondition& c, int channel, double dt); void cancel() noexcept; void clear() noexcept; /// Scans one chunk. Returns true while there is more to do. Takes `model.mutex()` itself, /// so the caller must *not* hold it. bool pump(ScopeModel& model); State state() const noexcept { return state_; } const std::vector& hits() const noexcept { return hits_; } /// Which hit the view is on, or -1 for none. int cursor() const noexcept { return cursor_; } void select(int i) noexcept; /// Moves the cursor and returns whether it moved; the ends do not wrap, because a wrap /// from the last hit to the first looks exactly like the view not having moved at all. bool next() noexcept; bool prev() noexcept; /// How much of the buffer has been scanned, and how much there was, in stream seconds. double scanned_s() const noexcept { return scanned_s_; } double total_s() const noexcept { return total_s_; } /// Cost, for the probe: chunks scanned, the worst single chunk, and the whole scan. std::uint64_t chunks() const noexcept { return chunks_; } double worst_chunk_ms() const noexcept { return worst_chunk_ms_; } double total_ms() const noexcept { return total_ms_; } private: EventDetector detector_{}; int channel_ = -1; State state_ = State::Idle; bool started_ = false; ///< the index range has been established. bool truncated_ = false; ///< the producer overtook the scan at least once; sticky for the whole scan. u64 at_ = 0; ///< next absolute index to scan. u64 end_ = 0; ///< one past the last absolute index to scan. double first_s_ = 0.0; ///< stamp of `at_` when the scan began. double scanned_s_ = 0.0; double total_s_ = 0.0; std::vector hits_; int cursor_ = -1; std::uint64_t chunks_ = 0; double worst_chunk_ms_ = 0.0; double total_ms_ = 0.0; std::vector t_scratch_; std::vector v_scratch_; }; } // namespace udpscope