Skip to content

Commit 420094e

Browse files
committed
feat(memory): NOTE_NO_RESPONSE_RAII opt-out for arena-only builds
Phase 1 Response RAII calls `Allocator::deallocate` on every interned string in `~Response()`. For users whose Allocator's `free` is a no-op — arena_allocator, heap_reset_allocator, custom pool — this is pure overhead: ~248 B AVR flash for dtor symbols + the `release_string_fields` helper, plus per-Response RAM for the AllocatorRef. The strings are already reclaimed wholesale by `arena.reset()` / `pool.reset()`. Define `NOTE_NO_RESPONSE_RAII=1` to drop the cleanup entirely. Under the flag, codegen omits: * `Response::alloc_` member and `note::AllocatorRef`. * `~Response()` body + the custom move ctor / move-assignment. * The SINGLETON-path `rsp_.alloc_.reset(...)` line in each generated `execute()`. * The non-SINGLETON `Notecard::execute(req, temp_alloc)` per-call overload (its contract depends on per-Response allocator tracking). Not auto-implied by NOTE_MINIMAL: a NOTE_MINIMAL user can still plug in the default heap `Allocator{}` (AVR-libc has malloc/free), where the flag would silently leak interned response strings. Tests on the `with-RAII` contract in `test_allocator_lifetime.cpp` are gated `#if !NOTE_NO_RESPONSE_RAII` — they pin behaviour the flag intentionally removes. Same gate added to the per-call overload tests in test_notecard.cpp / test_notecard_streaming.cpp. AVR `avr-notecpp` size delta: default (RAII on): 27,404 B flash, 782 B RAM NOTE_NO_RESPONSE_RAII=1: 25,198 B flash, 773 B RAM → -2,206 B flash, -9 B RAM The NO_RAII number matches the pre-Phase-1 avr_baselines.json entry exactly, confirming the full Phase 1 cost is recoverable on AVR. Host test counts: default: 1896 / 1896 pass NOTE_NO_RESPONSE_RAII=1: 1889 / 1889 pass (7 RAII tests gated) NOTE_SINGLETON=1: 1892 / 1893 (1 pre-existing failure) SINGLETON + NO_RAII: 1888 / 1889 (same pre-existing failure)
1 parent c66f7c5 commit 420094e

48 files changed

Lines changed: 297 additions & 19 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

src/note/api/card_attn.hpp

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -389,6 +389,7 @@ struct CardAttn {
389389
/// field will not be present when the attention pin is `LOW`.
390390
note::ResponseField<bool> set{};
391391

392+
#if !NOTE_NO_RESPONSE_RAII
392393
/// Allocator that minted this Response's interned string fields,
393394
/// attached by Notecard execute paths when the Response is parsed.
394395
/// Empty == no cleanup needed (default-constructed Response, or a
@@ -421,6 +422,7 @@ struct CardAttn {
421422
}
422423
Response(const Response&) = delete;
423424
Response& operator=(const Response&) = delete;
425+
#endif // !NOTE_NO_RESPONSE_RAII
424426

425427
#pragma GCC diagnostic push
426428
#pragma GCC diagnostic ignored "-Wdeprecated-declarations"
@@ -1700,6 +1702,7 @@ struct CardAttn {
17001702
/// time) that the payload was stored by the Notecard.
17011703
note::ResponseField<note::json_int_t> time{};
17021704

1705+
#if !NOTE_NO_RESPONSE_RAII
17031706
/// Allocator that minted this Response's interned string fields,
17041707
/// attached by Notecard execute paths when the Response is parsed.
17051708
/// Empty == no cleanup needed (default-constructed Response, or a
@@ -1731,6 +1734,7 @@ struct CardAttn {
17311734
}
17321735
Response(const Response&) = delete;
17331736
Response& operator=(const Response&) = delete;
1737+
#endif // !NOTE_NO_RESPONSE_RAII
17341738

17351739
#if !NOTE_NO_JSON_TREE
17361740
static Response parse(std::unique_ptr<JsonReader> reader_) {
@@ -2281,6 +2285,7 @@ struct CardAttn {
22812285
/// field will not be present when the attention pin is `LOW`.
22822286
note::ResponseField<bool> set{};
22832287

2288+
#if !NOTE_NO_RESPONSE_RAII
22842289
/// Allocator that minted this Response's interned string fields,
22852290
/// attached by Notecard execute paths when the Response is parsed.
22862291
/// Empty == no cleanup needed (default-constructed Response, or a
@@ -2306,6 +2311,7 @@ struct CardAttn {
23062311
}
23072312
Response(const Response&) = delete;
23082313
Response& operator=(const Response&) = delete;
2314+
#endif // !NOTE_NO_RESPONSE_RAII
23092315

23102316
#pragma GCC diagnostic push
23112317
#pragma GCC diagnostic ignored "-Wdeprecated-declarations"
@@ -2656,13 +2662,15 @@ inline ApiResult<typename CardAttn::Request::Response> CardAttn::Request::execut
26562662
if (!rv_) return ::note::Unexpected(rv_.error());
26572663
if (!nc_err_.empty()) return ApiResult<Response>(::note::ErrorInfo{::note::Error::Notecard, ::note::Cause::Unspecified, nc_err_.view()});
26582664
if (exhausted_) return ApiResult<Response>(::note::ErrorInfo{::note::Error::Overflow, ::note::Cause::Unspecified, NOTE_ERR("arena exhausted")});
2665+
#if !NOTE_NO_RESPONSE_RAII
26592666
// The type-erased thunk couldn't reach `attach_allocator`. Mark the
26602667
// Response as owning its interned strings so its dtor frees them on
26612668
// the SINGLETON path (parity with the Notecard::execute template path,
26622669
// which attaches via `detail::attach_allocator`). The dtor still gates
26632670
// the actual free on `g_singleton_allocator_present`, so marking when
26642671
// no allocator is configured stays safe.
26652672
rsp_.alloc_.reset(::note::detail::g_singleton_allocator);
2673+
#endif
26662674
return ApiResult<Response>(std::move(rsp_));
26672675
}
26682676
inline Result<void> CardAttn::Request::command() const {
@@ -3192,13 +3200,15 @@ inline ApiResult<typename CardAttn::Retrieve::Response> CardAttn::Retrieve::exec
31923200
if (!rv_) return ::note::Unexpected(rv_.error());
31933201
if (!nc_err_.empty()) return ApiResult<Response>(::note::ErrorInfo{::note::Error::Notecard, ::note::Cause::Unspecified, nc_err_.view()});
31943202
if (exhausted_) return ApiResult<Response>(::note::ErrorInfo{::note::Error::Overflow, ::note::Cause::Unspecified, NOTE_ERR("arena exhausted")});
3203+
#if !NOTE_NO_RESPONSE_RAII
31953204
// The type-erased thunk couldn't reach `attach_allocator`. Mark the
31963205
// Response as owning its interned strings so its dtor frees them on
31973206
// the SINGLETON path (parity with the Notecard::execute template path,
31983207
// which attaches via `detail::attach_allocator`). The dtor still gates
31993208
// the actual free on `g_singleton_allocator_present`, so marking when
32003209
// no allocator is configured stays safe.
32013210
rsp_.alloc_.reset(::note::detail::g_singleton_allocator);
3211+
#endif
32023212
return ApiResult<Response>(std::move(rsp_));
32033213
}
32043214
#endif
@@ -3416,13 +3426,15 @@ inline ApiResult<typename CardAttn::Query::Response> CardAttn::Query::execute()
34163426
if (!rv_) return ::note::Unexpected(rv_.error());
34173427
if (!nc_err_.empty()) return ApiResult<Response>(::note::ErrorInfo{::note::Error::Notecard, ::note::Cause::Unspecified, nc_err_.view()});
34183428
if (exhausted_) return ApiResult<Response>(::note::ErrorInfo{::note::Error::Overflow, ::note::Cause::Unspecified, NOTE_ERR("arena exhausted")});
3429+
#if !NOTE_NO_RESPONSE_RAII
34193430
// The type-erased thunk couldn't reach `attach_allocator`. Mark the
34203431
// Response as owning its interned strings so its dtor frees them on
34213432
// the SINGLETON path (parity with the Notecard::execute template path,
34223433
// which attaches via `detail::attach_allocator`). The dtor still gates
34233434
// the actual free on `g_singleton_allocator_present`, so marking when
34243435
// no allocator is configured stays safe.
34253436
rsp_.alloc_.reset(::note::detail::g_singleton_allocator);
3437+
#endif
34263438
return ApiResult<Response>(std::move(rsp_));
34273439
}
34283440
#endif

src/note/api/card_aux.hpp

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -492,6 +492,7 @@ struct CardAux {
492492
note::ResponseField<bool> power{};
493493
#endif
494494

495+
#if !NOTE_NO_RESPONSE_RAII
495496
/// Allocator that minted this Response's interned string fields,
496497
/// attached by Notecard execute paths when the Response is parsed.
497498
/// Empty == no cleanup needed (default-constructed Response, or a
@@ -523,6 +524,7 @@ struct CardAux {
523524
}
524525
Response(const Response&) = delete;
525526
Response& operator=(const Response&) = delete;
527+
#endif // !NOTE_NO_RESPONSE_RAII
526528

527529
#pragma GCC diagnostic push
528530
#pragma GCC diagnostic ignored "-Wdeprecated-declarations"
@@ -960,13 +962,15 @@ inline ApiResult<typename CardAux::Response> CardAux::execute() const {
960962
if (!rv_) return ::note::Unexpected(rv_.error());
961963
if (!nc_err_.empty()) return ApiResult<Response>(::note::ErrorInfo{::note::Error::Notecard, ::note::Cause::Unspecified, nc_err_.view()});
962964
if (exhausted_) return ApiResult<Response>(::note::ErrorInfo{::note::Error::Overflow, ::note::Cause::Unspecified, NOTE_ERR("arena exhausted")});
965+
#if !NOTE_NO_RESPONSE_RAII
963966
// The type-erased thunk couldn't reach `attach_allocator`. Mark the
964967
// Response as owning its interned strings so its dtor frees them on
965968
// the SINGLETON path (parity with the Notecard::execute template path,
966969
// which attaches via `detail::attach_allocator`). The dtor still gates
967970
// the actual free on `g_singleton_allocator_present`, so marking when
968971
// no allocator is configured stays safe.
969972
rsp_.alloc_.reset(::note::detail::g_singleton_allocator);
973+
#endif
970974
return ApiResult<Response>(std::move(rsp_));
971975
}
972976
inline Result<void> CardAux::command() const {

src/note/api/card_aux_serial.hpp

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -294,6 +294,7 @@ struct CardAuxSerial {
294294
note::ResponseField<note::json_int_t> rate{};
295295
#endif
296296

297+
#if !NOTE_NO_RESPONSE_RAII
297298
/// Allocator that minted this Response's interned string fields,
298299
/// attached by Notecard execute paths when the Response is parsed.
299300
/// Empty == no cleanup needed (default-constructed Response, or a
@@ -325,6 +326,7 @@ struct CardAuxSerial {
325326
}
326327
Response(const Response&) = delete;
327328
Response& operator=(const Response&) = delete;
329+
#endif // !NOTE_NO_RESPONSE_RAII
328330

329331
#pragma GCC diagnostic push
330332
#pragma GCC diagnostic ignored "-Wdeprecated-declarations"
@@ -1382,13 +1384,15 @@ inline ApiResult<typename CardAuxSerial::Request::Response> CardAuxSerial::Reque
13821384
if (!rv_) return ::note::Unexpected(rv_.error());
13831385
if (!nc_err_.empty()) return ApiResult<Response>(::note::ErrorInfo{::note::Error::Notecard, ::note::Cause::Unspecified, nc_err_.view()});
13841386
if (exhausted_) return ApiResult<Response>(::note::ErrorInfo{::note::Error::Overflow, ::note::Cause::Unspecified, NOTE_ERR("arena exhausted")});
1387+
#if !NOTE_NO_RESPONSE_RAII
13851388
// The type-erased thunk couldn't reach `attach_allocator`. Mark the
13861389
// Response as owning its interned strings so its dtor frees them on
13871390
// the SINGLETON path (parity with the Notecard::execute template path,
13881391
// which attaches via `detail::attach_allocator`). The dtor still gates
13891392
// the actual free on `g_singleton_allocator_present`, so marking when
13901393
// no allocator is configured stays safe.
13911394
rsp_.alloc_.reset(::note::detail::g_singleton_allocator);
1395+
#endif
13921396
return ApiResult<Response>(std::move(rsp_));
13931397
}
13941398
inline Result<void> CardAuxSerial::Request::command() const {

src/note/api/card_binary.hpp

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -130,6 +130,7 @@ struct CardBinary {
130130
/// Returns true if the Notecard is connected to the network.
131131
note::ResponseField<bool> connected{};
132132

133+
#if !NOTE_NO_RESPONSE_RAII
133134
/// Allocator that minted this Response's interned string fields,
134135
/// attached by Notecard execute paths when the Response is parsed.
135136
/// Empty == no cleanup needed (default-constructed Response, or a
@@ -162,6 +163,7 @@ struct CardBinary {
162163
}
163164
Response(const Response&) = delete;
164165
Response& operator=(const Response&) = delete;
166+
#endif // !NOTE_NO_RESPONSE_RAII
165167

166168
#if !NOTE_NO_JSON_TREE
167169
static Response parse(std::unique_ptr<JsonReader> reader_) {
@@ -416,6 +418,7 @@ struct CardBinary {
416418
/// Returns true if the Notecard is connected to the network.
417419
note::ResponseField<bool> connected{};
418420

421+
#if !NOTE_NO_RESPONSE_RAII
419422
/// Allocator that minted this Response's interned string fields,
420423
/// attached by Notecard execute paths when the Response is parsed.
421424
/// Empty == no cleanup needed (default-constructed Response, or a
@@ -448,6 +451,7 @@ struct CardBinary {
448451
}
449452
Response(const Response&) = delete;
450453
Response& operator=(const Response&) = delete;
454+
#endif // !NOTE_NO_RESPONSE_RAII
451455

452456
#if !NOTE_NO_JSON_TREE
453457
static Response parse(std::unique_ptr<JsonReader> reader_) {
@@ -680,13 +684,15 @@ inline ApiResult<typename CardBinary::Status::Response> CardBinary::Status::exec
680684
if (!rv_) return ::note::Unexpected(rv_.error());
681685
if (!nc_err_.empty()) return ApiResult<Response>(::note::ErrorInfo{::note::Error::Notecard, ::note::Cause::Unspecified, nc_err_.view()});
682686
if (exhausted_) return ApiResult<Response>(::note::ErrorInfo{::note::Error::Overflow, ::note::Cause::Unspecified, NOTE_ERR("arena exhausted")});
687+
#if !NOTE_NO_RESPONSE_RAII
683688
// The type-erased thunk couldn't reach `attach_allocator`. Mark the
684689
// Response as owning its interned strings so its dtor frees them on
685690
// the SINGLETON path (parity with the Notecard::execute template path,
686691
// which attaches via `detail::attach_allocator`). The dtor still gates
687692
// the actual free on `g_singleton_allocator_present`, so marking when
688693
// no allocator is configured stays safe.
689694
rsp_.alloc_.reset(::note::detail::g_singleton_allocator);
695+
#endif
690696
return ApiResult<Response>(std::move(rsp_));
691697
}
692698
inline Result<void> CardBinary::Status::command() const {
@@ -749,13 +755,15 @@ inline ApiResult<typename CardBinary::Clear::Response> CardBinary::Clear::execut
749755
if (!rv_) return ::note::Unexpected(rv_.error());
750756
if (!nc_err_.empty()) return ApiResult<Response>(::note::ErrorInfo{::note::Error::Notecard, ::note::Cause::Unspecified, nc_err_.view()});
751757
if (exhausted_) return ApiResult<Response>(::note::ErrorInfo{::note::Error::Overflow, ::note::Cause::Unspecified, NOTE_ERR("arena exhausted")});
758+
#if !NOTE_NO_RESPONSE_RAII
752759
// The type-erased thunk couldn't reach `attach_allocator`. Mark the
753760
// Response as owning its interned strings so its dtor frees them on
754761
// the SINGLETON path (parity with the Notecard::execute template path,
755762
// which attaches via `detail::attach_allocator`). The dtor still gates
756763
// the actual free on `g_singleton_allocator_present`, so marking when
757764
// no allocator is configured stays safe.
758765
rsp_.alloc_.reset(::note::detail::g_singleton_allocator);
766+
#endif
759767
return ApiResult<Response>(std::move(rsp_));
760768
}
761769
inline Result<void> CardBinary::Clear::command() const {

src/note/api/card_binary_get.hpp

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -147,6 +147,7 @@ struct CardBinaryGet : note::BinaryReceiveMixin {
147147
/// The MD5 checksum of the data returned, after it has been decoded
148148
note::ResponseField<note::string_view> status{};
149149

150+
#if !NOTE_NO_RESPONSE_RAII
150151
/// Allocator that minted this Response's interned string fields,
151152
/// attached by Notecard execute paths when the Response is parsed.
152153
/// Empty == no cleanup needed (default-constructed Response, or a
@@ -179,6 +180,7 @@ struct CardBinaryGet : note::BinaryReceiveMixin {
179180
}
180181
Response(const Response&) = delete;
181182
Response& operator=(const Response&) = delete;
183+
#endif // !NOTE_NO_RESPONSE_RAII
182184

183185
#if !NOTE_NO_JSON_TREE
184186
static Response parse(std::unique_ptr<JsonReader> reader_) {
@@ -395,13 +397,15 @@ inline ApiResult<typename CardBinaryGet::Response> CardBinaryGet::execute() cons
395397
if (!rv_) return ::note::Unexpected(rv_.error());
396398
if (!nc_err_.empty()) return ApiResult<Response>(::note::ErrorInfo{::note::Error::Notecard, ::note::Cause::Unspecified, nc_err_.view()});
397399
if (exhausted_) return ApiResult<Response>(::note::ErrorInfo{::note::Error::Overflow, ::note::Cause::Unspecified, NOTE_ERR("arena exhausted")});
400+
#if !NOTE_NO_RESPONSE_RAII
398401
// The type-erased thunk couldn't reach `attach_allocator`. Mark the
399402
// Response as owning its interned strings so its dtor frees them on
400403
// the SINGLETON path (parity with the Notecard::execute template path,
401404
// which attaches via `detail::attach_allocator`). The dtor still gates
402405
// the actual free on `g_singleton_allocator_present`, so marking when
403406
// no allocator is configured stays safe.
404407
rsp_.alloc_.reset(::note::detail::g_singleton_allocator);
408+
#endif
405409
return ApiResult<Response>(std::move(rsp_));
406410
}
407411
inline Result<void> CardBinaryGet::command() const {

src/note/api/card_binary_put.hpp

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -139,6 +139,7 @@ struct CardBinaryPut : note::BinarySendMixin {
139139
/// transmission
140140
note::ResponseField<note::string_view> err{};
141141

142+
#if !NOTE_NO_RESPONSE_RAII
142143
/// Allocator that minted this Response's interned string fields,
143144
/// attached by Notecard execute paths when the Response is parsed.
144145
/// Empty == no cleanup needed (default-constructed Response, or a
@@ -170,6 +171,7 @@ struct CardBinaryPut : note::BinarySendMixin {
170171
}
171172
Response(const Response&) = delete;
172173
Response& operator=(const Response&) = delete;
174+
#endif // !NOTE_NO_RESPONSE_RAII
173175

174176
#if !NOTE_NO_JSON_TREE
175177
static Response parse(std::unique_ptr<JsonReader> reader_) {
@@ -379,13 +381,15 @@ inline ApiResult<typename CardBinaryPut::Response> CardBinaryPut::execute() cons
379381
if (!rv_) return ::note::Unexpected(rv_.error());
380382
if (!nc_err_.empty()) return ApiResult<Response>(::note::ErrorInfo{::note::Error::Notecard, ::note::Cause::Unspecified, nc_err_.view()});
381383
if (exhausted_) return ApiResult<Response>(::note::ErrorInfo{::note::Error::Overflow, ::note::Cause::Unspecified, NOTE_ERR("arena exhausted")});
384+
#if !NOTE_NO_RESPONSE_RAII
382385
// The type-erased thunk couldn't reach `attach_allocator`. Mark the
383386
// Response as owning its interned strings so its dtor frees them on
384387
// the SINGLETON path (parity with the Notecard::execute template path,
385388
// which attaches via `detail::attach_allocator`). The dtor still gates
386389
// the actual free on `g_singleton_allocator_present`, so marking when
387390
// no allocator is configured stays safe.
388391
rsp_.alloc_.reset(::note::detail::g_singleton_allocator);
392+
#endif
389393
return ApiResult<Response>(std::move(rsp_));
390394
}
391395
inline Result<void> CardBinaryPut::command() const {

src/note/api/card_carrier.hpp

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -158,6 +158,7 @@ struct CardCarrier {
158158
/// Will display `true` when in `AUX_CHARGING` `"charging"` mode.
159159
note::ResponseField<bool> charging{};
160160

161+
#if !NOTE_NO_RESPONSE_RAII
161162
/// Allocator that minted this Response's interned string fields,
162163
/// attached by Notecard execute paths when the Response is parsed.
163164
/// Empty == no cleanup needed (default-constructed Response, or a
@@ -189,6 +190,7 @@ struct CardCarrier {
189190
}
190191
Response(const Response&) = delete;
191192
Response& operator=(const Response&) = delete;
193+
#endif // !NOTE_NO_RESPONSE_RAII
192194

193195
#if !NOTE_NO_JSON_TREE
194196
static Response parse(std::unique_ptr<JsonReader> reader_) {
@@ -383,13 +385,15 @@ inline ApiResult<typename CardCarrier::Response> CardCarrier::execute() const {
383385
if (!rv_) return ::note::Unexpected(rv_.error());
384386
if (!nc_err_.empty()) return ApiResult<Response>(::note::ErrorInfo{::note::Error::Notecard, ::note::Cause::Unspecified, nc_err_.view()});
385387
if (exhausted_) return ApiResult<Response>(::note::ErrorInfo{::note::Error::Overflow, ::note::Cause::Unspecified, NOTE_ERR("arena exhausted")});
388+
#if !NOTE_NO_RESPONSE_RAII
386389
// The type-erased thunk couldn't reach `attach_allocator`. Mark the
387390
// Response as owning its interned strings so its dtor frees them on
388391
// the SINGLETON path (parity with the Notecard::execute template path,
389392
// which attaches via `detail::attach_allocator`). The dtor still gates
390393
// the actual free on `g_singleton_allocator_present`, so marking when
391394
// no allocator is configured stays safe.
392395
rsp_.alloc_.reset(::note::detail::g_singleton_allocator);
396+
#endif
393397
return ApiResult<Response>(std::move(rsp_));
394398
}
395399
inline Result<void> CardCarrier::command() const {

0 commit comments

Comments
 (0)