Skip to content

FUG-71: solve device heap pressure (release BLE + zero-copy envelopes + RAM tooling) - #45

Open
issuefleet[bot] wants to merge 11 commits into
mainfrom
agent/fug-71-solve-device-heap-pressure
Open

FUG-71: solve device heap pressure (release BLE + zero-copy envelopes + RAM tooling)#45
issuefleet[bot] wants to merge 11 commits into
mainfrom
agent/fug-71-solve-device-heap-pressure

Conversation

@issuefleet

@issuefleet issuefleet Bot commented Aug 6, 2026

Copy link
Copy Markdown
Contributor

FUG-71: solve device heap pressure

Reclaims ~47 KiB of heap so the TLS (wss) handshake always gets its ~17 KiB contiguous block (device booted with only ~39 KiB free). Rebased onto the latest main (now incl. #46 streamed uploads, #56 derived AP-SSID/mDNS, #58/#60 FUG-85 Android-flash + rename/wss reliability + HITL suite); wins stack cleanly on top.

The reclaims

  • Release BLE once provisioned (~33 KiB) — the Bluedroid stack was up at boot and never torn down. improv_ble_end() releases it at the post-provision demote; a watchdog re-arms it if the LAN is lost (mirrors FUG-83: derive AP SSID + mDNS/DHCP hostname from the configured device name #56's derived AP SSID + hostnames + mDNS).
  • Zero-copy protobuf envelopesStoredMapChunk.dataServerMessage 1048 → 496 B; SetCountingPattern.blocksClientMessage 1560 → 472 B. envelope_size_test pins them.
  • Task-stack cuts — loopTask 24 → 18 KiB, httpd_ssl 28 → 20 KiB (~14 KiB), watched by a [stack] high-water log.

Tooling & test infra

  • .ram_chart static RAM auditor + docs/design/ram-budget.md.

Testing (on the rebase)

Notes on this rebase

Follow-up (ram-budget.md)

FX buffers (~36 KiB, reclaimable during a capture) — needs a #[global_allocator] (crate is #![no_std]).

🤖 Generated with Claude Code

Closes-Linear: FUG-71 (https://linear.app/fughilli/issue/FUG-71/solve-device-heap-pressure)

@linear-code

linear-code Bot commented Aug 6, 2026

Copy link
Copy Markdown
FUG-71 Solve device heap pressure

Claim in fughilli/splanc#44 is that the connection drop is due to heap exhaustion blocking TLS sessions. There are 512 KiB of RAM on the ESP32-C6. It is absurd that it is having heap/arena pressure for this complexity of an application.

First, build tooling for inspecting the RAM allocation of the device. This tooling should perform static analysis on the binaries (with a new .ram_chart subtarget on the firmware binary macro; add this tooling as a branch in the embedded repo and pin as you iterate).

Then, use this tooling to determine where the RAM is being spent. My hypothesis is that a lot of RAM is being spent for static allocations of micropb decode/encode buffers. This is a waste of space--we should have a zero-copy form of protocol buffer encoding/decoding which doesn't need these buffers. Build out that support as a dedicated PR and then stack the investigation on top (it's a large feature, I know. just do it).

Investigate any other places where the RAM is being spent stupidly/poorly. Be ruthless! Stack PRs as necessary to make them sensible to review. Test on HITL diligently.

Review in Linear

@github-actions

github-actions Bot commented Aug 6, 2026

Copy link
Copy Markdown
PR Preview Action v1.8.1

QR code for preview link

🚀 View preview at
https://fughilli.github.io/splanc/pr-preview/pr-45/

Built to branch gh-pages at 2026-08-09 01:43 UTC.
Preview will be ready when the GitHub Pages deployment is complete.

@issuefleet issuefleet Bot changed the title FUG-71: RAM inspection tooling (.ram_chart subtarget) FUG-71: RAM tooling + release BLE once provisioned (heap ~48K→~82K) Aug 6, 2026
@issuefleet issuefleet Bot changed the title FUG-71: RAM tooling + release BLE once provisioned (heap ~48K→~82K) FUG-71: solve device heap pressure (RAM tooling + release BLE once provisioned) Aug 6, 2026
@issuefleet issuefleet Bot changed the title FUG-71: solve device heap pressure (RAM tooling + release BLE once provisioned) FUG-71: solve device heap pressure (release BLE + zero-copy envelopes + RAM tooling) Aug 6, 2026
@issuefleet
issuefleet Bot force-pushed the agent/fug-71-solve-device-heap-pressure branch 2 times, most recently from 78e5693 to 251b489 Compare August 7, 2026 04:56
Claude Agent and others added 11 commits August 9, 2026 01:24
Build tooling to see where the C6's internal SRAM goes, so the device
heap-pressure work is driven by data rather than guesses.

- fw_memaudit.py: fix the static-RAM total (it was counting flash-mapped
  rodata — incl. the ~2 MB .flash_rodata_dummy reservation — as RAM,
  inflating the figure ~15x). Now classify sections by VMA against the
  HP-SRAM window, add a heap-headroom summary (static footprint vs the
  512 KiB SRAM, the ceiling on runtime free heap), and a --compare mode
  that diffs a --json snapshot per symbol (the lever for iterating a cut).
- ram_chart.bzl: a `ram_chart` macro that wraps a firmware_binary's `elf`
  output group in a runnable auditor, giving every image a `.ram_chart`
  sibling (a `bazel run` target, like the flash siblings).
- Wire //firmware/player_app:esp32c6.ram_chart.
- docs/design/ram-budget.md: methodology + the measured baseline
  (154.7 KiB static; rx 32K / FX_ARENA 24K / ARENA 16K / FX_TEX_PREV 8K
  dominate) + the runtime draws the static chart can't see (BLE never
  released, heap-allocated task stacks, mbedTLS sessions) + the roadmap.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The `.ram_chart` audit + on-device measurement showed the TLS heap
exhaustion behind FUG-69's mid-solve wss drop is NOT primarily the micropb
buffers — it's the Bluedroid BLE stack. Improv onboarding brings BLE up at
boot and it was never torn down, holding ~43 KiB of heap permanently, even
long after the device is on WiFi and BLE is pure overhead. Measured on a C6:
BLE init draws 43 KiB (heap 125.7K -> 82.3K).

Release it once provisioned + STA-only:

- improv_ble_end(): BLEDevice::deinit(true) tears down host + controller AND
  hands their arenas back (esp_bt_controller_mem_release); null the
  characteristic pointers so the guarded set_* helpers no-op. improv_ble_begin
  is now idempotent, doubling as the re-arm. improv_ble_set_name no-ops while
  down (the name still persists in NVS + re-applies on the next begin).
- main.cpp: call improv_ble_end() in the post-join demote (right where the
  soft-AP is already dropped), before the cert re-sign so ledmapper_selfsign
  runs with the reclaimed headroom. A re-onboarding watchdog re-raises soft-AP
  + BLE if the LAN is lost for >60 s (arduino auto-reconnect heals transients),
  so the device stays re-provisionable without a power cycle; a subsequent
  reconnect demotes + re-releases via the shared path.

Verified on the HITL rig (same-DUT flash+provision): on join the device logs
`[ble] released; heap 43560 -> 76736 (+33176)`, re-signs the cert with 106 KB
free, and steady-states at ~82 KB free heap — up from ~48 KB (BLE resident).
Full e2e (provision + wss + time-sync + rename) passes. docs/design/ram-budget.md
updated.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The heap-allocated FreeRTOS task stacks (loop 24K, httpd_ssl 28K, render
8K) are among the biggest single heap draws we control. Log each task's
stack high-water (smallest free-stack ever reached) in the periodic
[player] status, so the reserved-vs-used headroom — reclaimable heap — is
visible on the rig. Capture the render task handle from xTaskCreate and
look up the httpd task by name.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
pb::ServerMessage is a oneof sized to its largest arm, and StoredMapChunk
carries a Vec<u8, 1024> inline — so EVERY ServerMessage temporary on the
loop/httpd task stacks in lm_player_handle was ~1 KiB, even for a tiny
reply. But the player only ever PRODUCES that message (the phone decodes
it), in one place.

Encode ServerMessage{stored_map_chunk} straight into the output buffer in
handle_get_stored_map (hand-rolled, matching the existing arena/effects
walkers), decoding the GetStoredMap request the same way to avoid a
by-value ClientMessage on that path too. The firmware profile then shrinks
StoredMapChunk.data to an 8-byte stub (host keeps 1 KiB for the generated
encoder the conformance test drives), dropping ServerMessage to the Welcome
arm's ~496 B. envelope_size_test pins it so an arm can't silently re-bloat.

ffi_test + conformance pass; -c opt image builds.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
SetCountingPattern.blocks is Vec<ColorBlock, 32>, and each ColorBlock
carries an inline Vec<f64, 4> rgb — ~1.5 KiB, the fat arm that sized EVERY
by-value pb::ClientMessage on the loop/httpd task stacks (the oneof is
sized to its largest arm), even for a bare hello.

Decode that arm with the hand-rolled walker instead (like the arena /
effects arms): handle_set_counting_pattern walks the ColorBlock array,
reduces each [0,1]-double rgb to 8-bit, and installs the blocks via a new
Player::set_counting_blocks (which the generated-decode path — kept for the
host session/conformance tests — now also delegates to). The firmware
profile then stubs SetCountingPattern.blocks to 1, dropping ClientMessage
from 1560 to 472 B (a control-frame arm is now the ceiling).

Combined with the StoredMapChunk work, the by-value envelope cost on the
handler frame drops ~2.6 KiB → ~1 KiB. ffi_test gains a counting-walker
case (lm_counting_color reads back the reduced blocks); envelope_size_test
tightened to <=560 B. session/conformance/arena tests green; -c opt builds.

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

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The rebase onto #56 (FUG-83) removed kApSsid (now the derived g_ap_ssid) and
added per-name hostnames + mDNS to the soft-AP bring-up. The re-onboarding
watchdog's enter_onboarding still called WiFi.softAP(kApSsid, ...) — now mirror
the initial bring-up: setHostname + softAPsetHostname + softAP(g_ap_ssid) +
mdns_begin_or_update, so a LAN-loss re-provisioning surface is identical to a
fresh boot's.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@issuefleet
issuefleet Bot force-pushed the agent/fug-71-solve-device-heap-pressure branch from 251b489 to fabde23 Compare August 9, 2026 01:27
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.

0 participants