docs: a pause publish costs no packets, and the look is checked with frame path

An A/B of the same fast-mock run with and without a pause reads gaps=0 both
ways, the paused one having published in 86 ms -- so the gaps=73 recorded
yesterday as "a pause costs packet gaps, unexplained" was incidental load on
one run. It never scaled with the publish length and has not reproduced.

Record the publish figures as the range they actually span across runs
(event 69-115 ms, pause 22-86 ms) rather than as single readings, and name
`frame path:` across a pause as the check that the paused trace is drawn like
a live one -- raw=1 lod=555 against raw=0 lod=373, where the defect read
raw=21 lod=118.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Martino FerrariandClaude Opus 4.6 committed 2026-10-06 20:45:13 +02:00
1 parent d2c51f2448
commit f307d27e41
3 files changed
+37 -23

No files matched your search

+19 -11
View File
@@ -726,20 +726,22 @@ Two lines of the self-check output are assertions in disguise:
`publish_event()` runs on the producer with `mutex()` already held, so its cost lands on the UI `publish_event()` runs on the producer with `mutex()` already held, so its cost lands on the UI
as a long tick; `publish_pause_view()` runs on the UI thread and takes the lock, so its cost as a long tick; `publish_pause_view()` runs on the UI thread and takes the lock, so its cost
lands on the producer and can only surface as `gaps`. Read the event figure against `tick`'s max lands on the producer and can only surface as `gaps`. Read the event figure against `tick`'s max
and the pause figure against `gaps`. Measured on the fast mock over a 3 s run: one event at and the pause figure against `gaps`. Both are **wide**: on the fast mock the event publish has
**115 ms** and one pause at **22.3 ms** — the event figure is far above the ~40 ms the `tick` read **69, 89, 92 and 115 ms** and the pause publish **22 and 86 ms** across runs, so treat the
entry above quotes, so treat that 40 as the old reading and this line as the authority. line as an order of magnitude and not a figure to compare a change against at the millisecond.
The event reading matters anyway because it is far above the ~40 ms the `frame cost:` entry above
quotes — treat that 40 as the old reading and this line as the authority.
It is also **the number to judge a second live ring against**, which is the one design question It is also **the number to judge a second live ring against**, which is the one design question
it exists to answer: freezing a whole `BufferStore` copies nothing and inherits the pyramid a it exists to answer: freezing a whole `BufferStore` copies nothing and inherits the pyramid a
`ChannelBuffer` already maintains incrementally, so both `worst` figures would go to **0** — at `ChannelBuffer` already maintains incrementally, so both `worst` figures would go to **0** — at
the price of a second budget's worth of memory (README records that one live ring is why the price of a second budget's worth of memory (README records that one live ring is why
`--buffer-mb 200` really is 200 MB) and a second per-sample write on the producer. Two things `--buffer-mb 200` really is 200 MB) and a second per-sample write on the producer. What the line
make the trade less obvious than the memory arithmetic suggests, and both were measured rather says about that trade is that the stall is real but **costs no packets**: an A/B of the same run
than reasoned: the event publish really is ~115 ms of blocked UI, and **a pause costs `gaps` with and without a pause read `gaps=0` both ways, the paused one with an 86 ms publish, so a
where a run without one reads `gaps=0`** (73 with the pause against 0 without). That second second ring would buy a smoother frame at a latch rather than recovered data. (An earlier run
finding is *not* the publish length — it read 79 gaps with the pyramid build removed and 73 with reported `gaps=73` with a pause against 0 without, which is what made the stall worth measuring;
it, i.e. it does not scale with the stall — so it is **unexplained**, and anyone pricing a second it has not reproduced and did not scale with the publish length — 79 gaps with the pyramid build
ring should find out what it actually is first rather than assume the ring would fix it. removed against 73 with it — so it was incidental load on one run. Do not quote it as a cost.)
- `trace ink: N paints checked, C collapsed, S staircase, H short, worst cover P%, worst - `trace ink: N paints checked, C collapsed, S staircase, H short, worst cover P%, worst
drawn-vs-quoted D px over R raw paints` plus a drawn-vs-quoted D px over R raw paints` plus a
`[ink] paint N <flags> mode=.. axis=[..] n=.. idx=[..) spp=.. level=.. pts=.. kept=.. `[ink] paint N <flags> mode=.. axis=[..] n=.. idx=[..) spp=.. level=.. pts=.. kept=..
@@ -1173,7 +1175,13 @@ far the largest file.
anything `publish_event()` does to a snapshot, `publish_pause_view()` owes too. Building it is anything `publish_event()` does to a snapshot, `publish_pause_view()` owes too. Building it is
not free (see `publish cost:` below): it took the pause publish from 6.15 ms to 22.31 ms, a not free (see `publish cost:` below): it took the pause publish from 6.15 ms to 22.31 ms, a
one-off producer stall against ~5 ms a frame of draw for the pause's whole length, which at one-off producer stall against ~5 ms a frame of draw for the pause's whole length, which at
~60 fps repays about thirty times over on a 1.6 s pause. ~60 fps repays about thirty times over on a 1.6 s pause — and the producer absorbs it without
losing packets (`gaps=0` with and without a pause, the paused run at an 86 ms publish).
**The check that the look is restored is `frame path:` across the pause, not the cost line**:
a paused run must read the same path as a live one — measured `raw=1 lod=555 columns=555
graded=555` against `raw=0 lod=373 columns=373 graded=373`, same 24711-segment worst frame,
segment mean within 5 % — where the defect read `raw=21 lod=118`. A `raw` in the tens over a
pause is this returning, and it is the appearance as much as the cost.
- **`EventSeries::gather()` emits a bucket's envelope, not every `2^level`-th sample.** - **`EventSeries::gather()` emits a bucket's envelope, not every `2^level`-th sample.**
Point decimation hides exactly the spike the capture was triggered on; a scope has to Point decimation hides exactly the spike the capture was triggered on; a scope has to
draw a bucket's extremes. It needs no index rebasing, unlike the ring: the snapshot's draw a bucket's extremes. It needs no index rebasing, unlike the ring: the snapshot's
+16 -10
View File
@@ -995,10 +995,17 @@ cheaper case than a capture — it freezes the same rings.**
It is not free, and the trade is one-off against per-frame. Building the pause pyramid took It is not free, and the trade is one-off against per-frame. Building the pause pyramid took
that publish from 6.15 ms to 22.31 ms, and unlike the event publish this one runs on the that publish from 6.15 ms to 22.31 ms, and unlike the event publish this one runs on the
**UI** thread and so blocks the *producer* — against ~5 ms a frame of draw for as long as the **UI** thread and so blocks the *producer* — against ~5 ms a frame of draw for as long as the
pause lasts, which over a 1.6 s pause at ~60 fps repays thirty times over. One finding there pause lasts, which over a 1.6 s pause at ~60 fps repays thirty times over. The producer
is still open: a run that pauses reports `gaps=73` where the same run without a pause reads absorbs it: an A/B of the same fast-mock run with and without a pause read `gaps=0` both
`gaps=0`, and it does **not** scale with the publish length (79 gaps with the pyramid build ways, with the pause publish at 86 ms in the paused one. (One earlier run reported `gaps=73`
removed, 73 with it), so the stall is not what it is. with a pause against 0 without, which is why the stall was worth measuring at all — but it
has not reproduced, it did not scale with the publish length, and an 86 ms publish costing
nothing is the stronger reading. Treat that 73 as incidental load on one run.)
The verification that the *look* is restored is the same A/B read through `frame path:`:
`raw=0 lod=373 columns=373 graded=373` without a pause against `raw=1 lod=555 columns=555
graded=555` with one — the same path, the same 24711-segment worst frame and a segment mean
within 5 % — where before the fix the paused run read `raw=21 lod=118`.
The self-check's two `frame cost:` / `frame path:` lines are the guard: they split a frame The self-check's two `frame cost:` / `frame path:` lines are the guard: they split a frame
into the tick, the plot's own draw and the whole GTK paint, and report the segment counts into the tick, the plot's own draw and the whole GTK paint, and report the segment counts
@@ -1765,14 +1772,13 @@ now — they are listed so they are not mistaken for anything else:
latch after arming with no pre-trigger history. What a second ring would buy is the latch after arming with no pre-trigger history. What a second ring would buy is the
publish: freezing a whole `BufferStore` copies nothing and inherits the pyramid the publish: freezing a whole `BufferStore` copies nothing and inherits the pyramid the
producer already maintains, so both figures in the dump's `publish cost:` line would producer already maintains, so both figures in the dump's `publish cost:` line would
go to zero. They are worth knowing before pricing that — one event publish measured go to zero. They are worth knowing before pricing that — at 1.3 MS/s an event publish
**115 ms of blocked UI tick** at 1.3 MS/s and one pause publish **22 ms of blocked measured **69–115 ms of blocked UI tick** and a pause publish **22–86 ms of blocked
producer** — but they do not pay for a second budget's worth of memory, a second producer** — but they do not pay for a second budget's worth of memory, a second
per-sample write on the producer, and a single frozen slot where the app today holds per-sample write on the producer, and a single frozen slot where the app today holds
an event, a pause and an export's window at once. One thing to settle first: a run an event, a pause and an export's window at once. The *observable* cost is also
that pauses reports `gaps=73` against `gaps=0` without a pause, and that does not smaller than the stall: an A/B with and without a pause read `gaps=0` both ways, so
scale with the publish length, so it is not the stall and a second ring would not what a second ring would buy is a smoother frame at a latch, not recovered packets.
obviously fix it.
- **The buffer is allocated at the first CONFIG, not at initialisation.** - **The buffer is allocated at the first CONFIG, not at initialisation.**
The spec's "N+1 split" needs N, which only a CONFIG gives; `--buffer-mb` is the The spec's "N+1 split" needs N, which only a CONFIG gives; `--buffer-mb` is the
*budget*, and the power-of-two ring rounding means the final *budget*, and the power-of-two ring rounding means the final
+2 -2
View File
@@ -565,8 +565,8 @@ void ScopeModel::publish_pause_view(double lo_s, double hi_s, double x_offset) {
// It is not free, and the trade is one-off against per-frame: building it took the pause // It is not free, and the trade is one-off against per-frame: building it took the pause
// publish from 6.15 ms to 22.31 ms (`publish cost:`), which blocks the *producer* -- against // publish from 6.15 ms to 22.31 ms (`publish cost:`), which blocks the *producer* -- against
// ~5 ms a frame of draw for as long as the pause lasts, which at ~60 fps a 1.6 s pause repays // ~5 ms a frame of draw for as long as the pause lasts, which at ~60 fps a 1.6 s pause repays
// thirty times over. The gaps a pause costs are **not** this: they measured 79 without the // thirty times over. The producer absorbs the stall: `gaps=0` with and without a pause, the
// pyramid and 73 with, so they are the pause's own and still unexplained. // paused run having published in 86 ms.
auto& pyr = snap->pyramids[c]; auto& pyr = snap->pyramids[c];
pyr.reserve(take); pyr.reserve(take);
for (f32 v : vals) { for (f32 v : vals) {