Fixed triggering mode change when acquiring

This commit is contained in:
Martino Ferrari committed 2026-10-01 17:54:17 +02:00
1 parent 3d4085898e
commit 0d741c401b
10 files changed
+478 -80

No files matched your search

+89 -12
View File
@@ -42,7 +42,13 @@ xvfb-run -a env UDPSCOPE_EXIT_AFTER_MS=8000 UDPSCOPE_ENABLE_ALL_MS=1000 \
then dump the whole pipeline state to stdout (frame/sample counts, per-plot painted
frames, trace bindings, panel sizes, status line in both states).
- `UDPSCOPE_ENABLE_ALL_MS`, `UDPSCOPE_ARM_TRIGGER_MS`, `UDPSCOPE_PROBE_DISARM_MS`,
`UDPSCOPE_TRIG_TRACE_MS` — schedule individual probes.
`UDPSCOPE_TRIG_TRACE_MS` — schedule individual probes. `UDPSCOPE_PROBE_DISARM_MS` only
*starts* its sequence: it switches the trigger on and then polls for a capture to open,
so the Disarm lands ~300 ms into one rather than wherever a fixed delay happened to fall.
- `UDPSCOPE_PROBE_FIELDS_MS` — change the trigger panel's edge and Normal/Single
dropdowns. As much a crash test as a state check (see the GTK signal-signature invariant
below), so time it inside a capture: `[fields-probe] ... capturing=1` followed by no
SIGSEGV *is* the assertion.
- `UDPSCOPE_PROBE_PAUSE_MS` — pause acquisition mid-run so layout numbers are
measured on a still picture.
- `UDPSCOPE_EXPORT_MS` (+ optional `UDPSCOPE_EXPORT_PATH`, default
@@ -75,11 +81,20 @@ Two lines of the self-check output are assertions in disguise:
the documented probes have shown one ~320 px step — the side panel's width — that
reverts; it is intermittent and unexplained, so read `changes` before calling a range
the twitch bug.
- `[trig-probe] ... trigger=on ... display=...` (stderr, `UDPSCOPE_TRIG_TRACE_MS`) —
with `trigger=on`, `display` must read `event(frozen)` or `nothing`, **never**
`live`, except for the single interval that contains the toggle itself — recognise
it by `latches=0` *and* the next probe reading `nothing` with the same frozen
`axis=`. And `snap=[a..b]` must equal `axis=[a..b]`, with `b` positive by roughly
- `[trig-probe] ... trigger=on ... display=... epoch=...` (stderr,
`UDPSCOPE_TRIG_TRACE_MS`) — with `trigger=on`, `display` must read `event(frozen)`,
`live(capturing)` or `nothing`, **never plain `live`**, except for the single interval
that contains the toggle itself — recognise it by `latches=0` *and* the next probe
reading `nothing` or `live(capturing)` with a frozen `axis=`. Plain `live` with the
trigger on is the old "the plot keeps scrolling in trigger mode" defect; the named
`live(capturing)` is the feature. In that state `epoch=` must equal minus the capture's
edge and `axis=` must stay at `[-pre..+post]` across consecutive probes while
`data=`'s upper bound climbs towards `+post` — an axis that moves instead means the
capture is being treated as a live view (`clamp_to_data()` reaching a pinned window).
`epoch=` must be back to `0.0000` in every other state. Note the probe reads
`pending=` fresher than `display=` (which is last tick's decision), so a single
`pending=0 display=live(capturing)` line is one frame of sampling skew, not a defect.
And `snap=[a..b]` must equal `axis=[a..b]`, with `b` positive by roughly
the post-trigger share: that is the "post window is empty" defect's observable.
**`drawn=[a..b]` must equal `data=[a..b]`** in both modes: `data` is `x_at()` (what
the crosshair reads) and `drawn` is `x0() + gather()` (what the renderer maps), so a
@@ -109,6 +124,15 @@ Two lines of the self-check output are assertions in disguise:
is the same class of bug, not rounding: `publish_event()` clamps the slice to every
value ring's `produced()`, so publishing before each channel of the latching sample
has been pushed silently drops it.
- `[disarm-probe] was_capturing=1 display=.. seq=A->B events=C->D snap=[a..b] n=N
axis=[..]` (stderr, `UDPSCOPE_PROBE_DISARM_MS`) — with `was_capturing=1`, **`display`
must read `event(frozen)`** and `snap`/`axis` must be `[-pre..e]` where `e` is how far
the post window had filled, with `n` that span times the stream's real rate plus one.
`display=nothing` (or a `snap` from the *previous* event) is the "disarming mid-capture
makes the event completely disappear" defect. **`seq` must advance by exactly one while
`events` does not move**: no event fired, so the partial publish deliberately leaves
`trigger_latches()` alone — which is also what keeps the `trigger events: N total, M at
the disarm` line in the dump meaning what it says.
- `[export-probe] mode=.. axis=[a..b] rows=N cols=M time=[c..d] stream=[..]` (stderr,
`UDPSCOPE_EXPORT_MS`) — **`time=` must equal `axis=`** and **`rows=` must be the
span times the stream's real rate, plus one** (2 s at 10 kS/s is 20001). The export
@@ -116,7 +140,13 @@ Two lines of the self-check output are assertions in disguise:
cut from the LOD pyramid, and a `time=` narrower than `axis=` is a window searched on
the wrong clock (the live branch subtracts `display_epoch()` before searching the
ring). In `mode=event`, `stream=` must be the edge ± the window; in `mode=live` it
equals `time=`. The two `bytes=` lines are the writers: the `.npy` is
equals `time=`.
The one exception to both: an export taken **mid-capture** reports `mode=live` with
`time=[-pre..e]` where `e` is how far the post window has filled, and `stream=` offset
from it by the edge (so `stream - time` reads the edge exactly) — the export is what is
on screen, and during a capture that is a partly filled window. Pin the probe outside a
capture when checking the `rows=` count.
The two `bytes=` lines are the writers: the `.npy` is
`64·⌈(10+len(header)+1)/64⌉ + rows·(16 + 4·cols)`, so a size that is not that is a
mispacked record. Read it back with `pd.DataFrame(np.load(path))` after changing
either writer.
@@ -146,7 +176,9 @@ once per frame by the UI (`tick()` in `src/ui.cpp`) around `RowSet::bind()`/`acq
Samples themselves cross via `plot::data::RingBuffer` (single producer, single
consumer) and never through the lock. Cross-thread scalars are atomics on `ScopeModel`
(`trigger_enabled_`, `trigger_armed_`, `trigger_latches_`, `trigger_time_`,
`event_`, `generation_`).
`event_`, `generation_`). Two of them are a pair: the producer writes `capture_edge_`
before `trigger_pending_` and those two carry release/acquire, so a consumer that sees a
capture open sees which edge it belongs to.
### Data path
@@ -157,10 +189,37 @@ that is what guarantees a full pre-trigger history on the *first* latch after ar
The spec's second buffer (item 1) is the latched `EventSnapshot`, which is already an
immutable copy of the captured window; see README for why two live rings were dropped.
In trigger mode `RowSet::bind()` binds each trace to the snapshot and to nothing else
(the last event, or nothing before the first). **Never the live ring**, not even while
the post window is filling — SPECS:37 says the view must not refresh until the next
event, and `trigger_pending()` is informational only, for the status line's wording.
What a trace binds to is `ScopeModel::display_mode()`, one of four states, and
`RowSet::bind()` takes it as an argument rather than recomputing it:
- `Live` — no trigger: the live ring on the stream clock.
- `Capturing` — an edge has been accepted (so the pre window was full) and the post
window is still filling: the live ring, on the coming event's own clock
(`display_epoch() == -edge`), inside an axis already pinned to `[-pre, +post]`. The
post side fills in view and the marker sits at x = 0 throughout.
- `Held` — the frozen `EventSnapshot` of the last event.
- `Waiting` — armed, nothing captured yet: blank.
**Disarm during a capture publishes what the capture got to**, then disarms: the producer
cuts the window at the newest sample and that partial snapshot becomes the held event, so
the picture on screen at the moment of the Disarm stays on screen. `engine.disarm()` clears
`pending_`, which drops the display out of `Capturing`, and with nothing published the view
fell back to the *previous* event — or to blank on a first capture. No event fired, so
`trigger_latches()` is deliberately not bumped; `ScopeModel::event_seq()` counts published
snapshots instead, and the frame tick keys its event framing on *that*. Keep the two
distinct: the latch count is what the probes assert "how many events fired" with.
`Capturing` is a **deliberate departure from SPECS:37** ("data visualisation should not
be refreshed till next event"), asked for explicitly; that reading is what the old "never
the live ring mid-capture" rule enforced. What survives of the rule is that `Held` binds
the snapshot *only* — a held event is never quietly replaced by live data — and that the
*axis* does not move during a capture: it is the event's window from the edge onwards,
never a sliding live view.
`display_mode()` must be read **once per frame** and passed around (the tick does this,
and hands it to `bind()`). The producer can open or close a capture between any two
reads, and two reads in one frame put the bind, the epoch, the pin and the status line on
different clocks.
Layers to keep straight:
@@ -265,6 +324,15 @@ far the largest file.
capture may legitimately be shorter than asked for — the ring need not reach back a
full pre-trigger share on the first latch after arming, so `snap=[-0.339..1.000]`
for a ±1 s request is correct there rather than a defect.
- **A capture in progress is the one place the axis is pinned from the *request*.** There
is no snapshot to measure yet, so `DisplayMode::Capturing` frames
`[-(span - post_span_s(span)), +post_span_s(span)]` — computed with the producer's own
`TriggerConfig::post_span_s()`, not a second expression that rounds differently, because
the marker at x = 0 has to land where the coming cut will put it. The latch then re-pins
from the snapshot, and the two agreeing is why the picture does not jump at the latch.
- **`clamp_to_data()` must be skipped while `capturing` as well as while `event_held`.**
A capture is live data on a pinned axis — the exact combination the clamp was written
for and against: it would follow the live edge and drag the frozen window off the event.
- **`publish_event()` cuts one absolute index range and uses it for the time ring and
every value ring.** That is what makes `stamps` and each `values[c]` equal-length
and index-aligned by construction instead of by two truncations that can disagree —
@@ -288,6 +356,15 @@ far the largest file.
tick as the channel dump; the status line only formats what is already there. The
badge's markup is likewise rewritten on a change of phase (twice a second), never on
all sixty frames.
- **A `notify::<prop>` handler takes three parameters, a `value-changed` handler two.**
`GObject::notify` passes `(GObject*, GParamSpec*, gpointer)` while
`GtkSpinButton::value-changed` passes `(GtkSpinButton*, gpointer)`, so one callback cannot
serve both: on the `notify::` connection the `GParamSpec*` lands in the `user_data` slot
and the handler dereferences it as its `App*`. `G_CALLBACK` is a cast, so nothing warns.
That is why the trigger panel's spin buttons and its dropdowns have separate thin entry
points (`on_trigger_field_changed`, `on_trigger_drop_changed`) onto one
`publish_trigger_fields()`: changing the edge or Normal/Single dropdown used to segfault
outright. `UDPSCOPE_PROBE_FIELDS_MS` is the guard.
- **The header bar's buttons are icon-only**, so headless probes must reach them
through the `App` pointers (`trigger_toggle`, `pause_button`, …), not via
`find_labelled_button()`. The Arm/Disarm button in the trigger panel still has text.
+61 -12
View File
@@ -95,12 +95,45 @@ of the live ring into an immutable `EventSnapshot` and publishes it atomically.
Pre-trigger history is what the ring already holds, so the percentage is a share of
the event window and not of the buffer.
In trigger mode a trace is bound to the snapshot and to nothing else: the last
event latched, or nothing at all before the first one. Never the live ring, not
even while the post window is filling — a picture that changes only on an event is
the whole point of trigger mode (SPECS: "data visualisation should not be refreshed
till next event"), and binding the live ring mid-capture made the plot scroll for
most of every event cycle.
**A capture fills in view** (`DisplayMode::Capturing`). The moment an edge is accepted —
which, by the gate below, means the pre-trigger history is already complete — the axis is
pinned to the whole event window `[-pre, +post]` with the edge at x = 0, and the trace
follows the live ring inside it until the snapshot lands. So the pre side is there
immediately, the post side sweeps in from x = 0 to the right, and at the latch the
snapshot replaces the live binding on an axis that does not move: the only visible change
is that the picture stops.
This is a **deliberate deviation from the spec**, requested explicitly. SPECS:37 says the
visualisation "should not be refreshed till next event", and an earlier iteration of this
app read that strictly — a trace was bound to the snapshot and to nothing else, never the
live ring, not even while the post window was filling. What that reading cost was the whole
of every capture: for as long as the post side took to fill, the plot showed the *previous*
event with no sign that anything was happening. The literal rule is gone; what it was
protecting is not. A **held** event is still bound to the snapshot alone
and is never quietly overwritten by live data, and the axis is still the event's own
window throughout — a capture is live data on a frozen frame, not a live view.
The four display states are `Live` (no trigger), `Capturing`, `Held` (the frozen
snapshot) and `Waiting` (armed, nothing captured yet: blank). `ScopeModel::display_mode()`
answers which, and the frame tick reads it once and hands it to the bind, the display
epoch, the axis pin and the status line — the producer can open or close a capture between
any two reads, and the four disagreeing about which clock they are on is how a trace ends
up drawn a window away from its axis.
**Disarm during a capture keeps what the capture got to.** It cuts the window at the
newest sample, publishes that as the held event and freezes it — the axis tightens to the
part that filled (`[-pre, +0.3 s]` for a 1 s post window disarmed 300 ms in) with the edge
still at x = 0. It used to lose the capture outright: clearing the engine's pending flag
dropped the display out of `Capturing` with nothing published behind it, so the view fell
back to the *previous* event, or to blank if this was the first. No event fired, so the
latch counter does not move; a separate `ScopeModel::event_seq()` counts published
snapshots, and that is what the frame tick keys its framing on.
Mid-capture the live ring is read on the *event's* clock: `display_epoch()` is set to
minus the edge, so `x = 0` is the edge for the live channels exactly as it is in a
snapshot's own stamps. That re-basing machinery already existed (it was written for the
frozen view and then never needed, since snapshots carry edge-relative stamps); this is
what finally uses it.
**The whole trigger path measures the window in stream seconds, and never in
samples.** `TriggerEngine` is fed each sample's timestamp and latches once that clock
@@ -222,14 +255,15 @@ line always names the state it is in:
|---|---|
| `[live]` | trigger mode off: the view follows the stream continuously |
| `[trigger: armed, waiting for an event]` | watching for an edge; the view shows the previous state |
| `[trigger: CAPTURING (post-trigger window filling)]` | an edge was accepted; the post side is filling in view |
| `[trigger: EVENT HELD …]` | an event was latched; the view is frozen on it |
| `[trigger: disarmed (view frozen)]` | armed and waiting are not the same thing — nothing will arrive until it is armed again |
While an event is on screen a **red vertical line marks the trigger instant** on
every row, at the edge's own stream time — the latch may complete a post-window
later, but the line marks the edge, so the pre/post split can be read straight off
the plot. It is not one of the user's cursors: it has its own colour, ignores the
cursor show/hide toggle, and is parked (invisible) when no event is held.
While an event is on screen — being captured or held — a **red vertical line marks the
trigger instant** on every row, at x = 0. The latch completes a post-window later, but
the line marks the edge, so the pre/post split can be read straight off the plot. It is
not one of the user's cursors: it has its own colour, ignores the cursor show/hide
toggle, and is parked (invisible) when there is no event.
**The status line.** It starts with an activity badge and then the mode, the
connection, the measured rate, the packet/frame/gap counters, what plot 1 holds and
@@ -253,7 +287,10 @@ the buffer's size and span. Three of those are worth calling out:
then minutes, then hours. A frozen view looks the same whether the last event was a
second or an hour ago, so without it there is no way to tell a working trigger from
a dead one. Measured on the wall clock, not the stream's: it is a question about how
long *you* have been waiting. Before the first latch it reads `no event yet`.
long *you* have been waiting. The clock starts at the **edge**, not at the latch — that
is the instant the user saw, and it is also when the capture appeared on screen — so a
long capture reads as an event that is already ageing rather than one that has not
happened. Before the first event it reads `no event yet`.
Switching the header bar's **trigger** button off returns to `[live]` and resumes
drawing the stream; the trigger panel carries the same action as a
@@ -403,6 +440,12 @@ answered: a live view keeps scrolling while the file chooser is up. The live cut
through `ScopeModel::live_window_locked()`, the same index range `publish_event()`
uses, so an export and a capture of the same window hold the same samples.
The export follows the display, not the trigger switch: it reads the snapshot only while
one is **held**, and the live ring otherwise — including mid-capture, where the rows are
on the live ring too. So exporting during a capture writes the partly filled window that
is on screen (`time` from `-pre` up to wherever the post side has reached), not the
previous event.
## Review findings not (yet) implemented
These are known deviations, found by a spec/code review, that are deliberate for
@@ -429,6 +472,12 @@ now — they are listed so they are not mistaken for anything else:
- **Pre-trigger % is a share of the *event* window**, not of the retained history —
see the trigger section above for why (the spec's sentence predates the event
window being a separate control).
- **A capture is drawn as it fills**, against SPECS:37's "should not be refreshed till
next event", which the app used to honour literally. Requested explicitly: a capture
whose post window takes any appreciable time showed the previous event and no sign of
progress. What the spec was protecting still holds — a *held* event is bound to its
snapshot alone, and the axis is the event's own window from the edge onwards rather
than a sliding live view.
- **A plot holds at most four signals** (`RowSet::kSlotsPerRow`), because a `Plot`
cannot give up a trace and the slots are therefore created up front.
- **One toggle drives both cursor orientations.** The vertical markers and the
+31
View File
@@ -349,6 +349,32 @@ void Acquisition::on_data(const udps_frame_t* frame) {
model_.trigger_armed().store(true, std::memory_order_relaxed);
}
} else if (engine.armed()) {
// Disarm cuts a capture in progress where it got to and keeps it, instead of
// discarding it. `engine.disarm()` clears `pending_`, which drops the display out of
// `Capturing`; with nothing published the view fell back to the *previous* event, or
// to blank if this was the first -- the capture the user was watching simply
// vanished. Publishing first means the same snapshot path freezes it, so what is on
// screen at the moment of the Disarm stays on screen.
//
// The full post window is requested and `live_window_locked()` clips it to what the
// rings hold, so the snapshot ends at the newest sample and reports its own shorter
// `post_s` -- the same way a first latch whose pre window was short reports its own
// `pre_s`. No event fired, so `trigger_latches()` is deliberately *not* bumped; the
// UI notices the new picture through `event_seq()`.
if (engine.pending()) {
const double edge = engine.last_trigger_time();
model_.publish_event(event_span - post_want, post_want, edge);
model_.trigger_time().store(edge, std::memory_order_relaxed);
model_.trigger_time_valid().store(true, std::memory_order_relaxed);
auto snap = model_.event();
std::fprintf(stderr,
"[udpscope] trigger: disarmed mid-capture, edge at %.3f s kept as "
"[-%.4f..%.4f] n=%llu (post window asked %.4f s, filled %.4f s)\n",
edge, snap ? snap->pre_s : 0.0, snap ? snap->post_s : 0.0,
static_cast<unsigned long long>(snap ? snap->stamps.size() : 0), post_want,
engine.post_elapsed());
std::fflush(stderr);
}
engine.disarm();
model_.trigger_armed().store(false, std::memory_order_relaxed);
}
@@ -437,6 +463,11 @@ void Acquisition::on_data(const udps_frame_t* frame) {
last_stamp_ = last_stamp_used;
model_.trigger_armed().store(engine.armed(), std::memory_order_relaxed);
// The edge goes out before the flag that makes the UI read it (release/acquire on
// `trigger_pending`), so a tick that sees a capture open sees which edge it is for. A
// capture that opens and latches inside this one frame never reports pending, and that
// is right: the display goes straight to the frozen event.
if (engine.pending()) model_.set_capture_edge(engine.last_trigger_time());
model_.set_trigger_pending(engine.pending());
static std::int64_t next_engine_log = 0;
const std::int64_t now_e = static_cast<std::int64_t>(frame->recv_time * 1.0e6);
+6 -4
View File
@@ -98,11 +98,13 @@ ExportTable collect_export(ScopeModel& model, RowSet& rows) {
: std::string("ch") + std::to_string(c);
};
// What the rows bind to is what gets exported, which is the same branch `RowSet::bind()`
// takes: the frozen event in trigger mode, the live ring otherwise. Reading the ring
// while an event is on screen would export data the user cannot see.
// What the rows bind to is what gets exported, so the split is the same `DisplayMode`
// `RowSet::bind()` switches on: the frozen event only while one is held, the live ring
// otherwise -- including mid-capture, where the rows follow the live ring too. Reading
// the ring while an event is on screen would export data the user cannot see, and
// reading the stale snapshot mid-capture would export the *previous* event.
std::shared_ptr<const EventSnapshot> snap = model.event();
if (model.trigger_enabled() && snap != nullptr) {
if (model.display_mode() == DisplayMode::Held && snap != nullptr) {
// The snapshot is immutable and already index-aligned, so the visible window is two
// binary searches over its stamps and the same index range for every channel.
const std::vector<f32>& st = snap->stamps;
+3
View File
@@ -350,6 +350,9 @@ void ScopeModel::publish_event(double pre_s, double post_s, double edge) {
snap->pre_s = snap->stamps.empty() ? 0.0 : -static_cast<double>(snap->stamps.front());
snap->post_s = snap->stamps.empty() ? 0.0 : static_cast<double>(snap->stamps.back());
event_.store(std::move(snap), std::memory_order_release);
// Bumped after the snapshot is in place, so a consumer that sees the new sequence
// number sees the snapshot it counts.
event_seq_.fetch_add(1, std::memory_order_release);
}
void BufferStore::set_epoch(f64 epoch) noexcept {
+48 -7
View File
@@ -127,6 +127,21 @@ struct StreamStats {
std::atomic<bool> have_config{false};
};
/// What the plot is showing, which is one decision made once per frame: the bind, the
/// export and the status wording all have to agree, and the producer can latch between
/// any two of them.
///
/// `Capturing` is the live-updating capture: an edge has been accepted (so the pre
/// window was full) and the post window is still filling, and the plot follows the live
/// ring on an edge-relative axis until the snapshot lands. It is a deliberate departure
/// from SPECS:37 ("not refreshed till next event") -- see README.
enum class DisplayMode {
Live, ///< no trigger: the live ring, stream clock
Capturing, ///< an edge was accepted: the live ring, edge-relative clock
Held, ///< the frozen snapshot of the last event
Waiting, ///< armed, nothing captured yet: blank
};
/// The application's data model: the stream description, the two sample buffers,
/// the trigger configuration and the counters. Structural changes (a CONFIG
/// update, an enable/disable, a calibration) rebuild the buffers, so all mutating
@@ -191,6 +206,11 @@ class ScopeModel {
std::shared_ptr<const EventSnapshot> event() const noexcept {
return event_.load(std::memory_order_acquire);
}
/// How many snapshots `publish_event()` has produced. Not interchangeable with
/// `trigger_latches()`: a Disarm during a capture publishes what the capture got to
/// *without* an event having fired, so this counts "there is a new picture to frame"
/// while the latch count keeps answering "how many events fired".
std::uint64_t event_seq() const noexcept { return event_seq_.load(std::memory_order_acquire); }
/// Publishes the live buffer's new samples to its `ChannelBuffer`s.
void acquire_all();
@@ -211,16 +231,35 @@ class ScopeModel {
}
void set_trigger_enabled(bool on);
/// Whether the engine is mid-capture (an edge was seen and the post-trigger
/// window is still filling). Purely informational: it drives the status line's
/// wording. The plot deliberately does *not* react to it -- in trigger mode the
/// picture changes only on a latch (SPECS: "data visualisation should not be
/// refreshed till next event").
/// Whether the engine is mid-capture (an edge was seen -- so the pre window was
/// full -- and the post-trigger window is still filling). This is what puts the
/// display in `DisplayMode::Capturing`: the plot follows the live ring, on an axis
/// pinned to the coming event's window, until the snapshot replaces it.
///
/// Release/acquire, not relaxed: `capture_edge()` is written before this is set and
/// has to be visible to a consumer that sees the flag.
bool trigger_pending() const noexcept {
return trigger_pending_.load(std::memory_order_relaxed);
return trigger_pending_.load(std::memory_order_acquire);
}
void set_trigger_pending(bool on) noexcept {
trigger_pending_.store(on, std::memory_order_relaxed);
trigger_pending_.store(on, std::memory_order_release);
}
/// The stream time of the edge of the capture `trigger_pending()` refers to. Written
/// by the producer before it raises the flag, so a consumer that sees the flag sees
/// this too; stale once the flag clears.
f64 capture_edge() const noexcept { return capture_edge_.load(std::memory_order_relaxed); }
void set_capture_edge(f64 e) noexcept {
capture_edge_.store(e, std::memory_order_relaxed);
}
/// What the plot should bind to this frame. Read it *once* per frame and pass the
/// answer around: calling it twice can straddle a latch, and then the bind and the
/// axis disagree about which clock they are on.
DisplayMode display_mode() const noexcept {
if (!trigger_enabled()) return DisplayMode::Live;
if (trigger_pending()) return DisplayMode::Capturing;
return event() != nullptr ? DisplayMode::Held : DisplayMode::Waiting;
}
/// The x origin the plot uses: 0 for "seconds since the stream started", or minus
@@ -283,7 +322,9 @@ class ScopeModel {
f64 epoch_ = 0.0;
std::atomic<double> event_span_{1.0};
std::atomic<bool> trigger_pending_{false};
std::atomic<double> capture_edge_{0.0};
std::atomic<std::shared_ptr<const EventSnapshot>> event_; ///< the event on screen
std::atomic<std::uint64_t> event_seq_{0};
std::atomic<double> display_epoch_{0.0};
std::atomic<bool> trigger_armed_{false};
std::atomic<bool> trigger_enabled_{false};
+15 -9
View File
@@ -143,7 +143,7 @@ int RowSet::channel_of(int row_index, int slot) const {
return row(row_index).channel[static_cast<std::size_t>(slot)];
}
void RowSet::bind(ScopeModel& model) {
void RowSet::bind(ScopeModel& model, DisplayMode mode) {
const std::vector<SignalInfo>& signals = model.signals();
const std::vector<ChannelInfo>& channels = model.channels();
for (int i = 0; i < kMaxRows; ++i) {
@@ -181,20 +181,26 @@ void RowSet::bind(ScopeModel& model) {
r.slots[si].bind(nullptr);
continue;
}
// In trigger mode the trace shows the captured event and nothing else: the
// last one latched, or nothing at all before the first. Never the live ring,
// not even while the post window is filling -- a frozen picture that only
// changes on an event is the whole point of trigger mode, and binding the
// live ring mid-capture made the plot scroll for most of every event cycle.
// The source is the frame's one `DisplayMode`, decided by the caller: `Held` is
// the frozen snapshot, `Waiting` is blank (armed, nothing captured yet), and both
// `Live` and `Capturing` are the live ring -- what differs between those two is
// the *clock*, which the display epoch carries, not the binding.
const std::size_t ci = static_cast<std::size_t>(channel);
if (model.trigger_enabled()) {
switch (mode) {
case DisplayMode::Held:
if (auto snap = model.event()) {
r.slots[si].bind_event(std::move(snap), ci);
} else {
r.slots[si].bind(nullptr); // armed, waiting for the first event: blank
r.slots[si].bind(nullptr);
}
} else {
break;
case DisplayMode::Waiting:
r.slots[si].bind(nullptr);
break;
case DisplayMode::Live:
case DisplayMode::Capturing:
r.slots[si].bind(model.live().shared_channel(ci));
break;
}
}
}
+7 -4
View File
@@ -117,10 +117,13 @@ class RowSet {
/// The channel index a row/slot holds, or -1.
int channel_of(int row, int slot) const;
/// Rebinds every slot to what it should show this frame: the model's live buffer,
/// or -- in trigger mode -- the captured event. Called every frame, because the
/// channel list is renumbered by any structural change.
void bind(ScopeModel& model);
/// Rebinds every slot to what `mode` says it should show this frame. Called every
/// frame, because the channel list is renumbered by any structural change.
///
/// `mode` is passed in rather than read from the model, so that one frame's bind, axis
/// and status line all describe the same display: the producer can open or close a
/// capture between any two reads of `ScopeModel::display_mode()`.
void bind(ScopeModel& model, DisplayMode mode);
/// Publishes the live buffer's new samples.
void acquire(ScopeModel& model) { model.acquire_all(); }
+210 -32
View File
@@ -99,12 +99,26 @@ struct App {
/// Trigger-mode state for the status line: how many latches were on screen last
/// frame, and whether an event is held (which is what "frozen" means).
std::uint64_t seen_latches = 0;
/// Which snapshot was framed last. Separate from `seen_latches` because a Disarm during
/// a capture publishes a snapshot without an event having fired.
std::uint64_t seen_event_seq = 0;
bool event_held = false;
/// Monotonic time of the latch `seen_latches` counts, so the status line can age it.
/// Whether a capture is open on screen: the edge has been accepted and the post window
/// is filling in view, on the coming event's own clock. It is what keeps
/// `clamp_to_data()` off the pinned axis and tells the status line apart from "armed".
bool capturing = false;
/// The edge of the capture `capturing` refers to, so a second edge accepted before the
/// first capture closed re-frames instead of being mistaken for the same one.
double capture_edge = 0.0;
/// Monotonic time of the last event, and the edge that started the clock. The clock
/// starts when the *edge* is accepted, not when the capture closes -- that is the
/// instant the user saw -- and the edge is kept so the latch that ends a live-updated
/// capture does not restart it.
/// Wall time, not stream time: "how long since the last event" is a question about the
/// user's wait, and a stream whose clock runs at any other speed would answer it wrong.
/// 0 until the first latch of the session.
/// 0 until the first event of the session.
gint64 last_latch_us = 0;
double timed_edge = 0.0;
double window_choices_limit = -1.0; ///< the cap the dropdown was last built with
/// The rate each channel actually *stored* over the last second, and the declared rate
/// beside it. The app does no decimation, so the stored rate is the datasource's own
@@ -133,6 +147,10 @@ struct App {
int dbg_reset_visible = -1;
std::vector<std::uint64_t> last_stored;
std::uint64_t dbg_latches_at_disarm = 0;
std::uint64_t dbg_seq_at_disarm = 0;
bool dbg_capturing_at_disarm = false;
int dbg_disarm_polls = 0;
int dbg_disarm_in_capture = 0;
int dbg_swaps = 0;
int dbg_axes_w_min = 1 << 20;
int dbg_axes_w_max = -(1 << 20);
@@ -448,6 +466,7 @@ void on_arm_clicked(GtkButton* button, gpointer user) {
}
if (armed) {
app->event_held = false;
app->capturing = false;
app->model->trigger_time_valid().store(false, std::memory_order_relaxed);
}
// Arming is a statement that this signal should be watched, so its acquisition
@@ -457,8 +476,7 @@ void on_arm_clicked(GtkButton* button, gpointer user) {
gtk_button_set_label(GTK_BUTTON(app->arm_button), armed ? "Disarm" : "Arm");
}
void on_trigger_field_changed(GtkWidget*, gpointer user) {
App* app = static_cast<App*>(user);
void publish_trigger_fields(App* app) {
if (app->updating_panels) return;
// Read the widgets first (they may be touched again by a re-entrant handler if a
// setter ran while the lock was held), then publish the values under the lock the
@@ -481,6 +499,18 @@ void on_trigger_field_changed(GtkWidget*, gpointer user) {
}
}
void on_trigger_field_changed(GtkWidget*, gpointer user) {
publish_trigger_fields(static_cast<App*>(user));
}
/// The same publish, for the two dropdowns. It needs its own entry point because
/// `notify::<prop>` passes a `GParamSpec*` ahead of the user data while
/// `GtkSpinButton::value-changed` does not: a two-parameter callback on a `notify::`
/// signal reads the pspec as its `App*` and crashes on the first field it touches.
void on_trigger_drop_changed(GObject*, GParamSpec*, gpointer user) {
publish_trigger_fields(static_cast<App*>(user));
}
void on_trigger_signal_changed(GObject* obj, GParamSpec*, gpointer user) {
App* app = static_cast<App*>(user);
if (app->updating_panels) return;
@@ -897,6 +927,7 @@ void on_trigger_toggled(GtkToggleButton* button, gpointer user) {
signal = tc.channel;
}
app->event_held = false;
app->capturing = false;
if (!on) {
app->model->trigger_time_valid().store(false, std::memory_order_relaxed);
app->model->set_display_epoch(0.0); // back to seconds since the stream started
@@ -1317,12 +1348,30 @@ gboolean tick(gpointer user) {
}
if (model.generation() != app->signal_generation) rebuild_signal_panels(*app);
// What the display shows, decided once for the whole frame: the producer can open or
// close a capture between any two reads of the model, and the bind, the epoch, the pin
// and the status line all have to describe the same picture.
const DisplayMode display = model.display_mode();
const double cap_edge = model.capture_edge();
const bool new_capture =
display == DisplayMode::Capturing && (!app->capturing || cap_edge != app->capture_edge);
app->capturing = display == DisplayMode::Capturing;
if (new_capture) app->capture_edge = cap_edge;
{
// Serialise against the acquisition thread's CONFIG handling and against the
// UI's own structural changes: the bind takes shared handles into the live
// store, and acquire() reads it.
std::lock_guard<std::mutex> lock(model.mutex());
app->rows->bind(model);
// Mid-capture the live ring is drawn on the coming event's clock, so its edge sits at
// x = 0 exactly as a snapshot's does and the pinned [-pre, +post] axis means the same
// thing in both. Every other mode reads stream seconds. The epoch is a consumer-side
// offset on the live channels, so it has to be in place before the bind that hands
// them to the traces -- and it is written here, under the lock, because a rebuild can
// replace the store.
const double want_epoch = display == DisplayMode::Capturing ? -cap_edge : 0.0;
if (model.display_epoch() != want_epoch) model.set_display_epoch(want_epoch);
app->rows->bind(model, display);
app->rows->acquire(model);
}
@@ -1331,11 +1380,12 @@ gboolean tick(gpointer user) {
// the group and pushes it to the others, so the "which row acted" search this replaced
// (and the per-row pan/zoom propagation behind it) has no subject any more.
// While a captured event is on screen the window is pinned to [edge-pre,
// edge+post]; clamp_to_data would fight that pin whenever the bound series
// still reports the acquisition buffer's raw coordinates, dragging the view
// along the ring. Clamping is a live-view concern only.
if (!app->event_held) app->rows->clamp_to_data();
// While an event is on screen -- held, or still being captured -- the window is pinned
// to [-pre, +post]; clamp_to_data would fight that pin whenever the bound series still
// reports the acquisition buffer's raw coordinates, dragging the view along the ring.
// Clamping is a live-view concern only, and a capture is live data on a pinned axis,
// which is exactly the case it must not touch.
if (!app->event_held && !app->capturing) app->rows->clamp_to_data();
// Curator: the trigger's arm state can change under us (single mode disarms
// itself), so the button and the status line read the published atomics.
@@ -1345,11 +1395,55 @@ gboolean tick(gpointer user) {
}
const bool armed = model.trigger_armed().load(std::memory_order_relaxed);
gtk_button_set_label(GTK_BUTTON(app->arm_button), armed ? "Disarm" : "Arm");
if (new_capture) {
// The edge has been accepted, which means the pre window was already full, so the
// whole event window can be framed now and the post side filled in view.
//
// The split is the *requested* one -- the same `post_span_s()` the producer will cut
// with -- because there is no snapshot to read it off yet, and the marker has to sit
// where that cut will put it rather than where a different rounding would.
double span = 0.0;
double post = 0.0;
{
std::lock_guard<std::mutex> lock(model.mutex());
span = std::max(1.0e-3, model.event_span());
post = model.trigger().post_span_s(span);
}
app->event_held = false;
app->rows->pin_event_window(span - post, post);
app->rows->set_trigger_marker(0.0, true);
select_window_choice(*app, span);
// The user saw the event at its edge, so that is when the counter starts.
app->timed_edge = cap_edge;
app->last_latch_us = g_get_monotonic_time();
}
const std::uint64_t latches = model.trigger_latches().load(std::memory_order_relaxed);
if (latches != app->seen_latches) {
app->seen_latches = latches;
app->event_held = true;
const double edge = model.trigger_time().load(std::memory_order_relaxed);
// A capture the UI watched fill already started this clock at its edge; only one that
// opened and closed inside a single frame -- never reported pending, so never
// displayed as a capture -- starts it here.
if (edge != app->timed_edge) {
app->timed_edge = edge;
app->last_latch_us = g_get_monotonic_time();
}
}
// Framing the held event is keyed on *which snapshot was published*, not on the latch
// count: a Disarm during a capture publishes what the capture got to without an event
// having fired, and that partial has to be framed and held exactly like one that ran
// to term. The latch count stays the answer to "how many events fired", which is what
// the probes assert on.
const std::uint64_t seq = model.event_seq();
if (seq != app->seen_event_seq) {
app->seen_event_seq = seq;
// A publication and the *next* edge can land in the same frame. The display follows
// the newer of the two, so re-framing on the snapshot is only right when the snapshot
// is what is bound; otherwise the capture above has already framed its own window.
if (display == DisplayMode::Held) {
app->event_held = true;
// The window is pinned to exactly what the snapshot captured, read off the
// snapshot itself. Not off `event_span`/`pretrigger_pct`: those say what was
// *asked for*, in stream seconds, while how much the capture actually covers
@@ -1357,13 +1451,9 @@ gboolean tick(gpointer user) {
// request collapsed the waveform into a sliver at x = 0 and left the rest of
// the axis -- the whole post side included -- blank.
//
// The snapshot is edge-relative (the edge at x = 0), so the axis needs no epoch
// shift: snapshot, pin and marker all speak the same coordinates. Rebind first
// so the slots hold the snapshot before the pin is applied.
{
std::lock_guard<std::mutex> lock(model.mutex());
app->rows->bind(model);
}
// The snapshot is edge-relative (the edge at x = 0) and the epoch is back to 0, so
// the bind at the top of this frame already put the slots and the axis on the same
// coordinates the pin is about to state.
double pre = 0.0;
double post = 0.0;
if (auto snap = model.event()) {
@@ -1374,19 +1464,19 @@ gboolean tick(gpointer user) {
// Nothing was captured (no samples in the ring yet): fall back to the
// requested window so the axis is at least readable.
const double span = std::max(1.0e-3, model.event_span());
pre = std::min(0.99, model.trigger().pretrigger_pct / 100.0) * span;
post = span - pre;
post = model.trigger().post_span_s(span);
pre = span - post;
}
app->rows->pin_event_window(pre, post);
app->rows->set_trigger_marker(0.0, true); // the edge sits at x = 0
select_window_choice(*app, pre + post);
}
// The trigger instant is marked on every row while its event is the thing on
// screen, so the pre/post split around it can be read off the plot.
if (trigger_on && model.trigger_time_valid().load(std::memory_order_relaxed)) {
// x is seconds from the event: the display epoch is minus the edge time, so the
// edge sits at exactly x = 0 wherever the user has zoomed.
} else {
}
// The trigger instant is marked on every row while its event is the thing on screen --
// being captured or held. x is seconds from the event in both: the display epoch is
// minus the edge while the live ring is on a pinned axis, and the snapshot's own stamps
// are edge-relative, so the edge sits at exactly x = 0 wherever the user has zoomed.
if (!app->capturing && !(trigger_on && model.trigger_time_valid().load(std::memory_order_relaxed))) {
app->rows->set_trigger_marker(0.0, false);
}
@@ -1568,7 +1658,9 @@ gboolean tick(gpointer user) {
StreamStats& st = model.stats();
const char* mode = "live";
if (trigger_on) {
if (app->event_held) {
if (app->capturing) {
mode = "trigger: CAPTURING (post-trigger window filling)";
} else if (app->event_held) {
mode = "trigger: EVENT HELD (Arm to re-arm, or 'Go live' for streaming)";
} else if (armed && have_trigger_value) {
static char waiting[192];
@@ -1724,9 +1816,9 @@ GtkWidget* build_trigger_panel(App& app) {
g_signal_connect(w, "value-changed", G_CALLBACK(on_trigger_field_changed), &app);
}
g_signal_connect(app.trigger_edge_drop, "notify::selected",
G_CALLBACK(on_trigger_field_changed), &app);
G_CALLBACK(on_trigger_drop_changed), &app);
g_signal_connect(app.trigger_mode_drop, "notify::selected",
G_CALLBACK(on_trigger_field_changed), &app);
G_CALLBACK(on_trigger_drop_changed), &app);
return box;
}
@@ -2068,8 +2160,11 @@ gboolean trig_trace_probe(gpointer user) {
for (int s = 0; s < RowSet::kSlotsPerRow; ++s) {
const std::size_t si = static_cast<std::size_t>(s);
if (r.channel[si] < 0) continue;
// A live binding in trigger mode is only legitimate as a capture in progress, so
// the two are named apart: `live` with `trigger=on` is the defect, `live(capturing)`
// is the feature.
if (r.slots[si].bound_event() != nullptr) display = "event(frozen)";
else if (r.slots[si].bound() != nullptr) display = "live";
else if (r.slots[si].bound() != nullptr) display = a->capturing ? "live(capturing)" : "live";
shown = &r.slots[si];
found = true;
break;
@@ -2160,10 +2255,11 @@ gboolean trig_trace_probe(gpointer user) {
bound_hi, dx_nominal, dx_real,
dx_real > 0.0 ? dx_nominal / dx_real : 0.0, got_lo, got_hi, want_lo, want_hi);
std::fprintf(stderr,
"[trig-probe] trigger=%s pending=%d display=%s latches=%llu armed=%d "
"[trig-probe] trigger=%s pending=%d display=%s epoch=%.4f latches=%llu armed=%d "
"live_pub=%llu snap=[%.4f..%.4f] n=%zu axis=[%.4f..%.4f] "
"data=[%.4f..%.4f] drawn=[%.4f..%.4f]\n",
m.trigger_enabled() ? "on" : "off", int(m.trigger_pending()), display,
m.display_epoch(),
static_cast<unsigned long long>(m.trigger_latches().load()),
int(m.trigger_armed().load()),
static_cast<unsigned long long>(m.live().values(0).produced()), snap_lo, snap_hi,
@@ -2185,6 +2281,28 @@ gboolean arm_probe(gpointer user) {
return G_SOURCE_REMOVE;
}
/// Headless self-check: changes the trigger panel's two dropdowns, which is what a user
/// does to switch Normal/Single or the edge -- mid-capture, if the probe is timed into
/// one. Reaching the dropdowns directly is the point: their `notify::selected` handler
/// has a different signature from the spin buttons' `value-changed`, and connecting the
/// wrong one crashed on the first field read with no way to tell from the state dump.
/// A line here with no SIGSEGV after it is the assertion.
gboolean fields_probe(gpointer user) {
App* a = static_cast<App*>(user);
if (a->window == nullptr) return G_SOURCE_REMOVE;
GtkDropDown* mode = GTK_DROP_DOWN(a->trigger_mode_drop);
GtkDropDown* edge = GTK_DROP_DOWN(a->trigger_edge_drop);
gtk_drop_down_set_selected(mode, gtk_drop_down_get_selected(mode) == 0 ? 1 : 0);
gtk_drop_down_set_selected(edge, (gtk_drop_down_get_selected(edge) + 1) % 3);
std::lock_guard<std::mutex> lock(a->model->mutex());
std::fprintf(stderr, "[fields-probe] mode=%u edge=%u published mode=%d edge=%d capturing=%d\n",
gtk_drop_down_get_selected(mode), gtk_drop_down_get_selected(edge),
int(a->model->trigger().mode), int(a->model->trigger().edge),
int(a->model->trigger_pending()));
std::fflush(stderr);
return G_SOURCE_REMOVE;
}
/// Headless self-check: acquire every signal the CONFIG described, the way the
/// signal panel's switches would, and put each resulting channel on a plot, the way
/// a drag from the panel onto a row would. Without the second half nothing is bound
@@ -2214,6 +2332,8 @@ gboolean enable_all_probe(gpointer user) {
return G_SOURCE_REMOVE;
}
gboolean disarm_probe_step3(gpointer user);
gboolean disarm_probe_step2(gpointer user) {
App* a = static_cast<App*>(user);
if (a->window == nullptr) return G_SOURCE_REMOVE;
@@ -2226,15 +2346,70 @@ gboolean disarm_probe_step2(gpointer user) {
a->dbg_latches_at_disarm = a->model->trigger_latches().load();
std::fprintf(stderr, "[disarm-probe] %llu events so far, armed now=%d\n",
(unsigned long long)a->dbg_latches_at_disarm, int(a->model->trigger().armed));
a->dbg_capturing_at_disarm = a->model->trigger_pending();
a->dbg_seq_at_disarm = a->model->event_seq();
g_timeout_add(600, disarm_probe_step3, a);
return G_SOURCE_REMOVE;
}
/// Headless self-check, the half that reads the *outcome* of the Disarm. A disarm that
/// landed mid-capture (`was_capturing=1`) must leave an event on screen with the
/// capture's own span: `display` reading `nothing` there is the "the event completely
/// disappeared" defect, and `seq` must have advanced by one while `events` has not.
gboolean disarm_probe_step3(gpointer user) {
App* a = static_cast<App*>(user);
if (a->window == nullptr) return G_SOURCE_REMOVE;
const DisplayMode d = a->model->display_mode();
auto snap = a->model->event();
RowSet::Row& r = a->rows->row(0);
const plot::Range axis = r.plot != nullptr ? r.plot->x_range(r.axis_x) : plot::Range{0.0, 0.0};
std::fprintf(stderr,
"[disarm-probe] was_capturing=%d display=%s seq=%llu->%llu events=%llu->%llu "
"snap=[%.4f..%.4f] n=%llu axis=[%.4f..%.4f]\n",
int(a->dbg_capturing_at_disarm),
d == DisplayMode::Held ? "event(frozen)"
: d == DisplayMode::Live ? "live"
: d == DisplayMode::Capturing ? "live(capturing)"
: "nothing",
(unsigned long long)a->dbg_seq_at_disarm,
(unsigned long long)a->model->event_seq(),
(unsigned long long)a->dbg_latches_at_disarm,
(unsigned long long)a->model->trigger_latches().load(),
snap ? -snap->pre_s : 0.0, snap ? snap->post_s : 0.0,
(unsigned long long)(snap ? snap->stamps.size() : 0), double(axis.min),
double(axis.max));
std::fflush(stderr);
return G_SOURCE_REMOVE;
}
/// Waits for a capture to actually be open before clicking Disarm. The interesting case
/// is the Disarm that lands *mid-capture*, and a fixed delay catches it only by luck --
/// the capture opens as soon as the ring holds a pre window and closes one post window
/// later. Polling for `trigger_pending()` makes the probe deterministic.
gboolean disarm_probe_wait(gpointer user) {
App* a = static_cast<App*>(user);
if (a->window == nullptr) return G_SOURCE_REMOVE;
++a->dbg_disarm_polls;
if (a->model->trigger_pending()) ++a->dbg_disarm_in_capture;
// 300 ms into the capture: post-trigger data worth keeping, well short of the latch.
// The 10 s cap is for a stream that never triggers -- the probe still has to report.
if (a->dbg_disarm_in_capture >= 6 || a->dbg_disarm_polls > 200) {
if (a->dbg_disarm_in_capture == 0) {
std::fprintf(stderr, "[disarm-probe] no capture opened in 10 s; disarming anyway\n");
std::fflush(stderr);
}
disarm_probe_step2(a);
return G_SOURCE_REMOVE;
}
return G_SOURCE_CONTINUE;
}
gboolean disarm_probe_step1(gpointer user) {
App* a = static_cast<App*>(user);
if (a->window == nullptr) return G_SOURCE_REMOVE;
if (a->trigger_toggle != nullptr)
gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(a->trigger_toggle), TRUE);
g_timeout_add(2000, disarm_probe_step2, a);
g_timeout_add(50, disarm_probe_wait, a);
return G_SOURCE_REMOVE;
}
@@ -2350,6 +2525,9 @@ int run_ui(int argc, char** argv, ScopeModel& model, Acquisition& acq, RowSet& r
if (const char* env = g_getenv("UDPSCOPE_ARM_TRIGGER_MS")) {
g_timeout_add(static_cast<guint>(std::atoi(env)), arm_probe, &app);
}
if (const char* env = g_getenv("UDPSCOPE_PROBE_FIELDS_MS")) {
g_timeout_add(static_cast<guint>(std::atoi(env)), fields_probe, &app);
}
if (const char* env = g_getenv("UDPSCOPE_TRIG_TRACE_MS")) {
g_timeout_add(static_cast<guint>(std::atoi(env)), trig_trace_probe, &app);
}
+8
View File
@@ -0,0 +1,8 @@
1. better time window management:
- instead of a drop down simple combo should be an advanced sub menu with text input + unit selector (us,ms,s,min) + two button to increase / decrease (factor 10 each time)
- when `timewindow x sample rate x sample size` is greater than `buffer size` apply downsampling of input signals with low pass filter to match the correct freq: e.g. downsample at 100kSps -> low pass filter at 50kHz
2. improve layout management: instead of a fix number of figure, in the menu add a layout editor option, user can add and remove rows ([x] close to the top left of the row to remove, [+] in the top center and bottom center to add new one [above] and [below])
3. improve plot font rendering and line rendering
4. improve plot performances with large data (e.g. at 10MSamples the plot start to be laggy)