From 062346fb36e354e8731fdc40948f4e998bd018aa Mon Sep 17 00:00:00 2001 From: Faisal Shah Date: Thu, 30 Jul 2026 09:17:34 -0500 Subject: [PATCH] Enforce the heap budget in the simulator pvPortMalloc forwarded every request to the host malloc, so the simulated heap never ran out. configTOTAL_HEAP_SIZE was tracked for reporting but not enforced, vApplicationMallocFailedHook could not fire, and Sys Info showed zero allocation errors no matter what the firmware did. Code paths that check a pvPortMalloc result, and bugs that only appear when one returns null, were unreachable in simulation. Refuse allocations past the budget and count them, matching what heap_4 does on the watch. The first failure is reported on stderr so the null dereference that may follow has its cause in the log. Add INFINISIM_HEAP_BALLAST, which withholds a fixed number of bytes from the budget. The simulator boots with roughly 19 KB more free than the watch because it carries no BLE stack, so an enforced budget alone still leaves far more headroom than hardware has. Reserving the difference makes a run reproduce the margin the firmware actually has. Unset, it changes nothing. Replace the accumulate over the allocation map with a running total, since the budget check now runs on every allocation. --- sim/FreeRTOS.cpp | 74 +++++++++++++++++++++++++++++++++++++++--------- 1 file changed, 61 insertions(+), 13 deletions(-) diff --git a/sim/FreeRTOS.cpp b/sim/FreeRTOS.cpp index 2d696a3..cea7a44 100644 --- a/sim/FreeRTOS.cpp +++ b/sim/FreeRTOS.cpp @@ -1,9 +1,12 @@ #include "FreeRTOS.h" -#include #include #include #include +// Defined by the simulator main, mirroring the counter the firmware keeps in +// its vApplicationMallocFailedHook and shows in Sys Info. +extern int mallocFailedCount; + void NVIC_SystemReset(void) { } @@ -16,6 +19,7 @@ namespace { struct HeapTracking { std::unordered_map allocatedMemory; + size_t used = 0; size_t currentFreeHeap = configTOTAL_HEAP_SIZE; size_t minimumEverFreeHeap = configTOTAL_HEAP_SIZE; @@ -29,31 +33,75 @@ namespace { }; HeapTracking heapTracking; + + // Bytes withheld from the budget, from INFINISIM_HEAP_BALLAST. The simulator + // reports far more free heap than a watch does, for two reasons: it carries no + // BLE stack, and InfiniTime's stdlib.c, which routes malloc/new/littlefs onto + // this heap on the device, is not part of this build, so those allocations go + // to the host heap untracked. Rather than model that, withhold the difference + // you measure against Sys Info on a real watch. Read once. + size_t HeapBallast() { + static const size_t ballast = [] { + const char* env = getenv("INFINISIM_HEAP_BALLAST"); + size_t value = env != nullptr ? strtoul(env, nullptr, 10) : 0; + if (value >= configTOTAL_HEAP_SIZE) { + fprintf(stderr, "[heap] INFINISIM_HEAP_BALLAST=%zu leaves nothing of %d, clamping\n", value, configTOTAL_HEAP_SIZE); + value = configTOTAL_HEAP_SIZE - 1; + } + if (value > 0) { + fprintf(stderr, "[heap] reserving %zu bytes, %zu of %d usable\n", value, configTOTAL_HEAP_SIZE - value, configTOTAL_HEAP_SIZE); + } + return value; + }(); + return ballast; + } + + void UpdateFree() { + heapTracking.currentFreeHeap = configTOTAL_HEAP_SIZE - heapTracking.used - HeapBallast(); + heapTracking.minimumEverFreeHeap = std::min(heapTracking.currentFreeHeap, heapTracking.minimumEverFreeHeap); + } } void* pvPortMalloc(size_t xWantedSize) { + if (heapTrackingAlive && heapTracking.used + HeapBallast() + xWantedSize > configTOTAL_HEAP_SIZE) { + // What heap_4 does on the watch when it cannot satisfy a request. Returning + // host memory here instead would hide every allocation failure the firmware + // is written to cope with. Announce the first one, since whatever the caller + // does with the null pointer is easier to read with the cause in the log. + UpdateFree(); + if (mallocFailedCount == 0) { + fprintf(stderr, "[heap] allocation of %zu bytes failed, %zu free; further failures counted only\n", xWantedSize, heapTracking.currentFreeHeap); + } + mallocFailedCount++; + return nullptr; + } + void* ptr = malloc(xWantedSize); - if (!heapTrackingAlive) { + if (!heapTrackingAlive || ptr == nullptr) { return ptr; } + // Drop any stale size still recorded against this address before adding the + // new one. A running total only stays correct if every insertion is matched, + // and the accumulate this replaces was self-correcting by construction. + const auto previous = heapTracking.allocatedMemory.find(ptr); + if (previous != heapTracking.allocatedMemory.end()) { + heapTracking.used -= previous->second; + } heapTracking.allocatedMemory[ptr] = xWantedSize; - - const size_t currentSize = std::accumulate(heapTracking.allocatedMemory.begin(), - heapTracking.allocatedMemory.end(), - 0, - [](const size_t lhs, const std::pair& item) { - return lhs + item.second; - }); - - heapTracking.currentFreeHeap = configTOTAL_HEAP_SIZE - currentSize; - heapTracking.minimumEverFreeHeap = std::min(heapTracking.currentFreeHeap, heapTracking.minimumEverFreeHeap); + heapTracking.used += xWantedSize; + UpdateFree(); return ptr; } void vPortFree(void* pv) { if (heapTrackingAlive) { - heapTracking.allocatedMemory.erase(pv); + const auto entry = heapTracking.allocatedMemory.find(pv); + if (entry != heapTracking.allocatedMemory.end()) { + heapTracking.used -= entry->second; + heapTracking.allocatedMemory.erase(entry); + UpdateFree(); + } } free(pv); }