156 lines
4.7 KiB
C++
156 lines
4.7 KiB
C++
// SPDX-License-Identifier: MIT
|
|
// SPDX-FileCopyrightText: 2026 Martino Ferrari
|
|
|
|
#include "search.hpp"
|
|
|
|
#include <chrono>
|
|
#include <cmath>
|
|
#include <mutex>
|
|
|
|
namespace udpscope {
|
|
|
|
namespace {
|
|
double ms_since(const std::chrono::steady_clock::time_point& t0) {
|
|
const std::chrono::duration<double, std::milli> 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<std::size_t>(kChunk));
|
|
v_scratch_.resize(static_cast<std::size_t>(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<std::size_t>(i) >= hits_.size()) i = static_cast<int>(hits_.size()) - 1;
|
|
cursor_ = i;
|
|
}
|
|
|
|
bool BufferSearch::next() noexcept {
|
|
if (hits_.empty() || cursor_ + 1 >= static_cast<int>(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<std::mutex> lock(model.mutex());
|
|
if (channel_ < 0 || static_cast<std::size_t>(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<double>(stamp);
|
|
store.time().copy_from(end_ - 1, 1, &stamp);
|
|
total_s_ = static_cast<double>(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<std::size_t>(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<double>(t_scratch_[static_cast<std::size_t>(i)]);
|
|
const double e =
|
|
detector_.feed(static_cast<double>(v_scratch_[static_cast<std::size_t>(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
|