Expand description
Complete, contiguous sample blocks and a fill-side builder.
Block is deliberately a payload, not a queue. Compose it with the
transport whose overload policy matches the application:
EventBuf<Block<T, N>, Q>queues up toQcomplete blocks and rejects the newest block when full;LatestBuf<Block<T, N>>retains only the latest complete block (decision D3 composition).
Publication cannot expose a partial block because BlockBuilder only
yields a Block after all N samples have been written. Dropping or
clearing a partially filled builder publishes nothing.
Timestamps are payload policy: use a timestamped sample type for T when
each sample needs a stamp. The transport does not impose one.
§Known limitation: sequence-span aliasing
The contiguity check compares u32 sequence values and nothing else, and
the successor skips reserved 0, so sequence identity is modular over the
2^32 - 1 nonzero span — the same counter-width boundary the transports
disclose. A partial builder held while upstream omits exactly one whole
span of sequences (or any whole multiple) sees the recurring value as the
expected successor and completes the block as “contiguous” despite
~4.29 billion omitted samples; the gap-rejection promise (F2) is exact
only below one span. Reachability: the omission must span 2^32 - 1
sequences while the same partial builder stays live — ~71.6 minutes of
outage at a sustained 1 MHz sample rate, ~5 days at 10 kHz. The chosen
policy is to keep the sequence one word and disclose the bound rather than
carry a wider epoch: recovery from outages is the application’s job —
clear() the builder when your staleness watchdog or link-layer detects a
gap it cannot bound below one span.
§Costs and integration (measured; bound by decisions D3 and P)
RAM is multiple complete blocks, always. The latest composition
(LatestBuf<Block<T, N>>) holds three block slots plus this private
builder — 136–8,280 bytes of combined channel + builder RAM across the
measured 2/8/16-byte × N = 8/32/128 grid. The queued composition
stores Q blocks plus the builder. The cost is fixed in size but lives
wherever you place the value: a const-constructed static lands in
.bss (no flash image, no startup copy); a local consumes stack,
and at up to 8,280 bytes per combined shape that is a real stack
budget, not a rounding error. It is not small either way: state the
number for your shape and charge it to the right budget.
Small windows can invert the economics. Continuous block release
beats N individual sample publications for every measured 2-byte row
and for 8/16-byte samples at N >= 32, but costs 54% / 31% more at
the 8/16-byte N = 8 corners. For tiny windows, per-sample
publication through a plain channel may be the cheaper shape.
Publication cost scales with block bytes — 150–8,651 reference instructions across the measured grid — and rejection is within 2–25 instructions of acceptance, because the complete rejected block is preserved and returned rather than reduced to a scalar error. Budget rejection like acceptance, not like error plumbing.
DMA integrations: the double copy is currently unavoidable in ISR context. A DMA engine has already written the samples once, and this builder’s storage is deliberately private — the public API offers no address or writable slice a DMA controller could target — so filling the builder from the DMA buffer crosses the payload a second time. Either budget both copies against the accepted row for your shape, or publish from task context where the copy is off the interrupt path. A direct-to-granted-slot fill API is exactly the registered reopening condition of cycle decision S (the deferred SlotPool foundation) — a real adopter with this requirement reopens that lane rather than prying the builder open. (DMA cache maintenance remains outside this crate, per the taxonomy’s out-of-scope list.)
No partial block is ever visible — sample-level freshness inside a filling window is unobtainable by design, stated here so it is chosen, not discovered.
The measured rows behind these numbers live in
docs/proposals/block-buf-measurements.md and the joint composition
matrix; the decision record is docs/records/block-buf.md.
§Example
use ph_eventing::{BlockBuilder, EventBuf};
let mut fill = BlockBuilder::<i16, 4>::new();
for (sequence, sample) in [(10, 1), (11, 2), (12, 3)] {
assert!(fill.push(sequence, sample).expect("contiguous").is_none());
}
let block = fill.push(13, 4).expect("contiguous").expect("complete");
let queue = EventBuf::<_, 2>::new();
let producer = queue.try_producer().expect("producer");
let consumer = queue.try_consumer().expect("consumer");
// Backpressure is returned, never unwrapped: a full queue hands the
// complete block back through `Err` for the caller's policy.
assert!(producer.push(block).is_ok());
assert_eq!(consumer.pop().expect("one block queued").samples(), &[1, 2, 3, 4]);A zero-sized block is rejected at compile time:
use ph_eventing::BlockBuilder;
const BAD: BlockBuilder<u8, 0> = BlockBuilder::new();Structs§
- Block
- A complete, contiguous block of
Nsamples. - Block
Builder - Privately fills one block and publishes it to the caller only when complete.
Enums§
- Fill
Error - Why a sample could not be appended to a
BlockBuilder.