From b330c94e70cb22dbfbe1857820e21a337cc69554 Mon Sep 17 00:00:00 2001 From: David Cozens Date: Tue, 12 May 2026 12:41:04 +0100 Subject: [PATCH 1/2] feat: S21.02 tune freertos-cross MAX_MESSAGE_SIZE to 512 + tunable-driven BDD gating Points the freertos-cross preset's SOLIDSYSLOG_USER_TUNABLES_FILE at a new Bdd/Targets/FreeRtos/solidsyslog_user_tunables.h that redefines SOLIDSYSLOG_MAX_MESSAGE_SIZE to 512 (pre-S12.12 default). Empirically reclaims 4.5KB of per-Log/Service stack frame on the Cortex-M3 BDD target. Linux/Windows stay at 2048 for RFC 5424 SHOULD compliance. Bakes a permanent [stack-hwm] high-water-mark print into the FreeRTOS BDD target's InteractiveTask on quit so every bdd-freertos-qemu run leaves an empirical stack-usage trail in CI for future E21 tuning. Replaces the static `not @requires_message_size_1500` exclusion in ci/docker-compose.bdd.yml with a runtime before_feature/before_scenario hook in Bdd/features/environment.py. The hook parses any @requires__N tag, compares N to the build's tunable value from the generated solidsyslog_tunables.py, and skips when the gate isn't met. Single source of truth for tunable-driven test selection. Closes #349 --- Bdd/Targets/FreeRtos/FreeRTOSConfig.h | 1 + Bdd/Targets/FreeRtos/README.md | 16 ++++ Bdd/Targets/FreeRtos/main.c | 15 +++- .../FreeRtos/solidsyslog_user_tunables.h | 17 ++++ Bdd/features/environment.py | 43 ++++++++++ Bdd/features/udp_mtu.feature | 1 + CMakePresets.json | 3 +- DEVLOG.md | 81 +++++++++++++++++++ ci/docker-compose.bdd.yml | 4 + 9 files changed, 179 insertions(+), 2 deletions(-) create mode 100644 Bdd/Targets/FreeRtos/solidsyslog_user_tunables.h diff --git a/Bdd/Targets/FreeRtos/FreeRTOSConfig.h b/Bdd/Targets/FreeRtos/FreeRTOSConfig.h index d83003ba..24437ee6 100644 --- a/Bdd/Targets/FreeRtos/FreeRTOSConfig.h +++ b/Bdd/Targets/FreeRtos/FreeRTOSConfig.h @@ -53,6 +53,7 @@ #define INCLUDE_vTaskDelay 1 #define INCLUDE_xTaskGetSchedulerState 1 #define INCLUDE_xTimerPendFunctionCall 1 +#define INCLUDE_uxTaskGetStackHighWaterMark 1 /* Cortex-M3 NVIC: top 3 priority bits implemented. */ #define configPRIO_BITS 3 diff --git a/Bdd/Targets/FreeRtos/README.md b/Bdd/Targets/FreeRtos/README.md index f33016ad..dd47ab47 100644 --- a/Bdd/Targets/FreeRtos/README.md +++ b/Bdd/Targets/FreeRtos/README.md @@ -25,6 +25,22 @@ and the `freertos-target` devcontainer service ([`docs/containers.md`](../../docs/containers.md)) carry everything needed to build and run. +## Tuning for your MCU + +[solidsyslog_user_tunables.h](solidsyslog_user_tunables.h) is the worked +example of the E21 port-time configurability mechanism — the +`freertos-cross` preset points `SOLIDSYSLOG_USER_TUNABLES_FILE` at it so +the library compiles with `SOLIDSYSLOG_MAX_MESSAGE_SIZE 512` instead of +the default 2048. That reclaims ~4.5KB of stack frame per `SolidSyslog_Log` +call on Cortex-M3, where 4KB task stacks are normal. + +For your own port, copy the pattern: drop a `solidsyslog_user_tunables.h` +into your build tree, override whichever +[`SolidSyslogTunablesDefaults.h`](../../../Core/Interface/SolidSyslogTunablesDefaults.h) +macros suit the target, and set `-DSOLIDSYSLOG_USER_TUNABLES_FILE=` +in your CMake config (preset, cache file, or `-D` on the command line — +all equivalent). + ## In VS Code The simplest path. Switch into the FreeRTOS target devcontainer and use diff --git a/Bdd/Targets/FreeRtos/main.c b/Bdd/Targets/FreeRtos/main.c index ed307b3e..526a3cab 100644 --- a/Bdd/Targets/FreeRtos/main.c +++ b/Bdd/Targets/FreeRtos/main.c @@ -163,6 +163,10 @@ static SolidSyslogFreeRtosMutexStorage mutexStorage; * goes down and back up. */ static BaseType_t interactiveTaskCreated = pdFALSE; +/* Service task handle is captured at creation so the interactive task can + * report its peak stack usage alongside its own on `quit`. */ +static TaskHandle_t serviceTaskHandle = NULL; + extern NetworkInterface_t* pxMPS2_FillInterfaceDescriptor(BaseType_t xEMACIndex, NetworkInterface_t* pxInterface); static bool TryUpdateString(char* storage, size_t storageSize, const char* value); @@ -454,6 +458,15 @@ static void InteractiveTask(void* argument) BddTargetInteractive_Run(&g_message, stdin, BddTargetSwitchConfig_SetByName, OnSet); + /* Peak stack usage report on `quit`. Captured into every BDD run's QEMU + * console output so stack regressions surface in bdd-freertos-qemu logs + * and so E21 tunable changes (S21.02 onward) leave an empirical trail + * for the deferred stack-shrink optimisation to consume. Words, not + * bytes — FreeRTOS reports min free stack in StackType_t units (4 B on + * Cortex-M3). */ + (void) printf("[stack-hwm] interactive=%lu words service=%lu words\n", (unsigned long) uxTaskGetStackHighWaterMark(NULL), + (unsigned long) uxTaskGetStackHighWaterMark(serviceTaskHandle)); + SolidSyslog_Destroy(); SolidSyslogOriginSd_Destroy(); SolidSyslogTimeQualitySd_Destroy(); @@ -496,7 +509,7 @@ void vApplicationIPNetworkEventHook_Multi(eIPCallbackEvent_t eNetworkEvent, stru { if (xTaskCreate(InteractiveTask, "interactive", INTERACTIVE_TASK_STACK_DEPTH, NULL, tskIDLE_PRIORITY + 1, NULL) == pdPASS) { - (void) xTaskCreate(ServiceTask, "service", SERVICE_TASK_STACK_DEPTH, NULL, tskIDLE_PRIORITY + 1, NULL); + (void) xTaskCreate(ServiceTask, "service", SERVICE_TASK_STACK_DEPTH, NULL, tskIDLE_PRIORITY + 1, &serviceTaskHandle); interactiveTaskCreated = pdTRUE; } } diff --git a/Bdd/Targets/FreeRtos/solidsyslog_user_tunables.h b/Bdd/Targets/FreeRtos/solidsyslog_user_tunables.h new file mode 100644 index 00000000..e44efa3f --- /dev/null +++ b/Bdd/Targets/FreeRtos/solidsyslog_user_tunables.h @@ -0,0 +1,17 @@ +#ifndef SOLIDSYSLOG_USER_TUNABLES_H +#define SOLIDSYSLOG_USER_TUNABLES_H + +/* FreeRTOS BDD-target tuning. SolidSyslog_Log allocates a Formatter and + * working buffer of SOLIDSYSLOG_MAX_MESSAGE_SIZE on the caller's stack, + * so dropping from 2048 (the RFC 5424 section 6.1 SHOULD value) to 512 + * (the library's pre-S12.12 default) reclaims ~4.5KB per call — material + * on a Cortex-M3 with 4KB task stacks. RFC 5424 receivers are still + * required to accept up to 480 bytes, which this comfortably exceeds. + * + * BDD impact: path-MTU clipping scenarios (udp_mtu.feature) need MAX + * large enough to build a >1472-byte message and trigger EMSGSIZE — that + * feature is tagged @requires_message_size_1500 and excluded from the + * bdd-freertos-qemu tag filter in ci/docker-compose.bdd.yml. */ +#define SOLIDSYSLOG_MAX_MESSAGE_SIZE 512 + +#endif /* SOLIDSYSLOG_USER_TUNABLES_H */ diff --git a/Bdd/features/environment.py b/Bdd/features/environment.py index 0eec1aa2..5a349227 100644 --- a/Bdd/features/environment.py +++ b/Bdd/features/environment.py @@ -2,13 +2,31 @@ import logging import os import platform +import re import shutil import socket import subprocess import time +# Resolve the BDD-side mirror of build-time tunables (generated by CMake from +# Bdd/features/steps/solidsyslog_tunables.py.in). Importing here keeps the +# tag-gating hook a single source of truth: tags like @requires_message_size_N +# compare against the same value the library was compiled with. +import sys +sys.path.insert(0, os.path.join(os.path.dirname(__file__), "steps")) +from solidsyslog_tunables import SOLIDSYSLOG_MAX_MESSAGE_SIZE # noqa: E402 + logger = logging.getLogger("behave.environment") +# Maps a `@requires__` tag prefix to (build-time tunable name, +# current build value). Adding a new tunable gate (S21.03+) is one entry here. +_TUNABLE_TAG_GATES = { + "requires_message_size_": ("SOLIDSYSLOG_MAX_MESSAGE_SIZE", SOLIDSYSLOG_MAX_MESSAGE_SIZE), +} +_TUNABLE_TAG_PATTERN = re.compile( + r"^(" + "|".join(re.escape(prefix) for prefix in _TUNABLE_TAG_GATES) + r")(\d+)$" +) + SYSLOG_NG_CONF = "Bdd/syslog-ng/syslog-ng.conf" SYSLOG_NG_FULL_CONF = "Bdd/syslog-ng/syslog-ng-full.conf" SYSLOG_NG_CTL = "/var/lib/syslog-ng/syslog-ng.ctl" @@ -133,6 +151,31 @@ def before_all(context): os.environ[key] = "127.0.0.1" +def _skip_if_tunable_too_small(node): + """Skip a feature or scenario whose @requires__N tag is not met + by the current build's tunable value. Single source of truth: the build's + solidsyslog_tunables.py mirror of the C #define.""" + for tag in node.tags: + match = _TUNABLE_TAG_PATTERN.match(tag) + if match is None: + continue + prefix, threshold = match.group(1), int(match.group(2)) + tunable_name, actual = _TUNABLE_TAG_GATES[prefix] + if actual < threshold: + node.skip( + f"@{prefix}{threshold} not satisfied: build has {tunable_name}={actual}" + ) + return + + +def before_feature(context, feature): + _skip_if_tunable_too_small(feature) + + +def before_scenario(context, scenario): + _skip_if_tunable_too_small(scenario) + + def after_scenario(context, scenario): # Clean up any long-lived interactive process if hasattr(context, "interactive_process"): diff --git a/Bdd/features/udp_mtu.feature b/Bdd/features/udp_mtu.feature index 1ce2244e..4bafb3be 100644 --- a/Bdd/features/udp_mtu.feature +++ b/Bdd/features/udp_mtu.feature @@ -1,4 +1,5 @@ @udp +@requires_message_size_1500 Feature: UDP datagram path-MTU clipping When a UDP message exceeds the path MTU the sender clips it to the path-MTU's safe payload, walking back over any partial UTF-8 diff --git a/CMakePresets.json b/CMakePresets.json index 1b3c386c..b8a15bea 100644 --- a/CMakePresets.json +++ b/CMakePresets.json @@ -113,7 +113,8 @@ "toolchainFile": "${sourceDir}/Bdd/Targets/FreeRtos/cmake/arm-none-eabi.cmake", "cacheVariables": { "CMAKE_BUILD_TYPE": "Debug", - "CMAKE_EXPORT_COMPILE_COMMANDS": "ON" + "CMAKE_EXPORT_COMPILE_COMMANDS": "ON", + "SOLIDSYSLOG_USER_TUNABLES_FILE": "${sourceDir}/Bdd/Targets/FreeRtos/solidsyslog_user_tunables.h" } } ], diff --git a/DEVLOG.md b/DEVLOG.md index 8347466b..3790eddd 100644 --- a/DEVLOG.md +++ b/DEVLOG.md @@ -1,5 +1,86 @@ # Dev Log +## 2026-05-12 — S21.02 first use of the tunables override on the FreeRTOS BDD preset (#349) + +First *use* of the S21.01 mechanism. The `freertos-cross` CMake preset +now points `SOLIDSYSLOG_USER_TUNABLES_FILE` at +`Bdd/Targets/FreeRtos/solidsyslog_user_tunables.h`, which redefines +`SOLIDSYSLOG_MAX_MESSAGE_SIZE` from the library default 2048 down to 512 +— the pre-S12.12 baseline that's appropriate for a 4KB-task-stack +Cortex-M3. Linux and Windows presets keep 2048 (RFC 5424 §6.1 SHOULD +value) and so still exercise the path-MTU clipping scenarios. + +### Decisions + +- **Pick 512 over 1024 or 768.** 512 is the library's pre-S12.12 default + — a known-good baseline for small targets — and is the smallest of the + candidates that doesn't change BDD coupling (path-MTU clipping needs + >1472 bytes either way). RFC 5424 §6.1 still mandates receivers accept + ≥480 bytes; we're comfortably above. Reclaims ~4.5KB of per-call stack + frame, confirmed empirically (see below). +- **Bake the stack high-water-mark print into the FreeRTOS BDD target, + permanently.** Originally scoped as a one-shot measurement on + implementation, but we promoted it: `InteractiveTask` now prints + `[stack-hwm] interactive=N words service=M words` on `quit`, so every + BDD run leaves an empirical trail in the `bdd-freertos-qemu` log. + Future E21 stories tuning other things get regression detection for + free. Requires `INCLUDE_uxTaskGetStackHighWaterMark = 1` in + `FreeRTOSConfig.h`. The Service task handle is captured at + `xTaskCreate` so the interactive task can sample its peer. +- **Tunable-driven BDD tag gating, not a static docker-compose filter.** + Initial slice excluded `udp_mtu.feature` from FreeRTOS by adding `not + @requires_message_size_1500` to the `behave-freertos` tag filter. That + was a second source of truth: bumping the FreeRTOS MAX back to ≥1500 + wouldn't re-enable the test without a corresponding compose edit. + Replaced with a runtime `before_feature` / `before_scenario` hook in + `Bdd/features/environment.py` that parses any `@requires__N` + tag, compares N to the actual build value imported from + `solidsyslog_tunables.py`, and calls `node.skip(...)` when the gate + isn't met. The compose filter no longer mentions the tag. Generic + shape: future tunable gates (`@requires_block_size_N`, + `@requires_buffer_capacity_N`, …) plug in by adding one entry to + `_TUNABLE_TAG_GATES`. +- **Tag name `@requires_message_size_1500`, not `@mtu_overflow`.** Names + the constraint (matches `@requires_message_size_` parametric shape) + rather than the intent (which would be opaque the moment a second + feature wanted the same gate). + +### Empirical stack savings + +Captured via the new `[stack-hwm]` print, single `send 1; quit` cycle +under `qemu-system-arm -M mps2-an385`: + +| Build | Interactive HWM free | Service HWM free | +|---|---|---| +| MAX=2048 (default) | 4929 words | 1359 words | +| MAX=512 (this story) | 5697 words | 1743 words | +| **Reclaimed** | **+768 words = 3.0 KB** | **+384 words = 1.5 KB** | + +Total reclaimed: 4.5 KB across the two tasks per `Log`/`Service` cycle — +matches the analytical estimate (two `char[MAX]` frames in the library's +`SolidSyslog_Log` + `DrainBufferIntoStore` + `SendOneFromStore` paths +plus the `MAX_LINE_LENGTH`-sized line buffer and `HandleSet` name buffer +in `BddTargetInteractive`). `INTERACTIVE_TASK_STACK_DEPTH = *48` is now +much more conservative than it needs to be; the deferred stack-shrink +optimisation can use this baseline. + +### Deferred + +- **Shrink the FreeRTOS task stack depths to reclaim the new headroom.** + Worth doing once we've seen a few BDD runs at the lower MAX so the + worst-case bump (TLS path on FreeRTOS, S21.05+ store paths) is + visible. Don't pre-tune; let the `[stack-hwm]` numbers in CI guide it. +- **Tune other tunables for FreeRTOS** (`SEND_TIMEOUT_*`, buffer + capacities, SD limits) — S21.03+. + +### Open questions + +- Does the `[stack-hwm]` line's word-count format want bytes too, or is + words clear enough given Cortex-M3's 4-byte `StackType_t`? Leaving it + in words for now — matches `uxTaskGetStackHighWaterMark`'s native + unit, future ports on architectures with different `StackType_t` will + Just Work. + ## 2026-05-12 — S21.01 SOLIDSYSLOG_MAX_MESSAGE_SIZE as the first build-time tunable (#347) First slice of E21 (Port-Time Configurability). Introduces the mechanism diff --git a/ci/docker-compose.bdd.yml b/ci/docker-compose.bdd.yml index 8eb54c96..b2b23a51 100644 --- a/ci/docker-compose.bdd.yml +++ b/ci/docker-compose.bdd.yml @@ -110,6 +110,10 @@ services: # existing (@udp or @tcp) clause admits it; the @tls sibling stays # excluded until S08.07 lights up TLS on FreeRTOS. # @windows_wip excludes the Windows-only TCP singletask variant. + # Tunable-driven gates like @requires_message_size_1500 are not listed + # here — Bdd/features/environment.py skips them at runtime based on the + # build's SOLIDSYSLOG_MAX_MESSAGE_SIZE so the exclusion auto-tracks + # solidsyslog_user_tunables.h changes. command: behave --junit --junit-directory Bdd/junit --tags='not @wip and not @freertoswip and not @rtc and not @store and not @windows_wip and (@udp or @tcp)' Bdd/features/ volumes: From 550ca726a85374db7a5611d88b5d2dbed7999bc2 Mon Sep 17 00:00:00 2001 From: David Cozens Date: Tue, 12 May 2026 12:56:47 +0100 Subject: [PATCH 2/2] =?UTF-8?q?fix:=20S21.02=20review=20nits=20=E2=80=94?= =?UTF-8?q?=20guard=20service=20HWM=20read,=20accurate=20comments?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - main.c [stack-hwm] print: read serviceTaskHandle into a local guarded against NULL so a Service xTaskCreate failure can't make uxTaskGetStackHighWaterMark(NULL) silently double-report the interactive task's HWM. - solidsyslog_user_tunables.h: header comment said udp_mtu.feature was excluded by the compose tag filter — corrected to point at the runtime gate in Bdd/features/environment.py. - DEVLOG.md: spell out INTERACTIVE_TASK_STACK_DEPTH = configMINIMAL_STACK_SIZE * 48U instead of the ambiguous "*48" shorthand. (Punted out of S21.02: CodeRabbit's broader fix that reorders the two xTaskCreate calls so an interactive-task failure also tears down the service task. Pre-existing failure-handling shape, not introduced here.) --- Bdd/Targets/FreeRtos/main.c | 10 +++++++--- Bdd/Targets/FreeRtos/solidsyslog_user_tunables.h | 6 ++++-- DEVLOG.md | 7 ++++--- 3 files changed, 15 insertions(+), 8 deletions(-) diff --git a/Bdd/Targets/FreeRtos/main.c b/Bdd/Targets/FreeRtos/main.c index 526a3cab..d647ad98 100644 --- a/Bdd/Targets/FreeRtos/main.c +++ b/Bdd/Targets/FreeRtos/main.c @@ -463,9 +463,13 @@ static void InteractiveTask(void* argument) * and so E21 tunable changes (S21.02 onward) leave an empirical trail * for the deferred stack-shrink optimisation to consume. Words, not * bytes — FreeRTOS reports min free stack in StackType_t units (4 B on - * Cortex-M3). */ - (void) printf("[stack-hwm] interactive=%lu words service=%lu words\n", (unsigned long) uxTaskGetStackHighWaterMark(NULL), - (unsigned long) uxTaskGetStackHighWaterMark(serviceTaskHandle)); + * Cortex-M3). serviceTaskHandle is guarded because + * uxTaskGetStackHighWaterMark(NULL) means "the calling task" — passing a + * NULL handle after a Service xTaskCreate failure would silently + * double-report the interactive task's HWM. */ + const UBaseType_t interactiveHwm = uxTaskGetStackHighWaterMark(NULL); + const UBaseType_t serviceHwm = (serviceTaskHandle != NULL) ? uxTaskGetStackHighWaterMark(serviceTaskHandle) : 0U; + (void) printf("[stack-hwm] interactive=%lu words service=%lu words\n", (unsigned long) interactiveHwm, (unsigned long) serviceHwm); SolidSyslog_Destroy(); SolidSyslogOriginSd_Destroy(); diff --git a/Bdd/Targets/FreeRtos/solidsyslog_user_tunables.h b/Bdd/Targets/FreeRtos/solidsyslog_user_tunables.h index e44efa3f..c0a3d2c9 100644 --- a/Bdd/Targets/FreeRtos/solidsyslog_user_tunables.h +++ b/Bdd/Targets/FreeRtos/solidsyslog_user_tunables.h @@ -10,8 +10,10 @@ * * BDD impact: path-MTU clipping scenarios (udp_mtu.feature) need MAX * large enough to build a >1472-byte message and trigger EMSGSIZE — that - * feature is tagged @requires_message_size_1500 and excluded from the - * bdd-freertos-qemu tag filter in ci/docker-compose.bdd.yml. */ + * feature is tagged @requires_message_size_1500 and skipped at runtime + * by the tunable-driven gate in Bdd/features/environment.py (which + * compares the tag's threshold against this header's value via the + * generated Bdd/features/steps/solidsyslog_tunables.py mirror). */ #define SOLIDSYSLOG_MAX_MESSAGE_SIZE 512 #endif /* SOLIDSYSLOG_USER_TUNABLES_H */ diff --git a/DEVLOG.md b/DEVLOG.md index 3790eddd..f4519cc6 100644 --- a/DEVLOG.md +++ b/DEVLOG.md @@ -60,9 +60,10 @@ Total reclaimed: 4.5 KB across the two tasks per `Log`/`Service` cycle — matches the analytical estimate (two `char[MAX]` frames in the library's `SolidSyslog_Log` + `DrainBufferIntoStore` + `SendOneFromStore` paths plus the `MAX_LINE_LENGTH`-sized line buffer and `HandleSet` name buffer -in `BddTargetInteractive`). `INTERACTIVE_TASK_STACK_DEPTH = *48` is now -much more conservative than it needs to be; the deferred stack-shrink -optimisation can use this baseline. +in `BddTargetInteractive`). `INTERACTIVE_TASK_STACK_DEPTH = +configMINIMAL_STACK_SIZE * 48U` is now much more conservative than it +needs to be; the deferred stack-shrink optimisation can use this +baseline. ### Deferred