diff --git a/CLAUDE.md b/CLAUDE.md index ab758f4..a6c2350 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -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 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 - and the pause figure against `gaps`. Measured on the fast mock over a 3 s run: one event at - **115 ms** and one pause at **22.3 ms** — the event figure is far above the ~40 ms the `tick` - entry above quotes, so treat that 40 as the old reading and this line as the authority. + and the pause figure against `gaps`. Both are **wide**: on the fast mock the event publish has + read **69, 89, 92 and 115 ms** and the pause publish **22 and 86 ms** across runs, so treat the + 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 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 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 - make the trade less obvious than the memory arithmetic suggests, and both were measured rather - than reasoned: the event publish really is ~115 ms of blocked UI, and **a pause costs `gaps` - where a run without one reads `gaps=0`** (73 with the pause against 0 without). That second - finding is *not* the publish length — it read 79 gaps with the pyramid build removed and 73 with - it, i.e. it does not scale with the stall — so it is **unexplained**, and anyone pricing a second - ring should find out what it actually is first rather than assume the ring would fix it. + `--buffer-mb 200` really is 200 MB) and a second per-sample write on the producer. What the line + says about that trade is that the stall is real but **costs no packets**: an A/B of the same run + with and without a pause read `gaps=0` both ways, the paused one with an 86 ms publish, so a + second ring would buy a smoother frame at a latch rather than recovered data. (An earlier run + reported `gaps=73` with a pause against 0 without, which is what made the stall worth measuring; + it has not reproduced and did not scale with the publish length — 79 gaps with the pyramid build + 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 drawn-vs-quoted D px over R raw paints` plus a `[ink] paint N 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 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 - ~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.** 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 diff --git a/README.md b/README.md index 642ec9b..26a899d 100644 --- a/README.md +++ b/README.md @@ -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 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 -pause lasts, which over a 1.6 s pause at ~60 fps repays thirty times over. One finding there -is still open: a run that pauses reports `gaps=73` where the same run without a pause reads -`gaps=0`, and it does **not** scale with the publish length (79 gaps with the pyramid build -removed, 73 with it), so the stall is not what it is. +pause lasts, which over a 1.6 s pause at ~60 fps repays thirty times over. The producer +absorbs it: an A/B of the same fast-mock run with and without a pause read `gaps=0` both +ways, with the pause publish at 86 ms in the paused one. (One earlier run reported `gaps=73` +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 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 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 - go to zero. They are worth knowing before pricing that — one event publish measured - **115 ms of blocked UI tick** at 1.3 MS/s and one pause publish **22 ms of blocked + go to zero. They are worth knowing before pricing that — at 1.3 MS/s an event publish + 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 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 - that pauses reports `gaps=73` against `gaps=0` without a pause, and that does not - scale with the publish length, so it is not the stall and a second ring would not - obviously fix it. + an event, a pause and an export's window at once. The *observable* cost is also + smaller than the stall: an A/B with and without a pause read `gaps=0` both ways, so + what a second ring would buy is a smoother frame at a latch, not recovered packets. - **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 *budget*, and the power-of-two ring rounding means the final diff --git a/src/model.cpp b/src/model.cpp index 2a31597..b70a9f5 100644 --- a/src/model.cpp +++ b/src/model.cpp @@ -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 // 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 - // thirty times over. The gaps a pause costs are **not** this: they measured 79 without the - // pyramid and 73 with, so they are the pause's own and still unexplained. + // thirty times over. The producer absorbs the stall: `gaps=0` with and without a pause, the + // paused run having published in 86 ms. auto& pyr = snap->pyramids[c]; pyr.reserve(take); for (f32 v : vals) {