DeterministicESPAsyncWebServer v6.27.1
Zero-allocation, bounded-execution async HTTP server for ESP32
Loading...
Searching...
No Matches
scratch.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 scratch.cpp
6 * @brief Per-worker per-dispatch scratch arenas - implementation.
7 *
8 * One bump allocator per worker (DETWS_WORKER_COUNT arenas in BSS), selected by
9 * the caller's worker id. Each arena has exactly one accessor (its worker), so
10 * the lock-free / fixed-size guarantees in scratch.h hold per worker. With
11 * DETWS_WORKER_COUNT == 1 this is byte-for-byte the original single arena.
12 */
13
14#include "scratch.h"
16#include <assert.h>
17#include <stdint.h>
18
19#if defined(ARDUINO) && !defined(NDEBUG)
20#include "freertos/FreeRTOS.h"
21#include "freertos/task.h"
22#endif
23
24namespace
25{
26// Per-worker scratch metadata (bump offsets + high-water marks), owned by one
27// instance (internal linkage). scratch_reset() runs every dispatch and touches
28// only this, so this symbol is always live.
29struct ScratchCtx
30{
31 size_t off[DETWS_WORKER_COUNT]; // bump offset per worker
32 size_t high_water[DETWS_WORKER_COUNT]; // peak off per worker
33};
34ScratchCtx s_scratch;
35
36// The bump arenas, in their OWN owned instance so they are a distinct linker symbol.
37// Only scratch_alloc() references them, so a firmware that never allocates scratch
38// (e.g. a plain TLS/HTTP server - no SSH / WebSocket / OIDC) has scratch_alloc()
39// garbage-collected and, with it, these arenas - reclaiming DETWS_WORKER_COUNT *
40// DETWS_SCRATCH_ARENA_SIZE bytes of DRAM. They must NOT be merged into ScratchCtx:
41// scratch_reset()'s always-live reference to off[] would then anchor the whole
42// section (--gc-sections is per-symbol), keeping the multi-KB arenas linked in
43// builds that never touch them.
44struct ScratchArenaCtx
45{
46 // scratch_alloc aligns the bump OFFSET, so the arena base must itself be aligned to
47 // the strictest alignment a caller can request. As a struct member it would only
48 // inherit 8-byte alignment; force 32 (a standalone array got this from the linker).
49 alignas(32) uint8_t arena[DETWS_WORKER_COUNT][DETWS_SCRATCH_ARENA_SIZE]; // the arenas (BSS)
50};
51ScratchArenaCtx s_scratch_arena;
52
53// Default alignment when the caller passes 0: the strictest the platform needs.
54constexpr size_t SCRATCH_DEFAULT_ALIGN = sizeof(void *) > 8 ? sizeof(void *) : 8;
55
56// Resolve the calling worker, clamped into range so a stray id can never index
57// out of bounds (an unbound caller is worker 0, the only worker by default).
58inline int cur_worker()
59{
60 int w = detws_worker_self();
61 return (w >= 0 && w < DETWS_WORKER_COUNT) ? w : 0;
62}
63
64// Debug tripwire: each arena must only ever be touched from one task (its
65// worker). Record the first caller per arena and assert every later call
66// matches. Compiled out on native (no FreeRTOS) and in NDEBUG builds; the
67// structural single-accessor-per-arena invariant is what makes this lock-free,
68// the assert just makes a future violation loud instead of a silent cross-core
69// race.
70inline void assert_single_owner(int w)
71{
72#if defined(ARDUINO) && !defined(NDEBUG)
73 static TaskHandle_t s_owner[DETWS_WORKER_COUNT] = {nullptr};
74 TaskHandle_t cur = xTaskGetCurrentTaskHandle();
75 if (s_owner[w] == nullptr)
76 s_owner[w] = cur;
77 else
78 assert(s_owner[w] == cur && "scratch arena borrowed from a foreign task");
79#else
80 (void)w;
81#endif
82}
83} // namespace
84
85void *scratch_alloc(size_t n, size_t align)
86{
87 int w = cur_worker();
88 assert_single_owner(w);
89
90 if (align == 0)
91 align = SCRATCH_DEFAULT_ALIGN;
92 assert((align & (align - 1)) == 0 && "scratch alignment must be a power of two");
93
94 // Round the current offset up to the requested alignment.
95 size_t base = (s_scratch.off[w] + (align - 1)) & ~(align - 1);
96
97 // Overflow-safe capacity check: reject if n alone exceeds the arena, or if
98 // the aligned allocation would run past the end. (n <= capacity here, so
99 // capacity - n does not underflow; base may exceed it after rounding.)
101 return nullptr;
102
103 void *p = &s_scratch_arena.arena[w][base];
104 s_scratch.off[w] = base + n;
105 if (s_scratch.off[w] > s_scratch.high_water[w])
106 s_scratch.high_water[w] = s_scratch.off[w];
107 return p;
108}
109
111{
112 int w = cur_worker();
113 assert_single_owner(w);
114 s_scratch.off[w] = 0;
115}
116
117size_t scratch_mark(void)
118{
119 int w = cur_worker();
120 assert_single_owner(w);
121 return s_scratch.off[w];
122}
123
124void scratch_release(size_t mark)
125{
126 int w = cur_worker();
127 assert_single_owner(w);
128 assert(mark <= s_scratch.off[w] && "scratch_release mark is above the current offset");
129 s_scratch.off[w] = mark;
130}
131
132size_t scratch_used(void)
133{
134 return s_scratch.off[cur_worker()];
135}
136
138{
139 // Peak any single arena reached - the value to size DETWS_SCRATCH_ARENA_SIZE by.
140 size_t peak = 0;
141 for (int w = 0; w < DETWS_WORKER_COUNT; w++)
142 if (s_scratch.high_water[w] > peak)
143 peak = s_scratch.high_water[w];
144 return peak;
145}
146
148{
150}
#define DETWS_WORKER_COUNT
Number of server worker tasks (slots partitioned i % N). Default 1.
#define DETWS_SCRATCH_ARENA_SIZE
Size in bytes of the shared per-dispatch scratch arena.
size_t scratch_mark(void)
Capture the current arena offset (a savepoint for scratch_release()).
Definition scratch.cpp:117
size_t scratch_capacity(void)
Total arena capacity in bytes (DETWS_SCRATCH_ARENA_SIZE).
Definition scratch.cpp:147
void * scratch_alloc(size_t n, size_t align)
Borrow n bytes of scratch, aligned to align.
Definition scratch.cpp:85
size_t scratch_used(void)
Bytes currently handed out (0 immediately after a reset).
Definition scratch.cpp:132
size_t scratch_high_water(void)
Largest scratch_used() value seen since boot (for sizing the arena).
Definition scratch.cpp:137
void scratch_reset(void)
Reclaim the whole arena (empties it).
Definition scratch.cpp:110
void scratch_release(size_t mark)
Reclaim everything allocated since mark (LIFO).
Definition scratch.cpp:124
Shared per-dispatch scratch arena (Layer 5, session-scoped memory).
int detws_worker_self(void)
Worker id [0, count) of the calling task; 0 by default / single-worker.
Definition worker.cpp:28
Layer 5 (Session) - server worker identity.