GCC Code Coverage Report


Directory: ./
File: Generic/LocklessPriorityQueue.cpp
Date: 2026-09-03 22:13:09
Exec Total Coverage
Lines: 0 198 0.0%
Functions: 0 14 0.0%
Branches: 0 94 0.0%

Line Branch Exec Source
1 // ======================================================================
2 // \title Os/Generic/LocklessPriorityQueue.cpp
3 // \brief lockless ISR-safe priority queue implementation for Os::Queue
4 // ======================================================================
5 #include "Os/Generic/LocklessPriorityQueue.hpp"
6 #include <atomic>
7 #include <cstring>
8 #include <limits>
9 #include "Fw/LanguageHelpers.hpp"
10 #include "Fw/Time/TimeInterval.hpp"
11 #include "Fw/Types/Assert.hpp"
12 #include "Fw/Types/ByteArray.hpp"
13 #include "Fw/Types/MemAllocator.hpp"
14 #include "Os/Task.hpp"
15 #include "config/MemoryAllocatorTypeEnumAc.hpp"
16
17 namespace Os {
18 namespace Generic {
19
20 namespace {
21
22 //! Extract the state portion of a packed state-tag word.
23 constexpr LocklessStateTagType stateOf(LocklessStateTagType packed) {
24 return packed & LocklessSlot::STATE_MASK;
25 }
26
27 //! Extract the tag portion of a packed state-tag word.
28 constexpr LocklessStateTagType tagOf(LocklessStateTagType packed) {
29 return packed >> LocklessSlot::STATE_BITS;
30 }
31
32 //! Pack a (state, tag) pair into a single word.
33 constexpr LocklessStateTagType packStateTag(LocklessStateTagType state, LocklessStateTagType tag) {
34 return (tag << LocklessSlot::STATE_BITS) | (state & LocklessSlot::STATE_MASK);
35 }
36
37 } // namespace
38
39 bool LocklessPriorityQueue::isCandidatePreferred(FwQueuePriorityType candidatePriority,
40 U32 candidateSequence,
41 FwQueuePriorityType bestPriority,
42 U32 bestSequence) {
43 bool preferred = false;
44 if (candidatePriority > bestPriority) {
45 preferred = true;
46 } else if (candidatePriority == bestPriority) {
47 // Modular subtraction: if (candidate - best) interpreted unsigned has its top bit set,
48 // candidate is "older" (smaller in the wrap-aware ordering) than best.
49 const U32 difference = candidateSequence - bestSequence;
50 const U32 topBit = static_cast<U32>(1) << (std::numeric_limits<U32>::digits - 1);
51 preferred = (difference & topBit) != 0;
52 }
53 return preferred;
54 }
55
56 LocklessSlot::LocklessSlot()
57 : m_stateTag(packStateTag(LOCKLESS_SLOT_FREE, 0)), m_sequence(0), m_size(0), m_priority(0) {}
58
59 LocklessPriorityQueueHandle::LocklessPriorityQueueHandle()
60 : QueueHandle(),
61 m_slots(nullptr),
62 m_slotsAllocation(nullptr),
63 m_data(nullptr),
64 m_depth(0),
65 m_messageSize(0),
66 m_sequence(0),
67 m_count(0),
68 m_available(0),
69 m_highMark(0),
70 m_id(0) {}
71
72 LocklessPriorityQueue::~LocklessPriorityQueue() {
73 // Intentionally empty: cleanup is the responsibility of an explicit `teardown()` call.
74 // Freeing here would call the `Fw::MemAllocatorRegistry` singleton, whose destruction
75 // order relative to global queue objects is unspecified (matches
76 // `Os::Generic::PriorityQueue::~PriorityQueue()`).
77 }
78
79 QueueInterface::Status LocklessPriorityQueue::create(FwEnumStoreType id,
80 const Fw::ConstStringBase& name,
81 FwSizeType depth,
82 FwSizeType messageSize) {
83 static_cast<void>(name);
84
85 // Ensure that the queue has not already been created. A double create would leak memory and
86 // is a programming error.
87 FW_ASSERT(this->m_handle.m_slots == nullptr);
88 FW_ASSERT(this->m_handle.m_data == nullptr);
89
90 // The state-tag word relies on the underlying atomic being lock-free for ISR safety. The
91 // static_assert in the header covers platforms with a compile-time guarantee; this runtime
92 // check is the authoritative gate (`is_always_lock_free` is C++17 and this code targets
93 // C++14).
94 std::atomic<LocklessStateTagType> probe(0);
95 FW_ASSERT(probe.is_lock_free());
96 std::atomic<FwQueuePriorityType> priorityProbe(0);
97 FW_ASSERT(priorityProbe.is_lock_free());
98 std::atomic<U32> counterProbe(0);
99 FW_ASSERT(counterProbe.is_lock_free());
100 FW_ASSERT(depth > 0);
101 FW_ASSERT(messageSize > 0);
102
103 // Guard the multiplications used to compute allocation sizes against overflow.
104 const FwSizeType maxSize = std::numeric_limits<FwSizeType>::max();
105 FW_ASSERT(depth <= (maxSize / sizeof(LocklessSlot)));
106 FW_ASSERT(depth <= (maxSize / messageSize));
107
108 // The modular comparison in isCandidatePreferred is exact only while queued equal-priority
109 // messages span less than half the U32 sequence domain (SDD section 7); depth bounds message
110 // count, not sequence spread.
111 FW_ASSERT(depth < (std::numeric_limits<U32>::max() / 2));
112
113 Fw::MemAllocator& allocator = Fw::MemAllocatorRegistry::getInstance().getAnAllocator(
114 Fw::MemoryAllocation::MemoryAllocatorType::OS_GENERIC_PRIORITY_QUEUE);
115
116 LocklessSlot* slots = nullptr;
117 void* slotsAllocation = nullptr;
118 U8* data = nullptr;
119 QueueInterface::Status status = QueueInterface::Status::OP_OK;
120
121 // Allocate the slot array. Request padding for manual alignment: allocators are not required
122 // to honor the alignment argument (e.g. Fw::MallocAllocator ignores it).
123 FW_ASSERT(depth * sizeof(LocklessSlot) <= (maxSize - alignof(LocklessSlot)));
124 FwSizeType slotBytesRequested = (depth * sizeof(LocklessSlot)) + alignof(LocklessSlot);
125 FwSizeType slotBytesAllocated = slotBytesRequested;
126 slotsAllocation = allocator.allocate(id, slotBytesAllocated, alignof(LocklessSlot));
127 if (slotsAllocation == nullptr) {
128 status = QueueInterface::Status::ALLOCATION_FAILED;
129 } else if (slotBytesAllocated < slotBytesRequested) {
130 allocator.deallocate(id, slotsAllocation);
131 status = QueueInterface::Status::ALLOCATION_FAILED;
132 } else {
133 const PlatformPointerCastType base = reinterpret_cast<PlatformPointerCastType>(slotsAllocation);
134 const PlatformPointerCastType aligned =
135 (base + (alignof(LocklessSlot) - 1)) & ~static_cast<PlatformPointerCastType>(alignof(LocklessSlot) - 1);
136 const FwSizeType offset = static_cast<FwSizeType>(aligned - base);
137 slots = Fw::arrayPlacementNew<LocklessSlot>(
138 Fw::ByteArray(static_cast<U8*>(slotsAllocation) + offset, slotBytesAllocated - offset), depth);
139 }
140
141 // Allocate the message-data region.
142 if (status == QueueInterface::Status::OP_OK) {
143 FwSizeType dataBytesRequested = depth * messageSize;
144 FwSizeType dataBytesAllocated = dataBytesRequested;
145 void* dataAllocation = allocator.allocate(id, dataBytesAllocated, alignof(U8));
146 if (dataAllocation == nullptr) {
147 Fw::arrayPlacementDestruct<LocklessSlot>(slots, depth);
148 allocator.deallocate(id, slotsAllocation);
149 status = QueueInterface::Status::ALLOCATION_FAILED;
150 } else if (dataBytesAllocated < dataBytesRequested) {
151 Fw::arrayPlacementDestruct<LocklessSlot>(slots, depth);
152 allocator.deallocate(id, slotsAllocation);
153 allocator.deallocate(id, dataAllocation);
154 status = QueueInterface::Status::ALLOCATION_FAILED;
155 } else {
156 data = static_cast<U8*>(dataAllocation);
157 }
158 }
159
160 // Publish the configured handle once both allocations succeeded.
161 if (status == QueueInterface::Status::OP_OK) {
162 this->m_handle.m_id = id;
163 this->m_handle.m_messageSize = messageSize;
164 this->m_handle.m_depth = depth;
165 this->m_handle.m_slots = slots;
166 this->m_handle.m_slotsAllocation = slotsAllocation;
167 this->m_handle.m_data = data;
168 this->m_handle.m_sequence.store(0, std::memory_order_relaxed);
169 this->m_handle.m_count.store(0, std::memory_order_relaxed);
170 this->m_handle.m_available.store(0, std::memory_order_relaxed);
171 this->m_handle.m_highMark.store(0, std::memory_order_relaxed);
172 }
173 return status;
174 }
175
176 void LocklessPriorityQueue::teardown() {
177 if (this->m_handle.m_slots != nullptr) {
178 Fw::MemAllocator& allocator = Fw::MemAllocatorRegistry::getInstance().getAnAllocator(
179 Fw::MemoryAllocation::MemoryAllocatorType::OS_GENERIC_PRIORITY_QUEUE);
180 Fw::arrayPlacementDestruct<LocklessSlot>(this->m_handle.m_slots, this->m_handle.m_depth);
181 allocator.deallocate(this->m_handle.m_id, this->m_handle.m_slotsAllocation);
182 if (this->m_handle.m_data != nullptr) {
183 allocator.deallocate(this->m_handle.m_id, this->m_handle.m_data);
184 }
185 this->m_handle.m_slots = nullptr;
186 this->m_handle.m_slotsAllocation = nullptr;
187 this->m_handle.m_data = nullptr;
188 this->m_handle.m_depth = 0;
189 this->m_handle.m_messageSize = 0;
190 this->m_handle.m_count.store(0, std::memory_order_relaxed);
191 this->m_handle.m_available.store(0, std::memory_order_relaxed);
192 this->m_handle.m_highMark.store(0, std::memory_order_relaxed);
193 this->m_handle.m_sequence.store(0, std::memory_order_relaxed);
194 }
195 }
196
197 QueueInterface::Status LocklessPriorityQueue::send(const U8* buffer,
198 FwSizeType size,
199 FwQueuePriorityType priority,
200 QueueInterface::BlockingType blockType) {
201 // Programming-error checks: queue must be created and inputs must be well-formed. These are
202 // preconditions, not untrusted-input checks.
203 FW_ASSERT(this->m_handle.m_slots != nullptr);
204 FW_ASSERT(this->m_handle.m_data != nullptr);
205 FW_ASSERT(buffer != nullptr);
206
207 // Reject oversized messages without touching the queue.
208 if (size > this->m_handle.m_messageSize) {
209 return QueueInterface::Status::SIZE_MISMATCH;
210 }
211
212 const FwSizeType depth = this->m_handle.m_depth;
213 const bool blocking = (blockType == QueueInterface::BlockingType::BLOCKING);
214
215 // Bounded for non-blocking, unbounded for blocking (explicit user contract).
216 for (FwSizeType pass = 0; blocking || (pass < MAX_RETRY_PASSES); pass++) {
217 for (FwSizeType i = 0; i < depth; i++) {
218 LocklessSlot& slot = this->m_handle.m_slots[i];
219 LocklessStateTagType packed = slot.m_stateTag.load(std::memory_order_acquire);
220 if (stateOf(packed) != LOCKLESS_SLOT_FREE) {
221 continue;
222 }
223 const LocklessStateTagType desired = packStateTag(LOCKLESS_SLOT_WRITING, tagOf(packed) + 1);
224 if (slot.m_stateTag.compare_exchange_strong(packed, desired, std::memory_order_acq_rel,
225 std::memory_order_relaxed)) {
226 if (size > 0) {
227 const FwSizeType offset = i * this->m_handle.m_messageSize;
228 static_cast<void>(::memcpy(this->m_handle.m_data + offset, buffer, static_cast<size_t>(size)));
229 }
230 slot.m_size = size;
231 slot.m_priority.store(priority, std::memory_order_relaxed);
232 slot.m_sequence.store(this->m_handle.m_sequence.fetch_add(1, std::memory_order_relaxed),
233 std::memory_order_relaxed);
234
235 // Increment the occupancy count *before* publishing READY. This guarantees a
236 // consumer's decrement (which can only follow a READY observation) never precedes
237 // this increment, so m_count cannot transiently underflow.
238 const U32 nextCount = this->m_handle.m_count.fetch_add(1, std::memory_order_acq_rel) + 1;
239
240 slot.m_stateTag.store(packStateTag(LOCKLESS_SLOT_READY, tagOf(desired) + 1), std::memory_order_release);
241
242 // Increment the receivable count only after READY is published, so a nonzero
243 // getMessagesAvailable() implies at least one message has been made receivable.
244 static_cast<void>(this->m_handle.m_available.fetch_add(1, std::memory_order_acq_rel));
245
246 // Raise the high-water mark after publication so the message is never invisible
247 // while the producer runs this loop. Each strong-CAS failure strictly raises
248 // prevMark (mark only increases, capped at depth), so at most depth iterations run.
249 U32 prevMark = this->m_handle.m_highMark.load(std::memory_order_relaxed);
250 for (FwSizeType markPass = 0; (markPass < depth) && (nextCount > prevMark); markPass++) {
251 if (this->m_handle.m_highMark.compare_exchange_strong(
252 prevMark, nextCount, std::memory_order_relaxed, std::memory_order_relaxed)) {
253 break;
254 }
255 }
256 return QueueInterface::Status::OP_OK;
257 }
258 }
259 // No free slot found this pass. Blocking callers back off; non-blocking callers retry.
260 if (blocking) {
261 static_cast<void>(Os::Task::delay(Fw::TimeInterval(0, LOCKLESS_QUEUE_BLOCKING_BACKOFF_US)));
262 }
263 }
264 return QueueInterface::Status::FULL;
265 }
266
267 QueueInterface::Status LocklessPriorityQueue::receive(U8* destination,
268 FwSizeType capacity,
269 QueueInterface::BlockingType blockType,
270 FwSizeType& actualSize,
271 FwQueuePriorityType& priority) {
272 // Programming-error checks. These mirror the assertions in send().
273 FW_ASSERT(this->m_handle.m_slots != nullptr);
274 FW_ASSERT(this->m_handle.m_data != nullptr);
275 FW_ASSERT(destination != nullptr);
276
277 const FwSizeType depth = this->m_handle.m_depth;
278 const bool blocking = (blockType == QueueInterface::BlockingType::BLOCKING);
279
280 // Bounded for non-blocking, unbounded for blocking (explicit user contract).
281 for (FwSizeType pass = 0; blocking || (pass < MAX_RETRY_PASSES); pass++) {
282 // Fast path: skip the O(depth) scan while no message is receivable.
283 if (this->m_handle.m_available.load(std::memory_order_acquire) == 0) {
284 if (blocking) {
285 static_cast<void>(Os::Task::delay(Fw::TimeInterval(0, LOCKLESS_QUEUE_BLOCKING_BACKOFF_US)));
286 }
287 continue;
288 }
289 FwSizeType bestIndex = depth;
290 FwQueuePriorityType bestPriority = FwQueuePriorityType();
291 U32 bestSequence = 0;
292 LocklessStateTagType bestPacked = 0;
293
294 for (FwSizeType i = 0; i < depth; i++) {
295 LocklessSlot& slot = this->m_handle.m_slots[i];
296 LocklessStateTagType packed = slot.m_stateTag.load(std::memory_order_acquire);
297 if (stateOf(packed) != LOCKLESS_SLOT_READY) {
298 continue;
299 }
300 const FwQueuePriorityType candidatePriority = slot.m_priority.load(std::memory_order_relaxed);
301 const U32 candidateSequence = slot.m_sequence.load(std::memory_order_relaxed);
302 // Recheck: if the state-tag changed, the relaxed priority/sequence reads above may
303 // belong to a recycled slot; discard the candidate and keep scanning.
304 const LocklessStateTagType packedRecheck = slot.m_stateTag.load(std::memory_order_acquire);
305 if (packed != packedRecheck) {
306 continue;
307 }
308 if ((bestIndex == depth) ||
309 isCandidatePreferred(candidatePriority, candidateSequence, bestPriority, bestSequence)) {
310 bestIndex = i;
311 bestPriority = candidatePriority;
312 bestSequence = candidateSequence;
313 bestPacked = packed;
314 }
315 }
316
317 if (bestIndex == depth) {
318 if (blocking) {
319 static_cast<void>(Os::Task::delay(Fw::TimeInterval(0, LOCKLESS_QUEUE_BLOCKING_BACKOFF_US)));
320 }
321 continue;
322 }
323
324 const LocklessStateTagType desired = packStateTag(LOCKLESS_SLOT_READING, tagOf(bestPacked) + 1);
325 if (this->m_handle.m_slots[bestIndex].m_stateTag.compare_exchange_strong(
326 bestPacked, desired, std::memory_order_acq_rel, std::memory_order_relaxed)) {
327 // Decrement the receivable count at the successful READY->READING claim: this
328 // message can no longer complete another receive.
329 static_cast<void>(this->m_handle.m_available.fetch_sub(1, std::memory_order_acq_rel));
330 LocklessSlot& slot = this->m_handle.m_slots[bestIndex];
331 const FwSizeType storedSize = slot.m_size;
332 FW_ASSERT(storedSize <= capacity);
333 if (storedSize > 0) {
334 const FwSizeType offset = bestIndex * this->m_handle.m_messageSize;
335 static_cast<void>(
336 ::memcpy(destination, this->m_handle.m_data + offset, static_cast<size_t>(storedSize)));
337 }
338 actualSize = storedSize;
339 priority = slot.m_priority.load(std::memory_order_relaxed);
340 // Decrement the occupancy count *before* releasing the slot to FREE. This keeps
341 // m_count (and therefore the high-water mark) at or below the queue depth: a producer
342 // can only re-claim and re-count this slot after observing FREE, which follows the
343 // decrement.
344 static_cast<void>(this->m_handle.m_count.fetch_sub(1, std::memory_order_acq_rel));
345 slot.m_stateTag.store(packStateTag(LOCKLESS_SLOT_FREE, tagOf(desired) + 1), std::memory_order_release);
346 return QueueInterface::Status::OP_OK;
347 }
348 // CAS failed — another consumer claimed this slot; yield and rescan.
349 if (blocking) {
350 static_cast<void>(Os::Task::delay(Fw::TimeInterval(0, 0)));
351 }
352 }
353 return QueueInterface::Status::EMPTY;
354 }
355
356 FwSizeType LocklessPriorityQueue::getMessagesAvailable() const {
357 return static_cast<FwSizeType>(this->m_handle.m_available.load(std::memory_order_acquire));
358 }
359
360 FwSizeType LocklessPriorityQueue::getMessageHighWaterMark() const {
361 return static_cast<FwSizeType>(this->m_handle.m_highMark.load(std::memory_order_acquire));
362 }
363
364 QueueHandle* LocklessPriorityQueue::getHandle() {
365 return &this->m_handle;
366 }
367
368 } // namespace Generic
369 } // namespace Os
370