feat: the buffer can be searched for the trigger's own condition, and the hits stepped through
This commit is contained in:
1 parent
2cee85c52b
commit
415fceca87
4 files changed
+517
No files matched your search
+154
@@ -0,0 +1,154 @@
|
||||
// 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) {
|
||||
clear();
|
||||
condition_ = c;
|
||||
channel_ = channel;
|
||||
detector_.reset();
|
||||
detector_.configure(c);
|
||||
// `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;
|
||||
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();
|
||||
bool truncated = false;
|
||||
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_;
|
||||
total_ms_ += ms_since(chunk_t0);
|
||||
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
|
||||
Reference in new issue
Block a user