doc/help/Threading-and-Timing.md
A short, honest description of what Serial Studio's hot path guarantees, what it doesn't, and why the threading model looks the way it does. If you're integrating Serial Studio into something time-sensitive or trying to figure out where to expect jitter, read this once.
moodycamel::ReaderWriterQueue) that's FIFO by construction.steady_clock timestamp set at acquisition. Dashboard, CSV, MDF4, API, gRPC, MQTT, and the session database all see the same instant for the same frame. No consumer re-stamps.[FrameReader] Frame queue full -- frame dropped in the log. That message is the canary; treat it as a real signal, not a warning.new, no make_shared, no QByteArray::append after init. Each TimestampedFramePtr comes from a fixed-size slot pool inside FrameBuilder and is shared (refcounted) across every consumer; the slot is recycled when the last consumer drops the pointer. The pool falls back to a one-shot make_shared and logs a single warning only if every slot is in flight at once, which means a downstream consumer is not draining.steady_clock resolution is roughly 15 ms. Two frames produced inside the same tick get the same timestamp at acquisition. Export workers break ties using a monotonic counter (monotonicFrameNs), but on the dashboard you'll see them collapse onto one visual sample.steady_clock, not system_clock. They're great for measuring durations and ordering events; they're not synchronized to NTP and don't help you correlate with external systems by absolute time.The actual thread layout depends on which driver you've selected.
These drivers are explicit about owning a thread:
hidapi) runs a blocking hid_read loop on its own QThread.libusb) runs the libusb event loop on one thread and the bulk/isochronous read loop on another.QThread so the main thread isn't blocked on read() from a child process or named pipe.Qt::PreciseTimer to pull samples from miniaudio. The audio backend itself also delivers callbacks on internal device threads.For these drivers, HAL_Driver::dataReceived is emitted from the worker thread and Qt's AutoConnection promotes the hop to Qt::QueuedConnection automatically. The frame data crosses one thread boundary on the way to FrameReader.
These drivers don't spawn threads. They use Qt's async I/O facilities, which run on whatever thread the driver lives on (the main thread, in practice):
QSerialPort)QTcpSocket, QUdpSocket)QLowEnergyController, QLowEnergyService)QCanBusDevice)QModbusDevice)QMqttClient)For these drivers, dataReceived fires on the main thread and the connection to FrameReader is a same-thread dispatch. No copy, no event-queue insertion.
This is not laziness; it's a measured choice. The Qt classes for these protocols are already non-blocking and deliver bytes via signals. Wrapping them in a worker thread would only add a queued cross-thread emit per frame, which at high rates is the most expensive thing you can do in Qt.
This is the part that most often surprises people, so the rationale is worth stating outright.
Why FrameReader is on the main thread: moving it to a worker thread means every emit of
HAL_Driver::dataReceivedbecomes aQMetaCallEventallocation, an event-queue insertion, and a deferred dispatch on the receiving thread. At 256 kHz that's hundreds of thousands of allocations per second, and it dominates the profile. Serial Studio tried this. There were no gains. The CPU and memory cost of the cross-thread hop wiped out anything won by parallelizing the parser. The current design has been validated against UART, audio at 48 kHz+, network, CAN, and Modbus through experience, bug reports, and a fair amount of blood. If it keeps up with audio's FFT pipeline, it keeps up with almost everything else.
The connection from DeviceManager::frameReady to ConnectionManager::onFrameReady is Qt::DirectConnection for the same reason: a queued main-thread-to-main-thread hop is pure overhead.
Everything that consumes a parsed frame except the dashboard runs off the main thread, on a FrameConsumer worker:
The main thread enqueues a shared TimestampedFramePtr into each consumer's queue and moves on. The worker drains its queue on its own clock. A blocked or slow consumer can only fill its own queue; it can't back-pressure the producer.
The dashboard reads from the same TimestampedFramePtr everyone else reads from, on the main thread, on the UI tick (default 60 Hz, configurable from 1 to 240 Hz). It samples the latest frame; it doesn't process every frame. At 256 kHz input and a 60 Hz tick, the dashboard is rendering one out of roughly 4,000 frames. Everything else has already been logged or exported by the consumer threads.
This is the right tradeoff for a UI: a 250 kHz refresh would melt the GPU and the user can't see it anyway.
A frame's timestamp is set once, at the driver boundary, and never overwritten.
Driver
│ HAL_Driver::publishReceivedData(data, timestamp)
▼ (CapturedDataPtr carries timestamp + frameStep)
FrameReader (main thread)
│ splits chunk into N logical frames, propagates per-frame timestamps
▼
FrameBuilder (main thread)
│ when one captured chunk expands into N parsed frames,
│ publishes them at timestamp + step·i
▼
TimestampedFramePtr → Dashboard, CSV, MDF4, API, gRPC, Sessions, MQTT
A few specific guarantees fall out of this:
sr, the audio driver back-dates the timestamp to now - (N-1)/sr. The first sample in the buffer carries the correct acquisition time, not the time the OS got around to calling our callback.QMetaObject::invokeMethod to forward bytes to the main thread, it captures SteadyClock::now() in the originating thread first. Default-constructed timestamps would fire on the receiving thread, which is almost always wrong.monotonicFrameNs(frame->timestamp, baseline) to break ties when the OS clock is too coarse. That's a safety net, not a clock source.steady_clock ticks at ~15 ms. Same-tick frames did happen at the same time as far as the kernel is concerned. The export worker's monotonicFrameNs is what makes them strictly increasing for SQL/CSV ordering, but the visible chunks reflect real clock granularity.Serial Studio is a soft-real-time pipeline that survives 256 kHz audio without missing a beat, optimized for throughput and zero copies, with timestamps owned by the driver and a single-threaded hot path that the project has earned the right to keep through years of profiling. Treat it as a logger, not a controller, and it will not surprise you.