Skip to content

Commit 8359a78

Browse files
committed
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.
1 parent 18fb562 commit 8359a78

1 file changed

Lines changed: 59 additions & 13 deletions

File tree

sim/FreeRTOS.cpp

Lines changed: 59 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,12 @@
11
#include "FreeRTOS.h"
2-
#include <numeric>
32
#include <unordered_map>
43
#include <stdio.h>
54
#include <stdlib.h>
65

6+
// Defined by the simulator main, mirroring the counter the firmware keeps in
7+
// its vApplicationMallocFailedHook and shows in Sys Info.
8+
extern int mallocFailedCount;
9+
710
void NVIC_SystemReset(void) {
811
}
912

@@ -16,6 +19,7 @@ namespace {
1619

1720
struct HeapTracking {
1821
std::unordered_map<void*, size_t> allocatedMemory;
22+
size_t used = 0;
1923
size_t currentFreeHeap = configTOTAL_HEAP_SIZE;
2024
size_t minimumEverFreeHeap = configTOTAL_HEAP_SIZE;
2125

@@ -29,31 +33,73 @@ namespace {
2933
};
3034

3135
HeapTracking heapTracking;
36+
37+
// Bytes withheld from the budget, from INFINISIM_HEAP_BALLAST. The simulator
38+
// starts with far more of its heap free than the watch does, because it does
39+
// not carry the BLE stack and its buffers. Reserving the difference lets a
40+
// simulator run reproduce the headroom the firmware actually has. Read once.
41+
size_t HeapBallast() {
42+
static const size_t ballast = [] {
43+
const char* env = getenv("INFINISIM_HEAP_BALLAST");
44+
size_t value = env != nullptr ? strtoul(env, nullptr, 10) : 0;
45+
if (value >= configTOTAL_HEAP_SIZE) {
46+
fprintf(stderr, "[heap] INFINISIM_HEAP_BALLAST=%zu leaves nothing of %d, clamping\n", value, configTOTAL_HEAP_SIZE);
47+
value = configTOTAL_HEAP_SIZE - 1;
48+
}
49+
if (value > 0) {
50+
fprintf(stderr, "[heap] reserving %zu bytes, %zu of %d usable\n", value, configTOTAL_HEAP_SIZE - value, configTOTAL_HEAP_SIZE);
51+
}
52+
return value;
53+
}();
54+
return ballast;
55+
}
56+
57+
void UpdateFree() {
58+
heapTracking.currentFreeHeap = configTOTAL_HEAP_SIZE - heapTracking.used - HeapBallast();
59+
heapTracking.minimumEverFreeHeap = std::min(heapTracking.currentFreeHeap, heapTracking.minimumEverFreeHeap);
60+
}
3261
}
3362

3463
void* pvPortMalloc(size_t xWantedSize) {
64+
if (heapTrackingAlive && heapTracking.used + HeapBallast() + xWantedSize > configTOTAL_HEAP_SIZE) {
65+
// What heap_4 does on the watch when it cannot satisfy a request. Returning
66+
// host memory here instead would hide every allocation failure the firmware
67+
// is written to cope with. Announce the first one, since whatever the caller
68+
// does with the null pointer is easier to read with the cause in the log.
69+
UpdateFree();
70+
if (mallocFailedCount == 0) {
71+
fprintf(stderr, "[heap] allocation of %zu bytes failed, %zu free; further failures counted only\n", xWantedSize, heapTracking.currentFreeHeap);
72+
}
73+
mallocFailedCount++;
74+
return nullptr;
75+
}
76+
3577
void* ptr = malloc(xWantedSize);
36-
if (!heapTrackingAlive) {
78+
if (!heapTrackingAlive || ptr == nullptr) {
3779
return ptr;
3880
}
81+
// Drop any stale size still recorded against this address before adding the
82+
// new one. A running total only stays correct if every insertion is matched,
83+
// and the accumulate this replaces was self-correcting by construction.
84+
const auto previous = heapTracking.allocatedMemory.find(ptr);
85+
if (previous != heapTracking.allocatedMemory.end()) {
86+
heapTracking.used -= previous->second;
87+
}
3988
heapTracking.allocatedMemory[ptr] = xWantedSize;
40-
41-
const size_t currentSize = std::accumulate(heapTracking.allocatedMemory.begin(),
42-
heapTracking.allocatedMemory.end(),
43-
0,
44-
[](const size_t lhs, const std::pair<void*, size_t>& item) {
45-
return lhs + item.second;
46-
});
47-
48-
heapTracking.currentFreeHeap = configTOTAL_HEAP_SIZE - currentSize;
49-
heapTracking.minimumEverFreeHeap = std::min(heapTracking.currentFreeHeap, heapTracking.minimumEverFreeHeap);
89+
heapTracking.used += xWantedSize;
90+
UpdateFree();
5091

5192
return ptr;
5293
}
5394

5495
void vPortFree(void* pv) {
5596
if (heapTrackingAlive) {
56-
heapTracking.allocatedMemory.erase(pv);
97+
const auto entry = heapTracking.allocatedMemory.find(pv);
98+
if (entry != heapTracking.allocatedMemory.end()) {
99+
heapTracking.used -= entry->second;
100+
heapTracking.allocatedMemory.erase(entry);
101+
UpdateFree();
102+
}
57103
}
58104
free(pv);
59105
}

0 commit comments

Comments
 (0)