fix: a paused view carries its own LOD pyramid, so it is drawn like a held event
publish_pause_view() filled stamps, values and y and left pyramids empty, so level_for() descended to raw samples and a frozen pause was re-gathered from every sample it held on every frame. It cost draw time (12.34 -> 18.18 ms mean) and, because stage_trace_density() normalises each column against its own peak, it also changed how the trace looked -- which is how it was noticed. Both publishes are now timed and reported as `publish cost:`, counted apart because they stall opposite threads: the event publish runs on the producer under mutex() and blocks the UI, the pause publish runs on the UI thread and blocks the producer. That is the number a second live ring has to be judged against. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
1 parent
13574bfcef
commit
d2c51f2448
5 files changed
+175
-4
No files matched your search
@@ -719,6 +719,27 @@ Two lines of the self-check output are assertions in disguise:
|
||||
whole window fits in the raw path and the publish is too short to lose a race. (Do not
|
||||
raise `--batch` to chase the rate: a batch of 20000 exceeds the datagram limit and the mock
|
||||
dies with `OSError: [Errno 90] Message too long`.)
|
||||
- `publish cost: N events, A/B ms (mean/worst, blocks the UI); M pauses, C/D ms (mean/worst,
|
||||
blocks the producer)` — what *freezing a view* costs, and the line that attributes the stall
|
||||
`frame cost:`'s `tick` max can only hint at. The two publishes are counted apart because they
|
||||
stall **opposite threads**, and nothing else in the dump distinguishes them:
|
||||
`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.
|
||||
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.
|
||||
- `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 <flags> mode=.. axis=[..] n=.. idx=[..) spp=.. level=.. pts=.. kept=..
|
||||
@@ -1138,6 +1159,21 @@ far the largest file.
|
||||
is built once in `publish_event()` inside the `O(n)` pass that already computes the y
|
||||
bounds, and the same channel on two rows shares it. It costs ≈1 byte per sample per
|
||||
channel. `frame path:`'s `segments` is the guard.
|
||||
**"Every" includes the pause**, and that is the half this invariant was written without and
|
||||
which then went wrong for real: `publish_pause_view()` filled `stamps`, `values` and `y` and
|
||||
left `pyramids` empty, so a *paused* view was re-gathered raw on every frame. Both halves of
|
||||
the cost are readable, and the second is the one a performance line cannot show.
|
||||
`frame path:` read `raw=21 lod=118` over a 1.6 s pause against `raw=1 lod=170` for the same run
|
||||
live, with the draw mean 12.34 → 18.18 ms — and the **trace changed appearance**, which is what
|
||||
gets reported. `stage_trace_density()` grades a column against *its own peak* from a `1/height`
|
||||
deposit per segment, so a column fed ~660 raw inter-sample segments accumulates a far spikier
|
||||
distribution, and a far larger peak to normalise against, than one fed the one or two bucket
|
||||
envelopes the pyramid gives: the paused trace read as a hard bright core with its dwell tail
|
||||
dimmed out. **A pause is not a cheaper case than a capture — it freezes the same rings**, so
|
||||
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.
|
||||
- **`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
|
||||
|
||||
Reference in new issue
Block a user