feat(streaming): answer "am I actually getting the sample rate I asked for?" - #518
Conversation
…d for?" Adds AcquisitionStatistics, an opt-in aggregator a caller attaches to a streaming device for the duration of an acquisition, plus the immutable snapshot records it hands back. Per channel: sample count, the rate the host really received at, the rate the device's own timestamps claim, min/max/mean inter-sample interval, and min/max/mean value; device-wide: totals and the host-to-device-clock latency. It observes the per-channel SampleReceived events the decode pipeline already raises — the seam StreamSamplesAsync uses — so no decode-path code changed and an unattached consumer pays nothing. Recording a sample allocates nothing. Ported from daqifi-desktop's SummaryLogger, fixing three defects on the way across: value extremes are seeded from the first sample rather than left at zero, means divide by the samples actually seen rather than a configured window size, and a window runs until it is Reset instead of being swapped out every N samples. closes #502 Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
|
/agentic_review |
PR Summary by QodoAdd AcquisitionStatistics to report measured sample rate, jitter, and latency
AI Description
Diagram
High-Level Assessment
Files changed (5)
|
Code Review by Qodo
1.
|
TimestampProcessor deliberately reconstructs an earlier time when the device sends a frame out of order, so a sample's timestamp can precede the one before it. Left alone that turned a jitter figure into a negative number and, if the backwards step landed near the end of a window, dropped the device-clock rate to zero — a device that looked stopped because one frame arrived late. The device-clock span is now measured between the earliest and latest timestamps seen rather than the first and last recorded (identical whenever timestamps advance), backwards steps are kept out of the interval bounds the way TimestampGapDetector already treats non-positive deltas, and the new OutOfOrderSampleCount reports that it happened rather than smoothing it away. Zero-length intervals are still counted: firmware that stamps consecutive samples with one tick value is telling the truth about its own clock. Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
|
/agentic_review |
|
Code review by qodo was updated up to the latest commit 02664af |
… seen Excluding a backwards step from the jitter bounds was only half of it: the sample after it was still measured against the rewound timestamp, so the stream resuming where it left off was reported as a gap the size of the whole rewind — a five-second "worst gap" that never happened. Intervals are now measured against the furthest-advanced timestamp seen on the channel rather than whichever sample was recorded last, which for a stream whose timestamps advance is the same thing. Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
|
/agentic_review |
|
Code review by qodo was updated up to the latest commit 747cb6f |
The interval reference moved to the channel's high-water mark, which also moved what counts as out of order: a sample behind that mark, not merely behind the sample recorded before it. The two differ for a run of samples that all sit behind it, and the XML docs still described the old rule. Comments only; no behaviour change. Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
|
/agentic_review |
|
Code review by qodo was updated up to the latest commit 0f5afd5 |
|
Qodo-clean, CI green — ready for review. (4 rounds on head |
What was wrong
If you streamed from a DAQiFi device and wanted to know whether you were actually getting the rate you asked for, Core could not tell you. You could ask for 1 kHz and get something else — because frames were lost, or because the device's clock was not keeping real time — and nothing in the library would say so. The pieces that existed each answered a narrower question: the gap detector flags individual stalls as they happen, the dropped-sample counter reports only what a slow consumer of your own threw away, and the device's
SYSTem:STReam:STATS?counters describe what the firmware believes it sent rather than what your application received. Anyone who wanted the actual answer had to re-implement it, which is what the desktop app did.How it was fixed
A new
AcquisitionStatisticsyou attach for the duration of an acquisition and then read a snapshot from. Per channel it reports the sample count, the rate the host really received at, the rate the device's own timestamps claim, the smallest/largest/mean gap between samples, and the value range and mean; device-wide it reports the totals and how far behind the device's account of the time the host was.The two rates are reported side by side deliberately, and that is the part worth pushing back on if you disagree: both dropping below the commanded rate means samples went missing, while the two disagreeing means the device clock and real time have parted company — a distinction no single number can carry. The bench run below is exactly that case.
It hooks nothing. It subscribes to the per-channel sample events the decode pipeline already raises — the same seam
StreamSamplesAsyncuses — so no decode-path code changed at all, and a consumer that never attaches one is unaffected. Recording a sample allocates nothing.The device's timestamps are not assumed to advance.
TimestampProcessordeliberately reconstructs an earlier time when a frame arrives out of order, so a backwards step is excluded from the jitter bounds, the device-clock span is measured between the earliest and latest timestamps seen, and a per-channelOutOfOrderSampleCountreports that it happened rather than smoothing it away.Three things the desktop original got wrong are fixed on the way across, so a
SummaryLoggerreplacement is not a like-for-like port: value extremes are now seeded from the first sample (desktop left them at zero, so a channel sitting at 4.5 V reported a minimum of 0 V), means divide by the samples actually seen rather than a configured window size, and a window runs until youReset()it instead of being swapped out automatically every N samples.Verification
Full suite green on net9.0 (3069 Core + 86 Mcp) and net10.0 (3069), 0 warnings; 26 new tests. Six mutations confirm the tests bite: zero-seeded extremes (6 failures), an off-by-one in the rate denominator (3), a missed unsubscribe on channel repopulation (1), a removed post-dispose guard (1), a negative interval folded into the jitter bounds (1), and a device-clock span taken from first-and-last instead of the extremes (1).
Bench — Nq1 fw 3.7.2 on
/dev/cu.usbmodem1101, serial, non-destructive (connect, enable AI0-2, stream, stop, disconnect; no reboot/format/delete/SD:GET/firmware/LAN writes). Commanded 1000 Hz for 3 s:The device's clock says 1000.03 Hz; the host received 794.4 Hz — the 79.4 % ratio this bench unit has shown on every fire, and the firmware clock defect (daqifi-nyquist-firmware #716) that motivated reporting both. The numbers are self-consistent: 3 s × (1 − 794/1000) = 618 ms of accumulated drift, which is the 617.71 ms maximum latency. The alternating 0 ms / 2 ms intervals are the duplicate-timestamp quirk (firmware #717) showing up as jitter. A 500 Hz run and a mid-stream
Reset()behaved the same way.closes #502Not merging — for review.