DeterministicESPAsyncWebServer v6.27.1
Zero-allocation, bounded-execution async HTTP server for ESP32
Loading...
Searching...
No Matches
preempt_queue.cpp
Go to the documentation of this file.
1// Copyright (C) 2026 Douglas Quigg (dstroy0) <dquigg123@gmail.com>
2// SPDX-License-Identifier: AGPL-3.0-or-later
3
4/**
5 * @file preempt_queue.cpp
6 * @brief Preempting work queues + high-priority processing tasks - implementation.
7 *
8 * One queue + one task per lane (detws_pq_lane::DETWS_PQ_LANE_COUNT lanes). The no-lane detws_pq_* API
9 * lives in the header and forwards to the USER lane. Internal lanes default to a higher
10 * priority than the user lane so internal ingest preempts user work.
11 */
12
14
15#if DETWS_ENABLE_PREEMPT_QUEUE
16
17#include <string.h>
18
19namespace
20{
21// Common preempt-queue state (both host + device), owned by one instance (internal linkage):
22// the per-lane handler, its context, and the high-water mark. The backend-specific state (the
23// FreeRTOS queue/task on device, the ring on host) lives in its own owner where its types are
24// in scope. One named owner, unreachable from any other translation unit.
25struct PqCtx
26{
27 DetwsPqHandler handler[(size_t)detws_pq_lane::DETWS_PQ_LANE_COUNT] = {};
28 void *ctx[(size_t)detws_pq_lane::DETWS_PQ_LANE_COUNT] = {};
29 size_t high_water[(size_t)detws_pq_lane::DETWS_PQ_LANE_COUNT] = {}; // peak items queued at once (sizing aid)
30};
31PqCtx s_pq;
32
33// GCOVR_EXCL_START ESP32-only: only used to name the FreeRTOS task in the ARDUINO detws_pq_start_lane;
34// the coverage host never starts tasks, and this is internal-linkage (anon namespace) so not callable.
35const char *lane_name(detws_pq_lane lane)
36{
37 switch (lane)
38 {
39 case detws_pq_lane::DETWS_PQ_LANE_DMA:
40 return "detws_pq_dma";
41 case detws_pq_lane::DETWS_PQ_LANE_FORWARD:
42 return "detws_pq_fwd";
43 case detws_pq_lane::DETWS_PQ_LANE_DEVICE:
44 return "detws_pq_dev";
45 default:
46 return "detws_pq_user";
47 }
48}
49// GCOVR_EXCL_STOP
50
51bool lane_ok(detws_pq_lane lane)
52{
53 return (unsigned)lane < (unsigned)detws_pq_lane::DETWS_PQ_LANE_COUNT;
54}
55} // namespace
56
57// Default task priority per lane: internal lanes rank above the user lane (DMA highest),
58// staying below the lwIP tcpip (18) / WiFi tasks so networking is never starved.
59uint8_t detws_pq_lane_priority(detws_pq_lane lane)
60{
61 switch (lane)
62 {
63 case detws_pq_lane::DETWS_PQ_LANE_DMA:
64 return (uint8_t)(DETWS_PQ_INTERNAL_PRIORITY + 2);
65 case detws_pq_lane::DETWS_PQ_LANE_FORWARD:
66 return (uint8_t)(DETWS_PQ_INTERNAL_PRIORITY + 1);
67 case detws_pq_lane::DETWS_PQ_LANE_DEVICE:
68 return (uint8_t)(DETWS_PQ_INTERNAL_PRIORITY);
69 case detws_pq_lane::DETWS_PQ_LANE_USER:
70 default:
71 return 5; // used only when a config passes priority 0; kept below the internal lanes
72 }
73}
74
75#ifdef ARDUINO
76
77#include "freertos/FreeRTOS.h"
78#include "freertos/queue.h"
79#include "freertos/task.h"
80
81namespace
82{
83// All FreeRTOS-backend state, owned by one instance (internal linkage): the static queue
84// storage/control blocks, the queue + task handles, and the per-lane run flag. One named
85// owner, unreachable from any other translation unit.
86struct PqQueueCtx
87{
88 StaticQueue_t q_struct[(size_t)detws_pq_lane::DETWS_PQ_LANE_COUNT];
89 uint8_t q_storage[(size_t)detws_pq_lane::DETWS_PQ_LANE_COUNT][DETWS_PQ_DEPTH * DETWS_PQ_ITEM_SIZE];
90 QueueHandle_t q[(size_t)detws_pq_lane::DETWS_PQ_LANE_COUNT] = {};
91 TaskHandle_t task[(size_t)detws_pq_lane::DETWS_PQ_LANE_COUNT] = {};
92 volatile bool run[(size_t)detws_pq_lane::DETWS_PQ_LANE_COUNT] = {};
93};
94PqQueueCtx s_pqq;
95
96void note_depth(detws_pq_lane lane, UBaseType_t waiting)
97{
98 if ((size_t)waiting > s_pq.high_water[(size_t)lane])
99 s_pq.high_water[(size_t)lane] = (size_t)waiting;
100}
101
102// The dedicated processing task for one lane (its id is the task parameter): block until
103// an item lands (so a post preempts straight into here), then run the handler for each
104// item in order. It blocks forever between items (zero idle wakeups).
105void pq_task(void *arg)
106{
107 detws_pq_lane lane = static_cast<detws_pq_lane>(reinterpret_cast<uintptr_t>(arg));
108 uint8_t item[DETWS_PQ_ITEM_SIZE];
109 for (;;)
110 {
111 if (xQueueReceive(s_pqq.q[(size_t)lane], item, portMAX_DELAY) == pdTRUE && s_pq.handler[(size_t)lane])
112 s_pq.handler[(size_t)lane](item, s_pq.ctx[(size_t)lane]);
113 }
114}
115} // namespace
116
117bool detws_pq_start_lane(detws_pq_lane lane, const DetwsPqConfig *cfg)
118{
119 if (!lane_ok(lane) || s_pqq.run[(size_t)lane] || !cfg || !cfg->handler)
120 return false;
121 s_pq.handler[(size_t)lane] = cfg->handler;
122 s_pq.ctx[(size_t)lane] = cfg->ctx;
123 s_pq.high_water[(size_t)lane] = 0;
124 if (!s_pqq.q[(size_t)lane])
125 s_pqq.q[(size_t)lane] = xQueueCreateStatic(DETWS_PQ_DEPTH, DETWS_PQ_ITEM_SIZE, s_pqq.q_storage[(size_t)lane],
126 &s_pqq.q_struct[(size_t)lane]);
127 if (!s_pqq.q[(size_t)lane])
128 return false;
129 s_pqq.run[(size_t)lane] = true;
130 uint8_t prio = cfg->priority ? cfg->priority : detws_pq_lane_priority(lane);
131 int core = cfg->core % portNUM_PROCESSORS;
132 if (xTaskCreatePinnedToCore(pq_task, cfg->name ? cfg->name : lane_name(lane), DETWS_PQ_STACK,
133 (void *)(uintptr_t)lane, prio, &s_pqq.task[(size_t)lane], core) != pdPASS)
134 {
135 s_pqq.run[(size_t)lane] = false;
136 return false;
137 }
138 return true;
139}
140
141bool detws_pq_post_lane(detws_pq_lane lane, const void *item, uint32_t timeout_ticks)
142{
143 if (!lane_ok(lane) || !s_pqq.q[(size_t)lane] || !item)
144 return false;
145 if (xQueueSendToBack(s_pqq.q[(size_t)lane], item, (TickType_t)timeout_ticks) != pdTRUE)
146 return false;
147 note_depth(lane, uxQueueMessagesWaiting(s_pqq.q[(size_t)lane]));
148 return true;
149}
150
151bool detws_pq_post_lane_urgent(detws_pq_lane lane, const void *item, uint32_t timeout_ticks)
152{
153 if (!lane_ok(lane) || !s_pqq.q[(size_t)lane] || !item)
154 return false;
155 if (xQueueSendToFront(s_pqq.q[(size_t)lane], item, (TickType_t)timeout_ticks) != pdTRUE)
156 return false;
157 note_depth(lane, uxQueueMessagesWaiting(s_pqq.q[(size_t)lane]));
158 return true;
159}
160
161bool detws_pq_post_lane_from_isr(detws_pq_lane lane, const void *item)
162{
163 if (!lane_ok(lane) || !s_pqq.q[(size_t)lane] || !item)
164 return false;
165 BaseType_t woke = pdFALSE;
166 if (xQueueSendToBackFromISR(s_pqq.q[(size_t)lane], item, &woke) != pdTRUE)
167 return false;
168 note_depth(lane, uxQueueMessagesWaitingFromISR(s_pqq.q[(size_t)lane]));
169 portYIELD_FROM_ISR(woke); // switch to the processing task now if it outranks us
170 return true;
171}
172
173void detws_pq_drain_lane(detws_pq_lane)
174{
175 // The lane's task drains on device; nothing to do here.
176}
177
178void detws_pq_stop_lane(detws_pq_lane lane)
179{
180 if (!lane_ok(lane))
181 return;
182 s_pqq.run[(size_t)lane] = false;
183 if (s_pqq.task[(size_t)lane]) // the task blocks on the queue (portMAX_DELAY), so delete it directly
184 {
185 vTaskDelete(s_pqq.task[(size_t)lane]);
186 s_pqq.task[(size_t)lane] = nullptr;
187 }
188}
189
190bool detws_pq_running_lane(detws_pq_lane lane)
191{
192 return lane_ok(lane) && s_pqq.run[(size_t)lane];
193}
194
195size_t detws_pq_high_water_lane(detws_pq_lane lane)
196{
197 return lane_ok(lane) ? s_pq.high_water[(size_t)lane] : 0;
198}
199
200#else // host build - fixed per-lane rings, no tasks; detws_pq_drain_lane() runs the handler
201
202namespace
203{
204// All host-backend state, owned by one instance (internal linkage): the per-lane ring buffer,
205// its head/tail/count cursors, and the started flag. One named owner, unreachable cross-TU.
206struct PqRingCtx
207{
208 uint8_t buf[(size_t)detws_pq_lane::DETWS_PQ_LANE_COUNT][DETWS_PQ_DEPTH * DETWS_PQ_ITEM_SIZE];
209 size_t head[(size_t)detws_pq_lane::DETWS_PQ_LANE_COUNT] = {}; // next write slot
210 size_t tail[(size_t)detws_pq_lane::DETWS_PQ_LANE_COUNT] = {}; // next read slot
211 size_t count[(size_t)detws_pq_lane::DETWS_PQ_LANE_COUNT] = {};
212 bool started[(size_t)detws_pq_lane::DETWS_PQ_LANE_COUNT] = {};
213};
214PqRingCtx s_pqr;
215
216void note_count(detws_pq_lane lane)
217{
218 if (s_pqr.count[(size_t)lane] > s_pq.high_water[(size_t)lane])
219 s_pq.high_water[(size_t)lane] = s_pqr.count[(size_t)lane];
220}
221} // namespace
222
223bool detws_pq_start_lane(detws_pq_lane lane, const DetwsPqConfig *cfg)
224{
225 if (!lane_ok(lane) || s_pqr.started[(size_t)lane] || !cfg || !cfg->handler)
226 return false;
227 s_pq.handler[(size_t)lane] = cfg->handler;
228 s_pq.ctx[(size_t)lane] = cfg->ctx;
229 s_pqr.head[(size_t)lane] = 0;
230 s_pqr.tail[(size_t)lane] = 0;
231 s_pqr.count[(size_t)lane] = 0;
232 s_pq.high_water[(size_t)lane] = 0;
233 s_pqr.started[(size_t)lane] = true;
234 return true;
235}
236
237bool detws_pq_post_lane(detws_pq_lane lane, const void *item, uint32_t)
238{
239 if (!lane_ok(lane) || !item || s_pqr.count[(size_t)lane] >= DETWS_PQ_DEPTH)
240 return false; // fail closed when full
241 memcpy(s_pqr.buf[(size_t)lane] + s_pqr.head[(size_t)lane] * DETWS_PQ_ITEM_SIZE, item, DETWS_PQ_ITEM_SIZE);
242 s_pqr.head[(size_t)lane] = (s_pqr.head[(size_t)lane] + 1) % DETWS_PQ_DEPTH;
243 s_pqr.count[(size_t)lane]++;
244 note_count(lane);
245 return true;
246}
247
248bool detws_pq_post_lane_urgent(detws_pq_lane lane, const void *item, uint32_t)
249{
250 if (!lane_ok(lane) || !item || s_pqr.count[(size_t)lane] >= DETWS_PQ_DEPTH)
251 return false;
252 s_pqr.tail[(size_t)lane] =
253 (s_pqr.tail[(size_t)lane] + DETWS_PQ_DEPTH - 1) % DETWS_PQ_DEPTH; // step the read cursor back, write there
254 memcpy(s_pqr.buf[(size_t)lane] + s_pqr.tail[(size_t)lane] * DETWS_PQ_ITEM_SIZE, item, DETWS_PQ_ITEM_SIZE);
255 s_pqr.count[(size_t)lane]++;
256 note_count(lane);
257 return true;
258}
259
260bool detws_pq_post_lane_from_isr(detws_pq_lane lane, const void *item)
261{
262 return detws_pq_post_lane(lane, item, 0); // no ISRs on host
263}
264
265void detws_pq_drain_lane(detws_pq_lane lane)
266{
267 if (!lane_ok(lane))
268 return;
269 while (s_pqr.count[(size_t)lane] > 0)
270 {
271 if (s_pq.handler[(size_t)lane])
272 s_pq.handler[(size_t)lane](s_pqr.buf[(size_t)lane] + s_pqr.tail[(size_t)lane] * DETWS_PQ_ITEM_SIZE,
273 s_pq.ctx[(size_t)lane]);
274 s_pqr.tail[(size_t)lane] = (s_pqr.tail[(size_t)lane] + 1) % DETWS_PQ_DEPTH;
275 s_pqr.count[(size_t)lane]--;
276 }
277}
278
279void detws_pq_stop_lane(detws_pq_lane lane)
280{
281 if (lane_ok(lane))
282 s_pqr.started[(size_t)lane] = false;
283}
284
285bool detws_pq_running_lane(detws_pq_lane lane)
286{
287 return lane_ok(lane) && s_pqr.started[(size_t)lane];
288}
289
290size_t detws_pq_high_water_lane(detws_pq_lane lane)
291{
292 return lane_ok(lane) ? s_pq.high_water[(size_t)lane] : 0;
293}
294
295#endif // ARDUINO
296
297#endif // DETWS_ENABLE_PREEMPT_QUEUE
#define DETWS_PQ_INTERNAL_PRIORITY
Base FreeRTOS priority for the internal preempting lanes (DMA / forwarding / device access)....
#define DETWS_PQ_DEPTH
Capacity of the preempting queue in items (static-allocated).
#define DETWS_PQ_ITEM_SIZE
Bytes per preempting-queue item (the posted item must fit).
#define DETWS_PQ_STACK
Stack (bytes) for each preempting-queue processing task (ESP32).
User-configurable preempting work queues + high-priority processing tasks (DETWS_ENABLE_PREEMPT_QUEUE...