Real-time friendly snapshot path: bounded allocation-free sink queue, tryTakeSnapshot(), startRecording() - #74
Open
facontidavide wants to merge 1 commit into
Open
Real-time friendly snapshot path: bounded allocation-free sink queue, tryTakeSnapshot(), startRecording()#74facontidavide wants to merge 1 commit into
facontidavide wants to merge 1 commit into
Conversation
Three changes to the hot path, all on the producer side: - DataSinkBase: replace the unbounded ConcurrentQueue<Snapshot> with a bounded pool of pre-allocated Snapshot slots. pushSnapshot() copies into a free slot (no allocation once the slot has grown to the payload size) and drops the snapshot when the pool is exhausted instead of growing memory. Queue size is a constructor parameter (default 64), and dropped snapshots are counted in droppedSnapshotsCount(). - LogChannel::tryTakeSnapshot(): non-blocking variant of takeSnapshot() that returns false instead of waiting for the channel mutex. The sink list is now copy-on-write and read atomically, so neither variant takes sinks_mutex, which addDataSink() holds while doing sink I/O. - LogChannel::startRecording(): freezes the schema and registers it into the sinks explicitly, so that the first snapshot taken from a real-time thread does not perform sink setup (for MCAPSink, a disk write). Tests cover the schema freeze, the non-blocking path, the drop-on-full policy and, with a global operator new counter, that steady-state snapshots perform zero heap allocations on the calling thread. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Addresses the real-time items of #73 (blocking locks, per-tick heap allocations, sink setup on the first tick, unbounded queue). The
std::arraycompile error and the schema hash gap from that issue are left for separate PRs.What changes
DataSinkBase: bounded, allocation-free handoff (src/data_sink.cpp)ConcurrentQueue<Snapshot>is replaced by a pool ofqueue_sizepre-allocatedSnapshotslots and twoConcurrentQueue<Snapshot*>(free list and ready list) that never allocate ontry_enqueue/try_dequeue.pushSnapshot()takes a free slot, copies withvector::assign(no allocation once the slot's capacity covers the payload) and pushes the pointer. If no slot is free the snapshot is dropped and counted; memory never grows.DataSinkBase(size_t queue_size, size_t reserved_payload_bytes = 0); default constructor keepskDefaultQueueSize = 64. New gettersqueueSize()anddroppedSnapshotsCount().LogChannel::tryTakeSnapshot()(src/channel.cpp)takeSnapshot()buttry_locks the channel mutex and returnsfalseif another thread holds it. Dropping a sample is the failure mode a control loop wants.shared_ptr<const vector>swapped withstd::atomic_store). Neither snapshot variant takessinks_mutexanymore, so a concurrentaddDataSink()doing I/O inaddChannel()cannot stall the producer.addDataSink()also dedupes, matching the previousunordered_setsemantics._p->snapshotagainst two threads snapshotting the same channel (previously a race).LogChannel::startRecording()addChannel()on all sinks. Optional: the first snapshot still does this lazily, but a real-time caller can now do it at setup time. Idempotent; sinks added afterwards get the schema immediately, as before.README: new "Real-time usage" section.
Behaviour changes to be aware of
pushSnapshot()already documentedfalseas "queue full", so the contract is unchanged, but the outcome is now reachable.pushSnapshot()from a new thread lets the queue register that thread as a producer, which may allocate once. Pre-allocation covers up to 4 producer threads per sink.Tests
tests/realtime_tests.cpp, four cases:startRecording()registers the schema before any snapshot, freezes registration, is idempotent, and late sinks still get the schema.tryTakeSnapshot()returnsfalsewhilewriteMutex()is held by the test,trueafterwards.storeSnapshot()andqueue_size = 4, 10 pushes yield exactly 4 accepted anddroppedSnapshotsCount() == 6; after release, pushing works again.tryTakeSnapshot()on a channel with a 100-double vector, astd::array<float,16>and two scalars performs zero heap allocations on the calling thread, measured by a replaced globaloperator newgated by athread_localflag.Full suite (28 tests) passes in Debug and Release (
-Wall -Wextra, no new warnings). TheRealTime.*tests pass 50 consecutive repeats. Not run here: the ROS 2 build (ros2_publisher_sinkis untouched and only uses theDataSinkBaseinterface).🤖 Generated with Claude Code