Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions Bdd/Targets/FreeRtos/FreeRTOSConfig.h
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
16 changes: 16 additions & 0 deletions Bdd/Targets/FreeRtos/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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=<path>`
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
Expand Down
19 changes: 18 additions & 1 deletion Bdd/Targets/FreeRtos/main.c
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down Expand Up @@ -454,6 +458,19 @@ 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). 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();
SolidSyslogTimeQualitySd_Destroy();
Expand Down Expand Up @@ -496,7 +513,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;
}
}
Expand Down
19 changes: 19 additions & 0 deletions Bdd/Targets/FreeRtos/solidsyslog_user_tunables.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
#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 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 */
43 changes: 43 additions & 0 deletions Bdd/features/environment.py
Original file line number Diff line number Diff line change
Expand Up @@ -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_<tunable>_<N>` 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"
Expand Down Expand Up @@ -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_<tunable>_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"):
Expand Down
1 change: 1 addition & 0 deletions Bdd/features/udp_mtu.feature
Original file line number Diff line number Diff line change
@@ -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
Expand Down
3 changes: 2 additions & 1 deletion CMakePresets.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"
}
}
],
Expand Down
82 changes: 82 additions & 0 deletions DEVLOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,87 @@
# 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_<tunable>_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_<N>` 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 =
configMINIMAL_STACK_SIZE * 48U` 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
Expand Down
4 changes: 4 additions & 0 deletions ci/docker-compose.bdd.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
Loading