feat: the x axis is relative to now, and an export carries both clocks
Three asked-for changes, one commit because they are one coordinate system. The live axis is a fixed [-window, 0] with the newest sample at 0, so x reads as an age and the labels stand still while the trace rolls under them. That makes the display clock distinct from the stream clock for the first time, so `ScopeModel::stream_at_x0()` becomes the single conversion between them (`display_x = stream_t - stream_at_x0()` in all four modes) and a snapshot carries its own origin in `EventSnapshot::x_offset`. The pause view is now rebased like an event's, reversing an earlier deliberate choice: a frozen absolute-stamped picture would sit a whole uptime right of a newest-relative axis. That also fixed a latent bug, the export adding `trigger_time()` to a *paused* view's stamps. The status bar says how long you have been paused -- wall time, since the producer never stops, through `format_elapsed()` so the field cannot widen. The export writes two time columns: `time`, the axis as displayed, and `abs_seconds`, the stream's own clock for the same sample. f64 seconds and not the wire's integer, because `udps_frame_t::values` is a `const double*` and the integer never exists in this process. Cursors and measurement marks move in opposite directions under the rolling axis, deliberately: a cursor is a position on screen and stays put, a mark names a sample and travels with it. `App::measure_window` is stream-clocked for a sharper reason -- a display-coordinate live window never moves, so the "the window moved" recompute would never fire again. Moves the deps/plotter pin to 58473db, which stops a time label's mantissa reaching its own radix: a one-second tick differenced off a live axis read `1e+3ms`. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
1 parent
2f4530c128
commit
b313cac46e
18 files changed
+501
-145
No files matched your search
@@ -72,9 +72,14 @@ it, and `LineProgram::set_snap()` / `u_snap` rounds an axis-aligned segment's in
|
||||
whole pixels for the chrome that frames a plot (grid and axis lines), never for anything
|
||||
drawn at a data position. `generate_ticks()` also stores the tick at the origin as
|
||||
exactly `0.0` — the walk starts at `ceil(min/stride)*stride` and so never lands on zero
|
||||
when the stride is not a power of two, which labelled it `-139a`. All three carry
|
||||
when the stride is not a power of two, which labelled it `-139a`. `format_time()` picks its
|
||||
unit rung by the **rounded** mantissa (`t < u.scale * 0.9995`, and the same slack on the
|
||||
whole-second branch) — the `>= 999.5` spill guard `format_si()` in the same file always had —
|
||||
because a tick a hair under a rung's top rounds to 1000 at three significant figures and
|
||||
`%.3g` writes that `1e+03`, which is how a one-second tick read `1e+3ms`. All four carry
|
||||
upstream tests — `the_atlas_is_filtered_not_point_sampled`,
|
||||
`grid_hairlines_land_on_whole_pixels` and `the_tick_at_the_origin_reads_zero`. The minor
|
||||
`grid_hairlines_land_on_whole_pixels`, `the_tick_at_the_origin_reads_zero` and
|
||||
`format_time_never_lets_a_field_reach_its_radix`. The minor
|
||||
grid's look is set there too, not here: `grid_minor` carries 50% alpha, `grid_minor_width` is
|
||||
0.75 (sub-pixel, expressed as coverage at 1x) and `grid_minor_dash_on`/`_off` are 3/3 — a
|
||||
fragment discard, no extra geometry — so the subdivisions read as texture under the labelled
|
||||
@@ -208,8 +213,8 @@ finding.
|
||||
since `set_time_window()` frames the newest span before the pin clamps it — both are expected.
|
||||
- `UDPSCOPE_PROBE_PAUSE_MS` — pause acquisition mid-run so layout numbers are
|
||||
measured on a still picture. It now also asserts what a pause *is*
|
||||
(`[pause-probe] <action> paused=D ring_produced=N event_size=M frozen_x=[..] axis=[..]` on
|
||||
stderr):
|
||||
(`[pause-probe] <action> paused=D ring_produced=N event_size=M frozen_x=[..] axis=[..]
|
||||
glance=[..]` on stderr):
|
||||
it presses the **button**, not `Acquisition::set_paused()`, because pausing is a display
|
||||
state as well as an acquisition one and the view snapshot and axis pin live in the
|
||||
handler. Across the three lines **`ring_produced` must grow** (the producer never
|
||||
@@ -218,6 +223,17 @@ finding.
|
||||
the ring) and **`frozen_x` must not move between the pause and the resume**; after the
|
||||
resume `event_size` is 0 and the window is back on the newest data. Pausing used to stop
|
||||
the producer, which is why the counts grew by nothing and the resume left a hole.
|
||||
`frozen_x` is the *rebased* snapshot — the pause view's own stamps are relative to the freeze
|
||||
instant now, so the range reads `[-window..0]`-ish rather than stream seconds, and a
|
||||
`frozen_x` in the thousands is the rebase gone.
|
||||
**`glance=` is the assertion for the paused status field**, and this is the only line that can
|
||||
make it: the exit dump's `status bar:` is read live and its two `mode …` lines are the live and
|
||||
triggered states. While paused it must read `paused · <rate> · <window> · for <elapsed>`, with
|
||||
the elapsed figure **growing between the two paused lines** and **absent on the resume line**.
|
||||
It is **wall** time (`g_get_monotonic_time()`), not stream time, because the producer never
|
||||
stops while paused and "how long have I been looking at this" is a question about the user, not
|
||||
about the stream; it is written through `format_elapsed()` so the field steps to a coarser unit
|
||||
instead of growing digits (see the layout rule).
|
||||
- `UDPSCOPE_EXPORT_MS` (+ optional `UDPSCOPE_EXPORT_PATH`, default
|
||||
`/tmp/udpscope-export`) — collect the export table and write it both ways, skipping
|
||||
only the file chooser.
|
||||
@@ -258,7 +274,11 @@ Two lines of the self-check output are assertions in disguise:
|
||||
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
|
||||
`epoch=` is **minus the newest acquired stamp** in `live` and `nothing` — it is what makes the
|
||||
live axis `[-window, 0]`, so there it grows (in magnitude) with the stream and a `0.0000` is
|
||||
the defect, not the healthy reading. It is back to `0.0000` only in `event(frozen)`, where the
|
||||
snapshot carries its own origin (`EventSnapshot::x_offset`) and the display epoch has nothing
|
||||
left to do. 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
|
||||
@@ -305,19 +325,24 @@ Two lines of the self-check output are assertions in disguise:
|
||||
`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,
|
||||
- `[export-probe] mode=.. axis=[a..b] rows=N cols=M time=[c..d] abs=[..]` (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
|
||||
is the *visible* range at the *stored* rate, so a count far short of that is a table
|
||||
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 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 wrong clock (the live branch puts the window back on the stream's clock with
|
||||
`stream_at_x0()` before searching the ring, and that offset is a whole window while
|
||||
streaming — not the 0 it once was).
|
||||
**`abs - time` must be a single constant across both ends of a line**, and that constant is
|
||||
where the display's origin sits in the stream's own clock: `epoch()` plus the newest stamp in
|
||||
`mode=live`, `epoch()` plus the accepted edge in `mode=event`. That is the one assertion on
|
||||
the second column, and it replaces the old "`stream=` must be the edge ± the window" reading,
|
||||
which only ever held because the display clock *was* the stream clock. A `mode=live` line
|
||||
whose `abs - time` differs between the two ends is the two columns computed from different
|
||||
epochs — the defect `abs_base` exists as a single `const` to prevent.
|
||||
An export taken **mid-capture** reports `mode=live` with `time=[-pre..e]` where `e` is how far
|
||||
the post window has filled — 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
|
||||
@@ -401,11 +426,17 @@ Two lines of the self-check output are assertions in disguise:
|
||||
(a min/max showing both its marks counts two of its own). `measures window:` is the
|
||||
timeliness assertion -- the window the numbers were computed over against the one on screen.
|
||||
**The reading that must hold is `marks_in_window=K/K`**: every *time* mark inside the window
|
||||
on screen. `drift` itself is a **single sample of a sawtooth** and is not an assertion on its
|
||||
on screen. Both ranges are on the **stream's** clock, which is the space `measure_window` and
|
||||
the time marks are stored in -- a live axis is a fixed `[-window, 0]`, so comparing display x
|
||||
would read no drift whatever the data did.
|
||||
`drift` itself is a **single sample of a sawtooth** and is not an assertion on its
|
||||
own: a live view scrolls between recomputes, so it rises to one `kMeasureRefreshUs` of scroll
|
||||
and drops to zero at each pass. On a 2 s window at the 100 ms cadence the top of that sawtooth
|
||||
is `drift=5%`, which is what a dump landing just before a scheduled pass reads -- a `0.0000%`
|
||||
is a dump that landed just after one, not a better state. Read it against the recompute count:
|
||||
and drops to zero at each pass. The figure is `(|Δmin| + |Δmax|) / span`, so a pure scroll
|
||||
counts **twice** and the top of the sawtooth is `2 × refresh / window`: ~10 % on a 2 s window
|
||||
at the 100 ms cadence, and a few *hundred* percent on a window shorter than the interval (a
|
||||
50 ms window reads 254 % for 63.6 ms of scroll, and its ceiling is 400 %). Compute the ceiling
|
||||
for the window the run actually used before calling a figure large. A `0.0000%`
|
||||
is a dump that landed just after a pass, not a better state. Read it against the recompute count:
|
||||
*more* recomputes with a *higher* drift sample is the sawtooth, while a drift that exceeds one
|
||||
refresh interval of scroll, or marks outside the window, is the defect. A measurement is
|
||||
recomputed when the window moves (2 % of its span) or the held/live state changes, never on a
|
||||
@@ -443,12 +474,17 @@ Two lines of the self-check output are assertions in disguise:
|
||||
row-level `set_time_window()` does nothing** (a probe that changes the window must use the
|
||||
app's path). **`distinct` must be 1 on every line, and that is the assertion** — it says no two
|
||||
major labels on the axis are the same string, which is the one thing the relative form exists to
|
||||
buy and the only reading that holds whatever the stream's uptime is. `relative=D` deliberately
|
||||
is *not* fixed per window: the form is decided by the tick step against the label resolution at
|
||||
the axis' **magnitude** (see the invariant below), so the same 50 ms window reads absolute at
|
||||
2 s of uptime (`2.13s, 2.14s, 2.15s`) and relative at 17 s (`100µs, 10.1ms, 20.1ms`, with the
|
||||
caption) — both correct, and a probe pinned to one of them would assert the stream's clock
|
||||
rather than the app. The **caption must be non-empty exactly when `relative=1`**. Run it after
|
||||
buy and the only reading that holds whatever the stream's uptime is. `relative=D` is **expected
|
||||
to read 0 on every line now**, and that is a consequence of the newest-relative axis rather than
|
||||
a change to the rule: the form is decided by the tick step against the label resolution at the
|
||||
axis' **magnitude** (see the invariant below), and no display mode carries a large magnitude any
|
||||
more — live and waiting are `[-window, 0]`, a capture and a held event are edge-relative, a pause
|
||||
is freeze-relative. The 50 ms window that used to read absolute at 2 s of uptime (`2.13s, 2.14s,
|
||||
2.15s`) and relative at 17 s (`100µs, 10.1ms, 20.1ms`, with the caption) now reads
|
||||
`-40ms, -30ms, -20ms, -10ms` at any uptime. The rule and the caption are kept because they are
|
||||
the answer for *any* axis magnitude and nothing in them is mode-specific — do not delete them on
|
||||
the strength of this reading — but what the probe asserts is `distinct`, not the form. The
|
||||
**caption must be non-empty exactly when `relative=1`**, which here means empty throughout. Run it after
|
||||
`UDPSCOPE_ARM_TRIGGER_MS` as well, which is the case the span rule got wrong: a triggered 50 ms
|
||||
window must read `relative=0 note=[] ticks=[-20ms,-10ms,0,10ms]` — a `t0` caption on an axis
|
||||
that is already edge-relative re-anchors the ticks away from the trigger, which is the one
|
||||
@@ -571,7 +607,7 @@ Two lines of the self-check output are assertions in disguise:
|
||||
reconnects; `config=0`), and check it does **not** fire against a live source. The status
|
||||
line carries ` | N reconnects` only when N > 0, which is the same counter without the
|
||||
prose.
|
||||
- **The export's `time`/`stream_time` columns must be strictly increasing and evenly
|
||||
- **The export's `time`/`abs_seconds` columns must be strictly increasing and evenly
|
||||
spaced.** Check the written file, not just the probe: `np.diff(t)` with no `<= 0` and
|
||||
`rows / (t[-1] - t[0])` equal to the stored rate (the `ch` line's figure). Duplicate or
|
||||
backwards stamps mean the recorded timeline holds two clocks or a collapsed cadence —
|
||||
@@ -579,11 +615,17 @@ Two lines of the self-check output are assertions in disguise:
|
||||
README) and a per-sample step derived from the client's `lost` counter (now the
|
||||
stream's measured arrival cadence). A `rows=` count far below the span times the rate
|
||||
is the other signature, that of a table cut from the LOD pyramid.
|
||||
**`set(np.diff(t))` must hold exactly one value**, and this is the check that needs a long
|
||||
run at a high rate: a *plateau* of equal stamps, or a diff taking two or three values a few
|
||||
hundred nanoseconds apart, is the time ring losing precision with uptime (see the `f64`
|
||||
time-ring invariant below — the figure to compute is the ULP of `t[0]`, since the defect
|
||||
grows with how long the app has run, not with the export's own length).
|
||||
**The step must hold to a few ULP of the column's own magnitude**, and this is the check that
|
||||
needs a long run at a high rate: the old form of this assertion was "`set(np.diff(t))` holds
|
||||
exactly one value", and that was only true while `time` *was* the stored stamp. Both columns
|
||||
are derived now — `time` is `stamp - stream_at_x0()` and `abs_seconds` is that plus a base of a
|
||||
wholly different magnitude — so each subtraction costs a rounding and `set()` legitimately
|
||||
holds a handful of neighbouring doubles. Measured: four values spanning ~1.5 ULP, 6.8e-16 s.
|
||||
The thing that must not come back is the **plateau**: equal stamps, or a diff taking two or
|
||||
three values a few *hundred nanoseconds* apart, is the time ring losing precision with uptime
|
||||
(see the `f64` time-ring invariant below). The figure to compute is the ULP of `t[0]` and of
|
||||
`abs_seconds[0]` — the defect that invariant guards is microseconds, nine orders of magnitude
|
||||
above the arithmetic noise, so the two readings cannot be confused.
|
||||
- `badge frames: lit=L dim=D idle=I` — the activity badge's phase per frame. Over a run
|
||||
that acquired anything **both `lit` and `dim` must be non-zero**: that is what "it
|
||||
blinks" means, and a badge stuck lit is indistinguishable from a working one in a
|
||||
@@ -685,7 +727,7 @@ Two lines of the self-check output are assertions in disguise:
|
||||
it afterwards.
|
||||
- `[keys] a: armed 0->1 | space: paused 0->1 | t: trigger panel: 0->1 | … | editable focused: 'm' refused=1` (stderr, `UDPSCOPE_PROBE_KEYS_MS`) — **every `X->Y` pair must differ** and **`refused` must be 1**. The refusal is the whole reason the controller is in the bubble phase: every bare-letter accel is also a character, so an accel that fires while a `GtkEditable` has focus makes the window entry unusable. A pair reading `0->0` is an action whose dispatch does not reach the handler a click reaches. Note the format is `<key>: <widget-description>: <before>-><after>` — the name passed to `before_after()` includes the widget's description, so `t: trigger panel: 0->1` is the correct form (not `t: trigger panel 0->1` without the colon before the values).
|
||||
- `[zoomkeys] ctrl+plus x=[..]->[..] y_held=1 | alt+plus y=[..]->[..] x_held=1` (stderr, `UDPSCOPE_PROBE_ZOOMKEYS_MS`) — **both `_held` must be 1 and both zooms must still happen.** Run it inside a pinned state, where a frame cannot re-frame the other axis for its own reasons: `UDPSCOPE_PROBE_PAUSE_MS=1500 UDPSCOPE_PROBE_ZOOMKEYS_MS=2200`. A `_held=0` is the modifier not reaching the plotter's controller; an unchanged range on the *zoomed* axis is the opposite failure, the key doing nothing. Ctrl restricts to x and Alt to y; Ctrl+Alt zooms neither (no special case needed — neither flag alone fires).
|
||||
- `status bar: glance=[● live · 1.09 MS/s · 2 s window] max_glance_chars=53 detail_fields=11 popover_rebuilds=0` (exit dump) — **`max_glance_chars` must stay small** (under about 60) and **`popover_rebuilds` must be 0**. The status row is directly above the plot, so this is the "layout must not depend on content" rule read from the bar's own side: the state field is one word from a fixed set, the rate field is `%.3g` (so `10 kS/s`, never `10.0 kS/s`), and the only field trigger mode adds is the elapsed time, which steps units rather than growing digits. `detail_fields` is the popover's label count, a literal, so adding a label without thinking about layout shows up here. The glance rate format matters: `%.3g` drops the trailing zero that a `%.4g` or a fixed-precision format would keep — a rate that reads differently in the dump and in the help page is a format disagreement, and `14-status.txt`'s example must match it.
|
||||
- `status bar: glance=[● live · 1.09 MS/s · 2 s window] max_glance_chars=53 detail_fields=11 popover_rebuilds=0` (exit dump) — **`max_glance_chars` must stay small** (under about 60) and **`popover_rebuilds` must be 0**. The status row is directly above the plot, so this is the "layout must not depend on content" rule read from the bar's own side: the state field is one word from a fixed set, the rate field is `%.3g` (so `10 kS/s`, never `10.0 kS/s`), and the two fields a *state* adds are both elapsed times — trigger mode's `last event … ago` and a pause's `for <elapsed>` — which step units rather than growing digits (`format_elapsed()`). A **paused** glance is not in the dump at all: it is read by `[pause-probe]`'s `glance=` field, since the dump's own line is taken live. `detail_fields` is the popover's label count, a literal, so adding a label without thinking about layout shows up here. The glance rate format matters: `%.3g` drops the trailing zero that a `%.4g` or a fixed-precision format would keep — a rate that reads differently in the dump and in the help page is a format disagreement, and `14-status.txt`'s example must match it.
|
||||
|
||||
The probes are the regression net — when changing UI or plot-host behaviour, extend
|
||||
the probe/dump in `src/ui.cpp` rather than adding a separate harness.
|
||||
@@ -721,13 +763,16 @@ immutable copy of the captured window; see README for why two live rings were dr
|
||||
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.
|
||||
- `Live` — no trigger: the live ring, on the clock of the **newest sample**
|
||||
(`display_epoch() == -newest`), so the axis is a fixed `[-window, 0]` and x reads as an age.
|
||||
- `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.
|
||||
- `Held` — the frozen `EventSnapshot` of the last event, whose stamps are already edge-relative,
|
||||
so the display epoch is 0.
|
||||
- `Waiting` — armed, nothing captured yet: blank, with the live epoch (there is nothing bound,
|
||||
but the axis is still a live one).
|
||||
|
||||
**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
|
||||
@@ -750,6 +795,35 @@ and hands it to `bind()`). The producer can open or close a capture between any
|
||||
reads, and two reads in one frame put the bind, the epoch, the pin and the status line on
|
||||
different clocks.
|
||||
|
||||
**The display clock is not the stream clock, and `ScopeModel::stream_at_x0()` is the only
|
||||
conversion.** `display_x = stream_t - stream_at_x0()` in **every** mode, and the offset lives in
|
||||
one of two places: a snapshot carries its own (`EventSnapshot::x_offset`, frozen with the
|
||||
picture) and a live view carries it as minus `display_epoch()`. Anything crossing between the
|
||||
two clocks goes through that one accessor rather than picking the branch itself — the export's
|
||||
`abs_seconds` column, the pause handler putting a display window back on the ring to cut it, the
|
||||
measurement marks — because there are four modes and picking the branch by hand gets one of them
|
||||
wrong. It reads atomics only, so it is safe to call inside an existing `mutex()` guard.
|
||||
Three things about the live epoch specifically:
|
||||
- **It is written after `bind()` and `acquire_all()`, not before.** It must be minus the newest
|
||||
sample the *traces* can report, and asking before the acquire is one frame of production
|
||||
stale — at 1 MS/s over a 50 ms window that is a third of the screen to the right of the axis.
|
||||
- **There is no feedback loop**, even though the newest stamp is read through the epoch that is
|
||||
about to change: `RowSet::live_edge()` reports in the epoch currently in force, so
|
||||
`display_epoch() - live_edge()` is minus the *raw* stamp and the fixed point is reached in one
|
||||
frame rather than converged on.
|
||||
- **`Waiting` takes the live epoch too.** Nothing is bound, but the axis is still a live one, and
|
||||
an epoch of 0 there would make the axis jump on the frame a capture closes.
|
||||
|
||||
**A measurement mark is glued to its sample; a cursor stays on screen.** The two instruments move
|
||||
in opposite directions under a rolling axis and that is deliberate — a bench scope's cursors sit
|
||||
where the user put them while the waveform rolls under, and a measurement's mark names the sample
|
||||
it measured. The cursors need no code: their x *is* a display coordinate. The marks are stored in
|
||||
**stream** seconds and reconverted every frame by `place_measure_marks()`, since a mark left in
|
||||
display coordinates would crawl forward at one window per window. `App::measure_window` is
|
||||
stream-clocked for the same reason, and that one is load-bearing: the recompute trigger is "the
|
||||
window moved by 2 % of its span", and in display coordinates a live window is a fixed
|
||||
`[-window, 0]` — it never moves, so the measurements would never be recomputed again.
|
||||
|
||||
Layers to keep straight:
|
||||
|
||||
- `SignalInfo` — one signal as the CONFIG described it. The unit of enable/disable.
|
||||
@@ -1017,12 +1091,15 @@ far the largest file.
|
||||
the doubling against overflow); `live_window_locked()` compares its bounds in the ring's own
|
||||
type with **no rounding step**, the old `f32` round-trip having existed only so that a
|
||||
sample sitting exactly on a bound was kept whatever the stream's absolute time was; and
|
||||
`EventSnapshot::stamps` is `f64` because a **pause** view is deliberately *not* rebased and
|
||||
therefore carries full stream time (an event's own stamps are edge-relative and small — the
|
||||
symmetry is not the reason). `RingBuffer<T>` stores slots as `std::atomic<T>` behind a
|
||||
`static_assert(is_always_lock_free)`, which `double` satisfies on every supported target.
|
||||
The guard is the **export**: `np.diff(t)` over a multi-million-row `.npy` must hold exactly
|
||||
one distinct step. Three steps at 2, 2.5 and 3 ULP of the stamps' magnitude is this defect.
|
||||
`EventSnapshot::stamps` is `f64` because it is cut from this ring and is therefore in the same
|
||||
units, whatever a given snapshot's own origin is (both kinds are rebased now — an event's
|
||||
stamps are edge-relative, a pause's freeze-relative — so neither is magnitude-bound the way the
|
||||
ring is, and the width is for the cut rather than for the coordinate). `RingBuffer<T>` stores
|
||||
slots as `std::atomic<T>` behind a `static_assert(is_always_lock_free)`, which `double`
|
||||
satisfies on every supported target. The guard is the **export**: over a multi-million-row
|
||||
`.npy`, `np.diff(t)` must hold one step to within a few ULP of the column's magnitude (see
|
||||
the export guard above for why it is no longer exactly one value). Three steps at 2, 2.5 and
|
||||
3 ULP of *the stream's uptime* — microseconds, not femtoseconds — is this defect.
|
||||
- **A dropped time slot is clamped, never answered with a sentinel.** `acquire()` pins
|
||||
the published window to the value ring's base and both rings keep sliding afterwards —
|
||||
their capacities are equal, so there is no slack — which means the oldest few samples
|
||||
|
||||
Reference in new issue
Block a user