Skip to content

Baseline: static allocation throughout, and pools sized from measurement - #26

Merged
DavidCozens merged 7 commits into
mainfrom
baseline-static-allocation
Jul 28, 2026
Merged

Baseline: static allocation throughout, and pools sized from measurement#26
DavidCozens merged 7 commits into
mainfrom
baseline-static-allocation

Conversation

@DavidCozens

@DavidCozens DavidCozens commented Jul 28, 2026

Copy link
Copy Markdown
Contributor

What this adds

Four commits making the device's allocation static and bounded, so the
no-dynamic-allocation claim holds end to end and the RAM it costs is chosen up front
rather than observed afterwards. All of it belongs to the baseline and folds into the
Baseline commit at the next regeneration.

before after
static RAM (.bss + .data) 169,804 133,612
FreeRTOS heap reserved 96 KiB 12 KiB
heap used / peak 57,024 / 59,168 4,568 / 4,568
mbedTLS on the FreeRTOS heap 32 KiB static buffer, 22,252 peak

36,192 bytes of committed RAM recovered, and what remains cannot fail on a fragmented
heap.

The commits

  1. Static task stacks + heap_peak. All four application tasks move to
    xTaskCreateStatic. Measure gains heap_peak from
    xPortGetMinimumEverFreeHeapSize()heap_used is sampled after the handshake has
    freed its working state, so it understated what the device must provision, by 2,144 B.
  2. mbedTLS on its own buffer, via MBEDTLS_MEMORY_BUFFER_ALLOC_C, with
    MEMORY_DEBUG reporting the high-water mark so the buffer is sized from measurement.
    The FreeRTOS heap, now serving only lwIP, drops 96 KiB → 12 KiB.
  3. Task stacks sized from measured usage — log 4→2 KiB (832 B used), service 12→8 KiB
    (3,852 B used), harness 12→4 KiB.
  4. Static semaphores — the last dynamic allocation the application makes.

Two findings worth keeping

24 KiB of mbedTLS buffer fails despite a 22,252 B peak. buffer_alloc needs contiguous
space, so a peak that fits numerically can still be unsatisfiable — the headroom at 32 KiB
is not generosity. The failure is loud: the error handler reports MbedTlsStream 0x0402 on
every attempt and run.sh fails the run because nothing reached the collector.

SolidSyslog never allocated dynamically. SolidSyslogFreeRtosMutex_Create uses
xSemaphoreCreateMutexStatic against a buffer in the library's own static pool. The heap in
every figure this repository has published belonged to the device around it, not to the
library.

Note on the figures

Baseline.csv predates heap_peak and mbedtls_peak, so their used_above_baseline
columns are meaningless until the baseline is regenerated. The self-check is skipped
regardless at this tag, so nothing is masked.

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features

    • Added peak mbedTLS memory measurement (mbedtls_peak) to resource reports and baselines.
  • Improvements

    • Reduced dynamic runtime memory usage by switching multiple tasks and synchronization objects to static RTOS allocation.
    • Tightened FreeRTOS heap sizing for lwIP usage and updated mbedTLS to use a fixed in-image buffer.
    • Enhanced baseline reporting by tracking per-key presence and showing placeholders when keys are missing.
  • Bug Fixes

    • Improved detection/handling of static task and semaphore creation failures.
    • Refreshed run-report values and timestamps to match updated measurement behavior.

DavidCozens and others added 4 commits July 28, 2026 16:55
Every task the application creates — log, service, harness and the Ethernet RX
task — moves from xTaskCreate to xTaskCreateStatic, with its stack and TCB in
.bss. configKERNEL_PROVIDED_STATIC_MEMORY was already set, so the idle and timer
tasks were static already.

Measured on the same tip, before and after:

  heap_used    57,024 ->  25,872   (-31,152)
  heap_peak    59,168 ->  28,000   (-31,168)
  static_bss  169,148 -> 200,204   (+31,056)

A relocation, not a saving: 96 bytes net, which is the heap_4 per-block overhead
no longer paid. What it buys is determinism — no allocation can fail and no
fragmentation is possible in the part of the device we control.

Measure gains heap_peak, from xPortGetMinimumEverFreeHeapSize. heap_used is
sampled after the handshake has completed and freed its working state, so it
understates what the device had to provision; before this change the gap was
2,144 bytes.

What remains on the heap is lwIP's tcpip thread and its mailboxes, created inside
lwIP's own FreeRTOS port, and mbedTLS's session.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
mbedTLS sub-allocates from a static buffer via MBEDTLS_MEMORY_BUFFER_ALLOC_C
instead of pvPortMalloc, and MEMORY_DEBUG reports its high-water mark so the
buffer is sized from what was measured. The FreeRTOS heap, now serving only
lwIP's tcpip thread and mailboxes, drops from 96 KiB to 12 KiB.

Measured across this branch, same tip:

  static RAM  169,804 -> 147,660   (-22,144)
  heap_used    57,024 ->   4,888
  heap_peak    59,168 ->   4,888
  mbedtls_peak      -  ->  22,184

Not a relocation this time: 22 KiB of committed RAM comes back, because the 96 KiB
heap reservation was sized for allocations that are now static and bounded. What
remains is deterministic — nothing the device provisions for logging can fail on a
fragmented heap.

The mbedTLS buffer is 32 KiB against a 22,184-byte peak, and the headroom is not
generosity: 24 KiB fails. buffer_alloc needs contiguous space, so a peak that fits
numerically can still be unsatisfiable. The device reports the peak precisely so
this stays a measurement rather than a guess, and so a step that asks more of
mbedTLS grows the buffer and is charged for it.

The failure is loud, which is worth knowing: at 24 KiB the error handler reports
MbedTlsStream 0x0402 on every attempt and run.sh fails the run because nothing
reached the collector.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The log, service and harness stacks were sized generously and never revisited.
Static allocation makes them visible in static RAM, so they are now sized from what
the device reports using.

  log       4 KiB ->  2 KiB   (832 bytes used)
  service  12 KiB ->  8 KiB (3,852 bytes used)
  harness  12 KiB ->  4 KiB

  static_bss  147,004 -> 132,668   (-14,336)

The reported stack figures do not move: they are high-water usage, which does not
depend on the allocation. What changes is the allocation, which is the part that
costs RAM.

Headroom is roughly two-fold on both application seams, and
configCHECK_FOR_STACK_OVERFLOW is 2 with a hook, so a bad estimate fails loudly
rather than corrupting memory.

The service task is the one to watch: its depth comes from the TLS handshake, so
it is the seam that grows when the transport does.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The four binary semaphores the log and service seams use for idle and emit
signalling move to xSemaphoreCreateBinaryStatic.

  heap_used   4,888 -> 4,568   (-320)
  static_bss  132,668 -> 132,956   (+288)

This is the last dynamic allocation the application makes. What remains on the
FreeRTOS heap is lwIP's tcpip thread and its mailboxes, created inside lwIP's own
FreeRTOS port, which would mean patching an upstream tree to change.

SolidSyslog itself never allocated: SolidSyslogFreeRtosMutex_Create takes
xSemaphoreCreateMutexStatic against a buffer in the library's own static pool, so
the library's no-dynamic-allocation claim holds without qualification. The heap in
these figures has always belonged to the device around it.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@coderabbitai

coderabbitai Bot commented Jul 28, 2026

Copy link
Copy Markdown

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: f0e08843-6511-444c-bd8d-6a219f3a83f9

📥 Commits

Reviewing files that changed from the base of the PR and between 0b57e5d and 55d0391.

📒 Files selected for processing (6)
  • app/config/FreeRTOSConfig.h
  • app/config/mbedtls_user_config.h
  • app/measure/Baseline.c
  • app/measure/Baseline.h
  • app/measure/Measure.c
  • run-report.txt
🚧 Files skipped from review as they are similar to previous changes (2)
  • app/config/FreeRTOSConfig.h
  • app/config/mbedtls_user_config.h

📝 Walkthrough

Walkthrough

The change reduces RTOS heap usage, converts application-created objects to static storage, moves simulated mbedTLS allocation to a fixed buffer, exposes peak usage, and updates measurement baseline handling and reports.

Changes

Static memory and measurement updates

Layer / File(s) Summary
Memory allocation contracts
app/AppConfig.h, app/config/*, CMakeLists.txt
Task stack budgets, FreeRTOS heap sizing, mbedTLS static-buffer configuration, and the selected FreeRTOS allocator are updated.
Static FreeRTOS object creation
app/main.c, app/net/EthernetIf.c, app/tasks/*, app/storage/ffsystem.c
Harness, Ethernet RX, log, service, and storage mutex objects use static RTOS storage.
Static mbedTLS heap and peak API
app/sim/SimulatedExistingApp.*
mbedTLS uses a fixed in-image buffer, exposes its allocation high-water mark, and creates the network-ready semaphore statically.
Peak measurement reporting
app/measure/*, run-report.txt
The mbedTLS peak metric is added, missing baseline keys are tracked, and recorded resource output is refreshed.

Estimated code review effort: 3 (Moderate) | ~20 minutes

Possibly related PRs

🚥 Pre-merge checks | ✅ 3 | ❌ 2

❌ Failed checks (2 warnings)

Check name Status Explanation Resolution
Description check ⚠️ Warning The description covers the changes, but it does not follow the required template sections or include the checklist items. Rewrite it to match the template: add the required 'What this tag adds' section, summarize the component/configuration, and include the checklist items with statuses.
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (3 passed)
Check name Status Explanation
Title check ✅ Passed The title matches the PR’s main theme: moving allocation to static/bounded pools and sizing them from measurement.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch baseline-static-allocation

Comment @coderabbitai help to get the list of available commands.

…he heap

The netif bring-up semaphore and FatFs's volume mutex were the last objects the
application created dynamically. Both are now static, and the netif semaphore no
longer needs deleting.

  heap_used  4,568 -> 4,488

The application now calls vPortFree nowhere. Deleting a statically allocated
semaphore does not reach the allocator either — FreeRTOS checks how the object was
created — so the FatFs unmount path stays correct without being an exception.

What is left on the heap is entirely lwIP's, created inside its FreeRTOS port: a
recursive mutex for SYS_ARCH_PROTECT, a bring-up semaphore, the tcpip mailbox, and
the tcpip thread. The port calls xSemaphoreCreateRecursiveMutex, xSemaphoreCreateBinary,
xQueueCreate and xTaskCreate directly and offers no static variants, so removing it
means replacing an upstream source file rather than setting an option.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@app/config/FreeRTOSConfig.h`:
- Around line 25-28: Update the allocation comments at
app/config/FreeRTOSConfig.h lines 25-28 to describe only lwIP’s tcpip thread and
mailbox allocations, removing RX, interactive, and service tasks and obsolete 96
KiB or xTaskCreate references. At app/config/mbedtls_user_config.h lines 83-90,
replace the FreeRTOS-heap allocator description with the static-buffer
initialization performed in app/sim/SimulatedExistingApp.c; no code behavior
changes are required.

In `@app/measure/Measure.h`:
- Around line 21-25: Regenerate the frozen baseline for the expanded measurement
schema so heap_peak and mbedtls_peak are represented instead of defaulting
missing values to zero. Update app/measure/Measure.h lines 21-25 and
app/measure/Measure.c lines 55-59 consistently with the schema, refresh
measurements/Baseline.csv with both fields, and update run-report.txt lines
14-15 to match the regenerated output; alternatively make app/measure/Baseline.c
reject baselines missing required keys.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: a93cde4f-d7dc-41ef-8b2a-311ba87dddd2

📥 Commits

Reviewing files that changed from the base of the PR and between 07d25c7 and 2a0825a.

📒 Files selected for processing (12)
  • app/AppConfig.h
  • app/config/FreeRTOSConfig.h
  • app/config/mbedtls_user_config.h
  • app/main.c
  • app/measure/Measure.c
  • app/measure/Measure.h
  • app/net/EthernetIf.c
  • app/sim/SimulatedExistingApp.c
  • app/sim/SimulatedExistingApp.h
  • app/tasks/LogTask.c
  • app/tasks/ServiceTask.c
  • run-report.txt

Comment thread app/config/FreeRTOSConfig.h
Comment thread app/measure/Measure.h Outdated
DavidCozens and others added 2 commits July 28, 2026 17:18
heap_1 replaces heap_4. Every allocation the device makes now happens once during
bring-up and is never returned, so an allocator that cannot free is sufficient —
and fragmentation stops being possible rather than merely unlikely.

  flash_text  362,032 -> 361,616   (-416)
  heap_used     4,488 ->   4,440

heap_1's vPortFree is configASSERT(pv == NULL), so the invariant is enforced rather
than assumed: anything that frees stops the device instead of quietly leaking.

heap_peak goes with it. heap_1 only moves a pointer forward, so free memory
decreases monotonically and the current figure is the high-water mark by
construction — a separate peak would be the same number with a different name.
mbedtls_peak stays, because buffer_alloc does free internally.

What this constrains: nothing in this device may ever free, including anything
built on it. Creating and destroying a netif, or deleting a task, would assert.
That is a deliberate trade for a device whose allocation is all at init.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…ation comments

A key the frozen baseline predates was reported as current-minus-zero, which reads
as a delta and is not one — mbedtls_peak was claiming 22,236 bytes above a baseline
that has never measured it. Baseline_Load now says which keys the file carried, and
the report prints "-" for the rest.

The allocation comments in FreeRTOSConfig.h and mbedtls_user_config.h still
described xTaskCreate on a 96 KiB heap_4 region and a runtime calloc/free pair.
Both are wrong after this branch, and the code under them is what changed.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@DavidCozens
DavidCozens merged commit 013f64e into main Jul 28, 2026
2 checks passed
@DavidCozens
DavidCozens deleted the baseline-static-allocation branch July 28, 2026 18:28
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant