// SPDX-License-Identifier: MIT // SPDX-FileCopyrightText: 2026 Martino Ferrari #include "search.hpp" #include #include #include namespace udpscope { namespace { double ms_since(const std::chrono::steady_clock::time_point& t0) { const std::chrono::duration d = std::chrono::steady_clock::now() - t0; return d.count(); } } // namespace void BufferSearch::start(const EventCondition& c, int channel, double dt) { clear(); channel_ = channel; detector_.reset(); detector_.configure(c, dt); // `configure()` is a no-op for an unchanged condition, so the explicit reset above is what // makes a re-run of the *same* search start from a clean machine. detector_.reset(); state_ = State::Running; t_scratch_.resize(static_cast(kChunk)); v_scratch_.resize(static_cast(kChunk)); } void BufferSearch::cancel() noexcept { if (state_ == State::Running) state_ = State::Cancelled; } void BufferSearch::clear() noexcept { state_ = State::Idle; started_ = false; truncated_ = false; at_ = 0; end_ = 0; first_s_ = 0.0; scanned_s_ = 0.0; total_s_ = 0.0; hits_.clear(); cursor_ = -1; chunks_ = 0; worst_chunk_ms_ = 0.0; total_ms_ = 0.0; } void BufferSearch::select(int i) noexcept { if (hits_.empty()) { cursor_ = -1; return; } if (i < 0) i = 0; if (static_cast(i) >= hits_.size()) i = static_cast(hits_.size()) - 1; cursor_ = i; } bool BufferSearch::next() noexcept { if (hits_.empty() || cursor_ + 1 >= static_cast(hits_.size())) return false; ++cursor_; return true; } bool BufferSearch::prev() noexcept { if (hits_.empty() || cursor_ <= 0) return false; --cursor_; return true; } bool BufferSearch::pump(ScopeModel& model) { if (state_ != State::Running) return false; const auto chunk_t0 = std::chrono::steady_clock::now(); u64 n = 0; { std::lock_guard lock(model.mutex()); if (channel_ < 0 || static_cast(channel_) >= model.channel_count()) { state_ = State::Cancelled; return false; } BufferStore& store = model.live(); if (!started_) { // The whole retained buffer: the bounds are deliberately absurd, because // `live_window_locked()` clamps them to the time ring and to *every* value ring, which // is the one cut that keeps the stamps and the values index-aligned by construction. u64 first = 0; u64 last = 0; model.live_window_locked(-1.0e300, 1.0e300, first, last); at_ = first; end_ = last; started_ = true; if (end_ > at_) { f64 stamp = 0.0; store.time().copy_from(at_, 1, &stamp); first_s_ = static_cast(stamp); store.time().copy_from(end_ - 1, 1, &stamp); total_s_ = static_cast(stamp) - first_s_; } } // The producer keeps pushing while we scan, so the oldest samples go. `copy_from()` fixes // *which* indices are read, not that the slots still hold them -- a read behind the base // would silently return whatever overwrote it, which would be a hit at a time that never // existed. Skipping forward and reporting `Truncated` is the honest answer. const u64 base = store.time().base(); if (base > at_) { at_ = base; truncated_ = true; detector_.reset(); // the sequence has a gap in it; no pulse spans it } if (at_ >= end_) { state_ = truncated_ ? State::Truncated : State::Done; total_ms_ += ms_since(chunk_t0); return false; } n = end_ - at_ < kChunk ? end_ - at_ : kChunk; store.time().copy_from(at_, n, t_scratch_.data()); store.values(static_cast(channel_)).copy_from(at_, n, v_scratch_.data()); } // The evaluation is outside the lock: it is the expensive half, and it touches only the // copies above. for (u64 i = 0; i < n; ++i) { const double t = static_cast(t_scratch_[static_cast(i)]); const double e = detector_.feed(static_cast(v_scratch_[static_cast(i)]), t); if (std::isfinite(e)) { hits_.push_back(e); if (hits_.size() >= kHitsMax) { at_ += i + 1; scanned_s_ = t - first_s_; state_ = State::Capped; ++chunks_; const double cost = ms_since(chunk_t0); if (cost > worst_chunk_ms_) worst_chunk_ms_ = cost; total_ms_ += cost; return false; } } scanned_s_ = t - first_s_; } at_ += n; ++chunks_; const double cost = ms_since(chunk_t0); if (cost > worst_chunk_ms_) worst_chunk_ms_ = cost; total_ms_ += cost; if (at_ >= end_) { state_ = truncated_ ? State::Truncated : State::Done; return false; } return true; } } // namespace udpscope