The link tap¶
The link tap records what crosses a WireWeaver link: every frame as it crossed the medium (a USB packet, a UDP
datagram, an RTT chunk), every ww_link message as it went down to the framer or came back up, and the link events
the bytes don't show (connected, link ready, disconnected, errors). A decoder links frames and messages back
together, so you see which packet a request went out in, which packets a long reply was split over, and what a lost
packet took with it. On top of it sit ww monitor, ww stats, ww capture and ww explain FILE
(CLI reference).
$ ww monitor --layer frame
0.049465 L0 → frame #3 64 B: Full Data0 12 B, Full Data0 12 B, Full Data0 12 B, Full Data0 12 B, Start Data0 6/12 B
0.049311 L0 → Data0 12 B msg #3 in #3@2+12 Request Call add seq 1
..
0.053181 L0 ← frame #5 64 B: Start Data0 62/304 B
0.053186 L0 ← frame #6 64 B: Continue Data0 62/242 B
..
0.053194 L0 ← Data0 304 B msg #11 in #5..#9 (5 frames) Event Value blob seq 9 reply to #11 in 2.18 ms
Where it records¶
The tap is the ww_tap crate (no_std). A transport calls its Tap at the two layers it has: frame(dir, bytes)
and message(dir, kind, bytes); the event loop adds link_event. The tap only copies bytes and stamps a time:
linking, statistics and decoding happen in the consumer, so the link pays for a copy and nothing more.
- Host. Every
wire_weaver_clientevent loop records into aww_tap::Recorder: the process-widetap::default_recorder(), or the one named withClientConfig::tap(recorder). Nothing is copied while nobody subscribes.Command::RegisterTracerand itsTraceEvents are an adapter over the same records. - Device.
ww_device'sFramedTx::with_tap/FramedRx::with_taptake a tap; the default isNoTap, whose calls are empty and inline, so firmware without a tap is the same size as before (checked on an STM32G0 build).ww_frameritself has no callbacks:ww_framer::pieces(frame)walks a frame into its pieces with the same head and checksum codeFramedRxuses, and the decoder re-runs the framer on the recorded frames.
Batched into blocks¶
The Recorder batches records into pooled blocks of 64 KiB (RecorderConfig::block_size) and hands a block over
when it is full or RecorderConfig::flush_time (10 ms by default) after its first record, so a live monitor still
feels live while a fast link costs one hand-over per block, not per message. Blocks go back to the pool once every
consumer dropped them: the steady state allocates nothing. When a consumer falls so far behind that every block is
held, records are dropped and counted, and the next block on the link opens with a LinkEvent::Lost { records }: a
gap is always visible, and the link itself is never held up.
Subscribe with Recorder::subscribe(layers, queue) (a bounded queue of blocks; a consumer that falls further behind
misses blocks, seen as a gap in their numbers) or Recorder::subscribe_fn. Layers::MESSAGES leaves frames out,
Layers::ALL has them.
Records¶
The records are ordinary WireWeaver types: streams of the ww_tap::LinkTap trait, one entry per link
(link[id]) with the streams session (medium, framer, frame size, peer, the device's DeviceInfo once the link is
set up), events, tx_frames, rx_frames, tx_messages, rx_messages and, on a server, client_map (which
client's request a device-side request was). A capture is described by the API bundle of ww_tap like any device
log is by its device's, and every frame and message has a number, so the decoder sees a missing one.
Capture files¶
A capture is tape blocks appended back to back. ww_tape::TapeWriter writes it the way databases
write their logs: a new file as NAME.partial, renamed when finished, the file and its directory synced after the
create and after the rename; the data synced by a SyncPolicy (none, every N blocks, or an interval, 1 s by default:
fdatasync on Linux, F_FULLFSYNC on macOS, FlushFileBuffers on Windows); and a failed write or sync is the end of
the file, never retried (after a failed fsync Linux may drop the dirty pages, and a retry succeeds without them).
ww_tape::FileBlocks reads the longest valid prefix: it stops at the first block with a bad magic, length or CRC,
a cut one or one whose seq goes back, and says where; zeros at the end are the end. Recovery::Resync reads on past
damage instead. Splitting a capture into several files and preallocating space are not built yet. The describe blocks carry the device's API
bundle, so ww explain FILE explains the requests and replies of a capture on any PC, without the device.
Tape blocks¶
ww_tape is the generic block format the tap writes; nothing in it knows the tap. A block is a 32 byte header
(magic WWTP, version, kind, length, record count, the producer's block number, a time base), records (track,
time delta, length, item bytes as LEB128 varints) and a CRC-32C, as long as its records and never padded. A
describe block carries the producer (wall clock at its time 0), a TrackDef per track (number, stable key, API,
resource path, label, whether it is sticky) and the API bundles the items need, then the newest record of every
sticky track. It is written at the start, when a track is added, when a consumer joins, after a loss and every
second, so a consumer that joins late decodes from its first block.
The header and the CRC at the end stay the same in every version: a later version changes only the body (per-track chunks, an index, compression), and a reader skips a block of a version or kind it doesn't know by its length, as it skips describe records of a kind it doesn't know.
Decoding¶
wire_weaver_client::tap::Decoder takes blocks, live or from a file (ww_tape::FileBlocks), and gives out
Decoded frames, messages and link events:
- Linkage. Every frame is walked into its pieces and reassembled with the framer's rules: a message knows the
frame bytes it came in (
MessageInfo::frames,#5@2+62: frame 5, offset 2, 62 bytes), a frame its pieces and what became of them (FrameInfo::outcome), a lost frame shows as a gap in the frame numbers and as the split message it broke. - Cross-check. Each message the framer gives back is compared with the one the transport recorded: a
Check::Mismatchis a framer bug. - The API. Data messages are read as requests and events and explained with the device's API bundle (from the
capture, or the local cache); a reply is paired with its request by
seq, so its return value is explained by its method (no--foras withww explain event HEX). - Statistics.
Decoder::statsper link and direction: frames, fill, overhead, messages per frame, split messages, dropped pieces, frame gaps, mismatches, message-to-frame latency, link message kinds, and per method its calls and reply times;Stats::report()is whatww statsprints.
Live and saved data go through the same code, so a capture prints exactly as the live session did.