improved perfs and event management

This commit is contained in:
Martino Ferrari committed 2026-10-13 23:00:42 +02:00
1 parent 667ed0c100
commit bb99be5889
17 files changed
+626 -59

No files matched your search

+44 -3
View File
@@ -29,6 +29,21 @@ window the toolbar offers, default 600), `--holdoff` (default trigger hold-off),
added and removed from the **Layout editor** afterwards), `--relative-time` (see below),
`--capture-packets N` (see "Packet capture" below).
**What a large `--buffer-mb` costs at startup, and what the window says while it happens.** The
buffer's rings are value-initialised, so allocating one touches every byte of `--buffer-mb` --- about
a second per gigabyte, mostly first-touch page faults --- and it happens on the producer thread
holding the lock the frame tick also takes. It is therefore a real freeze and not a slow paint. Two
things follow. First, a rebuild whose plan describes the storage already allocated **clears** the
store instead of allocating a new one, which is what most structural changes are: startup used to ask
for fifteen allocations and now asks for two or three (3.36 s → 1.18 s at `--buffer-mb 1000`), and
every enable of a signal is applied in one batch rather than re-planning per signal. Second, the
window is **presented before** the buffer is planned and carries a notice naming what it is waiting
for --- `connecting to 127.0.0.1:8000 — planning the 4000 MB buffer` --- because the remaining second
or so at a multi-gigabyte buffer is unavoidable, and a blank window that says nothing is the part of
it that reads as a hang. Each allocation also names itself on stderr
(`[udpscope] buffer allocated: 4 channels, 839 s span, 74 ms`), so a run that prints a column of them
is a re-plan that should have been coalesced.
Every CONFIG is dumped to stderr once -- publish mode and, per signal, its name,
element count, rate, time mode and time-signal index -- because a signal that
refuses to plot is almost always a signal the stream described in an unexpected
@@ -109,6 +124,14 @@ export's second column reads — and it fixed a latent bug on the way, the expor
having added `trigger_time()` to a *paused* view's stamps: an unrelated edge, or none at all before
the first latch.
**An axis with nothing bound to it is still `[-window, 0]`.** "No samples yet" is the state every
structural change leaves behind — opening an event or a waveform re-plans the rings, which drops
their history, and the axis is then framed before the first sample of the new view lands. Framing it
at `[0, window]` there (which is what it did) offsets the view by a whole window under the x link
group's follow rule, so the two or three frames after every open drew the time base one screen to the
left of where the following frames drew it: a trace that jumps sideways and comes back, on every open.
The empty axis is now drawn in the same place as the full one.
Two things move in **opposite** directions under a rolling axis, and the asymmetry is deliberate.
A **cursor** stays on screen: placed at -1.5 s it keeps meaning "1.5 s ago" and the data flows
under it, which is a bench scope's roll behaviour and is free here, the cursors already being
@@ -523,9 +546,15 @@ empty column beside the plot is what *off* must not look like. Recording is swit
panel**, with the trigger's other options rather than in the pane whose contents it decides, and it is
**off by default** -- writing every latch to disk is the user's decision about their disk.
Each row carries **its own picture** beside the numbers -- time, duration, signal count, rate -- in a
fixed 126x42 cell (the preview's own 3:1), so the picture is the same size in every row whatever the
pane's width. Clicking a row **replays** it: the same loader `--open` uses, on the event's file. **The layout does
Each row carries **its own picture** beside the numbers -- time, duration, signal count, rate -- 138
pixels wide whatever the pane's width, and as tall as that event's own proportions make it: the
preview is one 44-pixel strip per signal, so a one-signal event is a short row and a six-signal one a
tall one (capped, so that a row stays a row). **Hovering it shows the whole preview at full size**,
through a tooltip -- the small picture says *which* event this is, the zoom says what is in it.
Clicking a row **replays** it: the same loader `--open` uses, on the event's file. The click **leaves
the list alone** -- your scroll position and the pair you had selected for a comparison both survive
it, because marking which event is on screen rewrites the rows' titles in place instead of rebuilding
the list. **The layout does
not change** -- the rows, which signal sits in which slot, the colours, the row count, the time window,
the cursors and the measurements all stay exactly where they are, and now read the stored event instead
of the stream; that is what "show me that event" asks for, and rebinding the rows to the file would
@@ -562,6 +591,8 @@ Three things worth knowing:
trigger edge as a brighter line -- not captured from the window: the plot is a `GtkGLArea`, and a
GL area's ink is not in the widget's paint tree, so a snapshot of it is empty. A data-drawn
preview is also the honest one: it shows the samples, not the zoom the window happened to be at.
It is a plain 24-bit BMP, written bottom-up with **blue first** — both are BMP's own order, and
writing RGB there is what made every external viewer show the traces in the wrong colour.
- **The picture is decoded synchronously** (`gdk_texture_new_from_filename()` into
`gtk_picture_set_paintable()`), not through `gtk_picture_set_filename()`: that path uses the image
loader's sandboxed, asynchronous decoder, which here fails silently -- the picture keeps a
@@ -569,9 +600,19 @@ Three things worth knowing:
It is then **scaled once to the row's size** and cached by file: a `GtkPicture`'s natural size is
its content, so the stored 480-pixel preview given straight to a row claims the row's whole width
and squeezes the text out of it -- a size request on the picture is a floor, not a cap.
- **A picture that is missing or will not decode is drawn again from the samples.** The `.npy` is the
event; the `.bmp` is only a picture of it, so a write that failed, a truncated file or a cache from
an older build is repaired rather than shown as a blank row. Once per row: a second failure is a
real one.
- **A replay stops the acquisition** (a live producer would overwrite the loaded samples within a
frame), so the status bar names the file until *Live* starts a fresh client and thread. The
replayed samples age out of the ring as the stream refills it.
- **Opening an event, a comparison or going back to *Live* is one re-plan of the buffer**, not one
per channel. Each of those switches rebuilds the signal set, and a rebuild that moves the plan
allocates --- and so touches --- the whole of `--buffer-mb`; enabling a comparison's eight channels
one at a time was eight of those, which is most of what made the switch feel slow. They are now
enabled in a single call, and a rebuild whose plan is unchanged clears the buffer instead of
allocating a new one.
- The switch in the pane's header stops the recording; the cache is only useful if it was already
on when the event you wanted happened, which is why it defaults to on.