fixed plot flicker, measurement cost and toolbar icons

The flicker ("no data" at no constant frequency, worse with the measurements
and cursors panes up) was a torn read of the window's first stamp: acquire()
pins the window to the ring's oldest slot, and reading it at paint time races
the producer lapping it, returning a stamp one ring span too new. x0() is the
origin every trace point is reconstructed from, so one torn read produced a
refused neighbours(), a filter that dropped three quarters of the points, and
a polyline shifted off its axis. It is cached by acquire() under the lock now.

EventSeries::neighbours() answered from the (x - x0)/dx grid, whose drift from
the real arrival stamps arrives as a constant offset in seconds -- a doubling
share of the screen per zoom step, which is the "zooming into a held event
clips it more from the left" report. Both series binary-search the stamps now.

The measurements pane went through the LOD pyramid rather than reading its
window raw (19.7 ms per recompute to 1.2 ms, 0.1% of a 2 M-sample window) and
recomputes on a cadence; the math engine lost a per-slot heap allocation, a
per-sample source lookup by name and a fetch_add that cost more than the
arithmetic it counted.

The cursors pane's mix mode drew a line per *named* slot, so a slot holding a
signal that can never produce a channel -- the stream's timestamp -- got a row
that could only read "--", and such names were re-saved every session. It
counts bound traces now and apply_layout() refuses them.

Four toolbar icons are drawn for this app and compiled in as a GResource: the
stock ones were a record dot for the trigger and three magnifiers for cursors,
measurements and math.

New probes and dump lines for all of it (trace ink, measure cost, math cost,
event zoom, toolbar icons); README and CLAUDE.md record what they cure.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Martino FerrariandClaude Opus 4.6 committed 2026-10-03 23:36:07 +02:00
1 parent 714d042809
commit a92a6b5bf0
20 files changed
+1945 -243

No files matched your search

+33
View File
@@ -204,6 +204,21 @@ class ScopeModel {
/// the streamed channels. `sample[c]` is channel `c`'s value for this slot; the math channels are
/// evaluated from those, in order, so a math channel may use another one.
void push_math_sample(const std::vector<f32>& sample);
/// What the derived channels cost the *producer*: every stored slot that went through
/// `push_math_sample()`, and the nanoseconds taken by the `math_timed()` of them that were
/// measured. The producer owns the budget between two DATA packets, so this cost is invisible in
/// the frame mean and surfaces only as dropped packets -- which is why it is counted rather than
/// inferred. It is *sampled*, so that the instrumentation cannot itself cause what it is looking
/// for: read `math_cost_ns() / math_timed()` as the per-slot figure, scaled by `math_slots()`.
std::uint64_t math_slots() const noexcept {
return math_slots_.load(std::memory_order_relaxed);
}
std::uint64_t math_timed() const noexcept {
return math_timed_.load(std::memory_order_relaxed);
}
std::uint64_t math_cost_ns() const noexcept {
return math_cost_ns_.load(std::memory_order_relaxed);
}
const std::vector<SignalInfo>& signals() const noexcept { return signals_; }
const std::vector<ChannelInfo>& channels() const noexcept { return channels_; }
@@ -434,6 +449,24 @@ class ScopeModel {
/// before the stream's own are re-appended -- a CONFIG must not leave a stale derived channel
/// behind, and must not drop a live one either.
std::vector<std::string> math_signal_names_;
/// The producer's scratch for one slot's channel values, derived ones appended as they are
/// computed. A member rather than a local because `push_math_sample()` runs once per *stored
/// slot* -- up to the wire rate -- and a local `std::vector` is a heap allocation and free per
/// slot. `assign()` reuses the capacity, so after the first slot it is a memcpy.
std::vector<f32> math_values_;
/// The counters' own copies, written and read by the producer and nobody else, so each publish
/// below is a relaxed **store** of a value already in hand rather than a `fetch_add`. The
/// read-modify-write is a locked instruction and this is the per-slot path: one `lock xadd` costs
/// about what a two-node graph's whole arithmetic does, so counting the slots cost more than the
/// work it was counting.
std::uint64_t math_slot_count_ = 0;
std::uint64_t math_timed_count_ = 0;
std::uint64_t math_cost_count_ = 0;
/// When the slot being timed began.
std::uint64_t math_cost_t0_ = 0;
std::atomic<std::uint64_t> math_slots_{0};
std::atomic<std::uint64_t> math_timed_{0};
std::atomic<std::uint64_t> math_cost_ns_{0};
std::vector<bool> enabled_;
Partition part_;